MySQL DISTINCT 必须排序才能去重,因其默认采用 filesort + 临时表方式:先取出所有匹配行,再排序,最后线性扫描相邻行比对重复;无索引时必全表扫描,大字段或表达式更导致索引失效。

MySQL DISTINCT 为什么必须排序才能去重
DISTINCT 不是简单地“扔掉重复行”,而是依赖排序或哈希机制识别唯一性。MySQL 默认采用 filesort + 临时表(Using temporary; Using filesort)方式实现:先将所有匹配行取出,再排序,最后线性扫描相邻行比对是否重复。数据量越大,排序 I/O 和内存开销越明显——尤其当 sort_buffer_size 不足时,会退化为磁盘排序,性能断崖式下跌。
没有索引时 DISTINCT 必然全表扫描
如果 DISTINCT col1, col2 涉及的字段没索引,MySQL 就无法跳过重复值判断逻辑,只能逐行读取整张表。即使只查两列,只要没覆盖索引,仍要回表取数据(除非用 SELECT DISTINCT col1, col2 FROM t 且这两列本身在聚簇索引中),而大数据量下回表本身就是随机 I/O 灾难。
- 单列去重:至少建
INDEX(col1) - 多列去重:必须建联合索引,顺序严格匹配
SELECT DISTINCT col1, col2中字段顺序,如INDEX(col1, col2) - 带 WHERE 条件时:把过滤字段前置,例如
WHERE status = 1 AND deleted = 0,索引应为INDEX(status, deleted, col1, col2)
GROUP BY 在某些场景下能绕过排序
MySQL 5.7+ 对 GROUP BY 支持松散索引扫描(Loose Index Scan),只要索引能覆盖分组字段且顺序一致,就能直接跳着读索引节点,避免全排序。而 DISTINCT 没有这个优化路径,即使语义等价,执行计划也可能更差。
例如:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
SELECT DISTINCT city FROM locations WHERE status = 1;
和
SELECT city FROM locations WHERE status = 1 GROUP BY city;
两者结果相同,但后者更容易命中 INDEX(status, city) 的松散扫描,Extra 字段可能显示 Using index for group-by,前者则常是 Using temporary; Using filesort。
大字段或表达式会让 DISTINCT 彻底失效
一旦 DISTINCT 包含 TEXT、长 VARCHAR 或函数(如 DISTINCT UPPER(name)),索引就无法使用,MySQL 只能走临时表 + 全字段比对。这时不仅慢,还容易触发 max_heap_table_size 或 tmp_table_size 限制,强制落盘。
- 避免对非短字符串字段直接去重;必要时加哈希列,如
content_hash CHAR(32),并建索引 - 不要写
DISTINCT *—— 这等于对所有字段组合去重,包括主键,实际永远无重复,纯属浪费 - 确认业务是否真需要数据库层去重:很多重复来自 JOIN 膨胀,应优先改写 JOIN 逻辑或加
STRAIGHT_JOIN
真正卡住 DISTINCT 性能的,往往不是语法本身,而是索引缺失、字段类型失配、或误以为“去重”是轻量操作。跑一次 EXPLAIN,盯着 type 和 Extra 看,比调优参数实在得多。

















