什么是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的交换操作生成全部不唯一的序列?(黑马式讲解)