使用5.7.22版本:
建表语句,注意这里字段a包含了一个索引,这是触发这个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
数据量:
mysql> select count(*) from testmy;
+----------+
| count(*) |
+----------+
| 262144 |
+----------+
1 row in set (5.17 sec)
执行DDL语句:
alter table testmy modify `a` varchar(30) comment 'test1111';
本DDL语句主要完成:
按照常理来讲这个DDL是只修改元数据的,因此应该瞬间完成,但是实际在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的长度,只要字符集字节数量*字符数量不跨越256,那么就是修改元数据,不会重建索引。
既然不符合官方文档的说明,那么这个问题肯定是某种BUG导致。当我们进行DDL操作的时候,需要对比更改部分和现有的数据字典中标定义的差别,然后根据这些差别来定义操作方式,然后根据操作方式来判断哪一种DDL 方式比较合适,关于定义操作方式的部分来自于函数fill_alter_inplace_info,而在函数中会根据新表的索引和老表的索引字段的长度判断是否需要drop索引和新建索引,代码中体现为如下:
/*
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++) //循环老表的索引
{
/* 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,因此就需要进行索引的删除和重建,因此关键就是函数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)这个条件是否满足,而判定的函数为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;
}
其重点为如下:
那么则返回IS_EQUAL_PACK_LENGTH,因此就这个点上has_index_def_changed函数就会返回false,不会删除和重建索引了。
这个BUG虽然有点老了,是5.7.23修复的,如下:
但是对于5.7.23之前的版本在评估类似DDL操作的时候需要谨慎,可能评估为瞬间操作,但是实际上线的时候跑了很久,这个就容易导致超过维护窗口,甚至更大的故障,因此还是建议任何DDL操作除了翻看官方文档以外,都需要在相同版本的数据库测试环境测试其耗时是否达到预估水平。