一、问题描述
涉及的两张表表结构如下:
CREATE TABLE `ep_ding_talk_class_relation` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`ding_talk_class_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '',
`care_class_id` char(32) NOT NULL DEFAULT '' COMMENT '',
`corp_id` varchar(128) NOT NULL DEFAULT '' COMMENT '',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_care_class_id` (`care_class_id`),
UNIQUE KEY `udx_ding_talk_class_id_corp_id` (`ding_talk_class_id`,`corp_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=817 DEFAULT CHARSET=utf8mb4 COMMENT=''
CREATE TABLE `ep_im_group_classroom` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`resourceid` varchar(45) CHARACTER SET utf8 NOT NULL,
`classroom_id` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
`class_nick_name` varchar(45) NOT NULL DEFAULT '',
`class_master_id` varchar(45) CHARACTER SET utf8 NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `res` (`resourceid`) USING BTREE,
UNIQUE KEY `uk_classroom_id` (`classroom_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1046177 DEFAULT CHARSET=utf8mb4
SQL语句以及explain后的结果如下:
查询优化器没有像预想的那样走uk_classroom_id索引。
二、问题分析及解决
之前听说过,MySQL在执行子查询时,可能会优化SQL语句,抱着试一试的心态查看了优化后的SQL语句,一下就找到了问题。
explain后执行show warnings; 得到MySQL优化后的SQL:
如上图所示,SQL语句被优化成convert(classroom_id using utf8mb4) 后再与care_class_id字段比较,是需要将ep_im_group_classroom表的classroom_id全部convert一次的,自然就全表扫描了(走索引也是把索引上的classroom_id全都convert一遍的)。
解决方式
方式一:
将ep_im_group_classroom表classroom_id字段的编码方式改为utf8mb4,这涉及到改表的结构,也需要评估其他风险,比如这张表是不是还和别的表有连接查询、子查询操作?改编码方式会不会导致其他查询语句索引失效?
方式二:
将子查询结果的care_class_id字段convert成utf8。如下图:
查询ep_im_group_classroom表时是走了索引的。(ep_ding_talk_class_relation表本来就没有高效的索引可走的,corp_id字段没索引)
这种解决方式也是有风险的,care_class_id字段原本的编码方式是utf8mb4,与utf8相比,utf8mb4可以存的字符种类更多,比如utf8mb4可以存emoji表情,utf8就不行,如果care_class_id字段存了emoji字符时,将其convert成utf8就会出错。这里可以这么做,是因为care_class_id字段必定为字母、数字组成的uuid。
三、一些思考
1.关于不同编码方式字符串的比较规则
说起来有些惭愧,一直以来都没了解过不同编码方式的字符串是怎么比较的,以为就是单纯的二进制比较。所以我一开始的疑问是:为什么需要转换字符集再比较?直接无视编码方式用二进制比较不就行了?
实际上,gbk编码方式的“我” 和 utf8mb4编码方式的“我” ,两者二进制不同,但是比较的结果是要相等,所以需要做一次转换再比较。
Unicode : 是一个字符集,规定了字符的二进制码,没规定这个二进制代码要怎么存。
比如汉字“严”的Unicode二进制是100111000100101,
GBK编码方式使用两个字节保存这个二进制码,结果的十六进制为D1CF ,
UTF8编码方式使用三个字节保存这个二进制码,结果的十六进制为E4B8A5。
编码方式的转换其实就是:
(1)用当前编码方式,解析出字符的Unicode码;
(2)用目标编码方式保存该字符的Unicode码。
关于编码方式转换,如果不清楚,可以学习一下这篇:
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
2.只要对字符集不同的两个字段做连接查询,索引就一定会失效吗?
我把classroom_id和care_class_id两个字段的字符集互换,再看看explain的执行情况。
explain的结果:
优化器优化后的SQL:
可以看出,classroom_id使用utf8mb4字符集,care_class_id使用utf8字符集时,查询语句被优化成
convert(care_class_id using utf8mb4),查询ep_im_group_classroom表时走了uk_classroom_id索引。
convert()哪一个字段,取决于它们的字符集哪个是子集,哪个是超集。
utf8是utf8mb4的子集,所以是convert()字符集为utf8的字段。
3.一定要是连接查询,索引才会因为字符集不同失效吗?
可见,该子查询未被优化成连接查询,也因为字符集的原因没走uk_classroom_id索引
四、总结
连接查询、子查询都有可能因为字符集不同导致查询不走索引。
不是字符集不同,连接查询、子查询就必定不走索引,还要看两字段字符集的子集、超集关系。
因为字符集导致连接查询、子查询不走索引时,可以尝试用convert()解决,但是是有风险的,需要注意convert的目标编码方式,是否可以存该字段可能存储的所有字符种类。