结论:用ROW_NUMBER()、RANK()或DENSE_RANK()配合PARTITION BY+ORDER BY是最常用且可控的数据打标方式;它保留原行数、一次扫描完成标签附加,避免子查询或自连接导致的性能差与维护难问题。

直接说结论:用 ROW_NUMBER()、RANK() 或 DENSE_RANK() 配合 PARTITION BY + ORDER BY,是最常用且可控的数据打标方式;别用子查询或自连接,性能差还难维护。
什么时候该用窗口函数打标而不是 WHERE 或 JOIN
数据打标本质是给原始行附加一个业务含义的标签(比如“首单用户”“复购第3次”“部门TOP3”),不是过滤也不是关联。一旦你发现需求里有“每个用户/每类商品/每季度中……第X个”,就该本能想到窗口函数。
- WHERE 会删行,打标必须保留原行数
- JT 子查询在大表上容易触发 Nested Loop,执行计划变慢几倍起
- 窗口函数天然支持分组内排序后编号,一次扫描完成打标
选 ROW_NUMBER() 还是 RANK()?关键看是否允许并列
打标逻辑是否容忍“相同值同标号”,决定了函数选择:
-
ROW_NUMBER():严格连续编号,1,2,3… 即使两行值完全一样,也会强制分出先后。适合“取最早一条”“分页序号”“去重留最小ID”等必须唯一标识的场景 -
RANK():值相同时同名次,后续跳号。例如分数 100,100,90 → 排名 1,1,3。适合“竞赛名次”“按金额分档但档位可重叠” -
DENSE_RANK():值相同时同名次,后续不跳号。100,100,90 → 1,1,2。适合“考试等级”“会员成长阶段”等需连续档位的业务
示例:标记每个用户的首笔订单
SELECT user_id, order_no, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at ASC) AS rn
FROM orders;
再加 WHERE rn = 1 就能取出首单;若用 RANK(),当两个订单时间完全一致(毫秒级),可能都标为 1,但你未必想要这种不确定性。
PARTITION BY 写错会导致全表误标
这是最常踩的坑:漏写 PARTITION BY,或写错字段,结果变成全局排序编号,完全偏离打标意图。
- 错误写法:
ROW_NUMBER() OVER (ORDER BY created_at)→ 全表按时间排 1~100000,不是“每个用户内” - 正确写法:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) - 复合分区也常见:
PARTITION BY region, product_category,用于区域+品类双维度打标
注意:PARTITION BY 字段必须是实际参与分组的业务键,不能是计算字段(如 EXTRACT(YEAR FROM created_at))除非你明确需要按年份分组打标——否则容易引发隐式类型转换或索引失效。
打标后别直接用,先确认 ORDER BY 的确定性
如果 ORDER BY 列存在重复值(比如多个订单同秒创建),PostgreSQL 不保证 ROW_NUMBER() 的顺序稳定性,两次查询结果可能不同。
- 解决方法:在
ORDER BY末尾补一个唯一字段,如主键id或created_at+id - 安全写法:
ORDER BY created_at ASC, id ASC - 否则线上查出“首单”今天是 A,明天变成 B,排查成本极高
另外,NULLS FIRST/LAST 要显式声明。默认行为因 PostgreSQL 版本而异,生产环境务必写死,比如 ORDER BY score DESC NULLS LAST。
真正麻烦的不是语法,而是打标逻辑和业务规则的对齐——比如“最新订单”是指 created_at 还是 updated_at?“部门TOP3”要不要排除已离职员工?这些得跟产品对清楚,窗口函数只是忠实执行工具。

















