ROW_NUMBER()用于标记重复记录中最新的一条为1,实现“留新删旧”;需用CTE或子查询生成序号后过滤,不能在WHERE中直接使用;PARTITION BY须按业务主键分组,ORDER BY建议用created_at DESC。

用 ROW_NUMBER() 标记重复历史记录
窗口函数的核心是给每行分配唯一序号,再按业务逻辑排序——比如按时间倒序,就能把最新的一条标为 1,旧的标为 2、3…。关键不是“去重”,而是“留新删旧”。
常见错误是直接在 WHERE 中写 ROW_NUMBER() > 1,这会报错:窗口函数不能出现在普通 WHERE 子句里。
- 必须先用子查询或 CTE 把
ROW_NUMBER()算出来,再在外层过滤 - 分区字段(
PARTITION BY)要对齐业务主键,比如用户ID、订单号;别漏掉,否则全表只排一次序 - 排序字段(
ORDER BY)建议用created_at DESC或id DESC,确保最新记录排第一
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC
) AS rn
FROM user_history
)
SELECT * FROM ranked WHERE rn = 1;安全删除多余记录前先验证结果集
删数据没有后悔药,尤其带 JOIN 或子查询的 DELETE 容易误伤。PostgreSQL 和 SQL Server 支持 DELETE ... USING,MySQL 8.0+ 支持 DELETE ... JOIN,但语法差异大,别套用。
- 先运行上一步的 CTE 查询,人工核对几条
rn > 1的记录是否确实该删 - 对 MySQL:用
DELETE t1 FROM user_history t1 INNER JOIN user_history t2 ON t1.user_id = t2.user_id AND t1.created_at 更稳妥,但性能差 - 对 PostgreSQL:直接
DELETE FROM user_history WHERE ctid NOT IN (SELECT ctid FROM ranked WHERE rn = 1),依赖系统列ctid,速度快但仅限本例
DENSE_RANK() 和 RANK() 不适合“留一删多”场景
它们用于处理并列排名,比如两个相同时间戳都该保留——但多数历史清理需求是“严格按时间取最新一条”,并列时反而需要额外逻辑判断(比如再比 id)。用错函数会导致删不干净或误删。
-
ROW_NUMBER()保证每行唯一编号,符合“只留一行”的目标 -
RANK()遇到并列会跳号(如1,1,3),DENSE_RANK()不跳(如1,1,2),二者都无法直接表达“只要第一个” - 如果业务允许同时间多条共存,应先用
GROUP BY + MAX(created_at)找出每个user_id的最新时间点,再关联原表删
大表执行时注意事务与索引影响
几十万行以上的历史表,DELETE 可能锁表、拖慢线上查询。窗口函数本身不写磁盘,但后续 DELETE 会触发大量日志和索引更新。
- 确保
(user_id, created_at)有联合索引,否则PARTITION BY + ORDER BY性能极差 - 分批删:加
LIMIT(PostgreSQL/MySQL 8.0+)或用WHERE id BETWEEN x AND y控制每次删几千行 - 避免在高峰时段执行;若用从库清理,确认复制延迟不会导致主从不一致
真正麻烦的不是语法,是删之前没确认分区键是否覆盖所有业务维度——比如漏了 tenant_id,跨租户数据就混了。

















