Hive及Hive SQL优化

Hive及Hive SQL优化

参考Hive/HiveSQL常用优化方法全面总结

1. 列裁剪和分区裁剪

最基本的操作。所谓列裁剪就是在查询时只读取需要的列,分区裁剪就是只读取需要的分区。以我们的日历记录表为例:

select uid,event_type,record_data from calendar_record_log 
where pt_date >= 20190201 and pt_date <= 20190224 and status = 0;

2. 少用count(distinct)

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;

3. GROUP BY优化

3.1 开启map端预聚合

  • set hive.map.aggr=true: 默认为true(hive 0.3+)
  • set hive.groupby.mapaggr.checkinterval=100000:默认值100000,在Map端进行聚合操作的行数阈值,超过该值就会分拆job

3.2 倾斜均衡配置项

hive.groupby.skewindata:默认值为false

当设定为true时,生成的查询计划会有两个MapReduce job。
在第一个job 中,map的输出结果集合会随机分布到 reduce 中, 每个 reduce 做部分聚合操作,相同的 Key 有可能分发到不同的 reduce 中,从而达到负载均衡的目的
在第二个 MapReduce 任务再根据第一步中处理的数据按照Key分布到reduce中,(这一步中相同的key在同一个reduce中),最终生成聚合操作结果。

4. join优化

4.1 小表前置

Hive在解析带 join 的SQL语句时,会默认将最后一个表作为probe table(大表),将前面的表作为build table(小表)并试图将它们读进内存

4.2 where使用位置

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;

4.3 多表join时key相同,利用hive的优化机制减少job数

不论是外关联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

4.4 合理使用MapJoin

4.4.1 概述

MapJoin是Hive的一种优化操作,其适用于小表JOIN大表的场景,由于表的JOIN操作是在Map端且在内存进行的,所以其并不需要启动Reduce任务也就不需要经过shuffle阶段,从而能在一定程度上节省资源提高JOIN效率

4.4.2 原理

先介绍一下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,就有多少结果文件

4.4.3 使用

  1. hive显示使用map join(hive<0.7)
select /*+MAPJOIN(b)*/ * from a join b on a.value=b.value;

注意:

  • 在Hive0.7前,必须使用MAPJOIN来标记显示地启动该优化操作
  • map join还支持不等值连接
  1. map join相关参数

0.8之后,默认启动map join,由以下参数控制

  1. 小表自动选择Mapjoin: set hive.auto.convert.join=true;
    默认值:true (hive > 0.11)。该参数为true时,Hive自动对左边的表统计量,若是小表就加入内存,即对小表使用Map join,对应逻辑优化器是MapJoinProcessor

  2. set hive.mapjoin.smalltable.filesize=25000000;

    默认值为2500000(25M), 通过配置该属性来确定使用该优化的表的大小,如果表的大小小于此值就会被加载进内存

  3. hive.mapjoin.cache.numrows=25000

    默认25000,缓存对少行数据到内存

  4. set hive.mapjoin.followby.gby.localtask.max.memory.usage=0.55;

    默认值:0.55。map join做group by操作时,可使用多大的内存来存储数据。若数据太大则不会保存在内存里

  5. set hive.mapjoin.localtask.max.memory.usage=0.90;

    默认值:0.90。本地任务可以使用内存的百分比

5. 数据倾斜优化

5.1 根本原因

数据倾斜产生的根本原因是少数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;

5.2 解决方法

1. 小表join大表,建议使用map join;

2. 单独处理倾斜的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

3. group by倾斜优化: 参考GROUP BY优化

4. 不同数据类型注意类型转换:

场景:用户表中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);

5. 小表过大,无法直接使用map join

有时,小表会大到无法直接使用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;

6. MapReduce优化

6.1 调整mapper数

