sparksql源码解析

1.sparkSQL的首要组件以及效果

Spark SQL是Apache Spark的一个模块,用于处理结构化和半结构化数据。它提供了编程接口,用于在大型分布式集群上进行数据查询、剖析和转化。Spark SQL支撑SQL查询,同时还答应运用Dataset和DataFrame API进行编程。它集成了Catalyst查询优化器和Tungsten履行引擎,以完成高性能和高可扩展性。

以下是Spark SQL首要组件和源码剖析:

  1. SQL解析器 (Parser)

    Spark SQL运用ANTLR4(另一个言语识别东西)生成的解析器将SQL句子解析成笼统语法树(AST)。解析器的源码坐落org.apache.spark.sql.catalyst.parser包中。

  2. 剖析器 (Analyzer)

    剖析器担任对AST进行语义剖析,首要包括:表名、列名解析,类型检查等。剖析器将AST转化成一个称为”逻辑方案”的目标,逻辑方案用于表明Spark SQL查询的逻辑结构。剖析器的源码坐落org.apache.spark.sql.catalyst.analysis包中。

  3. 优化器 (Optimizer)

    Spark SQL运用名为Catalyst的查询优化器。Catalyst是基于规矩的优化器,运用一组预界说的规矩进行查询优化。优化器的目标是生成一个更有效的逻辑方案,然后进步查询性能。优化器的源码坐落org.apache.spark.sql.catalyst.optimizer包中。

  4. 物理方案 (Physical Plan)

    在优化后的逻辑方案经过物理方案生成阶段,将逻辑方案转化为物理方案。物理方案表明了在Spark集群上履行查询的详细操作。Spark SQL运用名为Tungsten的履行引擎来生成物理方案。物理方案生成阶段的源码坐落org.apache.spark.sql.execution包中。

  5. 履行引擎 (Execution Engine)

    履行引擎担任运转物理方案,并生成查询成果。履行引擎会将物理方案转化为RDD操作,然后经过Spark Core履行这些操作。履行引擎的源码坐落org.apache.spark.sql.execution包中。

  6. 数据源 API (DataSource API)

    Spark SQL支撑多种数据源,如Hive、HDFS、Parquet、Avro、JSON、JDBC等。数据源API答应用户扩展并自界说数据源。数据源API的源码坐落org.apache.spark.sql.sources包中。

这只是对Spark SQL源码的一个简要概述。为了深入了解源码,建议从org.apache.spark.sql.SparkSession类开端,由于它是Spark SQL的进口点。从这儿,你能够深入研究每个组件的详细完成。

那你从SparkSession类作为sql进口点进行写sparksql源码的剖析

org.apache.spark.sql.SparkSession类开端,咱们能够剖析怎么运用Spark SQL来履行查询。SparkSession是用户与Spark SQL交互的首要进口点。下面是一个简略的比如,展现了怎么运用Spark SQL进行数据查询。

假定咱们有一个JSON数据文件example.json,其内容如下:

jsonCopy code
{"name":"Alice","age":30}
{"name":"Bob","age":28}

现在,咱们将运用SparkSession来加载这个JSON数据文件,并履行一个简略的查询。

scalaCopy code
import org.apache.spark.sql.SparkSession
object SparkSQLExample {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("Spark SQL Example")
      .master("local[*]")
      .getOrCreate()
    import spark.implicits._
    // 1. 加载JSON数据
    val df = spark.read.json("example.json")
    // 2. 创立一个临时视图
    df.createOrReplaceTempView("people")
    // 3. 履行SQL查询
    val result = spark.sql("SELECT name, age FROM people WHERE age >= 28")
    // 4. 显现查询成果
    result.show()
    // 5. 关闭SparkSession
    spark.stop()
  }
}

这个查询涉及到以下几个要害进程:

  1. SQL解析
  2. 逻辑方案剖析
  3. 逻辑方案优化
  4. 物理方案生成
  5. 查询履行

咱们将剖析SparkSession.sql()办法是怎么处理这些进程的。

SparkSession.sql()办法的源码如下:

scalaCopy code
def sql(sqlText: String): DataFrame = {
  Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}

