三、 Hive 逻辑算子及其生成(下)

来源:互联网 发布:php网站流量统计系统 编辑:程序博客网 时间:2024/06/01 10:43

3.3算子DAG的生成

   在第一章1.4节我们提到Hive首先将抽象语法树AST转换成查询块(QueryBlock),然后在将QueryBlock转化成算子DAG。

3.3.1QueryBlock生成

1) QB数据结构

QueryBlock数据结构定义在QB类中,QB的主要成员变量如下:

HashMap<String, String> aliasToTabs;HashMap<String, QBExpr> aliasToSubq;HashMap<String, Map<String, String>> aliasToProps;List<String> aliases; QBParseInfo qbp; QBMetaData qbm; QBJoinTree qbjoin;

aliasToTabs: 表别名和表名映射关系。

aliasToSubq:子查询语句别名和子查询语句映射关系。其中QBExpr表示子查询语句的集合运算,Hive中定义了以下几种运算类型:

(1) NULLOP:表示单个子查询语句,没有任何运算。其QBExpr#qb字段表示子查询语句的QB。

(2) UNION:表示union运算,包含两个QBExpr对象,分别表示参与运算的两个子查询语句。

(3) INTERSECT:表示表的交集运算,目前尚未实现。

(4) EXCEPT:表示表的差集运算,目前尚未实现。

aliasToProps:表别名和表属性映射关系。

aliases:语句中所有表和子语句的别名。

qbp:QBParseInfo类型,记录该语句相关的信息。主要包括该语句是否为子查询语句、表别名、join语句语法树、where语句语法树、select语句语法树以及body语句的语法树(groupby、where,limit,clusterby,order by等语句)。insert语句还包含insert 目的数据源表名称列表。注意:由于HiveQL insert语句中可以包含多个insert分支(参考第二章insert语法树部分的介绍),每个分支包含select语句以及body语句,因此QBParseInfo使用hash表存放每个分支的select语句语法树以及body语句语法树。其中,hash表的key是分支的名称(Hive称之为dest,即insert的目的地名称),value为语法树。

qbm:QBMetaData类型,存放语句中表和分区的元数据信息。

qbjoin:QBJoinTree类型,记录Join树结构信息。第二章介绍过,join树是一颗左型树,QBJoinTree与之对应也是一棵左型树。关于QBJoinTree的具体介绍,参见下文。

2)QB的生成

Hive通过遍历抽象语法树(AST)生成QB对象,这一部分的核心代码位于SemanticAnalyzer#doPhase1方法中,伪代码如下:

doPhase1(ASTNode ast,dest,QB qb){QBParseInfo qbb=qb.getParseInfo()//存放语句相关信息Boolean recursion=false;//ast节点是否需要递归处理switch(ast.nodeType) {case TOK_DESTINATION:  dest=newInsertName(); //生成新分支名称  qpb.setDest(dest,ast) //保存当前insert分支的目标数据源case TOK_SELECT:  //保存当前insert分支的select表达式  qpb.setSelExpr(dest,ast)  //保存当前insert分支的聚合函数表达式  qpb.setAggregationExpr(dest,getAggFunc(ast))case TOK_WHERE:  //保存当前insert分支的where表达式  qbp.setWhereExpr(dest,ast)case TOK_GROUPBY:  //保存当前insert分支的group by表达式  qbp.setGroupByExpr(dest,ast)case TOK_LIMIT:  //保存目标insert分支的limit语句  qbp.setDestLimit(dest,ast)case 其他body语句节点(sortby,orderby等):  保存相应内容到当前的insert分支下,这里省略。case TOK_FROM:   fromSrc=ast.getChild(0)   if(fromSrc==TOK_TABREF) {     processTable(qb,fromSrc)   } else if(fromSrc==TOK_SUBQUERY) {     processSubQuery(qb,fromSrc)   } else if(isJoin(fromSrc) {     processJoin(qb,frm)     qb.setJoinExpr(fromSrc)//保存join AST树   } else 其他fromSrc …case 其他节点:     //其他节点递归处理     recursion=true } if(recursion)    for(child:ast.getChildren()) {       doPhase1(child,dest,qb)    }}

doPhase1方法根据ast节点的类型提取相应节点的信息,保存到qb对象中。如对于TOK_SELECT节点,保存select表达式以及聚合函数信息。遇到TOK_DESTINATION节点时,生成新分支名称,同时保存目标数据源信息(目标数据源表名称,临时文件路径等)。后续TOK_SELECT节点和body语句节点(TOK_GROUPBY,TOK_LIMIT等)的信息保存在该Insert分支下面。

对于TOK_FROM节点的处理比较复杂。第二章讲过,from的数据源包含多种类型, doPhase1对不同类型的数据来源处理方式不同:

(1) from TOK_TABREF

对于表数据源,通过processTable方法进行处理。processTable方法从语法树中提取表名称和别名等内容保存在QB对象相关数据结构中。

(2) from TOK_SUBQUERY

对于子查询语句,通过processSubQuery方法进行处理。processSubQuery方法主要代码如下:

processSubQuery(QB qb,ASTNode node) {ASTNode subq=node.getChild(0)String  alias=node.getChild(1)            QBExpr qbexpr=new QBExpr(alias)doPhase1QBExpr(subq,qbexpr)        qb.setSubqAlias(alias,qbexpr)        qb.addAlias(alias)}

processSubQuery方法调用doPhase1QBExpr方法生成subquery的QBExpr对象,然后存放到qb对象中。doPhase1QBExpr方法伪代码如下:

doPhase1QBExpr(ASTNode subq,QBExpr qbexpr) {switch(subq.nodeType) {case TOK_QUERY:   QB qb=new QB()//构造子查询的QB对象   doPhase1(subq,qb,init_dest)//解析QB信息           qb.setOpcode(NULLOP)           qbexpr.setQB(qb)case TOK_UNIONALL:   QBExpr leftExpr=new QBExpr()   QBExpr rightExpr=new QBExpr()   //解析union左查询子语句QB   doPhase1QBExpr(subq.getChild(0),leftExpr)           //解析union右查询子语句QB           doPhase1QBExpr(subq.getChild(1),rightExpr)           qbexpr.setQBExpr1(leftExpr)           qbexpr.setQBExpr2(rightExr)   }}

(3)from JOIN

 对于JOIN类型的数据源,通过processJoin方法进行处理。processJoin伪代码如下:

processJoin(QB qb,ASTNode join) {for(child:join.getChildren()) {//遍历join的所有子节点   if(child.nodeType==TOK_TABREF) {      processTable(qb,child)          } else if(child.nodeType==TOK_SUBQUERY) {      processSubQuery(qb,child)         else if(isJoin(child.nodeType) {     processJoin(qb,child)         }       }}

processJoin遍历join语法树的所有数据源,调用数据源对于的方法进行处理。实际上,processJoin只是将join的所有数据源存放到QB对象中,在算子生成阶段再做进一步处理。

3)元数据信息处理

Hive生成QB对象后,接着调用getMetaData方法处理QB中所有表的元数据信息。getMetaData方法主要完成以下功能:

(1)  将QB中的对with语句的引用以替换成with语句的AST树,with语句语法树作为QB的subquery。

(2)  检查表是否offline,如果表已经下线,则认为查询非法,抛出异常。

(3)  将QB中对视图的引用替换成视图对应的语法树,视图作为QB的subquery。

(4)将每张表的元数据信息存放在QB#qbm中,表的元数据信息由org.apache.hadoop.hive.ql.metadata.Table对象表示。通过Table对象可以获取表的名称、数据库名称、InputFormat class、OutputFormat class以及字段信息内容等。

(5)处理子查询的元数据信息。通过递归调用getMetaData()进行处理。

 (6) 处理查询语句的目的数据源。目的数据源存放查询语句的结果,包括表和文件目录两种。getMetaData提取目的数据源的元数据信息以及文件目录信息到QB#qbm字段中。


3.3.2 算子DAG的生成

Hive遍历抽象语法树(AST)生成QB对象,然后将QB对象生成算子DAG。算子DAG生成的代码在SemanticAnalyzer#genPlan(QB)方法中,主要流程如下图所示:

 图3-9 算子DAG生成流程图

genPlan(QB)方法依次生成流程图中个模块的算子DAG,然后拼接得到整个语句的算子DAG。

3.3.2.1 生成子查询算子DAG

genPlan对QB中所有子查询QBExpr调用genPlan(QB,QBExpr)方法生成对应的算子DAG。genPlan(QB,QBExpr)伪代码实现如下:

genPlan(QB parent,QBExpr qbexpr) {        if(qbexpr.getOpcode()==NULLOP) //单一查询语句           return genPlan(qbexpr.getQB())        else if(qbexpr.getOpcode()==UNION) {//UNION查询语句          //生成UNION左查询语句DAG           opLeft=genPlan(parent,qbexpr.getQBExpr1())          //生成UNION右边查询语句DAG           opRight=genPlan(parent,qbexpr.getQBExpr2())          return genUnionPlan(opLeft,opRight)        }     return null;//其他集合运算如except等Hive不支持}

这里genPlan方法的返回值是算子DAG最后一个节点。gentPlan(QB,QBExpr)调用genUnionPlan(opLeft,opRight)方法生成union语句的算子DAG。opLeft和opRight分别是union语句左右查询语句算子树的最后一个节点。如果opLeft和opRight都是单一查询语句(即QBExpr#getOpcode()为NULLOP),那么gentUnionPlan生成一个UNION算子,opLeft和opRight算子作为UNION算子的父亲点,如下图所示。

   图3-10 union算子DAG

第二章语法树部分介绍过,union的语法树是一颗左型树,即左子节可以嵌套包含一个union节点,因此,opLeft算子树可以嵌套包含一棵union算子树。genUnionPlan方法会将rightOp作为opLeft union算子树的子节点合并到opLeft union算子树中去,如下图所示:

                               图3-11 union算子分支合并

3.3.2.2 生成源表的算子DAG

这一步对QB中的所有源表生成TS算子,例如select* from a join b,会分别给源表a和源表b生成TS算子。同时会将TS算子存放到topOps列表中,供后面使用,具体实现参考genTablePlan方法。

步骤1)和步骤2)生成的都是From数据源的算子DAG,这些DAG保存在Map<String,Operator> aliasToOpInfo中,后面步骤会用到。

PTF函数和LateralView的算子DAG生成本文暂不做介绍。

3.3.2.3生成Join算子DAG

Hive生成Join算子DAG的过程分两个阶段:生成QBJoinTree和生成Join算子DAG。

1) 生成QBJoinTree

Hive使用QBJoinTree对象描述Join的数据结构。第二章介绍过,Join语法树是一棵左型树,因此QBJoinTree对象与之对应也是一棵左型树。QBJoinTree的主要数据结构如下:

String leftAlias;String[] rightAliases;String[] leftAliases;QBJoinTree joinSrc;String[] baseSrc;JoinCond[] joinCond;ArrayList<ArrayList<ASTNode>> expressions;ArrayList<ArrayList<ASTNode>> filters

其中:

leftAlias:当Join Tree的左节点是叶子节点时(即不嵌套包含另外一棵Join Tree),leftAlias为join左数据源(可以是表,subquery等)的别名;否则,该字段为null。

rightAliases:Join Tree右数据源的别名。

leftAliases:JoinTree中除了最后一个数据源节点(在Join Tree的最右边)以外的所有数据源的别名。

joinSrc:当Join Tree的左节点为非叶子节点(即嵌套包含一棵Join Tree)时,该字段为左节点对应的QBTree对象;否则该字段为null。

baseSrc:Join Tree左右节点的数据源别名,为长度为2的数组。如果左节点为非叶子节点,那么baseSrc[0]为null。

joinCond:表示Join Tree的join类型。join类型包括left outer join、right outer join、full outer join、left semijoin以及inner join。

expressions:join on链接表达式。如joinon a.id=b.id。

filters:join on过滤表达式。如joinon a.id>100。

Hive调用SemanticAnalyzer#genJoinTree方法深度优先遍历Join语法树,生成对应的QBJoinTree对象,具体细节请参考这一部分的代码。

2) Join 算子DAG的生成

 这一步调用genJoinOperator方法生成Join算子DAG。genJoinOperator实现的伪代码如下:

genJoinOperator(QB qb,QBJoinTree joinTree) {QBJoinTree leftChild=joinTree.joinSrc        //join算子的左右父分支         Operator joinLeft,joinRight; if(leftChild==null) {    joinLeft = aliasToOpInfo.get(joinTree.baseSrc[0])        }else {            //生成左节点算子DAG    joinLeft =genJoinOperator(qb,leftChild)    joinRight =aliasToOpInfo.get(joinTree.baseSrc[1])    joinLeft =genNotNullFilter(qb,joinLeft,joinTree)    joinLeft =genReduceSinkOperator(qb,joinLeft,joinTree)    joinRight = genNotNullFilter(qb,joinRight,joinTree)    joinRight =genReduceSinkOperator(qb,joinRight,joinTree)    return genJoinOp(joinTree, joinLeft, joinRight)}

step1:取QBJoinTree的左子树(即joinSrc字段),如果为空,表明左节点是非叶子节点,执行step 2;否则,左节点是一棵QBTree, 执行setp3。

step2:从aliasToOpInfo中获取左节点数据源对应的算子DAG存放到joinLeft变量。如果左节点数据源是表,那么joinLeft是该表数据源的算子DAG;如果该数据源是subquery,那么joinLeft是该subquery生成的DAG;其他数据源类似。执行setp4。

step3:递归调用genJoinOperator生成该子QBTree的算子DAG,存放到joinLeft中。执行setp4。

step4: 从aliasToOpInfo中获取右节点数据源对应的算子DAG存放到joinRight变量。执行step5。

step5:首先,给joinLeft算子添加FilterOperater过滤左分支join key为null的数据。然后再添加RS算子。对joinRight执行同样的操作。执行step6。

step6:生成Join算子,并将joinLeft和joinRight作为Join算子的父节点。

以下面例子分析join算子DAG的生成过程:

select * from a join b on a.id=b.id
HiveQL语句对应QBJoinTree示意图如图3-11。该语句的QBJoinTree包含两个叶子节点:table a和table b。Join算子DAG的生成流程如下:

     图3-12 QBJoinTree

                                          图3-13 join算子DAG生成

首先执行step1、step2,从aliasToOpInfo中获取table a和table b的TS算子;由于然后执行step4,添加FIL算子,过滤a.id为null以及b.id为null的记录;接着添加RS算子,RS算子按照id进行数据分发以及排序;最后执行step6,生成Join算子,joinLeft和joinRight作为其父节点,完成join算子树的生成。

3.3.2.4生成Body语句算子DAG

这一阶段生成select语句和body语句的算子DAG,分三个阶段:where语句算子DAG生成、groupby语句算子DAG生成以及 group by之外其他语句算子DAG生成。Hive中可以包含多个insert分支,每个分支都包含select语句和body语句,Hive分别给每个insert分支生成算子DAG。

1) where语句的算子DAG生成

 Hive将where语句生成FIL算子,过滤数据源中不符合where表达式的记录。将FIL算子的生成放在第一个,是为了提前进行数据过滤,减少后续数据处理量。生成FIL算子的代码位于SemanticAnalyzer#genFilterPlan方法中。

2) group by语句算子DAG生成

Hive中提供了group by优化的两个重要配置hive.map.aggr和hive.groupby.skewindata。其中hive.map.aggr(默认true)控制是否需要map端group by聚合,即hashgroup by。hive.groupby.skewindata(默认false)用于优化数据倾斜导致的性能问题。如果该参数为true,在生成group by算子DAG时,会生成两个阶段group by过程:第一阶段group by执行局部聚合并对数据进行均匀分发,保证数据分布的均衡性;第二阶段执行最终的group by操作。

group by算子DAG根据这两个参数配置的不同,生成的DAG分以下四种情况:

(1) hive.map.aggr=true && hive.groupby.skewindata=true

stage1为第一阶段的group by运算,主要完成数据的均匀分发。第一个GBY算子为hash based GBY,实现局部聚合,减少数据的输出量。紧接着,RS算子进行数据分发和排序。数据分发和排序的规则如下:

a) 如果聚合函数中不包含distinct,那么数据随机分发。