6.1.1 mapper数确定规则

  1. 可以通过mapred.map.tasks(默认值2)来设定mapper的期望值,但不一定生效
  2. 默认情况下,default_mapper_num = total_input_size / dfs.block.size
  3. mapreduce.input.fileinputformat.split.minsizemapreduce.input.fileinputformat.split.maxsize分别指定split的最小和最大大小。
    • split大小计算规则是:split_size = Max(minsize, MIN(maxsize, dfs.block.size))
    • split数计算规则是:split_num = totol_input_size /split_size
  4. 得出mapper数:`mapper_num = MIN(split_num, MAX(default_num, mapred.map.tasks))

如果想减少mapper数,就适当调高minsize,split数就减少了。如果想增大mapper数,除了降低minsize之外,也可以调高mapred.map.tasks

6.1.2 如何调整mapper数

主要是要遵循两个原则:1.使大数据量利用合适的map的数;2.使单个map任务处理合适的数据量

  • 是不是map越多越好:答案是否定的。如果一个任务有很多小文件(远远小于块大小128m),则每个小文件也会被当做一个块,用一个map任务来完成,而一个map任务启动和初始化的时间远远大于逻辑处理的时间,就会造成很大的资源浪费。而且,同时可执行的map数是受限的
  • 是不是保证每个map处理接近128m的文件块,就高枕无忧了?:答案也是不一定。比如有一个127m的文件,正常会用一个map去完成,但这个文件只有一个或者两个小字段,却有几千万的记录,如果map处理的逻辑比较复杂,用一个map任务去做,肯定也比较耗时。

6.2 调整reducer数

  1. 使用 mapred.reduce.task 可以直接设定reducer的数量

  2. 如果不设置,hive会根据以下参数自行推测:

    1. 参数 hive.exec.reducers.bytes.per.reducer 用来设定每个reducer能够处理的最大数据量,默认值1G(1.2版本之前)或256M(1.2版本之后)。

    2. 参数hive.exec.reducers.max用来设定每个job的最大reducer数量,默认值999(1.2版本之前)或1009(1.2版本之后)。

    3. 得出reducer数:

      reducer_num = MIN(total_input_size / reducers.bytes.per.reducer, reducers.max)

  3. reducer数量与输出文件的数量相关。如果reducer数太多,会产生大量小文件,对HDFS造成压力。如果reducer数太少,每个reducer要处理很多数据,容易拖慢运行时间或者造成OOM。

6.3 合并小文件

6.3.1 输入阶段合并

  1. 更改hive的输入文件格式
    set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat, 默认值是org.apache.hadoop.hive.ql.io.HiveInputFormat,

  2. set mapred.min.split.size.per.node=100000000, 默认值:100M,单节点的最小split大小

  3. set mapred.min.split.size.per.rack=100000000, 默认值100M,单机架的最小split大小

如果split大小小于这两个值,则会进行合并

6.3.2 输出阶段合并

  1. set hive.merge.mapfiles=true : 表示将map-only任务的输出合并
  2. set hive.merge.mapredfiles=true : 表示将mapr-reduce任务的输出合并
  3. set hive.merge.size.per.task=256*1000*1000 每个task输出后合并文件大小的期望值
  4. set hive.merge.size.smallfiles.avgsize=128000000,可以指定所有输出文件大小的均值阈值,默认值都是1GB。如果平均大小不足的话,就会另外启动一个任务来进行合并。

6.4 启用压缩

压缩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;   

6.5 JVM重用

在MR job中,默认是每执行一个task就启动一个JVM。如果task非常小而碎,那么JVM启动和关闭的耗时就会很长。可以通过调节参数**mapred.job.reuse.jvm.num.tasks**来重用。例如将这个参数设成5,那么就代表同一个MR job中顺序执行的5个task可以重复使用一个JVM,减少启动和关闭的开销。但它对不同MR job中的task无效。

7. 并行模式和本地模式

7.1 并行模式

  • set hive.exec.parallel=true ,默认为false
  • hive.exec.parallel.thread.number=8, 可以设定并行执行的线程数,默认为8

7.2 本地模式

Hive也可以不将任务提交到集群进行运算,而是直接在一台节点上处理。因为消除了提交到集群的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

8. 严格模式

所谓严格模式,就是强制不允许用户执行3种有风险的HiveSQL语句,一旦执行会直接失败。这3种语句是:

  • 对于分区表,除非where语句中含有分区字段过滤条件来限制范围,否则不允许执行。
  • 对于使用了order by语句的查询,要求必须使用limit语句。
  • 限制笛卡尔积的查询。

set hive.mapred.mode=strict 开启严格模式,默认 nonstrict

9.合理使用动态分区

hive提供了一个动态分区功能,其可以基于查询参数的位置去推断分区的名称,从而建立分区

9.1 配置参数

set hive.exec.dynamic.partition = true; # 是否开启动态分区,默认为false
set hive.exec.dynamic.partition.mode = nonstrict; # (默认strict),表示允许所有分区都是动态的,否则必须有静态分区字段。

9.2 调优参数

# 每个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: 本文仅代表个人观点,如有错误,请指正

你可能感兴趣的:(hive,sql)