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的相反

被折叠的 条评论
为什么被折叠?