这个办法首要调用sessionState.sqlParser.parsePlan(sqlText)解析SQL文本。parsePlan办法的完成坐落org.apache.spark.sql.catalyst.parser.CatalystSqlParser类。它将SQL文本解析为一个笼统语法树(AST)并回来一个未剖析的逻辑方案(Unresolved Logical Plan)。

接下来,咱们将看到Dataset.ofRows()办法怎么处理其他进程。

scalaCopy code
def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan): DataFrame = {
  val qe = sparkSession.sessionState.executePlan(logicalPlan)
  qe.assertAnalyzed() // 保证逻辑方案已剖析
  new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))
}

Dataset.ofRows()办法接纳一个未剖析的逻辑方案。它首要调用sparkSession.sessionState.executePlan(logicalPlan)办法,该办法担任逻辑方案剖析、优化和物理方案生成。

executePlan办法的完成坐落org.apache.spark.sql.internal.SessionState类:

scalaCopy code
def executePlan(plan: LogicalPlan): QueryExecution = {
  new QueryExecution(sparkSession, plan)
}

executePlan办法创立一个QueryExecution目标。QueryExecution类坐落org.apache.spark.sql.execution包。这个类担任处理逻辑方案剖析、优化、物理方案生成以及查询履行。

QueryExecution类的要害特点和办法如下:

  • analyzed: LogicalPlan
  • optimizedPlan: LogicalPlan
  • sparkPlan: SparkPlan
  • executedPlan: SparkPlan
  • toRdd: RDD[InternalRow]

QueryExecution目标创立时,它会自动触发逻辑方案剖析(analyzed特点)。优化逻辑方案(optimizedPlan特点)和生成物理方案(sparkPlan特点)是惰性计算的,仅在需求时计算。executedPlan特点是最终用于履行查询的物理方案。

toRdd办法担任将物理方案转化为一个RDD[InternalRow],用于履行查询。这个办法调用executedPlan.execute()办法来履行物理方案。

最后,Dataset.ofRows()办法运用QueryExecution目标创立一个新的`DataFrame

2.将sparkSQL解析为 未解析的逻辑方案源码做下剖析

未解析的逻辑方案(Unresolved Logical Plan)是从原始SQL文本解析得到的逻辑方案。在这个阶段,表名、列名等都还没有解析,数据类型也还未确定。下面,咱们将详细剖析怎么从SQL文本生成未解析的逻辑方案。

SparkSession.sql()办法中,sessionState.sqlParser.parsePlan(sqlText)办法担任将原始SQL文本解析为一个未解析的逻辑方案。parsePlan办法的完成坐落org.apache.spark.sql.catalyst.parser.CatalystSqlParser类。

CatalystSqlParser.parsePlan办法的源码如下:

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)
  }
}
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.removeErrorListeners()
    lexer.addErrorListener(ParseErrorListener)
    val tokenStream = new CommonTokenStream(lexer)
    val parser = new SqlBaseParser(tokenStream)
    parser.addParseListener(PostProcessor)
    parser.removeErrorListeners()
    parser.addErrorListener(ParseErrorListener)
    parser.legacy_setops_precedence_enbled = conf.setOpsPrecedenceEnforced
    parser.legacy_exponent_literal_as_decimal_enabled = conf.exponentLiteralAsDecimalEnabled
    parser.SQL_standard_keyword_behavior = conf.ansiEnabled
    try {
      try {
        // first, try parsing with potentially faster SLL mode
        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
        toResult(parser)
      }
      catch {
        case e: ParseCancellationException =>
          // if we fail, parse with LL mode
          tokenStream.seek(0) // rewind input stream
          parser.reset()
          // Try Again.
          parser.getInterpreter.setPredictionMode(PredictionMode.LL)
          toResult(parser)
      }
    }
    catch {
      case e: ParseException if e.command.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)
    }
     }
}

这儿,parsePlan办法承受一个 SQL 文本字符串sqlText,然后调用父类中的 parse办法经过 SqlBaseParser 加载词法剖析器和语法剖析器来解析SQL文本。parse办法承受一个函数(to_result)作为参数,该函数接纳一个类型为SqlBaseParserparser` 目标。

parse 办法的代码块中,首要调用 parser.singleStatement() 办法从 SQL 文本中解分出单个 SQL 句子。然后,调用 astBuilder.visitSingleStatement() 办法遍历解析得到的语法树,并回来一个笼统语法树(AST)节点。astBuilder 是一个 AstBuilder 实例,它承继自 SqlBaseBaseVisitor,用于处理拜访语法树的进程。

