Spark优化规则-InferFiltersFromConstraints

1.规则解析

  优化器的作用,从当前过滤条件推断出新条件
  从日志来看这个规则的应用在PushDownPredicates之后,但是应用完以后又进行了一次PushDownPredicates,不过普通的SQL第二次应用没有任何改变
  在Spark的Optimizer中,defaultBatches首先有一个operatorOptimizationRuleSet,其中包含了PushDownPredicates

val operatorOptimizationRuleSet =
  Seq(
    // Operator push down
    PushProjectionThroughUnion,
    ReorderJoin,
    EliminateOuterJoin,
    PushDownPredicates,

  之后进入operatorOptimizationBatch,包含InferFiltersFromConstraints,在最后还增加了PushDownPredicates

val operatorOptimizationBatch: Seq[Batch] = {
  Batch("Operator Optimization before Inferring Filters", fixedPoint,
    operatorOptimizationRuleSet: _*) ::
  Batch("Infer Filters", Once,
    InferFiltersFromGenerate,
    InferFiltersFromConstraints) ::
  Batch("Operator Optimization after Inferring Filters", fixedPoint,
    operatorOptimizationRuleSet: _*) ::
  // Set strategy to Once to avoid pushing filter every time because we do not change the
  // join condition.
  Batch("Push extra predicate through join", fixedPoint,
    PushExtraPredicateThroughJoin,
    PushDownPredicates) :: Nil
}

  优化器的执行逻辑在inferFilters方法中,分两个分支,一个是Filter,一个是Join
  join中又有三个分支,分别为inner(semi)、right、left(anti)

1.1.Filter分支

  产生新过滤器,核心逻辑如下

val newFilters = filter.constraints --
  (child.constraints ++ splitConjunctivePredicates(condition))

1.1.1.constraints

  核心逻辑如下

validConstraints
  .union(inferAdditionalConstraints(validConstraints))
  .union(constructIsNotNullConstraints(validConstraints, output))
  .filter { c =>
    c.references.nonEmpty && c.references.subsetOf(outputSet) && c.deterministic
  }

  其中validConstraints的在Filter中定义,表示一组约束集。后续这些约束会被优化,不如只包含输出集对应的列的约束。这里可以简单理解为做过处理的Filter的过滤条件

override protected lazy val validConstraints: ExpressionSet = {
  val predicates = splitConjunctivePredicates(condition)
    .filterNot(SubqueryExpression.hasCorrelatedSubquery)
  child.constraints.union(ExpressionSet(predicates))
}

1.1.2.inferAdditionalConstraints

  constraints中调用的一个核心方法,这个方法的功能是基于(a = 5, a = b)可以推断出b = 5,返回这个新增的条件
  主要逻辑如下,case eq @ EqualTo是Scala的一种语法,就是eq对象匹配EqualTo类

predicates.foreach {
  case eq @ EqualTo(l: Attribute, r: Attribute) =>
    val candidateConstraints = predicates - eq
    inferredConstraints ++= replaceConstraints(candidateConstraints, l, r)
    inferredConstraints ++= replaceConstraints(candidateConstraints, r, l)
  case eq @ EqualTo(l @ Cast(_: Attribute, _, _, _), r: Attribute) =>
    inferredConstraints ++= replaceConstraints(predicates - eq, r, l)
  case eq @ EqualTo(l: Attribute, r @ Cast(_: Attribute, _, _, _)) =>
    inferredConstraints ++= replaceConstraints(predicates - eq, l, r)
  case _ => // No inference
}

1.1.3.replaceConstraints

  从inferAdditionalConstraints的流程看,核心完成推导的应该是replaceConstraints这个方法,这个方法很简约,需要结合其他的概念去理解

private def replaceConstraints(
    constraints: ExpressionSet,
    source: Expression,
    destination: Expression): ExpressionSet = constraints.map(_ transform {
  case e: Expression if e.semanticEquals(source) => destination
})

1.1.4.constructIsNotNullConstraints

  增加非空约束。功能就是从比如(a > 5)这样的条件,推断出isNotNull(a)这个条件
  这里只对属于输出范围的列增加非空约束

// First, we propagate constraints from the null intolerant expressions.
var isNotNullConstraints = constraints.flatMap(inferIsNotNullConstraints(_))

// Second, we infer additional constraints from non-nullable attributes that are part of the
// operator's output
val nonNullableAttributes = output.filterNot(_.nullable)
isNotNullConstraints ++= nonNullableAttributes.map(IsNotNull)

1.2.Join分支

  三条分支的处理都差不多,只不过right join少了对right分支的处理,left join少了对left分支的处理,inner的整体逻辑如下

case _: InnerLike | LeftSemi =>
  val allConstraints = getAllConstraints(left, right, conditionOpt)
  val newLeft = inferNewFilter(left, allConstraints)
  val newRight = inferNewFilter(right, allConstraints)
  join.copy(left = newLeft, right = newRight)

1.2.1.getAllConstraints

  getAllConstraints就是获取所有的约束,基本上是基于Set元素不重复的特性进行全部遍历加入
  核心推导新条件应该也是在这个方法里,调用了前面提到的

inferAdditionalConstraints
baseConstraints.union(inferAdditionalConstraints(baseConstraints))

1.2.2.inferNewFilter

  这个主要调用了前面提到过的constructIsNotNullConstraints,用来推导非空条件
  也就是说,Filter和Join其实经历的过程都差不多,主要核心还是在两个方法:inferAdditionalConstraints、constructIsNotNullConstraints。其中constructIsNotNullConstraints只是推导非空条件的,其他条件的推导基于inferAdditionalConstraints,需要重点理解

2.本地调试

2.1.依赖导入

  使用IDEA构建scala项目,导入Spark依赖

<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-core_2.12</artifactId>
  <version>3.2.1</version>
</dependency>
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql_2.12</artifactId>
  <version>3.2.1</version>
</dependency>

  这里需要注意spark用的是scala 2.12编译的,所以scala-library也得用对应的大版本

<properties>
  <scala.version>2.12.8</scala.version>
</properties>
<dependencies>
  <dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>${scala.version}</version>
  </dependency>

2.2.本地调试代码

  需要设置master为local进行本地调试

object DebugSql {
  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder
      .appName("DebugSql")
      .master("local")
      .getOrCreate()

    import spark.implicits._//导入隐式的转化函数
    import spark.sql //导入sql函数

    //使用Seq造数据
    val df = spark.sparkContext.parallelize(Seq(
      (1,"n1-1"),
      (2,"n1-2"),
      (3,"n1-3"))).toDF("id", "name")//转化df的数据
    df.createTempView("t1")//创建表t1

    val df1 = spark.sparkContext.parallelize(Seq(
      (1,"n2-1"),
      (2,"n2-2"),
      (4,"n2-4"))).toDF("id", "name")
    df1.createTempView("t2")

    val ds=sql("SELECT * FROM t1 join t2 ON t1.id = t2.id and t1.id < 2;");

    ds.show()
    spark.stop()
  }
}

