必须用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 找出重复的字段组合
直接看最常用也最可靠的方案:GROUP BY 配合 HAVING COUNT(*) > 1。它不依赖主键或唯一索引,只关心“哪些值出现了多次”。比如查 users 表里重复的 email:
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1;
注意:必须用 HAVING 而不是 WHERE,因为 COUNT(*) 是聚合结果,WHERE 在分组前过滤,HAVING 在分组后筛选。
- 如果要查多个字段重复(如
first_name和last_name同时相同),就把它们全写进GROUP BY -
COUNT(*)比COUNT(column)更安全——后者会忽略NULL值,可能漏掉含空值的重复组 - MySQL 8.0+ 或 PostgreSQL 可加
ORDER BY cnt DESC快速定位高频重复项
用窗口函数标记每条记录是否重复(需要保留原始行)
当不只是要统计,而是想看到所有重复的完整记录(比如导出、去重或人工核对),ROW_NUMBER() 窗口函数更合适。以 PostgreSQL 或 SQL Server 为例:
SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users WHERE email IN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1 );
这样每组重复记录会被编号,rn > 1 的就是“多余副本”。关键点:
-
PARTITION BY决定按什么分组,“重复”的定义就在这里定义 -
ORDER BY id确保编号稳定——没ORDER BY会导致每次执行结果顺序不同 - SQLite 不支持窗口函数,得用自连接或子查询模拟,性能差很多
避免 COUNT(*) 导致的性能陷阱
大表上直接跑 GROUP BY + COUNT(*) 可能慢到超时,尤其没索引时。实际优化思路:
- 先确认重复字段是否有索引:对
email建INDEX能让GROUP BY快几倍 - 用
EXISTS替代全量聚合(适合只判断“是否存在重复”,不需计数):
SELECT DISTINCT email FROM users u1 WHERE EXISTS ( SELECT 1 FROM users u2 WHERE u2.email = u1.email AND u2.id != u1.id );
这个写法在有索引时通常比 GROUP BY 更快,但无法知道重复次数。
- 千万级表慎用
COUNT(*),可先LIMIT 100测速,再决定是否加WHERE缩小范围
区分 NULL 是否算重复值
SQL 标准里,NULL = NULL 为 false,所以 GROUP BY 默认把所有 NULL 归为一组——这意味着多个 NULL 会被当成重复。这未必是你想要的:
- 如果业务上
NULL表示“未知”,不应视为重复,得提前过滤:WHERE email IS NOT NULL - 如果确实要识别
NULL重复,保留原逻辑即可,但需在报告里注明 - 某些数据库(如 MySQL 5.7 严格模式)可能对
GROUP BY中非聚合字段报错,记得检查sql_mode或改用ANY_VALUE()
重复检测看似简单,但字段是否允许 NULL、要不要保留原始行、表大小、索引状态——这几个点没对齐,查出来的结果可能完全不对。











