在数据库的日常维护和业务开发中,重复数据是一个绕不开的话题。无论是因为程序bug、并发写入缺少约束,还是历史数据导入时没有做唯一性校验,重复记录一旦进入表里,轻则让统计报表出现偏差,重则破坏业务逻辑的正确性。想在清理之前先把这些问题数据找出来,就需要掌握几种靠谱的SQL检测手段。本文将从最常见的GROUP BY方案讲起,逐步延伸到窗口函数的用法,并给出不同场景下的选择建议。

一、使用 GROUP BY 加 HAVING 检测重复值
这是最经典也是最容易被理解的方案。思路很简单:按照可能重复的字段分组,统计每组的行数,凡是数量大于1的组,就说明该字段值出现了重复。HAVING子句在分组之后进行过滤,正好适合这种对聚合结果设置条件的场景。
假设有一张员工表employees,包含emp_no、emp_name、email等字段,我们怀疑邮箱存在重复录入,可以这样写:
SELECT email, COUNT(*) AS cnt FROM employees GROUP BY email HAVING COUNT(*) > 1 ORDER BY cnt DESC;
这条语句会列出所有出现次数大于1的邮箱以及各自的出现次数,按重复次数从高到低排列。如果想进一步查看这些重复邮箱对应的完整记录明细,可以用子查询或者内连接的方式把结果关联回原表:
SELECT e.*
FROM employees e
INNER JOIN (
SELECT email
FROM employees
GROUP BY email
HAVING COUNT(*) > 1
) t ON e.email = t.email
ORDER BY e.email;需要注意的一点是,GROUP BY方案返回的是“哪些值重复了”,而不是“哪些行是多余的”。当你需要区分保留哪一条、删除哪一条时,它无法直接给出答案,这是它和后面要讲的窗口函数方案的核心区别。此外,如果要对多个字段的组合判断重复,比如同一个订单号加同一个商品号才算重复,只需要把这些字段都放进GROUP BY和SELECT里即可。
二、利用 COUNT 窗口函数标记每一行的重复情况
从MySQL 8.0、SQL Server等支持窗口函数的版本开始,可以用COUNT(*) OVER (PARTITION BY ...)来检测重复,它能保留明细行的同时给每一行打上重复计数标签,用起来更加直观。
还是以员工表为例,下面的语句会给每行计算同邮箱的记录总数:
SELECT
emp_no,
emp_name,
email,
COUNT(*) OVER (PARTITION BY email) AS email_cnt
FROM employees
ORDER BY email, emp_no;结果中email_cnt大于1的行就是重复行。与GROUP BY相比,这种写法不需要关联原表就能拿到完整明细,一次查询就能看到重复记录的全貌,在排查问题时非常方便。如果想只看重复的行,把查询包装成子查询再加一层WHERE过滤即可:
SELECT *
FROM (
SELECT
e.*,
COUNT(*) OVER (PARTITION BY email) AS email_cnt
FROM employees e
) t
WHERE email_cnt > 1;窗口函数的另一个优势是可以和ROW_NUMBER、RANK等函数组合使用,一次查询同时得到重复计数和行编号,为后续去重操作提供直接依据。不过要注意,窗口函数在MySQL 5.7及更早版本中不可用,如果项目还在使用旧版本数据库,只能退回到GROUP BY加自连接的方案。
三、用 ROW_NUMBER 精确定位需要处理的重复行
检测出重复值只是第一步,实际清洗数据时往往要回答“重复组里保留哪一条”的问题。ROW_NUMBER窗口函数可以在每个重复分组内按指定规则编号,编号大于1的行就是候选删除对象。
SELECT *
FROM (
SELECT
emp_no,
emp_name,
email,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY emp_no ASC
) AS rn
FROM employees
) t
WHERE rn > 1;这条语句按邮箱分组,组内按工号升序编号,工号最小的那条记录rn为1视为原始记录,其余行都是重复的、可以删除的。在SQL Server中可以直接借助CTE完成删除:
WITH dup AS (
SELECT
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY emp_no ASC
) AS rn
FROM employees
)
DELETE FROM dup WHERE rn > 1;MySQL中CTE不支持直接DELETE,可以建一张临时表保存要保留的主键,再按主键删除多余行。ROW_NUMBER方案的最大价值在于删除规则完全可控:想保留最新录入的记录,把ORDER BY改成按创建时间降序即可;想保留信息最完整的记录,可以按关键字段的非空程度排序。这种灵活性是单纯计数方案做不到的。
四、方法对比与预防重复的建议
三种方案各有适用场景。GROUP BY适合快速统计重复分布、输出汇总报告;COUNT窗口函数适合保留明细排查问题;ROW_NUMBER则面向需要精确去重的场景。下表做了简单对比:
| 方案 | 能否保留明细 | 能否定位多余行 | 版本要求 |
|---|---|---|---|
| GROUP BY + HAVING | 需关联原表 | 不能 | 所有版本 |
| COUNT 窗口函数 | 能 | 不能 | 需支持窗口函数 |
| ROW_NUMBER 窗口函数 | 能 | 能 | 需支持窗口函数 |
除了学会检测,更重要的还是在源头预防重复。对于业务上必须唯一的字段,应该建立唯一索引或唯一约束,数据库会在写入时自动拦截重复值,这比事后清理可靠得多。同时,插入数据时优先使用带唯一约束冲突处理的写法,例如MySQL的INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE,PostgreSQL的ON CONFLICT子句,都可以在并发场景下避免产生重复记录。
总结来说,检测重复数据并没有唯一的标准答案,关键是根据数据库版本、数据量和后续处理需求选择合适的工具。先通过GROUP BY了解重复的整体情况,再用ROW_NUMBER定位到具体行并制定保留规则,这套组合流程能够应对绝大多数数据清洗任务。清完之后别忘了加上唯一约束,让同类问题不再复发。
SQL重复数据GROUP BYROW_NUMBER修改时间:2026-09-01 18:46:32