distinct会创建临时表是因为无法利用索引有序性跳过重复值,需在内存或磁盘构建临时表进行哈希或排序去重;典型表现为explain中出现using temporary; using filesort。

为什么 DISTINCT 会创建临时表
MySQL 执行 DISTINCT 时,若无法利用索引的有序性跳过重复值,就会在内存或磁盘中构建临时表(Using temporary),再对整行做哈希或排序去重。这在大表、多列、无索引场景下非常常见。典型表现是 EXPLAIN 输出中 Extra 字段出现 Using temporary; Using filesort。
联合索引必须按查询字段顺序建立
单列 DISTINCT col1 可用 INDEX(col1);但多列如 SELECT DISTINCT col1, col2 FROM t,必须建 INDEX(col1, col2),不能反过来。原因在于:MySQL 只能利用索引最左前缀匹配 WHERE 条件,同时依赖索引项天然有序的特性来“流式跳重”——即遍历索引时,只要下一个 (col1, col2) 组合和上一个不同,就直接取值,无需缓存整行。
- 错误示例:
INDEX(col2, col1)对DISTINCT col1, col2几乎无效 - 带 WHERE 时更关键:比如
SELECT DISTINCT col1, col2 FROM t WHERE col3 = 'x',理想索引是INDEX(col3, col1, col2) - 如果
col1选择性极低(如只有 3 个值),即使建了索引,优化器也可能放弃使用
覆盖索引让 DISTINCT 完全不回表
当索引包含 SELECT 中所有字段,且这些字段又参与去重逻辑时,MySQL 就能纯靠索引页完成整个操作,彻底避免访问数据行(即“回表”)。这是性能飞跃的关键。
- 例如:表有
id,status,user_id,create_time,查询SELECT DISTINCT user_id FROM t WHERE status = 1,建INDEX(status, user_id)即可覆盖 - 别加多余字段:
INDEX(status, user_id, create_time)没坏处,但create_time不在SELECT列表里,就是冗余,还增大索引体积 -
NULL值不影响索引结构,但大量NULL会降低选择性,必要时可加NOT NULL约束
GROUP BY 在索引友好度上往往更可控
虽然 DISTINCT 和 GROUP BY 单列去重结果一致,但 MySQL 对 GROUP BY 的索引优化路径更成熟,尤其支持松散索引扫描(Loose Index Scan)——即只扫索引中每个分组的第一条记录,跳过中间重复项。而 DISTINCT 是否启用该优化,高度依赖版本和索引是否“严丝合缝”。
- 测试建议:对同一语义写两版,都跑
EXPLAIN,看谁没Using temporary - 示例对比:
SELECT DISTINCT a, b FROM tvsSELECT a, b FROM t GROUP BY a, b - 如果后续要加
COUNT(*)或HAVING,直接用GROUP BY更自然,不用改结构
真正卡住性能的,往往不是 DISTINCT 本身,而是它暴露了索引缺失或设计偏差。每次看到 Using temporary,先检查 WHERE 条件字段 + SELECT 字段是否被一个联合索引完整覆盖,而不是急着调大 sort_buffer_size。











