ROW_NUMBER()配合CTE是删重最稳妥的方式,因SQL Server/PostgreSQL等不支持窗口函数直接用于DELETE或WHERE,必须用CTE包裹生成序号后再按rn>1删除,且需确保PARTITION BY覆盖全部重复判定列、ORDER BY明确保留规则,并执行SELECT预览验证。

ROW_NUMBER() 配合 CTE 是删重最稳妥的方式
直接在 DELETE 语句里用 ROW_NUMBER() 会报错(SQL Server/PostgreSQL 不支持窗口函数出现在 WHERE 或 DELETE 的顶层),必须借助 CTE 或子查询包裹。CTE 写法清晰、可读性强,且能精准控制“保留哪一条”——比如按时间取最新、按 ID 取最小。
常见错误是写成:DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1,这语法非法,数据库直接拒绝执行。
- 必须把
ROW_NUMBER()放在 CTE 或派生表中,生成带序号的临时结果集 - CTE 中的排序字段(
ORDER BY)决定“谁被保留”:想要保留最新记录就按created_at DESC,保留最早就用ASC - 分区字段(
PARTITION BY)必须覆盖所有判定重复的列,漏一列就可能误删或漏删
删除前务必先验证哪些行会被删掉
删重是高危操作,没备份或没预览就执行 DELETE 容易翻车。永远先用 SELECT 替代 DELETE,确认逻辑正确再动手。
WITH dupes AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY email, user_id ORDER BY updated_at DESC
) AS rn
FROM users
)
SELECT * FROM dupes WHERE rn > 1;这个查询能直观看到所有将被删除的重复行。注意检查 PARTITION BY 列是否真能唯一标识业务上的“重复”——比如两个用户碰巧邮箱相同但属于不同租户,PARTITION BY email 就不合理,得加上 tenant_id。
不同数据库对 CTE + DELETE 的支持有差异
SQL Server 和 PostgreSQL 支持 DELETE FROM cte_name;MySQL 8.0+ 也支持,但 MySQL 5.7 及更早版本不支持,得改用 JOIN 方式。
- SQL Server / PostgreSQL:
DELETE FROM dupes WHERE rn > 1(CTE 名直接用于 DELETE) - MySQL 8.0+:
DELETE t1 FROM users t1 INNER JOIN dupes t2 ON t1.id = t2.id WHERE t2.rn > 1 - SQLite:不支持窗口函数,无法用此方案,需改用自连接或临时表
执行前查清自己用的数据库版本,别在 MySQL 5.7 上硬套 PostgreSQL 写法,否则报错 ERROR 1288: The target table dupes of the DELETE is not updatable。
性能和索引影响不能忽视
ROW_NUMBER() 在大数据量下会触发全表扫描和排序,如果没索引,几百万行可能卡住十几秒甚至超时。关键点在于 PARTITION BY 和 ORDER BY 字段的联合索引。
- 理想索引:
CREATE INDEX idx_users_dup_check ON users (email, user_id, updated_at)(顺序要匹配 PARTITION + ORDER) - 没有索引时,
ROW_NUMBER() OVER (PARTITION BY a,b ORDER BY c)可能比单列ORDER BY c慢数倍 - 删重后记得
VACUUM(PostgreSQL)或UPDATE STATISTICS(SQL Server),避免后续查询走错执行计划
真正麻烦的不是语法怎么写,而是想清楚“重复”的定义是否稳定、分区字段有没有隐含 NULL 值(NULL 不参与 PARTITION BY 分组,会导致意外漏分)、以及删完要不要重建唯一约束——这些细节比函数本身更容易出问题。

















