Hive解题思路
1 相关知识讲解
1.1 HQL语句的语法
sql语句的语法:
select ..... from .... join ..... where .....group by ... having...order by|sort by|cluster by|distribute by ....
(1)group by:按照某些字段的值进行分组,相同的一组。注意:限制了select字段,发生在select之前,不可以使用select别名,select后面非聚合列,必须出现在group by中 select后面除了普通列就是一些聚合操作 group by后面也可以跟表达式。
(2)where和having:都是限定返回的数据集。
where:对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚组函数,使用where条件显示特定的行。
having:在分组group by后,将分组后的不符合条件的数据过滤掉,
查询语句的聚合函数只能出现在select 和group by ,having 子句。
(3)四个排序关键字
①order by:对输入做全局排序,所以只有一个reduce,当数据量大时,导致效率低;
②sort by:局部排序,不是全局排序,其在数据进入 reducer 前完成排序。如果用 sort by 进行排序,并且设置 mapred.reduce.tasks>1,那么 sort by只保证每个 reducer 的输出有序,不能保证全局有序;
③distribute by:根据指定的字段将数据分到不同的 reducer,分发算法是 hash 散列;
④cluster by:如果distribute by后面的字段和sort by后面的字段一样,cluster by = distribute by + sort by
1.2 窗口函数over
聚合函数可以将多行数据按照规则聚集为一行,一般来讲聚集后的行数是要少于聚集前的行数的,但是有时我们想要既显示聚集前的数据,又要显示聚集后的数据,这时我们便引入了窗口函数。窗口函数的执行时间且仅位于Order by字句之前。
分析函数 over(开窗依据distribute by |partition by 每一个窗口的排序规则 sort by |order by ) 子句 窗口函数 开窗函数
distribute by与 sort by搭配,partition by与order by 。
over子句不可单独使用,有两种搭配方式:
(1)与聚合函数一起使用,求的是每一个窗口内的聚合
①聚合函数 + over(distribute by ) 整个窗口的整体聚合
select max(sumpv) over(distribute by userid) from test;
②聚合函数 + over(distribute by sort by ) 截止到排序数据之前的,即当前。sort by子句会让输入的数据强制排序
执行顺序:distribute by 整个窗口 + sort by一条 + max
select userid,month,sumpv,max(sumpv) over(distribute by userid sort by sumpv) from test;
③如果已经通过使用partition by子句将数据进行了分组的处理,如果我们想要更细粒度的划分,我们就要引入window子句了
window子句: - PRECEDING:往前 - FOLLOWING:往后 - CURRENT ROW:当前行
- UNBOUNDED:起点 UNBOUNDED PRECEDING 从前面的起点 UNBOUNDED FOLLOWING:到后面的终点
select
sum(sumpv) over() as sumpvv1 #所有行相加
sum(sumpv) over(partition by userid) as sumpvv2 #按userid分组,组内数据相加
sum(sumpv) over(partition by userid order by month) as sumpvv3 #按userid分组,组内数据累加
sum(sumpv) over(partition by userid order by month rows between UNBOUNDED PRECEDING and current row) as sumpvv4 #按userid分组,组内数据累加
sum(sumpv) over(partition by userid order by month rows between 1 PRECEDING and current row) as sumpvv5 #当前行和前面一行做聚合
sum(sumpv) over(partition by userid order by month rows between 1 PRECEDING and 1 FOLLOWING) as sumpvv6 #当前行和前边一行及后面一行
sum(sumpv) over(partition by userid order by month rows between current row and UNBOUNDED FOLLOWING) as sumpvv 7 #当前行及后面所有行
from test; |
④over(partition by)与group by的主要区别为,带上group by的hql语句只能显示与分组聚合相关的字段,而带上over(partition by ......)的hql语句能显示所有字段.。
(2)与序列函数使用
序列函数是不支持window子句的
①NTILE(n),用于将分组数据按照顺序切分成n片,返回当前切片值,如果切片不均匀,默认增加第一个切片的分布
使用场景:假如我们想要每位顾客购买数量前1/5的交易记录,我们便可以使用这个函数.
select userid,month,sumpv, nitle(5) over() #全局数据切片 nitle(5) over(partition by userid) #按照userid进行分组,在分组内切片 nitle(5) over(order by month) #全局按照月份升序排列,数据切分成5份 nitle(5) over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,数据切分成5份 from test; |
②row_number()、rank()、dense_rank()
row_number():生成分组内记录的序列,从1开始,按照顺序,row_number()的值不会存在重复,当排序的值相同时,按照表中记录的顺序进行排列;
RANK():生成数据项在分组中的排名,排名相等会在名次中留下空位 ;
DENSE_RANK():生成数据项在分组中的排名,排名相等会在名次中不会留下空位。
③lag和lead
LAG (scalar_expression [,offset] [,default]) OVER ([query_partition_clause] order_by_clause)
LEAD (scalar_expression [,offset] [,default]) OVER ([query_partition_clause] order_by_clause);
这两个函数为常用的窗口函数,可以返回上下数据行的数据.
使用场景:如要查看顾客上次的购买时间可以这样去查询
select userid,month,sumpv, lag(month,1,'1900-01-01') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,取上一行数据的值。当没有上一行时取1900-01-01' lag(month,2') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,取上面2行的数据的值,当没有上两行时取NULL lead(month,1,'1900-01-01') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,取上下行数据的值 from test; |
当lag函数未设置行数值时,默认为1行.未设定取不到时的默认值时,取null值.
④first_value和last_value
first_value取分组内排序后,截止到当前行,第一个值 ;last_value取分组内排序后,截止到当前行,最后一个值。
select userid,month,sumpv, first_value(month,1,'1900-01-01') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,截止到当前行的第一个值 last_value(month,2') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,截止到当前行的最后一个值,即当前行 lead(month,1,'1900-01-01') over(partition by userid order by month) #按照userid进行分组,在分组内按照月份升序排列,取上下行数据的值 from test; |
2 使用窗口分析函数
现有这么一批数据 三个字段的意思: A,2015-01,5 现要求出:如每个用户截止到每月为止的最大单月访问次数和累计到该月的总访问次数 |
分析:求每个用户截止到每月为止的最大单月访问次数和累计到该月的总访问次数
既显示聚集前的数据,又要显示聚集后的数据,并且如果单是求最大单月访问次数的话直接按照用户+月份分组就行,但是还要求累计到该月,就应该联想当窗口函数,窗口函数有很多,比如:聚合函数 + over() ---整个窗口的整体聚合 first_value() over--分组后截止到当前行第一个值等。因为是要求累计到当前月,所以前面的数据是全部都要的。那么就可以使用 sum(字段名) + over(distribute by 字段名 sort by 字段名) ---------截止到排序数据之前的即当前 或 sum(字段名) over(partition by 字段名 order by 字段名 rows between UNBOUNDED PRECEDING and current row)
select userid, MONTH, sumpv, max(sumpv) over ( PARTITION BY userid ORDER BY MONTH ) maxpv, sum(sumpv) over ( PARTITION BY userid ORDER BY MONTH ) sumpvv FROM test; |
当然这题还有其他的解决方案,比如自连接
SELECT buserid, bmonth, bsumpv, max(asumpv) maxpv, sum(asumpv) sumpv FROM ( SELECT * FROM ( SELECT a.userid auserid, a. MONTH amonth, a.sumpv asumpv, b.userid buserid, b. MONTH bmonth, b.sumpv bsumpv FROM test a JOIN test b ON a.userid = b.userid ) c WHERE amonth <= bmonth ) d GROUP BY buserid, bmonth, bsumpv; |
总结:当题目要求既显示聚集前的数据,又要显示聚集后的数据,并且还要对数据进行累加的时候要想到用窗口函数。
3 行列转换
(1)行转列
数据如下:
求:所有数学课程成绩 大于 语文课程成绩的学生的学号
思路:要求的是数学成绩>语文成绩的学生的学号,目前的数据一行是每个学生每一科的成绩,我们查询出来的数据是一条一条的,并且可能每个学生的科目数量有可能不一样,那么要比较不同的行的数据是非常困难的。而我们知道当数据在同一行时,这一行的数据是非常好进行比较的,那么我们就可以将行转换成列,那么要比较的就在同一行了,处理起来非常容易。
那么接下来就是怎样将列转换成行,我们知道查询出来的结果的列是根据select后面的字段名的个数而定的,select后有多少个字段,查询的结果就有多少个列。因为科目是固定的,那么列就加上所有的科目,所以我们就可以原表中的每一行查询出来后进行进行判断,并对这条记录中的科目放到对应的select后字段下。那么就可以使用case when条件判断。
case when switch(字段){ 语法2:相当于if else
|
使用条件判断语句后有如下结果
SELECT sid, CASE WHEN course = "yuwen" THEN score ELSE 0 END yuwen, CASE WHEN course = "shuxue" THEN score ELSE 0 END shuxue FROM course; |
接下来就要合并相同id的数据,最终语句如下
SELECT
|
需求如下:
思路:因为要显示所有的课程,所以首先要使用collect_set()函数---将分组中的某列转为一个数组返回,获取到所有课程,然后获取到每一个学员所选的科目,判断有哪些科目,这里可以用if判断语句。所以把查询出一个所有课程作为一个表,每一个学员所选的科目作为另一个表,因为所有课程的表只有一条数据,所以可以用笛卡尔积。
hive 处于安全考虑 没有开启笛卡尔积,所以要手动开启,设置如下
set hive.strict.checks.cartesian.product=false;
set hive.mapred.mode=nonstrict;
select )c; |
总结:行转列即是把行转列前的分行条件,分别作为行转列后的各列的筛选条件。可以借助case when,if等判断函数,与collect_set(),collect_list()函数
(2)列转行在下面的TopN例子中一起讲解
4 group by相关问题
现有如下数据 2001010310 需求:求出每一年的最高温度是哪一天(日期, 最高温度) |
这题根据题意可知需要求每一年...所以对年进行分组,相同年份的为一组,分组之后使用max函数就可知最高温度是哪一天。因为最高温度那一天只有一条,所以可以和原表进行笛卡尔积连接,即可得知那一天的的日期和最高温度等。
select a.year year,a.max_temp max_temp,substr(b.data,1,8) day from (select substr(data,1,4) year,max(cast(substr(data,-2) as int)) max_temp from test04 group by substr(data,1,4)) a join test04 b on a.year=substr(b.data,1,4) and a.max_temp=cast(substr(b.data,-2) as int); |
总结:当我们遇到每一,每一个这种字眼时,就需要使用到分组,分组的字段就是跟在这些字眼后面的字段。
5 求TopN,及列转行相关问题
现在有这样一份数据: 字段的意义: 每一条记录中的爱好有多个值,以"-"分隔 需求:求出每种爱好中,年龄最大的两个人(爱好,年龄,姓名) |
总体思路:由求出每种爱好中可知要对爱好进行分组,根据年龄最大的两个人可知分组之后对年龄进行倒叙排序,然后取前两个。
因为表中的爱好以a-c-d-f方式存在,这样无法对每种爱好进行分组,我们要想对每种爱好进行分组,那么就必须一行只有一种爱好我们才能求得。所以这里我们就需要列转换成行,就需要用到explode()函数,explode只能有两种类型的参数,如下:
explode(ARRAY): 列表中的每个元素生成一行
explode(MAP):map中每个key-value对,生成一行,key为一列,value为一列
注意:explode()是有很多限制的,限制如下
1、No other expressions are allowed in SELECT #SELECT中不允许有其他表达式 如:SELECT id, explode(adid_list) AS aa... is not supported
2、UDTF's can't be nested #UDTF不能嵌套 如:SELECT explode(explode(adid_list)) AS aaa... is not supported
3、GROUP BY / CLUSTER BY / DISTRIBUTE BY / SORT BY is not supported #不支持分组排序等 如:SELECT explode(adid_list) AS aaa ... GROUP BY aaa is not supported |
,所以经常和lateral view搭配,除以上限制 LATERAL VIEW explode(expression) tableAlias AS columnAlias (',' columnAlias)
这里将爱好explode()炸裂之后,有几个爱好对应就生成几行,那么就需要将原表与炸裂后的爱好进行连接,那么怎么连接呢?原表的数据全都需要所以用原表做连接炸裂后的爱好表,因为炸裂后的就得出一个爱好的字段,无法用其他字段进行关联,那就用array_contains()函数,判断炸裂出来的爱好是否在原始的a-c-d-f中,如果在的话,那么就关联起来。然后再对姓名,年龄,爱好分组,起去重作用。到这就将炸裂后的字段关联上了,因为需求中要保留原表的爱好,年龄,姓名等信息,那么就可以使用窗口函数row_number() over(partition by hobc order by agec desc)对爱好排序,对年龄分组,生成一个新的序列字段。因为窗口函数是除了order外最后执行的,所以我们需要在生成窗户函数的这个表外进行过滤。最终代码如下:
select d.hobc,d.agec,d.namec from (select hobc,agec,namec, row_number() over (partition by hobc order by agec desc) as seqs from (select b.name as namec,b.age as agec,a.hob as hobc from test05 b left join (select explode(hobby) as hob from test05 ) a on array_contains(hobby,hob) group by b.name,b.age,a.hob) c )d where d.seqs <=2; |
总结:当遇到需求中有最...的几个,前几个这种字眼眼时,就可以用求解TopN的思路去分析。当列中字段的内容是以组合的形式存在,而我们又要对该列中的内中进行分组等操作,就需要使用explode()函数,将列炸裂成行,由于explode()有诸多限制,所以经常会和lateral view一起使用。
6 内置函数的自定义UDF
统计每个月每天出现的情况,比如下面两串数字 现在是想把这两串数字拼起来,比如第一天,通话标识为0,流量标识为1,我需要的数字是两个标识同一天有1的就置为1,两个1也置为1。结果为 1101010101010101010100101010101 数据: |
建表语句:create table test06(day1 string,day2 string) row format delimited fields terminated by "," ;
(1)内置函数实现
思路:将通话记录和流量记录分别用两个表取出来,并使用explode()函数炸裂,并不对炸裂后的结果使用row_number() over() ,生成行号,然后将这两个表根据行号进行左连接查询(有为空的就可以过滤掉),最后再用concat_ws()行数将列转换成行。
最终实现如下:
select concat_ws('',collect_list(cast(dyd as string))) from (select c.seqs,cast(c.dyc as int)|cast(b.dyb as int) as dyd from (select row_number() over() as seqs,a.dy as dyc from (select explode(split(day2,"")) as dy from test06) a where a.dy!='' order by seqs desc ) c left join (select row_number() over () as seqs,a.dy as dyb from (select explode(split(day1,"")) as dy from test06) a where a.dy!='' order by seqs desc ) b on c.seqs=b.seqs order by c.seqs desc) d; |
(2)自定义UDF临时函数步骤:
①继承 org.apache.hadoop.hive.ql.exec.UDF,重载 evaluate 方法
②打成 jar 包上传到服务器
③将 jar 包添加到 hive 的 classpath hive>add JAR “jar包在服务器的路径”
④查看jar包是否加入成功,hive> list jar
⑤创建临时函数与开发好的 class 关联起来
hive>create temporary function tolowercase as '类名全路径';
⑥show functions;就可以看多多了一个函数,不过这个为临时函数。
//思路将两串数字遍历一遍,每一为进行或运算,再拼接起来
public class MyUDF extends UDF {public String evaluate(String str1,String str2)throws HiveException {if(str1.length()!=str2.length()) {throw new UDFArgumentTypeException(0,"The lengths of the arguments are inconsistent");}String[] s1=str1.split("");String[] s2=str2.split("");StringBuffer str = new StringBuffer();for(int i=0;i<s1.length;i++) {if(!s1[i].equals("")) {Integer or=Integer.parseInt(s1[i])|Integer.parseInt(s2[i]);String temp=or.toString();if(temp!=null) {str.append(temp);}}}return str.toString();}
}