row_number()本身不识别重复,需配合partition by定义判重字段、order by确定组内优先级,编号大于1即为重复;count(*) over可直接统计每组行数,>1即为重复项。

用 ROW_NUMBER() 标记每组内的行序号
窗口函数本身不直接“识别”重复,而是帮你构造识别条件。最常用的是 ROW_NUMBER() 配合 PARTITION BY 给相同字段组合内的记录编号。只要编号大于 1,就说明这条记录在该组里不是第一条——即属于重复。
关键点在于:重复是相对的,得先定义“按哪些字段判重”。比如用户表按 email 判重,订单表可能按 user_id 和 order_date 联合判重。
-
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC):按邮箱分组,新记录排前面,方便保留最新一条 - 别用
ORDER BY无确定性字段(如只写ORDER BY email),否则每次执行结果可能不同 - 如果字段含
NULL,注意PARTITION BY会把所有NULL归为同一组——这常被忽略,导致误判重复
用 COUNT(*) 窗口统计每组出现次数
比编号更直观的方式是直接数每组有多少条:COUNT(*) OVER (PARTITION BY email)。结果为 1 表示唯一,大于 1 就是重复项。
这个方法适合“找所有重复记录”,包括首条;而 ROW_NUMBER() 更适合“找重复中的非首条”。两者用途不同,别混用。
- 性能上,
COUNT(*) OVER在大数据量时通常比嵌套子查询快,但比单纯GROUP BY略重——因为要保留原行粒度 - PostgreSQL 和 SQL Server 支持
COUNT(*) OVER (...) > 1直接写在WHERE,但 MySQL 8.0 之前不支持,必须套一层子查询或 CTE - Oracle 中若字段有索引,
PARTITION BY字段顺序最好和索引前缀一致,避免排序开销
删除重复记录时,ROW_NUMBER() 必须配合子查询或 CTE
SQL 标准不允许在 DELETE 语句中直接使用窗口函数。所以不能写 DELETE WHERE ROW_NUMBER() > 1。
正确做法是把带窗口函数的结果作为临时结果集,再对它操作。CTE 是最清晰的方式,尤其在 PostgreSQL、SQL Server、MySQL 8.0+ 中都可用。
- MySQL 示例:
WITH ranked AS ( SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) DELETE u FROM users u INNER JOIN ranked r ON u.id = r.id WHERE r.rn > 1;
- SQLite 不支持 CTE 删除,得用临时表 +
rowid替代 - 千万别用
DELETE ... LIMIT想手动删多余行——没ORDER BY保证时,删哪条完全不可控
区分“逻辑重复”和“物理重复”,避免误删 数据库里两条记录字段全等,是物理重复;但业务上“重复”往往指某几个字段相同(如手机号相同但姓名不同),这时其他字段差异反而是关键线索。
比如用户注册时填错邮箱,生成了两条记录,但 phone 相同、name 不同——直接按 email 删可能丢数据,得先人工核对或加业务规则(如优先保留 status = 'active' 的那条)。
- 上线前务必在测试库跑
SELECT *查看实际重复样本,别只信统计数字 - 涉及多字段联合判重时,显式写出
WHERE col1 IS NOT NULL AND col2 IS NOT NULL,避免NULL参与分组引发意外分组 - 生产环境删重复前,至少保留一份
SELECT ... INTO backup_table备份,窗口函数结果一旦出错没法回滚










