spark sql逻辑计划和物理计划执行原理

一条 SQL 在 Apache Spark 之旅(中)

在 《一条 SQL 在 Apache Spark 之旅(上)》 文章中我们介绍了一条 SQL 在 Apache Spark 之旅的 Parser 和 Analyzer 两个过程,本文接上文继续介绍。

优化逻辑计划阶段 - Optimizer

在前文的绑定逻辑计划阶段对 Unresolved LogicalPlan 进行相关 transform 操作得到了 Analyzed Logical Plan,这个 Analyzed Logical Plan 是可以直接转换成 Physical Plan 然后在 Spark 中执行。但是如果直接这么弄的话,得到的 Physical Plan 很可能不是最优的,因为在实际应用中,很多低效的写法会带来执行效率的问题,需要进一步对Analyzed Logical Plan 进行处理,得到更优的逻辑算子树。于是, 针对 SQL 逻辑算子树的优化器 Optimizer 应运而生。

这个阶段的优化器主要是基于规则的(Rule-based Optimizer,简称 RBO),而绝大部分的规则都是启发式规则,也就是基于直观或经验而得出的规则,比如列裁剪(过滤掉查询不需要使用到的列)、谓词下推(将过滤尽可能地下沉到数据源端)、常量累加(比如 1 + 2 这种事先计算好) 以及常量替换(比如 SELECT * FROM table WHERE i = 5 AND j = i + 3 可以转换成 SELECT * FROM table WHERE i = 5 AND j = 8)等等。

与前文介绍绑定逻辑计划阶段类似,这个阶段所有的规则也是实现 Rule 抽象类,多个规则组成一个 Batch,多个 Batch 组成一个 batches,同样也是在 RuleExecutor 中进行执行,由于前文已经介绍了 Rule 的执行过程,本节就不再赘述。

那么针对前文的 SQL 语句,这个过程都会执行哪些优化呢?这里按照 Rule 执行顺序一一进行说明。

谓词下推

谓词下推在 Spark SQL 是由 PushDownPredicate 实现的,这个过程主要将过滤条件尽可能地下推到底层,最好是数据源。所以针对我们上面介绍的 SQL,使用谓词下推优化得到的逻辑计划如下:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

