一个案例教你如何深入理解窗口函数? | 窗口函数作为辅助变量解决复杂SQL问题

本文详细介绍了如何使用HiveSQL的窗口函数解决实际问题,包括每年每门学科的第一名学生和每年总成绩提升的学生查询。通过对比join思维,展示了窗口函数在简化代码、提高效率方面的优势,并探讨了窗口函数作为辅助列的计算原理。
摘要由CSDN通过智能技术生成

目录

0. 需 求

1.实现

2 小 结


0. 需 求

有如下数据表

yearsubjectstudentscore
2018语文A84
2018数学A59
2018英语A30
2018语文B44
2018数学B76
2018英语B68
2019语文A51
2019数学A94
2019英语A71
2019语文B87
2019数学B44
2019英语B38
2020语文A91
2020数学A50
2020英语A89
2020语文B81
2020数学B84
2020英语B98

需求如下:

针对上面一张学生成绩表(class),有year-学年,subject-课程,student-学生,score-分数这四个字段,请完成如下问题:

  • 问题1:每年每门学科排名第一的学生是?
  • 问题2:每年总成绩都有所提升的学生是?

1.实现

(1)数据准备

2018,语文,A,84
2018,数学,A,59
2018,英语,A,30
2018,语文,B,44
2018,数学,B,76
2018,英语,B,68
2019,语文,A,51
2019,数学,A,94
2019,英语,A,71
2019,语文,B,87
2019,数学,B,44
2019,英语,B,38
2020,语文,A,91
2020,数学,A,50
2020,英语,A,89
2020,语文,B,81
2020,数学,B,84
2020,英语,B,98

(2)创建hive表

drop table if exists class
CREATE TABLE dan_test.class ( 
        year string, 
        subject string,
        student string,
        score string )
ROW format delimited FIELDS TERMINATED BY ",";

(3) 导入数据

load data local inpath "/home/centos/dan_test/class.txt" into table class;

(4)实现

问题1:每年每门学科排名第一的学生是?

方法一:join思维

由题意知需要按year和subject进行分组,最终求的是学生是谁

先求出每年没门学科最高成绩

select year,subject,max(score)
from class
group by year,subject

OK
2018	数学	76
2018	英语	68
2018	语文	84
2019	数学	94
2019	英语	71
2019	语文	87
2020	数学	84
2020	英语	98
2020	语文	91

由于要求的是最高成绩对应的学生,所以需要在原始表中找出对应的学生,这时候就要相当数据不全需要补充数据,通过join来与不同表进行关联来获取我们需要的信息。按照year,subject,max(score)来与原表进行关联

select *
from (
 select year,subject,max(score) as max_score
 from class
 group by year,subject
) a 
join class b
on a.year = b.year and a.subject = b.subject
and a.max_score = b.score

关联后结果如下:

OK
2018	数学	76	2018	数学	B	76
2018	英语	68	2018	英语	B	68
2018	语文	84	2018	语文	A	84
2019	数学	94	2019	数学	A	94
2019	英语	71	2019	英语	A	71
2019	语文	87	2019	语文	B	87
2020	数学	84	2020	数学	B	84
2020	英语	98	2020	英语	B	98
2020	语文	91	2020	语文	A	91

因而最终的结果为:

select a.year,a.subject,a.max_score,b.student
from (
 select year,subject,max(score) as max_score
 from class
 group by year,subject
) a 
join class b
on a.year = b.year and a.subject = b.subject
and a.max_score = b.score


获得的结果为:
2018	数学	76	B
2018	英语	68	B
2018	语文	84	A
2019	数学	94	A
2019	英语	71	A
2019	语文	87	B
2020	数学	84	B
2020	英语	98	B
2020	语文	91	A

方法二:窗口思维

所谓的窗口思维其实本质就是把数据看成数据集(集合思维),把一张完整的表找出对应的需要计算的数据分片。数据分片指满足条件的数据范围,或计算时需要的作用阈。

利用窗口函数增加辅助列来计算,很明显本题窗口范围依旧是按照年和科目分组后的数据,我们可以利用分析函数max()对该窗口内的数据进行聚合求出每门课的最高成绩,作为辅助列。(辅助列往往是作为一种映射,一种对应关系而存在)

select year,subject,score,student
,max(score) over 
(partition by year,subject) as max_score 
--增加一列为聚合后的最高分作为辅助列
from `class`

计算结果如下:
2018	数学	76	B	76
2018	数学	59	A	76
2018	英语	30	A	68
2018	英语	68	B	68
2018	语文	44	B	84
2018	语文	84	A	84
2019	数学	44	B	94
2019	数学	94	A	94
2019	英语	38	B	71
2019	英语	71	A	71
2019	语文	87	B	87
2019	语文	51	A	87
2020	数学	50	A	84
2020	数学	84	B	84
2020	英语	89	A	98
2020	英语	98	B	98
2020	语文	91	A	91
2020	语文	81	B	91

