HiveSQL常用优化方法经验总结

1. 写在前面的话

此处省略150字…

2. Hive中解决数据倾斜的场景

2.1 大表Join小表时的数据倾斜(map join)

  在大表Join小表时,解决数据倾斜最好的方式是使用Map Join,避免Shuffle,从而也避免了数据倾斜. map join主要通过下面的参数来调节:

#默认是true
set hive.auto.convert.join=true  --开启map join  //1.x版本及以后默认是开启的
#设置小表的大小,生产环境可以适当调大
set hive.mapjoin.smalltable.filesize=25000000 --默认小表小于25mb

  在map join中,还有如下2个参数经常使用:

set hive.auto.convert.join.noconditionaltask=true;
set hive.auto.convert.join.noconditionaltask.size=60000000;

说明:

  1. 第一个参数的作用是,在内连接中,除了第一个表之外的其它表是小表的情况,将普通的join转化为Map join时,是否将多个Map join合并为一个Map Join.设为true表示合并.
  2. 第二个参数是限定表的大小,多个Map join合并成一个Map join时,其小表的总大小必须小于该值才合并.
  3. 在小表left join大表时,不能将小表写入内存,否则得到错误的结果.inner join,小表在左边或右边都可以.full out join不能使用map join.

2.2 大表Join大表时的数据倾斜

  上面的Map Join解决了小表关联的问题,假如关联的是2个大表就不太适用了.小表不小,所以不太适合加载进内存,那么Map Join肯定是不适合的;两个表都不小,所以必然带来大量的网络IO和磁盘IO,所以Reduce Join也不适合.这个时候就可以使用SMB Join,并带来性能很大的提升.
  使用SMB Join的前提: 1.创建表时必须是创建分桶表;2.创建表时必须指定sort by排序;3.分桶的字段与sort by排序的字段且和两表关联的字段必须是一样的.

set hive.enforce.bucketing=true; --默认为false,开启后,则写入数据时会自动分桶
set hive.enforce.sorting=true; --默认为false,开启后,插数据到表中会进行强制排序
#如果希望SMB Join能够转换为SMB Map Join,还需要设置以下参数
set hive.auto.convert.sortmerge.join=true; 
set hive.optimize.bucketmapjoin = true; 
set hive.optimize.bucketmapjoin.sortedmerge =true

原理说明:
  在map join的时候,两个表是以相同的方式来划分桶的,则处理左边表的某个桶的时候,Mapper是知道表内对应的记录也在右边表的相同的桶内.因此Mapper只需要获取对应的那个桶,然后进行连接就可以.桶中的数据是根据相同的字段进行排序,这样每个桶的join就变成了merge sort,可以进一步提升map join效率.

2.3 group by引起的数据倾斜

2.3.1 key的基数少(即key的值种类不多),但数据量很大引起的数据倾斜

#默认为fasle,这样就会开始map端聚合combiner.可以减少shuffle的数据量.
set hive.map.aggr=true;

2.3.2 个别key的值数据量大,造成这类key的reduce执行太久引起的数据倾斜

#默认为fasle,应设置为true
set hive.groupby.skewindata=true;
#设置group by后面的键的记录数超过该值就会优化
set hive.groupby.mapaggr.checkinterval=100000;
#预先取100000条数据聚合,如果聚合后的条数/1000000 > 0.5,则不再预聚合
set hive.map.aggr.hash.min.reduction=0.5;

其原理是,在group by时启动2个MR job,第一个job会将map端数据随机分发到各个reducer,每个reducer做部分聚合,相同key的所有数据就会分布在不同的reducer中.第二个job再将前面预处理过程过的数据按key聚合并输出结果,这样就启到了均衡的效果.

2.3.3 count(distinct)引起的数据倾斜

  当我们需要对某个字段数据进行去重统计时,如果数据量很大,count(distinct)就会非常慢,为了保证全局去重,count(distinct)只会有一个reduce来处理.这时可以用group by来改写.但是这样会启动两个MR job(单纯的distinct只会启动一个).所以如果数据量不是太大的话,使用count(distinct)或许更快.因为这时还使用group by的话,需要启动2个MR,就会耗费更多的时间.

