SparkSQL源码解析(⼆)Antlr4解析Sql并⽣成树
Spark SQL原理解析前⾔:
这⼀次要开始真正介绍Spark解析SQL的流程,⾸先是从Sql Parse阶段开始,简单点说,这个阶段就是使⽤Antlr4,将⼀条Sql语句解析成语法树。
可能有童鞋没接触过antlr4这个内容,推荐看看《antlr4权威指南》前四章,看完起码知道antlr4能⼲嘛。我这⾥就不多介绍了。
这篇⾸先先介绍调⽤spark.sql()时候的流程,再看看antlr4在这个其中的主要功能,最后再将探究Logical Plan究竟是什么东西。
初始流程
当你调⽤spark.sql的时候,会调⽤下⾯的⽅法:
def sql(sqlText: String): DataFrame = {
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}
parse sql阶段主要是parsePlan(sqlText)这⼀部分。⽽这⾥⼜会辗转去org.apache.spark.sql.catalyst.parser.AbstractSqlParser调⽤parse⽅法。这⾥贴下关键代码。  protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
logDebug(s"Parsing command: $command")
val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
lexer.addErrorListener(ParseErrorListener)
lexer.legacy_setops_precedence_enbled = setOpsPrecedenceEnforced
val tokenStream = new CommonTokenStream(lexer)
val parser = new SqlBaseParser(tokenStream)
parser.addParseListener(PostProcessor)
parser.addErrorListener(ParseErrorListener)
parser.legacy_setops_precedence_enbled = setOpsPrecedenceEnforced
try {
try {
// first, try parsing with potentially faster SLL mode
toResult(parser)
}
catch {
case e: ParseCancellationException =>
/
/ if we fail, parse with LL mode
tokenStream.seek(0) // rewind input stream
// Try Again.
toResult(parser)
}
}
catch {
case e: ParseException if emand.isDefined =>
throw e
case e: ParseException =>
throw e.withCommand(command)
case e: AnalysisException =>
val position = Origin(e.line, e.startPosition)
throw new ParseException(Option(command), e.message, position, position)
}
}
可以发现,这⾥⾯的处理逻辑,⽆论是SqlBaseLexer还是SqlBaseParser都是Antlr4的东西,包括最后的toResult(parser)也是调⽤访问者模式的类去遍历语法树来⽣成Logical Plan。如果对antlr4有⼀定了解,那么对这⾥这些东西⼀定不会陌⽣。那我们接下来看看Antlr4在这其中的⾓⾊。
Antlr4⽣成语法树
Spark提供了⼀个.g4⽂件,编译的时候会使⽤Antlr根据这个.g4⽣成对应的词法分析类和语法分析类,
同时还使⽤了访问者模式,⽤以构建Logical Plan(语法树)。
访问者模式简单说就是会去遍历⽣成的语法树(针对语法树中每个节点⽣成⼀个visit⽅法),以及返回相应的值。我们接下来看看⼀条简单的select语句⽣成的树是什么样⼦。
这个sqlBase.g4⽂件我们也可以直接拿出来玩,直接复制出来,⽤antlr相关⼯具就可以⽣成⼀个⽣成⼀个解析SQL的图了。
这⾥antlr4和grun都已经存储成bat⽂件,所以可以直接调⽤,实际命令在《antlr4权威指南》说得很详细了就不介绍了。调⽤完后就会⽣成这样的语法树。
这⾥,将SELECT TABLE_A.B FROM TABLE_A,转换成⼀棵语法树。我们可以看到这颗语法树⾮常复杂,这是因为SQL解析中,要适配这种SELECT语句之外,还有很多其他类型的语句,⽐如INSERT,ALERT等等。Spark SQL这个模块的最终⽬标,就是将这样的⼀棵语法树转换成⼀个可执
⾏的Dataframe(RDD)。
我们现阶段的⽬标则是要先⽣成Logical Plan,Spark使⽤Antlr4的访问者模式,⽣成Logical Plan。这⾥顺便说下怎么实现访问者模式吧,在使⽤antlr4命令的时候,加上-visit参数就会⽣成SqlBaseBaseVisitor,⾥⾯提供了默认的访问各个节点的触发⽅法。我们可以通过继承这个类,重写对应节点的visit⽅法,实现⾃⼰的访问逻辑,⽽这个继承的类就是org.apache.spark.sql.catalyst.parser.AstBuilder。
通过观察这棵树,我们可以发现针对我们的SELECT语句,⽐较重要的⼀个节点,是querySpecification节点,实际上,在AstBuilder类中,visitQuerySpecification 也是⽐较重要的⼀个⽅法(访问对应节点时触发),正是在这个⽅法中⽣成主要的Logical Plan的。
接下来重点看这个⽅法,以及探究Logical Plan。
⽣成Logical Plan
我们先看看AstBuilder中的代码:
class AstBuilder(conf: SQLConf) extends SqlBaseBaseVisitor[AnyRef] with Logging {
......其他代码
override def visitQuerySpecification(
error parse newctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
val from = OneRowRelation().optional(ctx.fromClause) {  //如果有FROM语句,⽣成对应的Logical Plan
visitFromClause(ctx.fromClause)
}
withQuerySpecification(ctx, from)
}
......其他代码
代码中会先判断是否有FROM⼦语句,有的话会去⽣成对应的Logical Plan,再调⽤withQuerySpecification()⽅法,⽽withQuerySpecification()⽅法是⽐较核⼼的⼀个⽅法。它会处理包括SELECT,FILTER,GROUP BY,HAVING等⼦语句的逻辑。
代码⽐较长就不贴了,有兴趣的童鞋可以去看看,⼤意就是使⽤scala的模式匹配,匹配不同的⼦语句⽣成不同的Logical Plan。
然后再来说说最终⽣成的LogicalPlan,LogicalPlan其实是继承⾃TreeNode,所以本质上LogicalPlan就是⼀棵树。
⽽实际上,LogicalPlan还有多个⼦类,分别表⽰不同的SQL⼦语句。
LeafNode,叶⼦节点,⼀般⽤来表⽰⽤户命令
UnaryNode,⼀元节点,表⽰FILTER等操作
BinaryNode,⼆元节点,表⽰JOIN,GROUP BY等操作
这⾥⼀元⼆元这些都是对应关系代数⽅⾯的知识,在学数据库理论的时候肯定有接触过,不过估计都还给⽼师了吧(/偷笑)。不过⼀元⼆元基本上也就是⽤来区分具体的操作,如上⾯说的FILTER,或是JOIN等,也不是很复杂。这三个类都位于org.apache.spark.sql.catalyst.plans.logical.LogicalPlan中,有兴趣的童鞋可以看看。⽽后,这三个类⼜会有多个⼦类,⽤以表⽰不同的情况,这⾥就不再赘述。
最后看看⽤⼀个测试案例,看看会⽣成什么吧。⽰例中简单⽣成⼀个临时的view,然后直接select查询这个view。代码如下:
//⽣成DataFrame
val df = Seq((1, 1)).toDF("key", "value")
//调⽤spark.sql
val queryCaseWhen = sql("select key from src ")
补充下,这⾥的sql()⽅法是做了⼀些封装的⽅法,可以直接看成spark.sql(...)。最终经过parse SQL后会变成如下的内容:
'Project ['key]
+- 'UnresolvedRelation `src`
这个Project是UnaryNode的⼀个⼦类(SELECT⾃然是⼀元节点),表明我们要查询的字段是key。
UnresolvedRelation是⼀个新的概念,这⾥顺便说下,我们通过SQL parse⽣成的这棵树,其实叫Unresolved LogicalPlan,这⾥的Unresolved的意思说,还不知道src是否存在,或它的元数据是什么
样,只有通过Analysis阶段后,才会把Unresolved变成Resolved LogicalPlan。这⾥的意思可以理解为,读取名为src的表,但这张表的情况未知,有待验证。
总的来说,我们的⽰例⾜够简单直接,所以内容会⽐较少,不过拿来学习是⾜够了。
下⼀个阶段是要使⽤这棵树进⾏分析验证了,也就是Analysis阶段,这⼀块留到下篇介绍吧。
以上~