1. 现象
表结构如下
CREATE TABLE `ACT_HI_INST` ( `ID` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '主键', `INST_ID_` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '流程实例id', `BUSINESS_KEY_` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '流程编号', `CREATE_TIME_` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `UPDATE_TIME_` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`ID`), UNIQUE KEY `UIDX_INST` (`INST_ID_`), UNIQUE KEY `UIDX_BKEY` (`BUSINESS_KEY_`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
执行下面语句, 发现没有走索引
explain SELECT * FROM ACT_HI_INST hi ORDER BY hi.BUSINESS_KEY_ DESC;
select count(*) from ACT_HI_INST ; -- 4332
同时我们发现rows与实际表的数量存在差异,使用下面语句重新统计索引信息, 更正统计信息, 更新之后发现 rows 就数据表的数量基本一致(不完全一样是因为rows是采样统计而来的)。
analyze table ACT_HI_INST;
也就是说对于下面这条语句,尽管BUSINESS_KEY_ 上有索引,MySQL还是选择了全表扫描。为什么会这样呢?
SELECT * FROM ACT_HI_INST hi ORDER BY hi.BUSINESS_KEY_ DESC;
2. 猜想
我猜想原因是 因为MySQL认为即使 使用 BUSINESS_KEY_ 索引树进行查询,不需要排序,但是最后仍然需要回表,回表的次数 == 表的大小,这种代价比全表扫描然后排序的代价更大。
3. 验证
为了验证我的猜想,我加上在SQL语句最后加上 LIMIT 10 。如下所示
explain SELECT * FROM ACT_HI_INST hi ORDER BY hi.BUSINESS_KEY_ DESC limit 10;
果然我们看到加上 LIMIT 的语句走了索引。因为这个时候MySQL认为回表的代价比排序的代价更小,所以这个时候选择了走 BUSINESS_KEY_ 索引。
除了上面这种方式可以验证我的猜想,还有一种方式,如下所示。我们只查询BUSINESS_KEY_ 。
因为此时叶子节点就包含我们需要查询的字段,这个时候不需要再回表,所以MySQL选择BUSINESS_KEY_ 代价最小。
explain SELECT hi.BUSINESS_KEY_ FROM ACT_HI_INST hi ORDER BY hi.BUSINESS_KEY_ DESC ;
4. 补充
这部分主要来源于极客时间林晓斌老师的《MySQL实战45讲》第16讲
order by a
如果 a 字段上有索引,MySQL innodb引擎是按照上诉的方式进行选择。如果a字段上没有索引,MySQL innodb引擎 就会有两种排序方式:
全字段排序 和 rowid 排序。
全字段排序:将所有要选择的字段加入到sort_buffer中,然后在内存或者外部进行排序。如果能在内存中进行排序就在内存中进行排序。
如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。这个时候MySQL就会采用rowId排序。
rowid排序:MySQL取出需要排序的字段和ID放入sort_buffer中进行排序,最后按照排序的结果,通过ID回表,返回数据到客户端。
MySQL 的一个设计思想是如果内存够,就要多利用内存,尽量减少磁盘访问。所以对应sort_buffer足够大的情况,MySQL会优选选择全字段排序。