2.3.4 两表Join时引起的数据倾斜

2.3.4.1 关联字段空值过多的情况

实际场景:
  在日志中,常会有信息丢失的问题,比如日志中的user_id,如果取其中的user_id和用户表中的user_id关联,会碰到数据倾斜的问题.因为这些空值在join时会被分配到一个reduce,拖累进度.(空值字段是连接字段的情况)
解决办法:
  如果user_id为null的数据不需要,可以直接过滤掉,不参与关联.

select
user_id
from
(
	select user_id from log where log.user_id is not null
)t
join users on t.user_id=users.user_id

  如果user_id为null的需要保留可以使用下面2种方式解决数据倾斜:

#先单独取出user_id为null的,
select
user_id
from
(
	select user_id from log where log.user_id is not null
)t
join users on t.user_id=users.user_id
union all
select * from log where user_id is null
#在进行join时,将null值用随机值替代然后进行join
select *
from log left 
join users
on case when log.user_id is null then cancat("hive",rand()) else log.user_id = users.user_id

说明:
  1. 在给定随机值的时候注意产生的随机值不要与另一张表有关联上的可能,一定要保证产生的随机值是关联不上的,因为空值本来就是关联不上的,不要改变了关联了结果.变为随机值也不要影响最终结果.
  2. 把空值的key变成一个字符串加上随机数,就能把倾斜的数据分到不同的reduce上,解决数据倾斜问题.而这些分散到各个reduce上的数据由于是随机值,不会产生关联,不会影响最后关联的结果.

2.3.4.2 关联字段空值过多的情况

  用户表中user_id字段类型为int , log表中user_id的字段类型为string.当两表进行关联,user_id作为关联字段时,默认的hash操作会按照int类型的user_id来进行分配,这样会导致所有string类型的user_id记录都会分配到同一个Reducer中,造成严重的数据倾斜.
解决办法: 把数字类型转换成字符串类型.

on user.user_id=cast(log.user_id as string)

3. Hive中解决小文件的问题

3.1 小文件产生的原因

  1. 对于MR任务,reducer的任务数过多,会有大量的小文件输出.

3.2 小文件带来的问题

  1. 如果有大量的小文件,就会产生大量的元数据信息,那么就会占用namenode大量的内存空间,影响namenode的性能.
  2. 大量的小文件就会产生大量的map个数,调度时间较长,影响执行效率.

3.3 解决小文件问题

3.3.1 通过JVM重用解决小文件产生的大量map数

  在MR job中,默认是每执行一个task就启动一个JVM.如果task非常小而碎,那么JVM启动和关闭的耗时就会很长.可以通过调节参数mapred.job.reuse.jvm.num.tasks的值来达到重用的目的(默认值为1).例如将这个参数设成5,那么就代表同一个MR job中顺序执行的5个task可以重复使用一个JVM,减少启动和关闭的开销.但它对不同MR job中的task无效.
  如果大量的小文件启动大量的map,可以通过开启JVM重用来解决.特别适合很难避免小文件或task特别多的场景,这类场景大多执行时间都很短.hadoop默认配置是使用派生JVM来执行map和reduce任务的,这时jvm的启动过程可能会造成相当大的开销,尤其是执行的job包含大量的task任务的情况时.
JVM重用可以使得JVM实例在同一个job中重新使用N次,N的值在Hive中可以通过set mapred.job.reuse.jvm.num.tasks参数来设置.
  JVM重用的缺点是一直占着资源,以便进行重用.所以如果有几个task迟迟执行不成功,那么被占着的资源迟迟得不到释放,那么被占着的插槽一直空闲确无法被其它job使用,直到所有task都结束才会释放.

3.3.1 在Mapper输入阶段对小文件进行合并

set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat

设置上面的参数之后就会自动合并小文件,具体合并的大小由下面的参数决定:

