根本原因是索引基数(cardinality)过低,导致优化器预估索引过滤效率差而弃用;可用 SHOW INDEX FROM table_name 查看 Cardinality 值,若远小于总行数(如性别字段仅2个值),则索引实际无效。

为什么 EXPLAIN 显示用了索引但查询还是慢?
根本原因常是索引基数(cardinality)太低——比如在性别字段建了索引,只有 'male' 和 'female' 两个值,MySQL 估算该索引过滤效率极差,优化器大概率会弃用它,甚至走全表扫描。这不是 bug,是成本估算的合理结果。
- 用
SHOW INDEX FROM table_name查看Cardinality列,若远小于表总行数(比如 - 基数不是实时更新的,
ANALYZE TABLE可触发重新采样,但对大表有锁开销,别频繁执行 - 低基数字段单独建索引几乎没意义,除非搭配
WHERE + ORDER BY或用于覆盖索引场景
哪些字段容易踩“低基数索引”坑?
典型陷阱集中在枚举类、状态位、布尔值、固定分类字段上,比如 status、is_deleted、type、gender。它们天然离散度差,索引选择性(selectivity)接近 0。
- 判断选择性:用
SELECT COUNT(DISTINCT col) / COUNT(*) FROM table_name,结果 - 别迷信“加了索引就快”,MySQL 5.7+ 对低选择性索引更激进地跳过,8.0 还可能因直方图统计更准而进一步降低使用意愿
- 如果必须按这类字段查,优先考虑组合索引:把低基数字段放在联合索引靠后位置,前面接高基数字段(如
(user_id, status))
如何让 MySQL “愿意用”低基数字段上的索引?
强制使用(FORCE INDEX)只是掩耳盗铃,真正可行的是调整数据分布或访问模式。硬要让它用,得满足两个条件之一:索引能覆盖查询,或者 WHERE 条件本身足够稀疏(比如 status = 'processing' 实际只占 0.001% 行)。
- 检查是否走了覆盖索引:
EXPLAIN的Extra列出现Using index,说明无需回表,即使基数低也可能被选中 - 避免
SELECT *,只查索引包含的列,尤其是组合索引中已包含低基数字段和高频查询字段时 - 如果业务允许,把低基数字段转为高基数:比如
status拆成status_v2+ 时间戳前缀,或用ENUM替代VARCHAR(虽不提升基数,但减小索引体积,间接影响优化器决策)
复合索引里低基数字段放前面还是后面?
放后面。B+ 树索引按最左前缀匹配,如果把 status 放第一位,等于先按两个值分叉,后续字段的区分能力被大幅稀释,等效于建了个“半废索引”。
- 正确顺序示例:
INDEX (created_at, status)—— 时间戳基数高,能快速定位范围,status在范围内再过滤 - 反例:
INDEX (status, created_at)—— 查询WHERE status = 'done'时,仍需扫描大量created_at值,且无法利用created_at的有序性做范围跳过 - 注意:如果查询中
status是等值,created_at是范围(如BETWEEN),那status必须在前;但这是特例,需结合具体WHERE条件重估,不能一概而论
基数低本身不是问题,问题在于误以为它能当主力索引用。真正关键的是理解优化器怎么算成本——它看的不是“有没有索引”,而是“用这个索引到底省多少随机 IO”。很多线上慢查,删掉一个低基数单列索引,性能反而提升,就是因为清除了干扰项。


















