从《Spark SQL实现原理-逻辑计划的创建》一文我们知道,通过创建和操作Dataset会创建一个逻辑计划树。但在创建逻辑计划树时有些属性的类型等信息,此时并不知道,把这些属性称为未解析的(Unresolved)。逻辑计划的分析这一步就是要处理这些未解析的属性,并使用合法性检查规则对表达式的合法性进行检查。

实现层面,Spark SQL使用Catalyst逻辑计划分析规则和一个Catalog对象跟踪所有数据源中的表来解析这些属性。

逻辑计划分析流程

逻辑计划的分析是在QueryExecution中完成,其实现代码相对简单:

lazy val analyzed: LogicalPlan = {
  SparkSession.setActiveSession(sparkSession)
  sparkSession.sessionState.analyzer.executeAndCheck(logical)
}

通过以上代码可以知,逻辑计划(LogicalPlan)的分析是通过分析器对象analyzer来完成的。分析器是一个Analyzer类的对象。

逻辑计划分析的函数调用流程如下:
Analyzer#executeAndCheck(logical)
 ->Analyzer#execute
   ->Analyzer#executeSameContext
     ->RuleExecutor#execute

当RuleExecutor#execute函数执行完成会,得到一个优化后的逻辑计划。

逻辑计划分析规则的执行:RuleExecutor#execute

逻辑计划分析的执行在RuleExecutor#execute中完成,其实现的逻辑如下:

1.按顺序遍历规则集列表:batches中的每一个规则集(Batch);

2.顺序遍历规则集中的每一个规则(Rule),并使用每一个规则来分析逻辑计划,每个处理结果传给下一个处理规则。

3.当一个规则集中的规则都遍历(使用)完成后,进行以下判断:

1)查看是否达到执行策略的阈值(迭代次数),若已经大于等于阈值,则不再遍历和执行当前规则集。

2)查看使用本次规则集前后的逻辑计划是否相等,若相等说明不需要再执行当前规则集了。

满足1)和2)中的任意一个,直接跳到4执行,否则继续使用当前规则集。

4.遍历和使用下一个规则集的每个规则,并按第3步的逻辑进行处理。

该函数的实现代码如下(有删减):

def execute(plan: TreeType): TreeType = {
  var curPlan = plan
  val queryExecutionMetrics = RuleExecutor.queryExecutionMeter

  // 遍历每个规则集batch
  batches.foreach { batch =>
    val batchStartPlan = curPlan
    var iteration = 1
    var lastPlan = curPlan
    var continue = true

    // 运行到fixpoint次数,或逻辑计划不再改变为止
    while (continue) {
      // 遍历每个规则集中的每个规则,使用每个规则来处理plan(效果叠加),并得到最终结果
      curPlan = batch.rules.foldLeft(curPlan) {
        case (plan, rule) =>
          val startTime = System.nanoTime()
          val result = rule(plan)
          val runTime = System.nanoTime() - startTime

        	// 若和初始的plan不相等,打印Trace级别的调试语句
          if (!result.fastEquals(plan)) {
            queryExecutionMetrics.incNumEffectiveExecution(rule.ruleName)
            queryExecutionMetrics.incTimeEffectiveExecutionBy(rule.ruleName, runTime)
            logTrace(
              s"""
                |=== Applying Rule ${rule.ruleName} ===
                |${sideBySide(plan.treeString, result.treeString).mkString("\n")}
              """.stripMargin)
          }
          queryExecutionMetrics.incExecutionTimeBy(rule.ruleName, runTime)
          queryExecutionMetrics.incNumExecution(rule.ruleName)

          // 检查分析后的逻辑计划树的结构是否完整
          if (!isPlanIntegral(result)) {
            ...
            throw new TreeNodeException(result, message, null)
          }
          result
      } // end foldLeft: 完成规则集中规则的遍历和应用

      // 迭代次数+1
      iteration += 1
      // 大于迭代策略(次数)不在分析了
      if (iteration > batch.strategy.maxIterations) {
        ...
        continue = false
      }
			// 若plan不在变化,说明分析已经完成
      if (curPlan.fastEquals(lastPlan)) {
        logTrace(
          s"Fixed point reached for batch ${batch.name} ...")
        continue = false
      }
      lastPlan = curPlan
    } // end while

    // 若开始的plan和目前的不相等,说明已经把rule使用到plan上,否则该规则集不起作用
    if (!batchStartPlan.fastEquals(curPlan)) {
      logDebug(
        s"""
          |=== Result of Batch ${batch.name} ===
          |${sideBySide(batchStartPlan.treeString, curPlan.treeString).mkString("\n")}
        """.stripMargin)
    } else {
      logTrace(s"Batch ${batch.name} has no effect.")
    }
  }
  // 返回分析完成的逻辑计划
  curPlan
}

分析器:Analyzer类

