问题背景
在一次开发任务中,同事跟我说他开发的一个列表分页查询请求要耗时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`))
解决方案
- 如上述实例,如果查询字段在t1和t2表中都存在,可以选择使用t1表中的字段进行查询。
- 如果查询字段必须通过t2表,则只能通过修改字符集处理,将关联的表的字符集都修改成一样的。
最后附上修改字符集sql:
alter table t1 convert to charset utf8mb4;
还要注意一点,alter table 改字符集的操作是阻塞写的(用lock = node会报错)所以业务高峰时不要操作。
问题总结
- 一个项目中的表和字段字符集最好统一,utf8还是utf8mb4按业务情况定好规则,不要随意使用。
- 擅于利用执行计划,优化SQL查询的利器。