1 表层面
1.1 利用分区表优化
分区表 是在某一个或者几个维度上对数据进行分类存储,一个分区对应一个目录。如果筛选条件里有分区字段,那么 Hive 只需要遍历对应分区目录下的文件即可,不需要遍历全局数据,使得处理的数据量大大减少,从而提高查询效率。
也就是说:当一个 Hive 表的查询大多数情况下,会根据某一个字段进行筛选时,那么非常适合创建为分区表,该字段即为分区字段。
CREATE TABLE page_view
(viewTime INT,
userid BIGINT,
page_url STRING,
referrer_url STRING,
ip STRING COMMENT 'IP Address of the User')
PARTITIONED BY(date STRING, country STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '1'
STORED AS TEXTFILE;
1 当你意识到一个字段经常用来做where,建分区表,使用这个字段当做分区字段。如按天过滤、按天分区
2 在查询的时候,使用分区字段来过滤,就可以避免全表扫描。只需要扫描这张表的一个分区的数据即可。分区表设计模式的本质是为了过滤数据避免全表扫描
1.2 利用分桶表优化
跟分区的概念很相似,都是把数据分成多个不同的类别
1、分区:按照字段值来进行,一个分区,就只是包含这个值的所有记录
不是当前分区的数据一定不在当前分区
当前分区也只会包含当前这个分区值的数据
2、分桶:默认规则,Hash的方式
一个桶中会有多个不同的值
如果一个分桶中,包含了某个值,这个值的所有记录,必然都在这个分桶里面
Hive Bucket,分桶,是指将数据以指定列的值为key进行hash,hash到指定数目的桶里面,这样做的目的和分区表类似,是的筛选时不用全局遍历所有的数据,只需要遍历所在的桶就好了,这样也只可以支持高效采样。
其实最主要的作用就是 采样、join
如下例就是以 userid 这一列为 bucket 的依据,共设置 32 个 buckets
CREATE TABLE page_view(viewTime INT, userid BIGINT,
page_url STRING, referrer_url STRING,
ip STRING COMMENT 'IP Address of the User')
COMMENT 'This is the page view table'
PARTITIONED BY(dt STRING, country STRING)
CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS
CLUSTERED BY(userid): 按照userid进行分桶
SORTED BY(viewTime): 按照viewTime进行桶内排序
INTO 32 BUCKETS: 分成多少个桶
两个表以相同方式(相同字段)划分桶,两个表的桶个数一定是倍数关系,这样在join的时候速度会大大增加
采样用的不多,也就不过多阐述了
1.3 选择合适的文件存储格式
在 HiveSQL 的 create table 语句中,可以使用 stored as … 指定表的存储格式。Apache Hive支持 Apache Hadoop 中使用的几种熟悉的文件格式,比如 TextFile、SequenceFile、RCFile、Avro、ORC、ParquetFile等。存储格式一般需要根据业务进行选择,在我们的实操中,绝大多数表都采用TextFile与Parquet两种存储格式之一。TextFile是最简单的存储格式,它是纯文本记录,也是Hive的默认格式。虽然它的磁盘开销比较大,查询效率也低,但它更多地是作为跳板来使用。RCFile、ORC、Parquet等格式的表都不能由文件直接导入数据,必须由TextFile来做中转。Parquet和ORC都是Apache旗下的开源列式存储格式。列式存储比起传统的行式存储更适合批量OLAP查询,并且也支持更好的压缩和编码。创建表时,特别是宽表,尽量使用 ORC、ParquetFile 这些列式存储格式,因为列式存储的表,每一列的数据在物理上是存储在一起的,Hive查询时会只遍历需要列数据,大大减少处理的数据量。
TextFile
1、存储方式:行存储。默认格式,如果建表时不指定默认为此格式。,
2、每一行都是一条记录,每行都以换行符"\n"结尾。数据不做压缩时,磁盘会开销比较大,数据解析开销也
比较大。
3、可结合Gzip、Bzip2等压缩方式一起使用(系统会自动检查,查询时会自动解压),推荐选用可切分的压
缩算法。
Sequence File
1、一种Hadoop API提供的二进制文件,使用方便、可分割、个压缩的特点。
2、支持三种压缩选择:NONE、RECORD、BLOCK。RECORD压缩率低,一般建议使用BLOCK压缩
RC File
1、存储方式:数据按行分块,每块按照列存储 。
A、首先,将数据按行分块,保证同一个record在一个块上,避免读一个记录需要读取多个block。
B、其次,块数据列式存储,有利于数据压缩和快速的列存取。
2、相对来说,RCFile对于提升任务执行性能提升不大,但是能节省一些存储空间。可以使用升级版的ORC格
式。
ORC File
1、存储方式:数据按行分块,每块按照列存储
2、Hive提供的新格式,属于RCFile的升级版,性能有大幅度提升,而且数据可以压缩存储,压缩快,快速列存取。
3、ORC File会基于列创建索引,当查询的时候会很快。
Parquet File
1、存储方式:列式存储。
2、Parquet对于大型查询的类型是高效的。对于扫描特定表格中的特定列查询,Parquet特别有用。Parquet一般使用Snappy、Gzip压缩。默认Snappy。
3、Parquet支持Impala 查询引擎。
4、表的文件存储格式尽量采用Parquet或ORC,不仅降低存储量,还优化了查询,压缩,表关联等性能
1.4 选择合适的压缩格式
Hive 语句最终是转化为 MapReduce 程序来执行的,而 MapReduce 的性能瓶颈在与 网络IO 和 磁盘IO,要解决性能瓶颈,最主要的是 减少数据量,对数据进行压缩是个好方式。压缩虽然是减少了数据量,但是压缩过程要消耗 CPU,但是在 Hadoop 中,往往性能瓶颈不在于 CPU,CPU 压力并不大,所以压缩充分利用了比较空闲的 CPU。
常用压缩方法对比
压缩格式 是否可拆分 是否自带 压缩率 速度 是否hadoop自带
gzip 否 是 很高 比较快 是
lzo 是 是 比较高 很快 否
snappy 否 是 比较高 很快 否
bzip2 是 否 最高 慢 是
压缩率对比
如何选择压缩方式呢?
1、压缩比例
2、解压缩速度
3、是否支持split
支持切割的文件可以并行的有多个mapper程序处理大数据文件,一般我们选择的都是支持切分的!
压缩带来的缺点和优点
1、计算密集型,不压缩,否则会进一步增加cpu的负担,真实的场景中hive对cpu的压力很小
2、网络密集型,推荐压缩,减小网络数据传输
# Job 输出文件按照 Block
## 默认值是false
set mapreduce.output.fileoutputformat.compress=true;
## 默认值是Record
set mapreduce.output.fileoutputformat.compress.type=BLOCK;
set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.lzo.LzoCodec;
# Map 输出结结果进行压缩
set mapred.map.output.compress=true;
set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.lzo.LzoCodec;
# 对 Hive 输出结果和中间都进行压缩
set hive.exec.compress.output=true ## 默认值是false,不压缩
set hive.exec.compress.intermediate=true ## 默认值是false,为true时MR设置的压缩才启用
2 HQL层面优化
2.1 执行计划
explain select * from movies;
2.1 列、行、分区裁剪
列裁剪就是在查询时只读取需要的列
行裁剪就是在查询时只读取需要的行,也就是提前过滤
分区剪裁就是在查询的时候只读取需要的分区。
set hive.optimize.cp = true; 列裁剪,取数只取查询中需要用到的列,默认是true
set hive.optimize.pruner=true; ## 分区剪裁
2.2 谓词下推
将 SQL 语句中的 where 谓词逻辑都尽可能提前执行,减少下游处理的数据量。对应逻辑优化器是PredicatePushDown。
set hive.optimize.ppd=true; ## 默认是true
eg:
select a.*, b.* from a join b on a.id = b.id where b.age > 20;
转换为下面的这样的
select a.*, c.* from a join (select * from b where age > 20) c on a.id = c.id;
2.3 合并小文件
如果一个mapreduce job碰到一对小文件作为输入,一个小文件启动一个Task,这样的话会出现maptask爆炸的问题。
Map端输入合并
在执行 MapReduce 程序的时候,一般情况是一个文件的一个数据分块需要一个 mapTask 来处理。但是如果数据源是大量的小文件,这样就会启动大量的 mapTask 任务,这样会浪费大量资源。可以将输入的小文件进行合并,从而减少 mapTask 任务数量。
## Map端输入、合并文件之后按照block的大小分割(默认)
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
## Map端输入,不合并
set hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;
Map/Reduce输出合并
大量的小文件会给 HDFS 带来压力,影响处理效率。可以通过合并 Map 和 Reduce 的结果文件来消除影响
## 是否合并Map输出文件, 默认值为true
set hive.merge.mapfiles=true;
## 是否合并Reduce端输出文件,默认值为false
set hive.merge.mapredfiles=true;
## 合并文件的大小,默认值为256000000 256M
set hive.merge.size.per.task=256000000;
## 每个Map 最大分割大小
set mapred.max.split.size=256000000;
## 一个节点上split的最少值
set mapred.min.split.size.per.node=1; // 服务器节点
## 一个机架上split的最少值
set mapred.min.split.size.per.rack=1; // 服务器机架
hive.merge.size.per.task 和 mapred.min.split.size.per.node 联合起来:
1、默认情况先把这个节点上的所有数据进行合并,如果合并的那个文件的大小超过了256M就开启另外一个文件继续合并
2、如果当前这个节点上的数据不足256M,那么就都合并成一个逻辑切片。
2.4 合理设置MapTask并行度
Map数过大 :当输入文件特别大,MapTask 特别多,每个计算节点分配执行的 MapTask 都很多,这时候可以考虑减少 MapTask 的数量。增大每个 MapTask 处理的数据量。而且 MapTask 过多,最终生成的结果文件数也太多。
1、Map阶段输出文件太小,产生大量小文件
2、初始化和创建Map的开销很大
Map数太小 :当输入文件都很大,任务逻辑复杂,MapTask 执行非常慢的时候,可以考虑增加MapTask 数,来使得每个 MapTask 处理的数据量减少,从而提高任务的执行效率。
1、文件处理或查询并发度小,Job执行时间过长
2、大量作业时,容易堵塞集群
一个MapReduce Job 的 MapTask 数量是由输入分片InputSplit 决定的。而输入分片是由 FileInputFormat.getSplit() 决定的。一个输入分片对应一个MapTask,而输入分片是由三个参数决定的:
参数 默认值 意义
dfs.blocksize 128M HDFS默认数据块大小
mapreduce.input.fileinputformat.split.minsize 1 最小分片大小(MR)
mapreduce.input.fileinputformat.split.maxsize 256M 最大分片大小(MR)
输入分片大小的计算是这么计算出来的:
long splitSize = Math.max(minSize, Math.min(maxSize, blockSize))
默认情况下,输入分片大小和 HDFS 集群默认数据块大小一致,也就是默认一个数据块,启用一个MapTask 进行处理,这样做的好处是避免了服务器节点之间的数据传输,提高 job 处理效率
两种经典的控制MapTask的个数方案:减少MapTask数 或者 增加MapTask数
1、减少 MapTask 数是通过合并小文件来实现,这一点主要是针对数据源
2、增加 MapTask 数可以通过控制上一个 job 的 reduceTask 个数
重点注意:不推荐把这个值进行随意设置!
推荐的方式:使用默认的切块大小即可。如果非要调整,最好是切块的N倍数
最好的方式就是 NodeManager节点个数:N ===》 Task = ( N * 0.95) * MapTask
合理控制 MapTask 数量
1、减少 MapTask 数可以通过合并小文件来实现
2、增加 MapTask 数可以通过控制上一个 ReduceTask 默认的 MapTask 个数
输入文件总大小:total_size
HDFS 设置的数据块大小:dfs_block_size
default_mapper_num = total_size / dfs_block_size
MapReduce 中提供了如下参数来控制 map 任务个数,从字面上看,貌似是可以直接设置 MapTask 个数的样子,但是很遗憾不行,这个参数设置只有在大于 default_mapper_num 的时候,才会生效
set mapred.map.tasks=10; ## 默认值是2
那如果我们需要减少 MapTask 数量,但是文件大小是固定的,那该怎么办呢?可以通过 mapred.min.split.size 设置每个任务处理的文件的大小,这个大小只有在大于dfs_block_size 的时候才会生效
split_size = max(mapred.min.split.size, dfs_block_size)
split_num = total_size / split_size
compute_map_num = Math.min(split_num, Math.max(default_mapper_num,
mapred.map.tasks))
这样就可以减少 MapTask 数量了
让我们来总结一下控制mapper个数的方法:
1、如果想增加 MapTask 个数,可以设置 mapred.map.tasks 为一个较大的值
2、如果想减少 MapTask 个数,可以设置 maperd.min.split.size 为一个较大的值
3、如果输入是大量小文件,想减少 mapper 个数,可以通过设置 hive.input.format 合并小文
如果想要调整 mapper 个数,在调整之前,需要确定处理的文件大概大小以及文件的存在形式(是大量小文件,还是单个大文件),然后再设置合适的参数。不能盲目进行暴力设置,不然适得其反。
MapTask 数量与输入文件的 split 数息息相关,在 Hadoop 源码org.apache.hadoop.mapreduce.lib.input.FileInputFormat 类中可以看到 split 划分的具体逻辑。可以直接通过参数 mapred.map.tasks(默认值2)来设定 MapTask 数的期望值,但它不一定会生效。
2.5 合理设置ReduceTask并行度
如果 ReduceTask 数量过多,一个 ReduceTask 会产生一个结果文件,这样就会生成很多小文件,那么如果这些结果文件会作为下一个 Job 的输入,则会出现小文件需要进行合并的问题,而且启动和初始化ReduceTask 需要耗费资源。
如果 ReduceTask 数量过少,这样一个 ReduceTask 就需要处理大量的数据,并且还有可能会出现数据倾斜的问题,使得整个查询耗时长。默认情况下,Hive 分配的 reducer 个数由下列参数决定:
Hadoop MapReduce 程序中,ReducerTask 个数的设定极大影响执行效率,ReducerTask 数量与输出文件的数量相关。如果 ReducerTask 数太多,会产生大量小文件,对HDFS造成压力。如果ReducerTask 数太少,每个ReducerTask 要处理很多数据,容易拖慢运行时间或者造成 OOM。这使得Hive 怎样决定 ReducerTask 个数成为一个关键问题。遗憾的是 Hive 的估计机制很弱,不指定ReducerTask 个数的情况下,Hive 会猜测确定一个ReducerTask 个数,基于以下两个设定:
参数1:hive.exec.reducers.bytes.per.reducer (默认256M)
参数2:hive.exec.reducers.max (默认为1009)
参数3:mapreduce.job.reduces (默认值为-1,表示没有设置,那么就按照以上两个参数进行设置)
ReduceTask 的计算公式为:
N = Math.min(参数2,总输入数据大小 / 参数1)
可以通过改变上述两个参数的值来控制 ReduceTask 的数量。也可以通过
set mapred.map.tasks=10;
set mapreduce.job.reduces=10;
通常情况下,有必要手动指定 ReduceTask 个数。考虑到 Mapper 阶段的输出数据量通常会比输入有大幅减少,因此即使不设定 ReduceTask 个数,重设 参数2 还是必要的。依据经验,可以将 参数2 设定为 M * (0.95 * N) (N为集群中 NodeManager 个数)。一般来说,NodeManage 和 DataNode 的个数是一样的
2.6 Join优化
1. Join的整体优化原则:
1、优先过滤后再进行Join操作,最大限度的减少参与join的数据量
2、小表join大表,最好启动mapjoin,hive自动启用mapjoin, 小表不能超过25M,可以更改
3、Join on的条件相同的话,最好放入同一个job,并且join表的排列顺序从小到大:
select a.*,b.*, c.* from a join b on a.id = b.id join c on a.id = c.i
4、如果多张表做join, 如果多个链接条件都相同,会转换成一个JOb
2. 优先过滤数据:
尽量减少每个阶段的数据量,对于分区表能用上分区字段的尽量使用,同时只选择后面需要使用到的列,最大限度的减少参与 Join 的数据量
3. 小表join大表的原则:
小表 join 大表的时应遵守小表 join 大表原则,原因是 join 操作的 reduce 阶段,位于 join 左边的表内容会被加载进内存,将条目少的表放在左边,可以有效减少发生内存溢出的几率。join 中执行顺序是从左到右生成 Job,应该保证连续查询中的表的大小从左到右是依次增加的。
4. 使用相同的连接键:
在 hive 中,当对 3 个或更多张表进行 join 时,如果 on 条件使用相同字段,那么它们会合并为一个MapReduce Job,利用这种特性,可以将相同的 join on 放入一个 job 来节省执行时间。
5. 尽量原子操作:
尽量避免一个SQL包含复杂的逻辑,可以使用中间表来完成复杂的逻辑。
6. 大表join大表:
1、空key过滤:有时join超时是因为某些key对应的数据太多,而相同key对应的数据都会发送到相同的reducer上,从而导致内存不够。此时我们应该仔细分析这些异常的key,很多情况下,这些key对应的数据是异常数据,我们需要在SQL语句中进行过滤。
2、空key转换:有时虽然某个key为空对应的数据很多,但是相应的数据不是异常数据,必须要包含在join的结果中,此时我们可以表a中key为空的字段赋一个随机的值,使得数据随机均匀地分不到不同的reducer上
7. 启用MapJoin:
这个优化措施,只要能用的时候一定要用,根据数据量大小来调整小表的大小,一般公司里面可以设置到512 到1G
MapJoin 是将 join 双方比较小的表直接分发到各个 map 进程的内存中,在 map 进程中进行 join 操作,这样就不用进行 reduce 步骤,从而提高了速度。只有 join 操作才能启用 MapJoin。
## 是否根据输入小表的大小,自动将reduce端的common join 转化为map join,将小表刷入内存中。
## 对应逻辑优化器是MapJoinProcessor
set hive.auto.convert.join = true;
## 刷入内存表的大小(字节)
set hive.mapjoin.smalltable.filesize = 25000000;
## hive会基于表的size自动的将普通join转换成mapjoin
set hive.auto.convert.join.noconditionaltask=true;
## 多大的表可以自动触发放到内层LocalTask中,默认大小10M
set hive.auto.convert.join.noconditionaltask.size=10000000;
Hive 可以进行多表 Join。Join 操作尤其是 Join 大表的时候代价是非常大的。MapJoin 特别适合大小表join的情况。在Hive join场景中,一般总有一张相对小的表和一张相对大的表,小表叫 build table,大表叫 probe table。Hive 在解析带 join 的 SQL 语句时,会默认将最后一个表作为 probe table,将前面的表作为 build table 并试图将它们读进内存。如果表顺序写反,probe table 在前面,引发 OOM 的风险就高了。在维度建模数据仓库中,事实表就是 probe table,维度表就是 build table。这种 Join 方式在 map 端直接完成 join 过程,消灭了 reduce,效率很高。而且 MapJoin 还支持非等值连接。当 Hive 执行 Join 时,需要选择哪个表被流式传输(stream),哪个表被缓存(cache)。Hive 将JOIN 语句中的最后一个表用于流式传输,因此我们需要确保这个流表在两者之间是最大的。如果要在
不同的 key 上 join 更多的表,那么对于每个 join 集,只需在 ON 条件右侧指定较大的表
也可以手动开启mapjoin:
--SQL方式,在SQL语句中添加MapJoin标记(mapjoin hint)
--将小表放到内存中,省去shffle操作
// 在没有开启mapjoin的情况下,执行的是reduceJoin
SELECT /*+ MAPJOIN(smallTable) */ smallTable.key, bigTable.value FROM
smallTable JOIN bigTable ON smallTable.key = bigTable.key;
在高版本中,已经进行了优化,会自动进行优化
8. Sort-Merge-Bucket(SMB) Map Join:
它是另一种Hive Join优化技术,使用这个技术的前提是所有的表都必须是分桶表(bucket)和分桶排序的(sort)。分桶表的优化!
具体实现:
1、针对参与join的这两张做相同的hash散列,每个桶里面的数据还要排序
2、这两张表的分桶个数要成倍数。
3、开启 SMB join 的开关!
一些常见的参数设置:
## 当用户执行bucket map join的时候,发现不能执行时,禁止查询
set hive.enforce.sortmergebucketmapjoin=false;
## 如果join的表通过sort merge join的条件,join是否会自动转换为sort merge join
set hive.auto.convert.sortmerge.join=true;
## 当两个分桶表 join 时,如果 join on的是分桶字段,小表的分桶数是大表的倍数时,可以启用
mapjoin 来提高效率。
# bucket map join优化,默认值是 false
set hive.optimize.bucketmapjoin=false;
## bucket map join 优化,默认值是 false
set hive.optimize.bucketmapjoin.sortedmerge=false;
9. Join数据倾斜优化:
在编写 Join 查询语句时,如果确定是由于 join 出现的数据倾斜,那么请做如下设置:
# join的键对应的记录条数超过这个值则会进行分拆,值根据具体数据量设置
set hive.skewjoin.key=100000;
# 如果是join过程出现倾斜应该设置为true
set hive.optimize.skewjoin=false;
如果开启了,在 Join 过程中 Hive 会将计数超过阈值 hive.skewjoin.key(默认100000)的倾斜 key 对应的行临时写进文件中,然后再启动另一个 job 做 map join 生成结果。通过 hive.skewjoin.mapjoin.map.tasks 参数还可以控制第二个 job 的 mapper 数量,默认10000。
例如set hive.skewjoin.mapjoin.map.tasks=10000;
2.7 CBO优化
join的时候表的顺序的关系:前面的表都会被加载到内存中。后面的表进行磁盘扫描
select a.*, b.*, c.* from a join b on a.id = b.id join c on a.id = c.id;
Hive 自 0.14.0 开始,加入了一项 “Cost based Optimizer” 来对 HQL 执行计划进行优化,这个功能通过 “hive.cbo.enable” 来开启。在 Hive 1.1.0 之后,这个 feature 是默认开启的,它可以 自动优化 HQL中多个 Join 的顺序,并选择合适的 Join 算法。
CBO,成本优化器,代价最小的执行计划就是最好的执行计划。传统的数据库,成本优化器做出最优化的执行计划是依据统计信息来计算的。Hive 的成本优化器也一样。Hive 在提供最终执行前,优化每个查询的执行逻辑和物理执行计划。这些优化工作是交给底层来完成的。根据查询成本执行进一步的优化,从而产生潜在的不同决策:如何排序连接,执行哪种类型的连接,并行度等等。要使用基于成本的优化(也称为CBO),请在查询开始设置以下参数:
set hive.cbo.enable=true;
set hive.compute.query.using.stats=true;
set hive.stats.fetch.column.stats=true;
set hive.stats.fetch.partition.stats=true;
2.8 Group By优化
默认情况下,Map 阶段同一个 Key 的数据会分发到一个 Reduce 上,当一个 Key 的数据过大时会产生数据倾斜。进行 group by 操作时可以从以下两个方面进行优化:
1、Map端部分预聚合:
事实上并不是所有的聚合操作都需要在 Reduce 部分进行,很多聚合操作都可以先在 Map 端进行部分聚合,然后在 Reduce 端的得出最终结果。
## 开启Map端聚合参数设置
set hive.map.aggr=true;
# 设置map端预聚合的行数阈值,超过该值就会分拆job,默认值100000
set hive.groupby.mapaggr.checkinterval=100000
2、有数据倾斜时进行负载均衡
当 HQL 语句使用 group by 时数据出现倾斜时,如果该变量设置为 true,那么 Hive 会自动进行负载均衡。策略就是把 MapReduce 任务拆分成两个:第一个先做预汇总,第二个再做最终汇总。
# 自动优化,有数据倾斜的时候进行负载均衡(默认是false) 如果开启设置为true
set hive.groupby.skewindata=false;
当选项设定为 true 时,生成的查询计划有两个 MapReduce 任务。
1、在第一个 MapReduce 任务中,map 的输出结果会随机分布到 reduce 中,每个 reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的`group by key`有可能分发到不同的 reduce 中,从而达到负载均衡的目的;
2、第二个 MapReduce 任务再根据预处理的数据结果按照 group by key 分布到各个 reduce 中,最后完成最终的聚合操作。
Map 端部分聚合:并不是所有的聚合操作都需要在 Reduce 端完成,很多聚合操作都可以先在 Map 端进行部分聚合,最后在 Reduce 端得出最终结果,对应的优化器为 GroupByOptimizer。
那么如何用 group by 方式同时统计多个列?
简单版:
select t.a, sum(t.b), count(t.c), count(t.d) from some_table t group by t.a;
优化版:
select t.a, sum(t.b), count(t.c), count(t.d) from (
select a,b,null c,null d from some_table
union all
select a,0 b,c,null d from some_table group by a,c
union all
select a,0 b,null c,d from some_table group by a,d
) t;
2.9 Order By优化
order by 只能是在一个 reduce 进程中进行,所以如果对一个大数据集进行 order by ,会导致一个reduce 进程中处理的数据相当大,造成查询执行缓慢。
1、在最终结果上进行order by,不要在中间的大数据集上进行排序。如果最终结果较少,可以在一个reduce上进行排序时,那么就在最后的结果集上进行order by。
2、如果是取排序后的前N条数据,可以使用distribute by和sort by在各个reduce上进行排序后前N条,然后再对各个reduce的结果集合合并后在一个reduce中全局排序,再取前N条,因为参与全局排序的order by的数据量最多是reduce个数 * N,所以执行效率会有很大提升。
在Hive中,关于数据排序,提供了四种语法,一定要区分这四种排序的使用方式和适用场景
1、order by:全局排序,缺陷是只能使用一个reduce
2、sort by:单机排序,单个reduce结果有序
3、cluster by:对同一字段分桶并排序,不能和sort by连用
4、distribute by + sort by:分桶,保证同一字段值只存在一个结果文件当中,结合sort by保证每个reduceTask结果有序
Hive HQL 中的 order by 与其他 SQL 方言中的功能一样,就是将结果按某字段全局排序,这会导致所有 map 端数据都进入一个 reducer 中,在数据量大时可能会长时间计算不完。
如果使用 sort by,那么还是会视情况启动多个 reducer 进行排序,并且保证每个 reducer 内局部有序。为了控制map 端数据分配到 reducer 的 key,往往还要配合 distribute by 一同使用。如果不加distribute by 的话,map 端数据就会随机分配到 reducer.
1、方式一:
-- 直接使用order by来做。如果结果数据量很大,这个任务的执行效率会非常低
select id,name,age from student order by age desc limit 3;
2、方式二:
set mapreduce.job.reduces=3;
select * from student distribute by (case when age > 20 then 0 when age < 18 then 2 else 1 end) sort by (age desc);
关于分界值的确定,使用采样的方式,来估计数据分布规律。
2.10 Count Distinct 优化
当要统计某一列去重数时,如果数据量很大,count(distinct) 就会非常慢,原因与 order by 类似,count(distinct) 逻辑只会有很少的 reducer 来处理。这时可以用 group by 来改写:
-- 先 group by 在 count
select count(1) from (
select age from student
where department >= "MA"
group by age
) t;
2.11 怎样写in/exists语句
在Hive的早期版本中,in/exists语法是不被支持的,但是从 hive-0.8x 以后就开始支持这个语法。但是不推荐使用这个语法。虽然经过测验,Hive-2.3.6 也支持 in/exists 操作,但还是推荐使用 Hive 的一个高效替代方案:left semi join
-- in / exists 实现
select a.id, a.name from a where a.id in (select b.id from b);
select a.id, a.name from a where exists (select id from b where a.id = b.id);
应该转换成:
-- left semi join 实现
select a.id, a.name from a left semi join b on a.id = b.id;
需要注意的是,一定要展示的数据只有左表中的数据!
2.12 使用 vectorization 矢量查询技术
在计算类似 scan, filter, aggregation 的时候, vectorization 技术以设置批处理的增量大小为 1024 行单次来达到比单条记录单次获得更高的效率。
set hive.vectorized.execution.enabled=true ;
set hive.vectorized.execution.reduce.enabled=true;
2.13 多重插入模式
如果你碰到一堆SQL,并且这一堆SQL的模式还一样。都是从同一个表进行扫描,做不同的逻辑。可优化的地方:如果有n条SQL,每个SQL执行都会扫描一次这张表
如果一个 HQL 底层要执行 10 个 Job,那么能优化成 8 个一般来说,肯定能有所提高,多重插入就是一个非常实用的技能。一次读取,多次插入,有些场景是从一张表读取数据后,要多次利用,这时可以使用 multi insert 语法:
from sale_detail
insert overwrite table sale_detail_multi partition (sale_date='2019',
region='china' )
select shop_name, customer_id, total_price where .....
insert overwrite table sale_detail_multi partition (sale_date='2020',
region='china' )
select shop_name, customer_id, total_price where .....;
说明:multi insert语法有一些限制。
1、一般情况下,单个SQL中最多可以写128路输出,超过128路,则报语法错误。
2、在一个multi insert中:
对于分区表,同一个目标分区不允许出现多次。
对于未分区表,该表不能出现多次。
3、对于同一张分区表的不同分区,不能同时有insert overwrite和insert into操作,否则报错返回
Multi-Group by 是 Hive 的一个非常好的特性,它使得 Hive 中利用中间结果变得非常方便。例如:
FROM (SELECT a.status, b.school, b.gender FROM status_updates a JOIN profiles b
ON (a.userid = b.userid and a.ds='2019-03-20' )) subq1
INSERT OVERWRITE TABLE gender_summary PARTITION(ds='2019-03-20')
SELECT subq1.gender, COUNT(1) GROUP BY subq1.gender
INSERT OVERWRITE TABLE school_summary PARTITION(ds='2019-03-20')
SELECT subq1.school, COUNT(1) GROUP BY subq1.school;
上述查询语句使用了 Multi-Group by 特性连续 group by 了 2 次数据,使用不同的 Multi-Group by。这一特性可以减少一次 MapReduce 操作。
2.14 启动中间结果压缩
map 输出压缩
set mapreduce.map.output.compress=true;
set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec;
中间数据压缩
中间数据压缩就是对 hive 查询的多个 Job 之间的数据进行压缩。最好是选择一个节省CPU耗时的压缩方式。可以采用 snappy 压缩算法,该算法的压缩和解压效率都非常高。
set hive.exec.compress.intermediate=true;
set hive.intermediate.compression.codec=org.apache.hadoop.io.compress.SnappyCodec;
set hive.intermediate.compression.type=BLOCK;
结果数据压缩
最终的结果数据(Reducer输出数据)也是可以进行压缩的,可以选择一个压缩效果比较好的,可以减少数据的大小和数据的磁盘读写时间;注:常用的 gzip,snappy 压缩算法是不支持并行处理的,如果数据源是 gzip/snappy压缩文件大文件,这样只会有有个 mapper 来处理这个文件,会严重影响查询效率。所以如果结果数据需要作为其他查询任务的数据源,可以选择支持 splitable 的 LZO 算法,这样既能对结果文件进行压缩,还可以并行的处理,这样就可以大大的提高 job 执行的速度了。
set hive.exec.compress.output=true;
set mapreduce.output.fileoutputformat.compress=true;
set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec;
set mapreduce.output.fileoutputformat.compress.type=BLOCK;
Hadoop集群支持的压缩算法:
org.apache.hadoop.io.compress.DefaultCodec
org.apache.hadoop.io.compress.GzipCodec
org.apache.hadoop.io.compress.BZip2Codec
org.apache.hadoop.io.compress.DeflateCodec
org.apache.hadoop.io.compress.SnappyCodec
org.apache.hadoop.io.compress.Lz4Codec
com.hadoop.compression.lzo.LzoCodec
com.hadoop.compression.lzo.LzopCodec
3、Hive架构层面
3.1 启用本地抓取(默认开启)
Hive 的某些 SQL 语句需要转换成 MapReduce 的操作,某些 SQL 语句就不需要转换成 MapReduce 操作,但是同学们需要注意,理论上来说,所有的 SQL 语句都需要转换成 MapReduce 操作,只不过Hive 在转换 SQL 语句的过程中会做部分优化,使某些简单的操作不再需要转换成 MapReduce,例如:
1、只是 select * 的时候
2、where 条件针对分区字段进行筛选过滤时
3、带有 limit 分支语句时
3.2 本地执行优化
Hive 在集群上查询时,默认是在集群上多台机器上运行,需要多个机器进行协调运行,这种方式很好的解决了大数据量的查询问题。但是在 Hive 查询处理的数据量比较小的时候,其实没有必要启动分布式模式去执行,因为以分布式方式执行设计到跨网络传输、多节点协调等,并且消耗资源。对于小数据集,可以通过本地模式,在单台机器上处理所有任务,执行时间明显被缩短。
三个参数:
## 打开hive自动判断是否启动本地模式的开关
set hive.exec.mode.local.auto=true;
## map任务数最大值,不启用本地模式的task最大个数
set hive.exec.mode.local.auto.input.files.max=4;
## map输入文件最大大小,不启动本地模式的最大输入文件大小
set hive.exec.mode.local.auto.inputbytes.max=134217728;
3.3 JVM重用
Hive 语句最终会转换为一系列的 MapReduce 任务,每一个MapReduce 任务是由一系列的 MapTask和 ReduceTask 组成的,默认情况下,MapReduce 中一个 MapTask 或者 ReduceTask 就会启动一个JVM 进程,一个 Task 执行完毕后,JVM 进程就会退出。这样如果任务花费时间很短,又要多次启动JVM 的情况下,JVM 的启动时间会变成一个比较大的消耗,这时,可以通过重用 JVM 来解决
JVM也是有缺点的,开启JVM重用会一直占用使用到的 task 的插槽,以便进行重用,直到任务完成后才会释放。如果某个 不平衡的job 中有几个 reduce task 执行的时间要比其他的 reduce task 消耗的时间要多得多的话,那么保留的插槽就会一直空闲却无法被其他的 job 使用,直到所有的 task 都结束了才会释放。
根据经验,一般来说可以使用一个 cpu core 启动一个 JVM,假如服务器有 16 个 cpu core ,但是这个节点,可能会启动 32 个mapTask,完全可以考虑:启动一个JVM,执行两个Task
3.4 并行执行
有的查询语句,Hive 会将其转化为一个或多个阶段,包括:MapReduce 阶段、抽样阶段、合并阶段、limit 阶段等。默认情况下,一次只执行一个阶段。但是,如果某些阶段不是互相依赖,是可以并行执行的。多阶段并行是比较耗系统资源的。
一个 Hive SQL 语句可能会转为多个 MapReduce Job,每一个 job 就是一个 stage,这些 Job 顺序执行,这个在 cli 的运行日志中也可以看到。但是有时候这些任务之间并不是是相互依赖的,如果集群资源允许的话,可以让多个并不相互依赖 stage 并发执行,这样就节约了时间,提高了执行速度,但是如果集群资源匮乏时,启用并行化反倒是会导致各个 Job 相互抢占资源而导致整体执行性能的下降。启用并行化
## 可以开启并行执行。
set hive.exec.parallel=true;
## 同一个sql允许最大并行度,默认为8。
set hive.exec.parallel.thread.number=16;
3.5 推测执行
在分布式集群环境下,因为程序Bug(包括Hadoop本身的bug),负载不均衡或者资源分布不均等原因,会造成同一个作业的多个任务之间运行速度不一致,有些任务的运行速度可能明显慢于其他任务(比如一个作业的某个任务进度只有50%,而其他所有任务已经运行完毕),则这些任务会拖慢作业的整体执行进度。为了避免这种情况发生,Hadoop采用了推测执行(Speculative Execution)机制,它根据一定的法则推测出“拖后腿”的任务,并为这样的任务启动一个备份任务,让该任务与原始任务同时处理同一份数据,并最终选用最先成功运行完成任务的计算结果作为最终结果
# 启动mapper阶段的推测执行机制
set mapreduce.map.speculative=true;
# 启动reducer阶段的推测执行机制
set mapreduce.reduce.speculative=true;
如果用户对于运行时的偏差非常敏感的话,那么可以将这些功能关闭掉。如果用户因为输入数据量很大而需要执行长时间的MapTask或者ReduceTask的话,那么启动推测执行造成的浪费是非常巨大大。其实我一般不使用
3.6 Hive严格模式
所谓严格模式,就是强制不允许用户执行有风险的 HiveQL 语句,一旦执行会直接失败。但是Hive中为了提高SQL语句的执行效率,可以设置严格模式,充分利用Hive的某些特点。
## 设置Hive的严格模式
set hive.mapred.mode=strict;
set hive.exec.dynamic.partition.mode=nostrict;
注意:当设置严格模式之后,会有如下限制:
1、对于分区表,必须添加where对于分区字段的条件过滤
select * from student_ptn where age > 25
2、order by语句必须包含limit输出限制
select * from student order by age limit 100;
3、限制执行笛卡尔积的查询
select a.*, b.* from a, b;
4、在hive的动态分区模式下,如果为严格模式,则必须需要一个分区列式静态分区
4 数据倾斜
4.1 不同数据类型关联产生数据倾斜
select * from users a
left outer join logs b
on a.usr_id = cast(b.user_id as string)
4.2 空值过滤
在生产环境经常会用大量空值数据进入到一个reduce中去,导致数据倾斜。
解决办法:
自定义分区,将为空的key转变为字符串加随机数或纯随机数,将因空值而造成倾斜的数据分不到多个Reducer。
注意:对于异常值如果不需要的话,最好是提前在where条件里过滤掉,这样可以使计算量大大减少
4.3 group by
采用sum() group by的方式来替换count(distinct)完成计算。
4.4 map join
以上讲过了
4.5 开启数据倾斜是负载均衡
以上也讲过了
5 调优方案
5.1 日志表和用户表做链接
select * from log a left outer join users b on a.user_id = b.user_id;
users 表有 600w+ (假设有5G)的记录,把 users 分发到所有的 map 上也是个不小的开销,而且MapJoin 不支持这么大的小表。如果用普通的 join,又会碰到数据倾斜的问题。
改进方案:
select /*+mapjoin(x)*/ * from log a
left outer join (
select /*+mapjoin(c)*/ d.*
from ( select distinct user_id from log ) c join users d on c.user_id =d.user_id
) x
on a.user_id = x.user_id;
假如,log 里 user_id 有上百万个,这就又回到原来 MapJoin 问题。所幸,每日的会员 uv 不会太多,有交易的会员不会太多,有点击的会员不会太多,有佣金的会员不会太多等等。所以这个方法能解决很多场景下的数据倾斜问题。
5.2 位图法求连续七天发朋友圈的用户
每天都要求 微信朋友圈 过去连续7天都发了朋友圈的小伙伴有哪些?假设每个用户每发一次朋友圈都记录了一条日志。每一条朋友圈包含的内容
日期,用户ID,朋友圈内容.....
dt, userid, content, .....
如果 微信朋友圈的 日志数据,按照日期做了分区。
2020-07-06 file1.log(可能会非常大)
2020-07-05 file2.log
…
解决方案:
假设微信有10E用户,我们每天生成一个长度为10E的二进制数组,每个位置要么是0,要么是1,如果为1,代表该用户当天发了朋友圈。如果为0,代表没有发朋友圈。
然后每天:10E / 8 / 1024 / 1024 = 119M左右
求Join实现:两个数组做 求且、求或、异或、求反、求新增
Hive SQL 编译成MapReduce过程
编译 SQL 的任务是在上节中介绍的 COMPILER(编译器组件)中完成的。Hive将SQL转化为MapReduce任务,整个编译过程分为六个阶段:
- 词法、语法解析: Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象语法树 AST Tree; Antlr是一种语言识别的工具,可以用来构造领域语言。使用Antlr构造特定的语言只需要编写一个语法文件,定义词法和语法替换规则即可,Antlr完成了词法分析、语法分析、语义分析、中间代码生成的过程。
- 语义解析: 遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock;
- 生成逻辑执行计划: 遍历 QueryBlock,翻译为执行操作树 OperatorTree;
- 优化逻辑执行计划: 逻辑层优化器进行 OperatorTree 变换,合并 Operator,达到减少 MapReduce Job,减少数据传输及 shuffle 数据量;
- 生成物理执行计划: 遍历 OperatorTree,翻译为 MapReduce 任务;
- 优化物理执行计划: 物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划。
下面对这六个阶段详细解析:
为便于理解,我们拿一个简单的查询语句进行展示,对5月23号的地区维表进行查询:
select * from dim.dim_region where dt = '2021-05-23';
阶段一:词法、语法解析
根据Antlr定义的sql语法规则,将相关sql进行词法、语法解析,转化为抽象语法树AST Tree:
-
ABSTRACT SYNTAX TREE:
-
TOK_QUERY
-
TOK_FROM
-
TOK_TABREF
-
TOK_TABNAME
-
dim
-
dim_region
-
TOK_INSERT
-
TOK_DESTINATION
-
TOK_DIR
-
TOK_TMP_FILE
-
TOK_SELECT
-
TOK_SELEXPR
-
TOK_ALLCOLREF
-
TOK_WHERE
-
=
-
TOK_TABLE_OR_COL
-
dt
-
'2021-05-23'
阶段二:语义解析
遍历AST Tree,抽象出查询的基本组成单元QueryBlock:
AST Tree生成后由于其复杂度依旧较高,不便于翻译为mapreduce程序,需要进行进一步抽象和结构化,形成QueryBlock。
QueryBlock是一条SQL最基本的组成单元,包括三个部分:输入源,计算过程,输出。简单来讲一个QueryBlock就是一个子查询。
QueryBlock的生成过程为一个递归过程,先序遍历 AST Tree ,遇到不同的 Token 节点(理解为特殊标记),保存到相应的属性中。
阶段三:生成逻辑执行计划
遍历QueryBlock,翻译为执行操作树OperatorTree:
Hive最终生成的MapReduce任务,Map阶段和Reduce阶段均由OperatorTree组成。
基本的操作符包括:
- TableScanOperator
- SelectOperator
- FilterOperator
- JoinOperator
- GroupByOperator
- ReduceSinkOperator
Operator在Map Reduce阶段之间的数据传递都是一个流式的过程。每一个Operator对一行数据完成操作后之后将数据传递给childOperator计算。
由于Join/GroupBy/OrderBy均需要在Reduce阶段完成,所以在生成相应操作的Operator之前都会先生成一个ReduceSinkOperator,将字段组合并序列化为Reduce Key/value, Partition Key。
阶段四:优化逻辑执行计划
Hive中的逻辑查询优化可以大致分为以下几类:
- 投影修剪
- 推导传递谓词
- 谓词下推
- 将Select-Select,Filter-Filter合并为单个操作
- 多路 Join
- 查询重写以适应某些列值的Join倾斜
阶段五:生成物理执行计划
- 生成物理执行计划即是将逻辑执行计划生成的OperatorTree转化为MapReduce Job的过程,主要分为下面几个阶段:
- 对输出表生成MoveTask
- 从OperatorTree的其中一个根节点向下深度优先遍历
- ReduceSinkOperator标示Map/Reduce的界限,多个Job间的界限
- 遍历其他根节点,遇过碰到JoinOperator合并MapReduceTask
- 生成StatTask更新元数据
- 剪断Map与Reduce间的Operator的关系
阶段六:优化物理执行计划
Hive中的物理优化可以大致分为以下几类:
- 分区修剪(Partition Pruning)
- 基于分区和桶的扫描修剪(Scan pruning)
- 如果查询基于抽样,则扫描修剪
- 在某些情况下,在 map 端应用 Group By
- 在 mapper 上执行 Join
- 优化 Union,使Union只在 map 端执行
- 在多路 Join 中,根据用户提示决定最后流哪个表
- 删除不必要的 ReduceSinkOperators
- 对于带有Limit子句的查询,减少需要为该表扫描的文件数
- 对于带有Limit子句的查询,通过限制 ReduceSinkOperator 生成的内容来限制来自 mapper 的输出
- 减少用户提交的SQL查询所需的Tez作业数量
- 如果是简单的提取查询,避免使用MapReduce作业
- 对于带有聚合的简单获取查询,执行不带 MapReduce 任务的聚合
- 重写 Group By 查询使用索引表代替原来的表
- 当表扫描之上的谓词是相等谓词且谓词中的列具有索引时,使用索引扫描
经过以上六个阶段,SQL 就被解析映射成了集群上的 MapReduce 任务。
SQL编译成MapReduce具体原理
在阶段五-生成物理执行计划,即遍历 OperatorTree,翻译为 MapReduce 任务,这个过程具体是怎么转化的呢
我们接下来举几个常用 SQL 语句转化为 MapReduce 的具体步骤:
Join的实现原理
以下面这个SQL为例,讲解 join 的实现:
select u.name, o.orderid from order o join user u on o.uid = u.uid;
在map的输出value中为不同表的数据打上tag标记,在reduce阶段根据tag判断数据来源。MapReduce的过程如下:
MapReduce CommonJoin的实现
Group By的实现原理
以下面这个SQL为例,讲解 group by 的实现:
select rank, isonline, count(*) from city group by rank, isonline;
将GroupBy的字段组合为map的输出key值,利用MapReduce的排序,在reduce阶段保存LastKey区分不同的key。MapReduce的过程如下:
Distinct的实现原理
以下面这个SQL为例,讲解 distinct 的实现:
select dealid, count(distinct uid) num from order group by dealid;
当只有一个distinct字段时,如果不考虑Map阶段的Hash GroupBy,只需要将GroupBy字段和Distinct字段组合为map输出key,利用mapreduce的排序,同时将GroupBy字段作为reduce的key,在reduce阶段保存LastKey即可完成去重:
Hive 千亿级数据倾斜
数据倾斜问题剖析
数据倾斜是分布式系统不可避免的问题,任何分布式系统都有几率发生数据倾斜,但有些小伙伴在平时工作中感知不是很明显,这里要注意本篇文章的标题—“千亿级数据”,为什么说千亿级,因为如果一个任务的数据量只有几百万,它即使发生了数据倾斜,所有数据都跑到一台机器去执行,对于几百万的数据量,一台机器执行起来还是毫无压力的,这时数据倾斜对我们感知不大,只有数据达到一个量级时,一台机器应付不了这么多的数据,这时如果发生数据倾斜,那么最后就很难算出结果。
所以就需要我们对数据倾斜的问题进行优化,尽量避免或减轻数据倾斜带来的影响。
在解决数据倾斜问题之前,还要再提一句:没有瓶颈时谈论优化,都是自寻烦恼。
大家想想,在map和reduce两个阶段中,最容易出现数据倾斜的就是reduce阶段,因为map到reduce会经过shuffle阶段,在shuffle中默认会按照key进行hash,如果相同的key过多,那么hash的结果就是大量相同的key进入到同一个reduce中,导致数据倾斜。
那么有没有可能在map阶段就发生数据倾斜呢,是有这种可能的。
一个任务中,数据文件在进入map阶段之前会进行切分,默认是128M一个数据块,但是如果当对文件使用GZIP压缩等不支持文件分割操作的压缩方式时,MR任务读取压缩后的文件时,是对它切分不了的,该压缩文件只会被一个任务所读取,如果有一个超大的不可切分的压缩文件被一个map读取时,就会发生map阶段的数据倾斜。
所以,从本质上来说,发生数据倾斜的原因有两种:一是任务中需要处理大量相同的key的数据。二是任务读取不可分割的大文件。
数据倾斜解决方案
MapReduce和Spark中的数据倾斜解决方案原理都是类似的,以下讨论Hive使用MapReduce引擎引发的数据倾斜,Spark数据倾斜也可以此为参照。
实际业务中有些大量的null值或者一些无意义的数据参与到计算作业中,表中有大量的null值,如果表之间进行join操作,就会有shuffle产生,这样所有的null值都会被分配到一个reduce中,必然产生数据倾斜。
之前有小伙伴问,如果A、B两表join操作,假如A表中需要join的字段为null,但是B表中需要join的字段不为null,这两个字段根本就join不上啊,为什么还会放到一个reduce中呢?
这里我们需要明确一个概念,数据放到同一个reduce中的原因不是因为字段能不能join上,而是因为shuffle阶段的hash操作,只要key的hash结果是一样的,它们就会被拉到同一个reduce中。
解决方案:
第一种:可以直接不让null值参与join操作,即不让null值有shuffle阶段
-
SELECT *
-
FROM log a
-
JOIN users b
-
ON a.user_id IS NOT NULL
-
AND a.user_id = b.user_id
-
UNION ALL
-
SELECT *
-
FROM log a
-
WHERE a.user_id IS NULL;
第二种:因为null值参与shuffle时的hash结果是一样的,那么我们可以给null值随机赋值,这样它们的hash结果就不一样,就会进到不同的reduce中:
-
SELECT *
-
FROM log a
-
LEFT JOIN users b ON CASE
-
WHEN a.user_id IS NULL THEN concat('hive_', rand())
-
ELSE a.user_id
-
END = b.user_id;
对于两个表join,表a中需要join的字段key为int,表b中key字段既有string类型也有int类型。当按照key进行两个表的join操作时,默认的Hash操作会按int型的id来进行分配,这样所有的string类型都被分配成同一个id,结果就是所有的string类型的字段进入到一个reduce中,引发数据倾斜。
解决方案:
如果key字段既有string类型也有int类型,默认的hash就都会按int类型来分配,那我们直接把int类型都转为string就好了,这样key字段都为string,hash时就按照string类型分配了:
-
SELECT *
-
FROM users a
-
LEFT JOIN logs b ON a.usr_id = CAST(b.user_id AS string);
当集群的数据量增长到一定规模,有些数据需要归档或者转储,这时候往往会对数据进行压缩;当对文件使用GZIP压缩等不支持文件分割操作的压缩方式,在日后有作业涉及读取压缩后的文件时,该压缩文件只会被一个任务所读取。如果该压缩文件很大,则处理该文件的Map需要花费的时间会远多于读取普通文件的Map时间,该Map任务会成为作业运行的瓶颈。这种情况也就是Map读取文件的数据倾斜。
解决方案:
这种数据倾斜问题没有什么好的解决方案,只能将使用GZIP压缩等不支持文件分割的文件转为bzip和zip等支持文件分割的压缩方式。
所以,我们在对文件进行压缩时,为避免因不可拆分大文件而引发数据读取的倾斜,在数据压缩的时候可以采用bzip2和Zip等支持文件分割的压缩算法。
在多维聚合计算时,如果进行分组聚合的字段过多,如下:
select a,b,c,count(1)from log group by a,b,c with rollup;
注:对于最后的with rollup关键字不知道大家用过没,with rollup是用来在分组统计数据的基础上再进行统计汇总,即用来得到group by的汇总信息。
如果上面的log表的数据量很大,并且Map端的聚合不能很好地起到数据压缩的情况下,会导致Map端产出的数据急速膨胀,这种情况容易导致作业内存溢出的异常。如果log表含有数据倾斜key,会加剧Shuffle过程的数据倾斜。
解决方案:
可以拆分上面的sql,将with rollup拆分成如下几个sql:
-
SELECT a, b, c, COUNT(1)
-
FROM log
-
GROUP BY a, b, c;
-
SELECT a, b, NULL, COUNT(1)
-
FROM log
-
GROUP BY a, b;
-
SELECT a, NULL, NULL, COUNT(1)
-
FROM log
-
GROUP BY a;
-
SELECT NULL, NULL, NULL, COUNT(1)
-
FROM log;
但是,上面这种方式不太好,因为现在是对3个字段进行分组聚合,那如果是5个或者10个字段呢,那么需要拆解的SQL语句会更多。
在Hive中可以通过参数 hive.new.job.grouping.set.cardinality 配置的方式自动控制作业的拆解,该参数默认值是30。表示针对grouping sets/rollups/cubes这类多维聚合的操作,如果最后拆解的键组合大于该值,会启用新的任务去处理大于该值之外的组合。如果在处理数据时,某个分组聚合的列有较大的倾斜,可以适当调小该值。
两表进行普通的repartition join时,如果表连接的键存在倾斜,那么在 Shuffle 阶段必然会引起数据倾斜。
解决方案:
通常做法是将倾斜的数据存到分布式缓存中,分发到各个 Map任务所在节点。在Map阶段完成join操作,即MapJoin,这避免了 Shuffle,从而避免了数据倾斜。
MapJoin是Hive的一种优化操作,其适用于小表JOIN大表的场景,由于表的JOIN操作是在Map端且在内存进行的,所以其并不需要启动Reduce任务也就不需要经过shuffle阶段,从而能在一定程度上节省资源提高JOIN效率。
在Hive 0.11版本之前,如果想在Map阶段完成join操作,必须使用MAPJOIN来标记显示地启动该优化操作,由于其需要将小表加载进内存所以要注意小表的大小。
如将a表放到Map端内存中执行,在Hive 0.11版本之前需要这样写:
-
select /* +mapjoin(a) */ a.id , a.name, b.age
-
from a join b
-
on a.id = b.id;
如果想将多个表放到Map端内存中,只需在mapjoin()中写多个表名称即可,用逗号分隔,如将a表和c表放到Map端内存中,则 / +mapjoin(a,c) / 。
在Hive 0.11版本及之后,Hive默认启动该优化,也就是不在需要显示的使用MAPJOIN标记,其会在必要的时候触发该优化操作将普通JOIN转换成MapJoin,可以通过以下两个属性来设置该优化的触发时机:
hive.auto.convert.join=true 默认值为true,自动开启MAPJOIN优化。
hive.mapjoin.smalltable.filesize=2500000 默认值为2500000(25M),通过配置该属性来确定使用该优化的表的大小,如果表的大小小于此值就会被加载进内存中。
注意:使用默认启动该优化的方式如果出现莫名其妙的BUG(比如MAPJOIN并不起作用),就将以下两个属性置为fase手动使用MAPJOIN标记来启动该优化:
hive.auto.convert.join=false (关闭自动MAPJOIN转换操作)
hive.ignore.mapjoin.hint=false (不忽略MAPJOIN标记)
再提一句:将表放到Map端内存时,如果节点的内存很大,但还是出现内存溢出的情况,我们可以通过这个参数 mapreduce.map.memory.mb 调节Map端内存的大小。
在一些操作中,我们没有办法减少数据量,如在使用 collect_list 函数时:
-
select s_age,collect_list(s_score) list_score
-
from student
-
group by s_age
collect_list:将分组中的某列转为一个数组返回。
在上述sql中,s_age有数据倾斜,但如果数据量大到一定的数量,会导致处理倾斜的Reduce任务产生内存溢出的异常。
collect_list输出一个数组,中间结果会放到内存中,所以如果collect_list聚合太多数据,会导致内存溢出。
有小伙伴说这是 group by 分组引起的数据倾斜,可以开启hive.groupby.skewindata参数来优化。我们接下来分析下:
开启该配置会将作业拆解成两个作业,第一个作业会尽可能将Map的数据平均分配到Reduce阶段,并在这个阶段实现数据的预聚合,以减少第二个作业处理的数据量;第二个作业在第一个作业处理的数据基础上进行结果的聚合。
hive.groupby.skewindata的核心作用在于生成的第一个作业能够有效减少数量。但是对于collect_list这类要求全量操作所有数据的中间结果的函数来说,明显起不到作用,反而因为引入新的作业增加了磁盘和网络I/O的负担,而导致性能变得更为低下。
解决方案:
这类问题最直接的方式就是调整reduce所执行的内存大小。
调整reduce的内存大小使用mapreduce.reduce.memory.mb这个配置。
Hive执行计划
Hive SQL的执行计划描述SQL实际执行的整体轮廓,通过执行计划能了解SQL程序在转换成相应计算引擎的执行逻辑,掌握了执行逻辑也就能更好地把握程序出现的瓶颈点,从而能够实现更有针对性的优化。此外还能帮助开发者识别看似等价的SQL其实是不等价的,看似不等价的SQL其实是等价的SQL。可以说执行计划是打开SQL优化大门的一把钥匙。
要想学SQL执行计划,就需要学习查看执行计划的命令:explain,在查询语句的SQL前面加上关键字explain是查看执行计划的基本方法。
学会explain,能够给我们工作中使用hive带来极大的便利!
- 查看SQL的执行计划 -
Hive提供的执行计划目前可以查看的信息有以下几种:
- explain:查看执行计划的基本信息;
- explain dependency:dependency在explain语句中使用会产生有关计划中输入的额外信息。它显示了输入的各种属性;
- explain authorization:查看SQL操作相关权限的信息;
- explain vectorization:查看SQL的向量化描述信息,显示为什么未对Map和Reduce进行矢量化。从 Hive 2.3.0 开始支持;
- explain analyze:用实际的行数注释计划。从 Hive 2.2.0 开始支持;
- explain cbo:输出由Calcite优化器生成的计划。CBO 从 Hive 4.0.0 版本开始支持;
- explain locks:这对于了解系统将获得哪些锁以运行指定的查询很有用。LOCKS 从 Hive 3.2.0 开始支持;
- explain ast:输出查询的抽象语法树。AST 在 Hive 2.1.0 版本删除了,存在bug,转储AST可能会导致OOM错误,将在4.0.0版本修复;
- explain extended:加上 extended 可以输出有关计划的额外信息。这通常是物理信息,例如文件名,这些额外信息对我们用处不大;
1. explain 的用法
Hive提供了explain命令来展示一个查询的执行计划,这个执行计划对于我们了解底层原理,Hive 调优,排查数据倾斜等很有帮助。
使用语法如下:
explain query;
在 hive cli 中输入以下命令(hive 2.3.7):
explain select sum(id) from test1;
得到结果:
-
STAGE DEPENDENCIES:
-
Stage-1 is a root stage
-
Stage-0 depends on stages: Stage-1
-
STAGE PLANS:
-
Stage: Stage-1
-
Map Reduce
-
Map Operator Tree:
-
TableScan
-
alias: test1
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int)
-
outputColumnNames: id
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Group By Operator
-
aggregations: sum(id)
-
mode: hash
-
outputColumnNames: _col0
-
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE
-
Reduce Output Operator
-
sort order:
-
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE
-
value expressions: _col0 (type: bigint)
-
Reduce Operator Tree:
-
Group By Operator
-
aggregations: sum(VALUE._col0)
-
mode: mergepartial
-
outputColumnNames: _col0
-
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE
-
File Output Operator
-
compressed: false
-
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE
-
table:
-
input format: org.apache.hadoop.mapred.SequenceFileInputFormat
-
output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat
-
serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
-
Stage: Stage-0
-
Fetch Operator
-
limit: -1
-
Processor Tree:
-
ListSink
看完以上内容有什么感受,是不是感觉都看不懂,不要着急,下面将会详细讲解每个参数,相信你学完下面的内容之后再看 explain 的查询结果将游刃有余。
一个HIVE查询被转换为一个由一个或多个stage组成的序列(有向无环图DAG)。这些stage可以是MapReduce stage,也可以是负责元数据存储的stage,也可以是负责文件系统的操作(比如移动和重命名)的stage。
我们将上述结果拆分看,先从最外层开始,包含两个大的部分:
先看第一部分 stage dependencies ,包含两个 stage,Stage-1 是根stage,说明这是开始的stage,Stage-0 依赖 Stage-1,Stage-1执行完成后执行Stage-0。
- stage dependencies:各个stage之间的依赖性
- stage plan:各个stage的执行计划
再看第二部分 stage plan,里面有一个 Map Reduce,一个MR的执行计划分为两个部分:
- Map Operator Tree:MAP端的执行计划树
- Reduce Operator Tree:Reduce端的执行计划树
这两个执行计划树里面包含这条sql语句的 operator:
- TableScan:表扫描操作,map端第一个操作肯定是加载表,所以就是表扫描操作,常见的属性:
- alias:表名称
- Statistics:表统计信息,包含表中数据条数,数据大小等
- Select Operator:选取操作,常见的属性 :
- expressions:需要的字段名称及字段类型
- outputColumnNames:输出的列名称
- Statistics:表统计信息,包含表中数据条数,数据大小等
- Group By Operator:分组聚合操作,常见的属性:
- aggregations:显示聚合函数信息
- mode:聚合模式,值有 hash:随机聚合,就是hash partition;partial:局部聚合;final:最终聚合
- keys:分组的字段,如果没有分组,则没有此字段
- outputColumnNames:聚合之后输出列名
- Statistics:表统计信息,包含分组聚合之后的数据条数,数据大小等
- Reduce Output Operator:输出到reduce操作,常见属性:
- sort order:值为空 不排序;值为 + 正序排序,值为 - 倒序排序;值为 +- 排序的列为两列,第一列为正序,第二列为倒序
- Filter Operator:过滤操作,常见的属性:
- predicate:过滤条件,如sql语句中的where id>=1,则此处显示(id >= 1)
- Map Join Operator:join 操作,常见的属性:
- condition map:join方式 ,如Inner Join 0 to 1 Left Outer Join0 to 2
- keys: join 的条件字段
- outputColumnNames:join 完成之后输出的字段
- Statistics:join 完成之后生成的数据条数,大小等
- File Output Operator:文件输出操作,常见的属性
- compressed:是否压缩
- table:表的信息,包含输入输出文件格式化方式,序列化方式等
- Fetch Operator 客户端获取数据操作,常见的属性:
- limit,值为 -1 表示不限制条数,其他值为限制的条数
2. explain 的使用场景
案例一:join 语句会过滤 null 的值吗?
现在,我们在hive cli 输入以下查询计划语句
select a.id,b.user_name from test1 a join test2 b on a.id=b.id;
问:上面这条 join 语句会过滤 id 为 null 的值吗
执行下面语句:
explain select a.id,b.user_name from test1 a join test2 b on a.id=b.id;
我们来看结果 (为了适应页面展示,仅截取了部分输出信息):
-
TableScan
-
alias: a
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Filter Operator
-
predicate: id is not null (type: boolean)
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int)
-
outputColumnNames: _col0
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
HashTable Sink Operator
-
keys:
-
0 _col0 (type: int)
-
1 _col0 (type: int)
-
...
从上述结果可以看到 predicate: id is not null 这样一行,说明 join 时会自动过滤掉关联字段为 null 值的情况,但 left join 或 full join 是不会自动过滤null值的,大家可以自行尝试下。
案例二:group by 分组语句会进行排序吗?
看下面这条sql
select id,max(user_name) from test1 group by id;
问:group by 分组语句会进行排序吗
直接来看 explain 之后结果 (为了适应页面展示,仅截取了部分输出信息)
-
TableScan
-
alias: test1
-
Statistics: Num rows: 9 Data size: 108 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int), user_name (type: string)
-
outputColumnNames: id, user_name
-
Statistics: Num rows: 9 Data size: 108 Basic stats: COMPLETE Column stats: NONE
-
Group By Operator
-
aggregations: max(user_name)
-
keys: id (type: int)
-
mode: hash
-
outputColumnNames: _col0, _col1
-
Statistics: Num rows: 9 Data size: 108 Basic stats: COMPLETE Column stats: NONE
-
Reduce Output Operator
-
key expressions: _col0 (type: int)
-
sort order: +
-
Map-reduce partition columns: _col0 (type: int)
-
Statistics: Num rows: 9 Data size: 108 Basic stats: COMPLETE Column stats: NONE
-
value expressions: _col1 (type: string)
-
...
我们看 Group By Operator,里面有 keys: id (type: int) 说明按照 id 进行分组的,再往下看还有 sort order: + ,说明是按照 id 字段进行正序排序的。
案例三:哪条sql执行效率高呢?
观察两条sql语句
-
SELECT
-
a.id,
-
b.user_name
-
FROM
-
test1 a
-
JOIN test2 b ON a.id = b.id
-
WHERE
-
a.id > 2;
-
SELECT
-
a.id,
-
b.user_name
-
FROM
-
(SELECT * FROM test1 WHERE id > 2) a
-
JOIN test2 b ON a.id = b.id;
这两条sql语句输出的结果是一样的,但是哪条sql执行效率高呢?
有人说第一条sql执行效率高,因为第二条sql有子查询,子查询会影响性能;
有人说第二条sql执行效率高,因为先过滤之后,在进行join时的条数减少了,所以执行效率就高了。
到底哪条sql效率高呢,我们直接在sql语句前面加上 explain,看下执行计划不就知道了嘛!
在第一条sql语句前加上 explain,得到如下结果
-
hive (default)> explain select a.id,b.user_name from test1 a join test2 b on a.id=b.id where a.id >2;
-
OK
-
Explain
-
STAGE DEPENDENCIES:
-
Stage-4 is a root stage
-
Stage-3 depends on stages: Stage-4
-
Stage-0 depends on stages: Stage-3
-
STAGE PLANS:
-
Stage: Stage-4
-
Map Reduce Local Work
-
Alias -> Map Local Tables:
-
$hdt$_0:a
-
Fetch Operator
-
limit: -1
-
Alias -> Map Local Operator Tree:
-
$hdt$_0:a
-
TableScan
-
alias: a
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Filter Operator
-
predicate: (id > 2) (type: boolean)
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int)
-
outputColumnNames: _col0
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
HashTable Sink Operator
-
keys:
-
0 _col0 (type: int)
-
1 _col0 (type: int)
-
Stage: Stage-3
-
Map Reduce
-
Map Operator Tree:
-
TableScan
-
alias: b
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Filter Operator
-
predicate: (id > 2) (type: boolean)
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int), user_name (type: string)
-
outputColumnNames: _col0, _col1
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Map Join Operator
-
condition map:
-
Inner Join 0 to 1
-
keys:
-
0 _col0 (type: int)
-
1 _col0 (type: int)
-
outputColumnNames: _col0, _col2
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: _col0 (type: int), _col2 (type: string)
-
outputColumnNames: _col0, _col1
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
File Output Operator
-
compressed: false
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
table:
-
input format: org.apache.hadoop.mapred.SequenceFileInputFormat
-
output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat
-
serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
-
Local Work:
-
Map Reduce Local Work
-
Stage: Stage-0
-
Fetch Operator
-
limit: -1
-
Processor Tree:
-
ListSink
在第二条sql语句前加上 explain,得到如下结果
-
hive (default)> explain select a.id,b.user_name from(select * from test1 where id>2 ) a join test2 b on a.id=b.id;
-
OK
-
Explain
-
STAGE DEPENDENCIES:
-
Stage-4 is a root stage
-
Stage-3 depends on stages: Stage-4
-
Stage-0 depends on stages: Stage-3
-
STAGE PLANS:
-
Stage: Stage-4
-
Map Reduce Local Work
-
Alias -> Map Local Tables:
-
$hdt$_0:test1
-
Fetch Operator
-
limit: -1
-
Alias -> Map Local Operator Tree:
-
$hdt$_0:test1
-
TableScan
-
alias: test1
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Filter Operator
-
predicate: (id > 2) (type: boolean)
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int)
-
outputColumnNames: _col0
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
HashTable Sink Operator
-
keys:
-
0 _col0 (type: int)
-
1 _col0 (type: int)
-
Stage: Stage-3
-
Map Reduce
-
Map Operator Tree:
-
TableScan
-
alias: b
-
Statistics: Num rows: 6 Data size: 75 Basic stats: COMPLETE Column stats: NONE
-
Filter Operator
-
predicate: (id > 2) (type: boolean)
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: id (type: int), user_name (type: string)
-
outputColumnNames: _col0, _col1
-
Statistics: Num rows: 2 Data size: 25 Basic stats: COMPLETE Column stats: NONE
-
Map Join Operator
-
condition map:
-
Inner Join 0 to 1
-
keys:
-
0 _col0 (type: int)
-
1 _col0 (type: int)
-
outputColumnNames: _col0, _col2
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
Select Operator
-
expressions: _col0 (type: int), _col2 (type: string)
-
outputColumnNames: _col0, _col1
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
File Output Operator
-
compressed: false
-
Statistics: Num rows: 2 Data size: 27 Basic stats: COMPLETE Column stats: NONE
-
table:
-
input format: org.apache.hadoop.mapred.SequenceFileInputFormat
-
output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat
-
serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
-
Local Work:
-
Map Reduce Local Work
-
Stage: Stage-0
-
Fetch Operator
-
limit: -1
-
Processor Tree:
-
ListSink
大家有什么发现,除了表别名不一样,其他的执行计划完全一样,都是先进行 where 条件过滤,在进行 join 条件关联。说明 hive 底层会自动帮我们进行优化,所以这两条sql语句执行效率是一样的。
以上仅列举了3个我们生产中既熟悉又有点迷糊的例子,explain 还有很多其他的用途,如查看stage的依赖情况、排查数据倾斜、hive 调优等,小伙伴们可以自行尝试。
2. explain dependency的用法
explain dependency用于描述一段SQL需要的数据来源,输出是一个json格式的数据,里面包含以下两个部分的内容:
- input_partitions:描述一段SQL依赖的数据来源表分区,里面存储的是分区名的列表,如果整段SQL包含的所有表都是非分区表,则显示为空。
- input_tables:描述一段SQL依赖的数据来源表,里面存储的是Hive表名的列表。
使用explain dependency查看SQL查询非分区普通表,在 hive cli 中输入以下命令:
explain dependency select s_age,count(1) num from student_orc;
得到结果:
{"input_partitions":[],"input_tables":[{"tablename":"default@student_tb _orc","tabletype":"MANAGED_TABLE"}]}
使用explain dependency查看SQL查询分区表,在 hive cli 中输入以下命令:
explain dependency select s_age,count(1) num from student_orc_partition;
得到结果:
-
{"input_partitions":[{"partitionName":"default@student_orc_partition@ part=0"},
-
{"partitionName":"default@student_orc_partition@part=1"},
-
{"partitionName":"default@student_orc_partition@part=2"},
-
{"partitionName":"default@student_orc_partition@part=3"},
-
{"partitionName":"default@student_orc_partition@part=4"},
-
{"partitionName":"default@student_orc_partition@part=5"},
-
{"partitionName":"default@student_orc_partition@part=6"},
-
{"partitionName":"default@student_orc_partition@part=7"},
-
{"partitionName":"default@student_orc_partition@part=8"},
-
{"partitionName":"default@student_orc_partition@part=9"}],
-
"input_tables":[{"tablename":"default@student_orc_partition", "tabletype":"MANAGED_TABLE"}]
explain dependency的使用场景有两个:
- 场景一:快速排除。快速排除因为读取不到相应分区的数据而导致任务数据输出异常。例如,在一个以天分区的任务中,上游任务因为生产过程不可控因素出现异常或者空跑,导致下游任务引发异常。通过这种方式,可以快速查看SQL读取的分区是否出现异常。
- 场景二:理清表的输入,帮助理解程序的运行,特别是有助于理解有多重子查询,多表连接的依赖输入。
下面通过两个案例来看explain dependency的实际运用:
案例一:识别看似等价的代码
对于刚接触SQL的程序员,很容易将
select * from a inner join b on a.no=b.no and a.f>1 and a.f<3;
等价于
select * from a inner join b on a.no=b.no where a.f>1 and a.f<3;
我们可以通过案例来查看下它们的区别:
代码1:
-
select
-
a.s_no
-
from student_orc_partition a
-
inner join
-
student_orc_partition_only b
-
on a.s_no=b.s_no and a.part=b.part and a.part>=1 and a.part<=2;
代码2:
-
select
-
a.s_no
-
from student_orc_partition a
-
inner join
-
student_orc_partition_only b
-
on a.s_no=b.s_no and a.part=b.part
-
where a.part>=1 and a.part<=2;
我们看下上述两段代码explain dependency的输出结果:
代码1的explain dependency结果:
-
{"input_partitions":
-
[{"partitionName":"default@student_orc_partition@part=0"},
-
{"partitionName":"default@student_orc_partition@part=1"},
-
{"partitionName":"default@student_orc_partition@part=2"},
-
{"partitionName":"default@student_orc_partition_only@part=1"},
-
{"partitionName":"default@student_orc_partition_only@part=2"}],
-
"input_tables": [{"tablename":"default@student_orc_partition","tabletype":"MANAGED_TABLE"}, {"tablename":"default@student_orc_partition_only","tabletype":"MANAGED_TABLE"}]}
代码2的explain dependency结果:
-
{"input_partitions":
-
[{"partitionName":"default@student_orc_partition@part=1"},
-
{"partitionName" : "default@student_orc_partition@part=2"},
-
{"partitionName" :"default@student_orc_partition_only@part=1"},
-
{"partitionName":"default@student_orc_partition_only@part=2"}],
-
"input_tables": [{"tablename":"default@student_orc_partition","tabletype":"MANAGED_TABLE"}, {"tablename":"default@student_orc_partition_only","tabletype":"MANAGED_TABLE"}]}
通过上面的输出结果可以看到,其实上述的两个SQL并不等价,代码1在内连接(inner join)中的连接条件(on)中加入非等值的过滤条件后,并没有将内连接的左右两个表按照过滤条件进行过滤,内连接在执行时会多读取part=0的分区数据。而在代码2中,会过滤掉不符合条件的分区。
案例二:识别SQL读取数据范围的差别
代码1:
-
explain dependency
-
select
-
a.s_no
-
from student_orc_partition a
-
left join
-
student_orc_partition_only b
-
on a.s_no=b.s_no and a.part=b.part and b.part>=1 and b.part<=2;
代码2:
-
explain dependency
-
select
-
a.s_no
-
from student_orc_partition a
-
left join
-
student_orc_partition_only b
-
on a.s_no=b.s_no and a.part=b.part and a.part>=1 and a.part<=2;
以上两个代码的数据读取范围是一样的吗?答案是不一样,我们通过explain dependency来看下:
代码1的explain dependency结果:
-
{"input_partitions":
-
[{"partitionName": "default@student_orc_partition@part=0"},
-
{"partitionName":"default@student_orc_partition@part=1"}, …中间省略7个分区
-
{"partitionName":"default@student_orc_partition@part=9"},
-
{"partitionName":"default@student_orc_partition_only@part=1"},
-
{"partitionName":"default@student_orc_partition_only@part=2"}],
-
"input_tables": [{"tablename":"default@student_orc_partition","tabletype":"MANAGED_TABLE"}, {"tablename":"default@student_orc_partition_only","tabletype":"MANAGED_TABLE"}]}
代码2的explain dependency结果:
-
{"input_partitions":
-
[{"partitionName":"default@student_orc_partition@part=0"},
-
{"partitionName":"default@student_orc_partition@part=1"}, …中间省略7个分区
-
{"partitionName":"default@student_orc_partition@part=9"},
-
{"partitionName":"default@student_orc_partition_only@part=0"},
-
{"partitionName":"default@student_orc_partition_only@part=1"}, …中间省略7个分区
-
{"partitionName":"default@student_orc_partition_only@part=9"}],
-
"input_tables": [{"tablename":"default@student_orc_partition","tabletype":"MANAGED_TABLE"}, {"tablename":"default@student_orc_partition_only","tabletype":"MANAGED_TABLE"}]}
可以看到,对左外连接在连接条件中加入非等值过滤的条件,如果过滤条件是作用于右表(b表)有起到过滤的效果,则右表只要扫描两个分区即可,但是左表(a表)会进行全表扫描。如果过滤条件是针对左表,则完全没有起到过滤的作用,那么两个表将进行全表扫描。这时的情况就如同全外连接一样都需要对两个数据进行全表扫描。
在使用过程中,容易认为代码片段2可以像代码片段1一样进行数据过滤,通过查看explain dependency的输出结果,可以知道不是如此。
3. explain authorization 的用法
通过explain authorization可以知道当前SQL访问的数据来源(INPUTS) 和数据输出(OUTPUTS),以及当前Hive的访问用户 (CURRENT_USER)和操作(OPERATION)。
在 hive cli 中输入以下命令:
-
explain authorization
-
select variance(s_score) from student_tb_orc;
结果如下:
-
INPUTS:
-
default@student_tb_orc
-
OUTPUTS:
-
hdfs://node01:8020/tmp/hive/hdfs/cbf182a5-8258-4157-9194- 90f1475a3ed5/-mr-10000
-
CURRENT_USER:
-
hdfs
-
OPERATION:
-
QUERY
-
AUTHORIZATION_FAILURES:
-
No privilege 'Select' found for inputs { database:default, table:student_ tb_orc, columnName:s_score}
从上面的信息可知:
上面案例的数据来源是defalut数据库中的 student_tb_orc表;
数据的输出路径是hdfs://node01:8020/tmp/hive/hdfs/cbf182a5-8258-4157-9194-90f1475a3ed5/-mr-10000;
当前的操作用户是hdfs,操作是查询;
观察上面的信息我们还会看到AUTHORIZATION_FAILURES信息,提示对当前的输入没有查询权限,但如果运行上面的SQL的话也能够正常运行。为什么会出现这种情况?Hive在默认不配置权限管理的情况下不进行权限验证,所有的用户在Hive里面都是超级管理员,即使不对特定的用户进行赋权,也能够正常查询。
通过上面对explain的介绍,可以发现explain中有很多值得我们去研究的内容,读懂 explain 的执行计划有利于我们优化Hive SQL,同时也能提升我们对SQL的掌控力。
Hive SQL底层执行原理
本节结构采用宏观着眼,微观入手,从整体到细节的方式剖析 Hive SQL 底层原理。第一节先介绍 Hive 底层的整体执行流程,然后第二节介绍执行流程中的 SQL 编译成 MapReduce 的过程,第三节剖析 SQL 编译成 MapReduce 的具体实现原理。
Hive 底层执行架构
我们先来看下 Hive 的底层执行架构图, Hive 的主要组件与 Hadoop 交互的过程:
在 Hive 这一侧,总共有五个组件:
- UI:用户界面。可看作我们提交SQL语句的命令行界面。
- DRIVER:驱动程序。接收查询的组件。该组件实现了会话句柄的概念。
- COMPILER:编译器。负责将 SQL 转化为平台可执行的执行计划。对不同的查询块和查询表达式进行语义分析,并最终借助表和从 metastore 查找的分区元数据来生成执行计划.
- METASTORE:元数据库。存储 Hive 中各种表和分区的所有结构信息。
- EXECUTION ENGINE:执行引擎。负责提交 COMPILER 阶段编译好的执行计划到不同的平台上。
上图的基本流程是:
步骤1:UI 调用 DRIVER 的接口;
步骤2:DRIVER 为查询创建会话句柄,并将查询发送到 COMPILER(编译器)生成执行计划;
步骤3和4:编译器从元数据存储中获取本次查询所需要的元数据,该元数据用于对查询树中的表达式进行类型检查,以及基于查询谓词修建分区;
步骤5:编译器生成的计划是分阶段的DAG,每个阶段要么是 map/reduce 作业,要么是一个元数据或者HDFS上的操作。将生成的计划发给 DRIVER。
如果是 map/reduce 作业,该计划包括 map operator trees 和一个 reduce operator tree,执行引擎将会把这些作业发送给 MapReduce :
步骤6、6.1、6.2和6.3:执行引擎将这些阶段提交给适当的组件。在每个 task(mapper/reducer) 中,从HDFS文件中读取与表或中间输出相关联的数据,并通过相关算子树传递这些数据。最终这些数据通过序列化器写入到一个临时HDFS文件中(如果不需要 reduce 阶段,则在 map 中操作)。临时文件用于向计划中后面的 map/reduce 阶段提供数据。
步骤7、8和9:最终的临时文件将移动到表的位置,确保不读取脏数据(文件重命名在HDFS中是原子操作)。对于用户的查询,临时文件的内容由执行引擎直接从HDFS读取,然后通过Driver发送到UI。
总结
通过上面的内容我们发现,shuffle阶段堪称性能的杀手,为什么这么说,一方面shuffle阶段是最容易引起数据倾斜的;另一方面shuffle的过程中会产生大量的磁盘I/O、网络I/O 以及压缩、解压缩、序列化和反序列化等。这些操作都是严重影响性能的。
所以围绕shuffle和数据倾斜有很多的调优点:
Mapper 端的Buffer 设置为多大?Buffer 设置得大,可提升性能,减少磁盘I/O ,但 是对内存有要求,对GC 有压力;Buffer 设置得小,可能不占用那么多内存, 但是可能频繁的磁盘I/O 、频繁的网络I/O 。