SQL查询中的窗口函数(主要以 PostgreSQL 为例)

窗口函数 (Window Functions) 在与查询结果集相关的"窗口"上执行计算,并为结果集的每一行返回一个值。

啥是"窗口"?为什么我查询几条数据还有窗口啊。带着这个问题读完看看能明白吗

为什么需要窗口函数?

在传统 SQL 中,我们使用 GROUP BY 和聚合函数(如 SUM, AVG)进行数据汇总。但这种方式存在一个核心局限:聚合后行数会减少。比如按部门计算平均销售额,原始的员工个体销售记录就"消失"了。

想象以下场景:你想查看每位员工的销售额,并在旁边同时显示该员工所在部门的平均销售额。传统的 GROUP BY 无法一次完成这个任务,您可能需要复杂的自连接或子查询。

窗口函数 允许在不减少原始行数的情况下,进行分组、排序和计算聚合,实现个体数据和聚合数据的并行观察。

它的核心特性就是不会折叠行,它将聚合计算的结果"投影"回每一行,而不是将多行合并成一行。

下面的书写上关键字都用的大写,但小写也是可以的

语法

窗口函数通用形式:

sql 复制代码
function(args) OVER (
    [PARTITION BY ...]
    [ORDER BY ...]
    [frame_clause]
)

组成部分:

  • function:SUM, AVG, MAX, ROW_NUMBER, LAG 等

  • OVER():定义窗口范围

  • PARTITION BY:按分组计算(不会减少行)

  • ORDER BY:定义分组内排序

  • frame_clause:帧,用于指定计算所覆盖的行(后续详讲)

示例:

sql 复制代码
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()

如果 OVER() 为空:

sql 复制代码
AVG(score) OVER ()

表示全表为一个分区,全局平均值会出现在每一行。

PARTITION BY

分组但不减少行。示例:

sql 复制代码
SUM(amount) OVER (PARTITION BY user_id)

与 GROUP BY 不同:

GROUP BY PARTITION BY
压缩行,只返回一行 不压缩行,保留所有明细
聚合后无法看到明细 明细和统计值可共存
不支持排名/序列分析 能跨行分析、排名、累计

ORDER BY

窗口函数中的排序。用于:

  • 取上一行/下一行:LAG() / LEAD()

  • 排名:ROW_NUMBER() / RANK()

  • 计算累计:SUM() OVER(ORDER BY ...)

  • 计算差值、连续行为分析等

如果 ORDER BY 为空:

  • 对排名函数基本无意义(顺序不可控)

  • 对 SUM/AVG 等聚合窗口会让结果不稳定

窗口定义复用(WINDOW)

sql 复制代码
SELECT
    SUM(amount) OVER w,
    AVG(amount) OVER w
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY create_time);

减少重复代码,还更清晰,建议总是使用这种写法。

分类

接下来是窗口函数的重点。

窗口函数不是特别多,熟悉了语法以后也不会再嫌写得长。

排名类

1. ROW_NUMBER()

行号这个用得最多

sql 复制代码
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC)

2.RANK()

排名,这个用得也不少。排名字段如果值一样,排名也会一样,后面的排名会顺延(跳号)

sql 复制代码
 RANK() OVER (ORDER BY sales DESC)

3. DENSE_RANK()

也是排名,但是如果前面出现了一样的排名,不会跳号,而是紧挨着往下排。

4. NTILE(n)

分位,就是(近似)平均分组。比如分4组

sql 复制代码
NTILE(4) OVER (ORDER BY score)

分析类

1. LAG()

排序后取上一行的字段值

sql 复制代码
SELECT 
    sale_date,
    amount,
    LAG(amount) OVER (ORDER BY sale_date) AS previous_day_amount
FROM sales
ORDER BY sale_date;

它最多接受三个参数,第二个参数表示往前取第几个,默认往前一个;第三个参数是取不到的时候的默认值。

2. LEAD()

同前,取后面一行的字段值。

3. FIRST_VALUE()

返回窗口分区中的第一行的值

sql 复制代码
FIRST_VALUE(salary) OVER (
        PARTITION BY department 
        ORDER BY hire_date
    ) AS first_salary_in_dept

4. LAST_VALUE

不说你也知道了吧。

聚合类

将传统的聚合函数(SUM, AVG, COUNT, MAX, MIN)与 OVER 子句结合使用

sql 复制代码
-- 计算每个员工销售额,并同时显示部门平均值
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;

