MySQL优化器因CARDINALITY极低而放弃索引,是因预估回表随机I/O代价远超顺序扫描聚簇索引;如status列仅3个值、500万行时,优化器估算需回表约166万次,而顺序扫描几页主键即可命中(尤其带LIMIT 1时)。

MySQL优化器面对大量重复值时选择全表扫描,不是它“不认索引”,而是算出来走索引更贵——尤其是当索引列的 CARDINALITY 极低时,优化器会预估你要回表几十万次,随机 I/O 总开销远超顺序扫几页主键。
为什么低区分度字段会让优化器放弃索引
区分度(CARDINALITY)是优化器判断“这个索引能筛掉多少行”的核心依据。如果一列只有 3 个不同值(比如 is_deleted、gender),而表有 500 万行,CARDINALITY 显示为 3,优化器就会认为:WHERE is_deleted = 0 要匹配约 166 万行,每行都得回表取数据 → 随机读 166 万次。相比之下,顺序扫描聚簇索引可能只读几百页就命中目标(尤其带 LIMIT 1 时)。
常见低区分度字段包括:
-
tinyint类型的状态字段(如status只有 'pending'/'done'/'failed') -
char(1)的性别、是否删除标记 - 前缀高度重复的
varchar(如source_id全是'1814613774586351713_xxx'开头)
如何验证 CARDINALITY 是否失真
别只看 EXPLAIN 的 key 为空,先查真实基数:
- 运行
SHOW INDEX FROM mapping_filter_record WHERE Key_name = 'idx_source_id',看CARDINALITY列 - 对比实际不同值数量:
SELECT COUNT(DISTINCT source_id) FROM mapping_filter_record - 如果
CARDINALITY / TABLE_ROWS (即低于 1%),说明该索引基本无过滤价值 - 如果
CARDINALITY = 1但你知道它至少有几十个不同值,说明统计严重过期
强制刷新统计:ANALYZE TABLE mapping_filter_record(注意:大表会加 MDL 锁,避开业务高峰)
低区分度字段建索引还有没有意义
单列建索引通常没用,但组合起来可能有效:
- 把低区分度字段放在联合索引**后位**,例如
INDEX(user_id, status)——user_id提供高区分度,status用于二次过滤 - 避免单独为
is_deleted建索引;但如果查询总是WHERE user_id = ? AND is_deleted = 0,那(user_id, is_deleted)就很合理 - 如果必须高频查低区分度字段,考虑分区(如按
statusLIST 分区)或冗余字段(如加deleted_at时间戳,建索引)
别信 USE INDEX,它只是绕过成本计算
USE INDEX (idx_status) 强制走索引,但不会改变底层代价。实测中常出现:
- 原本全表扫描 200ms,强制走索引后变成 1.2s(大量随机回表)
- 执行计划显示
type=ref,但rows和filtered暴露真实问题(如rows=892345,filtered=10.00) - 真正有效的干预是:更新统计、调整索引结构、改写 SQL(如用覆盖索引避免回表)
最易被忽略的一点:优化器看到 LIMIT 1 时,会优先选“最早能停下来的路径”。顺序扫主键一旦命中就结束;而索引+回表必须先把所有匹配主键捞出来,再逐条回表比对——哪怕你只要第 1 行。


















