row_number() 不能直接删除数据,必须配合delete与cte或子查询;正确做法是用cte生成序号后删rn>1的行,注意partition by定义重复依据、order by决定保留哪行,并务必先备份、验证、加索引。

ROW_NUMBER() 本身不能直接删除数据
想用 ROW_NUMBER() 删除重复行,必须配合 DELETE 和子查询(或 CTE),因为 ROW_NUMBER() 是窗口函数,只能生成序号,不能作为 DML 的操作目标。
常见错误是写成:DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1 —— 这会报错,SQL Server/PostgreSQL/MySQL 都不支持在 WHERE 中直接用窗口函数。
正确做法:用 CTE 包裹 ROW_NUMBER() 后 DELETE
主流方案是先用 CTE 给每组重复数据编号,再删掉序号 > 1 的行。以 PostgreSQL 或 SQL Server 为例:
WITH dupes AS (
SELECT id, name, email,
ROW_NUMBER() OVER (
PARTITION BY name, email
ORDER BY id
) AS rn
FROM users
)
DELETE FROM users
USING dupes
WHERE users.id = dupes.id AND dupes.rn > 1;
关键点:
-
PARTITION BY列必须是你定义“重复”的依据(比如name, email) -
ORDER BY决定哪一行被保留——通常选最小id或最早created_at - MySQL 8.0+ 支持 CTE,但不支持
DELETE ... USING语法,得改用子查询或临时表 - SQLite 不支持窗口函数用于 DML,需先建临时表存要删的
id
MySQL 8.0 的替代写法(不支持 DELETE + CTE JOIN)
MySQL 要分两步:先查出要删的 ID,再删。注意不能在子查询里直接引用原表:
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY name, email
ORDER BY id
) AS rn
FROM users
) t
WHERE t.rn > 1
);
这个写法在 MySQL 中可行,但要注意:
- 外层
DELETE的IN子句必须套一层匿名子查询(即SELECT id FROM ( ... ) t),否则报错 “You can't specify target table for update in FROM clause” - 如果数据量大,
IN可能变慢,建议给(name, email)加联合索引 - 没有事务包裹时,中途失败可能导致部分删除,务必加
BEGIN; ... COMMIT;
别忘了备份和 WHERE 条件验证
实际执行前,先用 SELECT 看看哪些行会被删:
SELECT id, name, email,
ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id) AS rn
FROM users
WHERE rn > 1; -- 注意:这句不能直接跑,要包在子查询里
更安全的验证写法:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id) AS rn FROM users ) t WHERE t.rn > 1;
真正执行 DELETE 前,至少做三件事:
- 对目标表做一次
mysqldump/pg_dump备份 - 在测试库跑通逻辑,确认
PARTITION BY和ORDER BY符合业务预期 - 生产环境加
LIMIT(MySQL)或TOP N(SQL Server)控制单次影响行数
最易被忽略的是 ORDER BY 的语义——它决定“留哪条”,而很多人只关注去重,却没想清楚该保留最新创建的、还是最早录入的、或是某个字段值最大的那条。










