distinct不走索引是因为其去重需先获取全部数据再排序/哈希,仅当字段有匹配索引且满足最左前缀、无函数操作时,才可能利用索引顺序读取实现松散扫描;否则触发全表扫描+临时表。

为什么 DISTINCT 不走索引?
绝大多数 DISTINCT 查询慢,不是语法写错,而是 MySQL 根本没用上索引——EXPLAIN 里 type 显示 ALL,Extra 出现 Using temporary,这就是典型信号。
根本原因:DISTINCT 去重逻辑只有在能“直接从索引中顺序读出唯一值”时,才跳过临时表。否则它就得先把所有行捞出来,再内存/磁盘去重。
- 单字段去重(如
SELECT DISTINCT city FROM users)→ 必须有INDEX(city),且不能在该字段上做函数操作(如DISTINCT UPPER(city)) - 多字段去重(如
SELECT DISTINCT dept, city FROM users)→ 必须建联合索引INDEX(dept, city),顺序必须和SELECT中字段一致 - 如果 WHERE 条件也参与过滤(如
WHERE status = 1),索引应扩展为INDEX(status, dept, city),满足最左前缀原则
如何验证索引是否生效?
别猜,用 EXPLAIN 看真实行为。重点关注三处:
-
type列:理想是index(索引全扫描)或range;出现ALL就是全表扫描 -
key列:是否命中你建的索引名(如idx_status_dept_city) -
Extra列:出现Using temporary或Using filesort,说明去重/排序仍依赖临时资源
例如:
EXPLAIN SELECT DISTINCT dept, city FROM users WHERE status = 1;
若返回 key: NULL 且 Extra: Using temporary,说明索引缺失或顺序不对。
GROUP BY 能替代 DISTINCT 吗?
可以,而且在某些场景下更优——特别是当索引支持松散索引扫描(Loose Index Scan)时,MySQL 可跳过大量重复值,直接取每组第一个值。
- 适用前提:查询字段完全匹配联合索引前缀,且无额外计算或非确定性函数
- 示例:已有
INDEX(status, dept, city),则SELECT dept, city FROM users WHERE status = 1 GROUP BY dept, city可能比DISTINCT快数倍 - 注意:
GROUP BY语义更重,若后续要加聚合(如COUNT(*)),它天然支持;但若只是纯去重,两者结果等价
不过别盲目替换:5.7+ 版本优化较好,8.0 默认更激进启用松散扫描,但老版本或复杂 WHERE 下未必生效。
容易被忽略的覆盖与回表陷阱
即使 DISTINCT 字段本身有索引,只要 SELECT 里混入了没包含在索引里的列,就会触发回表——这时索引只用来去重,数据还得回主键索引查一遍,I/O 暴涨。
- 错误写法:
SELECT DISTINCT city FROM addresses WHERE province = 'ZJ'→ 若只有INDEX(city),但province无索引,仍可能全表扫描 - 正确写法:
SELECT DISTINCT city FROM addresses WHERE province = 'ZJ'配合INDEX(province, city) - 更优写法:如果业务只需要
city,就别在同个查询里 JOIN 其他大表或 SELECT 大字段;去重和关联拆成两步
真正难的不是建索引,而是让整个查询路径(WHERE 过滤 → DISTINCT 去重 → 结果输出)全程落在同一棵索引树上。少一个字段对齐,就可能多一次全表扫描。