又或者简单点的

sql 复制代码
MAX(amount) OVER (PARTITION BY user_id)

分布类

用于计算当前行值在整个窗口或分区中的相对分布

1. PERCENT_RANK()

百分比排名, 计算当前行在分区中的百分比排名。结果范围 \(0 \le X < 1\)。

sql 复制代码
PERCENT_RANK() OVER (ORDER BY amount)

2. CUME_DIST()

累积分布,计算当前行值小于或等于分区中所有值的比例。结果范围 \(0 < X \le 1\)。

窗口帧 (Window Frame)

这是入门窗口函数的最后一块关键拼图了。

窗口帧是 OVER 子句的第三部分,用来定义当前行计算时需要考虑的物理行或逻辑值范围。

  • ROWS (基于行数): 关注物理行数。例如,"当前行之前的 2 行"。
  • RANGE (基于值域): 关注排序键的值域。例如,"当前行之前 \(N\) 个值"或"与当前行值相同的所有行"。当需要处理并列值时,RANGE 更有用。

前面的使用上我们好像没接触到窗口帧,是因为它有默认值:当 OVER 子句中包含 ORDER BY 但没有显式定义窗口帧时,PgSQL 使用的默认帧是: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

窗口帧的边界

  • CURRENT ROW,当前行
  • UNBOUNDED PRECEDING,分区内的第一行
  • UNBOUNDED FOLLOWING,分区内的最后一行
  • N PRECEDING,当前行之前 N 行/值
  • N FOLLOWING,当前行之后 N 行/值

跟分析类的4个函数很像是吧

通过这些边界我们看看默认值的含义:基于值的, 从分区第一行开始到当前行。

你能看到这里已经非常不容易,还要让你发现这里面的问题简直难于上青天!

这里有什么问题呢?

1. 同值同行帧问题

默认值的范围按排序字段的"值"划定的,不是按行!

如果当前行值 = 100,那之前所有 value <= 100 的都是帧内。

那咋了

sql 复制代码
SUM(amount) OVER (ORDER BY price)

你这样写可能是想一行一行的累加,但是由于是基于值的,如果数据如下

id price
1 100
2 100
3 101

在计算第一行的累积值时就已经包含了第二行的值,导致第一行和第二行都是200。

所以需要改成基于行的:

sql 复制代码
SUM(amount)
OVER (ORDER BY price ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

做为最佳实践:在 PostgreSQL 中尽量使用 ROWS,而非 RANGE

2. LAST_VALUE()恒等于CURRENT ROW

由于默认窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,如果不显式定义帧,LAST_VALUE() 看到的窗口截止到当前行,因此它通常只会返回当前行的值。

正确获取分区内最后一个值的方法:

sql 复制代码
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时总是强制指定


常用帧写法

(1)运行累计(running sum)

sql 复制代码
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

(2)滚动窗口(移动平均)

过去 2 行 + 当前行:

sql 复制代码
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

(3)只看上一行(环比/差值)

sql 复制代码
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING

(4)整组统计(等同于不写)

sql 复制代码
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

last but not least

最后说一些性能考量方面的东西。

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都有含义了,叫分段函数吧,歧义也大。所以只能使用"窗口"来统一语言了。

相关推荐
程序 代码狂人27 分钟前
开窗函数 集合运算 行列转换
sql
阿坤带你走近大数据35 分钟前
大数据行业中,什么是拉链表?具体怎么做?
大数据
数字化顾问1 小时前
(100页PPT)未来工厂大数据应用专题建设解决方案(附下载方式)
大数据
tiannian12202 小时前
如何选择适合企业的RFID系统解决方案?
大数据·人工智能
l1t2 小时前
达梦数据库和Oracle兼容性和性能比较
数据库·sql·oracle·达梦
程途拾光1582 小时前
绿色AI与低功耗推理架构
大数据·人工智能
G皮T3 小时前
【Elasticsearch】查询性能调优(三):track_total_hits 和 terminate_after 可能的冲突
大数据·elasticsearch·搜索引擎·全文检索·索引·性能·opensearch
weixin_436525073 小时前
NestJS-TypeORM QueryBuilder 常用 SQL 写法
java·数据库·sql
川西胖墩墩3 小时前
中文PC端跨职能流程图模板免费下载
大数据·论文阅读·人工智能·架构·流程图
TDengine (老段)3 小时前
TDengine 企业用户建表规模有多大?
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据