[Spark SQL] 源码解析之Parser

作者: BIGUFO | 来源:发表于2018-05-28 18:24 被阅读28次

    前言

    由上篇博客我们知道了SparkSql整个解析流程如下:

    • sqlText 经过 SqlParser 解析成 Unresolved LogicalPlan;
    • analyzer 模块结合catalog进行绑定,生成 resolved LogicalPlan;
    • optimizer 模块对 resolved LogicalPlan 进行优化,生成 optimized LogicalPlan;
    • SparkPlan 将 LogicalPlan 转换成PhysicalPlan;
    • prepareForExecution()将 PhysicalPlan 转换成可执行物理计划;
    • 使用 execute()执行可执行物理计划;

    详解Parser模块

    Parser就是将SQL字符串切分成一个个Token,再根据一定语义规则解析为一棵语法树。我们写的sql语句只是一个字符串而已,首先需要将其通过词法解析和语法解析生成语法树,Spark1.x版本使用的是scala原生的parser语法解析器,从2.x后改用的是第三方语法解析工具ANTLR4, 在性能上有了较大的提升。

    antlr4的使用需要定义一个语法文件,sparksql的语法文件的路径在sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBase.g4
    antlr可以使用插件自动生成词法解析和语法解析代码,在SparkSQL中词法解析器SqlBaseLexer和语法解析器SqlBaseParser,遍历节点有两种模式Listener和Visitor。

    Listener模式是被动式遍历,antlr生成类ParseTreeListener,这个类里面包含了所有进入语法树中每个节点和退出每个节点时要进行的操作。我们只需要实现我们需要的节点事件逻辑代码即可,再实例化一个遍历类ParseTreeWalker,antlr会自上而下的遍历所有节点,以完成我们的逻辑处理;

    Visitor则是主动遍历模式,需要我们显示的控制我们的遍历顺序。该模式可以实现在不改变各元素的类的前提下定义作用于这些元素的新操作。SparkSql用的就是此方式来遍历节点的。

    通过词法解析和语法解析将SQL语句解析成了ANTLR 4的语法树结构ParseTree。然后在parsePlan中,使用AstBuilder将ANTLR 4语法树结构转换成catalyst表达式逻辑计划logical plan。具体看源码:

    // 代码1
    val spark = SparkSession
        .builder
        .appName("SparkSQL Test") 
        .master("local[4]") 
        .getOrCreate()
    spark.sql("select * from table").show(false) 
    
    ---
    // 代码2
    def sql(sqlText: String): DataFrame = {
        Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
    }
    
    ---
    // 代码3
    override def parsePlan(sqlText: String): LogicalPlan = parse(sqlText) { parser =>
        astBuilder.visitSingleStatement(parser.singleStatement()) match {
          case plan: LogicalPlan => plan
          case _ =>
            val position = Origin(None, None)
            throw new ParseException(Option(sqlText), "Unsupported SQL statement", position, position)
        }
      }
    
    ---
    // 代码4
    protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
        logInfo(s"Parsing command: $command")
    
        val lexer = new SqlBaseLexer(new ANTLRNoCaseStringStream(command))
        lexer.removeErrorListeners()
        lexer.addErrorListener(ParseErrorListener)
    
        val tokenStream = new CommonTokenStream(lexer)
        val parser = new SqlBaseParser(tokenStream)
        parser.addParseListener(PostProcessor)
        parser.removeErrorListeners()
        parser.addErrorListener(ParseErrorListener)
    
        try {
          try {
            // first, try parsing with potentially faster SLL mode
            parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
            toResult(parser)
          ...
    
    

    代码2中的sqlParser为 SparkSqlParser,其成员变量val astBuilder = new SparkSqlAstBuilder(conf)是将antlr语法结构转换为catalyst表达式的关键类。

    可以看到代码3中parsePlan方法先执行parse方法(代码4),在代码4中先后实例化了分词解析和语法解析类,最后将antlr的语法解析器parser:SqlBaseParser 传给了代码3中的柯里化函数,使用astBuilder转化为catalyst表达式,可以看到首先调用的是visitSingleStatement,singleStatement为语法文件中定义的最顶级节点,接下来就是利用antlr的visitor模式显示的遍历整个语法树,将所有的节点都替换成了LogicalPlan 或者TableIdentifier。
    通过Parser解析后的AST语法树如图所示:


    相关文章

      网友评论

        本文标题:[Spark SQL] 源码解析之Parser

        本文链接:https://www.haomeiwen.com/subject/qxsijftx.html