排名函数为分区内的每一条输入行附加位置,不会把整个团队、部门或客户的数据压缩成一行聚合结果。真正需要选择的是并列的含义:每行占一个独立位置、允许并列且保留空位的竞赛名次,还是按不同分数组连续计数的名次。三种规则解决的问题不同,应先确定业务规则再写窗口。
三个问题对应三种排名
row_number() 从一开始给每行分配连续整数。如果两行的排序值完全相同,而窗口没有其他区分条件,它们的相对序号没有保证。需要恰好选出两位获胜者时,可以按分数降序,再按稳定且唯一的标识排序。这意味着同分时由标识决定谁占据剩余名额,不能把这个结果当成不影响业务的技术细节。
rank() 给同组并列行分配该组第一行的位置。分数为一百、九十、九十、八十时,名次依次为一、二、二、四。两位第二名占用了两个位置,因此下一档分数前面会留下空位。dense_rank() 则计算同分组的数量,对同一批数据给出一、二、二、三,不会因并列人数增加而跳号。
并列组由窗口 ORDER BY 中的全部表达式决定。如果把唯一标识也加入 rank 的窗口,即使本意只是稳定展示顺序,也会让原来的同分行不再并列。row_number 需要标识裁决、rank 需要保留同分时,应使用不同窗口定义。最终结果的展示顺序可以由外层 ORDER BY 控制,不必因此改掉排名所依据的并列关系。
明确分区与过滤时机
PARTITION BY 让每个组从头编号。不写分区条件时,窗口看到的所有输入行属于同一个分区。这和窗口帧是不同概念:排名依据分区顺序和并列组,不是对某段移动聚合帧计数。添加 ROWS 边界,不会把 rank 变成“这个边界内有多少行”的函数。
窗口看到的是 FROM、WHERE、分组和 HAVING 处理后剩余的行。因此,在同层查询中过滤,可能先删掉部分竞争者,再计算名次。如果需要先给所有人排名,再挑前几个位置,应在子查询或 CTE 中生成窗口值,然后到外层过滤。PostgreSQL 不允许直接把窗口函数放进 WHERE,这也提醒读者区分排名输入与排名输出。
“前两名”必须有明确含义。位置不大于二,每个分区最多返回两行;竞赛名次不大于二,会保留全部并列第二名,结果可能超过两行;密集名次不大于二,则选择前两个不同分数组。当第一档分数本身就有多人并列时,密集排名与竞赛排名的筛选结果也可能不同。这些是不同的结果集合,不是可以随意互换的性能写法。
坏掉的样子
row_number 缺少确定的并列裁决条件时,同分选手可能在重复查询中交换位置。反过来,在所有窗口都添加标识,又可能悄悄消除 rank 本来要保留的并列。测试时必须放入恰好卡在截止名次上的同分数据,不能只测每个分数都不同的情况。如果分数允许 NULL,还应明确空值顺序;PostgreSQL 降序默认把空值放前面,需要时应显式指定 NULLS LAST。
确定的排序也不能冻结持续变化的数据。新分数出现后,已有行可能在两次请求之间移动,所以偏移分页或保存的行号并不是持久身份。大规模排名可能需要排序和临时文件,多个不同的窗口顺序也可能增加处理步骤。应读取有代表性的执行计划,不能因为查询使用窗口函数,就假设一定只有一次廉价扫描。
快速验证
把下面内容保存为 ranking.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f ranking.sql。red 分区包含一次并列,blue 是独立分区;末尾回滚会清理临时表和临时视图。
\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE scores (id integer, team text, score integer);
INSERT INTO scores VALUES (1,'red',100),(2,'red',90),(3,'red',90),(4,'red',80),(5,'blue',50);
CREATE TEMP VIEW ranked AS
SELECT *, row_number() OVER (PARTITION BY team ORDER BY score DESC,id) AS position,
rank() OVER w AS rank_with_gaps, dense_rank() OVER w AS rank_without_gaps
FROM scores WINDOW w AS (PARTITION BY team ORDER BY score DESC);
SELECT * FROM ranked ORDER BY team,id;
SELECT count(*) AS top_two_positions FROM ranked WHERE team='red' AND position<=2;
SELECT count(*) AS top_two_ranks FROM ranked WHERE team='red' AND rank_with_gaps<=2;
DO $$ BEGIN
IF (SELECT array_agg(rank_with_gaps ORDER BY id) FROM ranked WHERE team='red')
IS DISTINCT FROM ARRAY[1,2,2,4]::bigint[] THEN RAISE EXCEPTION 'wrong rank'; END IF;
IF (SELECT array_agg(rank_without_gaps ORDER BY id) FROM ranked WHERE team='red')
IS DISTINCT FROM ARRAY[1,2,2,3]::bigint[] THEN RAISE EXCEPTION 'wrong dense rank'; END IF;
IF (SELECT array_agg(id ORDER BY position) FROM ranked WHERE team='red' AND position<=2)
IS DISTINCT FROM ARRAY[1,2] THEN RAISE EXCEPTION 'unstable top two'; END IF;
IF (SELECT count(*) FROM ranked WHERE team='red' AND rank_with_gaps<=2) <> 3 THEN
RAISE EXCEPTION 'tied second place was dropped';
END IF;
END $$;
ROLLBACK;
PostgreSQL 18.6 实测中,red 的位置为一至四,竞赛名次为一、二、二、四,密集名次为一、二、二、三。blue 独立从一开始。按 red 的位置筛选前两行,会得到标识一和二;按竞赛名次不大于二筛选,则返回三行,保留两位第二名。这同时说明“每行还在”与“每个分区重新计数”的行为。
断言检查两个名次数组、预定的同分裁决结果和截止处的行数。把共享排名窗口改为包含 id,应该让这些预期失败,从而暴露展示稳定性与并列语义被混用的问题。这里的观察来自固定数据集;若报表需要在分数持续变化时仍保持稳定,还必须单独设计快照或分页规则,不能让临时排名承担它没有承诺的身份语义。
来源
- PostgreSQL 窗口函数,排名函数与并列组,已核验 2026-09-10。
- PostgreSQL 窗口教程,分区、排序和排名后的过滤,已核验 2026-09-10。