四 、Hive逻辑优化

Hive生成逻辑算之后,会对逻辑算子进行优化。优化的目的是为了减少不必要的数据处理、消除冗余运算,以到达提高执行效率的目的,这个过程称为逻辑优化。

4.1 逻辑优化类型

Hive逻辑优化包括以下几点:

1) 谓词下推

谓词下推指的是将外层HiveQL的过滤表达式提前到内层HiveQL,以达到提前过滤,减少数据的处理量。如语句:selecta.* , b.* from a join b on a.id=b.id where a.num>10 and b.num>20过滤条件a.num>10 和b.num>20可以提前到join操作之前进行,提前对a,b进行过滤,这样可以减少join时表的大小。

2) 常量折叠

常量折叠包含三个内容:

(1)折叠表达式。如果表达式全是常量,计算出来该表达式的值并替代原表达式。

(2)逻辑短路。如果布尔表达式包含常量并且可根据常量进行短路操作,则进行短路运算。如2>1and a.id>b.id可直接计算出结果为true.

(3) 表达式常量传递。如a=1 ,b=a,常量1可以传递给b.

3)合并RS算子

将算子DAG中同一条路径上的符合特定条件的两个RS算子合并成一个RS算子,这样可以减少shuffle过程,提高执行速度。这两个RS算子位于路径前面的为parent RS(简称pRS),位于路径后面的为childRS(简称cRS)。合并后cRS算子将会被删除,只剩下pRS算子。pRS和cRS能合并必须同时满足以下条件:
   (1) sort key满足包含关系。如pRS sort key为(k1),cRS的sort key为(k1,k2),两组sortkey满足包含关系,可以合并。由于按照(k1,k2)有序则一定满足按照(k1)有序,因此合并后pRS的sortkey为(k1,k2),cRS将被删除。

  (2) partitionkey满足包含关系。如pRS partition key为(k1,k2),cRSpartition key为(k1),满足包含关系,可以合并。由于按照(k1)进行数据分发一定满足按照(k1,k2)进行分发的,因此合并后pRS的partiton key为(k1),cRS将被删除。

  (3) 排序顺序满足包含关系。如pRS sort key为(k1,k2),sort order为(+,+)(+表示升序),cRSsort key为(k1,k2,k3),sort order为(+,+,-),那么合并后排序顺序为(+,+,-)。

  (4) pRSreduce数量等于cRS reduce的数量后者其中一个为-1(-1表示reduce数量由运行时确定)。合并后RS的数量这两个RS的reduce数量有-1的情况下为-1。

4) join顺序优化

根据用户的STREAMTABLE提示,将大表放在join最后面。hive在join时,会将前n-1个表的数据缓存在内存中,和最后一张表数据做笛卡尔乘积,最后一张表的数据不需要存放内存中。将大表放在join最后面可以减少reduce端的内存压力。

5) 删除多余SEL

将select*类型的SEL删除。

其他优化还包括包括列裁剪、协同优化、group by优化等。

 

4.2 逻辑优化实现

下面介绍几种逻辑优化的具体实现。

4.2.1 谓词下推实现

基本原理:Hive谓词下推的实现类似于选举,选举从算子DAG最底层节点开始。底层节点推荐“候选表达式”给上级节点,其中候选表达式来自FIL算子的谓词表达式(如果该节点是FIL算子)或者该节点的子节点推荐的候选表达式。上级节点对下级节点推荐过来的候选式进行筛选,将符合条件的表达式再往其上级节点推荐,不符合条件的表达式则会在该上级节点和下级节点之间生成一个FIL算子。一直到达最顶层的TS算子节点为止,在TS算子和其子节点之间为推荐给TS算子的表达式生成一个FIL算子,完成整个谓词下推过程。

具体实现:Hive对算子DAG进行遍历,先访问所有子节点,再访问父节点(自底向上)。对每一个被访问的节点,涉及到针对该节点的表达式的“候选表达式” 的计算。计算方法如下:

step1:遍历表达式树的所有节点,找出所有可以成为下推谓词的候选节点,并标记。能成为候选节点的条件如下:1)对于函数节点:非确定性函数不能成为候选节点(如随机函数rand);所有的子节点都是候选节点;函数最多只引用一张表;2)对于字段节点(复杂数据类型引用a.b.c):最多只引用到一张表;3) 对应colum类型:groupby value表达式的引用不能成为候选节点(如selectcount(*) as cnt from a group by a.id having cnt>100中cnt不能作为候选节点)。

step2:提取候选表达式。如果表达式是and运算,从and运算所有子节点中提取候选表达式。否则,如果该表达式在步骤1中被标记为候选节点,则提取该节点作为候选表达式,否则该表达式不能成为候选表达式。

被访问节点的处理逻辑如下:

1)如果是FIL节点,先计算过滤表达式的”候选表达式”,方法和上面讲的相同,然后与子节点推荐表达式的”候选表达式”合并得到该FIL节点的”候选表达式”。

