mysql优化器选全表扫描是因为成本计算显示其比走索引更优,本质是顺序i/o优于随机i/o,常见原因包括过滤性差、回表、排序不匹配、索引区分度低及统计信息失真。

优化器选全表扫描,本质是成本算出来更便宜
MySQL 不是“不走索引”,而是 optimizer 算完发现:走索引要读更多页、回更多表、排序更费劲,总 query_cost 比直接扫聚簇索引还高。InnoDB 里所谓“全表扫描”,实际就是顺序遍历主键 PRIMARY 叶子节点——这是最稳定的顺序 I/O,而二级索引 + 回表是大量随机 I/O。
常见导致成本失衡的点:
-
WHERE条件匹配行数太多(比如status != 'done'覆盖 85% 行),索引过滤性差 - 查询列未被索引覆盖(
SELECT *或含非索引列),强制回表,每行一次随机读 -
ORDER BY字段不在索引中或顺序不匹配(如用idx_source_id却按id排序),无法复用索引有序性 - 索引前导列选择性低(如
source_id是varchar(64)但大量值前缀相同),导致范围扫描仍需检查大量叶子节点
Cardinality 失真会让优化器彻底“误判”
CARDINALITY 是优化器估算“这个索引能筛掉多少行”的核心依据,但它不是精确值,而是采样估算。一旦失真,优化器就可能把本该高效过滤的索引当成废索引。
查和修的实操步骤:
- 先运行
ANALYZE TABLE mapping_filter_record,强制刷新统计信息 - 再查
information_schema.STATISTICS:SELECT COLUMN_NAME, CARDINALITY, INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_NAME = 'mapping_filter_record' AND INDEX_NAME = 'idx_source_id'; - 如果
CARDINALITY / TABLE_ROWS (即低于 1%),说明该索引区分度极差,大概率被跳过 - 如果
CARDINALITY = 1或明显小于你肉眼所见的不同值数量(比如state有 5 种状态但显示为 2),说明统计严重过期
EXPLAIN FORMAT=JSON 和 optimizer_trace 才是真相入口
EXPLAIN 默认只告诉你“选了什么”,不告诉你“为什么没选别的”。真正定位原因必须用两个工具:
-
EXPLAIN FORMAT=JSON:看query_cost、rows_for_plan、used_columns,尤其关注range_analysis下各索引的cost_for_plan对比 -
optimizer_trace:执行前开开关,执行后查INFORMATION_SCHEMA.OPTIMIZER_TRACE,重点看steps数组里的considered_execution_plans—— 你会发现优化器其实试了idx_source_id,但算出成本比走PRIMARY高一截 - 别信
USE INDEX或FORCE INDEX的临时效果,它只是绕过成本计算,不解决底层代价失衡问题
低区分度字段建索引不是错,但单列无效、组合才可能起效
像 status、is_deleted、gender 这类只有几个固定值的字段,单独建索引基本等于白建——CARDINALITY 天然接近 1,优化器永远觉得“扫索引不如扫表”。但这不意味着完全不能用。
- 它们适合做联合索引的**后缀列**,比如
(create_time, status),靠前导列create_time提供高区分度,status帮助快速过滤子集 - 如果查询高频带
WHERE status = ? AND update_time > ?,那(status, update_time)就可能被用上——但前提是update_time本身区分度够高 - 永远优先验证:用
EXPLAIN FORMAT=JSON看真实rows_for_plan,而不是靠经验猜“应该能走”
真正容易被忽略的是:优化器的成本模型依赖磁盘 I/O 特性,而 SSD 和 HDD 下的随机读代价差异极大;同一句 SQL 在不同硬件、不同 MySQL 版本(比如 9.6.0 的 cost_model 调整)下,执行计划可能完全不同。