Analyzer是一个逻辑计划分析器,它使用SessionCatalog中的信息将没分析的属性(UnresolvedAttribute)和没分析的关系(UnresolvedRelation)转换为完全类型化的对象。

分析器的类继承关系。

RuleExecutor
     |
  Analyzer

Analyzer对象最终会调用RuleExecutor#execute函数来实现对逻辑计划的分析。

逻辑计划分析规则

在Analyzer中定义了多种类型的分析规则集(保存在变量batches中),每个规则集包括多个规则。根据源码中对分析规则的分类,主要有以下几类分析规则:

  • 线索提示类:用户通过提示符(一般是函数)来告诉SparkSQL需要完成的操作。比如:在执行join时调用broadcast函数来提示要广播某个数据集。
  • 简单健全性检查类:在2.4中只定义了一种分析规则(LookupFunctions),该规则只检查UnresolvedFunction标识的函数在注册表中是否存在,但不会对它进行解析。
  • 替换类:通过一定的规则来替换儿子节点的计划。
  • 解析类(Resolution):此类规则有很多,它们使用具体的表达式替换未解析的属性。比如:通过具体的函数表达(比如:AggregateWindowFunction等)来替换UnresolvedFunction标识的函数。
  • UDF类:在2.4中定义了一个分析规则HandleNullInputsForUDF。该规则:通过添加额外的[[If]]表达式来执行空值检查,正确处理 UDF 的空的原始输入。
  • 子查询类:分析子查询。把来自引用外部查询块的子查询的聚合表达式,下推到外部查询块进行评估。
  • 清除类:2.4中只定义了一个CleanupAliases规则,该规则用来清除计划内不必要的别名(Aliases)。

规则执行器:RuleExecutor

RuleExecutor类是规则执行器的基础类,负责执行一批(规则)集合来转换逻辑计划树的节点:TreeNode。

abstract class RuleExecutor[TreeType <: TreeNode[_]] extends Logging {
  /** Defines a sequence of rule batches, to be overridden by the implementation. */
  /** 定义一系列规则集,由实现覆盖。 */
  protected def batches: Seq[Batch]
  
  // 批量的规则应用到plan中 
  // 批次使用定义的执行策略连续执行。在每个批次中,规则也按顺序执行。
  def execute(plan: TreeType): TreeType = {
    //...  ...
  }
}
规则集(batchs)

每个规则集是通过一个Batch类来定义的,该类的实现代码如下:

// 在类:RuleExecutor中
protected case class Batch(name: String, strategy: Strategy, rules: Rule[TreeType]*)

该类各个成员变量的说明如下:

  • name
    该规则集的名称。
  • strategy
    它是一个Strategy对象,定义了执行该规则的策略,这里的策略主要是指:最大执行次数。定义如下:
// 指示最大执行次数的规则的执行策略。 如果执行在maxIterations次数之前达到固定点(即收敛),它将停止。
abstract class Strategy { def maxIterations: Int }
// 仅执行一次的策略
case object Once extends Strategy { val maxIterations = 1 }
// 运行到fix点或最大次数停止的策略
case class FixedPoint(maxIterations: Int) extends Strategy
  • rules
    Rule是一种命名转换规则,可以应来转换一个逻辑计划树的节点,从而生成一个新的节点。
Spark2.4定义的逻辑计划分析规则集

在batches变量中定义了各种规则集,定义代码(有删减)如下:

lazy val batches: Seq[Batch] = Seq(
    Batch("Hints", fixedPoint,
      new ResolveHints.ResolveBroadcastHints(conf),
      ResolveHints.ResolveCoalesceHints,
      ResolveHints.RemoveAllHints),
    Batch("Simple Sanity Check", Once,
      LookupFunctions),
    Batch("Substitution", fixedPoint,
      CTESubstitution,
      WindowsSubstitution,
      EliminateUnions,
      new SubstituteUnresolvedOrdinals(conf)),
    Batch("Resolution", fixedPoint,
      // ...    
      ResolveRandomSeed ::
      TypeCoercion.typeCoercionRules(conf) ++
      extendedResolutionRules : _*),
    Batch("Post-Hoc Resolution", Once, postHocResolutionRules: _*),
    Batch("Nondeterministic", Once,
      PullOutNondeterministic),
    Batch("UDF", Once,
      HandleNullInputsForUDF),
    Batch("FixNullability", Once,
      FixNullability),
    Batch("Subquery", Once,
      UpdateOuterReferences),
    Batch("Cleanup", fixedPoint,
      CleanupAliases)

注意:这个分析规则集不是固定的,对于hive的session来说,还会增加额外的规则集。

这里定义了每种规则集的名称、执行策略和规则列表。定义好了规则集,就可以使用这些规则集对逻辑计划树进行分析了。具体规则集的分析流程前面已经分析过了,见前面RuleExecutor#execute的执行流程。

小结

本文分析了通过逻辑计划分析规则来处理逻辑计划的总体流程,接下来就可以对每个具体的规则进行分析了。