ROW_NUMBER()不能直接在JOIN后对主表去重,因其编号对象是膨胀后的结果行而非主表记录;正确做法是先在从表用PARTITION BY+ORDER BY编号并筛选rn=1,再与主表LEFT JOIN,确保每主记录仅关联一条从记录。

ROW_NUMBER() 为什么不能直接解决主从多对多去重
直接在 JOIN 后对主表字段套 ROW_NUMBER(),大概率得不到你想要的“每个主记录只留一条从记录”的效果。因为 ROW_NUMBER() 是按结果集行排序编号的,而主从多对多 JOIN 后,主表一行会膨胀成多行(每条匹配的从记录对应一行),此时 ROW_NUMBER() OVER (PARTITION BY 主表ID ORDER BY ...) 编的是“从记录的序号”,不是“主记录是否保留”的开关。
正确做法:先用 ROW_NUMBER() 在从表侧打标再 JOIN
核心思路是——不把去重逻辑放在最终结果集上,而是提前在从表数据里选出“每组该保留哪一条”,再和主表关联。典型场景:一个订单(主)有多个订单项(从),你想查每个订单 + 它的最新一条订单项。
- 先对从表独立使用
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC),给每个order_id下的订单项按时间倒序编号 - 用子查询或 CTE 把编号 = 1 的那条从记录拎出来(即每个主键对应的“首选从记录”)
- 再把这个精简后的从表结果 LEFT JOIN 主表——这样主表不会被重复展开,也不会漏掉没从记录的主记录
WITH ranked_items AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) AS rn
FROM order_items
)
SELECT o.id, o.status, i.sku, i.quantity
FROM orders o
LEFT JOIN ranked_items i ON o.id = i.order_id AND i.rn = 1;
常见踩坑点:PARTITION BY 写错、JOIN 条件漏掉 rn = 1
这两个错误会导致结果完全失真,而且不容易一眼看出来:
-
PARTITION BY写成从表主键(如PARTITION BY item_id)——编号全变成 1,失去意义 -
LEFT JOIN时只写ON o.id = i.order_id,漏了AND i.rn = 1—— 等价于没过滤,又回到原始多对多膨胀状态 - ORDER BY 用了可能为 NULL 的字段(如
deleted_at),导致排序不稳定,rn = 1每次跑结果不一致;建议加COALESCE(deleted_at, '9999-12-31')或明确补NULLS LAST(PostgreSQL)/用IS NULL排序(MySQL 8.0+)
替代方案对比:ROW_NUMBER() vs DISTINCT ON vs LATERAL
不是所有数据库都推荐死磕 ROW_NUMBER():
- PostgreSQL 可直接用
DISTINCT ON (order_id) ... ORDER BY order_id, updated_at DESC,更简洁,语义更直白 - PostgreSQL / SQL Server 支持
LATERAL(或APPLY),能天然表达“对每个主记录执行一次子查询取 top 1”,可读性高且易加索引 - MySQL 8.0+ 虽支持
ROW_NUMBER(),但若从表无合适索引(如(order_id, updated_at)复合索引),性能可能明显劣于LATERAL子查询
真正难的不是写对语法,而是想清楚“去重的语义边界在哪”——是每个主键下取最新一条?金额最大的一条?还是按业务规则排优先级?这个逻辑一旦定错,后面怎么优化 ROW_NUMBER() 都白搭。

















