Spark SQL Catalyst源码分析之Analyzer

本文详细剖析了Spark SQL中的Analyzer组件,介绍了Analyzer如何将未解析的Logical Plan转换为完全类型化的对象,涉及到的关键步骤包括MultiInstanceRelation、ResolveReferences等规则的执行。Analyzer使用Catalog和FunctionRegistry,通过RuleExecutor的固定点迭代策略来处理Logical Plan,实现Hive类型兼容转换。文章还列举了Analyzer执行的一系列规则,并提供了实践和总结。

    /** Spark SQL源码分析系列文章*/

    前面几篇文章讲解了Spark SQL的核心执行流程和Spark SQL的Catalyst框架的Sql Parser是怎样接受用户输入sql,经过解析生成Unresolved Logical Plan的。我们记得Spark SQL的执行流程中另一个核心的组件式Analyzer,本文将会介绍Analyzer在Spark SQL里起到了什么作用。

    Analyzer位于Catalyst的analysis package下,主要职责是将Sql Parser 未能Resolved的Logical Plan 给Resolved掉。

    

一、Analyzer构造

    Analyzer会使用Catalog和FunctionRegistry将UnresolvedAttribute和UnresolvedRelation转换为catalyst里全类型的对象。

    Analyzer里面有fixedPoint对象,一个Seq[Batch].

