mysql执行器对distinct去重并非流式跳过重复值,而是先收集所有候选行,再通过哈希表或排序结构统一判重;即使有索引,若未触发松散索引扫描,仍需临时表,覆盖索引仅避免回表,无法消除using temporary。

MySQL执行器如何判断重复值
DISTINCT不是靠扫描时“跳过相同值”实现的,而是先收集所有候选行,再统一去重。执行器内部会构建一个临时结构(通常是哈希表或排序树)来存放已见过的值组合。比如SELECT DISTINCT a, b FROM t,每读一行就用(a,b)作为键插入哈希表;如果键已存在,这行就被丢弃。
关键点在于:这个过程发生在数据读取之后、结果返回之前,不是流式过滤。所以即使只查一列且该列有索引,只要没触发松散索引扫描(Loose Index Scan),仍要先把所有匹配行拉出来再比对。
什么时候用哈希表,什么时候排序
哈希表适用于字段少、重复率高、内存充足的情况;排序则在哈希内存超限时自动 fallback,或当ORDER BY字段与DISTINCT字段不一致时强制启用——这时EXPLAIN会同时显示Using temporary和Using filesort。
- 单列去重且该列是复合索引最左前缀 → 可能走松散索引扫描,不建临时结构
- 多列去重(如
SELECT DISTINCT a,b)→ 等价于GROUP BY a,b,优化器统一按分组逻辑处理 -
NULL值被当作相等处理:多个NULL只保留一个,哈希表里NULL算作一个有效键
临时表为什么无法绕过
即使用了覆盖索引(所有SELECT DISTINCT字段都在索引里),也避免不了Using temporary。覆盖索引只省掉回表,但去重本身仍需内存或磁盘临时结构来判重。
常见误判:看到key列有索引名,就以为没走临时表。其实只要Extra里有Using temporary,说明去重阶段仍在用临时结构。
- 有索引 ≠ 无临时表,只是可能减少扫描行数
- 低基数字段(如状态枚举)更容易触发松散索引扫描,高基数字段(如邮箱)基本必走临时表
- MySQL 8.0.13+ 的 SKIP SCAN 对
DISTINCT无直接优化作用
执行计划里Using temporary意味着什么
它代表去重必须依赖临时结构,这是DISTINCT的硬性开销,不是bug也不是配置问题。真正要盯的是是否能消除Using filesort——后者往往比前者更伤性能。
例如:SELECT DISTINCT city FROM users ORDER BY create_time,因为ORDER BY字段不在DISTINCT列中,MySQL必须先取全量再排序去重,无法利用索引加速。
- 若只需去重,去掉
ORDER BY或改用ORDER BY city可避免Using filesort -
type=ALL+Using temporary是典型全表扫描+内存去重组合,优先加索引 -
GROUP BY替换DISTINCT通常不会改变执行计划,两者在优化器层面常被等价重写











