学习数据库的过程中,练习题目的时候我们经常会遇到如下等问题:
查询连续三个月都投稿的作者姓名?——期刊表练习题
查询同年连续三个月都有违法记录的当事人信息?——交通执法练习题
用户登录表中连续登录了三次及以上的用户?
查询产品号存在断号的情况、及断开的号码?
可以看到,以上这些问题似乎都与“连续”相关,又各有不同,例如,第一问仅求解连续三个月有投稿记录,那么如下表格中,当我们把日期升序排序后,第1到3行是满足的,4到5行不满足:
时间 | 序号 |
---|---|
2023-01-03 | 1 |
2023-02-11 | 2 |
2023-03-22 | 3 |
2023-05-24 | 1 |
2023-06-10 | 2 |
第二问在此基础上多加了一个“同年”,那么如下表格中只有第3行到5行是满足要求的,类似地,题目还可以换为“同一天连续n个小时”、“同一月连续n天”,甚至两个、三个条件联立,如“同一年同一月同一小时连续n秒”,这里仅以同年连续3个月举例,观察下表的序号与第一个表格有什么区别:
时间 | 序号 |
---|---|
2022-01-03 | 1 |
2022-02-04 | 2 |
2023-03-08 | 1 |
2023-04-02 | 2 |
2023-05-01 | 3 |
2023-07-09 | 1 |
这就对我们产生了启发,我如果能使用教材上简单介绍的row_number
(或row_num
),对时间序列进行排序、并新加一列表示排序的话,似乎我们把这个加了排序的新表用一个where条件进行筛选后(比方说,where 序号为1,2,3?)我们就能直观地得到我们想要的结果。那这个思路是否可行呢?
开窗函数
关于开窗函数,百度或者谷歌查询后有很多介绍,在这里限于篇幅不作详细介绍,我以自己的理解作直白的解释:
函数形式
首先,开窗函数用在sql语句的select 与 from 之间,在我们应试时,能用到的就是:排序+聚合类函数(sum、max、min、avg、count)的升级版+偏移,用于查询出一列,且是新的一列(原表没有),其基本的语法如下(以row_number()为例):
select
row_number () over (partition by xx order by xx)
from table(表名)
我以表格的形式进行说明:
关键字 | 作用 | 是否可省略 |
---|---|---|
over | 告诉数据库,要开窗查询 | × |
partition by | 按...分组 | √ |
order by | 按...排序 | 聚合类√,排序类×,偏移类× |
这个over
就是所谓的“开窗函数”的关键字,要开窗必须跟上这个关键字,partition by
为“按...分组”的意思,可省略,具体看你的业务逻辑,因为不分组一样能求和、排序,order by
为“按...排序”的意思。只需要记住这有哪些开窗函数(下面会作介绍),及这三个关键字我们就能使用开窗了。
1.在原表的基础上新开一列
2.新列中,填充的值情况如下:
(1)序号,标明这一行某个数据的排序情况。
(2)求和、最大、最小、平均值、计数,这一行中,某个值隶属于的“组”中,它们这一组的求和、最大、最小、平均值、计数(结合partition by),其中,sum求和能与order by连用,进行累加
(3)偏移,新开的一列为原来某一列数据向前偏移一行或多行,或者向后偏移一行或多行
3.新开的列,全部是“伪列”,需要再套一层父查询,把查询结果“固定”下来。
以上这些说明,看起来会比较抽象,下面举例的时候可以回来这里对照查看和理解
开窗函数种类
Sql server或oracle中开窗函数种类很多,在此仅介绍常用的几个:
以下5个,最常与partition by 连用:
sum() over()——开窗求和
max() over()——开窗求最大
min() over()——开窗求最小
avg() over()——开窗求平均
count() over()——开窗计数(注:不能使用distinct)
以下3个,为排序函数,最常与partition by + order by 连用:
row_number() over()——开窗排序,值相同时序号不等
rank() over()——开窗排序,值相同时序号相等,且下一个值序号跳开
dense_rank() over()——开窗排序,值相同时序号相等,且下一个值序号连续
以下2个,为偏移函数,常与order by 连用,加上partition by可实现组内偏移:
lag() over()——开窗向后偏移,默认值为lag(某一列,1),即步长为1,向后偏移一行,即本行显示某一列的上一行值
lead() over()——开窗向前偏移,默认值为lead(某一列,1),即步长为1,向前偏移一行,即本行显示某一列的下一行值
例:自行创建表和插入以下数据,进行观察,demo表中第一列为id,第二列为数字num:
CREATE TABLE demo(
id INT4,
num INT4
);
INSERT INTO demo VALUES
(1,3),(2,8),(2,3),(1,4),(2,5),(2,7),(2,2),(1,3),
(1,5),(1,5),(2,20),(1,7),(3,7),(2,1),(3,7),(3,7),(3,9);
以sum() over()为例
聚合类,求和,partition by以及order by都可以跟,只跟partition by时居多,表示分组求和的意思。
以下语句、及结果:
可以完全省略partition by和order by,此时“降维”成普通sum函数,区别仅在于普通聚合函数最后只显示一行求和值。而开窗在每一行后面都添加一列,该列所有值都一样,都是对某列进行求和
SELECT *,
SUM(num) OVER() sum_num
FROM DEMO;
以下语句、及结果:
加了partition by以后,可以看到,结果发生变化,按组求和,并把同一组放在一起给你显示,可以看到,每一行对应的这个开窗值,都是与该组的“属性”相关的,这个属性指的就是:它们之所以都一样,是因为它们同属于一个id,即同一个partition by,因此当使用下面第二个代码块的语句(使用distinct,使用内嵌视图)就会使结果和普通聚合一模一样。
SELECT *,
SUM(num) OVER(PARTITION BY ID) sum_num
FROM DEMO;
SELECT DISTINCT ID,
sum_num
FROM (
SELECT *,
SUM(num) OVER(PARTITION BY ID) sum_num
FROM DEMO
);
请自行观察下列语句的执行结果(acc_num取名 accumulate的意思):
SELECT *,
SUM(num) OVER(ORDER BY NUM) acc_num
FROM DEMO;
剩余几个开窗函数(聚合+排序)
从该max开始,下面几个聚合类+排序我这里统一贴上一个语句,你可以单拿出一个,去试试看同时加分组排序,只要分组、排序中的一个、什么都不要,观察语句结果:
SELECT *,
MAX(num) OVER(PARTITION BY ID) max_num,
MIN(num) OVER(PARTITION BY ID) min_num,
AVG(NUM) OVER(PARTITION BY ID) avg_num,
COUNT(*) OVER(PARTITION BY ID) cnt_num,
ROW_NUMBER() OVER(PARTITION BY ID ORDER BY NUM) rn_num,
DENSE_RANK() OVER(PARTITION BY ID ORDER BY NUM) drk_num,
RANK() OVER(PARTITION BY ID ORDER BY NUM) rk_num
FROM DEMO;
偏移
lag() over()及lead() over()单独做一个,因为刚才的语句分了组,虽然偏移也支持组内偏移,但可能看起来会不太清晰,观察以下语句的结果:
SELECT *,
lag(num) OVER(ORDER BY NUM) lag_num,
lag(num,2) OVER(ORDER BY NUM) lag2_num,
lead(num) OVER(ORDER BY NUM) lead_num,
lead(num,2) OVER(ORDER BY NUM) lead2_num
FROM DEMO;
SELECT *,
lag(num) OVER(PARTITION BY ID ORDER BY NUM) ctlag_num,
lag(num,2) OVER(PARTITION BY ID ORDER BY NUM) ctlag2_num,
lead(num) OVER(PARTITION BY ID ORDER BY NUM) ctlead_num,
lead(num,2) OVER(PARTITION BY ID ORDER BY NUM) ctlead2_num
FROM DEMO;
可以看到,lag(num)或lead(num)只跟order by的时候,新开的这一列是把Num列往后或往前偏移了一行,因此上面第一个语句查询结果中第3列(lag)第1行、第5列(lead)最后一行出现了null值。lag(num,2)带第二个参数的时候,表示步长,就让num列往后偏移了2行,因此第4列前2行都是null值。
第二个语句中,lag(num)或lead(num)与partition by 和 order by 同时连用时,我们发现组内也可以进行偏移,于是出现了上图中按ID进行分组后的偏移
更大胆一点,我们可以让lag(num,x)中的步长x嵌套一个子查询:
SELECT *,
lead(num,(SELECT MAX(ID) FROM DEMO)) OVER(ORDER BY NUM) lead_numxx
FROM DEMO;
以题目学
ok了解了开窗的基本概念之后,用题目来看下实际运用:
1.投稿表题目
建TGB表语句:(如有请忽略)
CREATE TABLE TGB
(
ZZH character(4) NOT NULL,
QKH character(4) NOT NULL,
TGRQ timestamp NOT NULL,
WZMC varchar(50) ,
SGJG varchar(6) ,
CONSTRAINT PK_TGB PRIMARY KEY (ZZH, QKH, TGRQ)
) ;
插入数据:
INSERT INTO TGB ( ZZH,QKH,TGRQ,WZMC,SGJG) VALUES
('zz01','qk02','2007-06-03 00:00:00','信息安全的第二道防线——入侵检测系统','通过' ),
('zz01','qk02','2018-11-02 11:24:00','地铁界限系统的研究','未通过' ),
('zz01','qk03','2007-07-08 00:00:00','浅论教育和谐下的师德建设','未通过' ),
('zz01','qk04','2006-03-04 00:00:00','对全球化的一种全体化思考','通过' ),
('zz02','qk01','2006-03-01 00:00:00','基于VC与Ansys的参数化有限元分析','未通过' ),
('zz02','qk02','2008-06-09 00:00:00','一种基于移动agent技术的分布式数据检索系统的实现','通过' ),
('zz03','qk02','2007-08-29 00:00:00','无线传感器网络的研究','通过' ),
('zz03','qk02','2008-02-01 00:00:00','基于Web的考试系统新方案','通过' ),
('zz03','qk03','2007-06-05 00:00:00','论高效青年教师师德建设','通过' ),
('zz04','qk02','2008-03-09 00:00:00','基于J2EE的分布式事务研究','通过' ),
('zz04','qk05','2008-02-05 00:00:00','刍议医话','未通过' ),
('zz06','qk01','2007-07-08 00:00:00','CORBA和Java的结合使用','未通过' ),
('zz06','qk01','2008-01-03 00:00:00','高速公路通信系统的整合利用初探','未通过' ),
('zz06','qk02','2006-01-26 00:00:00','个性化协同学习环境本体的研究','通过' );
问:找出投稿表中同一年连续两个月都有投稿记录的作者信息?
最终要的是“作者信息”,因为原题考了连接查询,即还存在另一个ZZB(作者表),这里就不给了,因为本表中已有了zzh(作者号)的信息,我们能在本表里筛选出来满足条件的结果,那么用一个join就能把另一个表的信息连进来,因此连接查询不是关键,而在于解决连续的问题。
那我们不管三七二十一,先把窗开一下看看,partition by
采用zzh、和year(tgrq),即要根据同一作者、同一年份去分组,order by采用tgrq升序排列:
Step 1——
SELECT *,
ROW_NUMBER() OVER(PARTITION BY zzh,YEAR(TGRQ) ORDER BY TGRQ) rn
FROM TGB
可以看到,rn一列,已经按我们指定的同一人、同一年、日期升序进行排列,观察一下,可看到zz01在2007年在6月,7月是投过稿的,因此序号标出了1和2、zz03在2007年6月和8月投过稿,也标出了1和2、zz04在2008年2月和3月投过稿,标出了1和2。其他地方如果年份上出现了断开,因此不会出现2。
现在似乎我们已经知道结果了,是zz01、zz03、zz04么?
zz03不是,他的6月和8月没连上,但rn依然给到了他1和2。为什么?因为rn只根据日期升序进行了排序,其他全为1的那些“行”,是因为他们根本没有“同年”多次投稿的记录,所以rn只给到了1
Step 2——
核心思想的一步,把投稿日期(TGRQ)在月份上减去rn,这一点是运用开窗函数解决连续的核心思想,关键所在
这里代码块我做了缩进,以美观些,内嵌视图的这个“子查询”其实就是上一步的结果,只不过是把Step 1的结果表作为了from后面的一坨,相当于在上一步基础上又新开出了一列,这一列命名为delta。观察结果:
SELECT *,
dateadd('month',tgrq,-rn) delta
FROM (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY zzh,YEAR(TGRQ) ORDER BY TGRQ) rn
FROM TGB
)
再来关注delta列的情况:
zz01的6月和7月分别减1,2后,都变成了5月
zz03的6月和8月分别减1,2后,变成了5月和6月
zz04的2月和3月分别减1,2后,都变成了1月
因为row_number()优秀的“天然连续”,所以作差这一步的思想,是抹平某一个序列与自然数列的“差距”,例如:
(1,2,3,5,7,8,9)
(对应位置作差)
(1,2,3,4,5,6,7)
连续的部分有:
(1,2,3) => (0,0,0)
(7,8,9) => (2,2,2)
非连续的部分:
(3,5) => (0,1)
(5,7) => (1,2)
所以,日期减去rn后,如果delta相同,就表明连续,且有几个相同的,就连续几次
Step 3——
把刚刚Step 2的结果作为内嵌视图,使用一次普通聚合,把zzh作者号选出来,数一下delta有几个,这里还可以显示出开始投稿日期,因为数的是delta为2的有几个,因此trunc掉delta的日、时、分、秒部分,dateadd月份加1,再data_format转为'YYYY-MM'形式
SELECT zzh 作者号,
date_format(dateadd('mm',trunc(delta,'month'),1),'%Y-%m') 开始投稿日期,
COUNT(*) 连续投稿次数
FROM (
SELECT *,
dateadd('month',tgrq,-rn) delta
FROM (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY zzh,YEAR(TGRQ) ORDER BY TGRQ) rn
FROM TGB
)
)GROUP BY ZZH,trunc(delta,'month')
HAVING COUNT(*)=2;
为什么最后一步不开窗?数delta也可以用count,但总体会套4层代码,且最外层只是用来做where筛选,不太好看而已,代码如下:
这样也能做,只是最大程度保住了原表的样子(group by因聚合会丢弃很多列信息),但同时也能看到该作者连续投稿月份最早投的是哪个文章,通过与否(意义不是很大)
SELECT *
FROM(
SELECT *,
COUNT(*) OVER(PARTITION BY ZZH,date_format(delta,'%Y%m')) cnt
FROM (
SELECT *,
dateadd('month',tgrq,-rn) delta
FROM (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY zzh,YEAR(TGRQ) ORDER BY TGRQ) rn
FROM TGB
)
)
)WHERE cnt=2 AND rn=1;
2.订单表题目
建DDB(订单表)语句:
CREATE TABLE DDB(
ddh char(4) NOT NULL ,
zyh char(3) NOT NULL ,
khh char(4) NOT NULL ,
qdrq date NOT NULL ,
je decimal(10,2)
);
导入数据:
这里因为做题的时候给的是excel,且数据较多,这里如没有数据,请自行下载该excel并导入你的数据库:
文件名 | 链接 | 提取码 |
---|---|---|
ddb.xlsx | https://pan.baidu.com/s/1HzdiUc8YV8Eu7ZvaRmc3kw | 6666 |
问:订单表中的订单号应该是连续编号,找出订单表中的订单号是否有重号?重号是什么?
重号问题,使用开窗函数来做的话,最先想到的是用dense_rank(),因为这个排序函数会让两个相等的值获得一样的排名,观察这个语句在发生重号处的结果:
SELECT ddh,
DENSE_RANK() OVER(ORDER BY ddh::INT) rn
FROM DDB
可以看到,只需要把第二列排序结果里出现两次及以上的筛选出来,就得到了发生重号的地方
直接贴上完整的查重语句:
SELECT DDH 订单号,
COUNT(*) 重复次数
FROM (
SELECT ddh,
DENSE_RANK() OVER(ORDER BY ddh::INT) rn
FROM DDB
)
GROUP BY DDH
HAVING COUNT(*)>=2;
可以想见,本题里最多只出现了2次重号,如果使用自连接,确实只需要join
一次即可,但如果重号发生很多,使用开窗的这种方式不管重了多少次都可以检测出来。本题也可以用row_number(),但是语句会比dense_rank()复杂,语句贴在下面
这里最外层用到了lead偏移,思考一下,只偏移一行,能否找出所有重号的行?如果这题里重号的不止2个,而是3个,4个。。,答案是可以,因为作差后,只要重复,那么重号的那部分的下一行,差值都比他小,所以where条件后面一定可以筛出来。最后的结果每个重号有一行,可以推知,该号只重了一次,如果重两次的话,筛选结果里,该号会有两行
SELECT * FROM (
SELECT *,
lead(new_ddh) OVER(ORDER BY ddh::INT) lag_new_ddh
FROM (
SELECT *,
DDH::INT-rn new_ddh
FROM (
SELECT ddh,
ROW_NUMBER() OVER(ORDER BY ddh::INT) rn
FROM DDB
)
)
)WHERE new_ddh>lag_new_ddh;
问:订单表中的订单号应该是连续编号,看看订单表中订单号是否存在断号,断号是什么?
先来思考一下,现在我们已经可以用row_number()生成一个自然序列了,如果用这个自然序列左连接订单号,那么存在断号的地方,其肯定会有NULL值,然后再把NULL值筛选出来,这个思路可行么?
语句如下:
SELECT rn,new_ddh
FROM (
SELECT *
FROM (
SELECT DDH,
ROW_NUMBER() OVER(ORDER BY ddh::INT) rn
FROM DDB) a
LEFT JOIN
(SELECT DDH new_ddh FROM DDB) b
ON a.rn=new_ddh
)WHERE new_ddh IS NULL;
前面的部分看起来一切OK,但——订单号本来就存在重号、断号的情况,使用row_number()的话,行号必然会小于最大订单号,也就是说会筛漏掉一部分。
开窗函数中的row_number()是根据原表中有多少行来生成的,所以这里用这种方法的话,我们得自己考虑生成一个自然数列,而且这个数列的最大值只需要等于最大订单号即可,所以这里使用了level和connect by语法:
SELECT *
FROM (
SELECT LEVEL num
FROM dual
CONNECT BY LEVEL<=(SELECT max(ddh::INT) FROM DDB)) a
LEFT JOIN
(SELECT DDH::INT FROM DDB) b
ON a.num=b.ddh
WHERE ddh IS NULL
ORDER BY num;
对于连续来说,更常用的还是row_number()结合作差,这个题应该怎么解?
不好的地方:筛选结果为断号的下一个号,只知道断了几个号,但好在思路更通用一些
这个表的意思:44之前发生了断号,且断了5个号——>应该为39.40,41,42,43
SELECT *,
delta-lag_delta cnt_empty
FROM (
SELECT *,
lag(delta) OVER(ORDER BY ddh::INT) lag_delta
FROM (
SELECT *,
DDH-rn delta
FROM (
SELECT DDH,
ROW_NUMBER() OVER(ORDER BY DDH::INT) rn
FROM DDB
)
)
)
WHERE delta>lag_delta OR lag_delta IS NULL;
3.来个习题吧
tb_sale_amount为商品数量表,
good_category——商品类型
sale_date——销售日期
amount——销售数量
问:求占据前90%销售额的商品类型
输出结果:
good_category |
---|
1006 |
1005 |
1003 |
表结构:
create table tb_sale_amount(
good_category int,
sale_date date,
amount int,
primary key(good_category, sale_date)
);
插入数据:
insert into tb_sale_amount
(good_category, sale_date, amount) values
(1003, '2022-01-10', 264),
(1001, '2022-06-01', 21),
(1005, '2022-06-01', 73),
(1002, '2022-06-27', 44),
(1006, '2022-06-27', 405),
(1003, '2022-09-10', 16),
(1005, '2022-09-13', 72),
(1004, '2022-10-01', 29),
(1005, '2022-10-03', 332),
(1001, '2022-10-29', 10),
(1006, '2022-10-29', 137),
(1002, '2022-12-02', 23),
(1007, '2022-12-02', 19),
(1003, '2022-12-02', 30),
(1008, '2022-12-03', 3),
(1009, '2022-12-04', 1),
(1010, '2022-12-05', 9),
(1003, '2022-12-30', 121);
答案
-- step1. 计算每种商品的总销售额,并降序排序
select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc;
-- step2. 求全部商品的总销售额,为了step3求各种商品的占比,需要先求和。注意:求总和时,窗口值既不排序也不进行分组
select
*,
sum(good_amount) over () as all_amount
from
(select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc
) t1;
-- step3. 求占比
select
*,
good_amount * 1.0 / all_amount as ratio
from (select
*,
sum(good_amount) over () as all_amount
from (select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc
) as t1
) t2;
-- step4. 求累计占比,注意:求累计值时,一定要进行排序
select
*,
sum(ratio) over (order by ratio desc) as acc_ratio
from(
select
*,
good_amount * 1.0 / all_amount as ratio
from (select
*,
sum(good_amount) over () as all_amount
from (select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc
) as t1
) t2
) t3;
-- step5. 求前一行的累计占比
select
*,
lag(acc_ratio) over(order by ratio desc) as pre_acc_ratio
from(select
*,
sum(ratio) over (order by ratio desc) as acc_ratio
from(
select
*,
good_amount * 1.0 / all_amount as ratio
from (select
*,
sum(good_amount) over () as all_amount
from (select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc
) t1
) t2
) t3
) t4;
-- step6. 过滤
select *
from (
select
*,
lag(acc_ratio) over(order by ratio desc) as pre_acc_ratio
from(select
*,
sum(ratio) over (order by ratio desc) as acc_ratio
from(
select
*,
good_amount * 1.0 / all_amount as ratio
from (select
*,
sum(good_amount) over () as all_amount
from (select
good_category,
sum(amount) as good_amount
from tb_sale_amount
group by good_category
order by good_amount desc
) t1
) t2
) t3
) t4
) t5
where pre_acc_ratio IS NULL or pre_acc_ratio < 0.90;
关于伪列
教材上有一个案例,其用到的是rownum
select *,
rownum
from ddb
where rownum<=10
这个语句似乎很好理解,把表里前10行数据拿到了,但为什么下面这个语句拿不到除了第1行以外的数据呢?
select *,
rownum
from ddb
where rownum>1
rownum的生成是在where之前,可以理解为:数据库把表结果筛好,拿出来,在放到你的眼前时,给每一行加了一个行号
所以上面第一个语句能出结果的原因,是当数据库把第一条记录拿到的时候,给了一个rownum——1,再来看条件:<=10,满足?yes——输出
以此类推,拿到第二条记录,rownum—— 2 —— <=10?——输出。。。一直到第10行
第二个语句:
第一条记录,rownum—— 1 —— >1?——不输出
第二条记录,rownum—— 1(注意这里为什么还是给它1) —— >1?——不输出
其他还可用作练习的题
以下一些是可以使用开窗函数来解决的、之前做过的题,这里受限于篇幅不再详列数据,可根据题目去找一下原题在哪,看开窗的方法能否帮到你,理论上来说,那些需要使用group by的题目,开窗都可以用,只是实现路径不同
问:加油站编号的首位代表加油站所在的区域,每个区域的加油站都有加油记录。基于加油表查询哪些加油卡在全部区域都有加油记录,输出卡号。(阶段测验2 补充几个问题.docx)
问:基于加油卡表查询每天办理的加油卡的卡号是否正常,即卡号的后4位从0001开始,不重复且连续。输出日期和状态(卡号正常、不正常) (阶段测验2 补充几个问题.docx)
问:统计出哪些单位同一年连续3个月都有订购教材,输出单位信息。(阶段练习3.docx)
问:统计出哪些单位连续3个月都没有订购教材,输出单位信息。(阶段练习3.docx)
。。。
小结
1.求解连续问题,开窗函数优势在于:不论连续多少次,都可以解决
2.聚合类开窗,sum,min,max,avg,count等等大部分时候语法不如普通聚合简洁
3.由于往往需要根据上一次开窗的结果进行筛选、作差等等,开窗一般需要多层嵌套,代码形式上看起来会比普通连接查询复杂,不必刻意追求代码复杂性来炫技
4.开窗更像是一种excel思维,结果比group by 等可以保留更多列
5.开窗函数在解决连续问题上有一定优势,聚合类大部分时候比group by复杂,如果已经能熟练运用连接、group by等,可以把本篇文章讲的内容当作参考
6.写了那么多,其实就只有一点内容,也就是核心作差的那一步,只是为了讲清楚开窗函数,才理了很多概念内容,搬了很多语句,有点为了造出个轮子,把车身车体搬出来造一遍的意思
7.不足之处,请多指正