對于5.7.23之前的版本在評估類似DDL操作的時候需要謹慎,可能評估為瞬間操作,但是實際上線的時候跑了很久,這個就容易導致超過維護窗口,甚至更大的故障。
一、問題模擬
使用5.7.22版本:
建表語句,注意這里字段a包含了一個索引,這是觸發(fā)這個BUG的必要條件:
MySQL> show create table testmy G
*************************** 1. row ***************************
Table: testmy
Create Table: CREATE TABLE `testmy` (
`id` int(11) DEFAULT NULL,
`a` varchar(24) DEFAULT NULL COMMENT 'test1',
KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
數(shù)據(jù)量:
mysql> select count(*) from testmy;
+----------+
| count(*) |
+----------+
| 262144 |
+----------+
1 row in set (5.17 sec)
執(zhí)行DDL語句:
alter table testmy modify `a` varchar(30) comment 'test1111';
本DDL語句主要完成:
- 擴展varchar從24*4到30*4
- 更改字段的comment
按照常理來講這個DDL是只修改元數(shù)據(jù)的,因此應該瞬間完成,但是實際在5.7.22版本中這個語句重建了索引a,耗時如下:
mysql> alter table testmy modify `a` varchar(30) comment 'test1';
Query OK, 0 rows affected (2.50 sec)
Records: 0 Duplicates: 0 Warnings: 0
很明顯重建了索引,才會有這么高的耗時。而在5.7的新版本或者8.0中測試這個語句是瞬間完成的。
二、官方文檔說明
也就是說正常的擴展varchar的長度,只要字符集字節(jié)數(shù)量*字符數(shù)量不跨越256,那么就是修改元數(shù)據(jù),不會重建索引。
三、問題分析
既然不符合官方文檔的說明,那么這個問題肯定是某種BUG導致。當我們進行DDL操作的時候,需要對比更改部分和現(xiàn)有的數(shù)據(jù)字典中標定義的差別,然后根據(jù)這些差別來定義操作方式,然后根據(jù)操作方式來判斷哪一種DDL 方式比較合適,關于定義操作方式的部分來自于函數(shù)fill_alter_inplace_info,而在函數(shù)中會根據(jù)新表的索引和老表的索引字段的長度判斷是否需要drop索引和新建索引,代碼中體現(xiàn)為如下:
/*
Step through all keys of the old table and search matching new keys.
*/
for (table_key= table->key_info; table_key < table_key_end; table_key++) //循環(huán)老表的索引
{
/* Skip renamed keys. */
if (table_key->flags & HA_KEY_RENAMED)
continue;
new_key= find_key_cs(table_key->name, ha_alter_info->key_info_buffer,
new_key_end);//在新的定義中是否包含這個索引
if (new_key == NULL) //如果找不到說明這個索引要drop掉
{
/* Matching new key not found. This means the key should be dropped. */
ha_alter_info->add_dropped_key(table_key); //加入到drop key buffer
}
else if (has_index_def_changed(ha_alter_info, table_key, new_key))//是否索引有所改變
{
/* Key was modified. */
ha_alter_info->add_modified_key(table_key, new_key); //加入到modify key buffer
}
}
而加入到modify buffer后這個索引就是需要drop并且add的,因此DDL類型定義為,Alter_inplace_info::DROP_INDEX|Alter_inplace_info::ADD_INDEX,因此就需要進行索引的刪除和重建,因此關鍵就是函數(shù)has_index_def_changed的更改,我們先看5.7.22的這個BUG相關的判斷點:
if (key_part->length != new_part->length)
return true;
也就是當索引字段長度更改了就返回true。而在新版本中:
if (key_part->length != new_part->length &&
ha_alter_info->alter_info->flags == Alter_info::ALTER_CHANGE_COLUMN &&
(key_part->field->is_equal((Create_field *)new_field) == IS_EQUAL_PACK_LENGTH))
{
ha_alter_info->handler_flags|=
Alter_inplace_info::ALTER_COLUMN_INDEX_LENGTH;
}
else if (key_part->length != new_part->length)
return true;
變更還是比較大的,主要是key_part->field->is_equal((Create_field *)new_field) == IS_EQUAL_PACK_LENGTH)這個條件是否滿足,而判定的函數(shù)為Field_varstring::is_equal,
uint Field_varstring::is_equal(Create_field *new_field)
{
if (new_field->sql_type == real_type() &&
new_field->charset == field_charset)
{
if (new_field->length == max_display_length()) //新老字段長度相同
return IS_EQUAL_YES;
DBUG_ASSERT(0 == (new_field->length % field_charset->mbmaxlen));
DBUG_ASSERT(0 == (max_display_length() % field_charset->mbmaxlen));
if (new_field->length > max_display_length() && //新字段長度大于老字段長度,需要額外判斷
((new_field->length <= 255 && max_display_length() <= 255) ||
(new_field->length > 255 && max_display_length() > 255)))
return IS_EQUAL_PACK_LENGTH; // VARCHAR, longer variable length
}
return IS_EQUAL_NO;
}
其重點為如下:
- A:如果新的字段長度>老的字段的長度
- B:字段長度不能跨越255字節(jié)
那么則返回IS_EQUAL_PACK_LENGTH,因此就這個點上has_index_def_changed函數(shù)就會返回false,不會刪除和重建索引了。
四、相關BUG
這個BUG雖然有點老了,是5.7.23修復的,如下:
但是對于5.7.23之前的版本在評估類似DDL操作的時候需要謹慎,可能評估為瞬間操作,但是實際上線的時候跑了很久,這個就容易導致超過維護窗口,甚至更大的故障,因此還是建議任何DDL操作除了翻看官方文檔以外,都需要在相同版本的數(shù)據(jù)庫測試環(huán)境測試其耗時是否達到預估水平。