目录
0. 需 求
1.实现
2 小 结
有如下数据表
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)数据准备
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)
*很显然,利用窗口进行分析代码要简洁的多,而且执行效率较高,也不容易分析错误,也可起到简化思维的效果。
通过以上问题可以看出通过窗口函数形式进行分析具有如下效果:
本题主要使用的知识点如下: