row_number()不能直接用于delete的where子句,必须通过cte或子查询先标记序号(如rn>1),再关联删除;需确保partition by和order by正确、有唯一标识列,并执行select预览验证。

ROW_NUMBER() 不能直接写在 DELETE 的 WHERE 里
你写 DELETE FROM t WHERE ROW_NUMBER() OVER (PARTITION BY x ORDER BY y) > 1 会直接报错。PostgreSQL 提示 “window functions are not allowed in WHERE”,MySQL 8.0+ 报错 “This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME subquery’” 或语法错误,SQL Server 也拒绝执行。窗口函数只在查询阶段生成临时序号,不参与 DML 的行筛选逻辑。
必须用 CTE 或子查询中转再删
安全做法是先用 ROW_NUMBER() 标记每组内应保留的行(比如 rn = 1),再通过 JOIN 或 IN 删除其余行。关键点:
- CTE 必须包含唯一标识列(如
id),否则无法精准定位要删的物理行 -
PARTITION BY字段要和业务重复定义一致(例如去重邮箱就写PARTITION BY email) -
ORDER BY决定谁被保留:想留最早插入的就写ORDER BY id,留最新就写ORDER BY created_at DESC - MySQL 8.0+ 要两层子查询绕过 “You can't specify target table for update in FROM clause” 错误
MySQL 8.0+ 示例:
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;
没窗口函数的老版本怎么办
MySQL 5.7、SQLite 等不支持 ROW_NUMBER(),得靠自连接或 MIN() + 子查询,但有明显限制:
- 表必须有主键或唯一列(如
id),否则无法区分“哪条该删” - MySQL 自连接写法:
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id—— 删掉每组中id更大的行 - SQLite 可用
ROWID:DELETE FROM users WHERE ROWID NOT IN (SELECT MIN(ROWID) FROM users GROUP BY email) - 如果表既无主键又无唯一列(比如纯日志表),这些方法都不可控,删完可能只剩一条或全空,必须改用
CREATE TABLE ... AS SELECT DISTINCT重建
删之前不跑 SELECT 预览等于开盲盒
所有去重 DELETE 都必须先用等价 SELECT 验证逻辑,否则极易误删全部或一条不剩:
- 先跑:
SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users,确认rn = 1真是你想留的那条 - 再试删前 10 行:
... WHERE r.rn > 1 LIMIT 10(PostgreSQL/SQL Server 支持,MySQL 需嵌套) - 生产环境务必先
mysqldump或pg_dump备份,哪怕只是加个WHERE 1=0测试语句也得走一遍执行计划
真正危险的不是语法写错,而是 PARTITION BY 漏字段、ORDER BY 反向、或者 NULL 值参与分组导致整组被跳过——这些在预览 SELECT 里一眼就能暴露。










