当你发现一条SQL查询明明有索引却没用上,或者优化器选择了错误的索引时,是否想过背后的决策逻辑?查询优化器并非随机选择,而是基于统计信息和成本估算模型,在多个候选执行计划中挑选“成本最低”的方案。本文将以实际问题为起点,逐层剖析基数、选择率、回表代价等关键因素如何影响索引选择,并给出可操作的排查方法。
为什么我的查询没用上索引?
一个常见场景:在订单表上为 status 列创建了索引,但执行计划显示全表扫描。原因可能有三:一是查询条件的选择性太低,比如 status 字段只有两个值,且分布均匀,优化器认为全表扫描比索引扫描更划算;二是统计信息过期,导致优化器对行数估计错误;三是查询涉及的范围过大,回表成本太高。要验证,可以查看执行计划中的 rows 估算值和实际行数对比,若差距大,则需更新统计信息。
基数:索引选择的第一道门槛
基数(Cardinality)指索引列中不同值的数量。基数越高,索引的选择性越好,优化器越倾向使用该索引。例如,用户表的 id 列基数等于行数,而 gender 列基数可能只有 2。在联合索引中,基数还影响索引的排列顺序:通常将高基数列放在前面。但基数并非唯一标准,还需结合查询模式。
成本估算:优化器的决策核心
优化器为每个候选计划计算总成本,包括 I/O 成本、CPU 成本和内存成本。以 MySQL InnoDB 为例,索引扫描的成本大致为:读取索引页的 I/O 成本 + 回表读取数据页的 I/O 成本 + 处理行的 CPU 成本。回表成本与预估行数成正比,而预估行数则由选择率决定。选择率 = 满足条件的行数 / 总行数,通常基于统计信息中的直方图或均匀分布假设估算。
统计信息:成本估算的数据基础
优化器依赖表统计信息(如行数、页数、列基数、直方图)进行估算。若统计信息未及时更新,可能导致严重误判。例如,一张千万行表在删除大量数据后,优化器仍按旧行数估算,可能认为全表扫描成本更低。因此,在批量操作后应执行 ANALYZE TABLE 更新统计信息。MySQL 8.0 支持自动重算,但高并发场景下仍建议手动维护。
回表代价:覆盖索引为何高效
当索引包含查询所需的所有列时,无需回表,称为覆盖索引。优化器会优先考虑覆盖索引,因为省去了大量随机 I/O。例如,查询 SELECT name FROM users WHERE age > 30,若索引为 (age, name),则可以直接从索引中获取 name,成本远低于 (age) 索引加回表。在设计索引时,应尽量利用覆盖索引减少回表。
常见误区与排查步骤
误区一:认为索引越多越好。过多索引会增加写入成本和优化器选择负担。误区二:忽略复合索引列顺序。误区三:对低基数列强制使用索引,反而降低性能。排查步骤:1) 使用 EXPLAIN 查看执行计划,关注 possible_keys、key 和 rows 列;2) 对比预估行数与实际行数,判断统计信息是否准确;3) 使用 FORCE INDEX 临时测试,但最终应通过优化索引或查询解决;4) 检查索引选择性,若低于 20%,考虑其他方案。
参考资料
延伸阅读
