什么是MySQL窗口函数?
MySQL窗口函数,它跟函数没有一点关系,就是指在单个或多个指定窗口范围内执行函数 ^1^,而窗口呢,可以理解为一个个的组。
MySQL窗口函数的写法
MySQL的窗口函数是这样写的:^1^
sql
<函数> over ([PARTITION BY / GROUP BY <字段>] [ORDER BY <字段> [<排序方式>]] [ROWS BETWEEN / RANGE BETWEEN <起始行> AND <终止行>])
从中,我们可以看出,MySQL的窗口函数有三条子句,分别是:PARTITION BY / GROUP BY
分组子句 ,ORDER BY
排序子句 ,和**ROWS BETWEEN / RANGE BETWEEN
限行子句**。
其中,<函数>
<字段>
<排序方式>
这些见名知意。我这里就讲极其重要的三点,先讲第一点,over
关键字后面的**PARTITION BY
或GROUP BY
都用于分组** ,但这两者不同的是,PARTITION BY
在分组的时候不去重,GROUP BY
在分组的时候去重。 ^1^
然后第二点,ROWS BETWEEN
和RANGE BETWEEN
关键字都是来限定函数的行的,就跟萝卜白菜,各有所爱 一样,它们也有各自的关注点。ROWS BETWEEN
只关注于所给表格数据的行数,RANGE BETWEEN
只关注于具体日期范围的连续性。^[1](#ROWS BETWEEN只关注于所给表格数据的行数,RANGE BETWEEN只关注于具体日期范围的连续性。1 "#user-content-fn-1")^ 这样的关注点在BETWEEN
后面的<起始行>
和<终止行>
那也能体现到。
最后第三点,就是在限行子句ROWS BETWEEN / RANGE BETWEEN
后面的<起始行>
和<终止行>
那,这要怎么写呢?只需要看下面就行。
- 如果你在窗口函数的限行子窗口的前面用了
ROWS BETWEEN
:^1^- 起始行:
UNBOUNDED PRECEDING
(窗口的起始行)N PRECEDING
(当前行的前面N行)
- 当前行:
CURRENT ROW
(当前行)
- 终止行:
UNBOUNDED FOLLOWING
(窗口的终止行)N FOLLOWING
(当前行的后面N行)
- 起始行:
- 如果你在窗口函数的限行子窗口的前面用了
RANGE BETWEEN
:^1^- 起始行:
UNBOUNDED PRECEDING
(窗口的起始行)INTERVAL N <时间单位> PRECEDING
(当前行的前面一个相差某个时间的行)
- 当前行:
CURRENT ROW
(当前行)
- 终止行:
UNBOUNDED FOLLOWING
(窗口的终止行)INTERVAL N <时间单位> FOLLOWING
(当前行的后面一个相差某个时间的行)
- 起始行:
需要注意的是,如果窗口函数里边写了ORDER BY
排序子句,但是没有写ROWS BETWEEN
那样的限行子句,那么,在限行子句里边的BETWEEN
后面,就默认是UNBOUNDED PRECEDING AND CURRENT ROW
了;^1^
而如果,窗口函数里边既没写排序子句,又没写限行子句,那么,在限行子句里边的BETWEEN
后面,就默认是UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
了。^1^就以这样的表来举例:
salary_date | salary |
---|---|
2020-01-20 | 5000 |
2020-02-20 | 5000 |
2020-03-20 | 5000 |
2020-04-20 | 5000 |
2020-05-20 | 5000 |
2020-06-20 | 5000 |
2020-07-20 | 5000 |
2020-08-20 | 5000 |
2020-09-20 | 5000 |
2020-10-20 | 5000 |
2020-11-20 | 5000 |
2020-12-20 | 5000 |
2021-01-20 | 10000 |
2021-02-20 | 5000 |
2021-03-20 | 5000 |
2021-04-20 | 5000 |
2021-05-20 | 5000 |
2021-07-20 | 5000 |
2021-08-20 | 5000 |
2021-09-20 | 5000 |
2021-10-20 | 5000 |
2021-11-20 | 5000 |
2021-12-20 | 5000 |
这张表,是小明两年的工资表。现在,如果你想求出小明在这两年中累计发到了多少的钱,就需要用到窗口函数中的限行子句ROWS BETWEEN
和SUM
聚合函数来求。
那么,为什么这能用限行子句ROWS BETWEEN
和SUM
聚合函数来求呢?因为,ROWS BETWEEN
能够限制窗口的行 ,通过UNBOUNDED PRECEDING
起始行和CURRENT ROW
当前行,使SUM
聚合函数能够求出当前行及前面行中的salary
中的数据总和。就像这样。
因此,这就是这个查询操作的sql。
sql
SELECT
salary_date, salary,
SUM(salary) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) money
FROM
xiaomings_salary_tb;
执行之后,也的确如此。
现在,你仅仅是解决了第一个问题,还有第二个问题在等着你解答。现在,你觉得光求出了小明累计发到的工资还不够,想要再求一下小明连续发到工资的天数。这要怎么求呢?
首先,如果你没有仔细看的话,就会在不经意间犯错,因为在上表中的2021-05-20
这个数据的后面的数据中 ,不是2021-06-20
,而直接跳到了2021-07-20
,不连续了。
因此,我们就可以借助COUNT
聚合函数来为RANGE BETWEEN
限行子句中的INTERVAL 1 MONTH PRECEDING
和CURRENT ROW
求出每一个日期连续的组的头数据,因为如果这里的INTERVAL 1 MONTH PRECEDING
跟当前行的上一行数据不符合要求,那么COUNT
聚合函数就不会计算。
然后,在用了COUNT
聚合函数之后,先用ORDER BY
对salary_date
进行降序排序,排序好后,就作为子查询来继续查询,之后用WHERE
判断出每一个组的头数据,就在COUNT
函数返回1的行那。最后LIMIT
一下,让查询出的数据只显示前面1行,并让这行只查询salary_date
,以此来将@sec_grp_mark
设为刚查查询出来的数据,用于在后面分组。
接着,把这个分组的数据用一个变量@sec_grp_mark
存起来之后,另写一条sql语句。我们要想求小明连续发到工资的天数,就得要用到ROW_NUMBER
这样一个求出表的序列的函数来求出小明连续发到工资的天数,因为如果日期是连续的话,求出的数据也是连续的。
最后,在窗口函数里面,按IF
函数来分组,以此来用@sec_grp_mark
标记来获取每一个组,做好这一些后,直接执行即可。
sql
SELECT
salary_date
INTO
@sec_grp_mark
FROM
(
SELECT
(COUNT(*) OVER (ORDER BY salary_date RANGE BETWEEN INTERVAL 1 MONTH PRECEDING AND CURRENT ROW)) groupId, salary_date, salary
FROM
xiaomings_salary_tb
ORDER BY
salary_date DESC
) tb
WHERE
groupId = 1
LIMIT
1;
SELECT
ROW_NUMBER() OVER (PARTITION BY IF(salary_date < @sec_grp_mark, 1, 2)) comboDays,
salary_date, salary
FROM
xiaomings_salary_tb;
执行之后,如果查询出来的是这样的话,那对了。
好,那么所有关于MySQL窗口函数的写法的重点就都讲完了,接下来就去了解一下关于窗口函数的其它知识吧。
MySQL窗口函数的其它知识点
关于MySQL窗口函数的其他知识点。我这里给他列举了三类,分别是bug类,函数类和执行类。
bug类
bug类呢,别的不提,主要的就有这么一个。在MySQL8.0版本中,如果你用了RANGE BETWEEN
和INTERVAL N <时间单位> PRECEDING
或INTERVAL N <时间单位> FOLLOWING
的话,就会有这样的一个错误,像这样:
大致的意思呢,就是你在窗口函数的限行子句中使用RANGE BETWEEN
的时候,没有用ORDER BY
排序子句给日期类型的字段进行排序,会报错。因此,如果你在窗口函数的限行子句中使用RANGE BETWEEN
,就需要一个时间类型的字段让窗口函数中的ORDER BY
排序子句来进行排序。
函数类
在MySQL窗口函数中的<函数>
中,可以填入的函数分别有聚合函数,排序类函数和跨行类函数 ^1^,聚合函数不用多说,大家在MySQL中也都遇到过,没遇到过的可以不算,这里面讲的主要就是排序类函数和跨行类函数。
排序类函数
排序类函数,有ROW_NUMBER
函数,有RANK
函数,还有DENSE_RANK
函数。^1^
首先介绍ROW_NUMBER
函数,ROW_NUMBER
函数,无参,就是用于给一个区间中的每一行从上到下的获取序号 ,这个序号就像这样子:^1^
那么,ROW_NUMBER
函数中所说的区间又是什么呢?区间,默认是整张表,被用上窗口函数之后,就是窗口函数分组之后的一个个窗口。 再那么,ROW_NUMBER
函数会有什么样的实际用途呢?ROW_NUMBER
函数呢,主要的用途就是排名,会用于各种排行榜之中,这里就以这张表为例。
id | name | device_id | gender | age | university | gpa |
---|---|---|---|---|---|---|
1 | 小明 | 2138 | male | 21 | 北京大学 | 3.4 |
2 | 小帅 | 3214 | male | NULL | 复旦大学 | 4.0 |
3 | 小美 | 6543 | female | 20 | 北京大学 | 3.2 |
4 | 大美 | 2315 | female | 23 | 浙江大学 | 3.6 |
5 | 卞精 | 5432 | male | 25 | 山东大学 | 3.8 |
6 | 薛西 | 2131 | male | 28 | 山东大学 | 3.3 |
7 | 项素典 | 4321 | male | 28 | 复旦大学 | 3.6 |
假如,这是一张大学生表,如果你要对该表中的学生进行排名的话,就需要ROW_NUMBER
函数,为什么呢?因为这个函数会有排行榜一般的效果,不信就看。
在使用ROW_NUMBER
函数来对该表中的学生进行排名时,先降序排序gpa
字段,因为gpa
是平均学分绩点,gpa
的值越大,学生就越厉害,然后,代码就出来了。
sql
SELECT
id, name, device_id, gender, age, university, gpa,
ROW_NUMBER() OVER (ORDER BY gpa DESC) 排行
FROM
user_profile;
最后执行一下,就有这样结果。
不得不说,看似简单又看似与排名毫不相关的ROW_NUMBER
函数,实际上竟会有这般强大的排名功能,真是把他看扁了啊!
然后介绍RANK
函数,RANK
函数,无参,也会用于各种排行榜,具体的功能跟ROW_NUMBER
函数类似。其中,RANK
函数还有一个重要的点,那就是,窗口函数中没有ORDER BY
排序子句就没用 ,因此,在使用RANK
函数的时候,如果你要用到窗口函数,那窗口函数里边就一定要有ORDER BY
排序子句。也以这张表为例:
id | name | device_id | gender | age | university | gpa |
---|---|---|---|---|---|---|
1 | 小明 | 2138 | male | 21 | 北京大学 | 3.4 |
2 | 小帅 | 3214 | male | NULL | 复旦大学 | 4.0 |
3 | 小美 | 6543 | female | 20 | 北京大学 | 3.2 |
4 | 大美 | 2315 | female | 23 | 浙江大学 | 3.6 |
5 | 卞精 | 5432 | male | 25 | 山东大学 | 3.8 |
6 | 薛西 | 2131 | male | 28 | 山东大学 | 3.3 |
7 | 项素典 | 4321 | male | 28 | 复旦大学 | 3.6 |
再假如,这是刚才提到的那张大学生表,如果你要对该表中的数据进行排名的话,也可以用RANK
函数,而如果,你之后直接RANK
函数来搭配上无子句的窗口函数,就像这样的sql一样。
sql
SELECT
id, name, device_id, gender, age, university, gpa,
RANK() OVER () 排行
FROM
user_profile;
那在你执行它之后,就会发现排行那一字段的数据全部都是1,根本没有排行榜的效果,也就照应了前面说的话:窗口函数中没有ORDER BY
排序子句就没用。
因此,我们就要在窗口子句里面添加上ORDER BY
排序子句,用于给gpa
字段排序,所以就在窗口函数里面,给gpa
字段添加了ORDER BY
排序子句进行降序排序,之后这是改进的sql。
sql
SELECT
id, name, device_id, gender, age, university, gpa,
RANK() OVER (ORDER BY gpa DESC) 排行
FROM
user_profile;
这时你再执行,就也有排行榜那样的效果了。
最后,就要介绍DENSE_RANK
函数了。DENSE_RANK
函数,跟RANK
函数没有多大的差别,只不过,DENSE_RANK
函数可以排出更合理的名次 ,不像RANK
函数一样会跨N个名次。以这个查询的sql举例。
sql
SELECT
id, name, device_id, gender, age, university, gpa,
RANK() OVER (ORDER BY gpa DESC) 排行
FROM
user_profile;
这是刚才用于在刚才的大学生表中排名的sql查询语句,现在,如果把这里面的RANK
换成DENSE_RANK
的话,执行之后,就这样查询好了。
可以看到,刚才查询出来的结果比上次那个查询的结果的名次更合理了。因此,DENSE_RANK
函数能够让排出来的名次变得更合理。
跨行类函数
跨行类函数,有LEAD
函数和LAG
函数。其中,LEAD
函数和LAG
函数用途都是关于跨行的,且参数都是一样的,两个默认参数和一个普通参数:要比较的字段
,N
和默认值
。其中,N
后面的参数的都是默认参数,而N
的默认参数是1,默认值
的默认参数是NULL
。
但是,不同的是,LEAD
函数和LAG
函数,它们的返回值却不同。LEAD
函数,返回当前行的前面第N
行的要比较的字段的数据,如果该行不存在,或者该行超出窗口的范围,就返回默认值;
而LAG
函数,则返回当前行的后面第N
行的要比较的字段的数据,如果该行不存在,或者该行超出窗口的范围,就也返回默认值。现在以别的表进行举例。
name | point | likes |
---|---|---|
压力哥 | 2195 | 6 |
年级都是一 | 1340 | 0 |
博一也是一年级 | 1070 | 0 |
三秒干碎口算梦 | 2355 | 2 |
研一也是一年级 | 965 | 0 |
... | ... | ... |
假设这是小猿口算APP中的数据表,为了让大家不会看得倒头就睡,这里只显示五行数据,省略的内容先不用管。如果你现在不仅要给该表中的数据按倒序查询出来,并且还查询要出每个用户的下一名的名字及积分,那么就可以用当前学到的换行函数来查询出。
其中,查询表中的数据的事先不用管,主要就是如何用换行函数来查询出每个用户的下一名的名字及积分。很简单,方法如下。
首先,用LAG
函数查询出每一行的上一行的name
字段的数据;然后,再用LAG
函数查询出每一行的上一行的point
字段的数据;最后,在每个窗口函数里添上给point
字段进行降序排序的ORDER BY
排序子句,就行了。
sql
SELECT
name, point, likes,
LAG(name) OVER (ORDER BY point DESC) next_name,
LAG(point) OVER (ORDER BY point DESC) next_point
FROM
xiaoyuan_calculation_tb;
在你执行之后,这下面便是查询之后的结果。
执行类
在最后,关于窗口函数是如何执行的问题呢,这里我给大家讲一下。
首先,窗口函数会去执行PARTITION BY / GROUP BY
分组子句,用于初步给窗口进行切分。如果窗口函数里边只有分组子句,那么就会自动添上限行子句:ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
;
然后,窗口函数会去执行ORDER BY
排序子句,用于给每一个切分出来的窗口里面的数据进行排序,如果后面没有限行子句,那么在排序子句执行完之后,就会自动加上限行子句:ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
;
**最后,窗口函数会去执行ROWS BETWEEN / RANGE BETWEEN
限行子句,用于指定OVER
关键字前面的函数读取到的数据。之后,窗口函数就会调用OVER
关键字前面的函数,让这个函数在一个个窗口函数限定的窗口里面进行调用。**等函数执行好后,窗口函数也就执行完了。
至此,关于窗口函数的全部内容,也就在此全部讲完了。
下篇预告
如何在一个数列里边用新手都会玩得666的交换操作生成全部不唯一的序列?(黑马式讲解)