
























窗口函数 (Window Functions) 在与查询结果集相关的“窗口”上执行计算,并为结果集的每一行返回一个值。
啥是“窗口”?为什么我查询几条数据还有窗口啊。带着这个问题读完看看能明白吗
在传统 SQL 中,我们使用 GROUP BY 和聚合函数(如 SUM, AVG)进行数据汇总。但这种方式存在一个核心局限:聚合后行数会减少。比如按部门计算平均销售额,原始的员工个体销售记录就“消失”了。
想象以下场景:你想查看每位员工的销售额,并在旁边同时显示该员工所在部门的平均销售额。传统的 GROUP BY 无法一次完成这个任务,您可能需要复杂的自连接或子查询。
窗口函数 允许在不减少原始行数的情况下,进行分组、排序和计算聚合,实现个体数据和聚合数据的并行观察。
它的核心特性就是不会折叠行,它将聚合计算的结果“投影”回每一行,而不是将多行合并成一行。
下面的书写上关键字都用的大写,但小写也是可以的
窗口函数通用形式:
function(args) OVER (
[PARTITION BY ...]
[ORDER BY ...]
[frame_clause]
)
组成部分:
function:SUM, AVG, MAX, ROW_NUMBER, LAG 等
OVER():定义窗口范围
PARTITION BY:按分组计算(不会减少行)
ORDER BY:定义分组内排序
frame_clause:帧,用于指定计算所覆盖的行(后续详讲)
SELECT
user_id,
amount,
SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time)
AS running_total
FROM orders;
SUM(amount) OVER (PARTITION BY user_id ORDER BY create_time) 含义:
每个用户内部独立计算
按时间排序
累计求和
结果同时包含每条订单和累计结果。
如果 OVER() 为空:
AVG(score) OVER ()
表示全表为一个分区,全局平均值会出现在每一行。
分组但不减少行。示例:
SUM(amount) OVER (PARTITION BY user_id)
与 GROUP BY 不同:
| GROUP BY | PARTITION BY |
|---|---|
| 压缩行,只返回一行 | 不压缩行,保留所有明细 |
| 聚合后无法看到明细 | 明细和统计值可共存 |
| 不支持排名/序列分析 | 能跨行分析、排名、累计 |
窗口函数中的排序。用于:
取上一行/下一行:LAG() / LEAD()
排名:ROW_NUMBER() / RANK()
计算累计:SUM() OVER(ORDER BY ...)
计算差值、连续行为分析等
如果 ORDER BY 为空:
对排名函数基本无意义(顺序不可控)
对 SUM/AVG 等聚合窗口会让结果不稳定
SELECT
SUM(amount) OVER w,
AVG(amount) OVER w
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY create_time);
减少重复代码,还更清晰,建议总是使用这种写法。
接下来是窗口函数的重点。
窗口函数不是特别多,熟悉了语法以后也不会再嫌写得长。
行号这个用得最多
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC)
排名,这个用得也不少。排名字段如果值一样,排名也会一样,后面的排名会顺延(跳号)
RANK() OVER (ORDER BY sales DESC)
也是排名,但是如果前面出现了一样的排名,不会跳号,而是紧挨着往下排。
分位,就是(近似)平均分组。比如分4组
NTILE(4) OVER (ORDER BY score)
排序后取上一行的字段值
SELECT
sale_date,
amount,
LAG(amount) OVER (ORDER BY sale_date) AS previous_day_amount
FROM sales
ORDER BY sale_date;
它最多接受三个参数,第二个参数表示往前取第几个,默认往前一个;第三个参数是取不到的时候的默认值。
同前,取后面一行的字段值。
返回窗口分区中的第一行的值
FIRST_VALUE(salary) OVER (
PARTITION BY department
ORDER BY hire_date
) AS first_salary_in_dept
不说你也知道了吧。
将传统的聚合函数(SUM, AVG, COUNT, MAX, MIN)与 OVER 子句结合使用
-- 计算每个员工销售额,并同时显示部门平均值
SELECT
employee_name,
department,
sale_amount,
-- 在每个部门 (PARTITION BY department) 的窗口内计算平均销售额
AVG(sale_amount) OVER (PARTITION BY department) AS dept_avg_sale,
-- 计算该员工销售额占部门总销售额的百分比
sale_amount / SUM(sale_amount) OVER (PARTITION BY department) * 100 AS sales_percentage
FROM sales;
又或者简单点的
MAX(amount) OVER (PARTITION BY user_id)
用于计算当前行值在整个窗口或分区中的相对分布
百分比排名, 计算当前行在分区中的百分比排名。结果范围 \(0 \le X < 1\)。
PERCENT_RANK() OVER (ORDER BY amount)
累积分布,计算当前行值小于或等于分区中所有值的比例。结果范围 \(0 < X \le 1\)。
这是入门窗口函数的最后一块关键拼图了。
窗口帧是 OVER 子句的第三部分,用来定义当前行计算时需要考虑的物理行或逻辑值范围。
前面的使用上我们好像没接触到窗口帧,是因为它有默认值:当 OVER 子句中包含 ORDER BY 但没有显式定义窗口帧时,PgSQL 使用的默认帧是: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。
跟分析类的4个函数很像是吧
通过这些边界我们看看默认值的含义:基于值的, 从分区第一行开始到当前行。
你能看到这里已经非常不容易,还要让你发现这里面的问题简直难于上青天!
这里有什么问题呢?
默认值的范围按排序字段的“值”划定的,不是按行!
如果当前行值 = 100,那之前所有 value <= 100 的都是帧内。
那咋了
SUM(amount) OVER (ORDER BY price)
你这样写可能是想一行一行的累加,但是由于是基于值的,如果数据如下
| id | price |
|---|---|
| 1 | 100 |
| 2 | 100 |
| 3 | 101 |
在计算第一行的累积值时就已经包含了第二行的值,导致第一行和第二行都是200。
所以需要改成基于行的:
SUM(amount)
OVER (ORDER BY price ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
做为最佳实践:在 PostgreSQL 中尽量使用 ROWS,而非 RANGE
由于默认窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,如果不显式定义帧,LAST_VALUE() 看到的窗口截止到当前行,因此它通常只会返回当前行的值。
正确获取分区内最后一个值的方法:
SELECT
employee_name,
sale_amount,
FIRST_VALUE(sale_amount) OVER w AS first_sale,
LAST_VALUE(sale_amount) OVER (
PARTITION BY department
ORDER BY sale_date
-- 强制指定帧为整个分区
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS final_sale_in_dept
FROM sales
WINDOW w AS (PARTITION BY department ORDER BY sale_date);
做为最佳实践:在使用LAST_VALUE时总是强制指定
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
过去 2 行 + 当前行:
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
最后说一些性能考量方面的东西。
PG 对窗口函数优化很强:
排序一次,多窗口复用
窄帧(如 ROWS x PRECEDING)性能最好
宽帧(UNBOUNDED FOLLOWING 等)可能更重
RANGE 在重复值多时计算量更大
一般建议:
能用 ROWS 不用 RANGE
尽量只使用需要的帧,不要一律写 "UNBOUNDED"
仅在必要时排序: 如果不需要排名或偏移函数,可以省略 ORDER BY 子句,这会避免昂贵的排序操作。
使用 FILTER 子句减少聚合窗口函数的计算量: COUNT(sale_amount) FILTER (WHERE sale_amount > 10000) OVER (PARTITION BY department)
使用 WINDOW 子句避免重复定义。当多个窗口函数使用相同的分区和排序定义时,应使用 WINDOW 子句。这不仅提高了可读性和维护性,还允许 PgSQL 优化器更好地重用分区和排序结果,避免多次重复的昂贵操作。
回到开头的问题。
我觉得这个叫法并不清晰,当然也可能是翻译不够好。但是英语就叫"window"。
简单点说,窗口就是一个个的分组,或者叫分区。但是分组group,分区partition都有含义了,叫分段函数吧,歧义也大。所以只能使用“窗口”来统一语言了。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。