从上图可以看出,谓词下推将 Filter 算子直接下推到 Join 之前了(注意,上图是从下往上看的)。也就是在扫描 t1 表的时候会先使用 ((((isnotnull(cid#2) && isnotnull(did#3)) && (cid#2 = 1)) && (did#3 = 2)) && (id#0 > 50000)) && isnotnull(id#0) 过滤条件过滤出满足条件的数据;同时在扫描 t2 表的时候会先使用 isnotnull(id#8) && (id#8 > 50000) 过滤条件过滤出满足条件的数据。经过这样的操作,可以大大减少 Join 算子处理的数据量,从而加快计算速度。

列裁剪

列裁剪在 Spark SQL 是由 ColumnPruning 实现的。因为我们查询的表可能有很多个字段,但是每次查询我们很大可能不需要扫描出所有的字段,这个时候利用列裁剪可以把那些查询不需要的字段过滤掉,使得扫描的数据量减少。所以针对我们上面介绍的 SQL,使用列裁剪优化得到的逻辑计划如下:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

从上图可以看出,经过列裁剪后,t1 表只需要查询 id 和 value 两个字段;t2 表只需要查询 id 字段。这样减少了数据的传输,而且如果底层的文件格式为列存(比如 Parquet),可以大大提高数据的扫描速度的。

常量替换

常量替换在 Spark SQL 是由 ConstantPropagation 实现的。也就是将变量替换成常量,比如 SELECT * FROM table WHERE i = 5 AND j = i + 3 可以转换成 SELECT * FROM table WHERE i = 5 AND j = 8。这个看起来好像没什么的,但是如果扫描的行数非常多可以减少很多的计算时间的开销的。经过这个优化,得到的逻辑计划如下:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

我们的查询中有 t1.cid = 1 AND t1.did = t1.cid + 1 查询语句,从里面可以看出 t1.cid 其实已经是确定的值了,所以我们完全可以使用它计算出 t1.did。

常量累加

常量累加在 Spark SQL 是由 ConstantFolding 实现的。这个和常量替换类似,也是在这个阶段把一些常量表达式事先计算好。这个看起来改动的不大,但是在数据量非常大的时候可以减少大量的计算,减少 CPU 等资源的使用。经过这个优化,得到的逻辑计划如下:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

所以经过上面四个步骤的优化之后,得到的优化之后的逻辑计划为:

== Optimized Logical Plan ==

Aggregate [sum(cast(v#16 as bigint)) AS sum(v)#22L]

+- Project [(3 + value#1) AS v#16]

   +- Join Inner, (id#0 = id#8)

      :- Project [id#0, value#1]

      :  +- Filter (((((isnotnull(cid#2) && isnotnull(did#3)) && (cid#2 = 1)) && (did#3 = 2)) && (id#0 > 5)) && isnotnull(id#0))

      :     +- Relation[id#0,value#1,cid#2,did#3] csv

      +- Project [id#8]

         +- Filter (isnotnull(id#8) && (id#8 > 5))

            +- Relation[id#8,value#9,cid#10,did#11] csv

对应的图如下:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

到这里,优化逻辑计划阶段就算完成了。另外,Spark 内置提供了多达70个优化 Rule,详情请参见 这里

生成可执行的物理计划阶段 - SparkPlanner

前面介绍的逻辑计划在 Spark 里面其实并不能被执行的,为了能够执行这个 SQL,一定需要翻译成物理计划,到这个阶段 Spark 就知道如何执行这个 SQL 了。和前面逻辑计划绑定和优化不一样,这里使用的是策略(Strategy),而且前面介绍的逻辑计划绑定和优化经过 Transformations 动作之后,树的类型并没有改变,也就是说:Expression 经过 Transformations 之后得到的还是 Transformations ;Logical Plan 经过 Transformations 之后得到的还是 Logical Plan。而到了这个阶段,经过 Transformations 动作之后,树的类型改变了,由 Logical Plan 转换成 Physical Plan 了。

一个逻辑计划(Logical Plan)经过一系列的策略处理之后,得到多个物理计划(Physical Plans),物理计划在 Spark 是由 SparkPlan 实现的。多个物理计划再经过代价模型(Cost Model)得到选择后的物理计划(Selected Physical Plan),整个过程如下所示:

一条SQL在Spark之旅
如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众号:iteblog_hadoop

Cost Model 对应的就是基于代价的优化(Cost-based Optimizations,CBO,主要由华为的大佬们实现的,详见 SPARK-16026 ),核心思想是计算每个物理计划的代价,然后得到最优的物理计划。但是在目前最新版的 Spark 2.4.3,这一部分并没有实现,直接返回多个物理计划列表的第一个作为最优的物理计划,如下:

lazy val sparkPlan: SparkPlan = {

    SparkSession.setActiveSession(sparkSession)

    // TODO: We use next(), i.e. take the first plan returned by the planner, here for now,

    //       but we will implement to choose the best plan.

    planner.plan(ReturnAnswer(optimizedPlan)).next()

}

而 SPARK-16026 引入的 CBO 优化主要是在前面介绍的优化逻辑计划阶段 - Optimizer 阶段进行的,对应的 Rule 为 CostBasedJoinReorder,并且默认是关闭的,需要通过 spark.sql.cbo.enabled 或 spark.sql.cbo.joinReorder.enabled 参数开启。
所以到了这个节点,最后得到的物理计划如下:

== Physical Plan ==

*(3) HashAggregate(keys=[], functions=[sum(cast(v#16 as bigint))], output=[sum(v)#22L])

+- Exchange SinglePartition

   +- *(2) HashAggregate(keys=[], functions=[partial_sum(cast(v#16 as bigint))], output=[sum#24L])

      +- *(2) Project [(3 + value#1) AS v#16]

         +- *(2) BroadcastHashJoin [id#0], [id#8], Inner, BuildRight

            :- *(2) Project [id#0, value#1]

            :  +- *(2) Filter (((((isnotnull(cid#2) && isnotnull(did#3)) && (cid#2 = 1)) && (did#3 = 2)) && (id#0 > 5)) && isnotnull(id#0))

            :     +- *(2) FileScan csv [id#0,value#1,cid#2,did#3] Batched: false, Format: CSV, Location: InMemoryFileIndex[file:/iteblog/t1.csv], PartitionFilters: [], PushedFilters: [IsNotNull(cid), IsNotNull(did), EqualTo(cid,1), EqualTo(did,2), GreaterThan(id,5), IsNotNull(id)], ReadSchema: struct<id:int,value:int,cid:int,did:int>

            +- BroadcastExchange HashedRelationBroadcastMode(List(cast(input[0, int, true] as bigint)))

               +- *(1) Project [id#8]

                  +- *(1) Filter (isnotnull(id#8) && (id#8 > 5))

                     +- *(1) FileScan csv [id#8] Batched: false, Format: CSV, Location: InMemoryFileIndex[file:/iteblog/t2.csv], PartitionFilters: [], PushedFilters: [IsNotNull(id), GreaterThan(id,5)], ReadSchema: struct<id:int>

从上面的结果可以看出,物理计划阶段已经知道数据源是从 csv 文件里面读取了,也知道文件的路径,数据类型等。而且在读取文件的时候,直接将过滤条件(PushedFilters)加进去了。同时,这个 Join 变成了 BroadcastHashJoin,也就是将 t2 表的数据 Broadcast 到 t1 表所在的节点。图表示如下:

一条SQL在Spark之旅
 

到这里, Physical Plan 就完全生成了。由于篇幅的原因,剩余的 SQL 处理我将在下一篇文章进行介绍,包括代码生成(WholeStageCodeGen)以及执行相关的等东西,敬请关注。

转载本文请加上:转载自过往记忆(https://www.iteblog.com/)
本文链接: 【一条 SQL 在 Apache Spark 之旅(中)】(https://www.iteblog.com/archives/2562.html)

 

### Spark SQL Execution Plan Lineage Analysis 在 Spark SQL 中,执行计划(Execution Plan)是一个非常重要的概念,它描述了查询是如何被优化并最终被执行的过程。为了进行数据血缘分析(Lineage Analysis),可以利用 Spark SQL逻辑计划物理计划来追踪数据源及其转换过程。 #### 1. 执行计划的基础结构 Spark SQL 使用 Catalyst 查询优化器生成两种主要类型的计划逻辑计划(Logical Plan)物理计划(Physical Plan)。 - **逻辑计划**表示查询的抽象语法树(AST),它是基于关系代数构建的高层次表达式[^1]。 - **物理计划**则是经过优化后的低层次实现细节,用于实际的数据处理操作[^2]。 可以通过调用 `explain()` 方法查看某个 DataFrame 或 Dataset 的执行计划: ```scala val df = spark.read.format("csv").option("header", "true").load("path/to/data.csv") df.explain(true) ``` 上述代码会打印详细的执行计划信息,包括未优化的逻辑计划、优化后的逻辑计划以及最终的物理计划[^3]。 --- #### 2. 数据血缘解析的关键点 要完成 Spark SQL 的血缘解析,通常需要关注以下几个方面: ##### (a) 输入表与视图的关系 通过 `BaseRelation` 接口,用户定义的数据源能够暴露其支持的功能接口,比如 TableScan PrunedFilteredScan 等。这些接口决定了读取数据的方式,并影响后续的执行路径。 例如,在自定义数据源时,如果实现了 `PrunedFilteredScan`,则可以在扫描阶段应用过滤条件以减少不必要的 I/O 开销。这种行为可以直接反映到执行计划中,从而帮助我们理解哪些部分参与了计算。 ##### (b) 转换操作的影响 每一步转换都会改变当前 RDD/Dataset/DataFrame 的状态,并更新相应的元数据记录。因此,跟踪所有的 Transformation 可以为重建完整的数据流提供依据。 以下是常见的几种 Transformations: - Filter/Where 子句的应用; - Join 操作引入的新依赖项; - Aggregation 函数引发的状态变化等。 下面展示了一个简单的例子说明如何捕获这些动作: ```scala // 假设有一个初始 DataFrame 'employees' import org.apache.spark.sql.functions._ val filteredDf = employees.filter(col("salary") > 50000).select("name", "department") filteredDf.explain() ``` 此片段中的 `.filter()` `.select()` 都属于典型的 Transformation 步骤,它们共同构成了新的 LogicalPlan 结构。 --- #### 3. 工具辅助下的自动化血缘提取 除了手动分析外,还可以借助第三方工具或者框架自动收集可视化整个工作流程的信息。例如 Apache Atlas 提供了一套完善的解决方案用来管理大数据环境里的资产生命周期;而 Zeppelin Notebook 则允许开发者直观地探索不同阶段的结果差异。 另外值得注意的是,随着版本迭代升级,官方也在持续增强内置功能的支持力度——正如 SparkR 在 v2.2.0 版本里新增加了许多针对现有 Spark SQL 功能的支持特性一样。 --- ### 总结 通过对 Spark SQL 执行计划的研究,不仅可以深入了解底层工作机制原理,而且还能有效实施全面细致的数据血缘追溯任务。这不仅有助于提升系统的可维护性透明度,同时也为进一步改进性能提供了宝贵的参考资料。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值