seq搜索引擎优化至少包括那几步?(逻辑计划优化(LogicalLogical)阶段把标准的基于规则(Rule-based)的优化策略应用)
优采云 发布时间: 2022-01-30 09:16seq搜索引擎优化至少包括那几步?(逻辑计划优化(LogicalLogical)阶段把标准的基于规则(Rule-based)的优化策略应用)
逻辑优化阶段将标准的基于规则的优化策略应用于已分析的已解决逻辑计划。
优化规则的分类
逻辑计划的默认优化规则集在 Optimizer#defaultBatches 变量中定义。与逻辑计划的分析规则一样,逻辑计划的优化规则也是以规则集(Batch对象)的形式组织起来的,每个规则集中收录多个优化规则。
规则集定义及实现代码如下(有删减):
def defaultBatches: Seq[Batch] = {
...
Batch("Union", Once, CombineUnions) ::
Batch("LocalRelation early", fixedPoint, ...) ::
Batch("Pullup Correlated Expressions", Once, ...) ::
Batch("Subquery", Once, OptimizeSubqueries) ::
Batch("Replace Operators", fixedPoint,...) ::
Batch("Aggregate", fixedPoint, ...) :: Nil ++) :+
Batch("Join Reorder", Once, ...) :+
Batch("Remove Redundant Sorts", Once, ...) :+
Batch("Decimal Optimizations", fixedPoint, ...) :+
...
由于优化规则集数量众多,在某些情况下并非所有规则集都需要使用。为了让用户排除一些不必要的规则集,Spark SQL 增加了一个配置项:spark.sql.optimizer.excludedRules,默认为null。该配置项可用于配置要排除的优化规则名称列表,以逗号分隔。这些规则存储在 excludeRules 变量中。
排除优化规则的选项为用户提供了一些控制权,但对于 Spark SQL,一些优化规则是必需的,不能删除。因此,Spark SQL 在优化器中定义了另一个变量:nonExcludableRules,用于保存必须保留的优化规则。其代码实现(有删减)如下:
def nonExcludableRules: Seq[String] =
EliminateDistinct.ruleName ::
EliminateSubqueryAliases.ruleName ::
EliminateView.ruleName ::
ReplaceExpressions.ruleName ::
ComputeCurrentTime.ruleName ::
...
因此,最终使用的规则集是:
(defaultBatches - (excludedRules - nonExcludableRules))
// 也就是
默认规则集 -(排除规则集 - 保留规则集)
可以理解,原则上是默认规则集减去用户配置的排除规则集,但系统保留的规则集不能排除,所以必须从用户配置的列表中减去。换句话说,即使用户配置了要排除的规则集列表,如果它们在 nonExcludableRules(系统保留规则集)中,它们也不会被排除。
运营优化规则集
在优化规则集中,有一大类:操作优化规则集。操作优化规则集定义了操作的各种优化,是我们在查看逻辑计划时经常可以看到的非常重要的优化规则。操作优化规则集包括:
优化器:优化器
Optimizer 类对象实现逻辑计划规则的优化。Optimizer 是一个抽象父类,只有一个实现类:SparkOptimizer。Optimizer类继承了RuleExecutor,它们之间的关系如下:
RuleExecutor
|
Optimizer
|
SparkOptimizer
各种逻辑计划的优化规则集定义在抽象父类Optimizer中,实现类可以直接使用这些规则。
执行逻辑计划优化
在前面对文章的分析中,执行逻辑计划优化的函数调用如下:
// 2.对分析后的逻辑计划进行优化,得到优化后的逻辑计划
lazy val optimizedPlan: LogicalPlan = sparkSession.sessionState.optimizer.execute(withCachedData)
这个函数最终调用了父类的execute函数,也就是RuleExecutor中定义的execute函数。该功能的实现逻辑在《Spark SQL实现原理——逻辑计划分析的实现》一文中已有介绍。大致思路是依次遍历逻辑计划树的各个节点,根据优化规则集和执行策略对逻辑计划树的各个节点进行处理,直到逻辑计划树没有变化或者执行阈值达到到达。具体实现逻辑如下:
1. 依次遍历规则集列表:Optimizer#batches中的每个规则集(Batch);
2. 依次遍历规则集中的每条规则(Rule),并使用每条规则处理逻辑计划,将每条处理结果传递给下一条处理规则。
3.当一个规则集中的所有规则都被遍历(使用)后,会做出如下判断:
1)检查是否达到执行策略的阈值(迭代次数)。如果大于等于阈值,则不会遍历执行当前规则集。
2)检查使用该规则集前后的逻辑计划是否相等。如果相等,则表示不需要执行当前规则集。
如果满足1)和2)中的任何一个,则跳到4执行,否则继续使用当前规则集。
4.下一个规则集的每一个规则都被遍历使用,并按照步骤3的逻辑进行处理。
如何编写优化规则
除了 Spark SQL 自带的各种逻辑计划优化规则集外,您还可以编写自己的优化规则。《Spark SQL:Spark 中的关系数据处理》一文中介绍了自定义优化规则。
这条规则的目的是:在Spark SQL中添加一个固定精度的DECIMAL类型时,想在一个小精度的DECIMAL上优化求和或平均等聚合操作;用 12 行代码编写一个规则,用 SUM 和 AVG 表示 在公式中找到这样的小数,将它们转换为未缩放的 64 位 LONG,将它们聚合,然后将结果转换回 DECIMAL 类型。
object DecimalAggregates extends Rule[LogicalPlan] {
/** Maximum number of decimal digits in a Long */
val MAX_LONG_DIGITS = 18
def apply(plan: LogicalPlan): LogicalPlan = {
plan transformAllExpressions {
case Sum(e @ DecimalType.Expression(prec , scale))
if prec + 10
MakeDecimal(Sum(LongValue(e)), prec + 10, scale)
}
}
能够在规则中使用任意 Scala 代码使得表达这些超越子树结构模式匹配的优化变得容易。可见,编写逻辑计划优化规则并不难,只要遵循以下接口的编写规范即可。
object YourName extends Rule[LogicalPlan] {
// ...
def apply(plan: LogicalPlan): LogicalPlan = {
plan transformAllExpressions {
case xx1(...) if ... => // xx1是你想优化的逻辑计划节点对象
// ...
xxx2(...) // 优化后的目标逻辑计划节点对象
}
}
但是,要编写逻辑计划优化规则,首先需要熟悉现有的优化规则和每个逻辑计划节点,然后根据需求抽象出需要优化的逻辑。
逻辑计划优化视图
查看优化后的逻辑计划有多种方式,以scala终端为例。
(1)通过解释查看(true)
通过explain(true)可以看到从整个逻辑计划到物理计划的全过程:
scala> var ds1 = spark.range(100)
ds1: org.apache.spark.sql.Dataset[Long] = [id: bigint]
scala> var ds2 = spark.range(200)
ds2: org.apache.spark.sql.Dataset[Long] = [id: bigint]
scala> ds1.filter("id>10").union(ds2).filter("id>20").select("id").explain(true)
== Parsed Logical Plan ==
'Project [unresolvedalias('id, None)]
+- Filter (id#0L > cast(20 as bigint))
+- Union
:- Filter (id#0L > cast(10 as bigint))
: +- Range (0, 100, step=1, splits=Some(1))
+- Range (0, 200, step=1, splits=Some(1))
== Analyzed Logical Plan ==
id: bigint
Project [id#0L]
+- Filter (id#0L > cast(20 as bigint))
+- Union
:- Filter (id#0L > cast(10 as bigint))
: +- Range (0, 100, step=1, splits=Some(1))
+- Range (0, 200, step=1, splits=Some(1))
== Optimized Logical Plan ==
Union
:- Filter ((id#0L > 10) && (id#0L > 20))
: +- Range (0, 100, step=1, splits=Some(1))
+- Filter (id#2L > 20)
+- Range (0, 200, step=1, splits=Some(1))
//...
另外,可以通过以下命令查看逻辑计划节点和参数:
scala> ds1.filter("id>10").union(ds2).filter("id>20").select("id").queryExecution.optimizedPlan.prettyJson
(2)通过queryExecution对象查看
通过queryExecution,可以单独查看优化后的逻辑计划。
scala> ds1.filter("id>10").union(ds2).filter("id>20").select("id").queryExecution.optimizedPlan
res9: org.apache.spark.sql.catalyst.plans.logical.LogicalPlan =
Union
:- Filter ((id#0L > 10) && (id#0L > 20))
: +- Range (0, 100, step=1, splits=Some(1))
+- Filter (id#2L > 20)
+- Range (0, 200, step=1, splits=Some(1))
概括
本文分析了逻辑计划优化的总体实现过程,并简要介绍了实现自己的优化规则的优化规则。最后介绍了如何查看逻辑计划优化的结果。逻辑计划的优化可以说是 Catalyst 项目的核心。接下来,我们将通过一系列文章来介绍各种逻辑计划优化规则的使用和实现原理。