Skip to content

Commit e3b36b8

Browse files
committed
MDEV-31957 Concurrent ALTER and ANALYZE collecting statistics can result in stale statistical data
Example of what causes the problem: T1: ANALYZE TABLE starts to collect statistics T2: ALTER TABLE starts by deleting statistics for all changed fields, then creates a temp table and copies data to it. T1: ANALYZE ends and writes to the statistics tables. T2: ALTER TABLE renames temp table in place of the old table. Now the statistics from analyze matches the old deleted tables. Fixed by waiting to delete old statistics until ALTER TABLE is the only one using the old table and ensure that rename of columns can handle swapping of column names. rename_columns_in_stat_table() (former rename_column_in_stat_tables()) now takes a list of columns to rename. It uses the following algorithm to update column_stats to be able to handle circular renames - While there are columns to be renamed and it is the first loop or last rename loop did change something. - Loop over all columns to be renamed - Change column name in column_stat - If fail because of duplicate key - If this is first change attempt for this column - Change column name to a temporary column name - If there was a conflicting row, replace it with the current row. else - Remove entry from column list - Loop over all remaining columns in the list - Remove the conflicting row - Change column from temporary name to final name in column_stat Other things: - Don't flush tables for every operation. Only flush when all updates are done. - Rename of columns was not handled in case of ALGORITHM=copy (old bug). - Fixed that we do not collect statistics for hidden hash columns used by UNIQUE constraint on long values. - Fixed that we do not collect statistics for blob columns referred by generated virtual columns. This was achieved by storing the fields for which we want to have statistics in table->has_value_set instead of in table->read_set. - Rename of indexes was not handled for persistent statistics. - This is now handled similar as rename of columns. Renamed columns are now stored in 'rename_stat_indexes' and handled in Alter_info::delete_statistics() together with drooped indexes. - ALTER TABLE .. ADD INDEX may instead of creating a new index rename an existing generated foreign key index. This was not reflected in the index_stats table because this was handled in mysql_prepare_create_table instead instead of in the mysql_alter() code. Fixed by adding a call in mysql_prepare_create_table() to drop the changed index. I also had to change the code that 'marked the index' to be ignored with code that would not destroy the original index name. Reviewer: Sergei Petrunia <sergey@mariadb.com>
1 parent 388296a commit e3b36b8

File tree

12 files changed

+1311
-204
lines changed

12 files changed

+1311
-204
lines changed

