MySQL优化器选错索引不是bug,而是CBO在统计信息不准、采样偏差或成本模型静态化时的合理误判;本质是预估成本与真实I/O、CPU、缓存命中率等现实因素的偏差。

MySQL优化器选错索引不是 bug,而是 CBO(基于成本的优化器)在信息不完备时的合理误判——它算出来的“代价最低”路径,和你实际看到的“执行最慢”,本质是估算和现实的偏差。
统计信息不准导致 rows 估算严重失真
InnoDB 的索引基数(Cardinality)靠采样估算,不是全量扫描。默认每 20 次数据变更就触发一次重采样(innodb_stats_persistent = ON 时),但采样页数固定、分布不均时,估算值可能差一个数量级。
- 比如
show index from orders显示idx_user_id的Cardinality是 100,实际是 10 万——优化器就认为这个索引区分度极低,直接弃用 - 大表批量 delete + insert 后(如日志归档场景),旧统计没刷新,新数据分布已变,但优化器还在按“老地图”导航
-
ANALYZE TABLE orders能强制刷新,但它本身要加 MDL 锁、耗时随表大小增长,线上不敢随便跑
优化器只看“预估成本”,不看真实 I/O 和 CPU
它把“回表次数 × 主键索引深度”、“排序是否需要 filesort”、“临时表大小”都折算成抽象“cost”,但这些换算系数是静态的,无法反映 SSD 和 HDD 差异、buffer pool 命中率、甚至当前系统负载。
- 典型例子:
SELECT * FROM t WHERE a BETWEEN 10000 AND 20000 ORDER BY b LIMIT 1,优化器觉得走idx_b虽然要扫 5 万行,但省了排序;实际 buffer pool 里idx_a数据页全热,回表快得多 - 当
sort_buffer_size不足时,filesort 成本飙升,但优化器仍按默认值估算 -
EXPLAIN里的rows是单次访问的估算值,不包含重复读取、锁等待、刷脏页等真实开销
FORCE INDEX 不是银弹,它把“估算风险”转成了“硬依赖风险”
写 FORCE INDEX (idx_user_status) 确实能堵死错误路径,但代价是让 SQL 和索引名强绑定,且完全放弃自适应能力。
- 索引被
DROP INDEX idx_user_status ON orders或重命名后,SQL 直接报错ERROR 1176 (HY000): Key 'idx_user_status' doesn't exist in table 'orders',不会降级为 USE INDEX 或全表扫描 - 数据量从 100 万涨到 1 亿后,原来高选择性的
WHERE user_id = ?可能因 user_id 分布倾斜(如 80% 是测试账号),变成低效范围扫描,但FORCE INDEX依然死守 - MyBatis 或 JDBC 拼 SQL 时若没做索引存在性校验,上线即故障
真正难处理的不是“怎么强制用索引”,而是“怎么让优化器下次别再算错”——这要求你既要看懂 EXPLAIN 里每个字段的真实含义,也要清楚 INFORMATION_SCHEMA.STATISTICS 里那些采样值是怎么来的。一个 rows 数字背后,是磁盘、内存、CPU、统计模型四层博弈的结果。


















