必须用子查询或 CTE 先标出行号再删除,因 SQL Server、PostgreSQL、MySQL 8.0+ 均不支持窗口函数直接出现在 DELETE 的 WHERE 子句中;正确做法是通过 PARTITION BY 定义重复逻辑,ORDER BY 指定保留规则,并确保相关字段有联合索引以提升性能。

用 ROW_NUMBER() 删重复数据前必须加 WHERE 子查询
直接在 DELETE 里写 ROW_NUMBER() 会报错——SQL Server、PostgreSQL、MySQL 8.0+ 都不支持窗口函数出现在 DELETE 的 WHERE 子句里。你得先用子查询把要删的行标出来,再套一层 DELETE。
常见错误现象:Window function is not allowed in WHERE clause 或 Invalid use of window function。
- 正确做法是:用
WITH或子查询生成带rn(行号)的结果集,再对rn > 1的行执行删除 - 别漏掉
PARTITION BY字段——它定义“哪些算重复”,比如按email和name重复,就写PARTITION BY email, name -
ORDER BY决定哪条被保留:想留最新的一条,就按created_at DESC;想留最早的一条,就用ASC
MySQL 8.0+ 删除重复记录的最小可行写法
MySQL 支持 CTE,但不能直接在 DELETE 中引用 CTE 别名(会报 ERROR 1093)。得用两层包装。
WITH ranked AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
- 注意:外层
DELETE的FROM users必须和 CTE 中的表名一致,否则 MySQL 可能拒绝执行 - 如果表有主键且你想保留最小
id的那条,把ORDER BY换成id ASC - 没主键?用
ORDER BY (SELECT NULL)是允许的,但保留哪条完全随机,慎用
SQL Server 用 CTE + DELETE 更简洁
SQL Server 允许直接从 CTE 删除,只要 CTE 引用的是单个基表。
WITH dupes AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) AS rn FROM users ) DELETE FROM dupes WHERE rn > 1;
- 这个写法比 MySQL 少一层嵌套,但前提是 CTE 不能含
JOIN、聚合或子查询,否则报错View or function 'dupes' is not updatable - 如果表有触发器,
DELETE FROM CTE仍会触发,行为和普通DELETE一致 - 执行前建议先
SELECT * FROM dupes WHERE rn > 1确认要删的内容
性能与索引:PARTITION BY 字段没索引会很慢
ROW_NUMBER() 在大表上执行时,数据库需要排序并分组。如果 PARTITION BY 字段没索引,可能全表扫描 + 外部排序,几百万行就卡住。
- 给
PARTITION BY列建联合索引最有效,例如:CREATE INDEX idx_email_updated ON users(email, updated_at DESC); - PostgreSQL 对
ORDER BY …… DESC索引支持好;MySQL 8.0+ 也支持降序索引,但老版本会忽略DESC关键字 - 别在生产环境跑前不加
LIMIT测试——先用SELECT COUNT(*)看重复量级,再决定是否分批删
真正麻烦的不是语法,是删完发现业务逻辑依赖了被删掉的“重复但不同”的记录,比如用不同 source 字段标识导入渠道,结果只按 email 去重把有效数据干掉了。