区分度低的字段放复合索引左边会拖慢查询,因其导致B+树首层分叉极少,后续列无法有效过滤;优化器判定筛选率低而弃用索引,引发全表扫描,即使右侧含高区分度字段也无法挽救。

为什么区分度低的字段放复合索引左边会拖慢查询
区分度低(比如 status 只有 0/1/2 三个值)的字段放在复合索引最左位置,会导致索引树第一层就分叉极少,后续节点几乎无法过滤数据。MySQL优化器一看“这索引筛不出几行”,可能直接弃用,回退到全表扫描——哪怕你后面跟了个高区分度的 user_id,也救不回来。
典型现象是 EXPLAIN 输出里 key 为空、rows 高得离谱、type=ALL 或 type=index,但实际业务查询只想要几百条记录。
- 区分度计算可快速估算:
SELECT COUNT(DISTINCT status) / COUNT(*) FROM orders;,结果 - 别被“这个字段总在 WHERE 里出现”误导——高频 ≠ 高效,关键是它能不能把数据量压下去
- 如果必须用低区分度字段做查询入口(如查所有
status = 1的订单),优先考虑分区表或物化视图,而不是硬塞进复合索引头部
如何重排复合索引字段顺序才真正生效
核心原则:把能最快缩小结果集的列放最左。不是“写在 SQL 里顺序靠前”,而是“实际过滤效果最强”的列优先。
例如原索引是 INDEX (status, user_id, created_at),但 user_id 是唯一键或高基数字段,而 status 只有 3 个值——应该改成 INDEX (user_id, status, created_at)。
- 重排后,
WHERE user_id = 123 AND status = 1能走索引;WHERE user_id = 123也能走;但WHERE status = 1依然不走——这反而是对的,本来就不该依赖它过滤 - 如果查询确实常以
status开头(如后台批量任务),且无法避免,就把status单独建一个索引,再配合user_id的单独索引,让优化器用index_merge(需确认 MySQL 版本支持且代价合理) - 执行
ANALYZE TABLE orders;更新统计信息,否则优化器可能还按旧分布做决策
用覆盖索引绕过低区分度字段的回表开销
即使复合索引开头字段区分度低,只要整个索引能覆盖查询所需全部字段,就能避免回表——这对 SELECT COUNT(*)、SELECT status, COUNT(*) GROUP BY status 这类聚合场景特别有效。
比如表 orders(status, user_id, amount),想统计各状态订单总额:SELECT status, SUM(amount) FROM orders GROUP BY status;
- 建索引
INDEX (status, amount)就够了,user_id根本不需要参与——因为查询没要它 - 注意:
GROUP BY字段必须是索引最左列,否则无法利用索引排序,Using filesort会冒出来 - 验证是否覆盖:
EXPLAIN的Extra列出现Using index,而不是Using where; Using index(后者说明仍需回表过滤)
什么时候该放弃复合索引,改用其他方案
当低区分度字段(如 is_deleted、tenant_id)既是高频条件、又无法前置,且数据倾斜严重(比如 95% 行都是 is_deleted = 0),强行塞进复合索引只会让索引变大、写入变慢、缓存压力上升。
- 考虑物理拆分:把已删除数据归档到
orders_archive表,主表只留活跃数据,is_deleted字段自然消失 - 用生成列 + 函数索引(MySQL 5.7+):
ALTER TABLE orders ADD COLUMN status_active TINYINT GENERATED ALWAYS AS (IF(status = 1, 1, NULL)) STORED;,再给status_active建索引——把稀疏值转成可索引的非空子集 - 对超大数据集,评估是否上 ClickHouse 做分析层,MySQL 只负责事务,别让它扛不该扛的过滤压力
真正难的不是建索引,是判断哪条查询值得索引、哪条该从源头重构。低区分度字段就像索引里的“水货股东”——挂名不干活,还占坑。识别它、隔离它、必要时踢出去,比反复调序更治本。



















