很高兴遇到你~
HiveSQL使用技巧与优化
SQL执行顺序:FROM->JOIN->WHERE->GROUP BY->HAVING->SELECT->ORDER BY->LIMIT
- distinct去重与count
--distinct去重时,如果存在NULL,结果会异常,Hive不会将null值归为一个值处理,此时需要给NULL进行转换 select distinct nvl(column1,''),nvl(column2,0) from t; --count(*)、count(1)对所有行进行统计,包括null行,count(column_name)只对该列中非null的进行统计 --Hive中要避免使用count(distinct),它无法进行聚合操作,只在一个reduce上完成,容易出现性能瓶颈甚至oom内存溢出,使用group by来替代 --count distinct select col1,count(distinct id) as did from t group by col1; --使用group by优化替代 select col1,count(id) as did from(select col1,id from t group by col1,id) as temp group by col1;
- subquerys子查询&exists/in&left semi join
--subquerys子查询:hive只支持from和where后的子查询
--如果子查询中包含null值,不能使用not in(not in会报错,in不会)
--不推荐使用in/not in,可使用exists/not exists替代,支持子查询中的多值匹配 --not exists和left join可以有等价写法 --not exists select a,b from t1 where not exists(select 1 from t2 where t1.a=t2.a and t1.b=t2.b); --等价not exists的left join写法 select t1.a,t2.b from t1 left join t2 on (t1.a=t2.a and t1.b=t2.b) where t2.a is null; --left semi join 替代 in和exists,效率更高 --LEFT SEMI JOIN(左半连接)是IN/EXISTS子句查询的一种更高效的实现 --LEFT SEMI JOIN 的限制是:JOIN 子句中右边的表只能在ON 子句中设置过滤条件,在WHERE 子句、SELECT 子句或其他地方过滤都不行 --LEFT SEMI JOIN 只会显示出左边表的字段,left semi join会掉右表中重复的记录,不会因为右表重复key join出多条 --in/exists SELECT a.key, a.value FROM a WHERE a.key in (SELECT b.key FROM B);
--left semi join替代in/exists SELECT a.key, a.val FROM a LEFT SEMI JOIN b on (a.key = b.key)
in和exists的选取:使用exists时,内表会对外表进行循环查询匹配,它不在乎后面内表子查询的返回值,只在乎有没有返回值,存在返回值,则条件为true,该条数据匹配成功,加入查询结果集中,如果没有返回值,条件为假,丢弃该条数据。in用法与exists一致,查询结果和解析计划一致,in在HQL中只会查询一次,然后把结果集存在临时文件中,然后再与外层查询SQL进行匹配。如果子查询得出的结果集记录较少,主查询中的表较大时使用in,反之外层的主查询记录数较少,子查询中的表大,优先使用exists。其实区分in和exists主要是造成了驱动顺序的改变,如果是exists,那么以外层表为驱动表,先被访问,如果是in,那么先执行子查询。子查询的表大的时候,使用exists可以有效减少总的循环次数来提升速度,当外查询的表大的时候,使用in可以有效减少对外查询表循环遍历来提升速度。
select a.id,a.name from t1 as a where exists(select b.id from t2 as b where a.id=b.id); select a.id,a.name from t1 as a where a.id in (select b.id from t2 as b);
- sort by&distribute by&cluster by&order by
/****************************************************************** 数据量较小order by即可,Hive中尽量不要使用order by,除非非常确定结果集非常小; 如果在strict模式下使用order by语句,那么必须要在语句中加上limit关键字,因为执行order by只启动单个reduce,如果排序的结果集过大,那么执行时间会很久; 实际场景中一般先使用sort by再使用order by效率更高一些,使用distribute和sort进行分组排序,sort by+order by,sort by过程可以设置reducer个数(n),order by过程用n个reduce的输出文件进行一次全排序,得到最终结果; distribute指定map输出结果是如何分配的,上句中相同的id会被分配到同一个reduce上去处理,然后再通过sort by对各个reduce上的id进行排序(被distribute by设定的字段为KEY,数据会被HASH分发到不同的reducer机器上,然后sort by会对同一个reducer机器上的每组数据进行局部排序)。 ******************************************************************/set hive.mapred.mode=nonstrict; (default value / 默认值) set hive.mapred.mode=strict; --sort by&distribute by --sort by只能保证在单个reduce内有序 select * from baidu_click distribute by product_line sort by click desc; select * from t distribute by id sort by id; --cluster by(distribute by + sort by替代方案) --当distribute by和sort by的字段完全一致时,等价于cluster by,但cluster by排序只能是升序排序,不能指定排序规则为ASC或者DESC --cluster by 和 distribute by 是很相似的,最大的不同是, cluster by 里含有一个分桶的方法 select * from emp cluster by deptno; select * from emp distribute by deptno sort by deptno; --常见两种高效的排序实现 --可先通过一个group by的子查询来取一个小的结果集,然后再对这个结果集进行全局排序 select * from ( select id,count(id) as cnt from t group by id) as temp order by temp.cnt; --高效实现top排序 --先取出各个结果集的top n,再取出全局的top n select a.id,salary from (select id,salary from t1 distribute by sort by salary desc limit 10) as temp order by temp.salary limit 10; --partition by order by使用distribute by sort by替代 select * from ( select class,grade,row_number() over(partition by class order by grade) as rn from table_name) where rn<=10; --使用distribute by sort by替代更高效 select * from ( select class,grade,row_number() over(distribute by class sort by grade) as rn from table_name) where rn<=10;
limit抽样
--使用limit时限制数据,不会进行全盘扫描,而是根据限制的数据量进行抽样,带有reduce的limit会产生不同结果 --设置参数优化(建议使用时手动开启) set hive.limit.optimize.enable=true; --默认false set hive.limit.row.max.size=xxx; --最大抽样数量 默认10万 set hive.limit.optimize.limit.file=xxx; --最大抽样文件数量 默认10
Join on和where的区别
--join on和where在做条件筛选时,on会显示所有满足条件和不满足条件的数据,而where只显示满足条件的数据
select t1.a,t2.a from t1 left join on t1.key=t2.key and t2.a>10; --t1.a 非>10的数据仍会显示
select t1.a,t2.a from t1 left join on t1.key=t2.key
where t2.a>10; --过滤掉前面join后的结果集,只显示t1.a>10的数据
Join优化
在做join连接时,倾向于将小表放入内存中,然后对大表进行map操作,此时join只发生在map阶段,即当扫描一个大表时,同时会去小表中查找与之匹配的数据进行连接,而无任何shuffle过程(shuffle往往带来大量的内存、磁盘、数据传输等资源消耗),从而提高执行效率。
--设置mapjoin参数 --开启方式(如果开启如下设置,小于阈值大小的表将被自动认为是小表而加载到内存(分布式缓存)中去) --建议在使用join时都开启此功能 set hive.auto.convert.join=true set hive.mapjoin.smalltable.filesize=xxxx --默认是25M
多表join的连接顺序:多表连接中join连接顺序,会转换为多个MR任务,每一个MR任务在Hive中成为join阶段(stage),在每一个stage,按照join顺序最后一个表尽量是最大的表,因为join前一阶段生成的数据会存在于reduce的buffer中,通过stream最后面的表,直接从reduce的buffer中读取已经缓冲的中间结果数据(中间结果数据可能是join顺序中,前面表连接的结果key,数据量相对较小,内存开销就越小),这样在与后面的大表进行连接时,只要从buffer中读取缓存的key,与大表的指定key进行连接,速度会更快,也避免可能的内存缓冲区溢出。
--多个表a、b、c、d表join时,数据量a<b<c<d select a.val,b.val,c.val,d.val from a join b on a.key=b.key join c on b.key=c.key join d on c.key=d.key;
Join key为null容易导致的错误:有别于其它传统关系型数据库,如在mysql中,join时,join key遇到null值会自动被忽略,但是在Hive中,作为join key的字段比较关联,null=null是有意义的,且返回ture,但是这个情景是没有任何实际意义的,且在null值比例很大的情况下,会导致严重的数据倾斜,所以在hive中一般需要手动过滤掉null值。
select t1.id,count(t1.id) from t1 join t2 on (t1.id=t2.id and t1.id is not null and t2.id is not null) group by t1.id;
Hive分区表使用踩过的坑
为了避免太多小文件,使用了这样一种数据归档的操作处理,跑批按照每日进行分区的表,如pt_dt为分区字段的分区表,按照日期进行归档处理,如:
CASE WHEN PT_DT<'2020-01-01' THEN SUBSTR(PT_DT,1,4)||'-01-01'
WHEN PT_DT>='2020-01-01' AND PT_DT<'2021-01-01' THEN SUBSTAR(PT_DT,1,7)||'-01'
ESLE PT_DT END AS PT_DT
归档操作先将原表(分区表)中的数据插入到一个临时表(parquet格式的非分区表)中,归档到临时表后,再清除原分区表中的数据,结果出现一个情况,当归档pt_dt时间分区数超过100多个时,SQL执行出现job任务报错,报错信息为:Error:Failed:Execution Error(state=08s01,code=2),具体报错:Fatal error occurred when node tried to create too many dynamic partitions.The maximum number of dynamic partitions is controlled by hive.exec.max.dynamic.partitions.pernode.Maximum was set to 100 partitions per node.
经查原因是动态分区属性导致的,hive.exec.max.dynamic.partitions.pernode=100,表示每个map或reduce可以创建的最大动态分区个数,默认100。这里因为创建的临时表不是分区表,再insert overwrite到原目标表(分区表)时,只启动了一个map,即该操作map数为1,reduce数为0,导致一个map任务创建的最大分区数超过了限制,导致报错。
解决办法:
1、在SQL执行脚本中设置属性,调大map的最大分区数据控制,如:hive.exec.max.dynamic.partitions.pernode=400;
2、调整归档操作中的case when,调整其可能分区的数量,更大粒度的进行分区。
3、或者在归档操作中创建的临时表也为对应的分区表,以此在将临时表中的数据插入到目标时,启动的map数为分区的数量,实际的操作也会是将对应文件进行位置移动,该方法处理后也不会发生该报错,三种方法均可。
分区表涉及的几个重要参数设置
--动态分区属性:设置为true表示开启动态分区功能(默认为false) hive.exec.dynamic.partition=true; --动态分区属性:设置为nonstrict,表示允许所有分区都是动态的(默认为strict) --设置为strict,表示必须保证至少有一个分区是静态的 hive.exec.dynamic.partition.mode=strict; --动态分区属性:每个mapper或reducer可以创建的最大动态分区个数 hive.exec.max.dynamic.partitions.pernode=100; --动态分区属性:一个动态分区创建语句可以创建的最大动态分区个数 hive.exec.max.dynamic.partitions=1000; --动态分区属性:全局可以创建的最大文件个数 hive.exec.max.created.files=100000;
HiveSQL使用注意项
-
创建表和删除表使用if not exists/if exists防止异常;
-
分区字段不能出现在建表中,只能出现在partition by中;
-
使用具体列名避免使用select *;
-
where 条件过滤时,!=、<>都会将null值过滤掉,导致实际结果集变小,如果需要保留null值:where (col1 <> 'value' or col1 is null);
-
group by时,select的列别名不能被group by解析,group by后不能使用别名,因为hive执行解析严格按照SQL执行顺序,先group by,后select;
-
Hive不支持UPDATE操作,只能drop再insert;
-
hive创建视图和其它数据库创建视图无异;
-
hive int与string类型,null底层默认存储为\N,查询显示为null,导出文件会以存储格式导出,需要注意。若导出为null,存储的字符串就是null字符串而非null值;SQL中null代表空值, 值得警惕的是, 在HiveQL中String类型的字段若是空(empty)字符串, 即长度为0, 那么对它进行IS NULL的判断结果是False;
-
分号是SQL语句结束标记,在HiveQL中也是,但是在HiveSQL中,对分号的识别没有那么智慧,在DBeaver SQL IDE中也会出现因为加了;导致SQL报错的情况。另外字符';'使用需要转义,如:select concat(key,concat(';',key)); 在Hive中会报错,应使用分号的八进制的ASCII码进行转义,应写成:select concat(key,concat('\073',key));
-
hive支持嵌入mapreduce程序,来处理复杂的逻辑,但一般不使用,为了维护方便,类似桶表一般也不使用;
-
如何查看Hive的属性设置情况,如:set hive.mapred.mode; --hive.mapred.mode=nonstrict,要注意的是strict模式也会限制分区表的查询,解决方案是必须指定分区;
-
Hive的Join on条件只支持等值连接(如:t1.key=t2.key),where条件则无限制。hive从2.2.0版本开始,join on也支持复杂表达式,包括非等值连接;
-
窗口函数row_number()使用不当问题,在使用row_number()时,over()里面的分组以及排序晚于where、group by、order by执行。partition by用于给结果集分组,在hive中使用row_number()窗口函数时,如果使用了partition参数,需要特别注意partition by的key如果太多,超过了集群的限制,则会报错。如:select empno,deptno,salary,row_number(partition by deptno order by salary desc) rank from employee;
-
create table as select * from other_table 容易引起风险问题,如果源表某个字段修改了字段类型,如col1本是int型,修改为了string,那么通过这种方式创建的表该字段还是会被推断为int,这就存在了风险隐患,建议使用:drop table if exists table_name; create table t_new as select * from table_old where 1=2;或者:create table t_new like t_old;或者使用标准表创建再插入数据。