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

ROW_NUMBER() 不能直接删除数据,只能标识重复行
ROW_NUMBER() 是窗口函数,只生成序号,不修改表。想靠它“删除重复”必须配合 DELETE + 子查询或 CTE。常见错误是写成 SELECT ROW_NUMBER() ... FROM t WHERE ... 就以为删掉了——其实什么都没动。
真正可行的路径是:用 ROW_NUMBER() 标出每组重复里的“该留哪条”,再把其他行删掉。关键在 PARTITION BY 列的选择——它定义“什么是重复”。比如按 email 和 phone 判重,就得写 PARTITION BY email, phone。
用 CTE + ROW_NUMBER() 安全去重(推荐)
CTE 写法清晰、可预览、不易误删。先运行 SELECT 部分确认逻辑,再改成 DELETE。
假设表 users 中 email 重复要保留 id 最小的那条:
WITH dup AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
)
SELECT * FROM dup WHERE rn > 1;确认结果无误后,把 SELECT 换成 DELETE:
WITH dup AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
)
DELETE FROM users WHERE id IN (SELECT id FROM dup WHERE rn > 1);-
ORDER BY id决定谁被保留:升序 →rn = 1是最小id;降序则相反 - PostgreSQL 和 SQL Server 支持 CTE 直接 DELETE;MySQL 8.0+ 可以,但 MySQL 5.7 不支持,得改用 JOIN 方式
- 别忘了加
WHERE rn > 1,否则会删光整张表
MySQL 5.7 怎么绕过 CTE 限制?
MySQL 5.7 不允许在子查询中直接引用目标表,所以不能写 DELETE FROM t WHERE id IN (SELECT ...) 这种结构。必须多套一层:
DELETE t1 FROM users t1 INNER JOIN users t2 ON t1.email = t2.email AND t1.id > t2.id;
这个语句含义是:“对每个 email 组,删掉所有比另一行 id 大的记录”,效果等价于保留最小 id。
- 没用
ROW_NUMBER(),但逻辑一致;如果要按时间字段保留最新一条,把t1.id > t2.id换成t1.created_at - 务必加索引:在
(email, id)或(email, created_at)上建联合索引,否则慢到卡死 - 执行前先
SELECT COUNT(*)确认影响行数,避免误删
为什么不能只靠 ROW_NUMBER() + WHERE 就删?
有人试过这样写:DELETE FROM users WHERE id IN (SELECT id FROM (SELECT id, ROW_NUMBER() OVER (...) AS rn FROM users) t WHERE rn > 1) —— 在 MySQL 5.7 会报错 You can't specify target table 'users' for update in FROM clause;在 SQL Server 虽然语法通过,但若没加 ORDER BY,rn 分配顺序不确定,可能删错行。
根本问题在于:ROW_NUMBER() 的结果依赖执行时的数据快照和排序稳定性。没有显式 ORDER BY,相同 PARTITION BY 值的行顺序不可控,哪怕看起来“每次结果一样”,也不能保证生产环境绝对安全。
真正容易被忽略的是:去重逻辑是否覆盖了业务意义上的“重复”。比如两个用户 email 相同但 is_deleted = 1,你是否该把软删的也当重复处理?这得看业务规则,ROW_NUMBER() 本身不判断逻辑,只忠实地按你写的条件分组排序。

















