ROW_NUMBER() 必须配合 PARTITION BY 才能识别重复行,仅用 ORDER BY 会全表连续编号;需用 CTE 先标记重复(PARTITION BY 去重字段,ORDER BY 决定保留逻辑),再删除 rn > 1 的行,并注意 NULL 处理、索引优化与分批删除。

ROW_NUMBER() 必须配合 PARTITION BY 才能标记重复行
单独用 ROW_NUMBER() 不会识别重复,它只是按顺序编号。真正定位重复数据,得靠 PARTITION BY 按待去重的字段分组,再用 ORDER BY 决定哪条保留(比如按时间、ID 升序,序号为 1 的留,其余删)。
常见错误是只写 ORDER BY 而漏掉 PARTITION BY,结果全表编一串连续号,完全无法区分重复块。
- 假设要去重的字段是
email和name,就写PARTITION BY email, name -
ORDER BY created_at ASC表示保留最早插入的那条;若想留最新的一条,改用DESC - 不能对包含
NULL的字段直接PARTITION BY—— 多个NULL在多数数据库里不被视为同一组,会导致重复没被归并
删除重复行必须用 CTE 或子查询套一层
MySQL 8.0+、PostgreSQL、SQL Server 都不允许在 DELETE 语句里直接引用窗口函数,所以得先用 CTE 把带 ROW_NUMBER() 的结果算出来,再删掉序号 > 1 的行。
例如 PostgreSQL/SQL Server 写法:
WITH duplicates AS (
SELECT id, email, name,
ROW_NUMBER() OVER (PARTITION BY email, name ORDER BY id ASC) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM duplicates WHERE rn > 1);MySQL 8.0+ 也支持类似写法;但 MySQL 5.7 及更早版本不支持 CTE,只能用自连接或临时表模拟。
- 别用
DELETE FROM (SELECT ...)这种写法 —— 大部分数据库报错This type of clause is not allowed in this context - 务必在
DELETE前加SELECT * FROM duplicates WHERE rn > 1预览要删哪些,避免误删 - 如果表没主键或唯一标识字段,
id替换成ctid(PostgreSQL)或%%physloc%%(SQL Server),否则无法精准定位行
不同数据库对 NULL 的处理差异极大
重复判断时,NULL = NULL 在标准 SQL 中是未知(UNKNOWN),不是 true。所以 PARTITION BY col 遇到多个 NULL 值,默认不会把它们分进同一组 —— 这和直觉相反,也是最常踩的坑。
- PostgreSQL:可用
PARTITION BY COALESCE(col, '___NULL___')临时替换,但要注意字符串不能真实存在于数据中 - SQL Server:支持
PARTITION BY col且默认将 NULL 归为一组(行为不标准,但方便) - MySQL:从 8.0 开始与 PostgreSQL 一致,NULL 不参与分组匹配
- 更稳妥的方式是提前用
WHERE col IS NOT NULL过滤,或单独处理 NULL 行
大表执行前必须加 WHERE 条件限制范围
直接对千万级用户表跑 ROW_NUMBER() OVER (PARTITION BY ...) 极易触发内存溢出或锁表,尤其在 OLTP 环境下可能阻塞业务写入。
- 先加
WHERE created_at > '2024-01-01'缩小扫描范围 - 确保
PARTITION BY字段上有索引(复合索引优先,如(email, name)) - 生产环境别一次性删完,用循环分批删:每次
DELETE TOP (10000)(SQL Server)或LIMIT 10000(PostgreSQL/MySQL),配合WHERE rn > 1和递增偏移 - 删完立刻
ANALYZE TABLE(PostgreSQL)或UPDATE STATISTICS(SQL Server),避免后续查询计划劣化
真正麻烦的从来不是语法,而是 NULL 怎么算相等、大表怎么不锁死、以及删之前到底有没有人确认过哪条该留——这些没法靠一条 SQL 解决。

















