sparksql源码解析-解析器源码分析

1,796 阅读18分钟

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的各个节点,然后构建未解析的逻辑计划。在逻辑计划分析阶段,分析器将解析未解析的逻辑计划中的表名、列名和数据类型,生成一个完全解析的逻辑计划。