根本原因是低区分度列导致b+树退化为宽浅结构,索引查找变成顺序扫描大量重复值,回表开销大且无法跳过无效数据;cardinality为采样估算值,易失真,真实区分度低于5%时优化器通常弃用索引。

MySQL 在低区分度列(比如 status 只有 0/1,gender 只有 'M'/'F')上建索引,不仅不加速查询,反而可能拖慢整体性能——根本原因不在“能不能用”,而在 B+Tree 节点查找时的磁盘 IO 效率崩塌。
低区分度列导致 B+Tree 查找退化为“顺序扫描”
联合索引或单列索引本质是 B+Tree,每个叶子节点存的是「索引值 + 主键」。当字段区分度极低(例如 Cardinality 接近 1),大量行共享同一个索引值,B+Tree 就会变成“宽而浅”的结构:根节点下直接挂几十万条相同 status=1 的记录指针。
此时 MySQL 仍要从根开始查,但真正耗时的不是树高,而是:一旦定位到该索引值对应的第一条记录,就必须顺着叶子节点链表逐条比对其他 WHERE 条件(如 user_id = 12345),因为 B+Tree 并不保证这些记录在物理上连续或有序——它只按索引列排序,不按其他列排。
- 实际效果 ≈ 先扫出 80% 的行,再在内存里过滤剩下条件
- 如果该索引还被用于
ORDER BY status,排序毫无意义;用于GROUP BY status,分组也几乎无压缩 -
EXPLAIN中rows值虚高,key_len小,但Extra出现Using filesort或Using where频繁
为什么 SHOW INDEX FROM table 的 Cardinality 不可靠?
Cardinality 是采样估算值,不是精确统计。InnoDB 默认只采样 16 个叶子节点页来推算整棵树的去重数,当表数据倾斜严重(比如 95% 的 status=1,5% 的 status=0),采样结果可能完全失真。
更麻烦的是:这个值不会自动刷新。即使你大批量更新了 status,Cardinality 仍可能停留在旧值,导致优化器误判索引有效,继续走错执行计划。
- 手动触发更新:
ANALYZE TABLE your_table; - 但即便更新后,若真实区分度仍低于 5%,MySQL 通常仍会放弃使用该索引(尤其配合范围查询时)
- 验证方式不是看
Cardinality绝对值,而是执行SELECT COUNT(DISTINCT status) / COUNT(*) FROM your_table;算真实区分度
哪些场景下低区分度列“意外有用”?
不是绝对不能建,而是必须满足特定前提——否则就是给优化器埋雷。
- 该列是联合索引的**最左前缀**,且后续列区分度足够高(如
(status, create_time, user_id),其中create_time是时间戳、user_id是主键) - 查询中同时存在该列的等值条件和后续列的范围条件(如
WHERE status = 1 AND create_time > '2025-01-01'),这时 B+Tree 能先切出status=1子树,再在子树内高效走范围 - 该列被用于覆盖索引(
SELECT status FROM t WHERE status = 1),无需回表,IO 完全落在索引页内 - 注意:如果写成
WHERE status IN (0,1),哪怕只有两个值,也会让索引失效——IN 列表过长或含多个低区分值,优化器大概率转为全表扫描
B+Tree 不是魔法盒,它只忠实地按索引定义排序并组织数据。低区分度列的问题不在“建不建”,而在“建了之后,MySQL 还有没有机会跳过无效数据”。多数时候,它只是把随机 IO 换成了更密集的顺序 IO,而且回表成本更高——这点很容易被 EXPLAIN 的 type=ref 假象掩盖。











