row_number() 不能直接用于 delete 的 where 子句,必须通过 cte 或子查询先计算行号,再在外层删除行号大于 1 的重复记录。

ROW_NUMBER() 必须配合 CTE 或子查询才能删数据
窗口函数本身不能出现在 DELETE 的 WHERE 子句里,直接写会报错:WINDOW function is not allowed in WHERE clause。必须先用 CTE 或嵌套子查询把行号算出来,再在外层引用结果做删除。
常见错误写法:DELETE FROM users WHERE ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) > 1 —— 这条语法非法,数据库直接拒绝执行。
- 正确路径只有一条:先编号,再删编号≠1的行
-
CTE写法更清晰,但部分旧版 MySQL(DELETE FROM cte_name,得改用子查询 +IN或临时表 - 若表无主键,
id字段不存在,可用ctid(PostgreSQL)或%%physloc%%(SQL Server)替代定位物理行
PARTITION BY 和 ORDER BY 缺一不可
PARTITION BY 定义“哪些列相同算重复”,ORDER BY 决定“同一组里留哪一条”。漏掉 ORDER BY 是高频翻车点——ROW_NUMBER() 行为未定义,每次执行可能保留不同行,删出来的结果不可复现。
- 业务上通常要留最新/最旧的一条,优先用时间字段如
created_at、updated_date;没时间字段就用主键id -
ORDER BY created_at DESC留最新;ORDER BY id ASC留最小 ID - 注意
NULL值默认排最前(PostgreSQL/SQL Server),如果created_at允许为空,可能意外把脏数据留下;可加NULLS LAST(PostgreSQL/Oracle)或用COALESCE(created_at, '1970-01-01')统一兜底
MySQL 8.0+ 可直接用 CTE 删除,老版本必须绕过限制
MySQL 在 8.0 之前不支持在 DELETE 中引用同一张表的子查询,下面这句会报错:ERROR 1093 (HY000): You can't specify target table for update in FROM clause。
绕过方法只有两个:
- 用两层子查询包装,让内层结果变成“虚拟临时表”:
SELECT id FROM (SELECT id FROM users GROUP BY email) AS tmp - 或显式建临时表:
CREATE TEMPORARY TABLE keep_ids AS SELECT MIN(id) FROM users GROUP BY email,再DELETE时NOT IN这张临时表 - MySQL 8.0+ 支持
WITH cte AS (...) DELETE FROM users WHERE id IN (SELECT id FROM cte WHERE rn > 1),但要注意:CTE 名不能和原表同名,否则仍报错
删之前务必加 WHERE 条件缩小范围
生产环境表动辄千万级,全表扫一遍 ROW_NUMBER() 开销极大,还可能锁表太久引发业务阻塞。别一上来就跑完整去重逻辑。
- 先加业务过滤条件,比如只处理近 3 个月的数据:
WHERE created_at >= '2026-02-01' - 若只清理某类状态的数据,比如
WHERE status = 'draft' AND email IS NOT NULL - 分批删更稳妥:用
AND id BETWEEN 10000 AND 20000控制每次删 1 万行,配合COMMIT避免长事务 - 特别注意:
ORDER BY字段如果没有索引,PARTITION BY字段也最好建联合索引,否则排序阶段会变全表扫描
SELECT 预览将被删的行:SELECT id, email, created_at FROM (SELECT id, email, created_at, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn FROM users) t WHERE rn > 1。
真正危险的不是语法错,而是删掉了不该删的“最新”或“唯一有效”记录——尤其是当 ORDER BY 字段有大量 NULL 或业务含义模糊时。











