GROUP BY + HAVING COUNT(*) > 1 是定位重复值的标准解法;需注意WHERE不能用聚合函数、NULL不参与分组、查完整重复行需子查询或窗口函数,ROW_NUMBER()更灵活但需注意版本与排序。

用 GROUP BY + HAVING 定位重复记录最直接
想查哪些行在表里重复了,别绕弯写多层嵌套查询——GROUP BY 配合 HAVING COUNT(*) > 1 是标准解法,语义清晰、性能可控、几乎所有 SQL 数据库都支持。
常见错误是把 COUNT(*) 放在 WHERE 子句里,结果报错:ERROR: aggregate functions are not allowed in WHERE。因为 WHERE 执行在分组前,而 COUNT 是聚合函数,必须等分组后才能算。
- 只关心“哪些值重复”,就按目标字段
GROUP BY,比如查邮箱重复:GROUP BY email - 要返回完整重复的行(含主键、时间等),得用子查询或窗口函数,不能只靠
GROUP BY输出原始行 - 注意 NULL 值:多数数据库中
NULL = NULL为 false,所以多个NULL默认不被归为同一组;如需把 NULL 当作相同值处理,得提前用COALESCE(email, '')等转换
需要返回所有重复的原始行?用 IN + 子查询兜底
当业务要求列出每一条重复数据(比如导出给运营核对),而不仅是统计重复值本身,就得借助子查询把重复的“键值”拎出来,再用 IN 回查原表。
示例:查 users 表中 phone 字段重复的所有记录:
SELECT * FROM users WHERE phone IN ( SELECT phone FROM users GROUP BY phone HAVING COUNT(*) > 1 );
这里容易踩的坑:
-
IN子查询结果里如果含NULL,整条IN判断会失效(col IN (..., NULL)永远不返回 true),务必加AND phone IS NOT NULL过滤 - 子查询没加索引字段时可能很慢,确保
phone列有索引,否则全表扫描两次 - MySQL 5.7+ 或 PostgreSQL 可改用
JOIN写法避免IN的空值陷阱,但逻辑更重,小数据量够用就行
用窗口函数 ROW_NUMBER() 更灵活地标记重复行
如果既要识别重复,又要区分“第一次出现”和“后续重复”,或者只取每组重复中的最新/最早一条,ROW_NUMBER() 是比嵌套查询更可控的选择。
它在不改变原表结构的前提下,给每行打上组内序号:
SELECT *,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
FROM users;
之后可轻松筛选:
- 全部重复行:
WHERE rn > 1 - 每组保留最新一条:
WHERE rn = 1 - 只取重复组的首尾记录:
WHERE rn IN (1, 2)(假设至少重复两次)
注意:ROW_NUMBER() 是标准 SQL-99 起支持的,但 SQLite 直到 3.25.0 才支持,旧版得换方案;另外 PARTITION BY 的字段顺序和 ORDER BY 会影响编号结果,别漏写 ORDER BY——没它的话排序未定义,每次执行结果可能不同。
嵌套查询不是万能解,小心性能和语义歧义
有人习惯写两层 SELECT 套着 COUNT,比如:SELECT * FROM t1 WHERE (SELECT COUNT(*) FROM t2 WHERE t2.id = t1.id) > 1。这种写法在小表上看着能跑,但实际问题很多:
- 是相关子查询,外层每扫一行,内层就执行一次,复杂度接近 O(n²),10 万行就可能卡住
- 语义模糊:它查的是“当前行 id 在另一张表出现超过一次”,而不是“本表中该字段值重复”——容易误用成跨表逻辑
- 无法利用索引加速内层
COUNT,优化器很难生效
真正需要嵌套的场景极少,比如动态构造去重条件或配合变量计算;日常查重复,优先走 GROUP BY + HAVING 或窗口函数。
重复数据识别看似简单,但 NULL 处理、索引缺失、窗口函数版本兼容性这几个点,上线后最容易突然暴露问题。










