参考Hive/HiveSQL常用优化方法全面总结
最基本的操作。所谓列裁剪就是在查询时只读取需要的列,分区裁剪就是只读取需要的分区。以我们的日历记录表为例:
select uid,event_type,record_data from calendar_record_log
where pt_date >= 20190201 and pt_date <= 20190224 and status = 0;
count(distinct)是由一个reduce task来完成的,这一个reduce需要处理的数据量太大,就会导致整个job很难完成,可以使用先 group by 在 count代替
select count(id) from (select id from bigtable group by id) a;
但是这样写会启动两个MR job(单纯distinct只会启动一个),所以要确保数据量大到启动 job 的 overhead 远小于计算耗时,才考虑这种方法。当数据集很小或者key的倾斜比较明显时,group by还可能会比distinct慢。
group by方式同时统计多个列:
select t.a, sum(t.b), count(t.c), count(t.d) from (
select a, b, null, null from some_table
union all
select a, 0, c, null from some_table group by a,c
union all
select a, 0, null, d from some_table group by a,d
) t;
set hive.map.aggr=true
: 默认为true(hive 0.3+)set hive.groupby.mapaggr.checkinterval=100000
:默认值100000,在Map端进行聚合操作的行数阈值,超过该值就会分拆jobhive.groupby.skewindata
:默认值为false
当设定为true时,生成的查询计划会有两个MapReduce job。
在第一个job 中,map的输出结果集合会随机分布到 reduce 中, 每个 reduce 做部分聚合操作,相同的 Key 有可能分发到不同的 reduce 中,从而达到负载均衡的目的
在第二个 MapReduce 任务再根据第一步中处理的数据按照Key分布到reduce中,(这一步中相同的key在同一个reduce中),最终生成聚合操作结果。
Hive在解析带 join 的SQL语句时,会默认将最后一个表作为probe table(大表),将前面的表作为build table(小表)并试图将它们读进内存
1. 主表的分区限制条件可以写在where字句中(最好先用子查询过滤)
2. 主表的where子句建议写在sql最后
3. 从表分区限制条件不要写在where子句中,建议写在on条件或子查询中
select * from a join(select * from b where dt=20210301) b on a.id=b.id where a.dt=20210301;
-- 不建议使用下面sql,会先进行join,后分区裁剪,导致数据量变大
select * from a join b on b.id=a.id where b.dt=20210301;
select * from (select * from a where dt=20210301) a join (select * from b where dt=20210301) b on a.id=b.id;
不论是外关联outer join还是内关联inner join,如果join的key相同,不管有多少表,都会合并为一个MapReduce任务。负责这个的是相关性优化器CorrelationOptimizer,它的功能除此之外还非常多,逻辑复杂
select a.val,b.val,c.val from a JOIN b ON (a.key = b.key1) JOIN c ON (c.key2 = b.key1); -- 一个job
select a.val,b.val,c.val from a JOIN b ON (a.key = b.key1) JOIN c ON (c.key2 = b.key2); -- 两个job
MapJoin是Hive的一种优化操作,其适用于小表JOIN大表的场景,由于表的JOIN操作是在Map端且在内存进行的,所以其并不需要启动Reduce任务也就不需要经过shuffle阶段,从而能在一定程度上节省资源提高JOIN效率
先介绍一下hive里的两种join
1. Hive Common Join
如果不主动指定MapJoin或者不符合MapJoin的条件,Hive解析器默认的Join操作就是Common Join,即在Reduce阶段完成Join,过程如下:
1. Map阶段 :以关联键的组合为key,value为join之后关心的列,按照key排序,value中会包含表的tag信息,用于标明此value属于哪个表
2. Shuffle阶段:根据key的值进行hash,并将key/value按照hash值推送至不同的reduce中
3. reduce阶段:根据key的值完成join操作,期间通过tag来识别不同表中的数据
2. Hive Map Join
MapJoin通常用于一个很小的表和一个大表进行join的场景。过程如下:
1. 首先是一个Local task,我们暂称为taskA,它负责扫描小表b的数据,将其转化为一个hashtable的结构,并写入本地文件,之后将该文件加载到DistributeCache中。
2. 接下来是一个没有reduce的MR,我们暂称之为taskB,它启动MapTasks扫描大表a,在Map阶段根据a的每一条记录去和DistributeCache中b表对应的HashTable关联,并直接输出结果。
3. 由于MapJoin没有Reduce,所以Map直接输出结果文件,有多少map,就有多少结果文件
select /*+MAPJOIN(b)*/ * from a join b on a.value=b.value;
注意:
0.8之后,默认启动map join,由以下参数控制
小表自动选择Mapjoin: set hive.auto.convert.join=true;
默认值:true (hive > 0.11)。该参数为true时,Hive自动对左边的表统计量,若是小表就加入内存,即对小表使用Map join,对应逻辑优化器是MapJoinProcessor
set hive.mapjoin.smalltable.filesize=25000000;
默认值为2500000(25M), 通过配置该属性来确定使用该优化的表的大小,如果表的大小小于此值就会被加载进内存
hive.mapjoin.cache.numrows=25000
默认25000,缓存对少行数据到内存
set hive.mapjoin.followby.gby.localtask.max.memory.usage=0.55;
默认值:0.55。map join做group by操作时,可使用多大的内存来存储数据。若数据太大则不会保存在内存里
set hive.mapjoin.localtask.max.memory.usage=0.90;
默认值:0.90。本地任务可以使用内存的百分比
数据倾斜产生的根本原因是少数worker处理的数据量远远超过其他worker处理的数据量,因此少数Worker的运行时长远远超过其他Worker的平均运行时长,导致整个任务运行时间超长,造成任务延迟。
在实际场景中,如果发生数据倾斜,但无法获取导致数据倾斜的key信息,可以使用如下方法查看数据倾斜:
-- 执行如下语句产生数据倾斜
select * from a join b on a.key = b.key;
-- 执行如下sql,查看key的分布,判断执行Join操作是否会有数据倾斜
select left.key, left.cnt * right.cnt from (
select key, count(1) as cnt from a group by key) left join (
select key, count(1) as cnt from b group by key) right
on left.key=right.key;
假设两边的key中有大量null数据导致倾斜,则在join前先过滤掉null数据或者补上一个较小的随机数
select * from a join b on case when a.value is null then conacat('value',rand()) else a.value end = b.value
场景:用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。当按照user_id进行两个表的Join操作时,默认的Hash操作会按int型的id来进行分配,这样会导致所有string类型id的记录都分配到一个Reducer中。
解决方法:把数字类型转换成字符串类型
select * from users a left outer join logs b on a.usr_id = cast(b.user_id as string);
有时,小表会大到无法直接使用map join的地步,比如全量用户维度表,而使用普通join又有数据分布不均的问题。这时就要充分利用大表的限制条件,削减小表的数据量,再使用map join解决。代价就是需要进行两次join。举个例子:
select /*+mapjoin(b)*/ a.uid,a.event_type,b.status,b.extra_info
from calendar_record_log a
left outer join (
select /*+mapjoin(s)*/ t.uid,t.status,t.extra_info
from (select distinct uid from calendar_record_log where pt_date = 20190228) s
inner join user_info t on s.uid = t.uid
) b on a.uid = b.uid
where a.pt_date = 20190228;
mapred.map.tasks
(默认值2)来设定mapper的期望值,但不一定生效default_mapper_num = total_input_size / dfs.block.size
mapreduce.input.fileinputformat.split.minsize
和 mapreduce.input.fileinputformat.split.maxsize
分别指定split的最小和最大大小。
split_size = Max(minsize, MIN(maxsize, dfs.block.size))
split_num = totol_input_size /split_size
如果想减少mapper数,就适当调高minsize
,split数就减少了。如果想增大mapper数,除了降低minsize
之外,也可以调高mapred.map.tasks
主要是要遵循两个原则:1.使大数据量利用合适的map的数;2.使单个map任务处理合适的数据量
使用 mapred.reduce.task
可以直接设定reducer的数量
如果不设置,hive会根据以下参数自行推测:
参数 hive.exec.reducers.bytes.per.reducer
用来设定每个reducer能够处理的最大数据量,默认值1G(1.2版本之前)或256M(1.2版本之后)。
参数hive.exec.reducers.max
用来设定每个job的最大reducer数量,默认值999(1.2版本之前)或1009(1.2版本之后)。
得出reducer数:
reducer_num = MIN(total_input_size / reducers.bytes.per.reducer, reducers.max)
reducer数量与输出文件的数量相关。如果reducer数太多,会产生大量小文件,对HDFS造成压力。如果reducer数太少,每个reducer要处理很多数据,容易拖慢运行时间或者造成OOM。
更改hive的输入文件格式
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat
, 默认值是org.apache.hadoop.hive.ql.io.HiveInputFormat
,
set mapred.min.split.size.per.node=100000000
, 默认值:100M,单节点的最小split大小
set mapred.min.split.size.per.rack=100000000
, 默认值100M,单机架的最小split大小
如果split大小小于这两个值,则会进行合并
set hive.merge.mapfiles=true
: 表示将map-only任务的输出合并set hive.merge.mapredfiles=true
: 表示将mapr-reduce任务的输出合并set hive.merge.size.per.task=256*1000*1000
每个task输出后合并文件大小的期望值set hive.merge.size.smallfiles.avgsize=128000000
,可以指定所有输出文件大小的均值阈值,默认值都是1GB。如果平均大小不足的话,就会另外启动一个任务来进行合并。压缩job的中间结果数据和输出数据,可以用少量CPU时间节省很多空间。
set mapred.output.compress = true;
set mapred.output.compression.codec = org.apache.hadoop.io.compress.LzoCodec;
# 选择对块(BLOCK)还是记录(RECORD)压缩,BLOCK的压缩率比较高。
set mapred.output.compression.type = BLOCK;
set mapred.compress.map.output = true;
set mapred.map.output.compression.codec = org.apache.hadoop.io.compress.LzoCodec;
set hive.exec.compress.intermediate = true;
set hive.intermediate.compression.codec = org.apache.hadoop.io.compress.LzoCodec;
在MR job中,默认是每执行一个task就启动一个JVM。如果task非常小而碎,那么JVM启动和关闭的耗时就会很长。可以通过调节参数**mapred.job.reuse.jvm.num.tasks
**来重用。例如将这个参数设成5,那么就代表同一个MR job中顺序执行的5个task可以重复使用一个JVM,减少启动和关闭的开销。但它对不同MR job中的task无效。
set hive.exec.parallel=true
,默认为falsehive.exec.parallel.thread.number=8
, 可以设定并行执行的线程数,默认为8Hive也可以不将任务提交到集群进行运算,而是直接在一台节点上处理。因为消除了提交到集群的overhead,所以比较适合数据量很小,且逻辑不复杂的任务。
set hive.exec.mode.local.auto=true;
# 设置local mr的最大输入数据量,当输入数据量小于这个值的时候会采用local mr的方式, 默认128M
set hive.exec.mode.local.auto.inputbytes.max = 50000000;
# 设置local mr的最大输入文件个数,当输入文件个数小于这个值的时候会采用local mr的方式, 默认为4
set hive.exec.mode.local.auto.tasks.max=10;
当这三个参数同时成立,且reduce数为0或1时,才会采用本地mr
所谓严格模式,就是强制不允许用户执行3种有风险的HiveSQL语句,一旦执行会直接失败。这3种语句是:
set hive.mapred.mode=strict
开启严格模式,默认 nonstrict
hive提供了一个动态分区功能,其可以基于查询参数的位置去推断分区的名称,从而建立分区
set hive.exec.dynamic.partition = true; # 是否开启动态分区,默认为false
set hive.exec.dynamic.partition.mode = nonstrict; # (默认strict),表示允许所有分区都是动态的,否则必须有静态分区字段。
# 每个maper或reducer(每个MR节点上) 可以允许创建的最大动态分区个数,默认是100,超出则会报错
set hive.exec.max.dynamic.partitions.pernode=100 ;
# 表示一个动态分区语句(所有节点) 可以创建的最大动态分区个数,超出报错,默认1000
set hive.exec.max.dynamic.partitions =1000;
# 默认100000,全局可以创建的最大文件个数,超出报错。
set hive.exec.max.created.files =100000;
# 当有空分区生成时,是否抛出异常,默认值:false,一般不需要设置。
set hive.error.on.empty.partition=false
因为分区表的分区字段默认也是该表中的字段,且依次排在表中字段的最后面。所以分区字段为select的最后一个字段
ps: 本文仅代表个人观点,如有错误,请指正