class Analyzer(catalog: Catalog, registry: FunctionRegistry, caseSensitive: Boolean)
  extends RuleExecutor[LogicalPlan] with HiveTypeCoercion {

  // TODO: pass this in as a parameter.
  val fixedPoint = FixedPoint(100)

  val batches: Seq[Batch] = Seq(
    Batch("MultiInstanceRelations", Once,
      NewRelationInstances),
    Batch("CaseInsensitiveAttributeReferences", Once,
      (if (caseSensitive) Nil else LowercaseAttributeReferences :: Nil) : _*),
    Batch("Resolution", fixedPoint,
      ResolveReferences ::
      ResolveRelations ::
      NewRelationInstances ::
      ImplicitGenerate ::
      StarExpansion ::
      ResolveFunctions ::
      GlobalAggregates ::
      typeCoercionRules :_*),
    Batch("AnalysisOperators", fixedPoint,
      EliminateAnalysisOperators)
  )
    Analyzer里的一些对象解释:

    FixedPoint:相当于迭代次数的上限。

  /** A strategy that runs until fix point or maxIterations times, whichever comes first. */
  case class FixedPoint(maxIterations: Int) extends Strategy

    Batch: 批次,这个对象是由一系列Rule组成的,采用一个策略(策略其实是迭代几次的别名吧,eg:Once)

  /** A batch of rules. */,
  protected case class Batch(name: String, strategy: Strategy, rules: Rule[TreeType]*)
    Rule:理解为一种规则,这种规则会应用到Logical Plan 从而将UnResolved 转变为Resolved

abstract class Rule[TreeType <: TreeNode[_]] extends Logging {

  /** Name for this rule, automatically inferred based on class name. */
  val ruleName: String = {
    val className = getClass.getName
    if (className endsWith "$") className.dropRight(1) else className
  }

  def apply(plan: TreeType): TreeType
}

    Strategy:最大的执行次数,如果执行次数在最大迭代次数之前就达到了fix point,策略就会停止,不再应用了。

  /**
   * An execution strategy for rules that indicates the maximum number of executions. If the
   * execution reaches fix point (i.e. converge) before maxIterations, it will stop.
   */
  abstract class Strategy { def maxIterations: Int }

   Analyzer解析主要是根据这些Batch里面定义的策略和Rule来对Unresolved的逻辑计划进行解析的。

   这里Analyzer类本身并没有定义执行的方法,而是要从它的父类RuleExecutor[LogicalPlan]寻找,Analyzer也实现了HiveTypeCosercion,这个类是参考Hive的类型自动兼容转换的原理。如图:

    

    RuleExecutor:执行Rule的执行环境,它会将包含了一系列的Rule的Batch进行执行,这个过程都是串行的。

    具体的执行方法定义在apply里:

    可以看到这里是一个while循环,每个batch下的rules都对当前的plan进行作用,这个过程是迭代的,直到达到Fix Point或者最大迭代次数。

 def apply(plan: TreeType): TreeType = {
    var curPlan = plan

    batches.foreach { batch =>
      val batchStartPlan = curPlan
      var iteration = 1
      var lastPlan = curPlan
      var continue = true

      // Run until fix point (or the max number of iterations as specified in the strategy.
      while (continue) {
        curPlan = batch.rules.foldLeft(curPlan) {
          case (plan, rule) =>
            val result = rule(plan) //这里将调用各个不同Rule的apply方法,将UnResolved Relations,Attrubute和Function进行R
要实现 Spark SQL 字段血缘分析,可以通过继承 `org.apache.spark.sql.catalyst.analysis.Analyzer` 类来实现自定义的分析器。下面是一个 Java 版本的实现: ```java import java.util.HashSet; import java.util.Set; import org.apache.spark.sql.catalyst.analysis.Analyzer; import org.apache.spark.sql.catalyst.expressions.Alias; import org.apache.spark.sql.catalyst.expressions.Attribute; import org.apache.spark.sql.catalyst.expressions.Expression; import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan; import org.apache.spark.sql.catalyst.plans.logical.Project; import org.apache.spark.sql.types.DataType; public class FieldLineageAnalyzer extends Analyzer { public FieldLineageAnalyzer() { super(); } @Override public LogicalPlan execute(LogicalPlan plan) { FieldLineageVisitor visitor = new FieldLineageVisitor(); visitor.visit(plan); return super.execute(plan); } private static class FieldLineageVisitor { private Set<String> currentFields = new HashSet<String>(); public void visit(LogicalPlan plan) { plan.transformExpressions(expr -> { if (expr instanceof Attribute) { Attribute attr = (Attribute) expr; String fieldName = attr.name(); DataType dataType = attr.dataType(); LineageUtils.addLineage(fieldName, dataType, currentFields); } else if (expr instanceof Alias) { Alias alias = (Alias) expr; Attribute attr = alias.toAttribute(); String fieldName = alias.name(); DataType dataType = attr.dataType(); LineageUtils.addLineage(fieldName, dataType, currentFields); } return expr; }); if (plan instanceof Project) { Project project = (Project) plan; currentFields = new HashSet<String>(); for (Expression expr : project.projectList()) { expr.foreach(attr -> { if (attr instanceof Attribute) { String fieldName = ((Attribute) attr).name(); currentFields.add(fieldName); } }); } } plan.children().forEach(child -> visit(child)); } } } ``` 这个 `FieldLineageAnalyzer` 类继承了 `Analyzer` 类,并覆盖了 `execute` 方法。在 `execute` 方法中,它首先创建了一个 `FieldLineageVisitor` 实例,并调用它的 `visit` 方法来遍历逻辑计划,执行字段血缘分析。 `FieldLineageVisitor` 类实现了逻辑计划的遍历,并在遍历过程中,使用 `LineageUtils` 类来处理字段血缘关系。在遍历 `Project` 节点时,它会根据 `projectList` 中的表达式来确定当前字段集合。 `LineageUtils` 类用于处理字段血缘关系,它的实现如下: ```java import java.util.Set; import java.util.stream.Collectors; import org.apache.spark.sql.types.DataType; public class LineageUtils { private static Set<FieldInfo> fieldInfoSet = new HashSet<FieldInfo>(); public static void addLineage(String fieldName, DataType dataType, Set<String> currentFields) { FieldInfo fieldInfo = new FieldInfo(fieldName, dataType, currentFields); fieldInfoSet.add(fieldInfo); } public static String getFieldLineage(String fieldName) { Set<FieldInfo> lineageSet = fieldInfoSet.stream() .filter(fieldInfo -> fieldInfo.getFieldName().equals(fieldName)) .collect(Collectors.toSet()); Set<String> sourceFields = new HashSet<String>(); for (FieldInfo fieldInfo : lineageSet) { Set<String> currentFields = fieldInfo.getCurrentFields(); if (currentFields.isEmpty()) { sourceFields.add(fieldInfo.getFieldName()); } else { for (String currentField : currentFields) { String sourceField = getFieldLineage(currentField); sourceFields.add(sourceField); } } } return String.join(", ", sourceFields); } private static class FieldInfo { private String fieldName; private DataType dataType; private Set<String> currentFields; public FieldInfo(String fieldName, DataType dataType, Set<String> currentFields) { this.fieldName = fieldName; this.dataType = dataType; this.currentFields = currentFields; } public String getFieldName() { return fieldName; } public DataType getDataType() { return dataType; } public Set<String> getCurrentFields() { return currentFields; } } } ``` 在 `LineageUtils` 类中,它定义了一个 `fieldInfoSet` 集合,用于保存字段的详细信息。在 `addLineage` 方法中,它首先构造一个 `FieldInfo` 对象,并将其添加到 `fieldInfoSet` 中。在 `getFieldLineage` 方法中,它根据字段名来查找对应的字段信息,并递归地处理血缘关系,最终返回源字段集合。 这个实现中,字段血缘关系的处理是在遍历逻辑计划的过程中完成的。在遍历每个表达式时,如果它是一个字段或别名,就将其添加到 `LineageUtils` 中。在遍历 `Project` 节点时,它会根据 `projectList` 中的表达式来决定当前字段集合。这样,就可以在执行 Spark SQL 语句时,同时获取到字段血缘关系信息了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值