用row_number()识别重复日志最新行的核心是按业务主键分组、created_at desc排序并加id次级排序,再通过cte或子查询删除rn>1的行;须避坑时区、精度、语法限制及性能风险。

用 ROW_NUMBER() 识别重复日志的最新行
核心思路是:按业务主键(如 user_id, event_type)分组,用 ORDER BY created_at DESC 确保最新时间排第一,再用 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 标记序号。注意必须用 DESC,否则 rn = 1 会保留最旧记录。
常见错误是忽略时区或字段精度——比如 created_at 是 DATETIME 但实际插入时毫秒被截断,导致“相同时间”误判为重复;建议优先用带纳秒精度的列(如 PostgreSQL 的 TIMESTAMP WITH TIME ZONE),或加 id 作为次级排序:ORDER BY created_at DESC, id DESC,避免时间相同时序不确定。
DELETE 语句必须套在 CTE 或子查询里
MySQL 8.0+、PostgreSQL、SQL Server 都不支持直接在 DELETE 中引用窗口函数,必须先用 CTE 或派生表把 ROW_NUMBER() 结果算出来,再删 rn > 1 的行。不能写成 DELETE FROM logs WHERE ROW_NUMBER(...) > 1——这语法直接报错。
WITH ranked AS (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY user_id, event_type
ORDER BY created_at DESC, id DESC
) AS rn
FROM logs
)
DELETE FROM logs
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
注意点:
- MySQL 5.7 不支持 CTE,得用多层子查询或临时表
- PostgreSQL 要求
DELETE ... USING语法,不能直接IN子查询(性能差且可能锁表) - 如果表没主键或
id,得用ctid(PostgreSQL)或%%physloc%%(SQL Server)这类物理标识,但风险高,慎用
执行前务必加 WHERE 条件限制影响范围
线上日志表动辄千万级,全表跑 ROW_NUMBER() 会锁表、拖慢查询、甚至触发 OOM。永远不要直接执行无条件的去重删除。
实操建议:
- 先用
SELECT COUNT(*)确认候选重复数:SELECT COUNT(*) FROM (SELECT ..., ROW_NUMBER() ...) t WHERE rn > 1 - 加时间窗口,例如只处理
created_at >= '2024-01-01'的数据 - 分批删:用
LIMIT(PostgreSQL/MySQL 8.0+)或TOP(SQL Server)控制每次删 1000 行,配合循环或脚本 - 删完立刻
ANALYZE(PostgreSQL)或UPDATE STATISTICS(SQL Server),避免后续执行计划劣化
替代方案:用 NOT EXISTS 比 ROW_NUMBER() 更轻量
如果只要保留最新一条、且不关心“第几条”,NOT EXISTS 通常更快、更兼容老版本数据库,也绕过窗口函数限制。
DELETE FROM logs l1
WHERE EXISTS (
SELECT 1 FROM logs l2
WHERE l2.user_id = l1.user_id
AND l2.event_type = l1.event_type
AND (l2.created_at > l1.created_at
OR (l2.created_at = l1.created_at AND l2.id > l1.id))
);
这个逻辑等价于“删掉所有存在更新同类型日志的行”。关键细节:
- 必须包含
created_at = ... AND id > ...分支,否则时间完全相同时会漏删 - 索引必须覆盖
(user_id, event_type, created_at, id),否则性能急剧下降 - 在 MySQL 中,若
id是自增主键,可简化为只比id,但前提是插入顺序严格对应时间顺序
真正难的不是写对语法,而是确认“重复”的业务定义是否稳定、时间字段是否真能代表先后、以及删之前有没有人正往里写——这些比 ROW_NUMBER() 本身容易出问题得多。










