GROUP BY去重通常不比DISTINCT更快,甚至更慢——除非需聚合计算或多字段组合去重;DISTINCT语义更直接、优化器更易识别,而GROUP BY易触发Using temporary或Using filesort,尤其无索引时。

GROUP BY 去重是否比 DISTINCT 更快?
不一定,甚至通常更慢——除非你同时需要聚合计算或按多字段组合去重。DISTINCT 是语义最直接、优化器最易识别的去重方式;而 GROUP BY 会强制触发分组哈希或排序(取决于执行计划),尤其在没索引时容易走 Using temporary; Using filesort。
验证方法很简单:EXPLAIN SELECT DISTINCT user_id FROM orders; 和 EXPLAIN SELECT user_id FROM orders GROUP BY user_id; 对比两者的 Extra 列。若都显示 Using index,性能接近;一旦出现 Using temporary,GROUP BY 就已掉队。
- 只取唯一值 → 优先用
DISTINCT - 要统计每组数量、取最大时间、或保留某条完整记录 → 才轮到
GROUP BY - 复合去重(如
(user_id, product_id))且后续要关联其他字段 →GROUP BY+ 聚合函数是刚需
怎么用 GROUP BY 留下每组“最新一条”完整记录?
这是最常踩坑的场景:只写 SELECT * FROM t GROUP BY a, b 在 MySQL 5.7+ 严格模式下直接报错,因为非聚合字段未出现在 GROUP BY 子句中;即使旧版本能跑,返回哪一行完全不可控(不是最新,也不是最小 id)。
正确做法是先用子查询定位目标行 ID,再回表取全字段:
SELECT t1.* FROM orders t1 INNER JOIN ( SELECT user_id, MAX(create_time) AS max_time FROM orders GROUP BY user_id ) t2 ON t1.user_id = t2.user_id AND t1.create_time = t2.max_time;
注意点:
-
MAX(create_time)必须和user_id同时出现在子查询的GROUP BY中 - 如果
create_time有重复,可能匹配多行 → 改用MAX(id)或加ORDER BY create_time DESC, id DESC LIMIT 1配合窗口函数 - 务必给
(user_id, create_time)建联合索引,否则子查询会扫全表
大表去重时 GROUP BY 的性能陷阱
百万级以上数据跑 GROUP BY,最容易卡在三件事上:没索引、NULL 值干扰、内存不足导致落盘。
典型症状是查询耗时陡增、CPU 拉满、临时表暴涨。应对策略很具体:
- 去重字段必须建索引 —— 单列去重建单列索引,多字段组合去重建联合索引(顺序按
GROUP BY字段顺序) -
NULL值会被归为同一组,如果业务上NULL不代表相同含义,提前用COALESCE(col, 'N/A')处理 - 调大
sort_buffer_size和tmp_table_size(但别超过物理内存 20%),避免频繁写磁盘临时表 - 实在跑不动,拆成
WHERE id BETWEEN ? AND ?分批处理,每次GROUP BY限定 10 万行以内
GROUP BY 去重后还要删数据?小心主键冲突
查出重复项只是第一步,真正删数据时,DELETE ... JOIN 写法看似简洁,但极易误删或锁表太久。
安全删除的核心逻辑是:只删“非代表行”,即保留每个分组中 MIN(id) 的那条,其余全删:
DELETE o1 FROM orders o1 INNER JOIN orders o2 WHERE o1.user_id = o2.user_id AND o1.product_id = o2.product_id AND o1.id > o2.id;
关键约束:
- 必须确保
(user_id, product_id)是你要去重的维度,且id是自增主键 - 执行前加
LIMIT 1000测试,确认影响行数合理 - 生产环境务必在低峰期操作,并开启事务:
BEGIN; DELETE ... ; SELECT ROW_COUNT(); ROLLBACK;先验逻辑 - 千万避免
DELETE FROM orders WHERE id NOT IN (SELECT MIN(id) FROM orders GROUP BY user_id, product_id)—— 子查询里不能引用外层表,MySQL 会报错
真正难的从来不是语法,而是判断哪条该留——时间戳、ID、业务权重,选哪个做“代表”得看字段是否可靠、是否覆盖所有边界 case,比如 create_time 为空怎么办,id 是否连续等。这些细节一漏,删完才发现留错了行。

















