Hive 基于规则的优化和基于代价的优化

本文假定读者对执行计划,普通 join 执行,Map join,Bucket Map join,SMB((Sort Merge Bucket) join, Skew Join 的执行过程比较熟悉。

背景知识

Hive 执行计划解释示例
Hive Bucket Table 的功能和使用方法详解
Hive 使用 List Bucketing 表解决数据倾斜问题

1. 基于规则的优化

基于规则的优化(Rule Based Optimization):预先定义一些规则,按这些规则进行优化,效率更高。
不同的 SQL 处理器根据不同的引擎有不同的规则。如 Oracle 等传统数据库有使用索引的很多规则,这些规则在大数据框架里不适用。大数据计算框架里也有很多规则,传统的数据库不适用。

常见的基于规则的优化的规则有:谓词下推、常量上推和 Limit 限定。

1.1 PPD – Predicate Push Down(谓词下推):

谓词下推包含两个内容,第 1 个是检索条件下推,第 2 个是查询字段下推。举例如下:
orders: 订单表,每个订单一条记录
lineitem:订单明细表,每个订单中每个物品一条记录,按 orderkey 和 orders 表关联。

select  c.name, sum(quatity),sum(totalprice) 
from (
     select l.quatity, l.totalprice, o.custkey
     from lineitem l join orders o
     on (l.orderkey = o.orderkey)
 ) temp join customer c on (temp.custkey = c.custkey)
 where temp.o_order_date='20210101' and temp.custkey='001'
 group by c.name ;

以上查询是查询某一个客户编号的客户名称,和 2021 年买的所有商品的数量和金额。

1.1.1 检索条件下推:

检索条件下推就是尽量把检索条件尽可能的推到里面。
谓词下推后的操作类似于以下 SQL:

select  c.name, sum(quatity),sum(totalprice) 
from (
     select l.quatity, l.totalprice, o.custkey
     from lineitem l 
     join 
     (select * 
          from orders 
          where o_order_date='20210101' and custkey='001'
     ) o
     on (l.orderkey = o.orderkey)
 ) temp join customer c on (temp.custkey = c.custkey)
 group by c.name ;

谓词下推后,orders 表中参加 join 计算的记录数减少,提高了计算效率。

1.1.2 查询字段下推

查询字段下推是指尽量在表扫描中查询需要的字段。
优点:

  1. 列存储的表可以只从文件里读取相应的列,提高读取速度。
  2. 可以减少参加计算的数据量,提高计算速度。
 select  c.name, sum(quatity),sum(totalprice) 
from (
     select l.quatity, l.totalprice, o.custkey
     from lineitem l join orders o
     on (l.orderkey = o.orderkey)
 ) temp join customer c on (temp.custkey = c.custkey)
 where temp.o_order_date='20210101' and temp.custkey='001'
 group by c.name ;

如果在表扫描中不是检索所有的字段,而是仅检索需要的字段。需要的字段包括 select 中的字段,谓词判断中用到的字段,关联条件用到的字段,group by 用到的字段。
在表扫描的时候只检索的字段如下:
lineitem: orderkey, quatity,totalprice。
orders: orderkey,o_order_date,custkey。
customer:custkey,name 字段。

同样的道理,如果子查询中查询的字段,在上层查询用不到,在子查询中也可以去掉。

1.1.1.3 关联条件的下推
select * from A a join B b on a.id=b.a_id and a.id=1

on 部分的关联条件下推到 A 表的遍历中。

Filter Set 下推

如以下语句,先求两张表的 union all,再把 union all 的结果执行过滤。

select * from (
  (select ws_item_sk item, ws_quantity quantity from web_sales) 
  union all 
  (select ss_item_sk item, ss_quantity quantity from store_sales)
) t where item='22144';

改写为以下语句,在 table scan 的时候进行过滤:

select * from (
  (select ws_item_sk item, ws_quantity quantity from web_sales where ws_item_sk='22144') 
  union all 
  (select ss_item_sk item, ss_quantity quantity from store_sales where ss_item_sk='22144')
) t;

1.2 汇聚去除

create table t1(c1 string);
alter table t1 add constraint t1_pk primary key(c1) disable novalidate rely;
insert into t1 values("1");
explain select distinct c1 from t1;

可以看到,没有 group by 操作。因为 t1 中 c1 是主键,所以 distinct 可以直接去掉。

STAGE DEPENDENCIES:
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-0
    Fetch Operator
      limit: -1
      Processor Tree:
        TableScan
          alias: t1
          Statistics: Num rows: 1 Data size: 1 Basic stats: COMPLETE Column stats: NONE
          Select Operator
            expressions: c1 (type: string)
            outputColumnNames: _col0
            Statistics: Num rows: 1 Data size: 1 Basic stats: COMPLETE Column stats: NONE
            ListSink

Time taken: 0.201 seconds, Fetched: 17 row(s)

1.3 投影去除 Project Remove

select a_id,  b_id ,c.id c_id from (
select a.id a_id, a.name a_name, b.id b_id, b.name b_name from a join b on a.id = b.id
) t_ab 
join c on a_id = c.id

因为最终结果不需要 a_name, b_name ,所以中间的查询 t_ab 可以把 a_name, b_name 部分去掉。

1.4 排序去除

如果底层的数据已经有序,则上层的排序可以去除。

1.5 常量上推

常量上推就是把常量推到上面的查询中,越往后计算越好。

select a_c1, b_c1, name, xxx from (
  select a.a_c1,b.b_c1,'fix value'  name 
  from a join b 
  on xxx 
  where xxx
) t join c on xxx where xxx

‘fix value’ 这个字段在子查询中是一个常量,可以推到上面的查询中,改写之后SQL以下 :

select a_c1, b_c1, 'fix value' name, xxx from (
  select a.a_c1,b.b_c1 from a join b on xxx where xxx
) join c on xxx where xxx

1.6 检索条件的传递

以下语句查询 某个客户2021年1月1日的订单数量。

select count(1) cnt 
from orders o join customer c on (o.custkey = c.custkey)
where o.o_order_date='20210101' and c.custkey='001';

在遍历 orders 的记录时,自动加上 custkey = ‘001’ 的条件。

1.7 Limit 限定

select  * from web_sales join web_site on ws_web_site_sk = web_site_sk and web_site_sk=10 limit 10;

由于最多只保留 10 个结果,可以让每个 reduce 都限定输出 10 条数据后结束。

1.8 or 转换为 in

如此 SQL 查找特定 100 个用户的订单数量

select count(1) from orders where custkey = '001' or custkey = '002' oror custkey = '100';

如果以上 SQL,custkey 的基数很大,那么绝大部分会判断 100 次。
当按 custkey 的 or 的数量大于一定阈值时(默认 31),可以转换为 in,可以执行的更高效。以上 SQL 改写为:

select count(1) from orders where custkey in ( '001', '002' ,,'100');

In 的条目组成一个 hash 表,每个 orders 的记录,根据 custkey 的值,判断 hash 表里是否有记录,只用判断一次。

1.9 Distinct 汇聚改写

select count(distinct c1) from t1;
Map 对 c1 进行 group by,然后按 c1 进行 partition 发往 reduce 2。c1 相同的记录发往同一个 reduce。
Reduce 2把接收的数据再进行 group by 汇聚,这步完成去重操作。然后进行 count(_col0) 操作,输出本 reduce 中 distinct 的个数。
Reduce 3 把各 reduce2 的结果进行 partial 汇聚操作。

hive> explain select count(distinct c1) from t1;
OK
Plan optimized by CBO.

Vertex dependency in root stage
Reducer 2 <- Map 1 (SIMPLE_EDGE)
Reducer 3 <- Reducer 2 (CUSTOM_SIMPLE_EDGE)

Stage-0
  Fetch Operator
    limit:-1
    Stage-1
      Reducer 3
      File Output Operator [FS_11]
        Group By Operator [GBY_9] (rows=1 width=8)
          Output:["_col0"],aggregations:["count(VALUE._col0)"]
        <-Reducer 2 [CUSTOM_SIMPLE_EDGE]
          PARTITION_ONLY_SHUFFLE [RS_8]
            Group By Operator [GBY_7] (rows=1 width=8)
              Output:["_col0"],aggregations:["count(_col0)"]
              Group By Operator [GBY_4] (rows=1 width=22540)
                Output:["_col0"],keys:KEY._col0
              <-Map 1 [SIMPLE_EDGE] vectorized
                SHUFFLE [RS_14]
                  PartitionCols:_col0
                  Group By Operator [GBY_13] (rows=1 width=22540)
                    Output:["_col0"],keys:c1
                    Select Operator [SEL_12] (rows=1 width=22540)
                      Output:["c1"]
                      TableScan [TS_0] (rows=1 width=22540)
                        test@t1,t1,Tbl:COMPLETE,Col:NONE,Output:["c1"]

基于代价的优化

基于代价的优化是比较不同的执行计划,选择代价低的执行计划执行。

设置 hive.cbo.enable=true 开启基于代价的优化。

基于代价的优化用于 JOIN 之间的排序和 join 算法的选择,依靠统计信息。

JOIN 顺序对执行效率有比较大的影响,举例说明如下:

select  c.name, sum(quatity),sum(totalprice) 
from (
     select l.quatity, l.totalprice, o.custkey
     from lineitem l join orders o
     on (l.orderkey = o.orderkey)
 ) temp join customer c on (temp.custkey = c.custkey)
 where  temp.custkey='001'
 group by c.name ;

如 lineitem 有 10 万条记录,orders有 1 万条记录。customer 有100 条,按 custkey=‘001’ 过滤后剩 1 条。

  • 方案 1
    inter1 = lineitem JOIN orders ,inter1 有10 万条记录。
    result = inter1 join customer, 10 万条 记录 join 1 条 得到结果
  • 方案 2
    inter1 = customer join orders,结果仅有 100 条
    result = inter1 join lineitem

方案 1 第 1 步两个大表进行 join 计算,需要两个表的数据都需要 shuffle 到 reduce。
方案 2 每一个 join 都是一个大表关联一个小表,都可以用 map join 完成。

基于规则的优化考虑以下因素:

基于规则的优化考虑以下因素:

  1. CPU 使用量
  2. IO 使用量
  3. 计算产生的记录数(不同的算法,产生的中间计算结果的数量不相等)
  4. 记录平均大小

记录平均大小和记录数用来估计存储需要的内存,可以决定是否可以采用某些 join 算法,如 Map/ Bucket join。

Calcite 默认使用火山优化器(Volcano optimizer)来计算不同执行计划的代价。火山代价(VolcanoCost)仅用记录数来评判执行计划是否高效。

HiveCost 是 Hive 的代价实现,该代价包括 CPU, I/O, 记录数和记录平均大小。HiveCost 认为 CPU + I/O 优先级更高。

计算代价时使用的代价变量

  • Hr - 从 HDFS 读 1 字节花费的代价(纳秒数)
  • Hw - 往 HDFS 写 1 字节花费的代价(纳秒数)
  • Lr - 从本地文件系统读 1 字节花费的代价(纳秒数)
  • Lw - 往本地文件系统写 1 字节花费的代价(纳秒数)
  • NEt - 在网络上任意两个节点传输 1 个字节的代价(纳秒数)
  • T® - 关系 R 记录数
  • Tsz - 记录的平均大小
  • V(R, a) - 关系 R 中 属性 a 不同值的数量。
  • CPUc - 做一次比较的 cpu 代价(纳秒数)

代价计算假设

    1. 磁盘, HDFS, 网络读写比其他代价更高
    1. 计算 I/O 代价时,不考虑硬件的类型,一次 IO 操作的数据大小,顺序读写还是随机读写,磁盘一个 block 的大小,存储分布情况。
  • 所有的记录数大小一样。
  • 不同服务器之间做 shuffle 时,不考虑部分下游 task 在本机时不用传输数据的情况。
  • 对应 CPU 代价,仅考虑比较代价。假定一次比较花费 1 纳秒。
  • 不考虑 TEZ 容器重用。
  • HDFS 读的代价是本地读代价的 1.5 倍。HDFS 写的代价是本地写代价的 10 倍。

代价的变量设置(可以通过 Hive 参数调整)

  • CPUc = 1 纳秒
  • NEt = 150 * CPUc 纳秒
  • Lw = 4 * NEt
  • Lr = 4 * NEt
  • Hw = 10 * Lw
  • Hr = 1.5 * Lr

各种操作的代价

参考 HiveOnTezCostModel.java

Table Scan 的代价

T® = 记录数
Tsz = 记录平均大小
V(R, a) = 从 meta 获取
CPU Usage = 0
IO Usage = Hr * T® * Tsz. (从 HDFS 读一字节的开销 * 记录数 * 平均记录大小)

Filter

CPU Usage = T® * CPUc
IO Usage = 0

Select

CPU Usage = 0
IO Usage = 0

Common Join

R1 join R2
CPU Usage = 每张表的排序代价 + 两个排序流的 merge 代价 = [(T(R1) * Log(T(R1) * CPUc + T(R2) * Log(T(R2) * CPUc) ] + [(T(R1) + T(R2) ) * CPUc]

IO Usage = Map 写 shuffle 数据到本地文件系统,Map 从本地文件系统读 shuffle 数据, shuffle 数据 通过网络从 Map 服务器传输到 reduce 所在的服务器 = Lw * (T(R1) * Tsz1 + T(R2) * Tsz2) + Lr * (T(R1) * Tsz1 + T(R2) * Tsz2) + NEt * (T(R1) * Tsz1 + T(R2) * Tsz2)

Map Join

R1 JOIN R2,R1是小表,R2 是大表
CPU Usage = 小表构建 HashTable 开销 + 大表 Join 的开销 = T(R1) + T(R2) * CPUc

IO Usage = 传输一次小表的代价 * Map 的数量 = NEt * (T(R1) * Tsz1) * Map 的数量
假定大表数据在本地,不需要传输。存算分离场景是否也要计算大表 IO 开销?

Bucket Map Join

R1 JOIN R2 on R1.c1 = R2.c2。R1 按 c1 bucket,R2 按c2 bucket。
一张表的 bucket 的数量是另一张的整数倍。

CPU Usage = 小表构建 HashTable 开销 + 大表 Join 的开销 = T(R1) * CPUc + T(R2) * CPUc
IO Usage 和 Map Join 一样

SMB JOIN(Sort Merge Bucket Join)

数据都有序,小表不需要构建 Hash Table。
CPU Usage = (T(R1) + T(R2)) * CPUc
IO Usage 和 Map Join 一样

Skew Join

查询重写为两个 join 的结果 union 在一起。计算各自 join 的代价

Distinct/ Group by

CPU Usage = 排序的代价 + 分组的代价 = (T® * log T®) * CPUc

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值