以下是SqlBaseBaseVisitor办法的一些要害完成:

  • visitSelect:处理SELECT句子,创立一个Project节点。
  • visitFromClause:处理FROM子句,创立一个UnresolvedRelation节点。
  • visitWhere:处理WHERE子句,创立一个Filter节点。
  • visitJoin:处理JOIN子句,创立一个Join节点。
  • visitGroupBy:处理GROUP BY子句,创立一个Aggregate节点。
  • visitOrderBy:处理ORDER BY子句,创立一个Sort节点。
  • visitLimit:处理LIMIT子句,创立一个GlobalLimit节点。

未解析的逻辑方案包括一系列未解析的逻辑方案节点,这些节点对应于SQL查询的各个组成部分。在逻辑方案剖析阶段,Spark SQL将解析表名、列名和数据类型,生成一个彻底解析的逻辑方案。

3.解析后生成的未解析的逻辑方案 是什么

在生成未解析的逻辑方案(Unresolved Logical Plan)之后,咱们会得到一个逻辑方案的树形结构,该结构表明SQL查询的逻辑方式。此刻,表名、列名和数据类型仍然是未解析的,因而逻辑方案中的这些信息需求在后续的剖析阶段进行解析。

未解析的逻辑方案是由一系列的org.apache.spark.sql.catalyst.plans.logical.LogicalPlan节点构成的。这些节点表明SQL查询的各个组成部分,例如Project(表明选择操作)、Filter(表明过滤操作)、Join(表明衔接操作)等。详细来说,未解析的逻辑方案树包括以下几种类型的节点:

  1. UnresolvedRelation:表明SQL查询中引证的表,此刻表名仍未解析。
  2. UnresolvedAttribute:表明查询中引证的列,此刻列名仍未解析。
  3. UnresolvedFunction:表明查询中引证的函数,此刻函数名仍未解析。
  4. 其他节点,如Project、Filter、Join等,表明查询中的各种操作,但它们的子节点或许包括未解析的表名、列名或函数名。

在逻辑方案剖析阶段,Spark SQL的剖析器将处理这些未解析的信息。剖析器首要解析表名,然后解析列名和数据类型。最后,剖析器将这些解析后的信息填充到逻辑方案中,生成一个彻底解析的逻辑方案,该方案包括实践的数据表、列和类型信息。

剖析器会运用org.apache.spark.sql.catalyst.analysis.Analyzer类中界说的一系列规矩进行处理。这些规矩包括但不限于:

  • ResolveRelations:解析表名,将UnresolvedRelation节点替换为LogicalRelation节点。
  • ResolveReferences:解析列名,将UnresolvedAttribute节点替换为已解析的AttributeReference节点。
  • ResolveFunctions:解析函数名,将UnresolvedFunction节点替换为已解析的详细函数节点(如CountSum等)。

完成逻辑方案剖析后,Spark SQL将持续进行逻辑方案优化、物理方案生成和查询履行。

那解析履行方案是怎么遍历语法树的?

在Spark SQL中,解析履行方案的进程中,ANTLR4生成的解析器担任遍历语法树。ANTLR4是一个用于构建编译器和解析器的强大东西,能够解析各种方式的语法(如SQL)。

ANTLR4生成的解析器从SQL文本中创立一个笼统语法树(AST),这是一种树形数据结构,用于表明源代码的逻辑结构。遍历AST是解析进程的核心部分,由于它答应解析器识别SQL查询中的各种组件(如SELECT子句、WHERE子句、JOIN子句等)。

Spark SQL运用org.apache.spark.sql.catalyst.parser.SqlBaseBaseVisitor类作为ANTLR4生成的解析器的自界说拜访器。SqlBaseBaseVisitor类完成了一组visit*办法,这些办法用于处理特定类型的AST节点。在遍历AST时,解析器会依据AST节点类型调用相应的visit*办法。

