Mysql上sql执行为何选错索引
1、问题缘起
生产环境阿里云PolarDB-Mysql的慢日志查看到,用户课程记录表,class_info表中的分页查询sql执行时间将近5s,
1 | SELECT * FROM class_info WHERE (use_point = 'buy' AND lesson_date >= '2025-03-01' AND lesson_date <= '2025-03-31') ORDER BY start_time ASC,id ASC LIMIT 20; |
可以看到该sql查询逻辑并不复杂,就是个简单的条件查询后再排序分页,并且表中也有索引index_lesson_date(use_point,lesson_date),按理也是会正常走索引,为什么会查询如此之慢呢?
2、排查过程
因为本身有索引,所以第一时间是怀疑服务实例负载过高导致的,查看了实例状态监控,并无异常。于是登录到从库实例,执行了这个问题sql,一样是执行时间将近5s,这个情况的话,马上使用explain命令查看sql的执行计划,发现执行计划中使用的索引并不是index_lesson_date(use_point,lesson_date),而是index_add_time(use_point,add_time),这个结果一时令人费解。
sql的where条件明明白白,为什么执行过程中没有使用index_lesson_date(use_point,lesson_date)这个正正好的索引,而是用了这个看起来毫不相干的index_add_time(use_point,add_time),带着疑问,修改了一下sql语句,强制指定了查询使用的索引,执行了一下,700ms返回了结果。那么我们现在可以明确做出结论,Mysql在执行这个查询sql时选错了索引。
1 | SELECT * FROM class_info force index (index_lesson_date) WHERE (use_point = 'buy' AND lesson_date >= '2026-03-01' AND lesson_date <= '2026-03-31') ORDER BY start_time ASC,id ASC LIMIT 20; |
我们知道索引选择是由Mysql的优化器做的,优化器选择索引的工作,会通过扫描行数、是否使用临时表、是否排序等因素进行综合判断。所以来查看一下这个查询sql详细的OPTIMIZER_TRACE。
1、发现在analyzing_range_alternatives范围查询分析过程中,优化器认为index_lesson_date(use_point,lesson_date)和index_add_time(use_point,add_time)两个索引都要扫描40W+行,优化器都没有选择。
2、在considered_excution_plans考虑的执行计划中,优化器判断index_add_time(use_point,add_time)索引需要扫描16w行,而index_lesson_date(use_point,lesson_date)索引需要扫描24w行,最终优化器选择了索引index_add_time(use_point,add_time)。但使用这个索引,完全起不到缩小数据集范围的效果,执行时会每扫描一条数据就回表读取数据,看lesson_date是否落在where条件内,收集完所有符合要求的数据,再在内存中排序,自然慢到不行。
那么现在问题关键就在于,为什么优化器认为index_add_time(use_point,add_time)索引会比index_lesson_date(use_point,lesson_date)索引扫描的行数少?
mysql在sql实际执行之前,执行计划中的这个扫描行数是估算出来的,并不一定是实际的情况,它这个估算是通过一个采样方法估算的,每次更新一定量的数据时,会更新每个索引的这个统计数值。
而在class_info表中use_point字段的数据,极不均匀,将近88%的数据都是use_point='buy'的数据。
index_lesson_date(use_point,lesson_date)索引,lesson_date是上课日期,数据按照use_point + 日期分散插入,采样时可能恰好扫到很多都是use_point='buy'的page,导致估算基数 ≈ 1(全是use_point= 'buy'),因此优化器的估算行数rows=80W+;index_add_time(use_point,add_time)索引,add_time是记录创建时间,通常单调递增,数据分布可能更均匀,采样时可能扫到了use_point='buy'和use_point='buy'混合的page,估算基数 ≈ 2,因此优化器的估算行数rows=40W+;
综上,这个查询sql执行的时候选错索引的根本原因就在于,use_point的索引,建的不好,数据区分度不高。
3、解决方案
mysql查询索引选错,一般3个办法处理:
- 查询语句用force index强制指定索引;
- 修改sql语句,引导mysql使用我们期望的索引;
- 新建一个更加合适的索引,或者删掉某些索引;
对应我们这个场景:
- 强制指定索引为
index_lesson_date(use_point,lesson_date),执行时长约700ms上下,但是还是会using file sort; - 修改sql,
order by条件最前面增加字段lesson_date,index_lesson_date索引后面再加start_time和id –>(use_point,lesson_date,start_time,id),实际业务语义是无变化的,而且不会有using file sort;
1 | SELECT * FROM class_info WHERE (use_point = 'buy' AND lesson_date >= '2026-03-01' AND lesson_date <= '2026-03-31') ORDER BY lesson_date, start_time, id LIMIT 20; |

index_add_time索引,业务上有使用到,不太好删除;
最终选择方案二解决问题。不过远期来看,class_info表的索引,有一些建的确实不合理,待后面全盘整理再优化。