ROW_NUMBER()是删除重复数据并保留最新记录最可靠方法,需用PARTITION BY定义重复组、ORDER BY时间DESC加id DESC二级排序确保唯一性,再通过CTE或子查询过滤rn>1的行;不可在DELETE中直接嵌套窗口函数。

用 ROW_NUMBER() 给重复行打序号再过滤
直接删重复但留最新,核心是先识别哪条算“最新”——通常靠时间字段(如 created_at)或自增主键(如 id)。窗口函数不支持在 DELETE 里直接用,所以得套一层子查询或 CTE。
常见错误是写成 DELETE FROM table WHERE id IN (SELECT id FROM ... ROW_NUMBER() ...) 却没加排序条件,导致保留的不是最新而是任意一条。
- 必须按时间倒序或 ID 倒序排序:
ORDER BY created_at DESC或ORDER BY id DESC -
PARTITION BY列要选真正定义“重复”的字段,比如email、user_id,别多加或漏加 - MySQL 8.0+、PostgreSQL、SQL Server 都支持;SQLite 3.25+ 可用,但旧版不行
WITH ranked AS (
SELECT id, email, created_at,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
为什么不用 RANK() 或 DENSE_RANK()
RANK() 和 DENSE_RANK() 在遇到相同 created_at 时会并列编号,比如两条同时间记录都标为 1,下一条就跳到 3 或 2——这会导致本该删掉的重复行被漏删,或者误删唯一行。
ROW_NUMBER() 强制每行唯一序号,哪怕排序字段完全一样,也会按物理顺序或引擎内部顺序分出先后,确保只留一条。
- 如果业务明确要求“同时间取 ID 最大的那条”,就在
ORDER BY里补上id DESC作为第二排序条件 - 别依赖数据库默认顺序——没
ORDER BY的ROW_NUMBER()结果不可靠
MySQL 中执行报错 “You can’t specify target table for update in FROM clause” 怎么办
这是 MySQL 特有报错,意思是不能在 DELETE 的子查询里直接引用要删的表。CTE 在 MySQL 8.0+ 能绕过,但低版本必须用派生表套一层。
- 把 CTE 改成内联视图:把
WITH ranked AS (...) DELETE...拆成DELETE FROM users WHERE id IN (SELECT id FROM (SELECT ...) AS tmp WHERE rn > 1) - 或者先用
CREATE TEMPORARY TABLE存下要删的id列表,再删 - 注意临时表名不能和原表同名,否则可能触发锁或元数据冲突
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY created_at DESC, id DESC
) AS rn
FROM users
) AS ranked
WHERE rn > 1
);
删之前一定要先 SELECT 验证,尤其涉及生产环境
窗口函数逻辑一旦写错,可能删掉全部数据或只删了零条——看不出错,但结果灾难性。别跳过验证步骤。
- 先把
DELETE换成SELECT *,看输出是不是你预期要保留的那些“最新”行 - 检查
PARTITION BY字段是否真有重复值:SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1 - 如果表很大,加
LIMIT 100先试跑,观察执行计划里有没有走索引(email + created_at复合索引能极大加速)
真正难的不是语法,是判断“重复”的业务定义是否准确,以及时间字段是否存在 NULL 或时区偏差——这些不会报错,但会让“最新”变成“最旧”。

















