窗口函数比distinct更适合处理join前重复键,因为distinct在join后去重,笛卡尔积已发生、资源被浪费;而row_number()可在join前对右表按连接键分组并排序,只取每组所需一行(如最新),从源头避免膨胀,且逻辑确定、不掩盖数据质量问题。

为什么窗口函数比DISTINCT更适合处理JOIN前重复键
因为DISTINCT是在JOIN完成、所有行生成之后才去重,此时笛卡尔积已经发生,内存和计算资源早已被浪费;而窗口函数(如ROW_NUMBER())可以在JOIN之前就为右表每组重复键生成唯一锚点,从源头掐断膨胀路径。尤其当右表存在“一个用户对应多条配置记录”“一个订单对应多个状态快照”这类业务场景时,DISTINCT不仅无效,还会掩盖数据质量问题。
用ROW_NUMBER()在JOIN前收拢“多”侧数据
核心思路是:对右表按连接键分组,只取每组中你真正需要的那一行(最新、最早、主用、默认等),再与左表关联。不是靠运气选中某条,而是用确定性逻辑筛出单条。
- 写法必须带
PARTITION BY和ORDER BY,否则ROW_NUMBER()无法定位“第几条” - 排序字段要能反映业务优先级,比如
created_at DESC取最新,is_primary DESC取主用 - 子查询或CTE里完成筛选,别把
ROW_NUMBER()直接扔进JOIN的ON条件里——语法不支持且不可读 - 过滤必须用
WHERE rn = 1,不能用HAVING(窗口函数不能在HAVING中使用)
SELECT u.id, u.name, o.order_id, o.amount
FROM users u
LEFT JOIN (
SELECT
user_id,
order_id,
amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) o ON u.id = o.user_id AND o.rn = 1;
ON里写rn = 1还是WHERE里写?关键看JOIN类型
如果是INNER JOIN,两种写法结果一致;但LEFT JOIN必须把rn = 1放在ON里,否则WHERE o.rn = 1会把右表无匹配的左表行整个过滤掉,等效于INNER JOIN。
-
LEFT JOIN ... ON u.id = o.user_id AND o.rn = 1→ 左表全保留,右表只取每user_id的第一条 -
LEFT JOIN ... ON u.id = o.user_id WHERE o.rn = 1→ 实际丢弃了所有没订单的用户 - 如果右表筛选逻辑复杂(比如要排除
status = 'cancelled'),建议先在子查询里过滤,再套窗口函数,避免ON条件过载
容易被忽略的性能陷阱
窗口函数本身不慢,但若没索引支撑,PARTITION BY + ORDER BY可能触发大量临时磁盘排序。特别是当右表上没有(user_id, created_at)联合索引时,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)会全表扫描并排序。
- 检查执行计划:PostgreSQL中看
WindowAgg节点的Buffers是否异常高;MySQL中注意Extra列是否有Using filesort - 联合索引顺序必须匹配窗口函数的
PARTITION BY和ORDER BY字段,不能颠倒 - 如果只是要聚合值(如最新金额),直接用
MAX(amount) FILTER (WHERE rn = 1)或子查询SELECT ... FROM (SELECT ...) WHERE rn = 1比保留全部明细更省资源










