Skip to content

窗口函数

INFO

要点概述窗口函数是 PostgreSQL 中的强大特性,允许你对行集合执行计算,同时保留每行的独立身份。它们在数据分析、排名、累计计算等场景中极为有用。

什么是窗口函数?

窗口函数(Window Functions)是一类特殊的函数,它们能够在保留每行独立标识的同时,对与当前行相关的一组表行执行计算。与普通聚合函数不同,窗口函数不会将多行合并为一行,而是为每一行生成一个结果。

窗口函数为数据分析提供了强大的功能,使我们能够在同一个查询中既显示详细数据又显示汇总数据,极大简化了复杂报表的编写。

窗口函数的基本语法

sql
SELECT 列名,
       窗口函数() OVER (
           [PARTITION BY 分组列]
           [ORDER BY 排序列]
           [窗口帧定义]
       )
FROM 表名;

核心组件解析:

  1. 窗口函数():可以是专用窗口函数(如rank()dense_rank()等)或聚合函数(如sum()avg()等)
  2. OVER子句:将普通函数转变为窗口函数的关键
  3. PARTITION BY:定义行分组方式,类似于GROUP BY但不合并行
  4. ORDER BY:定义窗口内行的排序方式
  5. 窗口帧定义:指定计算范围(默认由系统设定)

窗口函数的分类

PostgreSQL 中的窗口函数可以分为两大类:

类型描述常见函数
排名函数根据指定条件对数据行进行排名rank(), dense_rank(), row_number(), ntile()
值函数访问前后行的值或计算聚合值lag(), lead(), first_value(), last_value(), nth_value()
聚合窗口函数在窗口内执行聚合计算sum(), avg(), count(), max(), min()

PARTITION BY:数据分组

PARTITION BY 子句将数据集划分为多个分区(组),窗口函数会在每个分区内单独计算。这类似于 GROUP BY,但不会减少结果集中的行数。

示例:按部门分组计算平均工资

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

输出结果:

  depname  | empno | salary |      dept_avg_salary
-----------+-------+--------+-----------------------
 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

理解分区操作在上面的例子中:

  1. 数据按 depname 列分为三个分区:develop、personnel 和 sales
  2. 对每个分区分别计算平均工资
  3. 每行都保留原始数据,并附加了所在分区的平均值

ORDER BY:窗口内排序

在窗口函数中,ORDER BY 不仅决定结果的排序,更重要的是它定义了窗口函数计算时的行顺序和窗口帧范围。

示例:部门内工资排名

sql
SELECT
    depname,
    empno,
    salary,
    rank() OVER (PARTITION BY depname ORDER BY salary DESC) AS salary_rank
FROM
    empsalary;

输出结果:

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

分析:

  1. 数据首先按 depname 分组
  2. 在每个部门内,按 salary 降序排列
  3. rank() 函数为每个部门内的员工分配排名
  4. 注意相同工资的员工获得相同排名,下一个排名会跳过(如开发部门的排名是 1,2,2,4,5)

窗口帧:计算的行范围

窗口帧(Window Frame)定义了窗口函数计算时考虑的行范围。默认情况下:

  • 如果有 ORDER BY:帧包括从分区开始到当前行的所有行,以及与当前行相等的任何后续行
  • 如果没有 ORDER BY:帧包括分区中的所有行

示例 1:无 ORDER BY 的窗口帧(整个分区)

sql
SELECT
    salary,
    sum(salary) OVER () AS total_sum
FROM
    empsalary;

输出结果:

 salary | total_sum
--------+-----------
   5200 |     47100
   5000 |     47100
   3500 |     47100
   4800 |     47100
   3900 |     47100
   4200 |     47100
   4500 |     47100
   4800 |     47100
   6000 |     47100
   5200 |     47100

分析:

  • PARTITION BYORDER BY,整个表作为一个分区
  • 窗口帧是整个表的所有行
  • 每行显示相同的总和(47100)

示例 2:有 ORDER BY 的窗口帧(累计计算)

sql
SELECT
    salary,
    sum(salary) OVER (ORDER BY salary) AS running_total
FROM
    empsalary;

输出结果:

 salary | running_total
