本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net
MySQL 从 8.0 开始支持窗口函数(Window Functions),因其经常被用在数据分析的场景,窗口函数也被称为分析函数(Analytic Functions)。
一、窗口函数概念
窗口的意思是将数据进行分组,每个分组即是一个窗口,这和使用聚合函数时的 group by 分组类似,但与聚合函数不同的地方是:
- 聚合函数(例如:sum/avg/min/max)会针对每个分组(窗口)聚合出一个结果(每一组返回一个结果)。
- 窗口函数会对每一条数据进行计算,并不会使返回的数据变少(每一行返回一个结果)。
窗口函数会逐行计算,其重点是计算当前行与窗口内其他成员之间的关系,例如:组内排序,累积分布等。
窗口函数可以分为两类:
- 一类既可以做为聚合函数,也可以作为窗口函数,当函数单独使用时是聚合函数,当与 over 关键字同时使用时作为窗口函数。
- 另一类是专用窗口函数,他们必须与 over 关键字同时使用。
判断一个函数是不是窗口函数只需要盯着是否有 over 关键字即可。
二、基础语法
窗口函数在使用时需要定义一个窗口(分组),然后对每一行应用窗口函数,正在计算的这行叫做 "当前行"(current row)。
2.1 窗口的定义
在使用聚合函数的时候我们通过 group by 关键字来定义如何分组,而窗口函数是通过 over 关键字和 partition by 关键字来定义分组(这里的 partition by 是分组的意思,和分区表没有任何关系)。
下面用一组示例来说明,先创建实验数据:
create table wf_example(
id smallint unsigned not null auto_increment primary key,
wind varchar(32),
val smallint);
insert into wf_example values
(null,'Window_A',1),
(null,'Window_A',2),
(null,'Window_A',2),
(null,'Window_A',3),
(null,'Window_A',3),
(null,'Window_A',3),
(null,'Window_B',100),
(null,'Window_B',200),
(null,'Window_B',300),
(null,'Window_B',400),
(null,'Window_B',500);
select * from wf_example;
对于 sum() 函数我们先作为聚合函数,按照 wind 列进行分组,求 val 列的和:
select wind,sum(val) from wf_example group by wind;
这是最简单的聚合函数示例,sum 按 wind 列对数据分组后求和,每组分别返回一条数据。
下面在将 sum() 作为窗口函数使用:
select wind,
sum(val) over () total_sum,
sum(val) over (partition by wind) group_sum
from wf_example;
这里我们在 sum 后增加了一个 over 关键字(代表 sum 作为窗口函数),over 关键字后面括号中即是窗口定义。
从返回结果我们看到 sum() 针对每一行都进行计算并返回了数据:
- total_sum 中我们在 over 后面使用了一个空括号,没有明确的窗口划分,代表所有数据作为一组。
- group_sum 中,我们通过 partition by wind 表示通过 wind 列进行分组,可以看到求和被限制在 Window_A 和 Window_B 组内。
2.2 命名窗口
当一个窗口被多次引用的时候,在每个 over 后面都写一遍定义就显得有些繁琐了,此场景可以通过命名窗口优化:一次定义,多次引用。
命名窗口的定义是通过 window wind_name as () 来进行定义的,括号内的部分就是原 over 子句后的窗口定义,在用 over 关键字调用窗口时,直接引用窗口名 wind_name 即可:
select wind,
sum(val) over w group_sum -- 通过名称 w 引用窗口
from wf_example
window w as (partition by wind); -- 命名窗口定义
通常情况下使用时只需要直接引用窗口名称即可,有时需要对窗口进一步加工,例如排序等,可以用括号将窗口名扩起来,后面跟上 order by 子句:
select wind,
first_value(val) over (w order by val desc) first_val_desc, -- 通过窗口名引用,并降序排列
first_value(val) over (w order by val asc) first_val_asc -- 通过窗口名引用,并升序排列
from wf_example
window w as (partition by wind); -- 命名窗口定义
三、专用窗口函数
除了上面示例的 sum,其他的聚合函数例如 count, max, min, avg 等都可以作为窗口函数使用,但是专用窗口函数在数据分析中更加常用。专用窗口函数在使用时必须搭配 over 关键字。
3.1 row_number()
语法:row_number() over window
没有参数,返回当前行在组内的位置编号,从 1 开始,order by 子句会影响行的编号顺序,如果没有 order by,那么行的编号是不确定的。另外,即使行完全相同,它们的编号也是不同的,这点和后面的 rank() 和 dense_rank() 不同。
select wind, val,
row_number() over w 组内编号
from wf_example
window w as (partition by wind);
上面示例中:
- 每一行都有唯一的编号,从 1 开始,即使数据完全相同,编号也不同。
3.2 rank()
语法:rank() over window
没有参数,返回当前行在组内的排序,排序带间隙(排名数字不连续),在 partition by 后面可以跟上 order by 子句来指定按某列排序,示例中按照 val 值升序排列:
select wind,val,
rank() over (partition by wind order by val) 带间隙排序
from wf_example;
上面示例中:
- order by val 定义了按照 val 的值排序,注意相同的值,排序也相同,如果没有 order by val 子句,那么所有行的排序都是 1。
- Window_A 中,由于存在两个 2,因此下一个排序是 4,排序存在间隙。
3.3 dense_rank()
语法:dense_rank() over window
没有参数,返回当前行在组内的排序,排序不带间隙(排名是连续的):
select wind, val,
rank() over (partition by wind order by val) 带间隙排序,
dense_rank() over (partition by wind order by val) 不带间隙排序
from wf_example;
上面示例中:
- Window_A 的组内排序中,虽然存在 2 个 2,下一个排序依然是 3,排序不存在间隙。
3.4 percent_rank()
语法:percent_rank() over window
百分比排序,返回当前行在组内的百分比位置,返回值范围为 [0, 1],可以用 当前行排序 /(行数 - 1) 计算得出,但与 rank 不同,这里排序是从 0 开始而不是从 1(相当于 rank-1),因此第 1 行的百分比位置是 0%,相当于:(rank-1)/(rows-1)。
select wind,val,
rank() over (partition by wind order by val) 带间隙排序,
percent_rank() over (partition by wind order by val) 百分比排序
from wf_example;
上面示例中:
- window_A 中第 1 行 rank 为 1,组内 rows 为 6,代入 (rank-1)/(rows-1) 得到 percent_rank 为 (1-1)/(6-1),结果为 0.
- Window_A 中第 3 行 rank 为 2(和第 2 行并列第 2),组内 rows 为 6,代入 (rank-1)/(rows-1) 得到 percent_rank 为 (2-1)/(6-1),结果为 0.2。
- window_B 中最后一行 rank 为 5,rows 为 5,代入 (rank-1)/(rows-1) 得到 percent_rank 为 (5-1)/(5-1),结果为 1.
3.5 cume_dist()
语法:cume_dist() over window
累积分布(cumulative distribution)。返回 "当前行之前" 与 "和当前行相等(包含当前行)" 的行数,占组内行数的百分比。
(当前行及之前行数 + 和当前行相等的行数)/ 组内数量,这个和 rank 类似,但是统计的是一个累积的比例,数据分布从 0 到 1.
select wind,val,
rank() over w 行编号,
percent_rank() over w 百分比排序,
cume_dist() over w 累积比例
from wf_example
window w as (partition by wind order by val);
上面示例中:
- Window_A 中,第 1 行为独立一行,前面没有行,也没有和自己相同的行,因此累积数量为 1,累积比例为:1/6,即 0.166666666
- Window_A 中,第 2 行,累积数量为:前面的 1 行,自己第 2 行,和自己相等的第 3 行,因此累积数量为 3,累积比例为:3/6,即 0.5
- Window_A 中,第 4 行,累积数量为:前面的 3 行,自己第 4 行,和自己相等的第 5,6 行,因此累积数量为 6,累积比例为:6/6,即 1。
- Window_B 中,每一行都是独立的,不存在和自己相同的行,行编号就包含了自己及之前所有的行,因此每行的累积比例都是:行编号 / 组内行数
3.6 first_value()
语法:first_value(expr) over window
返回当前框架内第一行计算出的 expr 值,第一个值会随着排序定义而不同。
这里引入了一个重要概念:框架(frame),框架是一个动态的概念,是组的子集,first_value 中每一行的默认框架由 3 部分组成:
- 当前行之前的所有行
- 当前行
- 和当前行相同的行
这 3 部分相当于框架定义:range between unbound preceding and current row,框架定义语法在后面解释。
select wind,val,
first_value(val) over (w order by val desc) 指定列倒序框架内第一个值,
first_value(val) over (w order by val asc) 指定列正序框架内第一个值
from wf_example
window w as (partition by wind);
上面示例中:
- window_B 中当 order by val desc 时,框架内第一个值是 500,这里直接取列的值,你也可以替换为表达式。
- window_B 中当 order by val asc 时,框架内第一个值是 100。
3.7 last_value()
语法:last_value(expr) over window
last_value 返回当前框架的最后一行计算出的 expr 值,从 last_value 函数可以更好看出框架的动态变化:
select wind,val,
last_value(val*10) over w 框架内最后一个值的十倍
from wf_example
window w as (partition by wind order by val);
上面的示例中:
- Window_B 中,last_value(val*10) 返回的值每一行都不同,截止当前行的框架内最后一个值(就是当前行自己),而不是整个组的最后一个值。
3.8 nth_value()
语法:nth_value(expr, N) over window
返回框架内的第 N 行计算出的表达式 expr 值,当没有时返回 null:
select wind,val,
nth_value(val, 2) over w 框架内第二个值,
nth_value(val, 3) over w 框架内第三个值,
nth_value(val, 4) over w 框架内第四个值
from wf_example
window w as (partition by wind order by val);
上面的示例中:
- nth_value(val, 2) 返回框架内第二个值,第 1 行计算时由于框架只有一行,没有第二个值,所以返回 null。
- nth_value(val, 3) 返回框架内第三个值,注意 window_A 由于 2,3 行 val 是相等的,计算第二行时的框架会包含第三行,而 Window_B 是独立的,第二行返回 null。
- nth_value(val, 4) 返回框架内第四个值,window_A 和 Window_B 都在第四行才取到值,前三行都是 null。
3.9 lag()
语法:lag(expr [, N [, default]])
返回在当前窗口内,当前行的 “前面 N 行” 计算出的 expr 结果,如果没有满足条件的行,则返回 default,其中参数 N 和 default 可以省略,如果省略了则默认 N 为 1,default 为 null。
select wind,val,
lag(val*10) over w 当前行前面一个值,
lag(val*10,1,'不存在') over w 当前行前面一个值带默认值,
val-lag(val,2) over w 当前值与前两个值的差
from wf_example
window w as (partition by wind order by val);
上面的示例中:
- lag(val10) 返回前一行表达式 val10 计算值(N 参数省略,默认为 1,default 参数省略,默认为 nulll,第一行没有前一行,返回 null)
- lag(val10,1,'不存在') 返回前一行表达式 val10 计算值(default 为 "不存在",因此第一行返回字符串 "不存在")
- val-lag(val,2) 计算当前行与前 2 行之间的差额
3.10 lead()
语法:lead(expr [N [, default]])
lead() 和 lag() 类似,返回在当前窗口内,当前行的 “后面 N 行” 计算出的 expr 结果,如果没有满足条件的行,则返回 default,其中参数 N 和 default 可以省略,如果省略了则默认 N 为 1,default 为 null。
select wind,val,
lead(val*10) over w 当前行后面一个值的十倍,
lead(val*10,1,'不存在') over w 当前行后面一个值十倍带默认值,
val-lead(val,2) over w 当前行与后面第二个值的差
from wf_example
window w as (partition by wind order by val);
上面的示例中:
- lead(val10) 返回后一行表达式 val10 计算值(N 参数省略,默认为 1,default 参数省略,默认为 null,最后一行由于没有后一行,返回 null)
- lag(val10,1,'不存在') 返回后一行表达式 val10 计算值(default 为 "不存在",因此最后一行返回字符串 "不存在")
- val-lead(val,2) 计算当前行与后面第 2 行之间的差额
3.11 ntile()
语法:ntile(N) over window
将组内成员再次分为 N 个小组(子分组 / buckets),返回子分组的编号。
select wind,val,
ntile(2) over w 将每个组再次分为2个组,
ntile(4) over w 将每个组再次分为4个组,
ntile(100) over w 将每个组再次分为100个组
from wf_example
window w as (partition by wind order by val);
上面示例中:
- ntile(2) 将每个组再次分为 2 个组,并返回每一行所属子分组的编号
- ntile(100) 当组的数量超过行数时,每一行都是一个独立子分组。
四、窗口函数框架
框架(frame)是窗口的一个子集,它会根据当前行动态变化。在前面在 first_vlalue 和 last_value 函数中,我们提到了框架的定义,它们默认的框架是从组内第一行到当前行(也包含和当前行相等的行)。
窗口函数还支持框架子句,让你可以显式定义框架的范围,根据当前行的位置,来计算组内一小部分集合的数据。
4.1 框架的定义
框架的定义语法:frame_units frame_extent
框架的定义由 frame_units(框架单位)和 frame_extent(框架范围)两子句组成。
当定义框架时,我们首先要指定框架使用的单位(frame_units 子句),框架单位可以有 2 种选择:
rows:通过起始行和结束行来划定框架的范围,边界是明确的一行。
range:通过具有相同值的行来划定框架的范围,边界是一个范围,具有相同值的行作为一个整体看待。
定义好单位后,我们还需要定义框架范围(frame_extent 子句),也有两种定义方式:
- 只定义起始点(frame_start),终止点(frame_end)默认就是当前行。
- 通过 between frame_start and frame_end 子句,同时定义起始点(frame_start)和终止点 (frame_end)。
合法的 frame_start 和 frame_end 可以有如下 5 种选择:
- current row:当框架单位是 rows 时,即当前行。当框架单位是 range 时,包含当前行和当前行相同的行(一个范围)。
- unbound preceding:窗口内第 1 行。
- unbound following:窗口内最后 1 行。
- expr preceding:当框架单位是 rows 时, 边界时当前行的前 expr 行。当框架单位是 range 时,边界是值和 "当前行的值 - expr" 相等的行,如果当前行的值是 null,那边界就是和当前行相等的行。
- expr following:当框架单位是 rows 时, 边界时当前行的后 expr 行。当框架单位是 range 时,边界时和 "当前行的值 + expr" 相等的行,如果当前行的值是 null,那边界就是和当前行相等的行。
光看定义可能有些晦涩,我们通过几个示例来帮助理解。
4.2 框架定义示例
我们用几个示例来加深对框架定义的理解:
select wind,val,
last_value(val*10) over (partition by wind order by val rows unbounded preceding) 显式定义框架内最后一个值的十倍
from wf_example;
上面示例中:
- 框架的定义是 rows unbouned preceding。
- 框架单位是 rows(行),框架范围是 unbounded preceding(组内第 1 行)。
- 这里采用了仅定义起始点的方式,框架的终止点默认就是当前行(current row),定义等同于:rows between unbound preceding and current row
下面的示例为滚动求和,计算当前行和前一行的和:
select wind,val,
sum(val) over (partition by wind order by val rows 1 preceding) 当前行和前1行的和,
sum(val) over (partition by wind order by val rows between 1 preceding and current row) 第二种定义方式
from wf_example;
上面示例中:
- 第一个框架的定义是 rows 1 preceding,框架单位是 rows(行),
- 第一个框架范围是 1 preceding(当框架单位为 rows 时,1 preceding 代表当前行的前 1 行).
- 第一个框架采用了仅定义起始点的方式,框架的终止点默认就是当前行。
- 第二个框架采用了 between 1 preceding and current row 的方式,显式指定了框架的起始和结束范围,效果是相同的。
4.3 框架单位 rows 和 range 的区别
这里仅仅将上一个滚动求和 SQL 中的框架单位定义由 rows 改为 range,再看一下效果:
select wind,val,
sum(val) over (partition by wind order by val range 1 preceding) range单位下当前行和当前行值减1范围的和
from wf_example;
上面示例中,当框架单位变为 range 时:
- 框架定义为 range 1 preceding,等价于 range between 1 preceding and current row。
- 当框架单位为 range 时,这里的 1 preceding 不再是前 1 行的意思,而是 "当前行的值 - 1"。
- 而 range between 1 preceding and current row 代表值的范围落在区间 [当前行值 - 1,当前行值] 内所有行。
- 在 Window_A 中,第二行 val 值为 2,因此框架包含值在 [2-1, 2] 范围内的所有行,即 1,2,3 行,sum 求和结果为 5,第三行同理。
- 在 Window_A 中,第四行 val 值为 3,因此框架包含值在 [3-1, 3] 范围内的所有行,即 2,3,4,5,6 行,sum 求和结果为 13,第五、六行同理。
- 在 Window_B 中,第 2 行 val 值为 200,因此框架包含值在 [200-1, 200] 范围内的所有所有行,只有第二行,sum 求和结果就是自己,后面的行同理。
上面的 SQL 通过加入 first_value 和 last_value 函数我们可以更直观的看出框架的边界(first_value 返回框架内第 1 个值,last_value 返回框架内最后一个值):
select wind,val,
sum(val) over (partition by wind order by val range 1 preceding) range单位下当前行和当前行值减1范围的和,
first_value(val) over (partition by wind order by val range 1 preceding) first_val,
last_value(val) over (partition by wind order by val range 1 preceding) last_val
from wf_example;
- 在 Window_A 中,val 的值差距为 1,因此 range 1 preceding 可以触及前面的行。
- 在 Window_B 中,val 的值差距为 100,因此 range 1 preceding 无法触及前面的行(first_value 和 last_value 都是自己),每一行的框架都只包含当前行自己。
但如果我们把 range 1 preceding 改成 range 100 preceding,则 Window_B 中框架可以触及前面的行:
select wind,val,
sum(val) over (partition by wind order by val range 100 preceding) range单位下当前行和当前行值减1范围的和,
first_value(val) over (partition by wind order by val range 100 preceding) first_val,
last_value(val) over (partition by wind order by val range 100 preceding) last_val
from wf_example;
可以看到 Window_B 中求和列变成了当前行和前 1 行的 val 的和,同时框架的 first_val 变成了前 1 行的值(代表当前行的框架包含前 1 行)。
框架单位 rows 和 range 的区别总结就是:
- rows 是通过行来划分边界,框架边界是明确的某一行。
- range 是通过值来划定边界,框架边界是具有某个值的所有行。
4.4 缺少 order by 子句时的默认框架
当没有显式的框架定义时,某些函数会采用默认框架定义,而默认框架定义会受到是否有 order by 子句影响,因此是否有 order by 子句可能会导致某些函数的计算结果不同。
根据窗口定义是否有 order by 子句:
- 有 order by 子句时,默认的框架定义是:range between unbound preceding and current row
- 没有 order by 子句时,默认的框架定义是:range between unbound preceding and unbound following
即:当有 order by 子句时,框架是从组内第一行到当前行(注意框架单位是 range,也包含当前行相同值的行)。当没有 order by 子句时,框架就是从组内第 1 行到最后一行(组内所有行),所有的行都是相等的。
我们通过最初的 sum 函数来观察这种默认框架的区别:
select wind,val,
sum(val) over (partition by wind order by val) 带orderby子句,
sum(val) over (partition by wind) 不带orderby子句
from wf_example;
上面示例中:
- 带 order by 子句时,sum 函数求和范围是第 1 行到当前行(包含和当前行相等的行)的和,sum 的结果是递增的。
- 不带 order by 子句时,每一行 sum,求出来都是组内全部行的和,没有 order by 子句,众生平等。