由上面计算结果可以看出,最后一列为max_score列,该列的左边为数据表本身对应的字段值,为了求出每年没门学科成绩最高的学生, 我们可以进行过滤通过原表class中score字段值与辅助列中字段值一致时筛选出我们需要的结果。因而最终结果如下:

select a.year,a.subject,a.score,a.student
from (
  select year,subject,score,student
        ,max(score) over 
        (partition by year,subject) as max_score 
--增加一列为聚合后的最高分
from `class`
) a
where a.score = a.max_score  --保留与最高分相同的记录

计算结果如下:
2018	数学	76	B
2018	英语	68	B
2018	语文	84	A
2019	数学	94	A
2019	英语	71	A
2019	语文	87	B
2020	数学	84	B
2020	英语	98	B
2020	语文	91	A

采用row_number()分析函数完成如下,思路同上

select a.year,a.subject,a.score,a.student
from (
  select year,subject,score,student
        ,row_number() over 
        (partition by year,subject order by score desc) as rn --给按照条件筛选的窗口中每条记录打上序号
--增加一列为聚合后的最高分
from `class`
) a
where rn = 1  --选出成绩按降序排序后最高的记录

计算结果如下:

2018	数学	76	B
2018	英语	68	B
2018	语文	84	A
2019	数学	94	A
2019	英语	71	A
2019	语文	87	B
2020	数学	84	B
2020	英语	98	B
2020	语文	91	A

 采用first_value()分析函数计算。first_value()返回分组排序后,组内第一行某个字段的值

未去重的结果
select year,subject,score
,first_value(student) over 
(partition by year,subject 
order by score desc) as student
from class

2018	数学	76	B
2018	数学	59	B
2018	英语	68	B
2018	英语	30	B
2018	语文	84	A
2018	语文	44	A
2019	数学	94	A
2019	数学	44	A
2019	英语	71	A
2019	英语	38	A
2019	语文	87	B
2019	语文	51	B
2020	数学	84	B
2020	数学	50	B
2020	英语	98	B
2020	英语	89	B
2020	语文	91	A
2020	语文	81	A

最终sql 
select distinct year,subject,score --去重是因为first_value(student)取出的是窗口内排序后第一条记录的学生值,由于该字段生成是针对每条记录的,因而会有重复,需要去重
,first_value(student) over 
(partition by year,subject 
order by score desc) as student
from class

计算后的结果为:
2018	数学	B
2018	英语	B
2018	语文	A
2019	数学	A
2019	英语	A
2019	语文	B
2020	数学	B
2020	英语	B
2020	语文	A

由以上可以看出,采用窗口函数进行分析要比join思维代码要简洁,而且效率要高,通过窗口函数对原纪录增加新列进行辅助计算避免了join操作,该新列的建立其实是针对每条记录按照条件进行的映射,可以看成标志位,如本题中的max_score及cn等,然后根据标志位再进行筛选得出最终的结果。

*此种应用场景体现了窗口函数的辅助计算功能,之所以能进行辅助计算,其本质是利用窗口进行条件关系之间的映射。

②问题2:每年总成绩都有所提升的学生是?

join 思维

a.容易想到可以先求每年每个学生的总成绩

select year,student,sum(score)
from class
group by year,student

-------------------------------------
2018	A	173.0
2018	B	188.0
2019	A	216.0
2019	B	169.0
2020	A	230.0
2020	B	263.0

b.要求每年总成绩有所提升的学生,由于步骤a求得数据明显不全,需要补全数据,因此对步骤a求得的表进行自关联,按照学生来进行关联。自关联也是将信息补全,便于求解

select a.year,a.student,a.sum_score,b.year,b.student,b.sum_score
from (
  select year,student,sum(score) as sum_score
  from class
  group by year,student
) a join (
  select year,student,sum(score) as sum_score
  from class
  group by year,student
) as b on a.student = b.student

-------------------------------------
2018	A	173.0	2018	A	173.0
2018	A	173.0	2020	A	230.0
2018	A	173.0	2019	A	216.0
2018	B	188.0	2018	B	188.0
2018	B	188.0	2020	B	263.0
2018	B	188.0	2019	B	169.0
2019	A	216.0	2018	A	173.0
2019	A	216.0	2020	A	230.0
2019	A	216.0	2019	A	216.0
2019	B	169.0	2018	B	188.0
2019	B	169.0	2020	B	263.0
2019	B	169.0	2019	B	169.0
2020	A	230.0	2018	A	173.0
2020	A	230.0	2020	A	230.0
2020	A	230.0	2019	A	216.0
2020	B	263.0	2018	B	188.0
2020	B	263.0	2020	B	263.0
2020	B	263.0	2019	B	169.0

 c.按照步骤b所求的自关联结果,我们对结果集中每条数据打标签,如果b_sum_score-c.a_sum_score>0则置为1,小于0则置为0,打完标签后的结果如下