以下是SqlBaseBaseVisitor类中一些要害的visit*办法及其功用:

  1. visitSelect:处理SELECT子句。在遍历AST时,当遇到SELECT子句时,解析器会调用此办法。这个办法会创立一个Project逻辑方案节点。
  2. visitFromClause:处理FROM子句。当遇到FROM子句时,解析器会调用此办法。这个办法会创立一个UnresolvedRelation逻辑方案节点。
  3. visitWhere:处理WHERE子句。当遇到WHERE子句时,解析器会调用此办法。这个办法会创立一个Filter逻辑方案节点。
  4. visitJoin:处理JOIN子句。当遇到JOIN子句时,解析器会调用此办法。这个办法会创立一个Join逻辑方案节点。
  5. visitGroupBy:处理GROUP BY子句。当遇到GROUP BY子句时,解析器会调用此办法。这个办法会创立一个Aggregate逻辑方案节点。
  6. visitOrderBy:处理ORDER BY子句。当遇到ORDER BY子句时,解析器会调用此办法。这个办法会创立一个Sort逻辑方案节点。
  7. visitLimit:处理LIMIT子句。当遇到LIMIT子句时,解析器会调用此办法。这个办法会创立一个GlobalLimit逻辑方案节点。

经过这些visit*办法,SqlBaseBaseVisitor遍历AST并创立逻辑方案树,该树包括表明查询逻辑的逻辑方案节点。在此进程中,表名、列名和函数名仍然是未解析的。在逻辑方案剖析阶段,剖析器将解析这些未解析的信息并生成一个彻底解析的逻辑方案。

4.解析成语法树以及将拜访语法树来生成逻辑方案样例什么样的

以一个简略的非嵌套SELECT句子为例,例如:SELECT name, age FROM users WHERE age > 18,其生成的笼统语法树(AST)结构相似于以下层次结构:

csharpCopy code
statement
  |- query
       |- select
       |   |- name
       |   |- age
       |- from
       |   |- users
       |- where
           |- age > 18

此刻,SqlBaseBaseVisitor会依据以下进程拜访AST节点并遍历一切节点:

  1. 首要,解析器将调用visit(ctx.statement)作为进口点,其间ctx是一个SqlBaseParser.StatementContext目标。这将触发解析器拜访statement节点。

  2. visitStatement办法中,它会调用visitQuery办法,传入query节点。

  3. visitQuery办法中,解析器将持续拜访子节点:

    • 调用visitSelect办法,传入select节点,然后拜访nameage子节点。
    • 调用visitFromClause办法,传入from节点,然后拜访users子节点。
    • 调用visitWhere办法,传入where节点,然后拜访age > 18子节点。

在遍历进程中,SqlBaseBaseVisitor会依据AST节点类型调用相应的visit*办法。假如某个节点没有对应的visit*办法,那么解析器将调用visitChildren(visitor),遍历当时节点的一切子节点。

关于这个简略的SELECT句子,SqlBaseBaseVisitor会依照上述次序拜访AST的各个节点,然后构建未解析的逻辑方案。在逻辑方案剖析阶段,剖析器将解析未解析的逻辑方案中的表名、列名和数据类型,生成一个彻底解析的逻辑方案。

假定咱们有一个包括多个处理函数的SQL查询,例如:SELECT UPPER(name), AVG(age) FROM users WHERE LOWER(city) = 'new york' GROUP BY department HAVING COUNT(*) > 5。关于这个查询,笼统语法树(AST)结构将相似于以下层次结构:

sqlCopy code
statement
  |- query
       |- select
       |   |- UPPER(name)
       |   |- AVG(age)
       |- from
       |   |- users
       |- where
       |   |- LOWER(city) = 'new york'
       |- group by
       |   |- department
       |- having
           |- COUNT(*) > 5

此刻,SqlBaseBaseVisitor拜访AST节点并遍历一切节点的进程与之前相似,但需求处理额定的函数节点:

  1. 首要,解析器将调用visit(ctx.statement)作为进口点,其间ctx是一个SqlBaseParser.StatementContext目标。这将触发解析器拜访statement节点。

  2. visitStatement办法中,它会调用visitQuery办法,传入query节点。

  3. visitQuery办法中,解析器将持续拜访子节点:

    • 调用visitSelect办法,传入select节点,然后拜访UPPER(name)AVG(age)子节点。在拜访这些节点时,它会调用相应的函数处理办法。
    • 调用visitFromClause办法,传入from节点,然后拜访users子节点。
    • 调用visitWhere办法,传入where节点,然后拜访LOWER(city) = 'new york'子节点。在拜访这个节点时,它会调用LOWER函数的处理办法。
    • 调用visitGroupBy办法,传入group by节点,然后拜访department子节点。
    • 调用visitHaving办法,传入having节点,然后拜访COUNT(*) > 5子节点。在拜访这个节点时,它会调用COUNT函数的处理办法。

