Cardinality低的JOIN字段会导致索引失效,因优化器依据其估算选择性,若远低于总行数(如100万行中仅几十),则倾向全表扫描;复合索引中低Cardinality字段置左会破坏最左前缀匹配,且JOIN时易引发驱动表误判、回表成本过高而弃用索引。

Cardinality低的JOIN字段会让索引失效
MySQL优化器判断是否使用索引,核心依据是Cardinality——它估算该列不同值的数量。如果Cardinality远小于表总行数(比如100万行表,Cardinality只有几十),说明该字段重复度极高(如status、del_flag),即使建了索引,优化器也大概率跳过它,直接走全表扫描。
典型表现:EXPLAIN中type为ALL或index,key为NULL;执行计划里本该走ref却变成range甚至ALL。
-
SHOW INDEX FROM your_table查Cardinality列,不是看有没有索引,而是看这个值是否足够高 - 真实区分度低于5%(即
Selectivity = Cardinality / Total_Rows < 0.05)时,单列索引基本无效 - 大批量导入/删除后必须执行
ANALYZE TABLE your_table,否则统计信息过期,Cardinality失真
复合索引里低Cardinality字段放最左会破坏整个索引
把status这种低区分度字段放在复合索引最左侧(如(status, user_id)),会导致最左前缀原则失效:对user_id单独查询时无法命中该索引,等于白建。
更隐蔽的问题是JOIN时用到这个复合索引,但驱动表过滤后status只剩1–2个值,被驱动表实际要匹配的user_id范围极大,优化器预估回表成本过高,直接弃用索引。
- 复合索引顺序应按
Selectivity从高到低排列,比如(user_id, status)比反过来合理得多 - 用
SELECT COUNT(DISTINCT col)/COUNT(*) AS selectivity FROM tbl手动验证字段选择性 - 不要依赖“状态字段+业务字段”这种惯性组合,先算
selectivity再决定是否合并建索引
JOIN慢不一定是没索引,而是驱动表选错了
优化器选驱动表的依据不是“谁小”,而是“WHERE条件过滤后谁的结果集更小”。如果WHERE没生效,或者过滤字段本身Cardinality极低(比如WHERE status IN (0,1)),那驱动表可能被误判为“大表”,导致嵌套循环里外层遍历次数爆炸。
例如:A表100万行,WHERE created > '2026-01-01'后剩80万行;B表50万行,WHERE type = 'order'后剩2万行——此时B才是真正的驱动表,但优化器若误信type的Cardinality失真(比如直方图不准),就可能选错。
- 用
EXPLAIN FORMAT=JSON看rows_examined_per_scan和filtered字段,确认实际过滤效果 - 避免在低
Cardinality字段上写IN或BETWEEN,它们容易让优化器低估返回行数 - 必要时用
STRAIGHT_JOIN强制指定驱动表,但前提是已验证过滤后的真实行数
Cardinality失真时,Hint不是万能解药
CARDINALITY hint(如/*+ cardinality(t 1000) */)能覆盖优化器对单表结果集的预估,但它只影响代价计算,不改变数据分布本身。如果底层字段就是低区分度,hint强行压低预估行数,反而可能导致哈希连接内存不足、临时磁盘溢出,或嵌套循环内层扫描次数失控。
真正卡住性能的,往往不是统计不准,而是建索引前根本没看Cardinality——hint只是补救手段,不是设计前提。
- hint适合紧急绕过错误执行计划,不适合长期替代索引设计
- Oracle中直方图(
FREQUENCY)能缓解低Cardinality字段的统计偏差,但MySQL 8.0+才支持类似功能,且需手动收集 - 比hint更稳的做法:拆分查询,先用高
Cardinality字段过滤出子集,再JOIN


