--------+--------------
   3500 |          3500
   3900 |          7400
   4200 |         11600
   4500 |         16100
   4800 |         25700
   4800 |         25700
   5000 |         30700
   5200 |         41100
   5200 |         41100
   6000 |         47100

分析:

  • ORDER BY 但无 PARTITION BY
  • 窗口帧是从表的开始到当前行(包括与当前行 salary 相等的行)
  • 结果显示累计总和(running total)

自定义窗口帧

可以使用以下语法明确定义窗口帧:

sql
OVER (... ROWS|RANGE BETWEEN frame_start AND frame_end)

常用的窗口帧定义:

窗口帧定义描述
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW从分区开始到当前行(默认,当有 ORDER BY 时)
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING从当前行到分区结束
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING整个分区(默认,当无 ORDER BY 时)
ROWS BETWEEN n PRECEDING AND CURRENT ROW当前行及其前 n 行
ROWS BETWEEN CURRENT ROW AND n FOLLOWING当前行及其后 n 行
ROWS BETWEEN n PRECEDING AND n FOLLOWING当前行及其前后各 n 行

示例:移动平均值计算

移动平均值计算是窗口函数在数据分析中的经典应用,它展示了如何使用窗口帧来定义计算范围,从而实现复杂的统计分析功能。

INFO

什么是移动平均值?移动平均值(Moving Average)是通过计算一个数据点及其相邻数据点的平均值来平滑数据波动的方法。它可以帮助识别数据的整体趋势,减少随机波动的影响。

sql
SELECT
    salary,
    avg(salary) OVER (
        ORDER BY salary
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING   ---当前行前后各1行
    ) AS moving_avg
FROM
    empsalary;
  • 实际计算过程模拟:假设我们有以下工资数据(按工资排序):
行号salary计算窗口moving_avg 计算过程
13500[3500, 3900](3500 + 3900) ÷ 2 = 3700
23900[3500, 3900, 4200](3500 + 3900 + 4200) ÷ 3 = 3866.67
34200[3900, 4200, 4500](3900 + 4200 + 4500) ÷ 3 = 4200
44500[4200, 4500, 4800](4200 + 4500 + 4800) ÷ 3 = 4500
54800[4500, 4800, 4800](4500 + 4800 + 4800) ÷ 3 = 4700
64800[4800, 4800, 5000](4800 + 4800 + 5000) ÷ 3 = 4866.67
75000[4800, 5000, 5200](4800 + 5000 + 5200) ÷ 3 = 5000
85200[5000, 5200, 5200](5000 + 5200 + 5200) ÷ 3 = 5133.33
95200[5200, 5200, 6000](5200 + 5200 + 6000) ÷ 3 = 5466.67
106000[5200, 6000](5200 + 6000) ÷ 2 = 5600

移动平均值的优势

  1. 平滑数据:减少数据中的随机波动和噪音
  2. 趋势识别:更容易看出数据的整体趋势方向
  3. 异常值处理:极端值对结果的影响被周围值稀释
  • 向前移动平均值
sql
-- 计算股票的5日移动平均价格
SELECT
    trade_date,
    close_price,
    avg(close_price) OVER (
        ORDER BY trade_date
        ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
    ) AS ma_5_days
FROM
    stock_prices;

常用窗口函数案例

排名函数比较

sql
SELECT
    salary,
    row_number() OVER (ORDER BY salary DESC) AS row_num,
    rank() OVER (ORDER BY salary DESC) AS rank_val,
    dense_rank() OVER (ORDER BY salary DESC) AS dense_rank_val,
    percent_rank() OVER (ORDER BY salary DESC) AS percent_rank_val
FROM
    empsalary;

各排名函数的区别:

排名函数对比

  • row_number():分配唯一序号(1,2,3,4...)
  • rank():相同值获得相同排名,排名有间隔(1,2,2,4...)
  • dense_rank():相同值获得相同排名,排名无间隔(1,2,2,3...)
  • percent_rank():计算百分比排名(0 到 1 之间)

获取前后行的值

sql
SELECT
    empno,
    salary,
    lag(salary) OVER (ORDER BY salary) AS prev_salary,
    lead(salary) OVER (ORDER BY salary) AS next_salary
FROM
    empsalary;

计算同比增长率

