MySQL优化器选择全表扫描(type=ALL)是因为成本估算显示索引+回表比全表扫描更贵,根本原因在于匹配行数多、索引区分度低或回表开销大,而非未建索引。

不是执行计划“更倾向”全表扫描,而是优化器在成本模型下算出来——走索引+回表,确实比直接扫一遍还贵。
EXPLAIN 显示 type=ALL 但有索引可用,根本原因是成本估算
MySQL 8.0 的优化器不会因为“存在索引”就用它。它会预估三条路径的成本:全表扫描、索引扫描+回表、覆盖索引扫描。当 rows 预估值高(比如匹配 80% 行)、索引区分度低(如 gender 只有两值)、或返回字段多导致回表开销大时,ALL 就成了“最省”的选择。
- 常见诱因:
WHERE status != 'done'(高匹配率)、WHERE city = 'Beijing'(城市分布极不均匀)、SELECT *+ 大字段(触发大量随机 I/O) - 别只看
possible_keys是否非空——关键看key是否真被选中,以及rows是否接近表总行数 - 用
SHOW INDEX FROM table_name查Cardinality,若某列唯一值占比
统计信息过期或直方图缺失,会让优化器“瞎算”
优化器依赖统计信息做成本判断。MySQL 8.0 默认开启直方图(HISTOGRAM),但只对显式创建的列生效;未创建直方图的列,仍按“均匀分布”假设估算——而真实数据常严重倾斜(比如 95% 订单状态是 'success')。
- 检查是否启用直方图:
SELECT * FROM INFORMATION_SCHEMA.COLUMN_STATISTICS WHERE TABLE_NAME = 'orders'; - 为高频过滤列手动创建直方图:
ANALYZE TABLE orders UPDATE HISTOGRAM ON status;(需 MySQL 8.0.19+) - 更新基础统计信息:
ANALYZE TABLE orders;—— 特别在大批量 INSERT/DELETE 后必须做 - 直方图不是万能的:它只改善单列过滤估算,对多列组合(如
WHERE a=1 AND b=2)仍依赖独立性假设
optimizer_trace 是唯一能看清“它到底怎么想”的方式
靠猜或调参数不如直接看优化器内部决策过程。MySQL 8.0+ 的 optimizer_trace 输出里,cost_for_plan 和 considered_execution_plans 字段明确列出每条路径的成本构成。
- 开启并执行查询:
SET optimizer_trace="enabled=on"; SELECT ...; SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE; - 重点看:
"read_cost"(读取索引页成本)、"eval_cost"(逐行过滤开销)、"prefix_cost"(连接/排序附加成本) - 你会发现:哪怕
read_cost很低,但eval_cost因预估匹配行数太多而爆炸,总成本就反超了全表扫描 -
FORCE INDEX不解决根本问题——它绕过成本计算,但实际执行时该慢还是慢
复合索引设计不当,让 INDEX SKIP SCAN 也救不了场
即使你建了 (a, b, c) 索引,但查询只写 WHERE b = ?,MySQL 8.0 的 INDEX SKIP SCAN 也未必触发——它要求前导列 a 基数足够高(比如 COUNT(DISTINCT a) / COUNT(*) > 0.01),且不能含 NULL 主导、不能有范围条件。
- 验证是否启用:
EXPLAIN FORMAT=TRADITIONAL SELECT ...,看Extra是否含Using index skip scan - 跳过前导列 ≠ 跳过代价:SKIP SCAN 本质是按前导列每个值分段扫描,若前导列只有 3 个值,它就做 3 次等值索引查找;若前导列有 10 万不同值,那它就扫 10 万次——比全表扫描还糟
- 真正靠谱的做法:把高频等值过滤列放在联合索引最左;避免为“偶尔跳过最左列”的场景强行依赖 SKIP SCAN
真正卡住性能的,往往不是没索引,而是优化器基于错误数据做出了“合理但错误”的决定。查 optimizer_trace、更新统计信息、重审索引列顺序——这三步做完,type=ALL 才可能变成 ref 或 range。其他所有操作,都只是在掩盖这个事实。


















