PostgreSQL 18 中文参考文档(未翻译部分保留英文)🌐 切换到英文原版 →

3.5. 窗口函数 #

窗口函数(window function)针对与当前数据行存在某种逻辑关联的一组行集合执行计算。这与普通的聚合函数所能完成的统计计算非常相似。然而核心区别在于:窗口函数不会像普通聚合函数那样将多行输入压缩折叠为单行输出,参与计算的每一行都完整保留各自的独立行标识。在底层处理中,窗口函数不仅能访问当前行的数据,还能灵活感知查询结果集中当前行所属窗口范围内的其他上下文行。

以下示例展示了如何将每位员工的薪水与其所在部门的平均薪资进行对比:

SELECT depname, empno, salary, avg(salary) OVER (PARTITION BY depname) FROM empsalary;

  depname  | empno | salary |          avg
-----------+-------+--------+-----------------------
 develop   |    11 |   5200 | 5020.0000000000000000
 develop   |     7 |   4200 | 5020.0000000000000000
 develop   |     9 |   4500 | 5020.0000000000000000
 develop   |     8 |   6000 | 5020.0000000000000000
 develop   |    10 |   5200 | 5020.0000000000000000
 personnel |     5 |   3500 | 3700.0000000000000000
 personnel |     2 |   3900 | 3700.0000000000000000
 sales     |     3 |   4800 | 4866.6666666666666667
 sales     |     1 |   5000 | 4866.6666666666666667
 sales     |     4 |   4800 | 4866.6666666666666667
(10 rows)

前三列直接取自底层员工薪水表 empsalary,表中的每一行对应输出结果中的一行。第四列代表在与当前行拥有相同部门名(depname)的所有行集合上计算出的部门平均薪水(这里的 avg 函数在底层实现上与普通聚合函数相同,但其后附加的 OVER 子句使其被判定为窗口函数,并在对应的窗口帧范围内完成求值)。

窗口函数调用在语法上最直观的特征,是紧随函数名及其参数之后必定附带一个 OVER 子句。这是它在语法层面与普通标量函数或普通聚合函数的根本区分标志。OVER 子句精确定义了查询数据行如何被切分与组织供窗口函数遍历计算。OVER 内部的 PARTITION BY 子句将数据行按指定表达式分组为若干独立的分区(partitions),对于查询中的每一行,窗口函数将作用于与其归属同一分区的相关行集合。

你还可以通过在 OVER 内部添加 ORDER BY 子句,来精确控制窗口函数在分区内部处理数据行的先后顺序(窗口内的 ORDER BY 顺序不必与整个查询最终输出的行顺序一致)。示例代码如下:

SELECT depname, empno, salary,
       row_number() OVER (PARTITION BY depname ORDER BY salary DESC)
FROM empsalary;

  depname  | empno | salary | row_number
-----------+-------+--------+------------
 develop   |     8 |   6000 |          1
 develop   |    10 |   5200 |          2
 develop   |    11 |   5200 |          3
 develop   |     9 |   4500 |          4
 develop   |     7 |   4200 |          5
 personnel |     2 |   3900 |          1
 personnel |     5 |   3500 |          2
 sales     |     1 |   5000 |          1
 sales     |     4 |   4800 |          2
 sales     |     3 |   4800 |          3
(10 rows)

如上所示,row_number 窗口函数按照 ORDER BY 声明的降序规则,为每个部门分区内的行赋予连续递增的排名序号(薪水并列的行其序号排列由系统内部决定)。row_number 不需要显式传递任何函数参数,因为其计算逻辑完全由后面的 OVER 规则驱动。

窗口函数所作用的数据集,是由查询语句的 FROM 子句生成、并经过 WHERE、GROUP BY 和 HAVING 过滤之后所形成的“虚拟结果表”。例如,因未满足 WHERE 过滤条件而在早期被剔除的行,对任何窗口函数都是完全不可见的。单个查询中可以包含多个具有不同 OVER 规则的窗口函数,它们以不同的维度对数据进行切分,但它们处理的原始数据基础均属于该虚拟结果集。

