MySQL优化器通过统计信息和代价模型估算全表扫描成本,以逻辑读页数和CPU开销为核心,而非实际执行扫描;EXPLAIN的rows仅是行数估计,不反映真实I/O,小表或高索引维护成本时全表扫描代价更低。

MySQL优化器怎么算全表扫描的代价
MySQL优化器不会真的去扫一遍表来测速度,而是靠统计信息和预设公式估算成本。核心依据是 handler::records_in_range 和 ha_statistics 提供的行数、索引深度、页大小等,再套入内部代价模型(比如 Cost_model_server 中的 read_cost 和 cpu_cost 权重)。
关键点在于:它把一次“逻辑读”(读一个数据页)当作基本单位,乘以预估要读的页数;再叠加 CPU 处理每行的开销。这个页数不是行数除以 innodb_page_size 简单换算——InnoDB 还要考虑 B+ 树层级、页内碎片、MVCC 版本链长度等隐含因素。
为什么 EXPLAIN 显示 rows 很小却还是走全表扫描
rows 只是优化器对「需要检查的行数」的粗略估计,不等于实际 I/O 量。真正影响决策的是「总代价」,而代价由两部分主导:I/O 成本(读页)和 CPU 成本(过滤、排序、计算)。当满足以下情况时,哪怕 rows 小,全表扫描也可能更便宜:
- 表非常小(比如
- 查询条件选择性差(如
WHERE status IN ('active', 'pending')),索引过滤后仍需读大量页,优化器发现全表扫描的页数更少 -
innodb_stats_persistent = OFF且表长期未ANALYZE TABLE,导致行数统计严重失真,rows值不可信 - 用了
ORDER BY+LIMIT,但没有覆盖索引,优化器判断排序成本太高,干脆全扫后用文件排序
optimizer_switch 里哪些开关会影响全表扫描决策
几个直接影响代价评估路径的开关:
-
condition_fanout_filter=on:开启后,优化器会尝试估算条件过滤后的扇出比,让rows更准——关掉它可能导致低估过滤效果,误判索引更优 -
use_index_extensions=on:允许使用索引扩展列(如主键自动附加到二级索引末尾),提升覆盖扫描可能性;关掉可能让本可走索引的查询退化为全表扫描 -
skip_scan=on:启用跳跃扫描(Skip Scan),对低基数前导列的复合索引更友好;关掉后,某些本可利用复合索引的场景被迫全表扫描 -
mrr=on和mrr_cost_based=on:影响多范围读(MRR)是否被选用;若关闭,范围查询可能因回表随机读代价高而放弃索引
修改前务必在测试库验证,SET SESSION optimizer_switch = '...' 比全局改更安全。
如何验证优化器是不是真觉得全表扫描便宜
最直接的方法是打开优化器追踪(Optimizer Trace),看它内部怎么权衡的:
SET optimizer_trace="enabled=on"; SELECT * FROM orders WHERE created_at > '2024-01-01'; SELECT * FROM information_schema.OPTIMIZER_TRACE\G SET optimizer_trace="enabled=off";
重点看 considered_execution_plans 里的 cost 字段,对比「使用某个索引」和「全表扫描」两条路径的 total_cost。注意:这个 cost 是相对值,单位是「微秒级等效开销」,不能直接换算成真实耗时,但可用于横向比较。
容易忽略的一点是:InnoDB 缓冲池命中率会动态影响实际 I/O 成本,但优化器完全不考虑这点——它永远按「所有页都要从磁盘读」来算。所以线上缓存充足时,全表扫描可能比预估快得多;而刚重启后,又可能慢得超预期。


















