MySQL-窗口函数

一般的商业数据库(其实也就是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 
        employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC )
    

    很明显,from table后面加了一个关键字WINDOW,后面跟了一个子句,这样在select中就可以使用这个w1作为窗口了。

  • partition子句:窗口按照那些字段进行分组,窗口函数在不同的分组上分别执行。比如上面例子中,就是用Company字段分组。(跟group by一个意思,但是这里用PARTITION BY)
  • Salary 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 
        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 
        employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC rows BETWEEN 1 preceding AND unbounded following )
    
    image.png

    还是关注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 
        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 
        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 
        employee 
    WHERE
        Company = 'Microsoft'
    

    从图中可以看到,lag取的是当前行的上一个行的对应字段的值,当然我们也可以对lag中的字段进行计算(非聚合)。
    lead就是当前行的下一个行的值,虽然有点难理解,但是只能记住,如下图:

    SELECT
        lead ( salary - 10000, 1 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
        Company,
        Salary 
        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 
        employee 
    WHERE
        Company = 'Microsoft'
    

    用途:返回窗口中第N个expr的值,expr可以是表达式,也可以是列名

    SELECT
        nth_value ( Salary,3 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
        Company,
        Salary 
        employee 
    WHERE
        Company = 'Microsoft'
    

    用途:将分区中的有序数据分为n个桶,记录桶号。

    SELECT
        ntile ( 3 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
        Company,
        Salary 
        employee 
    WHERE
        Company = 'Microsoft'
    

    聚合函数作为窗口函数

    用途:在窗口中每条记录动态应用聚合函数(sum/avg/max/min/count),可以动态计算在指定的窗口内的各种聚合函数值。
    这就很好理解了,以avg为例:

    SELECT
        avg( Salary ) over ( PARTITION BY Company ORDER BY Salary DESC ),
        Company,
        Salary 
        employee 
    WHERE
        Company = 'Microsoft'