ROW_NUMBER()不能直接删除数据,必须配合CTE或子查询定位冗余行后再用DELETE删除;核心是PARTITION BY定义重复组、ORDER BY确定保留规则(如按id升序留最小值),且执行前须用SELECT预览验证。

ROW_NUMBER() 本身不能直接删除数据
想用 ROW_NUMBER() 一键删重?不行。它只是个窗口函数,只生成序号,不修改表。真正删数据得靠 DELETE + 子查询或 CTE 配合它来定位重复行。
用 CTE + ROW_NUMBER() 标记并删除重复行
这是最常用也最安全的做法:先用 CTE 给每组重复数据编号,再删掉序号大于 1 的行。关键在 PARTITION BY 要选对去重依据列(比如 email、name 和 phone 组合),ORDER BY 决定哪条被保留(通常按主键或时间戳升序,留最早的)。
示例(保留每组中 id 最小的记录):
WITH dup AS (
SELECT id, email, name,
ROW_NUMBER() OVER (PARTITION BY email, name ORDER BY id) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM dup WHERE rn > 1);- 必须把
DELETE的目标表名写在DELETE FROM后,不能写成DELETE FROM dup—— CTE 不是物理表 - PostgreSQL / SQL Server / Oracle 支持这种写法;MySQL 8.0+ 可以,但 MySQL 5.7 不支持 CTE 删除,得改用自连接
-
ORDER BY里别漏掉确定性字段(如id),否则rn = 1的结果可能每次不同
MySQL 5.7 怎么办?用自连接模拟 ROW_NUMBER()
老版本 MySQL 没 CTE,也没窗口函数,只能靠关联子查询“手算”序号。核心思路:对每一行,统计同组中比它“小”的记录数,+1 就是它的序号。
等效实现(同样保留 email+name 组内 id 最小者):
DELETE u1 FROM users u1 INNER JOIN users u2 WHERE u1.email = u2.email AND u1.name = u2.name AND u1.id > u2.id;
- 这里没显式用
ROW_NUMBER(),但逻辑上等价于 “给每组按id排序后删掉非首行” - 注意条件
u1.id > u2.id:确保只删大的,留小的 - 如果去重字段允许 NULL,
=会失效,得改用IS NOT DISTINCT FROM或补 NULL 处理逻辑
删之前务必备份,且确认 WHERE 条件无误
重复数据清理是高危操作。哪怕语句看着简单,一旦 PARTITION BY 列选错、或漏了 WHERE rn > 1,就可能全表清空。
- 执行前先用
SELECT *把要删的行查出来看一眼:SELECT * FROM dup WHERE rn > 1 - 生产环境严禁直接跑
DELETE,先在测试库还原一份数据验证 - 如果表有外键引用或触发器,删数据可能连带引发异常,得提前评估
真正麻烦的从来不是写对那几行 SQL,而是搞清“哪些字段才算重复”——业务语义错了,删得再准也没用。

















