Hive-SQL面试题2详解(窗口函数作为辅助列在计算中的应用)

目录

0. 需 求

1.实现

2 小 结


0. 需 求

有如下数据表

year subject student score
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

需求如下:

针对上面一张学生成绩表(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)窗口函数作为辅助列在计算中的应用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章