背 景
对于Hive的使用者来说,熟悉掌握DDL语句(参考博客:Hive从入门到放弃——HiveQL表级别DDL设计的艺术性(五))和DML语句(参考博客:Hive从入门到放弃——Hive表DML的艺术性(八))是必不可少的入门篇,但是因为Hive的机制是将Hive SQL转化成Map-Reduce执行的,这也就是为什么Hive也叫作解析器的本质原因,这样的话就存在一个问题,平时我们所掌握的跟Hive-SQL相似的MySQL语句使用经验,优化经验,并不一定完全适用于Hive-SQL,该博客就是为了了更好的学习Hive-SQL的优化和执行原理讲解。
阅读此文前除了对Hive SQL熟悉基本的操作外,还需要你对MapReduce的原理有一定的了解,需要补习下的可以参考博客Hadoop集群大数据解决方案之核心组件MapReduce(四)。
select语句执行图解
这里假设我们需要抓取下2017年1月1日订单表ods_orders_table
的所有购买了iphone7的订单明细,那Hive SQL的实现也非常简单,一个select就能搞定,具体如下;
select
order_id as `订单id`
,buyer_id as `客户id`
,cate_name as `购买商品名称`
from ods_orders_table
where event_day=' 20170101'
and cate_name=’ iphone7';
前面说了,Hive SQL的本质是要被解析成MapReduce任务执行的,所以Hive SQL的执行阶段和MapReduce的执行过程是一样的,这也就是为啥你在Hive Cli模式下跑一个Hive SQL语句的时候你会看到如下的关键字;
2020-07-02 09:32:29,206 Stage-1 map = 0%, reduce = 0%
2020-07-02 09:32:38,293 Stage-1 map = 17%, reduce = 0%, Cumulative CPU 8.59 sec
2020-07-02 09:32:43,620 Stage-1 map = 50%, reduce = 0%, Cumulative CPU 27.61 sec
2020-07-02 09:32:46,772 Stage-1 map = 100%, reduce = 0%, Cumulative CPU 54.91 sec
接下来通过图1来聊聊Hive SQL的执行过程,总体也跟MapReduce一样大致分为4个阶段,即输入split分片,Map阶段,Shuffle阶段和Reduce阶段,但是单纯select只是把数据列查询出来,并不涉及重新计算组合分发和分布,所以这里不涉及Shuffle和Reduce,具体流程如下;
输入split分片
:数据仓库建设的时候,通常采用按天对Hive表进行分区,所以利用Hive-SQL限制event_day=' 20170101'
来限制订单表的20170101的分区文件(否则Hive会读取所有订单表的文件,加大hadoop集群的成本和开销不说,查出来的结果也未必是你想要的),读取到20170101分区的订单文件后,对该文件机型split分片处理,分片大小公式为Math.max(minSize, Math.min(maxSize, blockSize)),因此默认情况下,一个split的大小为一个blocksize的大小,hadoop2.x默认为128M,假设20170101 的订单文件为 248MB,那么对于此任务split个数为3 ,其大小分别为:128MB ,120MB,一个split调用一次map函数,即对应一个map的输入,输出个数,所以这里也会切割成2个分片,调用2次Map函数;Map阶段
:Map 任务的个数由分片阶段的 split个数决定,即 20170101订单文件被分成了2个输入分片,那么 Hadoop 集群就会启动 2 个 Map 任务 , 每个 Map 任务接收自己的分片文件,并在 Map 函数中逐行对输入文件进行检查:商品类目是否为 iPhone7?不是的话,将其过滤 ;是的话,获取select语句 中指定的列值作为value输出,并保存到本地文件中 ;Shuffle 和 Reduce 阶段
:此 SQL 任务不涉及数据的重新分发和分布,不需要启动任何的 Reduce 任务 。输出文件
:Hadoop 直接合并 Map 任务的输出文件到输出目录,对于本例,只需将各个 Map 任务
的本地输出合并到输出目录即可 。
group by语句执行图解
统计分析中group by是很常用的一个语法,这里假设销售人员想查一下20170101日购买iPhone7客户在各城市中的分布情况,即哪个城市购买得最多、 哪个城市购买得最少 。 此业务需求的实现Hive SQL如下;
select city as `城市`
count(*) as `iphone7销售数量`
from ods_orders_table
where event_day='20170101'
and cate_name='iphone7'
group by city;
Hive SQL 的 group by 语句涉及数据的重新分发和分布,因此会触发Shuffle和Reduce过程,即其执行过程完整地包含了MapReduce 任务的执行过程,具体流程如图2所示;
输入split分片
:group by 语句的输入文件依然为 day=20170101 的分区文件,其输入分片过程和个数同select 语句,也是被分为大小分别为: 128MB ,120MB 两个分片文件 ;Map阶段
:Hadoop 集群同样启动两个 Map 任务,处理对应的2个分片文件;每个 map 任务处理其对应分片文件中的每行,检查其商品类目是否为 iPhone7 ,如果是,则输出形如<city,1 >的键值对,因为需要按照 city 对订单数目进行统计;Combine 阶段
:Combiner 阶段是可选的,如果指定了Combiner 操作,那么Hadoop会在每个Map任务的本地输出中执行Combiner 操作,其好处是可以去除冗余输出,避免不必要的后续处理和网络传输开销等 。 在此例中 , Map Task1的输出中 <hz,1>出现了两次,那么 Combiner 操作就可以将其合并为 <hz,2>,其功能可以理解为对单个的Map结果先进行一次Reduce; 注意: Combiner 操作是有风险的,使用它的原则是 Combiner 的输出不会影响到 Reduce 计算的最终输入,例如,如果计算只是求总数、最大值和最小值,可以使用Combiner ,但是如果做平均值计算使用了 Combiner ,最终的 Reduce 计算结果就会出错 。Shuffle 阶段
:Map任务的输出必须经过一个名叫 Shuffle 的阶段才能交给 Reducer 处理,Shuffle 过程是MapReduce 的核心,指的是 Map 任务输出到 Reduce 任务输入的整个处理过程 。 完整的Shuffle 过程包含了分区( partition )
、排序( sort )
和分隔( spill )
、复制( copy )
、合并(merge)
等过程,同时 Shuffle 又分为了 Map 端和 Reduce 端的 Shuffle ;对于理解 Hive SQL 的 group by 语句来说,关键的过程实际就两个,即分区和合并,所谓分区,即 Hadoop 如何决定将每个 Map 任务的、每个输出键值对分配到那个 Reduce Task 。 所谓合井,即在一个 Reduce Task 中,如何将来自于多个 Map Task 的同样一个键的值进行合并 。以图2为例, Map Taskl 的输出包含了<hz,2>和<bj,l>两个键值对,那么 Map Task1 应该将上述键值对传递给 Reduce Task1还是 Reduce Task2 处理呢? Hadoop 中最为常用的分区方法是 Hash Partitioner ,即 Hadoop 会对每个键取 hash 值,然后再对此 hash 值按照 Reduce 任务数目取模,从而得到对应的 reducer ,这样保证相同的键,肯定被分配到同一个 Reducr 上,同时 hash 函数也能确保 Map 任务的输出被均匀地分配到所有的 Reduce 任务上。多个 reduce 任务的同样键值对会被 partiton 过程分配到同样的 Reduce Task 上,而merge 操作即将它们的值进行合井,比如对于 key=hz 的键值对, Map Taskl 的输出为<hz,2> , Map Task2 的输出为<hz,l>, Reduce Taskl 的 merge 过程会将其合并为<hz, {2,1}>,从而作为 Reduce Task1 的输入键值对。Reduce 阶段
:对于 group by 语句,Reduce Task 接收形如<hz,{2, l }>的输人,Reduce Task 只需调用 Reduce 函数逻辑将它们汇总即可,对于<hz,{2, l }>,即得到 2+1=3 。 同样如果输入为<hz,{2, 1, 2, 3 } >,则得到 2+1+2+3=8 ,每个 Reduce 任务的输出存到本地文件中 。输出文件
:Hadoop 合并 Reduce Task 任务的输出文件到输出目录,对于本例,只需将各两个Reduce Task 的本地输出合并到输出目录即可。
join语句执行图解
数据分析中还经常需要进行关联分析,也就是需要对两个表进行join 操作,如业务人员希望分析购买iPhone7的客户的年龄分布情况,订单表只包含了客户的ID,客户的年龄保存在另一个买家表中,此时就需要对订单表和客户表进行 join 才能得到分析的结果,相关的 SQL 如下;
select
t1.order_id as `订单id`
,t1.buyer_id as `客户id`
,t2.age as `客户年龄`
from
(
select
order id
,buyer id
from ods_orders_table
where event_day='20170101'
and cate_name='iphone7'
) t1
join
(
select
buyer_id
,age
from ods_buyer_table
where buyer_status='有效'
and event_day='20170101'
) t2
on t1.buyer_id=t2.buyer_id;
注意,其实以上Hive SQL可以规范的写成如下形式,按照上面的写法只是为了更好的理解Hive SQL的执行过程而已,所以不要怀疑我的专业哈;
select
t1.order_id as `订单id`
,t1.buyer_id as `客户id`
,t2.age as `客户年龄`
from ods_orders_table t1
inner join ods_buyer_table t2
on t1.buyer_id=t2.buyer_id
and t2.buyer_status='有效'
and t2.event_day='20170101'
where t1.event_day='20170101'
and t1.cate_name='iphone7'
上述的 join SQL 在 Hadoop 集群中会被拆分成三个 MapReduce 任务 。
- 第一个 MapReduce 任务:就是t1表部分,具体过程实际上就是图1的select 语句部分,不过此时的输出文件仅包含 order_id 和 buyer_id 列;
- 第二个 MapReduce 任务:就是t2表部分,同样类似于图1的select 语句部分,不过此时表变为了ods_buyer_table表,输出列变为了 buyer_id 和 age ,同时过滤条件变为了buyer_status=’有效’ and t2.event_day=‘20170101’;
- 第三个 MapReduce 任务:即t1 和t2表join过程,它将第一和第二的结果文件输出进行关联合并,然后输出;
上述 Hive join 语句的执行大图如图3所示(为了说明方便, 第一个 MapReduce 任务和第二个MapReduce 任务的执行过程大图不再展示,具体请参考图1的 select 语句执行大图,这里直接使用它们的输出文件) 。
Hive SQL的join语句也涉及数据的重新分发和分布,但不同于group by语句的是,group by 语句会根据 group by的列进行数据重分布和分发,而join语句则根据join的列进行数据的重分布和分发(在此即为根据 buyer_id 进行数据重分发和分布),所以join语句也是会触发Shuffle和Reduce过程的,是个完整的MapReduce执行过程,具体流程如下;
输入分片
:join 语句的输入文件包括第一个 MapReduce 任务和第 二个 MapReduce 任务的输出文件,对于 Hadoop 来说,依然会对它们根据文件大小进行分片,假如第一个输出文件的大小为 100MB ,那么会有1个分片:即 100MB 的分片;第二个 MapReduce 任务的输出文件也会按照这样的方式划分分片 ,这里假设为50M,也只有一个分片;Map 阶段
:Hadoop 集群会对两个输出文件的 split 结果数据启动相应数目的 Map Task ,比如第一个输出文件包含了1个 split,那么第一个输出文件会启动1个 map task ,第二个输出文件类似,也会启动一个map task;Shuffle 阶段
: 对于 join 语句, Shuffle 过程主要是 Partition ,即根据其 join 的列进行数据的重分布和分发的过程 ,join 语句 partition 列即为 join 的列,在此为 buyer_id ,因此对于输出文件 1 和2 对应的所有 Map 任务,其输出将会根据 buyer_id 的值进行数据的重新分发和分布 ,join 过程的 key 分发规则类似于 group by 语句,最常用的 partition 方法也为 Hash Partitioner ,即会对每个 join 的键取 hash 值,然后对此 hash 值按照 Reduce 任务数目取模,从而得到对应的 Reducer,这样保证相同的join键,肯定被分配到同一个 reducer 上,从而完成列的关联;图 3中假定, buyer_id 为 111 和 222 的都会按照上述规则被分发到 Reduce Task1 ,而buyer_id 为 333 和 444 的都会被分发到 Reduce Task2 ;Reduce 阶段
:join 语句的 Reduce 过程,是根据 join 键值将其他列关联进来的过程,比如对于 buyer_id=222 的列, Map Task1-1 的order_id=l003 buyer_id=222 的行会被分发到 Reduce Taskl1上,而 Map Task2-1 的 buyer_id=222 age=25 的列也会被分发到 Reduce Task1上(因为它们的buyer_id 都等于 222 ),此时 Reduce Task1 就根据buyer_id 将它们的值关联合并成一行,并写人本地的输出文件中 。
在此需要注意的是,如果 buyer 表中买家存在重复,那么 Reduce 任务的输出也会存在重复, 比如 buyer 表 中存在分别为 buyer_id=222 age=25 和 buyer_id=222 age=26 的两行数据, 那么订单表中的 order_id= l 003 buyer_id=222 的行会被重复两次, 买家表中 buyer_id 重复几行, 输出就会重复几次 ,如下;
order id=1003 buyer id=222 age=25
order id=l003 buyer id=222 age=26
输出文件
:Hadoop 合并 Reduce Task任务的输出文件到输出目录,在本例中,只需将每两个Reduce Task 的本地输出合并到输出 目录即可 。
上述join语句涉及两个表的 join , 那么如果是多表 join 呢?比如还有一个买家粒度为30天的成交汇总表( 比如包含其最近 30 天的成交量、 成交金额等),假设 SQL 语句如下;
select
t1.order_id as `订单id`
,t1.buyer_id as `客户id`
,t2.age as `客户年龄`
,t3.byr_order_count_30d as `30天的成交汇总`
from
(
select
order id
,buyer id
from ods_orders_table
where event_day='20170101'
and cate_name='iphone7'
) t1
(
select
buyer_id
,age
from ods_buyer_table
where buyer_status='有效'
and event_day='20170101'
) t2
on t1.buyer_id=t2.buyer_id
join
(
select
buyer_id
,byr_order_count_30d
from ods_buyer_stat_table
where event_day='20170101'
) t3
on t1.buyer_id=t3.buy_id
此SQL的执行过程和上面类似,只不过多了一个t3表 。 由于三个表都是通过 buyer_id来 join , 因此三个表的行都会根据 buyer_id在 Shuffle 过程进行分发,同一个 buyer_id 的三个表的行都会到一个 Reduce Task 上完成关联并输出结果 。
那么如果t1表和t3表不是通过 buyer_id 关联,假如 t3 表是卖家表, join的SQL如下;
select
t1.order_id as `订单id`
,t1.buyer_id as `客户id`
,t2.age as `客户年龄`
,t3.seller_star_level as `卖家星级`
from
(
select
order id
,buyer id
from ods_orders_table
where event_day='20170101'
and cate_name='iphone7'
) t1
(
select
buyer_id
,age
from ods_buyer_table
where buyer_status='有效'
and event_day='20170101'
) t2
on t1.buyer_id=t2.buyer_id
join
(
select
seller_id
,seller_star_level
from ods_seller_table
where event_day='20170101'
) t3
on t1.seller_id=t3.seller_id
也就是说, t1订单表通过buyer_id和t2买家表关联,通过seller_id和 t3卖家表关联,那么其执行过程又是如何的呢?上述 SQL 实际上可以拆分成两次 join :表t1 和 t2 表通过buyer_id关联, 其结果再和t3表通过seller_id关联, Hadoop 实际上也就是这样处理的;这样实际上数据被 Shuffle和 Reduce 了两次,第 一次 t1 表和 t2 表根据 buyer_id 被分发和关联(第一次 Shuffle 和Reduce),然后其结果再和t3表根据seller_id被分发和关联(第二次 Shuffle 和Reduce ),所以多表关联花费的时间肯定更长,因此其需要完成两次顺序的 Reduce 过程 ,这也就是为什么你要是有相对较复杂的Hive SQL的join操作的时候,你会发现耗时久,而且Hive Cli环境下会多次出现以下MapReduce日志的原因;
Query ID = liuxiaowei_20200703103708_dc57783a-505c-464c-a592-eb193e0dd298
Total jobs = 2
Launching Job 1 out of 2
Number of reduce tasks not specified. Estimated from input data size: 1
In order to change the average load for a reducer (in bytes):
set hive.exec.reducers.bytes.per.reducer=<number>
In order to limit the maximum number of reducers:
set hive.exec.reducers.max=<number>
In order to set a constant number of reducers:
set mapreduce.job.reduces=<number>
Starting Job = job_1587046987432_51449, Tracking URL = http://shucang-10.szanba.ren:8088/proxy/application_1587046987432_51449/
Kill Command = /data/tools/hadoop/current//bin/hadoop job -kill job_1587046987432_51449
Hadoop job information for Stage-1: number of mappers: 6; number of reducers: 1
2020-07-03 10:37:33,958 Stage-1 map = 0%, reduce = 0%
2020-07-03 10:37:48,704 Stage-1 map = 17%, reduce = 0%, Cumulative CPU 10.4 sec
2020-07-03 10:37:49,748 Stage-1 map = 50%, reduce = 0%, Cumulative CPU 29.86 sec
2020-07-03 10:37:50,790 Stage-1 map = 83%, reduce = 0%, Cumulative CPU 48.83 sec
2020-07-03 10:37:51,833 Stage-1 map = 100%, reduce = 0%, Cumulative CPU 57.64 sec
2020-07-03 10:38:03,381 Stage-1 map = 100%, reduce = 100%, Cumulative CPU 60.96 sec
MapReduce Total cumulative CPU time: 1 minutes 0 seconds 960 msec
Ended Job = job_1587046987432_51449
Launching Job 2 out of 2
Number of reduce tasks determined at compile time: 1
In order to change the average load for a reducer (in bytes):
set hive.exec.reducers.bytes.per.reducer=<number>
In order to limit the maximum number of reducers:
set hive.exec.reducers.max=<number>
In order to set a constant number of reducers:
set mapreduce.job.reduces=<number>
Starting Job = job_1587046987432_51450, Tracking URL = http://shucang-10.szanba.ren:8088/proxy/application_1587046987432_51450/
Kill Command = /data/tools/hadoop/current//bin/hadoop job -kill job_1587046987432_51450
Hadoop job information for Stage-2: number of mappers: 1; number of reducers: 1
2020-07-03 10:38:28,422 Stage-2 map = 0%, reduce = 0%
2020-07-03 10:38:44,232 Stage-2 map = 100%, reduce = 0%, Cumulative CPU 3.29 sec
2020-07-03 10:39:00,999 Stage-2 map = 100%, reduce = 100%, Cumulative CPU 7.12 sec
MapReduce Total cumulative CPU time: 7 seconds 120 msec
Ended Job = job_1587046987432_51450
MapReduce Jobs Launched:
Stage-Stage-1: Map: 6 Reduce: 1 Cumulative CPU: 60.96 sec HDFS Read: 5886548 HDFS Write: 2216 SUCCESS
Stage-Stage-2: Map: 1 Reduce: 1 Cumulative CPU: 7.12 sec HDFS Read: 7173 HDFS Write: 1887 SUCCESS
Total MapReduce CPU Time Spent: 1 minutes 8 seconds 80 msec
OK
对于更多表的 join , 均可根据上述过程来完成,读者可以自己分析,在此不做过多说明 ,基本原理都差不多。