select  c.a_year
       ,c.a_student
       ,c.a_sum_score
       ,c.b_year
       ,c.b_student
       ,c.b_sum_score
       ,case when c.b_sum_score-c.a_sum_score > 0 then '1' else '0' end as flag
from (
  select a.year as a_year,a.student as a_student,a.sum_score as a_sum_score
      ,b.year as b_year,b.student as b_student,b.sum_score as b_sum_score
  from (
    select year,student,sum(score) as sum_score
    from class
    group by year,student
   ) a join (
            select year,student,sum(score) as sum_score
            from class
            group by year,student
   ) as b on a.student = b.student
) c
-----------------------------------------------------
2018	A	173.0	2018	A	173.0	0
2018	A	173.0	2020	A	230.0	1
2018	A	173.0	2019	A	216.0	1
2018	B	188.0	2018	B	188.0	0
2018	B	188.0	2020	B	263.0	1
2018	B	188.0	2019	B	169.0	0
2019	A	216.0	2018	A	173.0	0
2019	A	216.0	2020	A	230.0	1
2019	A	216.0	2019	A	216.0	0
2019	B	169.0	2018	B	188.0	1
2019	B	169.0	2020	B	263.0	1
2019	B	169.0	2019	B	169.0	0
2020	A	230.0	2018	A	173.0	0
2020	A	230.0	2020	A	230.0	0
2020	A	230.0	2019	A	216.0	0
2020	B	263.0	2018	B	188.0	0
2020	B	263.0	2020	B	263.0	0
2020	B	263.0	2019	B	169.0	0

d 对步骤c中的结果进行判断,按照学生进行分组,在分组的结果集中过滤出需要的结果

select d.a_student
from(
select  c.a_year as a_year
       ,c.a_student as a_student
       ,c.a_sum_score a_sum_score
       ,c.b_year as b_year
       ,c.b_student as b_student
       ,c.b_sum_score as b_sum_score
       ,case when c.b_sum_score-c.a_sum_score > 0 then '1' else '0' end as flag
from (
  select a.year as a_year,a.student as a_student,a.sum_score as a_sum_score
      ,b.year as b_year,b.student as b_student,b.sum_score as b_sum_score
  from (
    select year,student,sum(score) as sum_score
    from class
    group by year,student
   ) a join (
            select year,student,sum(score) as sum_score
            from class
            group by year,student
   ) as b on a.student = b.student
) c

) d
where d.a_year in (select min(year) from class group by student) --在自关联表中只有a表中最小的年份才会遇到有对比性可以看出增长性,where子句中不可以使用聚合函数,因而利用子查询先求出最小年份。
group by d.a_student
having sum(d.flag) = count(d.a_year)-1 --由于自关联中会出现与自己相同的,因而相减的时候会有年份相同的值,排除自己的,需要减一进行判断。having子句是在分组后的结果集中进行筛选,因而可以使用聚合函数

-----------------------------------------------------------------------
--------------------------------------------------------------------------------
        VERTICES      STATUS  TOTAL  COMPLETED  RUNNING  PENDING  FAILED  KILLED
--------------------------------------------------------------------------------
Map 1 ..........   SUCCEEDED      1          1        0        0       0       0
Map 3 ..........   SUCCEEDED      1          1        0        0       0       0
Map 6 ..........   SUCCEEDED      1          1        0        0       0       0
Reducer 2 ......   SUCCEEDED      1          1        0        0       0       0
Reducer 4 ......   SUCCEEDED      1          1        0        0       0       0
Reducer 5 ......   SUCCEEDED      1          1        0        0       0       0
Reducer 7 ......   SUCCEEDED      1          1        0        0       0       0
--------------------------------------------------------------------------------
VERTICES: 07/07  [==========================>>] 100%  ELAPSED TIME: 10.33 s    
--------------------------------------------------------------------------------
OK
A
Time taken: 11.374 seconds, Fetched: 1 row(s)

*注:显示利用join思维的方式解法非常麻烦,而且自关联的方式效率比较低。上述只是为了用join的方法给出一种结果,并不是最优,读者如果有更好的方法可以留言,一起讨论。

窗口思维

下面给出比较好的解法,利用窗口函数进行求解。

我们知道,分析函数中lag()函数可以不用进行自关联,取除当前行外获取前面指定行某字段的值。因为为了比较每年学生总成绩都有所提升,我们可以通过该函数获取上一年学生的总成绩与当前行成绩进行比较。lag()函数又称行比较分析函数。

a.分析的主表还是每年每个学生的总成绩表,很明显需要需要将学生分成一组,按年的正序进行排序的窗口进行分析

