MySQL执行语句时选择的索引,是优化器经过评估后作出的‘最优’选择。实际工作中,我们会遇到一些索引选择错误的情况。
优化器的选择逻辑
优化器的选择,会从多个方面考量,其中包括:扫描行数、是否使用临时表、是否排序、是否回查主键索引等。
扫描行数
执行SQL时需要扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。那么MySQL是怎么判断扫描行数的?
其实在真正的执行SQL之前,MySQL计算扫描行数是根据统计信息来估算的。每个索引都有自己的区分度,索引上不同的值越多,区分度约好。我们把一个索引上不同的值的个数,成为索引的基数cardinality。
基数cardinality的计算
对一张表逐行统计可以得到精确结果,但是统计和维护代价太高了,所以只能采用采样统计。采样统计会导致一定的不准确可能。
采样统计:InnoDB会选择N个数据页,统计这些数据页上的不同值,得到一个平均值,然后乘以这个索引的页面数,得到这个索引的基数。当数据表由于更新,导致变更行的数量超过1/M时,就会自动触发一次重新采样统计。
MySQL中索引统计的存储有两种方式,通过innodb_stats_persistent参数设置:
1)设置为on,表示统计信息会持久化存储,此时N默认20,M是10.
2)设置为off,表示统计信息只存在缓存中,此时N默认8,M是16.
对于统计信息不准的,可以通过执行analyze table T命令,通信统计修正索引信息。
回查主键索引
扫描行数不是影响优化器选择索引的唯一条件,比如使用主键索引需要扫描10万行记录,使用普通索引a需要5万行记录,但是使用普通索引需要查询完普通索引的值之后,回到主键索引上查出最终结果,优化器也会把这个成本考虑进去。
排序影响索引选择
select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
使用普通索引a需要扫描1000行;使用普通索引b需要扫描50001行。虽然索引b的扫描行数多,但是如果使用索引b可以避免排序(索引本身有序),最终导致MySQL预估使用索引b综合成本更小。但是只代表预估结果,不代表实际执行结果。
如何纠正MySQL索引选择错误
主要有三种手段,需要结合不同场景选择:
1)SQL中加入force index强制选择指定索引:select * from T force index(idx_a) where ….;
2)修改SQL语句,但是保证语义相同。比如order by b改为order by b,a,这是使用a或b索引都需要排序,因此排序不再是主要决定因素,优化器最终选择扫描行数更少的索引a。
3)新建一个更合适的索引,或者删除误用的索引。如果索引b没有存在必要且会影响优化器错误选择,可以考虑删掉。