2)如果是JOIN节点,根据join类型不同,有不同的处理方法:

a)inner join

所有的表的表达式均可以下推

b)left outerjoin

对于join on的过滤表达式,左表的表达式不能下推。对于where的表达式,右表表达式不能下推。

c) rightouter join

与leftouter  join情况相反。对于join on的过滤表达式,右表的表达式不能下推。对于where的表达式,左表表达式不能下推。

d) fullouter join

所有表的过滤表达式都不能下推。

3)如果是ScriptOperator 由于脚本是黑盒,淘汰所有候选表达式。即所有下级节点的候选表达式都不会往上推荐,而是在ScriptOperator之后给这些候选表达式生成一个FIL算子。

4)LimitOperator 和ScriptOperator相同。

5) 如果是RS节点,所有候选人都通过,同时会对join进行优化,如select  a.*,b.* from a join b on a.id=b.id where a.n>10

由于b.id=a.id,a.n>10,所以b.id>10。因此可以在表b operator分支上添加一个b.id>10的过滤条件对b进行过滤。

6)如果是TS节点,给所有下级节点推荐过来的表达式生成一个FIL算子。谓词下推过程到此结束。

7)其他节点,计算当前节点上各个子节点推荐表达式的候选表达式,符合条件的上推给上层节点,不符合条件的在当前节点和下级节点之间生成FIL算子。

 4.2.2 常量折叠

前面介绍过,常量折叠优化包括三方面类容:表达式折叠、逻辑短路和表达式常量传递。Hive对算子DAG进行遍历,先访问父节点,所有父节点访问完再访问子节点。对每个被访问的节点进行以上三方面的优化。主要代码位于ConstantPropagateProcFactory#foldExpr方法中,伪代码表示如下:

其中expr为要折叠的表达式,op为当前访问的节点,parentIndex为访问op节点之前的父节点的下标(每个节点有多个父节点),propagate表示该表达式能否进行常量传播,ctx为算子DAG遍历过程中的上下文,主要存放了每个节点的常量表,具体数据结构如下:

Map, Map> opToConstantExprs

常量折叠的具体处理过程如下:

ExprNodeDesc foldExpr(ExprNodeDesc expr,Operator op,
int parentIndex,boolean propagate,ConstantCtx ctx) {
   if(expr为变量){ 
	Operator parent=op.getParents().get(parentIndex)
	ExprNodeDesc colConst=evaluateColumn(expr,parent,ctx)
	if(colConst!=null) {//expr为常量
		return colConst;
       }
   } else if(expr为函数) {
	if(expr为不确定函数) {
	    return expr;//不确定函数不进行常量折叠
	}
        //expr为and函数时,propagatable为true
	boolean propagateNext=propagate&& propagatable(expr);
	for(ExprNodeDesc child:expr.getChildren()) {
	    newChildren.add(foldExpr(child,op,propagateNext);
	}	
	if(newChildren全部是常量) {
	    ExprNodeDesc constant=expr.value(newChildren);
	    return constant;
        }
        ExprNodeDesc cut= shortcutFunction(expr.getUDF(),newChildren);
        if(cut!=null)
	    return cut;
        if(propagate)
	   propagate(expr,newChildren);//=和is null运算常量传播
   }
   return expr;
}


step1:如果expr为变量,调用evaluateColumn方法计算该变量的值,如果是常量则返回常量值,否则返回expr变量。

step2:如果expr为非确定函数,则终止常量折叠运算,返回expr。

step3:如果expr为确定函数,首先对函数的参数进行常量折叠运算,得到得到后的新参数newChildren。如果参数全部是常量,直接计算该函数的值并返回。否则step4。

step4:调用shortcutFunction方法进行逻辑短路运算。shortcutFunction方法对and、or、case和when函数进行逻辑短路运算。如果该表达式无法进行短路运算则执行step5。

step5:如果propagate为true,则对该表达式进行常量传播。目前只有FIL算子的表达式可以进行常量传播。常量传播伪代码如下:

propagate(GenericUDF exprUdf,List chidren,Operator op,ConstantCtx ctx) {
    //eg:a=1,那么变量a将作为常量1存放到常量表中。
    if(exprUdf为equal函数) {
        ExprNodeColumn vc=children中为变量的参数
        ExprNodeConstantDesc const=children中为常量的参数
        //将变量名称和其对应常量值存放到op节点的常量表中。
        ctx.put2ContantMap(op, vc.getColumnInfo(),constant);
   } else if(exprUdf为is null函数) {
        ExprNodeColumn vc=children.get(0)
        ctx.put2ConstantMap(op,vc.getColumnInfo(),null)
   }
}
这段代码只会针对FIL算子的表达式执行,核心思想是将表达式中等号右边的常量代替等号左边的变量。





你可能感兴趣的:(hive)