您可以在 PostgreSQL 或 TimescaleDB 中使用窗口函数对与当前行相关的行集(称为“窗口”)执行复杂计算。
窗口函数或分析函数使用数据库表中一行或多行的值来执行计算并返回值。
窗口函数与聚合函数的不同之处在于,行不会分组到单个输出中。在窗口函数中,每一行都可以保持独立,但函数不仅可以访问当前行中的数据。
窗口函数总是在查询后直接使用 OVER
子句。这个子句是窗口函数与普通函数的不同之处。OVER
子句通过确定查询中的多少行被分割到每个计算中来创建数据行中的窗口框架。当您使用窗口函数时,该行的值是根据与当前行位于同一分区中的所有行计算的。
您可以将窗口函数与 PARTITION BY
和 ORDER BY 一起使用。PARTITION BY
定义记录必须匹配才能成为窗口框架的一部分的条件。ORDER BY
确定记录的顺序。
OVER
、PARTITION BY
和 ORDER BY
语法
OVER ([PARTITION BY <columns>] [ORDER BY <columns>])
ROWS BETWEEN
用于指定相对于当前行的窗口框架。
ROWS BETWEEN
语法
OVER ([PARTITION BY <columns>] [ORDER BY <columns>] [ROWS BETWEEN <lower_bound> AND <upper_bound>])
ROWS BETWEEN
中的边界可以是以下五种情况中的任何一种
UNBOUNDED
PRECEDING
:当前行之前的所有行。
n PRECEDING
:当前行之前的 n 行。
CURRENT ROW
:仅当前行。
n FOLLOWING
:当前行之后的 n 行。
UNBOUNDED FOLLOWING
:当前行之后的所有行。
使用 WINDOW
创建一个窗口子句,将窗口函数与 SELECT
子句分开。
WINDOW
语法
OVER w FROM WINDOW w AS ([PARTITION BY <columns>] [ORDER BY <columns>] [ROWS BETWEEN <lower_bound> AND <upper_bound>])
对结果集中的所有行使用窗口函数
对窗口框架中的记录进行排序
对窗口框架中的记录进行分区
对窗口框架中的记录进行排序和分区
使用窗口子句
在窗口子句中使用 ROWS BETWEEN
这些示例使用名为 sales_data
的数据库表中的销售数据,如下所示
id | sale_time | branch | item | quantity | total |
1 | 2021-08-11 | 纽约 | 手表 | 1 | 100 |
2 | 2021-08-11 | 芝加哥 | 手表 | 2 | 200 |
3 | 2021-08-12 | 芝加哥 | 项链 | 3 | 600 |
4 | 2021-08-13 | 凤凰城 | 戒指 | 1 | 250 |
5 | 2021-08-13 | 纽约 | 戒指 | 1 | 250 |
6 | 2021-08-14 | 迈阿密 | 手表 | 2 | 200 |
如果在使用 OVER
时没有定义 PARTITION BY
、ORDER BY
或 ROWS
子句,则计算将针对包含记录集中所有行的窗口执行。以下是一个获取销售摘要的示例查询
SELECT branch, SUM(total) OVER() AS sum FROM sales_data;
结果
branch | sum |
纽约 | 1600 |
芝加哥 | 1600 |
芝加哥 | 1600 |
凤凰城 | 1600 |
纽约 | 1600 |
迈阿密 | 1600 |
sum 列中的金额是表中所有值的总和。
如果将 ORDER BY
子句与 OVER
结合使用,则会针对当前行和结果集中所有先前行执行聚合。这是因为,默认情况下,窗口框架使用 UNBOUNDED PROCEEDING
进行聚合。
此示例查询还获取销售摘要,但它按时间列对结果进行排序
SELECT branch, SUM(total) OVER(ORDER BY id) AS sum FROM sales_data;
结果
branch | sum |
纽约 | 100 |
芝加哥 | 300 |
芝加哥 | 900 |
凤凰城 | 1150 |
纽约 | 1400 |
迈阿密 | 1600 |
sum 中的金额是销售额的累积总计。
如果按包含重复值的列对结果进行排序,则结果会有所不同。例如
SELECT branch, SUM(total) OVER(ORDER BY sale_time) AS sum FROM sales_data;
结果
branch | sum |
纽约 | 300 |
芝加哥 | 300 |
芝加哥 | 900 |
凤凰城 | 1400 |
纽约 | 1400 |
迈阿密 | 1600 |
聚合总和仍然是累积总计,但与上一个示例不同。这是因为窗口包括所有先前行,还包括销售时间匹配的行。
PARTITION BY
在窗口框架中的作用类似于 GROUP BY
。它根据您设置的条件对所有结果进行分组。此示例使用 GROUP BY
获取数据中每个分支的销售总和
SELECT branch, SUM(total) AS sum FROM sales_data sd GROUP BY branch;
结果
branch | sum |
芝加哥 | 800 |
纽约 | 350 |
迈阿密 | 200 |
凤凰城 | 250 |
此示例在窗口框架上使用 PARTITION BY
SELECT id, branch, SUM(total) OVER(PARTITION BY branch) AS sum FROM sales_data;
结果
id | branch | sum |
2 | 芝加哥 | 800 |
3 | 芝加哥 | 800 |
6 | 迈阿密 | 200 |
1 | 纽约 | 350 |
5 | 纽约 | 350 |
4 | 凤凰城 | 250 |
两个示例中的总和相同,但第二个示例不需要对它们进行分组。
在 OVER
中同时使用 ORDER BY
和 PARTITION BY
时,可以指定应用窗口函数的每个分区中结果的顺序。此示例检索数据集中按位置划分的销售额累积总计
SELECT sale_time, branch, SUM(total) OVER(PARTITION BY branch ORDER BY sale_time) AS sum FROM sales_data;
结果
sale_time | branch | sum |
2021-08-11 | 芝加哥 | 200 |
2021-08-12 | 芝加哥 | 800 |
2021-08-14 | 迈阿密 | 200 |
2021-08-11 | 纽约 | 100 |
2021-08-13 | 纽约 | 350 |
2021-08-13 | 凤凰城 | 250 |
如果不想使用内联窗口函数,可以将其转换为窗口子句。以下是使用窗口子句重写的上一个示例查询。它以两种格式返回相同的结果。如果要在查询中使用多个窗口函数,这将非常有用
SELECT sale_time, branch, SUM(total) OVER w AS sum
FROM sales_data WINDOW w AS (PARTITION BY branch ORDER BY sale_time);
这些示例使用包含来自几个城市五天的降水量和温度数据的数据集。此数据位于名为 city_data
的表中
日期 | 城市 | 温度 | 降水量 |
2021-09-01 | 迈阿密 | 65.30 | 0.28 |
2021-09-01 | 亚特兰大 | 63.14 | 0.20 |
2021-09-02 | 迈阿密 | 64.40 | 0.79 |
2021-09-02 | 亚特兰大 | 62.60 | 0.59 |
2021-09-03 | 迈阿密 | 68.18 | 0.47 |
2021-09-03 | 亚特兰大 | 66.20 | 0.39 |
2021-09-04 | 迈阿密 | 68.36 | 0.00 |
2021-09-04 | 亚特兰大 | 67.28 | 0.00 |
2021-09-05 | 迈阿密 | 72.50 | 0.00 |
2021-09-05 | 亚特兰大 | 68.72 | 0.00 |
在窗口子句中使用 ROWS BETWEEN
时,ORDER BY
子句的工作方式略有不同。
在窗口框架中使用 ORDER BY
时,默认框架为 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。但是,如果不使用 ORDER BY
,则默认框架为 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
。
重要的是要考虑如何在窗口框架中使用 ORDER BY
子句,尤其是在同时使用 ROWS
子句时。
例如,如果要计算每个城市温度的三天移动平均值,可以使用以下查询
SELECT city, date, temperature,
AVG(temperature) OVER (
PARTITION BY city
ORDER BY date DESC
ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING) avg_3day
FROM city_data
ORDER BY city, date;
要获取每个城市温度的三天移动平均值,请首先按城市对窗口框架进行分区。然后,必须对每个城市分区中的日期进行排序,以便可以根据当前行的位置选择三天的行集。然后,可以按降序对日期进行排序,并使用当前行和接下来的两行来计算平均温度
结果
城市 | 日期 | 温度 | avg_3day |
亚特兰大 | 2021-09-01 | 63.14 | 63.14 |
亚特兰大 | 2021-09-02 | 62.60 | 62.87 |
亚特兰大 | 2021-09-03 | 66.20 | 63.98 |
亚特兰大 | 2021-09-04 | 67.28 | 65.36 |
亚特兰大 | 2021-09-05 | 68.72 | 67.4 |
迈阿密 | 2021-09-01 | 65.30 | 65.3 |
迈阿密 | 2021-09-02 | 64.40 | 64.85 |
迈阿密 | 2021-09-03 | 68.18 | 65.96 |
迈阿密 | 2021-09-04 | 68.36 | 66.98 |
迈阿密 | 2021-09-05 | 72.50 | 69.68 |
因为 ROWS
子句依赖于窗口框架中的 ORDER BY
子句,所以可以通过在窗口框架中按升序对日期进行排序,并使用当前行加上前两行来计算平均值来获得相同的结果,如下所示
SELECT city, date, temperature,
AVG(temperature) OVER (
PARTITION BY city
ORDER BY date ASC
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) avg_3day
FROM city_data
ORDER BY city, date;
CUME_DIST()
计算一组值中值的累积分布。此函数在统计分析中特别有用。
SELECT salesperson_id, COUNT(*), CUME_DIST() OVER (ORDER BY COUNT(*) DESC)
FROM sales
GROUP BY salesperson_id;
DENSE_RANK()
为窗口分区中的每一行分配一个排名,排名值之间没有间隙。
SELECT salesperson_id, COUNT(*), DENSE_RANK() OVER (ORDER BY COUNT(*) DESC)
FROM sales
GROUP BY salesperson_id;
要详细了解如何使用 RANK() 和 DENSE_RANK(),请查看 了解 PostgreSQL 中的 RANK() 和 DENSE_RANK()。
FIRST_VALUE()
返回有序值集中的第一个值。
SELECT product_name, sales, FIRST_VALUE(product_name) OVER (ORDER BY sales DESC)
FROM product_sales;
LAG()
从同一结果集中的上一行获取值。
SELECT product_name, sales, LAG(sales) OVER (ORDER BY sales)
FROM product_sales;
LAST_VALUE()
返回有序值集中的最后一个值。
SELECT product_name, sales, LAST_VALUE(product_name) OVER (ORDER BY sales DESC)
FROM product_sales;
LEAD()
从同一结果集中的下一行获取值。
SELECT product_name, sales, LEAD(sales) OVER (ORDER BY sales)
FROM product_sales;
NTILE(n)
将有序结果集分成 n 个大致相等的组。
SELECT product_name, sales, NTILE(4) OVER (ORDER BY sales)
FROM product_sales;
NTH_VALUE(n)
返回窗口框架第一行起的第 n 行的值。
SELECT product_name, sales, NTH_VALUE(product_name, 2) OVER (ORDER BY sales DESC)
FROM product_sales;
PERCENT_RANK()
计算一组值中值的百分比排名。
SELECT salesperson_id, COUNT(*), PERCENT_RANK() OVER (ORDER BY COUNT(*) DESC)
FROM sales
GROUP BY salesperson_id;
RANK()
为窗口分区中的每个不同行提供唯一排名。
SELECT salesperson_id, COUNT(*), RANK() OVER (ORDER BY COUNT(*) DESC)
FROM sales
GROUP BY salesperson_id;
ROW_NUMBER()
为窗口分区中的每一行分配唯一的行号。
SELECT salesperson_id, COUNT(*), ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC)
FROM sales
GROUP BY salesperson_id;
有关窗口函数以及如何在 PostgreSQL 中使用它们的更多信息,请参阅 PostgreSQL 文档。要了解有关 PostgreSQL 中如何处理窗口函数的更多信息,请参阅 PostgreSQL 文档的此部分。有关窗口函数语法的更多详细信息,请参阅 此部分。有关如何在查询中使用窗口函数的更多示例,请查看以下 Timescale 文档部分