如果行处理顺序对计算结果无影响,ORDER BY 可以直接省略。同样,PARTITION BY 也可以省略,此时整个结果集被视作一个包含全部数据行的单一全局分区。

与窗口函数紧密相连的另一个核心概念是窗口帧(window frame)。对于分区内的每一行,都存在一个动态界定的行子集称为其窗口帧。部分窗口函数仅作用于当前行所在的窗口帧、而非整个静态分区。默认规则是:如果显式指定了 ORDER BY,那么默认窗口帧包含从当前分区起始行到当前行之间的所有行,外加按 ORDER BY 比对值等于当前行的后续重复行;如果省略了 ORDER BY,默认窗口帧则扩展为当前分区的全量数据行。 [5] 以下展示结合 sum 求和的示例:

SELECT salary, sum(salary) OVER () FROM empsalary;
 salary |  sum
--------+-------
   5200 | 47100
   5000 | 47100
   3500 | 47100
   4800 | 47100
   3900 | 47100
   4200 | 47100
   4500 | 47100
   4800 | 47100
   6000 | 47100
   5200 | 47100
(10 rows)

在上述查询中,由于 OVER 子句中未声明 ORDER BY,窗口帧等同于分区,且由于缺少 PARTITION BY,分区即为整张表;因此每一行都返回了整张表的总薪资和。但如果我们追加了 ORDER BY,结果将发生显著变化:

SELECT salary, sum(salary) OVER (ORDER BY salary) FROM empsalary;
 salary |  sum
--------+-------
   3500 |  3500
   3900 |  7400
   4200 | 11600
   4500 | 16100
   4800 | 25700
   4800 | 25700
   5000 | 30700
   5200 | 41100
   5200 | 41100
   6000 | 47100
(10 rows)

此时求和计算从最低薪资行开始累积累加至当前行,并自动包含了薪资相同的所有并列行(请特别观察薪水并列为 4800 与 5200 时的累加结果)。

窗口函数仅被允许出现在查询的 SELECT 选择列表以及最外层的 ORDER BY 排序子句中。在 GROUP BY、HAVING 或 WHERE 等子句中是绝对禁止编写窗口函数的。这是因为在执行计划的物理算子流中,窗口计算在逻辑上严格发生于上述子句求值完成之后。此外,窗口函数的执行时机晚于普通非窗口聚合函数,这意味着在窗口函数的输入参数中嵌套普通聚合函数调用是合法的,反之则不合法。

如果业务需要在窗口计算执行完毕后进一步对结果进行过滤或二次聚合,可以使用外层嵌套查询(sub-select)。例如:

SELECT depname, empno, salary, enroll_date
FROM
  (SELECT depname, empno, salary, enroll_date,
     row_number() OVER (PARTITION BY depname ORDER BY salary DESC, empno) AS pos
     FROM empsalary
  ) AS ss
WHERE pos < 3;

上述查询仅过滤并返回内部子查询中排名序号小于 3 的记录行(即各部门薪水排名前两位的员工)。

当查询中包含多个窗口函数时,虽然可以为每一个函数单独编写各自的 OVER 子句,但当多个函数需要采用完全相同的窗口切分逻辑时,逐一重复书写既繁琐又容易出错。SQL 允许在查询末尾通过 WINDOW 子句为窗口规则统一定义别名,随后在各函数的 OVER 中直接引用。示例:

SELECT sum(salary) OVER w, avg(salary) OVER w
  FROM empsalary
  WINDOW w AS (PARTITION BY depname ORDER BY salary DESC);

关于窗口函数的全面系统阐述,详见 Section 4.2.8、Section 9.22、Section 7.2.5 以及 SELECT 命令参考手册。



[5] SQL 标准还支持通过其他高级选项显式灵活定义窗口帧的滑动范围,本快速入门暂不展开。详见 Section 4.2.8。