优化器因CARDINALITY失真导致成本估算错误:Selectivity = CARDINALITY / total_rows,若status真实有2值但采样报1,则误判选择性极低而弃用索引,key变NULL、type变ALL。

Cardinality不准时,优化器怎么“算错账”
MySQL优化器不是靠直觉选索引,而是用成本模型估算:走哪个索引预计I/O最少、CPU开销最低。而CARDINALITY是它算“选择性”的核心输入——Selectivity = CARDINALITY / total_rows。一旦这个值严重失真,整个成本估算就塌了半边。
比如status字段真实有 2 个值('active'/'inactive'),但采样只扫到全是'active'的页,CARDINALITY被报成 1。优化器一算:Selectivity ≈ 1 / 100w = 0.0001%,认为这索引几乎不过滤数据,不如全表扫描——于是key变NULL,type变ALL。
为什么20页采样在现实里经常不够用
默认innodb_stats_persistent_sample_pages = 20,对千万级表或倾斜数据就是杯水车薪。尤其以下场景误差会被放大:
- 数据分布倾斜:95% 行的
status = 'pending',但采样页恰好没抽到这批页,CARDINALITY虚高 - 联合索引前导列低基数:
INDEX(status, created_at)中status只有两个值,采样偏差直接让整个索引“看起来”无区分度 - 批量导入后未触发重采样:
innodb_stats_auto_recalc = ON但变更行数未达 10%,旧统计继续误导优化器
看到EXPLAIN中key为NULL,先别急着加FORCE INDEX
key为空不等于索引不存在,而是优化器“不信它”。排查顺序比干预更重要:
- 用
SHOW INDEX FROM t1查当前CARDINALITY,再用SELECT COUNT(DISTINCT status)手动核对真实值,偏差超30%就得动统计 - 确认
WHERE条件没隐式转换:status VARCHAR(10)却写WHERE status = 0,MySQL会在列上加转换函数,索引直接失效 - 检查是否用了函数:
WHERE DATE(created_at) = '2024-01-01'→ 索引无法走,CARDINALITY再准也没用 - 执行
ANALYZE TABLE t1后仍无效?看innodb_stats_persistent是否为ON,否则统计重启即丢
调高sample_pages和建直方图的实际效果边界
把innodb_stats_persistent_sample_pages从20提到100~200确实能收窄估算波动,但不是万能解:
- 仅对
IN、=、BETWEEN等精确/范围查询有效;对LIKE '%abc'或函数条件完全无效 - 直方图(
ANALYZE TABLE t1 UPDATE HISTOGRAM ON status)只在MySQL 8.0+支持,且对低基数字段(如状态码)提升明显,对高基数主键意义不大 - 大表上调高采样页数会延长
ANALYZE TABLE时间,并在执行期间持有意向锁,必须避开写高峰
最易被忽略的一点:Cardinality不准本身不报错、不记日志、不触发告警。它安静地让优化器每天选错一次索引,直到某次分页查询突然卡住,你才意识到——那条一直“可用”的索引,其实早被悄悄弃用了。


















