一般的商业数据库(其实也就是DB2,Oracle,SQL Server)都具备窗口函数这个功能,只不过名称不同,我比较熟悉的Oracle叫做分析函数,DB2好像叫做OLAP函数?
MySQL以前是不支持这个的,很多时候要实现一些功能就会非常麻烦,不过在8.0加入了这个功能,而且还比其他数据库多了一些有趣的支持模式。
以下内容部分参考:
https://zhuanlan.zhihu.com/p/49082967
窗口的概念
什么叫窗口?
窗口的概念非常重要,它可以理解为记录集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数,有的函数,随着记录不同,窗口大小都是固定的,这种属于静态窗口;有的函数则相反,不同的记录对应着不同的窗口,这种动态变化的窗口叫滑动窗口。
窗口函数和普通聚合函数也很容易混淆,二者区别如下:
聚合函数是将多条记录聚合为一条;而窗口函数是每条记录都会执行,有几条记录执行完还是几条。
聚合函数也可以用在窗口函数中,这个后面会举例说明。
窗口函数的格式:
函数名([expr]) over子句
其中,over是关键字,用来指定函数执行的窗口范围,如果后面括号中什么都不写,则意味着窗口包含满足where条件的所有行,窗口函数基于所有行进行计算;如果不为空,则支持以下四种语法来设置窗口:
- window_name:给窗口指定一个别名,如果SQL中涉及的窗口较多,采用别名可以看起来更清晰易读(主要是方便),例如:
SELECT
rank ( ) over w1
FROM
employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC )
很明显,from table后面加了一个关键字WINDOW,后面跟了一个子句,这样在select中就可以使用这个w1作为窗口了。
- partition子句:窗口按照那些字段进行分组,窗口函数在不同的分组上分别执行。比如上面例子中,就是用Company字段分组。(跟group by一个意思,但是这里用PARTITION BY)
- order by子句:按照哪些字段进行排序,窗口函数将按照排序后的记录顺序进行编号。可以和partition子句配合使用,也可以单独使用。
- frame子句:frame是当前分区的一个子集,子句用来定义子集的规则,通常用来作为滑动窗口使用,这是个很有意思的特性,不同于Oracle,例如:
SELECT
avg( Salary ) over w1,
Company,
Salary
FROM
employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC rows BETWEEN 1 preceding AND unbounded following )
这条语句的意思是,以Company分组窗口,Salary排序,从当前行的前一行到该组的最后一行,取平均Salary(工资)。
则有如下图:
注意Microsoft中的平均Salary,第一个行是60000,他没有前一行,最后一行是50000(因为是根据Company分组了的),那么平均Salary就是
(60000+60000+55000+50000)/4 = 56250。
而第二行也是60000,他的上一行是60000,到最后一行求平均值就是:
(60000+60000+55000+50000)/4 = 56250(不要混淆)
第三行是55000,他的上一行是60000,到最后一行,平均值就是:
(660000+55000+50000)/3 = 55000.
这种窗口范围的限定,因为是随着当前查询行的变化而变化的,就叫做动态窗口,对于动态窗口的范围指定,有两种方式,基于行和基于范围,具体区别如下:
- 基于行:通常使用BETWEEN frame_start AND frame_end语法来表示行范围,frame_start和frame_end可以支持如下关键字,来确定不同的动态行记录
CURRENT ROW 边界是当前行,一般和其他范围关键字一起使用
UNBOUNDED PRECEDING 边界是分区中的第一行
UNBOUNDED FOLLOWING 边界是分区中的最后一行
expr PRECEDING 边界是当前行减去expr的值
expr FOLLOWING 边界是当前行加上expr的值
比如,下面都是合法的范围:
rows BETWEEN 1 PRECEDING AND 1 FOLLOWING 窗口范围是当前行、前一行、后一行一共三行记录。
rows UNBOUNDED FOLLOWING 窗口范围是当前行到分区中的最后一行
rows BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 窗口范围是当前分区中所有行,等同于不写。
- 基于范围:和基于行类似,但有些范围不是直接可以用行数来表示的,比如希望窗口范围是一周前的订单开始,截止到当前行,则无法使用rows来直接表示,此时就可以使用范围来表示窗口:INTERVAL 7 DAY PRECEDING。Linux中常见的最近1分钟、5分钟负载是一个典型的应用场景。
有的函数不管有没有frame子句,它的窗口都是固定的,也就是前面介绍的静态窗口,这些函数包括如下:
CUME_DIST()
DENSE_RANK()
LAG()
LEAD()
NTILE()
PERCENT_RANK()
RANK()
ROW_NUMBER()
常用函数
窗口函数的核心还是在over前面的函数上,除了常用的聚合函数之外,还可以用一些特定的窗口函数,下面举例:
序号函数--row_number()/rank()/dense_rank()
用途:显示分区中的当前行号
SELECT
row_number ( ) over w1,
Company,
Salary
FROM
employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC rows BETWEEN 1 preceding AND unbounded following )
可以看到,根据Company进行分组之后,按照Salary倒序排列,给了行号,依次是1,2,3,4,如果有相同的,那么随机处理。
rank和dense_rank就是用来处理有相同情况的:
SELECT
row_number ( ) over w1,
rank ( ) over w1,
dense_rank ( ) over w1,
Company,
Salary
FROM
employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC rows BETWEEN 1 preceding AND unbounded following )
还是关注Microsoft,可以看到,rank是把相同Salary的取了同样的排名,再下一位的,按照实际行号直接取排名(于是变成了3),而dense_rank则是依次顺延,下一个是第二高的Salary,那么就是2.
dense_rank,密集排序,可以理解为需要更密集一点展示排名,所以就不按照行号来排。
分布函数--percent_rank()/cume_dist()
percent_rank()
用途:和之前的RANK()函数相关,每行按照如下公式进行计算:
(rank - 1) / (rows - 1)
其中,rank为RANK()函数产生的序号,rows为当前窗口的记录总行数
该函数可以用来计算分位数。
例子:(这里没有使用窗口的别名,感受一下正常使用这个函数的格式)
SELECT
percent_rank ( ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
结果有:
很明显,按照倒序排列,统计学中有个分位数的概念(有序数列中的百分之多少的数)。
总共5行,第2行就是(2-1)/(5-1) = 0.25,正好是25分位数,以此类推。
cume_dist()
用途:分组内小于等于当前rank值的行数/分组内总行数。
SELECT
cume_dist ( ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
注意是按照rank值来求值,而不是实际大小。
以第二行为例,rank值小于等于他的有第一行和第二行,总行数是5,那么有:2/5=0.4,所以就是0.4.
前后函数--lead(字段,n)/lag(字段,n)
这个是带参数的,要注意。
用途:分区中位于当前行前n行(lead)/后n行(lag)的记录值
SELECT
lag ( salary, 1 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
从图中可以看到,lag取的是当前行的上一个行的对应字段的值,当然我们也可以对lag中的字段进行计算(非聚合)。
lead就是当前行的下一个行的值,虽然有点难理解,但是只能记住,如下图:
SELECT
lead ( salary - 10000, 1 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
头尾函数--first_value (expr)/last_value(expr)
用途:得到分区中的第一个/最后一个指定参数的值
很好理解,就是求当前分组的第一个值和最后一个值(这个有order by决定。),注意求的是对应的expr(字段名)的值,而不是其他的值。
SELECT
first_value ( id ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
意思就是,当前分组中,按照Salary倒序排列,第一行是70000,这行对应的id是12.
SELECT
last_value ( Salary ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
而last中需要注意,指的是最后一行,是当前分组的最后一行,所以会出现如上的情况。
所以不能用于最大值最小值判断。
nth_value(expr,n)/ntile(n)
nth_value(expr,n)
用途:返回窗口中第N个expr的值,expr可以是表达式,也可以是列名
SELECT
nth_value ( Salary,3 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
第三个是60000的薪水,那么就显示60000,注意是分组内的每一行都有值。
ntile(n)
用途:将分区中的有序数据分为n个桶,记录桶号。
SELECT
ntile ( 3 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
数据分桶,根据先后顺序,把行分为n个桶,优先满足前面的行,图中要分为3个桶,因此最后一桶只有1行。
桶号就是1,2,3……
聚合函数作为窗口函数
用途:在窗口中每条记录动态应用聚合函数(sum/avg/max/min/count),可以动态计算在指定的窗口内的各种聚合函数值。
这就很好理解了,以avg为例:
SELECT
avg( Salary ) over ( PARTITION BY Company ORDER BY Salary DESC ),
Company,
Salary
FROM
employee
WHERE
Company = 'Microsoft'
按照分组的每一行,求累计的平均值。
通过这个,应该能明白窗口的含义。(根据over子句分组之后,挨个行进行select,并执行over前面的函数,因为over子句已经把记录变成了符合条件的一些行集,所以select的方式就改变了。)
窗口函数还是非常常用的,简单总结一下,备忘。
窗口函数