【MySQL】表字段字符集不同导致的索引失效问题

问题背景

在一次开发任务中,同事跟我说他开发的一个列表分页查询请求要耗时10多分钟,查询SQL是新建的一个表关联了两张主表,主表数据量较大。但正常情况下如果有索引的话,查询也不会慢,这显然是有问题的。

问题原因

通过排查,得知问题原因在于新建的表字符集用的是utf8mb4,而之前的两张主表的字符集设置的是utf8,查询进行表关联,导致索引失效。

问题再现

我们可以通过新建两张表,插入一些数据,查看sql执行计划,再现问题。

  • 创建表及初始化数据:
-- 表1utf8字符集,并建立code和name列索引
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`code` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 表2 utf8mb4字符集,并建立code和name列索引
CREATE TABLE `t2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`code` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

INSERT INTO `t1` (`name`,`code`) VALUES ('zzz','00006'),('xxx','00002'),('aaa','000003'),('sss','000004'),('ddd','000005');

INSERT INTO `t2` (`name`,`code`) VALUES ('zzz','00001'),('xxx','00002'),('aaa','000003'),('sss','000004'),('ddd','000005');
  • 联表查询SQL及对应的执行计划结果:
-- t1全表扫描
desc select * from t1 left join t2 on t1.code = t2.code where t2.name = 'ddd';

在这里插入图片描述

-- t1全表扫描
desc select * from t2 left join t1 on t1.code = t2.code where t2.name = 'ddd';

在这里插入图片描述

-- 两个表索引都有效
desc select * from t2 left join t1 on t1.code = t2.code where t1.name = 'qqq';

在这里插入图片描述

-- 两个表索引都有效
desc select * from t1 left join t2 on t1.code = t2.code where t1.name = 'qqq';

在这里插入图片描述

  • 索引失效原因:

我们可以使用以下语句,得到更加详细的SQL执行计划

(1) t2 left join t1

EXPLAIN EXTENDED select * from t2 left join t1 on t1.code = t2.code where t2.name = 'ddd';
SHOW WARNINGS;

结果如下:

/* select#1 */ select `demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code`,`demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code` from `demo`.`t2` left join `demo`.`t1` on((convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`)) where (`demo`.`t2`.`name` = 'ddd')

(2) t1 left join t2

EXPLAIN EXTENDED select * from t1 left join t2 on t1.code = t2.code where t2.name = 'ddd';
SHOW WARNINGS;

结果如下:

/* select#1 */ select `demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code`,`demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code` from `demo`.`t1` join `demo`.`t2` where ((`demo`.`t2`.`name` = 'ddd') and (convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`))

从上面可以看出,不论是t1 join t2,还是t2 join t1,在查询过程中会对t1表进行了一次字符集的转换(convert(demo.t1.code using utf8mb4) )。字符集转换遵循由小到大的原则,因为utf8mb4是utf8的超集,所以这里把utf8转换成utf8mb4。而实际上t1表中的索引是utf8格式的,所以会导致t1表全表扫描。

但如果我们以t1中的字段作为查询条件的话,两个表的索引是可以都生效的:

EXPLAIN EXTENDED select * from t2 left join t1 on t1.code = t2.code where t1.`code` = '00001';
SHOW WARNINGS;

通过以下执行计划结果,可以看出在转换字符集之前,就先进行了查询,所以t1表中的索引也使用到了:

/* select#1 */ select `demo`.`t2`.`id` AS `id`,`demo`.`t2`.`name` AS `name`,`demo`.`t2`.`code` AS `code`,`demo`.`t1`.`id` AS `id`,`demo`.`t1`.`name` AS `name`,`demo`.`t1`.`code` AS `code` from `demo`.`t2` join `demo`.`t1` where ((`demo`.`t1`.`code` = '00001') and (convert(`demo`.`t1`.`code` using utf8mb4) = `demo`.`t2`.`code`))

解决方案

  1. 如上述实例,如果查询字段在t1和t2表中都存在,可以选择使用t1表中的字段进行查询。
  2. 如果查询字段必须通过t2表,则只能通过修改字符集处理,将关联的表的字符集都修改成一样的。

最后附上修改字符集sql:

alter table t1 convert to charset utf8mb4;

还要注意一点,alter table 改字符集的操作是阻塞写的(用lock = node会报错)所以业务高峰时不要操作。

问题总结

  1. 一个项目中的表和字段字符集最好统一,utf8还是utf8mb4按业务情况定好规则,不要随意使用。
  2. 擅于利用执行计划,优化SQL查询的利器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章