为什么你的MySQL索引偶发失效?从成本优化视角重看
线上最让人头疼的慢查询,往往不是“没加索引”,而是“昨天还走索引,今天突然全表扫”。你去看 `EXPLAIN`,有时 `key` 有值,有时是 `NULL`;加个 `FORCE INDEX` 立刻变快,过几天又有人反馈同一类 SQL 开始抖动。于是大家习惯说“MySQL 索引偶发失效”。但从优化器视角看,索引并没有消失,它只是没有被选中。
MySQL 的优化器不是一套“看到索引就必须用”的规则引擎,而是一个成本估算器。它会为可能的执行计划算一笔账:走哪个索引、要不要回表、回表多少次、是否排序、是否建临时表、全表扫描顺序读是否更便宜。最终选成本最低的那个。所谓“偶发失效”,多数时候是成本估算变了,或者优化器对现实的假设和你的业务现实错位了。
索引没有失效,是优化器“算账”后放弃了它
二级索引查询通常要两步:先在索引上找到主键,再回表取整行。回表是随机 IO,行数一多,成本会迅速膨胀。如果某个查询要返回大量行,或者过滤后仍然命中几万行,优化器可能认为“全表扫描顺序读”比“走索引+大量随机回表”更便宜。于是 `key` 变成 `NULL`,看起来像索引失效,其实是成本比较的结果。
这也是为什么同一个索引,在小范围查询里很香,在大范围查询里可能被放弃。范围越大,回表越多,索引的边际收益越低。
成本估算依赖统计信息,而统计信息会“过期”
优化器主要依赖统计信息估算行数、选择性和数据分布。InnoDB 默认会采样部分页来估算,采样页数、持久化统计、数据变化速度都会影响结果。如果表最近批量写入、删除、数据倾斜严重,或者某个状态值突然占比很高,优化器可能还按旧分布估算。
比如 `status='pending'` 原本只占 1%,走索引很划算;某天积压后占到 40%,走索引回表 40% 的行,成本就未必低了。此时 `ANALYZE TABLE` 可能改善,但也不是银弹。MySQL 8.0 的直方图能帮助优化器理解倾斜分布,但对复杂关联和表达式列仍有局限。
同一个 SQL,不同参数就是不同计划
预编译语句和绑定变量很容易掩盖参数分布差异。优化器可能按平均选择性生成计划,但实际执行时,有的租户查最近一天,有的租户查最近一年;有的 `IN` 列表只有 3 个值,有的有 300 个值。范围长度、`LIMIT`、`ORDER BY`、`JOIN` 顺序一变,成本排序就变了。
所以“偶发”常常不是随机,而是特定参数、特定时间窗口、特定租户触发了另一种成本结构。只拿一条样本 SQL 去测,很容易复现不出来。
回表、随机 IO 与覆盖索引:成本模型的核心矛盾
想稳定用上索引,核心是降低优化器眼里的回表成本。覆盖索引让查询字段都在索引里,避免回表;延迟关联先通过索引拿主键,再关联取整行,减少随机 IO;索引下推把过滤条件下推到存储引擎层,减少无效回表。对于 `ORDER BY ... LIMIT`,如果排序字段和过滤字段能被同一个索引覆盖,优化器更愿意走索引。
反过来,如果索引选择性差、回表比例高、排序和临时表成本高,优化器转向全表扫描并不奇怪。它不是“不聪明”,而是在当前统计和成本常数下做了理性选择。
优化器成本常数和硬件现实可能不一致
MySQL 的成本模型里有一组成本常数,比如随机读、顺序读、临时表创建等。默认值未必匹配你的硬件。NVMe SSD 的随机读很快,内存命中率很高,但优化器未必完全知道 Buffer Pool 里热点页有多热。于是会出现:实际执行很快的计划,优化器估算很贵;或者估算很便宜的计划,实际被 IO 拖垮。
这也是执行计划偶发漂移的原因之一:缓存状态、并发、数据冷热变化,都会让“真实成本”和“估算成本”出现偏差。
排查时别急着 FORCE INDEX
遇到索引没被选中,先用 `EXPLAIN FORMAT=JSON` 看 `rows`、`filtered`、`cost_info`,再考虑 `optimizer_trace` 看优化器为什么排除某个索引。检查统计信息是否过期,数据分布是否倾斜,参数是否代表真实业务。`FORCE INDEX` 可以验证判断,但不适合作为长期方案,它可能把问题从“选错计划”变成“锁死坏计划”。
更稳妥的方向是:补覆盖索引、改写大范围查询、拆分批量操作、更新统计信息、必要时使用直方图或优化器提示,并持续观察执行计划。
索引偶发失效,本质上不是索引坏了,而是成本优化下的动态选择。不要只问“索引为什么没走”,要问“优化器为什么觉得它更贵”。把统计信息、参数分布、回表成本和硬件现实一起纳入治理,执行计划才会从偶发漂移变成稳定可控。
转载请注明出处,版权归原作者所有。
管理员
黑卡会员



