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算子的表达式执行,核心思想是将表达式中等号右边的常量代替等号左边的变量。