Window function 的语义
在一个 SELECT 语句里,window function 作用于 GROUP BY、HAVING之后,SELECT DISTINCT、ORDER BY、LIMIT 之前,其主要作用是,对输入的每一行数据,设定一个包含前后数据的“窗口”,在这个窗口内计算聚合函数以及一些特定的窗口函数的值。
这里通过一个例子展示窗口函数的用途。
mysql> SELECT
-> year, country, product, profit,
-> SUM(profit) OVER(PARTITION BY country) AS country_profit
-> FROM sales
-> ORDER BY country, year, product, profit;
+------+---------+---------+--------+----------------+
| year | country | product | profit | country_profit |
+------+---------+---------+--------+----------------+
| 1999 | FINLAND | P0 | 100 | 300 |
| 2000 | FINLAND | P1 | 100 | 300 |
| 2001 | FINLAND | P2 | 100 | 300 |
| 1999 | UK | P0 | 100 | 100 |
| 1999 | USA | P0 | 100 | 200 |
| 2000 | USA | P1 | 100 | 200 |
+------+---------+---------+--------+----------------+
上面的 SELECT 获取了 sales 表里面的所有数据,并通过窗口函数 SUM(profit) OVER(PARTITION BY country) 为每一行添加了“当前这一行所对应的 country 的 total profit”,也就是说,这个窗口“覆盖”了当前这一行所对应 country 的所有数据;
为了避免 window function / window operator / window expression / window 等称呼的歧义,我们把SUM(profit) OVER (PARTITION BY country) 这样的 clause 称为 window function(窗口函数),它的实现称为 window operatorSUM(profit) 这样的表达式称为 window expression (window 表达式)(PARTITION BY country) 这样一个窗口的定义称为 window(窗口)
在窗口函数里,如何定义“窗口”,是关键。一个窗口的定义包含“三要素”(partition、order、frame):
window_spec:
[window_name] [partition_clause] [order_clause] [frame_clause]
比如:
(PARTITION BY column_set ORDER BY column_set ROWS BETWEEN ... )
或者
(PARTITION BY column_set ORDER BY column_set RANGE BETWEEN ...)
用户通过
- PARTITION BY 定义如何对输入数据进行分区;如果没有定义则所有输入数据成为一个分区;
- ORDER BY 定义每个分区里的数据如何排序;如果没有定义则默认不排序;
- ROWS BETWEEN / RANGE BETWEEN 定义窗口起始和结束位置,也称为一个 "frame";如果没有定义 frame 且没有 order by 则默认的 frame 是整个分区;如果没有定义 frame 但是定义了 order by 则默认的 frame 是分区第一行到 current row;需要注意,每一行的 frame 都不能越过这一行所在的分区;
下图中展示了窗口的几个概念:首先将输入数据按照 partition by 进行分区,然后按照 order by 对每个分区的数据进行排序,最后通过 ROWS BETWEEN 或者 RANGE BETWEEN 为当前行指定窗口的起始和结束;图中灰色的一行就是当前行(current row),当窗口滑动到下一行时,下一行变成当前行,窗口的起始和结束也随之变化;
其中,frame 的定义有两种方式
- ROWS BETWEEN 选择前后几行;比如 ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示往前 3 行到往后 3 行,一共 7 行数据 (如果前面或者后面遇到了 partition 的边界,则少于 7 行)
- RANGE BETWEEN 根据数据范围选择前后几行;比如 RANGE BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示所有值在 [val−3, val+3] 这个范围内的行,val 为当前行的值 (同样地,也不能越过 partition 的边界)
通过上述窗口的定义,用户就可以通过
SELECT
col0,
col1,
window_expr0(...) over (window0),
window_expr1(...) over (window1)
FROM
tables;
为 tables 中的每一行增加一个作用于 window0 的 window_expr0 的值,以及作用于 window1 的 window_expr1 的值;MYSQL 支持的所有窗口表达式在这里可以找到;另外,大多数的聚合函数都可以用作窗口表达式。
不难看出,窗口函数与 aggr/groupby 操作的区别是:aggr / groupby 操作会将多行聚合为一行或几行,而窗口函数不改变数据的行数,只会为每一行增加一个或多个窗口表达式的值。
窗口函数只能出现于 SELECT 以及 ORDER BY clause 中。
关于窗口函数,可以参考 MySQL 官方文档对 window function 的描述:https://dev.mysql.com/doc/refman/8.0/en/window-functions.html这篇 paper(的前几个章节)也是不错的参考:Efficient Processing of Window Functions in Analytical SQL Queries
MySQL 对 window function 的支持
SQL 标准里面定义的 window function 的语法比较灵活,然而 mysql (以及其他一些商业数据库)并不完全支持所有的 window function 语义[MYSQL LIMITATION AND RESTRICTIONS];这意味着 IMCI 不需要实现所有的 window function 的语义,能够简化我们的设计;
在 MySQL 不支持的相关特性中,下列三条是比较重要的:
- aggregate window expression 中,不能带有 DISTINCT;i.e., 不支持 SUM(DISTINCT col0) OVER (window) 这样的语义;
- 不支持根据 current row 的值动态指定 frame 的边界;i.e., 不支持 ORER BY col ROWS BETWEEN col - 1 AND col + 1; 这样的窗口;
- 不支持嵌套的 window function
简单地对比一下其他数据库对 window function 的支持:
SQL SERVER
SQL SERVER 也不支持上面 3 条特性;总体上来说,SQL SERVER 对 window function 的支持度甚至没有 mysql 高 [SQL SERVER LIMITATION AND RESTRICTIONS]。
SQL SERVER 甚至只能在 RANGE BETWEEN 中使用 UNBOUNDED 和 CURRENT ROW 来指定 frame 的起始和结束i.e., 不支持 RANGE BETWEEN 1 PRECEDING AND CURRENT ROW 这样的操作(这基本上就等同于不支持 RANGE BETWEEN 这种模式了,因为可以用 ROWS BETWEEN 来表达同样的语义);这个约束使得它的所有 frame 的起始和结束位置都与 current row 的数据没有关系,大大简化了它对 window function 的实现;
实际上,SQL SERVER 里面甚至没有一个 window operator,它只增加了一个“window spool”即可实现 window function;
POSTGRESQL
从文档和使用体验来看,PG 对 window function 的支持程度跟 MySQL 差不多;同样不支持上面 3 条特性;
ORACLE
从文档和使用体验来看,相比于上述三款数据库,oracle 对 window function 的支持程度是最高的;支持 aggregate window function 中带有 DISTINCT;支持根据 current row 的值动态指定 frame 的边界;不过也不支持嵌套的 window function;