关于包括多个处理函数的SQL查询,SqlBaseBaseVisitor会依照上述次序拜访AST的各个节点,处理每个函数节点,然后构建未解析的逻辑方案。在逻辑方案剖析阶段,剖析器将解析未解析的逻辑方案中的表名、列名、数据类型和函数,生成一个彻底解析的逻辑方案。

让咱们剖析visit(ctx.statement)办法及其背面的逻辑:
visit(ctx.statement).asInstanceOf[LogicalPlan]是在CatalystSqlParser中将SQL文本解析为逻辑方案的要害部分。这行代码表明ANTLR4生成的解析器开端遍历笼统语法树(AST),并生成未解析的逻辑方案。

  1. ctx是一个SqlBaseParser.StatementContext目标,它表明ANTLR4生成的解析器解析SQL文本后的上下文。ctx.statement表明SQL查询的顶层节点。
  2. visit()办法是SqlBaseBaseVisitor类的一个实例办法,用于遍历AST并创立逻辑方案树。visit(ctx.statement)表明从顶层节点开端遍历AST,并依据AST节点类型调用相应的visit*办法。
  3. .asInstanceOf[LogicalPlan]是一个类型转化操作,将visit()办法的成果转化为LogicalPlan类型。由于visit()办法回来的是一个泛型类型T,咱们需求将其转化为LogicalPlan类型,以便后续的逻辑方案处理和履行。

经过visit(ctx.statement).asInstanceOf[LogicalPlan]这行代码,CatalystSqlParser类将SQL文本解析为一个未解析的逻辑方案。然后,在逻辑方案剖析阶段,剖析器将解析未解析的逻辑方案中的表名、列名和数据类型,生成一个彻底解析的逻辑方案。

4.再深入visit源码 里面代码:

SqlBaseBaseVisitor类中,visit办法的完成如下:

scalaCopy code
override def visit(tree: ParseTree): T = {
  tree.accept(this)
}

这儿的visit办法接纳一个ParseTree类型的参数tree,该参数表明ANTLR4生成的笼统语法树(AST)的节点。tree.accept(this)是调用ParseTreeaccept办法,并将当时SqlBaseBaseVisitor实例作为参数传递。这个调用是ANTLR4遍历AST的要害部分。

accept办法完成坐落ANTLR4的 SingleStatementContext类中也就是上面 ctx.statement’,其源码如下:

public static class SingleStatementContext extends ParserRuleContext {
   public StatementContext statement() {
      return getRuleContext(StatementContext.class,0);
   }
   public TerminalNode EOF() { return getToken(SqlBaseParser.EOF, 0); }
   public SingleStatementContext(ParserRuleContext parent, int invokingState) {
      super(parent, invokingState);
   }
   @Override public int getRuleIndex() { return RULE_singleStatement; }
   @Override
   public void enterRule(ParseTreeListener listener) {
      if ( listener instanceof SqlBaseListener ) ((SqlBaseListener)listener).enterSingleStatement(this);
   }
   @Override
   public void exitRule(ParseTreeListener listener) {
      if ( listener instanceof SqlBaseListener ) ((SqlBaseListener)listener).exitSingleStatement(this);
   }
   @Override
   public <T> T accept(ParseTreeVisitor<? extends T> visitor) {
      if ( visitor instanceof SqlBaseVisitor) return ((SqlBaseVisitor<? extends T>)visitor).visitSingleStatement(this);
      else return visitor.visitChildren(this);
   }
}

这个办法承受一个ParseTreeVisitor类型的参数visitor,在咱们的情况下,它是SqlBaseBaseVisitor实例。这个办法首要检查传入的visitor是否是ParseTreeVisitor类型。假如是,它将调用visitor.visit(this);否则,它将调用visitChildren(visitor)。在咱们的场景中,visitor确实是一个ParseTreeVisitor类型的实例,因而visitor.visit(this)将被调用。

