MySQL-窗口函数

一般的商业数据库(其实也就是DB2,Oracle,SQL Server)都具备窗口函数这个功能,只不过名称不同,我比较熟悉的Oracle叫做分析函数,DB2好像叫做OLAP函数?

MySQL以前是不支持这个的,很多时候要实现一些功能就会非常麻烦,不过在8.0加入了这个功能,而且还比其他数据库多了一些有趣的支持模式。

以下内容部分参考:
https://zhuanlan.zhihu.com/p/49082967

窗口的概念

什么叫窗口?

窗口的概念非常重要,它可以理解为记录集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数,有的函数,随着记录不同,窗口大小都是固定的,这种属于静态窗口;有的函数则相反,不同的记录对应着不同的窗口,这种动态变化的窗口叫滑动窗口。

窗口函数和普通聚合函数也很容易混淆,二者区别如下:

聚合函数是将多条记录聚合为一条;而窗口函数是每条记录都会执行,有几条记录执行完还是几条。
聚合函数也可以用在窗口函数中,这个后面会举例说明。

窗口函数的格式:

函数名([expr]) over子句

其中,over是关键字,用来指定函数执行的窗口范围,如果后面括号中什么都不写,则意味着窗口包含满足where条件的所有行,窗口函数基于所有行进行计算;如果不为空,则支持以下四种语法来设置窗口:

  1. window_name:给窗口指定一个别名,如果SQL中涉及的窗口较多,采用别名可以看起来更清晰易读(主要是方便),例如:
SELECT
    rank ( ) over w1 
FROM
    employee window w1 AS ( PARTITION BY Company ORDER BY Salary DESC )

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

  1. partition子句:窗口按照那些字段进行分组,窗口函数在不同的分组上分别执行。比如上面例子中,就是用Company字段分组。(跟group by一个意思,但是这里用PARTITION BY)
  1. order by子句:按照哪些字段进行排序,窗口函数将按照排序后的记录顺序进行编号。可以和partition子句配合使用,也可以单独使用。
  1. 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(工资)。
则有如下图:


frame子句

注意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 )
row_number

可以看到,根据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 )
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 
FROM
    employee 
WHERE
    Company = 'Microsoft'

结果有:


percent_rank

很明显,按照倒序排列,统计学中有个分位数的概念(有序数列中的百分之多少的数)。
总共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'
cume_dist

注意是按照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取的是当前行的上一个行的对应字段的值,当然我们也可以对lag中的字段进行计算(非聚合)。
lead就是当前行的下一个行的值,虽然有点难理解,但是只能记住,如下图:

SELECT
    lead ( salary - 10000, 1 ) over ( PARTITION BY Company ORDER BY Salary DESC ),
    Company,
    Salary 
FROM
    employee 
WHERE
    Company = 'Microsoft'
lead

头尾函数--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'
first_value

意思就是,当前分组中,按照Salary倒序排列,第一行是70000,这行对应的id是12.

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

而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'
nth_value

第三个是60000的薪水,那么就显示60000,注意是分组内的每一行都有值。

ntile(n)

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

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

数据分桶,根据先后顺序,把行分为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'
avg( Salary )

按照分组的每一行,求累计的平均值。

通过这个,应该能明白窗口的含义。(根据over子句分组之后,挨个行进行select,并执行over前面的函数,因为over子句已经把记录变成了符合条件的一些行集,所以select的方式就改变了。)

窗口函数还是非常常用的,简单总结一下,备忘。

窗口函数

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • 分析函数,也称为窗口函数,通常被认为仅对数据仓库SQL有用。使用分析函数的查询,基于对数据行的分组来计算总量值。与...
    猫猫_tomluo阅读 3,289评论 3 18
  • 2017/3/14 RDBMS:关系型数据库管理系统 关系模型独立于语言 SQL有几种不同类型的语言:数据定义语言...
    ancherl阅读 1,569评论 0 6
  • 高级函数_分析函数与窗口函数 分析函数往往与窗口函数一起使用,over()为窗口函数 一、分析函数 1.01、排名...
    lingoling阅读 1,083评论 0 2
  • 英文代码以及讲解来自datacamp窗口的概念非常重要,它可以理解为记录集合,窗口函数也就是在满足某种条件的记录集...
    鲸鱼酱375阅读 6,379评论 0 3
  • 阵阵冷风吹过, 树叶黄了落了, 草也枯了, 唯有那金灿灿的野菊花恣意地开放。 不在花坛里, 不住盆器中, 山坡上,...
    d91c9d8ed61d阅读 515评论 2 4