#最大的split大小.一个split就对应1个map. 默认为256MB,可以调大
mapred.max.split.size=256000000; --决定每个map处理的最大的文件大小,单位B
#单个节点最小的split的大小.默认为1Byte,需要调大.那么小于这个数的文件就会合并
mapred.min.split.size.per.node=1; --一个节点上split的至少的大小
#单个机架最小的split的大小
mapred.min.split.size.per.rack=1;  --一个机架中split的至少的大小

3.3.2 在MR的输出阶段进行合并

#在只有map阶段时进行合并
set hive.merge.mapfiles=true
#MR引擎适用,在reduce阶段合并
set hive.merge.mapredfiles=true
#tez引擎适用
set hive.merge.tezfiles=true
#合并后期望的文件大小
set hive.merge.size.per.task=256000000(默认256MB) 
#当输出文件大小的平均值大小小于该值时,启动一个独立的mapReduce任务进行文件merge
set hive.merge.smallfiles.avgsize=16000000(默认16MB)

4. 适当开启压缩

  当数据比较大的时候,可以考虑压缩数据.Snappy压缩率低,但压缩,解压速度最快.
  压缩Job的中间结果数据和输出数据.具体的压缩方式根据场景来定,如果需要考虑分片,可以使用lzo压缩,为压缩文件建立索引之后,就可以进行分片了.如果不考虑分片,可以使用snappy压缩,因为它的压缩速度是最快的.
  开启map和reduce中间过程的压缩,减少map和ruduce task之间的数据传输量.

set hive.exec.compress.intermediate=true; --默认为false
--设置解码格式
set mapred.map.output.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;

  Hive的最终输出结果压缩:

set Hive.exec.compress.output=true; --默认为false,开启最终压缩
set mapred.output.compression.type=BLOCK; --默认为record
--设置压缩方式,这里为snappy
set mapred.output.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
set mapreduce.output.fileoutputformat.compress=true;

说明:
  通常,为了减轻存储和数据传输,提升效率,我们都会选择压缩,但压缩的不同格式有不同的适合场景,这个每个公司都有差异,需要根据实际情况来选择。

5. 开启并行执行

  开启并行执行,在同一个应用中,Hive中互相没有依赖关系的job间是可以并行执行的,最典型的就是多个子查询union all,在集群资源相对充足的情况下,可以开启并行执行,即将参数hive.exec.parallel设为true(默认为false).另外hive.exec.parallel.thread.number可以设定并行执行的线程数,默认为8,一般都够用(即指定并行的最大job个数,默认为8个)。
  在有union all的场景下,特别适用,多个任务之间没有依赖,独立运行.在资源不是核心瓶颈的前提下,可以直接缩短运行时间.如果集群只有数台机器,资源有限,开并行会导致资源紧张,这种方式就不一定能提到调休的效果了.

6. 开启本地执行

  Hive也可以不将任务提交到集群进行运算,而是直接在一台节点上处理。因为消除了提交到集群的消耗.所以比较适合数据量很小,且逻辑不复杂的任务。要启用本地模式,需要设置以下参数:

set hive.exec.mode.local.auto=true; --默认为false,设为ture开启本地执行
set hive.exec.mode.local.auto.inputbytes.max=134217728; --任务输入的总数据量必须小于设定的值,默认128MB
set hive.exec.mode.local.auto.tasks.max=4; --且Mapper的数量必须小于设定的值.默认为4
reduce的数量必须0或1个

7. 开启严格模式

  所谓严格模式,就是强制不允许用户执行3种有风险的HiveSQL语句,一旦执行就会直接失败.设置hive.mapred.mode的值为strict可以开启严格模式.
严格模式开启后,会禁止以下3类查询:

  1. 查询分区表时不限定分区列的语句在严格模式下执行失败
  2. 两表join时产生了笛卡尔积的语句在严格模式下执行失败(没有连接条件或连接条件失效都会产生笛卡尔积)
  3. 用order by来排序但没有指定limit的语句在严格模式下也会执行失败.

