必须用group by+having count()>1筛选重复字段组合,因where在分组前执行无法访问聚合结果;查单列重复写group by email having count()>1,查多列组合则group by user_id,order_date having count()>1;要获取所有重复原始行,需用窗口函数count() over(partition by ...)或子查询join。

用 GROUP BY + HAVING 找出重复的字段组合
直接上结论:重复数据本质是「某组字段值出现次数大于 1」,所以必须用 HAVING COUNT(*) > 1 过滤分组结果,而不是在 WHERE 里判断——WHERE 在分组前执行,无法访问聚合结果。
常见错误是写成 WHERE COUNT(*) > 1,这会报错 ERROR: aggregate functions are not allowed in WHERE。
- 只查单列重复:比如找重复的
email,写GROUP BY email HAVING COUNT(*) > 1 - 查多列组合重复:比如
(user_id, order_date)不该重复却重复了,就GROUP BY user_id, order_date HAVING COUNT(*) > 1 - 想看具体哪些行重复?得把原表和这个分组结果
JOIN,不能只靠GROUP BY输出汇总行
查出重复数据对应的所有原始记录(不止一行)
仅靠 GROUP BY 只能返回每组一条汇总(如 email 和计数),但排查异常通常需要看到所有重复的完整记录。这时得用窗口函数或子查询关联。
推荐用 COUNT(*) OVER (PARTITION BY ...):它不改变行数,只为每行打上所属分组的计数标签。
SELECT * FROM orders WHERE user_id IN ( SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 1 );
或者更直接(兼容性更好):
SELECT *
FROM (
SELECT *,
COUNT(*) OVER (PARTITION BY user_id) AS cnt
FROM orders
) t
WHERE t.cnt > 1;
- 窗口函数方案在 PostgreSQL / SQL Server / Oracle / BigQuery 中都支持;MySQL 8.0+ 也支持
- 子查询方案兼容性最广,但若重复字段有
NULL,要注意GROUP BY中NULL会被视为同一组——这是标准行为,不是 bug - 别用
DISTINCT或UNION替代,它们无法体现“重复几遍”,也丢失原始行上下文
注意 NULL 值在 GROUP BY 中的特殊行为
NULL 在 GROUP BY 中会被当作相同值归为一组,哪怕语义上它代表“未知”。这意味着 GROUP BY email 会把所有 email IS NULL 的行合并成一组,并计入 COUNT(*)。
- 如果业务上
NULL不算重复(例如未填写邮箱是正常状态),需提前过滤:WHERE email IS NOT NULL - 如果想单独检查
NULL是否过多,可加条件:GROUP BY email HAVING COUNT(*) > 1 OR (email IS NULL AND COUNT(*) > 5) - 某些场景下需用
COALESCE(email, '>')把NULL转为字符串再分组,但要小心字符串碰撞(比如真有邮箱叫>)
性能关键点:给 GROUP BY 字段建联合索引
当表很大时,GROUP BY a, b 没索引会导致全表扫描+临时文件排序,非常慢。优化核心是让数据库能用索引完成分组。
- 建索引优先按
GROUP BY字段顺序来:CREATE INDEX idx_user_order ON orders(user_id, order_date); - 如果还要查其他字段(比如
WHERE status = 'pending'),把过滤字段放索引前面:CREATE INDEX idx_status_user ON orders(status, user_id); - 避免在
GROUP BY字段上用函数,如GROUP BY UPPER(email)—— 这会让索引失效,改用生成列或提前标准化存储
真正难的不是写出语句,而是确认「哪些字段组合才算业务意义上的重复」——比如订单号重复是严重异常,但用户多次下单同一商品就不是。这个边界得和业务方对齐,SQL 只是验证工具。