mysql-test/main/analyze.result

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,290 @@ set use_stat_tables=default;
9797
set histogram_type=default;
9898
set histogram_size=default;
9999
#
100+
# MDEV-31957 Concurrent ALTER and ANALYZE collecting statistics can
101+
# result in stale statistical data
102+
#
103+
CREATE TABLE t1 (a INT, b VARCHAR(128));
104+
INSERT INTO t1 SELECT seq, CONCAT('s',seq) FROM seq_1_to_100;
105+
connect con1,localhost,root,,;
106+
ALTER TABLE t1 MODIFY b BLOB;
107+
connection default;
108+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
109+
connection con1;
110+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
111+
connection default;
112+
disconnect con1;
113+
select db_name,table_name,column_name from mysql.column_stats;
114+
db_name table_name column_name
115+
test t1 a
116+
drop table t1;
117+
#
118+
# Testing swapping columns
119+
#
120+
create or replace table t1 (a int primary key, b varchar(100), c varchar(100), d varchar(100)) engine=innodb;
121+
insert into t1 select seq, repeat('b',seq),repeat('c',mod(seq,5)), repeat('d',mod(seq,10)) from seq_1_to_100;
122+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
123+
Table Op Msg_type Msg_text
124+
test.t1 analyze status Engine-independent statistics collected
125+
test.t1 analyze status OK
126+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
127+
db_name table_name column_name avg_length
128+
test t1 a 4.0000
129+
test t1 b 50.5000
130+
test t1 c 2.0000
131+
test t1 d 4.5000
132+
alter table t1 change b c varchar(200), change c b varchar(200);
133+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
134+
db_name table_name column_name avg_length
135+
test t1 a 4.0000
136+
test t1 b 2.0000
137+
test t1 c 50.5000
138+
test t1 d 4.5000
139+
alter table t1 change b c varchar(200), change c d varchar(200), change d b varchar(200) ;
140+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
141+
db_name table_name column_name avg_length
142+
test t1 a 4.0000
143+
test t1 b 4.5000
144+
test t1 c 2.0000
145+
test t1 d 50.5000
146+
alter table t1 change b c varchar(200), change c d varchar(200), change d e varchar(200) ;
147+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
148+
db_name table_name column_name avg_length
149+
test t1 a 4.0000
150+
test t1 c 4.5000
151+
test t1 d 2.0000
152+
test t1 e 50.5000
153+
alter table t1 change e d varchar(200), drop column d;
154+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
155+
db_name table_name column_name avg_length
156+
test t1 a 4.0000
157+
test t1 c 4.5000
158+
test t1 d 50.5000
159+
# Test having non existing column in column_stats
160+
insert into mysql.column_stats (db_name,table_name,column_name) values ("test","t1","b");
161+
alter table t1 change c d varchar(200), change d b varchar(200);
162+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
163+
db_name table_name column_name avg_length
164+
test t1 a 4.0000
165+
test t1 b 50.5000
166+
test t1 d 4.5000
167+
# Test having a conflicting temporary name
168+
insert into mysql.column_stats (db_name,table_name,column_name) values ("test","t1",concat("#sql_tmp_name#1",char(0)));
169+
alter table t1 change d b varchar(200), change b d varchar(200);
170+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
171+
db_name table_name column_name avg_length
172+
test t1 a 4.0000
173+
test t1 b 4.5000
174+
test t1 d 50.5000
175+
drop table t1;
176+
truncate table mysql.column_stats;
177+
create or replace table t1 (a int primary key, b varchar(100), c varchar(100), d varchar(100)) engine=myisam;
178+
insert into t1 select seq, repeat('b',seq),repeat('c',mod(seq,5)), repeat('d',mod(seq,10)) from seq_1_to_100;
179+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
180+
Table Op Msg_type Msg_text
181+
test.t1 analyze status Engine-independent statistics collected
182+
test.t1 analyze status OK
183+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
184+
db_name table_name column_name avg_length
185+
test t1 a 4.0000
186+
test t1 b 50.5000
187+
test t1 c 2.0000
188+
test t1 d 4.5000
189+
alter table t1 change b c varchar(200), change c b varchar(200);
190+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
191+
db_name table_name column_name avg_length
192+
test t1 a 4.0000
193+
test t1 d 4.5000
194+
analyze table t1 persistent for columns(b,c) indexes all;
195+
Table Op Msg_type Msg_text
196+
test.t1 analyze status Engine-independent statistics collected
197+
test.t1 analyze status Table is already up to date
198+
alter table t1 change b c varchar(200), change c d varchar(200), change d b varchar(200) ;
199+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
200+
db_name table_name column_name avg_length
201+
test t1 a 4.0000
202+
test t1 b 50.5000
203+
test t1 c 2.0000
204+
analyze table t1 persistent for columns(d) indexes all;
205+
Table Op Msg_type Msg_text
206+
test.t1 analyze status Engine-independent statistics collected
207+
test.t1 analyze status Table is already up to date
208+
alter table t1 change b c varchar(200), change c d varchar(200), change d e varchar(200) ;
209+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
210+
db_name table_name column_name avg_length
211+
test t1 a 4.0000
212+
test t1 c 50.5000
213+
test t1 d 2.0000
214+
test t1 e 50.5000
215+
alter table t1 change e d varchar(200), drop column d;
216+
select db_name,table_name,column_name,avg_length from mysql.column_stats order by column_name;
217+
db_name table_name column_name avg_length
218+
test t1 a 4.0000
219+
test t1 c 50.5000
220+
test t1 d 50.5000
221+
drop table t1;
222+
truncate table mysql.column_stats;
223+
create table t1 (a int, b blob, unique(b)) engine= innodb;
224+
analyze table t1 persistent for all;
225+
Table Op Msg_type Msg_text
226+
test.t1 analyze status Engine-independent statistics collected
227+
test.t1 analyze Warning Engine-independent statistics are not collected for column 'b'
228+
test.t1 analyze status OK
229+
select column_name from mysql.column_stats where table_name = 't1';
230+
column_name
231+
a
232+
drop table t1;
233+
create table t1 (a int, b blob, c int generated always as (length(b)) virtual) engine= innodb;
234+
analyze table t1 persistent for all;
235+
Table Op Msg_type Msg_text
236+
test.t1 analyze status Engine-independent statistics collected
237+
test.t1 analyze Warning Engine-independent statistics are not collected for column 'b'
238+
test.t1 analyze status OK
239+
select column_name from mysql.column_stats where table_name = 't1';
240+
column_name
241+
a
242+
c
243+
drop table t1;
244+
CREATE or replace TABLE t1 (a INT, b CHAR(8));
245+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
246+
Table Op Msg_type Msg_text
247+
test.t1 analyze status Engine-independent statistics collected
248+
test.t1 analyze status Table is already up to date
249+
ALTER TABLE t1 CHANGE b c INT, ORDER BY b;
250+
SELECT db_name, table_name, column_name FROM mysql.column_stats where table_name = 't1';
251+
db_name table_name column_name
252+
test t1 a
253+
test t1 c
254+
drop table t1;
255+
CREATE or replace TABLE t1 (a INT, b CHAR(8));
256+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
257+
Table Op Msg_type Msg_text
258+
test.t1 analyze status Engine-independent statistics collected
259+
test.t1 analyze status Table is already up to date
260+
ALTER TABLE t1 RENAME COLUMN b to c, ALGORITHM=COPY;
261+
SELECT db_name, table_name, column_name FROM mysql.column_stats where table_name = 't1';
262+
db_name table_name column_name
263+
test t1 a
264+
test t1 c
265+
drop table t1;
266+
#
267+
# Testing swapping indexes
268+
#
269+
create or replace table t1 (a int primary key, b varchar(100), c varchar(100), d varchar(100), index (b), index(c), index(d,b)) engine=innodb;
270+
insert into t1 select seq, repeat('b',seq),repeat('c',mod(seq,5)), repeat('d',mod(seq,10)) from seq_1_to_100;
271+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
272+
Table Op Msg_type Msg_text
273+
test.t1 analyze status Engine-independent statistics collected
274+
test.t1 analyze status OK
275+
select * from mysql.index_stats order by index_name, prefix_arity;
276+
db_name table_name index_name prefix_arity avg_frequency
277+
test t1 PRIMARY 1 1.0000
278+
test t1 b 1 1.0000
279+
test t1 b 2 1.0000
280+
test t1 c 1 20.0000
281+
test t1 c 2 1.0000
282+
test t1 d 1 10.0000
283+
test t1 d 2 1.0000
284+
test t1 d 3 1.0000
285+
alter table t1 rename index b to c, rename index c to d, rename index d to b;
286+
select * from mysql.index_stats order by index_name;
287+
db_name table_name index_name prefix_arity avg_frequency
288+
test t1 PRIMARY 1 1.0000
289+
test t1 b 1 10.0000
290+
test t1 b 2 1.0000
291+
test t1 b 3 1.0000
292+
test t1 c 1 1.0000
293+
test t1 c 2 1.0000
294+
test t1 d 1 20.0000
295+
test t1 d 2 1.0000
296+
alter table t1 rename index b to c, rename index c to d, rename index d to e;
297+
select * from mysql.index_stats order by index_name, prefix_arity;
298+
db_name table_name index_name prefix_arity avg_frequency
299+
test t1 PRIMARY 1 1.0000
300+
test t1 c 1 10.0000
301+
test t1 c 2 1.0000
302+
test t1 c 3 1.0000
303+
test t1 d 1 1.0000
304+
test t1 d 2 1.0000
305+
test t1 e 1 20.0000
306+
test t1 e 2 1.0000
307+
alter table t1 rename index e to b;
308+
alter table t1 change b c varchar(200), change c d varchar(200), change d e varchar(200) ;
309+
show create table t1;
310+
Table Create Table
311+
t1 CREATE TABLE `t1` (
312+
`a` int(11) NOT NULL,
313+
`c` varchar(200) DEFAULT NULL,
314+
`d` varchar(200) DEFAULT NULL,
315+
`e` varchar(200) DEFAULT NULL,
316+
PRIMARY KEY (`a`),
317+
KEY `d` (`c`),
318+
KEY `b` (`d`),
319+
KEY `c` (`e`,`c`)
320+
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
321+
select * from mysql.index_stats order by index_name, prefix_arity;
322+
db_name table_name index_name prefix_arity avg_frequency
323+
test t1 PRIMARY 1 1.0000
324+
test t1 b 1 20.0000
325+
test t1 b 2 1.0000
326+
test t1 c 1 10.0000
327+
test t1 c 2 1.0000
328+
test t1 c 3 1.0000
329+
test t1 d 1 1.0000
330+
test t1 d 2 1.0000
331+
# Test having a conflicting temporary name
332+
insert into mysql.index_stats (db_name,table_name,index_name,prefix_arity) values ("test","t1",concat("#sql_tmp_name#1",char(0)),1);
333+
alter table t1 rename index c to d, rename index d to c;
334+
select * from mysql.index_stats order by index_name, prefix_arity;
335+
db_name table_name index_name prefix_arity avg_frequency
336+
test t1 PRIMARY 1 1.0000
337+
test t1 b 1 20.0000
338+
test t1 b 2 1.0000
339+
test t1 c 1 1.0000
340+
test t1 c 2 1.0000
341+
test t1 d 1 10.0000
342+
test t1 d 2 1.0000
343+
test t1 d 3 1.0000
344+
drop table t1;
345+
select * from mysql.index_stats order by index_name, prefix_arity;
346+
db_name table_name index_name prefix_arity avg_frequency
347+
#
348+
# Test of adding key that replaces foreign key
349+
#
350+
CREATE TABLE t1 (aaaa INT, b INT, KEY(b), FOREIGN KEY(aaaa) REFERENCES t1(b)) ENGINE=InnoDB;
351+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
352+
Table Op Msg_type Msg_text
353+
test.t1 analyze status Engine-independent statistics collected
354+
test.t1 analyze status OK
355+
SELECT index_name FROM mysql.index_stats WHERE table_name = 't1' order by index_name;
356+
index_name
357+
aaaa
358+
b
359+
ALTER TABLE t1 ADD KEY idx(aaaa);
360+
SHOW CREATE TABLE t1;
361+
Table Create Table
362+
t1 CREATE TABLE `t1` (
363+
`aaaa` int(11) DEFAULT NULL,
364+
`b` int(11) DEFAULT NULL,
365+
KEY `b` (`b`),
366+
KEY `idx` (`aaaa`),
367+
CONSTRAINT `t1_ibfk_1` FOREIGN KEY (`aaaa`) REFERENCES `t1` (`b`)
368+
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
369+
SELECT index_name FROM mysql.index_stats WHERE table_name = 't1' order by index_name;
370+
index_name
371+
b
372+
truncate table mysql.index_stats;
373+
ANALYZE TABLE t1 PERSISTENT FOR ALL;
374+
Table Op Msg_type Msg_text
375+
test.t1 analyze status Engine-independent statistics collected
376+
test.t1 analyze status OK
377+
SELECT index_name FROM mysql.index_stats WHERE table_name = 't1' order by index_name;
378+
index_name
379+
b
380+
idx
381+
ALTER TABLE t1 DROP KEY idx;
382+
ERROR HY000: Cannot drop index 'idx': needed in a foreign key constraint
383+
DROP TABLE t1;
384+
#
100385
# End of 10.6 tests
101386
#

0 commit comments

Comments
 (0)