visitor.visit(this)调用将使遍历进入下一个层次的AST节点。SqlBaseBaseVisitor类的每个visit*办法将处理AST中的特定类型节点,然后逐渐构建未解析的逻辑方案。

经过这种方式,ANTLR4使用tree.accept(this)遍历AST,SqlBaseBaseVisitor类处理各种类型的节点,并逐渐生成未解析的逻辑方案。在逻辑方案剖析阶段,剖析器将解析未解析的逻辑方案中的表名、列名和数据类型,生成一个彻底解析的逻辑方案。

visitor.visit(this) 和 visitChildren(visitor) 差异是什么

visitor.visit(this)visitChildren(visitor)之间的首要差异在于它们在遍历笼统语法树(AST)时所采取的行为。

  1. visitor.visit(this):这个办法调用visitor目标中与当时节点类型匹配的visit*办法。在咱们的场景中,visitor是一个SqlBaseBaseVisitor实例,它包括一系列为特定节点类型编写的visit*办法。当调用visitor.visit(this)时,将依据当时节点类型履行相应的visit*办法。这是一种定制化的处理进程,针对不同类型的节点进行特定的操作。
  2. visitChildren(visitor):这个办法用于遍历当时节点的一切子节点,并依次调用这些子节点的accept(visitor)办法。它是一种通用办法,用于处理那些没有特定visit*办法的节点。visitChildren(visitor)保证整个AST的一切节点都被拜访和处理。

以下是它们之间的首要差异:

  • visitor.visit(this)针对详细的节点类型履行特定的操作,而visitChildren(visitor)则是一种通用的处理办法,遍历当时节点的一切子节点。
  • visitor.visit(this)通常用于处理那些需求特定操作的节点类型,而visitChildren(visitor)则用于处理那些没有为其完成特定visit*办法的节点类型。

在遍历AST时,假如有针对特定节点类型的visit*办法,那么visitor.visit(this)将被调用,以履行定制化的处理。假如没有为特定节点类型完成visit*办法,那么visitChildren(visitor)将被调用,以保证整个AST的一切节点都能被拜访和处理。

那给一个嵌套 的select 句子生成语法树结构是什么?

假定咱们有一个包括嵌套查询的SQL查询,例如:SELECT name, AVG(age) FROM (SELECT name, age FROM users WHERE age > 18) AS filtered_users GROUP BY name。关于这个查询,笼统语法树(AST)结构将相似于以下层次结构:

csharpCopy code
statement
  |- query
       |- select
       |   |- name
       |   |- AVG(age)
       |- from
       |   |- subquery
       |       |- query
       |           |- select
       |           |   |- name
       |           |   |- age
       |           |- from
       |           |   |- users
       |           |- where
       |               |- age > 18
       |- group by
           |- name

此刻,SqlBaseBaseVisitor会依据以下进程拜访AST节点并遍历一切节点:

  1. 首要,解析器将调用visit(ctx.statement)作为进口点,其间ctx是一个SqlBaseParser.StatementContext目标。这将触发解析器拜访statement节点。

  2. visitStatement办法中,它会调用visitQuery办法,传入外部查询的query节点。

  3. 在外部查询的visitQuery办法中,解析器将持续拜访子节点:

    • 调用visitSelect办法,传入外部查询的select节点,然后拜访nameAVG(age)子节点。
    • 调用visitFromClause办法,传入外部查询的from节点,然后拜访subquery子节点。
    • 在拜访subquery节点时,解析器将调用内部查询的visitQuery办法,传入内部查询的query节点。
  4. 在内部查询的visitQuery办法中,解析器将持续拜访子节点:

    • 调用visitSelect办法,传入内部查询的select节点,然后拜访nameage子节点。
    • 调用visitFromClause办法,传入内部查询的from节点,然后拜访users子节点。
    • 调用visitWhere办法,传入内部查询的where节点,然后拜访age > 18子节点。
  5. 回到外部查询的visitQuery办法中,解析器将持续拜访子节点:

    • 调用visitGroupBy办法,传入外部查询的group by节点,然后拜访name子节点。

关于嵌套的SELECT句子,SqlBaseBaseVisitor会依照上述次序拜访AST的各个节点,然后构建未解析的逻辑方案。在逻辑方案剖析阶段,剖析器将解析未解析的逻辑方案中的表名、列名和数据类型,生成一个彻底解析的逻辑方案。