mysql优化器因采样偏差导致统计信息失真,如status=1占95%时cardinality被严重低估,误判索引成本高于全表扫描;show index显示的cardinality值除以总行数即为估算选择率。

优化器依赖的统计信息本身就被倾斜数据污染
MySQL 优化器不做真实扫描,只靠 ANALYZE TABLE 采样估算成本。当某值(如 status = 1)占全表 95% 行时,采样很可能集中在这部分,导致 CARDINALITY 被严重低估——比如千万级表,CARDINALITY 算出来才几万,优化器就“信以为真”,认为走索引要扫几十万行,不如全表顺序读。
这不是优化器蠢,是它被输入骗了。你喂给它的统计数字,已经不能反映真实分布。
-
SHOW INDEX FROM t查出的CARDINALITY值 ÷ 总行数 - 批量导入/删除后没跑
ANALYZE TABLE,统计信息就一定过期 - 直方图(MySQL 8.0+)默认不参与有索引列的估算,即使你建了也没用
WHERE 条件一写错,索引就从“加速器”变“放大器”
低选择性字段(如 status)一旦放在联合索引最左,MySQL 就会先按它筛一遍——结果匹配出 50 万行,再逐条回表查 user_id 和 created_at。这时 type 显示 ref,看着像走了索引,实际 I/O 开销比全表扫描还高。
真正致命的是:优化器只算“索引扫描行数”,不算“回表次数”。它以为扫完索引就完事了,没把磁盘随机读的代价算足。
- 用
SELECT COUNT(*) FROM t WHERE status = 'pending'对比总行数,确认倾斜程度 - 监控
Handler_read_rnd_next暴涨 +Handler_read_next平稳,就是回表放大的铁证 -
WHERE status = 1 AND user_id = 123这种写法,MySQL 会自动重排匹配顺序,但前提是user_id在索引里且非最右
函数、隐式转换、OR 条件让优化器彻底“失明”
哪怕数据分布正常,只要 WHERE 里出现 UPPER(status)、user_id = '123'(INT 字段配字符串)、或 status = 1 OR type = 'vip',索引下推就会中断。优化器要么放弃索引,要么误判成本——因为函数无法走 B+ 树有序查找,隐式转换导致类型不一致,OR 则可能触发索引合并逻辑,但代价模型很难准估。
-
WHERE YEAR(created_at) = 2023→ 改成created_at >= '2023-01-01' AND created_at -
WHERE user_id = '123'→ 改成user_id = 123,避免字符串转数字 -
OR条件尽量拆成UNION ALL,让每个分支能独立走索引
别只盯着 EXPLAIN 的 rows,要看 Handler_read_rnd_next
rows 是优化器拍的脑袋,Handler_read_rnd_next 才是你硬盘在喘的气。很多慢查的 rows 显示几千,实际 Handler_read_rnd_next 高达百万级——说明它在用索引加速全表扫描,不是在精准定位。
这种误差在数据倾斜场景下会被指数级放大:一个本该过滤掉 99% 数据的条件,因为基数误估,反而让优化器选了一条“看起来快、实际更慢”的路。
最麻烦的是,你没法光靠改 SQL 或加索引解决;得先验证统计是否可信,再看查询是否触发了回表放大,最后才轮到调索引结构。漏掉任何一环,优化都只是隔靴搔痒。











