1.什么是窗口函数
窗口函数可以进行排序,生成序列号等一般的聚合函数无法实现的高级操作。
窗口函数也叫OLAP(Online Analytical Processing)函数,即对数据库数据进行实时分析处理,窗口函数就是为了实现OLAP而添加的标准SQL功能
窗口函数语法:
<
窗口函数可以放以下两种函数:
1.rank , dense_rank , row_number等专用窗口函数
2.能够作为窗口函数的聚合函数(sum , avg , count , max , min等)
由于窗口函数是对where或者group by子句处理后的结果进行操作,所以窗口函数原则上只能写在select字句中
2.如何使用窗口函数
![054d98a95fca221b0d58f6c8914b1466.png](https://img-blog.csdnimg.cn/img_convert/054d98a95fca221b0d58f6c8914b1466.png)
在每个班级内按成绩排名:
select*, rank() over (partition by 班级 order by 成绩 desc) as ranking
from 班级表;
select*, rank() over (order by 成绩 desc) as ranking
from 班级表;
partition by:功能类似于group by,对表进行分组即按班级来进行分组,若省略partition by子句,则将会对所有成绩排序
order by:对分组后的结果进行排序,desc表示降序排序
结果如下
![192512dda1fe2272b9a8e7abddaa86df.png](https://img-blog.csdnimg.cn/img_convert/192512dda1fe2272b9a8e7abddaa86df.png)
![8520441b280f5fecc2802cef52928357.png](https://img-blog.csdnimg.cn/img_convert/8520441b280f5fecc2802cef52928357.png)
窗口函数group by子句分组和order by字句排序的功能,但是,使用group by分组汇总后改变了表的行数,相当于根据分组的类别将同类的行合并了。同样使用班级表为初始表,具体代码及结果如下:
select
![4d5ad691d91d9d886cef947c6a65fddd.png](https://img-blog.csdnimg.cn/img_convert/4d5ad691d91d9d886cef947c6a65fddd.png)
![ebc750c23fdeab4231a3f6a497f1c492.png](https://img-blog.csdnimg.cn/img_convert/ebc750c23fdeab4231a3f6a497f1c492.png)
总的来说,窗口函数在具有分组及排序的功能的同时还不会减少原表的行数。
3.其他专用窗口函数
窗口函数中关于排序三个窗口函数为rank , dense_rank , row_number,这三个专用窗口函数的区别如下:
select
![6e378fecaed050b82ce7a67a1544f2a9.png](https://img-blog.csdnimg.cn/img_convert/6e378fecaed050b82ce7a67a1544f2a9.png)
![db9defbb21b27304a162f27937df7581.png](https://img-blog.csdnimg.cn/img_convert/db9defbb21b27304a162f27937df7581.png)
![c7a0513d5ff1064f79cfd8531dfa7b0c.png](https://img-blog.csdnimg.cn/img_convert/c7a0513d5ff1064f79cfd8531dfa7b0c.png)
rank函数:若排名时有并列名次的行,会占用下一名次的位置,即如结果1所示,会有3个第五名但第五名之后就是第八名了。
dense_rank函数:若有并列名次的行,不占用下一名次的位置,即如结果2所示,即在3个第五名之后是第六名。
row_number函数:不考虑并列名次的情况,排名会对三个相同的成绩根据其他列的情况重新排名。
最后,在上述这三个专用窗口函数中,函数后面的括号不需要任何参数,保持()空着就可以了。
面试经典topN问题:
![3a6b568096bed441b49c4e9d55a8f76e.png](https://img-blog.csdnimg.cn/img_convert/3a6b568096bed441b49c4e9d55a8f76e.png)
按课程号分组取成绩最大值所在行的数据
select
![60322d03398b5a6d6a53dfab18bd359e.png](https://img-blog.csdnimg.cn/img_convert/60322d03398b5a6d6a53dfab18bd359e.png)
使用关联子查询可以达到目的,注意,子查询中必须添加where a.课程号 = b.课程号 的条件,否则,子查询返回多行的数据,会报错。程序无法运行。
-- 错误示范
select*
from score
where 成绩 = (select max(成绩) from score group by 课程号);
![16097452aed348307da148de3b1990d6.png](https://img-blog.csdnimg.cn/img_convert/16097452aed348307da148de3b1990d6.png)
按课程号分组取成绩最小值所在行的数据
select*
from score as a
where 成绩 = (select min(成绩) from score as b where a.课程号 = b.课程号 group by 课程号);
![5d890653eb0765fcdd738636ea082c9d.png](https://img-blog.csdnimg.cn/img_convert/5d890653eb0765fcdd738636ea082c9d.png)
同样适用关联子查询可以达到目的。
查询每名学生成绩前两名的纪录
方法一:先使用group by 将课程分组,之后再通过order by对成绩排序,并用limit限制输出的行数,将每个学生最高的两门课的成绩取出来
(select*
from score
where 学号 = '0001'
order by 成绩 desc
limit 2)
union
(select*
from score
where 学号 = '0002'
order by 成绩 desc
limit 2)
union
(select*
from score
where 学号 = '0003'
order by 成绩 desc
limit 2);
![529f9aec25a175781b9692f76f6609f7.png](https://img-blog.csdnimg.cn/img_convert/529f9aec25a175781b9692f76f6609f7.png)
方法二:使用窗口函数
select*,row_number() over (partition by 学号 order by 成绩 desc) as ranking
from score;
![eb1d9c780e48167acddd964d64d4f706.png](https://img-blog.csdnimg.cn/img_convert/eb1d9c780e48167acddd964d64d4f706.png)
为得到每个学生成绩最高的两门课的成绩,需要加ranking<=2的条件
select *,row_number() over (partition by 学号 order by 成绩 desc) as ranking
from score
where ranking <=2;
![1f6f984840d8e15eb87dcb2bb7354d6d.png](https://img-blog.csdnimg.cn/img_convert/1f6f984840d8e15eb87dcb2bb7354d6d.png)
直接在where之后加ranking<=2的条件是错误的,报错信息如上。这是因为select子句的运行顺序是在where子句之后的,在where子句运行时ranking这一列还没被定义。
正确代码如下:
select *
from (select*,row_number() over (partition by 学号 order by 成绩 desc) as ranking
from score) as a
where ranking <=2;
这里在from子句里使用了子查询,相当于通过窗口函数建立了一个新表
![eb1d9c780e48167acddd964d64d4f706.png](https://img-blog.csdnimg.cn/img_convert/eb1d9c780e48167acddd964d64d4f706.png)
然后在新表中查找ranking<=2的行,得到的结果如下:
![e8b526d0c0d7785ae98213422bc283ed.png](https://img-blog.csdnimg.cn/img_convert/e8b526d0c0d7785ae98213422bc283ed.png)
将此题的代码改一下,可以作为此类问题的常用解法,格式如下:
select*
from (select*,row_number() over
(partition by <要分组的列名> order by <要排序的列名> desc)
from <表名>) as a
where ranking [<=N];
代码块中[]内的部分可以根据问题修改,查询每组最大/最小的N条记录
关于MySQL 8.0版本的其他窗口函数,具体介绍见:
12.21.1 Window Function Descriptionsdev.mysql.com4.聚合函数作为窗口函数
聚合窗口函数和上面提到的专用窗口函数用法完全相同,只需把聚合函数写在窗口函数的位置即可,但是函数后面括号不能为空,需要指定聚合的列名
以班级表为例:
![860c05d294949a8d72ba236a40f06c11.png](https://img-blog.csdnimg.cn/img_convert/860c05d294949a8d72ba236a40f06c11.png)
-- 聚合函数的窗口函数
![7e5a3bf0b644129461068a291452bbb7.png](https://img-blog.csdnimg.cn/img_convert/7e5a3bf0b644129461068a291452bbb7.png)
以sum函数举例,sum的窗口函数相当于是在对自身这一行的记录及位于自己记录之上的记录进行求和,比如:对于学号0005的这一行来说,current_sum就是对0001,0002,0003,0004,0005这五个学生的成绩求和,可得这五名学生的总成绩。
avg,max,min,count这些聚合函数的窗口函数都是同样的原理,avg(成绩)就是查询包括自身这一行在内以上所有行的平均值,max(成绩)就是查询包括自身一行在内以上所有行的最大值,min(成绩)就是查询包括自身一行在内以上所有行的最小值,count(成绩)就是查询包括自身一行在内以上所有行的行数
当我在窗口函数中加入基于班级的分组时,可得在对班级分组后在各班级内分别求和,求平均,查询最大,最小值以及计数。,结果如下:
select*,
sum(成绩) over (partition by 班级 order by 学号) as current_sum,
avg(成绩) over (partition by 班级 order by 学号) as current_avg,
max(成绩) over (partition by 班级 order by 学号) as current_max,
min(成绩) over (partition by 班级 order by 学号) as current_min,
count(成绩) over (partition by 班级 order by 学号) as current_count
from 班级表;
![50ba4c483a75fa13dcb7f905d9a58550.png](https://img-blog.csdnimg.cn/img_convert/50ba4c483a75fa13dcb7f905d9a58550.png)
当没有order by时,代码及结果如下:
select*,
sum(成绩) over (partition by 班级) as current_sum,
avg(成绩) over (partition by 班级) as current_avg,
max(成绩) over (partition by 班级) as current_max,
min(成绩) over (partition by 班级) as current_min,
count(成绩) over (partition by 班级) as current_count
from 班级表;
![86a8687c9ec98f18a3459ca13d049780.png](https://img-blog.csdnimg.cn/img_convert/86a8687c9ec98f18a3459ca13d049780.png)
当无order by之后,窗口函数会在分组之后直接对每组的所有数据计算,又由于窗口函数并不会缩减行数,故每行的窗口函数结果相同。
聚合函数作为窗口函数,可以在每一行的数据里直观的看到,截止到本行数据,统计数据是多少(最大值,最小值等)。同时可以看到每一行数据,对整体统计数据的影响。
练习:
![a198a0fd1e48f0469a3e44976dc2257f.png](https://img-blog.csdnimg.cn/img_convert/a198a0fd1e48f0469a3e44976dc2257f.png)
问题:查找单科成绩高于该科平均成绩的学生名单
此题的解题方法与求topN的问题的解法类似,都需要先重新查询建立一个新表,然后在新表中查询。具体代码,及结果如下:
-- 查询各科成绩高于该科目平均成绩的学生名单
![d2dc54e82529e9fbc6bf7cfe674d2993.png](https://img-blog.csdnimg.cn/img_convert/d2dc54e82529e9fbc6bf7cfe674d2993.png)
![c577236341348d3bece4db188c8b270a.png](https://img-blog.csdnimg.cn/img_convert/c577236341348d3bece4db188c8b270a.png)
还有一种方法就是使用关联子查询,同样可以查询各科高于平均成绩的学生,代码及结果如下:
select*
from 各科成绩表 as a
where 成绩 >(select avg(成绩)
from 各科成绩表 as b
where a.科目 = b.科目
group by 科目);
![c3be7e6854e2afa6c8d00a73fca771b0.png](https://img-blog.csdnimg.cn/img_convert/c3be7e6854e2afa6c8d00a73fca771b0.png)
5.窗口函数的移动平均
同样以班级表举例:
![b32ab06ff2fd71c15bb579c963a082b0.png](https://img-blog.csdnimg.cn/img_convert/b32ab06ff2fd71c15bb579c963a082b0.png)
select*,avg(成绩) over (order by 学号) as current_avg
from 班级表;
![a0e77072a97bf4051ab7251367d79591.png](https://img-blog.csdnimg.cn/img_convert/a0e77072a97bf4051ab7251367d79591.png)
select*,avg(成绩) over (order by 学号 rows 2 preceding) as current_avg
from 班级表;
![8ff538582b01d5a918445556bd66ad26.png](https://img-blog.csdnimg.cn/img_convert/8ff538582b01d5a918445556bd66ad26.png)
经过对比可以发现,两次计算的平均值不一样,这是因为在第二个结果的代码里加入计算条件rows...preceding...,每一行得到的结果是自身这一行的记录及前两行的平均,如:学号0006这一行的current_avg表示的是0004,0005,0006这三行的成绩的平均值,即83,86,92的平均值。
每一行的得到的结果,都是当前行和前面两行的平均,想要计算当前行与前n行(共n+1行)的平均时,只需调整rows...preceding中间的数字即可。
不仅如此,还可以计算当前数据与前n行,后n行数据的移动平均,比如,我想要计算每行数据与上一行,下一行数据的平均,只要将preceding关键词和following关键词结合就可以了,举例如下:
select*,
avg(成绩) over (order by 学号 rows between 1 preceding and 1 following)
as current_avg
from 班级表;
![5041ecb59384c9562af4276892b596a0.png](https://img-blog.csdnimg.cn/img_convert/5041ecb59384c9562af4276892b596a0.png)
6.总结
窗口函数可以解决很多普通group by&order by子句难以解决的业务问题。
- 可以在不改变行数的情况进行排序,分组,求和,求平均等操作;
- 可以对特定的某几行求和,求平均等等;
- 窗口函数原则上只能写在select子句中。