8. 选择合适的存储格式

  因为在Hive中很多场景下,我们并不需要查询每行数据中的所有字段,更多的时候是查询需要的某几个字段,这时可以考虑使用列式存储格式,比如parquet和orc格式.如果仅仅是使用HiveSQL查询,可以选用ORC格式,性能是最好的.如果考虑到多分析引擎的使用,比如Spark,Impala等,可以考虑Parquet格式.

9. 调整Map的数量

Map数量是否是越多越好?

  如果一个任务有很多小文件(<<128M),每个小文件也会被当做一个数据块,用一个mapTask来完成.每个mapTask启动和初始化时间>>处理时间,会造成资源浪费,而且系统中同时可用的map数量是有限的.所以对于小文件采用的策略是合并.
每个Map处理接近128M的文件,就是最合适的么?
  有一个125M的文件,一般情况下用一个MapTask来完成.假设这个文件字段很少,但记录数却非常多,如果map函数处理的逻辑比较复杂,用一个mapTask去做的话,性能也不好.对于这种复杂文件采用的策略是增加Map数量.
可以通过下面的方式调节map的数量:

computeSplitSize(max(minSize,min(maxSize,blocksize)))
minSize : mapred.min.split.size(默认值为1)
maxSize : mapred.max.split.size(默认值256M)
如果都是默认参数那么计算出来就是按128MB作为一个分片.
也可调整maxSize的最大值可以改变map的数量.
1.如果让maxSize最大值小于blocksize就可以增加map的个数.假如maxSize的
值调为64,那么分片大小计算出来就为64

10. 调整reducer的数量

  reducer数量的确定方法比mappper简单的多,可以直接使用mapred.reduce.tasks参数来设置.如果未设置该参数,Hive会自动推测Reducer的数量,推测逻辑如下:

  1. 参数hive.exec.reducers.bytes.per.reducer用来设定每个reducer能够处理的最大数据量,默认值256MB.
  2. 参数hive.exec.reducers.max用来设定每个job最多能启动的reducer数量,默认值999(1.2版本之前)或1009(1.2版本之后)
  3. 计算出reducer数量=min(reduce的输入数据总量/256M,1009)
    reducer的数量与输出文件的数量有关,如果reducer数太多,会产生大量小文件,对HDFS造成压力.如果redcuer数量太少,每个reducer要处理很多数据,容易拖慢运行时间或者造成OOM.
    说明:
    也有些情况是固定只有一个reduce的(不管有没指定reduce数量):
    a.没有group by的汇总
    b.使用order by全局排序
    c.笛卡尔积
    d.count(distinct)
    但这几种情况一般是我们需要避免的,因为会造成性能瓶颈.

11.其它方面的优化

11.1 列裁剪和分区裁剪

  所谓列裁剪就是在查询时只读取需要的列.分区裁剪就是只读取需要的分区.创建分区表,避免全表扫描.

11.2 谓词下推

  简单理解,在进行join操作时,先使用where条件过滤出需要的行数据,再进行join,这样就能减少下游处理的数据量.而不是先全表join,再进行过滤.

12. 减少job数优化举例

  启动一次job尽可能的多做事情,一个job能完成的事情,不要两个job来做.可以尽量减少job的数量,即减少MR的数量,让一个job完成更多的功能.提升执行速度.

12.1 尽量使用union all替换union

  一般在实际场景中不会使用union,因为union除了将几张表拼接在一起,还会去重,会增加job的数量.union all会单纯的将几张表拼接在一起,不会产生额外的job,如果关联在一起后真的要去重,也不使用union,而是用group by来去重.

12.2 有多张表要分别求出每张表的记录数优化的场景

#1.最简单的方式.有4个job.但是效率比较低.
select count(1) from table1;
select count(1) from table2;
select count(1) from table3;
select count(1) from table4;
#2.改进后的方式.只有一个job.只是一个job中有多个map和reduce任务
select
type,count(1)
from
(  
    select 'table1' as type,name from table1
    union all
    select 'table2' as type,name from table2
    union all
    select 'table3' as type,name from table3
    union all
    select 'table4' as type,name from table4 
)tmp
group by type;

你可能感兴趣的:(随笔,hive,大数据)