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)窗口函數作爲輔助列在計算中的應用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章