b) 如果聚合函数中包含distinct,那么数据按照group by key+distinct key分发。

第二个GBY算子为sort base GBY,对数据进行局部聚合。然后进入stage2,通过RS算子按照groupby key进行数据分发和排序。 最后执行GBY算子,执行合并操作,得到最终结果。

(2) hive.map.aggr=true && hive.groupby.skewindata=false

这种情况下只有state1的算子,DAG如下图所示。其中hash based GBY算子执行map端局部聚合运算,RS算子对数据进行分发和排序,

分发和排序的规则为:根据group by key进行数据分发,group by key+distinct key进行排序。最后执行sort based GBY进行数据合并运算。

(3)hive.map.aggr=false && hive.groupby.skewindata=true

和(1)相比,减少了map端的hashbased GBY,其他情况和(1)相同。

(4)hive.map.aggr=false && hive.groupby.skewindata=false

  和(2)相比,减少了map端的hash based GBY,其他情况和(2)相同。

  

3)group by之外其他语句算子DAG生成

这一阶段完成对select、having、sortby、order by、distribute by、limit以及cluster by语句的算子DAG的生成。代码位于SemanticAnalyzer#genPostGroupByBodyPlan方法中,流程图如下:


图3-14 group by之外语句的算子DAG生成

首先处理Having语句,如果group by后面有having语句,则调用genHavingPlan方法,给having语句生成FIL算子。

其次生成select算子,select算子的生成代码位于genSelectPlan方法中。

然后,判断语句中是否存在sort by、order by、distriubteby和cluster by这些与数据排序分区相关的语句。如果存在,则生成相应的RS算子,RS算子生成的代码位于genReduceSinkPlan中。

接着,生成Limit算子,如果存在limit语句,则生成相应的LIM算子。LIM算子生成的代码位于genLimitMapRedPlan方法中。

最后,判断是否为subquery子查询语句,如果是则结束流程;否则生成FS算子,输出数据。


 









0 0
原创粉丝点击