sql
SELECT
    date_trunc('month', date) AS month,
    sales,
    lag(sales) OVER (ORDER BY date_trunc('month', date)) AS prev_month_sales,
    (sales - lag(sales) OVER (ORDER BY date_trunc('month', date))) /
        lag(sales) OVER (ORDER BY date_trunc('month', date)) * 100 AS growth_rate
FROM
    monthly_sales;

窗口函数的使用限制

使用限制窗口函数只能出现在以下位置:

  • SELECT 列表(投影列)
  • ORDER BY 子句

不允许在以下位置使用:

  • WHERE 子句
  • GROUP BY 子句
  • HAVING 子句
  • 其他位置

这是因为窗口函数在逻辑上是在这些操作之后执行的。窗口函数也总是在非窗口聚合函数之后执行。

使用子查询处理窗口函数结果

如果需要基于窗口函数的结果进行过滤,可以使用子查询:

sql
-- 获取每个部门薪资排名前2的员工
SELECT
    depname,
    empno,
    salary,
    enroll_date
FROM (
    SELECT
        depname,
        empno,
        salary,
        enroll_date,
        rank() OVER (PARTITION BY depname ORDER BY salary DESC) AS pos
    FROM
        empsalary
) AS ss
WHERE
    pos <= 2;

使用命名窗口

当查询中有多个窗口函数使用相同的窗口定义时,可以使用WINDOW子句为窗口定义命名,然后在OVER中引用:

sql
SELECT
    depname,
    empno,
    salary,
    sum(salary) OVER w AS dept_total,
    avg(salary) OVER w AS dept_avg,
    rank() OVER w AS dept_rank
FROM
    empsalary
WINDOW
    w AS (PARTITION BY depname ORDER BY salary DESC);

这样可以减少代码重复,提高可读性和可维护性。

窗口函数的实际应用场景

1. 销售数据分析

sql
-- 计算每个产品在各个地区的销售百分比
SELECT
    product_name,
    region,
    sales,
    sales / sum(sales) OVER (PARTITION BY product_name) * 100 AS region_percent
FROM
    sales_data;

2. 时间序列分析

sql
-- 计算月度销售额及同比增长
SELECT
    date_trunc('month', order_date) AS month,
    sum(amount) AS monthly_sales,
    sum(amount) - lag(sum(amount)) OVER (ORDER BY date_trunc('month', order_date)) AS sales_growth,
    (sum(amount) - lag(sum(amount)) OVER (ORDER BY date_trunc('month', order_date))) /
        lag(sum(amount)) OVER (ORDER BY date_trunc('month', order_date)) * 100 AS growth_percent
FROM
    orders
GROUP BY
    date_trunc('month', order_date)
ORDER BY
    month;

3. 数据分组和分布分析

sql
-- 将客户按消费金额分为4个等级组
SELECT
    customer_id,
    customer_name,
    total_spent,
    ntile(4) OVER (ORDER BY total_spent DESC) AS spending_quartile
FROM
    customer_spending;

窗口函数 vs GROUP BY

对比 窗口函数:

  • 保留所有行
  • 每行可以访问相关行的信息
  • 可以在同一查询中显示详细数据和聚合数据

GROUP BY:

  • 将多行合并为一行
  • 每个分组只返回一行
  • 无法同时显示详细数据和聚合数据

窗口函数执行顺序

在 SQL 查询处理中,窗口函数的执行顺序如下:

总结

核心要点

  1. 窗口函数允许在保留行独立性的同时进行计算,是数据分析的强大工具
  2. OVER 子句是窗口函数的核心,可包含 PARTITION BY 和 ORDER BY
  3. PARTITION BY 定义分组方式,ORDER BY 定义排序和窗口帧范围
  4. 窗口帧定义了计算涉及的行范围,可以自定义
  5. 窗口函数只能在 SELECT 列表和 ORDER BY 子句中使用
  6. 使用子查询可以基于窗口函数结果进行过滤
  7. WINDOW 子句可以为重复使用的窗口定义命名

窗口函数是进行高级数据分析的强大工具。掌握它们可以帮助你编写更简洁、更高效的 SQL 查询,特别是在需要同时展示详细数据和汇总数据的场景下。

通过深入理解窗口函数,你可以极大地提升数据分析能力,简化复杂报表的编写,更加优雅地解决各种数据分析问题。