无论Hive Cli还是HiveServer2,一个HQl语句都要经过Driver进行解析和执行,粗略如下图:
HQL解析(生成AST语法树) =>
语法分析(得到QueryBlock) =>
生成逻辑执行计划(Operator) =>
逻辑优化(Logical Optimizer Operator) =>
生成物理执行计划(Task Plan) =>
物理优化(Task Tree) =>
构建执行计划(QueryPlan) =>
表以及操作鉴权 =>
执行引擎执行
SELECT NAME,AGE FROM STUDEN WHERE AGE>30;
上边一个简单的sql查询,在分析执行的时候会经历下面所示的几个步骤:
Query=> Parse=> Bind=> Optimize=> Execute
即:语法解析,操作绑定,优化执行策略,交付执行。
语法解析之后,会形成一棵语法树,如下图所示。树中的每个节点是执行的rule,整棵树称之为执行策略。
形成上述的执行策略树还只是第一步,因为这个执行策略可以进行优化,所谓的优化就是对树中节点进行合并或是进行顺序上的调整。以大家熟悉的join操作为例,下图给出一个join优化的示例。A JOIN B等同于B JOIN A,但是顺序的调整可能给执行的性能带来极大的影响,下图就是调整前后的对比图。
在Hash Join中,首先被访问的表称之为“内部构建表”,第二个表为“探针输入”。创建内部表时,会将数据移动到数据仓库指向的路径;创建外部表,仅记录数据所在的路径。
HiveContext是Spark提供的用户接口,HiveContext继承自SqlContext。
既然是继承自SqlContext,那么我们将普通sql与hiveql分析执行步骤做一个对比,可以得到下图。
HiveQL是整个的入口点:
def hiveql(hqlQuery: String): SchemaRDD = {
val result = new SchemaRDD(this, HiveQl.parseSql(hqlQuery))
// We force query optimization to happen right away instead of letting it happen lazily like
// when using the query DSL. This is so DDL commands behave as expected. This is only
// generates the RDD lineage for DML queries, but does not perform any execution.
result.queryExecution.toRdd
result
}
上述hiveql的定义与sql的定义几乎一模一样,唯一的不同是sql中使用parseSql的结果作为SchemaRDD的入参而hiveql中使用HiveQl.parseSql作为SchemaRdd的入参。对比:
def sql(sqlText: String): SchemaRDD = {
val result = new SchemaRDD(this, parseSql(sqlText))
result.queryExecution.toRdd
result
}
parseSql的函数定义如代码所示,解析过程中将指令分成两大类:
def parseSql(sql: String): LogicalPlan = {
try {
if (sql.toLowerCase.startsWith("set")) {
NativeCommand(sql)
} else if (sql.toLowerCase.startsWith("add jar")) {
AddJar(sql.drop(8))
} else if (sql.toLowerCase.startsWith("add file")) {
AddFile(sql.drop(9))
} else if (sql.startsWith("dfs")) {
DfsCommand(sql)
} else if (sql.startsWith("source")) {
SourceCommand(sql.split(" ").toSeq match { case Seq("source", filePath) => filePath })
} else if (sql.startsWith("!")) {
ShellCommand(sql.drop(1))
} else {
val tree = getAst(sql)
if (nativeCommands contains tree.getText) {
NativeCommand(sql)
} else {
nodeToPlan(tree) match {
case NativePlaceholder => NativeCommand(sql)
case other => other
}
}
}
} catch {
case e: Exception => throw new ParseException(sql, e)
case e: NotImplementedError => sys.error(
s"""
|Unsupported language features in query: $sql
|${dumpTree(getAst(sql))}
""".stripMargin)
}
}
哪些指令是nativecommand呢,答案在HiveQl.scala中的nativeCommands变量。对于非nativeCommand,最重要的解析函数就是nodeToPlan。Spark对HiveQL所做的优化主要体现在Query相关的操作,其它的依然使用Hive的原生执行引擎。
native command的执行流程
由于native command是一些非耗时的操作,直接使用Hive中原有的exeucte engine来执行即可。这些command的执行示意图如下
SparkTask的生成和执行
我们通过一个例子来看一下一个简单的两表JOIN查询如何被转换为SparkTask并被执行。下图左半部分展示了这个查询的Operator Tree,以及该Operator Tree如何被转化成SparkTask;右半部分展示了该SparkTask执行时如何得到最终的RDD并通过foreachAsync提交Spark任务。
SparkCompiler遍历Operator Tree,将其划分为不同的MapWork和ReduceWork。
MapWork为根节点,总是由TableScanOperator(Hive中对表进行扫描的操作符)开始;后续的Work均为ReduceWork。ReduceSinkOperator(Hive中进行Shuffle输出的操作符)用来标记两个Work之间的界线,出现ReduceSinkOperator表示当前Work到下一个Work之间的数据需要进行Shuffle。因此,当我们发现ReduceSinkOperator时,就会创建一个新的ReduceWork并作为当前Work的子节点。包含了FileSinkOperator(Hive中将结果输出到文件的操作符)的Work为叶子节点。
与MapReduce最大的不同在于,我们并不要求ReduceWork一定是叶子节点,即ReduceWork之后可以链接更多的ReduceWork,并在同一个SparkTask中执行。
总结执行SparkTask步骤:
在logicalPlan到physicalPlan的转换过程中,toRdd最关键的元素
override lazy val toRdd: RDD[Row] =
analyzed match {
case NativeCommand(cmd) =>
val output = runSqlHive(cmd)
if (output.size == 0) {
emptyResult
} else {
val asRows = output.map(r => new GenericRow(r.split("\t").asInstanceOf[Array[Any]]))
sparkContext.parallelize(asRows, 1)
}
case _ =>
executedPlan.execute().map(_.copy())
}
查看执行计划:explain select count(*) from ods_start_log;
Explain
STAGE DEPENDENCIES:
Stage-1 is a root stage
Stage-0 depends on stages: Stage-1
STAGE PLANS:
Stage: Stage-1
Spark //执行引擎
Edges://边
Reducer 2 <- Map 1 (GROUP, 1) //边描述
DagName: wxq_20211113113616_4d7fa751-f57a-4378-bab7-d0f062c2dc9a:1
Vertices: //顶点
Map 1
Map Operator Tree: //map 语法树
TableScan //扫描表操作
alias: ods_start_log
Statistics: Num rows: 308758 Data size: 118565680 Basic stats: COMPLETE Column stats: NONE //统计数字 多少行,数据大小
Select Operator //字段选择操作
Statistics: Num rows: 308758 Data size: 118565680 Basic stats: COMPLETE Column stats: NONE //统计数字 多少行,数据大小
Group By Operator //分组操作
aggregations: count() //聚合算子
mode: hash //hash分组
outputColumnNames: _col0 //输出列名
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE //统计数字 多少行,数据大小
Reduce Output Operator //reduce输出操作
sort order:
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE //统计数字 多少行,数据大小
value expressions: _col0 (type: bigint) //输出信息描述
Execution mode: vectorized //执行模式:矢量化
Reducer 2
Execution mode: vectorized //执行模式:矢量化
Reduce Operator Tree: //Reduce操作树
Group By Operator //分组操作
aggregations: count(VALUE._col0) //根据列统计
mode: mergepartial //模式:合并部分
outputColumnNames: _col0 //输出列名
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE //统计数字 多少行,数据大小
File Output Operator //文件输出算子
compressed: false //不压缩
Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE Column stats: NONE//统计数字 多少行,数据大小
table:
input format: org.apache.hadoop.mapred.SequenceFileInputFormat //输入格式
output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat //输出格式
serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe //序列化
Stage: Stage-0
Fetch Operator //抓取数据
limit: -1
Processor Tree:
ListSink //list输出
查看作业日志:
参考文章:
大数据手册(Hive)--HiveQL_WilenWu-CSDN博客_hiveql
Hive on spark的架构与解析SQL的过程 - 简书