3.Debug结果

3.1.进入规则的情况

  选择规则的inner join分支处理的函数,直接运行到此,可以看到,join对应的左右子树,还有额外增加的conditionOpt,其实就是on的id相等的条件

在这里插入图片描述

3.2.getAllConstraints

  进入getAllConstraints方法,可以看到,当前条件下,首次进入产生的约束集合有四条

在这里插入图片描述

3.3.inferAdditionalConstraints

  上诉的四条约束进入inferAdditionalConstraints经过过滤,只剩下两条

在这里插入图片描述

  可以看到,在后续的模式匹配里,满足条件的是EqualTo的类,因此只有后一个进入下一层处理,即两张表字段相等的条件。同时基于predicates - eq的处理,剩下的一个t1.id < 2的条件成为candidateConstraints

在这里插入图片描述

  这里注意,inferredConstraints调试的时候会无法解析,直接右键转为object就可以了
  这一次的<条件是在左表的,所以完成左表向右表的推导之后就获得了新的条件

在这里插入图片描述

3.4.整体逻辑

  核心就是两个:1、基于EqualTo的判断,确认了source和destination的等价关系;2、在等价关系的基础上,利用replaceConstraints,把source相关的约束里的source全部替换成destination,这样新的条件就产生了
  调用replaceConstraints的时候目标就已经很明确了

private def replaceConstraints(
    constraints: ExpressionSet,
    source: Expression,
    destination: Expression): ExpressionSet = constraints.map(_ transform {
  case e: Expression if e.semanticEquals(source) => destination
})

  对于这个方法来说,constraints是传入的约束,此处为(id#8 < 2),重点是source和destination。按照这个方法,其实就是把constraints里的source都变为destination,可能具体的变的过程有点复杂,但本质就是这么个东西

在这里插入图片描述

  后面还要理解一下其他CASE分支的场景,Cast应该是一种特殊的格式

case eq @ EqualTo(l @ Cast(_: Attribute, _, _, _), r: Attribute) =>

3.5.transformDownWithPruning

  这个就是完成转换的一个方法,基于replaceConstraints里的case条件做转换,满足条件的其实就是前面说的source,确认是source以后就会换成destination

val afterRule = CurrentOrigin.withOrigin(origin) {
  rule.applyOrElse(this, identity[BaseType])
}

  这里基于TreeNode结构,进行了好几层的迭代反复调用,其实就是把传入的约束拆分成子节点,然后找source子节点的过程
  这里的过程是,首先以id#8 < 2作为TreeNode进入进行转换,没有变化

在这里插入图片描述

  之后有一个子节点迭代处理的过程

// Check if unchanged and then possibly return old copy to avoid gc churn.
if (this fastEquals afterRule) {
  val rewritten_plan = mapChildren(_.transformDownWithPruning(cond, ruleId)(rule))

  子节点其实就是id#8 < 2拆成了id#8和2,<应该不包含处理
  id#8进入的时候,根据条件,满足转换条件,于是完成了转换
在这里插入图片描述

3.6.left/right join

  sql改为SELECT * FROM t1 left join t2 ON t1.id = t2.id and t1.id < 2;
在这里插入图片描述

  在达到left join条件时,可以看到,t1.id = t2.id和t1.id < 2的约束都在conditionOpt里,这里在join的时候讲过,left join的时候left的条件不会下推,否则结果会不一样
  allConstraints拿到的结果跟前面inner的结果是一样的,都是新增了一个约束t2.id < 2,只是这里只把约束用在右表
  right就是left的相反

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值