ROW_NUMBER()必须配合PARTITION BY才能实现分组去重,仅用ORDER BY仅编号不分组;PARTITION BY定义重复粒度,ORDER BY决定保留哪条,且需加唯一字段兜底确保稳定性。

ROW_NUMBER() 必须配合 PARTITION BY 使用才能去重
单独写 ROW_NUMBER() 不会自动去重,它只是给每行打序号。真正控制“按什么分组取最新/最早”靠的是 PARTITION BY —— 它定义了“重复”的粒度。比如按 user_id 分组,就表示“每个 user_id 下的记录算一组”,再用 ORDER BY updated_at DESC 排序,就能让最新更新的排第一。
常见错误是漏掉 PARTITION BY,结果整张表只编 1~N 的序号,后续 WHERE rn = 1 只返回一行,完全不是去重效果。
-
PARTITION BY字段必须是你认为构成“逻辑重复”的关键列,例如(product_id, sku_code) -
ORDER BY决定保留哪条:要最新版本就用updated_at DESC,要初始版本就用created_at ASC - 注意
NULL值在排序中的位置(不同数据库默认行为不同),必要时加COALESCE(updated_at, '1970-01-01')显式控制
DELETE 语句里不能直接嵌套 ROW_NUMBER()
多数数据库(如 MySQL 8.0+、PostgreSQL、SQL Server)不允许在 DELETE 中直接写 ROW_NUMBER() 窗口函数。你得先把它包进 CTE 或子查询里,再对结果做删除。
典型写法是用 CTE 给每组标序号,然后删掉序号 > 1 的行:
WITH ranked AS (
SELECT id, product_id, updated_at,
ROW_NUMBER() OVER (
PARTITION BY product_id
ORDER BY updated_at DESC
) AS rn
FROM products_history
)
DELETE FROM products_history
WHERE id IN (
SELECT id FROM ranked WHERE rn > 1
);
- MySQL 5.7 及更早不支持 CTE,得用多层子查询或临时表
- PostgreSQL 支持
DELETE ... USING语法,可避免子查询嵌套过深 - 务必确认
id是主键或唯一标识字段,否则IN子查询可能误删
ORDER BY 里的字段缺失会导致去重逻辑错乱
如果 ORDER BY 依据的字段有大量重复值(比如多个版本都用同一时间戳),ROW_NUMBER() 的排序结果是不确定的 —— 数据库可能每次返回不同“第 1 名”,导致去重结果不稳定。
解决办法是在 ORDER BY 末尾补一个确定性字段兜底:
- 加主键
id DESC:确保同时间戳下取最大 ID(即最后插入的) - 加自增序列或唯一版本号字段,如
version_num DESC - 避免只用
ORDER BY status这类低区分度字段
例如:ORDER BY updated_at DESC, id DESC 比单用 updated_at DESC 更可靠。
去重前务必备份或用 SELECT 验证逻辑
执行 DELETE 之前,先跑一遍等价的 SELECT 查看哪些会被删:
SELECT *, ROW_NUMBER() OVER ( PARTITION BY product_id ORDER BY updated_at DESC, id DESC ) AS rn FROM products_history ORDER BY product_id, rn;
这样能直观看到每组中 rn = 1 是哪条,rn > 1 是否真属于该丢弃的历史版本。线上环境尤其不能跳过这步 —— 一旦误删,恢复成本远高于多花两分钟验证。
另外注意事务隔离级别:在高并发写入场景下,刚插入的新版本可能还没被当前事务看到,导致误删“最新”记录。这类边界问题往往藏在测试数据覆盖不到的地方。

















