ROW_NUMBER()本身不直接去重,仅编号;真正去重需用PARTITION BY按业务主键(如order_no或user_id+product_id)分组、ORDER BY按时间字段DESC加唯一字段兜底排序,再通过WHERE rn=1或CTE+DELETE筛选保留每组首行。

直接用 ROW_NUMBER() 本身不会去重,它只编号;真正去重必须靠 PARTITION BY 分组 + ORDER BY 定序 + 外层 WHERE rn = 1 筛选——否则只是多加一列数字。
怎么写 PARTITION BY 才算“按版本逻辑分组”
多版本历史数据的“重复”,本质是同一业务实体(如订单、用户、配置项)在不同时间点的多次快照。判断哪些行属于“同一个实体的多个版本”,关键看你的业务主键字段,而不是数据库主键。
-
PARTITION BY order_no:适用于订单号唯一、但可能多次更新的场景 -
PARTITION BY user_id, product_id:适用于“用户对某商品的操作记录”,组合才是业务唯一键 - 别写
PARTITION BY id:数据库自增 ID 是每行都不同,这样分组等于没分,ROW_NUMBER()就退化成全表编号 - 如果业务键含 NULL(如
tenant_id可为空),需提前WHERE tenant_id IS NOT NULL,否则所有 NULL 会被归为一组,导致误删
ORDER BY 怎么排才能稳定取到“最新有效版本”
历史数据常有多个时间字段(created_at、updated_at、effective_date),选哪个排序、怎么处理 NULL,直接决定哪条被保留。
- 优先用业务语义最明确的时间字段,比如配置表用
effective_date DESC,日志表用event_time DESC - 必须补一个唯一字段兜底,例如
ORDER BY effective_date DESC, id DESC—— 否则同一天多条记录时,数据库可能任意排序,结果不可复现 - MySQL 8.0+ 支持
NULLS LAST,但老版本不支持;稳妥做法是显式转换:ORDER BY COALESCE(effective_date, '1970-01-01') DESC, id DESC - 别用
ORDER BY updated_at ASC想取“最早生效版”,除非业务真要那个;多数情况要的是“当前有效版”,得用DESC
DELETE 时为什么不能直接嵌套 ROW_NUMBER()
MySQL 报错 ERROR 1093、PostgreSQL 报错 cannot modify table "xxx" which is also used in subquery,都是因为你在删一张表的同时,又在子查询里查它——数据库不允许这种自引用修改。
- 正确解法只有两种:
CTE(MySQL 8.0+/PostgreSQL/SQL Server)或JOIN 子查询(兼容性更好) - CTE 写法必须显式列出主键:
SELECT id, ROW_NUMBER() OVER (...) AS rn FROM history_table,不能写*,否则DELETE ... IN (SELECT *)语法不合法 - JOIN 写法注意别漏条件:
DELETE t1 FROM history_table t1 JOIN (SELECT ...) t2 ON t1.id = t2.id WHERE t2.rn > 1,ON 和 WHERE 缺一不可 - 线上执行前,一定先跑:
SELECT COUNT(*) FROM (SELECT ..., ROW_NUMBER() OVER (...) AS rn FROM history_table) t WHERE rn > 1,确认要删多少行
最容易被忽略的是排序稳定性:当 effective_date 字段大量为 NULL 或精度仅到天时,仅靠它排序会失效;必须用 COALESCE 或末尾加 id 强制唯一序号,否则同一批 SQL 多次执行可能留下不同的“最新版”。

