-------------------------------------
2018	A	173.0
2018	B	188.0
2019	A	216.0
2019	B	169.0
2020	A	230.0
2020	B	263.0


很明显需要需要将学生分成一组,按年的正序进行排序的窗口进行分析,如下所示
--------------------------------------
2018	A	173.0
2019	A	216.0
2020	A	230.0
-----------------------
2018	B	188.0
2019	B	169.0
2020	B	263.0

b.我们利用lag()函数访问上一行的成绩,利用本行的成绩减去上一行的成绩进行判断,如果差值大于0则设置标签为1说明今年成绩提高,然后按照学生分组,分组后判断flag为1的值的和是否和年份的记录数一致,如果一致则表示每年都在增长。具体SQL代码如下:

select student
from
(
  select year,student
 ,case when (sum_score - lag(sum_score,1,0) 
  over 
  (
    partition by student 
    order by year
  )) > 0 then 1 else 0 end as flag
  --按照student进行分区并进行year正序排序
  --找到每个学生的上一年学年总成绩
  --并用当年成绩减去上一年的成绩,如果大于0则置为1,否则将flag值置为0
  from
  (
    select year,student
    ,sum(score) as sum_score 
    --按照学年和学生进行成绩汇总
    from class
    group by year,student
  ) a 
) b 
group by student
having sum(flag) = count(year) 
--flag值为1的和与count(year)的个数相同代表每年成绩都在增长。

执行结果如下:
--------------------------------------------------------------------------------
        VERTICES      STATUS  TOTAL  COMPLETED  RUNNING  PENDING  FAILED  KILLED
--------------------------------------------------------------------------------
Map 1 ..........   SUCCEEDED      1          1        0        0       0       0
Reducer 2 ......   SUCCEEDED      1          1        0        0       0       0
Reducer 3 ......   SUCCEEDED      1          1        0        0       0       0
Reducer 4 ......   SUCCEEDED      1          1        0        0       0       0
--------------------------------------------------------------------------------
VERTICES: 04/04  [==========================>>] 100%  ELAPSED TIME: 10.12 s    
--------------------------------------------------------------------------------
OK
A
Time taken: 10.879 seconds, Fetched: 1 row(s)

*很显然,利用窗口进行分析代码要简洁的多,而且执行效率较高,也不容易分析错误,也可起到简化代码的效果。

通过以上问题可以看出通过窗口函数形式进行分析具有如下效果:

  • (1)简化代码、简化思维
  • (2)提高代码的执行效率
  • (3)窗口函数并不改变原表的结构,只是作为计算的辅助手段,可以起到辅助变量的作用简化代码
  • (4)窗口函数辅助标签是针对表中每条记录进行标签的,会辅助增加一列,建立在每一条记录上。而这一列其实本质就是根据条件建立的某种映射关系,这种关系成为辅助计算的关键(解题的突破口)。

2 小 结

     本题主要使用的知识点如下:

  • (1)窗口思维与join思维的认识
  • (2)first_value()函数与lag()函数的使用
  • (3)窗口函数作为辅助列在计算中的应用

应用案例

SQL进阶技巧:断点分组算法实践与应用_sql多重分组-CSDN博客

SQL进阶技巧:间隔连续问题【断点分组思想】_sql重分组思想-CSDN博客

Sql进阶技巧:如何分析去掉最大最小值的平均薪水【字节跳动】_hive sum 排除最高和最低-CSDN博客

Sql进阶技巧:如何进行有效的数据清洗?【京东面试题之有效值问题】_hivesql面试题50题-CSDN博客

经典连续性问题:数学思维在SQL编程中的应用_经典连续性问题--数学思维在sql编程中的应用-CSDN博客Sql进阶技巧:多指标累计去重问题_hive 累计去重统计-CSDN博客

SQL高阶技巧:一文说透窗口函数_窗口函数应用之移动范围计算【详细剖析窗口函数】-CSDN博客 SQL进阶技巧:贪心算法应用之重叠交叉区间问题分析_semanticexception end of a windowframe cannot be u-CSDN博客

SQL高阶技巧:一种中位数的分析技巧及思路_sql cnt-CSDN博客

SQL高级技巧:如何精准计算非连续日期累计值【闪电快车面试题】-CSDN博客

SQL高阶技巧:如何统计当前时间点状态情况【辅助变量+累计变换思路】_sql查询统计某状态出现的次数及累计时间-CSDN博客 水位线思想在解决SQL复杂场景问题中的应用与研究_水位线的思想处理问题,我们先根据实际场景需要解决的问题来引出这一话题,例如有下-CSDN博客

SQL进阶技巧:如何按照区间或时段对数据进行动态分桶?_sql分时间段(分区间)统计问题--hivesql面试题20-CSDN博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值