最近工做上遇到一个“神奇”的问题,或许对你们有帮助,所以造成本文。mysql
图片来自 Pexelssql
问题大概是,我有两个表 TableA,TableB,其中 TableA 表大概百万行级别(存量业务数据),TableB 表几行(新业务场景,数据还未膨胀起来)。ide
语义上 TableA.columnA=TableB.columnA,其中 columnA 上创建了索引,但查询的时候确巨慢无比,基本上到 5-6 秒,明显跟预期不符合。工具
下面我以一个具体的例子来讲明,模拟其中的 SQL 查询场景。oop
场景重现测试
索引状况以下图:优化
查询业务场景:已知 user_score.id,须要关联查询对应 user_info 的信息,(你们先忽略这个具体业务场景是否合理哈)。ui
那么对应的 SQL 很天然的以下:
请忽略其中的数据,我刚开始 mock 了 100W,而后又重复导入了两遍,所以数据有一些重复。spa
300W 数据,最后查询出来也是 1.18 秒,按道理应该更快的,老规矩 explain 看看啥状况?3d
发现 user_info 表没用上索引,全表扫描近 300W 数据?现象是这样,为何呢?
你不妨思考一下,若是你遇到这种场景,应该怎么去排查?
我当时也是“一顿操做猛如虎”,然并卵?尝试了什么多种 SQL 写法来完成这个操做。
好比更换 Join 表的顺序(驱动表/被驱动表),再好比用子查询。最终,仍是没有结果。但直接单表查询写 SQL 确能用上索引。
问题解决
在准备求助 DBA 前,我看了下表的建表语句:
彻底有理由怀疑由于字符集不一致的问题致使索引失效的问题。
因而修改了小表(真实线上环境可别乱操做)的字符集与大表一致,再测试下:
mysql> select * from user_score us
-> inner join user_info ui on us.uid = ui.uid
-> where us.id = 5;
+----+-----------+-------+---------+-----------+---------+
| id | uid | score | id | uid | name |
+----+-----------+-------+---------+-----------+---------+
| 5 | 111111111 | 100 | 1 | 111111111 | tanglei |
| 5 | 111111111 | 100 | 3685399 | 111111111 | tanglei |
| 5 | 111111111 | 100 | 3685400 | 111111111 | tanglei |
| 5 | 111111111 | 100 | 3685401 | 111111111 | tanglei |
| 5 | 111111111 | 100 | 3685402 | 111111111 | tanglei |
| 5 | 111111111 | 100 | 3685403 | 111111111 | tanglei |
+----+-----------+-------+---------+-----------+---------+
6 rows in set (0.00 sec)
mysql> explain
-> select * from user_score us
-> inner join user_info ui on us.uid = ui.uid
-> where us.id = 5;
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
| 1 | SIMPLE | us | const | PRIMARY,index_uid | PRIMARY | 4 | const | 1 | NULL |
| 1 | SIMPLE | ui | ref | index_uid | index_uid | 194 | const | 6 | NULL |
+----+-------------+-------+-------+-------------------+-----------+---------+-------+------+-------+
2 rows in set (0.00 sec)
挖掘根因
此次这个 case,若是知道 explain extended+show warnings 这个工具的话,(之前都不知道 explain 后面还能加 extended 参数),可能就尽早“恍然大悟”了。(最新的 MySQL 8.0 版本貌似不须要另外加这个关键字)
看下效果:(啊,我还得把字符集改回去)
mysql> explain extended select * from user_score us inner join user_info ui on us.uid = ui.uid where us.id = 5;
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
| 1 | SIMPLE | us | const | PRIMARY,index_uid | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 1 | SIMPLE | ui | ALL | NULL | NULL | NULL | NULL | 2989934 | 100.00 | Using where |
+----+-------------+-------+-------+-------------------+---------+---------+-------+---------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1003 | /* select#1 */ select '5' AS `id`,'111111111' AS `uid`,'100' AS `score`,`test`.`ui`.`id` AS `id`,`test`.`ui`.`uid` AS `uid`,`test`.`ui`.`name` AS `name` from `test`.`user_score` `us` join `test`.`user_info` `ui` where (('111111111' = convert(`test`.`ui`.`uid` using utf8mb4))) |
+-------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
索引列参与计算了,每次都要根据字符集去转换,全表扫描,你说能快得起来么?
至于这个问题为何会发生?综合来看,就是由于历史缘由,老业务场景中的原表是假 utf8,新业务新表采用了真 utf8mb4。
①考虑新表的时候,忽略和原库字符集的比较。其实,发现库里面的不一样表可能都有不一样的字符集,不一样人建的时候可能都依据我的喜爱去选择了不一样的字符集。因而可知,开发规范有多重要。
②虽然知道索引列不能参与计算,但这个场景下都是相同的类型,varchar(64) 最终查询过程当中仍然发生了类型转换。所以须要把字段字符集不一致等同于字段类型不一致。
③若是这个 case,利用 fail-fast 的理念的话,发现不一致,直接不让 join 会不会更好?(就像 char v.s varchar 不能 join 同样)
说明:本文测试场景基于 MySQL 5.6,另外,本文案例只是为了说明问题,其中的 SQL 并不规范(例如尽可能别用 select * 之类的),请勿模仿(模仿了我也不负责)。
最后留一个思考题供讨论,欢迎留言说出你的见解。
你能解释以下状况吗?查询结果表现为什么不一致?注意一下 SQL 的执行顺序,查询优化器工做流程,以及其中的 Using join buffer(Block Nested Loop)。
能够多看看 MySQL 官方手册深刻了解背后的过程和原理:
https://dev.mysql.com/doc/refman/5.6/en/