目录
2.2 Phase2 SQL基本组成单元QueryBlock
2.3 Phase3 逻辑操作符Operator和逻辑操作树Operator Tree
2.5 Phase5 OperatorTree生成MapReduce Job的过程
文章主体源于美团网技术陈纯大作,值得拥有。原文在美团技术团队中已被删除。梳理了其中一些关键内容。并添加了一些自己的理解。
其中一部分图来自Recruit Technologies的slice:Internal Hive,一些看不懂的可以去参考一下原文(日文)。
1. MapReduce实现基本SQL操作的原理
详细讲解SQL编译为MapReduce之前,我们先来看看MapReduce框架实现SQL基本操作的原理
1.1 Join的实现原理
select u.name, o.orderid from order o join user u on o.uid = u.uid;
在map的输出value中为不同表的数据打上tag标记,在reduce阶段根据tag判断数据来源。MapReduce的过程如下(这里只是说明最基本的Join的实现,还有其他的实现方式)
1.2 Group By的实现原理
select rank, isonline, count(*) from city group by rank, isonline;
将GroupBy的字段组合为map的输出key值,利用MapReduce的排序,在reduce阶段保存LastKey区分不同的key。MapReduce的过程如下(当然这里只是说明Reduce端的非Hash聚合过程)
1.3 Distinct的实现原理
select dealid, count(distinct uid) num from order group by dealid;
如果有多个distinct字段呢,如下面的SQL
select dealid, count(distinct uid), count(distinct date) from order group by dealid;
实现方式有两种:
(1)如果仍然按照上面一个distinct字段的方法,即下图这种实现方式,无法跟据uid和date分别排序,也就无法通过LastKey去重,仍然需要在reduce阶段在内存中通过Hash去重
(2)第二种实现方式,可以对所有的distinct字段编号,每行数据生成n行数据,那么相同字段就会分别排序,这时只需要在reduce阶段记录LastKey即可去重。
这种实现方式很好的利用了MapReduce的排序,节省了reduce阶段去重的内存消耗,但是缺点是增加了shuffle的数据量。需要注意的是,在生成reduce value时,除第一个distinct字段所在行需要保留value值,其余distinct数据行value字段均可为空。
2. SQL转化为MapReduce的过程
了解了MapReduce实现SQL基本操作之后,我们来看看Hive是如何将SQL转化为MapReduce任务的,整个编译过程分为六个阶段:
- Antlr定义SQL的语法规则,完成SQL词法,语法解析,将SQL转化为抽象语法树AST Tree
- 遍历AST Tree,抽象出查询的基本组成单元QueryBlock
- 遍历QueryBlock,翻译为执行操作树OperatorTree,
- 逻辑层优化器进行OperatorTree变换,合并不必要的ReduceSinkOperator,减少shuffle数据量
- 遍历OperatorTree,翻译为MapReduce任务
- 物理层优化器进行MapReduce任务的变换,生成最终的执行计划
在slice中,将着六个阶段表示为:
- 解析:将HIVEQL解析为AST。
- 语义解析:将AST翻译为QB。
- 生成逻辑计划:将QB转化为OperatorTree
- 优化逻辑计划
- 生成物理执行计划:将OperatorTree转化为TaskTree
- 优化物理计划
2.1 Phase1 SQL词法,语法解析
Antlr
Hive使用Antlr实现SQL的词法和语法解析。Antlr是一种语言识别的工具,可以用来构造领域语言。
这里不详细介绍Antlr,只需要了解使用Antlr构造特定的语言只需要编写一个语法文件,定义词法和语法替换规则即可,Antlr完成了词法分析、语法分析、语义分析、中间代码生成的过程。
抽象语法树AST Tree(看不懂概念没关系,重点看guo)
经过词法和语法解析后,如果需要对表达式做进一步的处理,使用 Antlr 的抽象语法树语法Abstract Syntax Tree,在语法分析的同时将输入语句转换成抽象语法树,后续在遍历语法树时完成进一步的处理。
下面的一段语法是Hive SQL中SelectStatement的语法规则,从中可以看出,SelectStatement包含select, from, where, groupby, having, orderby等子句。
(在下面的语法规则中,箭头表示对于原语句的改写,改写后会加入一些特殊词标示特定语法,比如TOK_QUERY标示一个查询块)
selectStatement
:
selectClause
fromClause
whereClause?
groupByClause?
havingClause?
orderByClause?
clusterByClause?
distributeByClause?
sortByClause?
limitClause? -> ^(TOK_QUERY fromClause ^(TOK_INSERT ^(TOK_DESTINATION ^(TOK_DIR TOK_TMP_FILE))
selectClause whereClause? groupByClause? havingClause? orderByClause? clusterByClause?
distributeByClause? sortByClause? limitClause?))
;
为了详细说明SQL翻译为MapReduce的过程,这里以一条简单的SQL为例,SQL中包含一个子查询,最终将数据写入到一张表中。
FROM
(
SELECT
p.datekey datekey,
p.userid userid,
c.clienttype
FROM
detail.usersequence_client c
JOIN fact.orderpayment p ON p.orderid = c.orderid
JOIN default.user du ON du.userid = p.userid
WHERE p.datekey = 20131118
) base
INSERT OVERWRITE TABLE `test`.`customer_kpi`
SELECT
base.datekey,
base.clienttype,
count(distinct base.userid) buyer_count
GROUP BY base.datekey, base.clienttype
SQL生成AST Tree(重点)
Antlr对Hive SQL解析的代码如下,HiveLexerX,HiveParser分别是Antlr对语法文件Hive.g编译后自动生成的词法解析和语法解析类,在这两个类中进行复杂的解析。
// command 为hiveQL
HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command)); //词法解析,忽略关键词的大小写
TokenRewriteStream tokens = new TokenRewriteStream(lexer);
if (ctx != null) {
ctx.setTokenRewriteStream(tokens);
}
HiveParser parser = new HiveParser(tokens); //语法解析
parser.setTreeAdaptor(adaptor);
HiveParser.statement_return r = null;
try {
r = parser.statement(); //转化为AST Tree
} catch (RecognitionException e) {
e.printStackTrace();
throw new ParseException(parser.errors);
}
最终生成的AST Tree如下图右侧(使用Antlr Works生成,Antlr Works是Antlr提供的编写语法文件的编辑器),图中只是展开了骨架的几个节点,没有完全展开。
子查询1/2,分别对应右侧第1/2两个部分。
这里注意一下内层子查询也会生成一个TOK_DESTINATION节点。请看上面SelectStatement的语法规则,这个节点是在语法改写中特意增加了的一个节点。原因是Hive中所有查询的数据均会保存在HDFS临时的文件中,无论是中间的子查询还是查询最终的结果,Insert语句最终会将数据写入表所在的HDFS目录下。
详细来看,将内存子查询的from子句展开后,得到如下AST Tree,每个表生成一个TOK_TABREF节点,Join条件生成一个“=”节点。其他SQL部分类似,不一一详述。
看一个slice里面的例子:
2.2 Phase2 SQL基本组成单元QueryBlock
AST Tree仍然非常复杂,不够结构化,不方便直接翻译为MapReduce程序,AST Tree转化为QueryBlock就是将SQL进一部抽象和结构化
QueryBlock(非重点)
QueryBlock是一条SQL最基本的组成单元,包括三个部分:输入源,计算过程,输出。简单来讲一个QueryBlock就是一个子查询。
下图为Hive中QueryBlock相关对象的类图,解释图中几个重要的属性
- QB.aliasToSubq(表示QB类的aliasToSubq属性)保存子查询的QB对象,aliasToSubq的key值是子查询的别名
- QB.qbp即QBParseInfo保存一个基本SQL单元中的各个操作部分的AST Tree结构,QBParseInfo.nameToDest这个HashMap保存查询单元的输出,key的形式是inclause-i(由于Hive支持Multi Insert语句,所以可能有多个输出),value是对应的ASTNode节点,即TOK_DESTINATION节点。类QBParseInfo其余HashMap属性分别保存输出和各个操作的ASTNode节点的对应关系。
- QBParseInfo.JoinExpr保存TOK_JOIN节点。QB.QBJoinTree是对Join语法树的结构化。
- QB.qbm保存每个输入表的元信息,比如表在HDFS上的路径,保存表数据的文件格式等。
- QBExpr这个对象是为了表示Union操作。
AST Tree生成QueryBlock(重点)
AST Tree生成QueryBlock的过程是一个递归的过程,先序遍历AST Tree,遇到不同的Token节点,保存到相应的属性中,主要包含以下几个过程:
- TOK_QUERY => 创建QB对象,循环递归子节点
- TOK_FROM => 将表名语法部分保存到QB对象的
aliasToTabs
等属性中 - TOK_INSERT => 循环递归子节点
- TOK_DESTINATION => 将输出目标的语法部分保存在QBParseInfo对象的nameToDest属性中
- TOK_SELECT => 分别将查询表达式的语法部分保存在
destToSelExpr
、destToAggregationExprs
、destToDistinctFuncExprs
三个属性中 - TOK_WHERE => 将Where部分的语法保存在QBParseInfo对象的destToWhereExpr属性中
看一下slice中的例子:
2.3 Phase3 逻辑操作符Operator和逻辑操作树Operator Tree
Operator
Hive最终生成的MapReduce任务,Map阶段和Reduce阶段均由OperatorTree组成。逻辑操作符,就是在Map阶段或者Reduce阶段完成单一特定的操作。基本的操作符包括TableScanOperator,SelectOperator,FilterOperator,JoinOperator,GroupByOperator,ReduceSinkOperator。
从名字就能猜出各个操作符完成的功能,TableScanOperator从MapReduce框架的Map接口原始输入表的数据,控制扫描表的数据行数,标记是从原表中取数据。JoinOperator完成Join操作。FilterOperator完成过滤操作。ReduceSinkOperator将Map端的字段组合序列化为Reduce Key/value, Partition Key,只可能出现在Map阶段,同时也标志着Hive生成的MapReduce程序中Map阶段的结束。
Operator在Map Reduce阶段之间的数据传递都是一个流式的过程。每一个Operator对一行数据完成操作后之后将数据传递给childOperator计算。
QueryBlock生成Operator Tree
QueryBlock生成Operator Tree就是遍历上一个过程中生成的QB和QBParseInfo对象的保存语法的属性,包含如下几个步骤:
- QB.aliasToSubq => 有子查询,递归调用
- QB.aliasToTabs => TableScanOperator
- QBParseInfo.joinExpr => QBJoinTree => ReduceSinkOperator + JoinOperator
- QBParseInfo.destToWhereExpr => FilterOperator
- QBParseInfo.destToGroupby => ReduceSinkOperator + GroupByOperator
- QBParseInfo.destToOrderby => ReduceSinkOperator + ExtractOperator
2.4 Phase4 逻辑层优化器
大部分逻辑层优化器通过变换OperatorTree,合并操作符,达到减少MapReduce Job(② ),减少shuffle数据量的目的(①)。
名称 | 作用 |
---|---|
② SimpleFetchOptimizer |
优化没有GroupBy表达式的聚合查询 |
② MapJoinProcessor |
MapJoin,需要SQL中提供hint,0.11版本已不用 |
② BucketMapJoinOptimizer |
BucketMapJoin |
② GroupByOptimizer |
Map端聚合 |
① ReduceSinkDeDuplication |
合并线性的OperatorTree中partition/sort key相同的reduce |
① PredicatePushDown |
谓词前置 |
① CorrelationOptimizer |
利用查询中的相关性,合并有相关性的Job,HIVE-2206 |
ColumnPruner |
字段剪枝 |
表格中①的优化器均是一个Job干尽可能多的事情/合并。②的都是减少shuffle数据量,甚至不做Reduce。CorrelationOptimizer优化器非常复杂,都能利用查询中的相关性,合并有相关性的Job,参考 Hive Correlation Optimizer。
注:具体的每个优化器优化了什么,没看太明白。其中:PredicatePushDown看slice是把一个查询的谓词(LKIE、BETWEEN、IS NULL、IS NOT NULL、IN、EXISTS)提到他在OperatorTree中的前置查询里。以提前减少数据量。
2.5 Phase5 OperatorTree生成MapReduce Job的过程
OperatorTree转化为MapReduce Job的过程分为下面几个阶段
- 对输出表生成MoveTask
- 从OperatorTree的其中一个根节点向下深度优先遍历
- ReduceSinkOperator标示Map/Reduce的界限,多个Job间的界限
- 遍历其他根节点,遇过碰到JoinOperator合并MapReduceTask
- 生成StatTask更新元数据
- 剪断Map与Reduce间的Operator的关系
这一段太复杂。再次学习之后 再作补充。
大致思想是:从operatorTree的根节点开始遍历。把各个根节点入队,然后遇到符合一定规则的组合,就划分Map/Reduce的界限,或者划分Job的界限。有好几个规则(Rules)。
例如:遇到TS(QB.aliasToTabs),就生成一个MapWork,再遇到RS(ReduceSinkOperator),就从TS到RS划分为一个MapWork,从该RS到下一个RS前,划分为一个ReduceWork。下一个RS又生成一个MapWork。
下图是一个从OperatorTree到MapReduceJob的示例。FS[19]和FS[21]是Stage间生成的中间文件。
2.6 Phase6 物理层优化器
这里不详细介绍每个优化器的原理,单独介绍一下MapJoin的优化器
名称 |
作用 |
---|---|
Vectorizer |
HIVE-4160,将在0.13中发布 |
SortMergeJoinResolver |
与bucket配合,类似于归并排序 |
SamplingOptimizer |
并行order by优化器,在0.12中发布 |
CommonJoinResolver + MapJoinResolver |
MapJoin优化器 |
MapJoin原理
MapJoin简单说就是在Map阶段将小表读入内存,顺序扫描大表完成Join。
上图是Hive MapJoin的原理图,出自Facebook工程师Liyin Tang的一篇介绍Join优化的slice,从图中可以看出MapJoin分为两个阶段:
-
通过MapReduce Local Task,将小表读入内存,生成HashTableFiles上传至Distributed Cache中,这里会对HashTableFiles进行压缩。
-
MapReduce Job在Map阶段,每个Mapper从Distributed Cache读取HashTableFiles到内存中,顺序扫描大表,在Map阶段直接进行Join,将数据传递给下一个MapReduce任务。
3. 待续
还有很多没看懂的地方,后续再总结和整理进来。