Spark SQL 物理执行计划各操作实现

目录(?)[+]

SparkStrategy: logical to physical

Catalyst作为一个实现无关的查询优化框架,在优化后的逻辑执行计划到真正的物理执行计划这部分只提供了接口,没有提供像Analyzer和Optimizer那样的实现。

本文介绍的是Spark SQL组件各个物理执行计划的操作实现。把优化后的逻辑执行计划映射到物理执行操作类这部分由SparkStrategies类实现,内部基于Catalyst提供的Strategy接口,实现了一些策略,用于分辨logicalPlan子类并替换为合适的SparkPlan子类。


SparkPlan继承体系如下。接下里会具体介绍其子类的实现。



SparkPlan

主要三部分:LeafNode、UnaryNode、BinaryNode

各自的实现类:



提供四个需要子类重载的方法

[java]  view plain  copy
  1. // TODO: Move to `DistributedPlan`  
  2. /** Specifies how data is partitioned across different nodes in the cluster. */  
  3. def outputPartitioning: Partitioning = UnknownPartitioning(0// TODO: WRONG WIDTH!  
  4. /** Specifies any partition requirements on the input data for this operator. */  
  5. def requiredChildDistribution: Seq[Distribution] =  
  6.   Seq.fill(children.size)(UnspecifiedDistribution)  
  7.   
  8. def execute(): RDD[Row]  
  9. def executeCollect(): Array[Row] = execute().collect()  

Distribution和Partitioning类用于表示数据分布情况。有以下几类,可以望文生义。


LeafNode


ExistingRdd

先介绍下Row和GenericRow的概念。

Row是一行output对应的数据,提供getXXX(i: Int)方法

[java]  view plain  copy
  1. trait Row extends Seq[Any] with Serializable  

支持数据类型包括Int, Long, Double, Float, Boolean, Short, Byte, String。支持按序数(ordinal)读取某一个列的值。读取前需要做isNullAt(i: Int)的判断。

对应的有一个MutableRow类,提供setXXX(i: Int, value: Any)方法。可以修改(set)某序数上的值


GenericRow是Row的一种方便实现,存的是一个数组

[java]  view plain  copy
  1. class GenericRow(protected[catalyst] val values: Array[Any]) extends Row  

所以对应的取值操作和判断是否为空操作会转化为数组上的定位取值操作。

它也有一个对应的GenericMutableRow类,可以修改(set)值。


ExistingRdd用于把绑定了case class的rdd的数据,转变为RDD[Row],同时反射提取出case class的属性(output)。转化过程的单例类和伴生对象如下:

[java]  view plain  copy
  1. object ExistingRdd {  
  2.   def convertToCatalyst(a: Any): Any = a match {  
  3.     case s: Seq[Any] => s.map(convertToCatalyst)  
  4.     case p: Product => new GenericRow(p.productIterator.map(convertToCatalyst).toArray)  
  5.     case other => other  
  6.   }  
  7.   // 把RDD[A]映射成为RDD[Row],map A中每一行数据  
  8.   def productToRowRdd[A <: Product](data: RDD[A]): RDD[Row] = {  
  9.     // TODO: Reuse the row, don't use map on the product iterator.  Maybe code gen?  
  10.     data.map(r => new GenericRow(r.productIterator.map(convertToCatalyst).toArray): Row)  
  11.   }  
  12.   
  13.   def fromProductRdd[A <: Product : TypeTag](productRdd: RDD[A]) = {  
  14.     ExistingRdd(ScalaReflection.attributesFor[A], productToRowRdd(productRdd))  
  15.   }  
  16. }  
  17.   
  18. case class ExistingRdd(output: Seq[Attribute], rdd: RDD[Row]) extends LeafNode {  
  19.   def execute() = rdd  
  20. }  

UnaryNode


Aggregate

隐式转换声明,针对本地分区的RDD,扩充了一些操作

[java]  view plain  copy
  1. /* Implicit conversions */  
  2. import org.apache.spark.rdd.PartitionLocalRDDFunctions._  

Groups input data by`groupingExpressions` and computes the `aggregateExpressions` for each group.

@param child theinput data source.

[java]  view plain  copy
  1. case class Aggregate(  
  2.     partial: Boolean,  
  3.     groupingExpressions: Seq[Expression],  
  4.     aggregateExpressions: Seq[NamedExpression],  
  5.     child: SparkPlan)(@transient sc: SparkContext)  

在初始化的时候,partial这个参数用来标志本次Aggregate操作只在本地做,还是要去到符合groupExpression的其他partition上都做。该判断逻辑如下:

[java]  view plain  copy
  1. override def requiredChildDistribution =  
  2.     if (partial) { // true, 未知的分布  
  3.       UnspecifiedDistribution :: Nil  
  4. else {  
  5.   // 如果为空,则分布情况是全部的tuple在一个single partition里  
  6.       if (groupingExpressions == Nil) {   
  7.         AllTuples :: Nil  
  8.       // 否则是集群分布的,分布规则来自groupExpressions  
  9.       } else {  
  10.         ClusteredDistribution(groupingExpressions) :: Nil  
  11.       }  
  12.     }  

最重要的execute()方法:
[java]  view plain  copy
  1. def execute() = attachTree(this"execute") {  
  2.   // 这里进行了一次隐式转换,生成了PartitionLocalRDDFunctions  
  3.   val grouped = child.execute().mapPartitions { iter =>  
  4.     val buildGrouping = new Projection(groupingExpressions)  
  5.     iter.map(row => (buildGrouping(row), row.copy()))  
  6.   }.groupByKeyLocally()  // 这里生成的结果是RDD[(K, Seq[V])]  
  7.   
  8.   val result = grouped.map { case (group, rows) =>  
  9. // 这一步会把aggregateExpressions对应到具体的spark方法都找出来  
  10. // 具体做法是遍历aggregateExpressions,各自newInstance  
  11.     val aggImplementations = createAggregateImplementations()  
  12.   
  13.     // Pull out all the functions so we can feed each row into them.  
  14.     val aggFunctions = aggImplementations.flatMap(_ collect { case f: AggregateFunction => f })  
  15.   
  16.     rows.foreach { row =>  
  17.       aggFunctions.foreach(_.update(row))  
  18.     }  
  19.     buildRow(aggImplementations.map(_.apply(group)))  
  20.   }  
  21.   
  22.   // TODO: THIS BREAKS PIPELINING, DOUBLE COMPUTES THE ANSWER, AND USES TOO MUCH MEMORY...  
  23.   if (groupingExpressions.isEmpty && result.count == 0) {  
  24.     // When there is no output to the Aggregate operator, we still output an empty row.  
  25.     val aggImplementations = createAggregateImplementations()  
  26.     sc.makeRDD(buildRow(aggImplementations.map(_.apply(null))) :: Nil)  
  27.   } else {  
  28.     result  
  29.   }  
  30. }  

AggregateExpression继承体系如下,这部分代码在Catalyst expressions包的aggregates.scala里:


他的第一类实现AggregateFunction,带一个update(input: Row)操作。子类的update操作是实际对Row执行变化。


DebugNode

DebugNode是把传进来child SparkPlan调用execute()执行,然后把结果childRdd逐个输出查看

[java]  view plain  copy
  1. case class DebugNode(child: SparkPlan) extends UnaryNode  

Exchange

[java]  view plain  copy
  1. case class Exchange(newPartitioning: Partitioning, child: SparkPlan) extends UnaryNode  

为某个SparkPlan,实施新的分区策略。
execute()方法:
[java]  view plain  copy
  1. def execute() = attachTree(this , "execute") {  
  2.     newPartitioning match {  
  3.       case HashPartitioning(expressions, numPartitions) =>  
  4.         // 把expression作用到rdd每个partition的每个row上  
  5.         val rdd = child.execute().mapPartitions { iter =>  
  6.           val hashExpressions = new MutableProjection(expressions)  
  7.           val mutablePair = new MutablePair[Row, Row]() // 相当于Tuple2  
  8.           iter.map(r => mutablePair.update(hashExpressions(r), r))  
  9.         }  
  10.         val part = new HashPartitioner(numPartitions)  
  11.         // 生成ShuffledRDD  
  12.         val shuffled = new ShuffledRDD[Row, Row, MutablePair[Row, Row]](rdd, part)  
  13.         shuffled.setSerializer(new SparkSqlSerializer(new SparkConf(false)))  
  14.         shuffled.map(_._2) // 输出Tuple2里的第二个值  
  15.   
  16.       case RangePartitioning(sortingExpressions, numPartitions) =>  
  17.         // TODO: RangePartitioner should take an Ordering.  
  18.         implicit val ordering = new RowOrdering(sortingExpressions)  
  19.   
  20.         val rdd = child.execute().mapPartitions { iter =>  
  21.           val mutablePair = new MutablePair[Row, Null](nullnull)  
  22.           iter.map(row => mutablePair.update(row, null))  
  23.         }  
  24.         val part = new RangePartitioner(numPartitions, rdd, ascending = true)  
  25.         val shuffled = new ShuffledRDD[Row, Null, MutablePair[Row, Null]](rdd, part)  
  26.         shuffled.setSerializer(new SparkSqlSerializer(new SparkConf(false)))  
  27.         shuffled.map(_._1)  
  28.   
  29.       case SinglePartition =>  
  30.         child.execute().coalesce(1, shuffle = true)  
  31.   
  32.       case _ => sys.error(s"Exchange not implemented for $newPartitioning")  
  33.       // TODO: Handle BroadcastPartitioning.  
  34.     }  
  35.   }  

Filter

[java]  view plain  copy
  1. case class Filter(condition: Expression, child: SparkPlan) extends UnaryNode  
  2.   
  3. def execute() = child.execute().mapPartitions { iter =>  
  4.   iter.filter(condition.apply(_).asInstanceOf[Boolean])  
  5. }  

Generate

[java]  view plain  copy
  1. case class Generate(  
  2.     generator: Generator,  
  3.     join: Boolean,  
  4.     outer: Boolean,  
  5.     child: SparkPlan)  
  6.   extends UnaryNode  

首先,Generator是表达式的子类,继承结构如下


Generator的作用是把input的row处理后输出0个或多个rows,makeOutput()的策略由子类实现。

Explode类做法是将输入的input array里的每一个value(可能是ArrayType,可能是MapType),变成一个GenericRow(Array(v)),输出就是一个


回到Generate操作,

join布尔值用于指定最后输出的结果是否要和输入的原tuple显示做join

outer布尔值只有在join为true的时候才生效,且outer为true的时候,每个input的row都至少会被作为一次output


总体上,Generate操作类似FP里的flatMap操作

[java]  view plain  copy
  1. def execute() = {  
  2.   if (join) {  
  3.     child.execute().mapPartitions { iter =>  
  4.       val nullValues = Seq.fill(generator.output.size)(Literal(null))  
  5.       // Used to produce rows with no matches when outer = true.  
  6.       val outerProjection =  
  7.         new Projection(child.output ++ nullValues, child.output)  
  8.   
  9.       val joinProjection =  
  10.         new Projection(child.output ++ generator.output, child.output ++ generator.output)  
  11.       val joinedRow = new JoinedRow  
  12.   
  13.       iter.flatMap {row =>  
  14.         val outputRows = generator(row)  
  15.         if (outer && outputRows.isEmpty) {  
  16.           outerProjection(row) :: Nil  
  17.         } else {  
  18.           outputRows.map(or => joinProjection(joinedRow(row, or)))  
  19.         }  
  20.       }  
  21.     }  
  22.   } else {  
  23.     child.execute().mapPartitions(iter => iter.flatMap(generator))  
  24.   }  
  25. }  

Project

[java]  view plain  copy
  1. case class Project(projectList: Seq[NamedExpression], child: SparkPlan) extends UnaryNode  

project的执行:

[java]  view plain  copy
  1. def execute() = child.execute().mapPartitions { iter =>  
  2.   @transient val reusableProjection = new MutableProjection(projectList)  
  3.   iter.map(reusableProjection)  
  4. }  

MutableProjection类是Row => Row的继承类,它构造的时候接收一个Seq[Expression],还允许接收一个inputSchema: Seq[Attribute]。MutableProjection用于根据表达式(和Schema,如果有Schema的话)把Row映射成新的Row,改变内部的column。


Sample

[java]  view plain  copy
  1. case class Sample(fraction: Double, withReplacement: Boolean, seed: Int, child: SparkPlan)  extends UnaryNode  
  2.   
  3. def execute() = child.execute().sample(withReplacement, fraction, seed)  

RDD的sample操作:

[java]  view plain  copy
  1. def sample(withReplacement: Boolean, fraction: Double, seed: Int): RDD[T] = {  
  2.   require(fraction >= 0.0"Invalid fraction value: " + fraction)  
  3.   if (withReplacement) {  
  4.     new PartitionwiseSampledRDD[T, T](thisnew PoissonSampler[T](fraction), seed)  
  5.   } else {  
  6.     new PartitionwiseSampledRDD[T, T](thisnew BernoulliSampler[T](fraction), seed)  
  7.   }  
  8. }  

生成的PartitionwiseSampledRDD会在RDD的每个partition都选取样本

PossionSampler和BernoulliSampler是RandomSampler的两种实现。


Sort

[java]  view plain  copy
  1. case class Sort(  
  2.     sortOrder: Seq[SortOrder],  
  3.     global: Boolean,  
  4.     child: SparkPlan)  
  5.   extends UnaryNode  

对分布有要求:

[java]  view plain  copy
  1. override def requiredChildDistribution =  
  2.   if (global) OrderedDistribution(sortOrder) :: Nil   
  3. else UnspecifiedDistribution :: Nil  

SortOrder类是UnaryExpression的实现,定义了tuple排序的策略(递增或递减)。该类只是为child expression们声明了排序策略。之所以继承Expression,是为了能影响到子树。

[java]  view plain  copy
  1. case class SortOrder(child: Expression, direction: SortDirection) extends UnaryExpression  

[java]  view plain  copy
  1. // RowOrdering继承Ordering[Row]  
  2. @transient  
  3.   lazy val ordering = new RowOrdering(sortOrder)  
  4.   
  5.   def execute() = attachTree(this"sort") {  
  6.     // TODO: Optimize sorting operation?  
  7.     child.execute()  
  8.       .mapPartitions(iterator => iterator.map(_.copy()).toArray.sorted(ordering).iterator,  
  9.         preservesPartitioning = true)  
  10.   }  

有一次隐式转换过程,.sorted是array自带的一个方法,因为ordering是RowOrdering类,该类继承Ordering[T],是scala.math.Ordering[T]类。


StopAfter

[java]  view plain  copy
  1. case class StopAfter(limit: Int, child: SparkPlan)(@transient sc: SparkContext) extends UnaryNode  

StopAfter实质上是一次limit操作

[java]  view plain  copy
  1. override def executeCollect() = child.execute().map(_.copy()).take(limit)  
  2. def execute() = sc.makeRDD(executeCollect(), 1// 设置并行度为1  

makeRDD实质上调用的是new ParallelCollectionRDD[T]的操作,此处的seq为tabke()返回的Array[T],而numSlices为1:

[java]  view plain  copy
  1. /** Distribute a local Scala collection to form an RDD. */  
  2.   def parallelize[T: ClassTag](seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T] = {  
  3.     new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())  
  4.   }  

TopK

[java]  view plain  copy
  1. case class TopK(limit: Int, sortOrder: Seq[SortOrder], child: SparkPlan)  
  2. (@transient sc: SparkContext) extends UnaryNode  

可以把TopK理解为类似Sort和StopAfter的结合,

[java]  view plain  copy
  1. @transient  
  2. lazy val ordering = new RowOrdering(sortOrder)  
  3.   
  4. override def executeCollect() = child.execute().map(_.copy()).takeOrdered(limit)(ordering)  
  5. def execute() = sc.makeRDD(executeCollect(), 1)  

takeOrdered(num)(sorting)实际触发的是RDD的top()()操作
[java]  view plain  copy
  1. def top(num: Int)(implicit ord: Ordering[T]): Array[T] = {  
  2.    mapPartitions { items =>  
  3.      val queue = new BoundedPriorityQueue[T](num)  
  4.      queue ++= items  
  5.      Iterator.single(queue)  
  6.    }.reduce { (queue1, queue2) =>  
  7.      queue1 ++= queue2  
  8.      queue1  
  9.    }.toArray.sorted(ord.reverse)  
  10.  }  

BoundedPriorityQueue是Spark util包里的一个数据结构,包装了PriorityQueue,他的优化点在于限制了优先队列的大小,比如在添加元素的时候,如果超出size了,就会进行对堆进行比较和替换。适合TopK的场景。

所以每个partition在排序前,只会产生一个num大小的BPQ(最后只需要选Top num个),合并之后才做真正的排序,最后选出前num个。


BinaryNode


BroadcastNestedLoopJoin

[java]  view plain  copy
  1. case class BroadcastNestedLoopJoin(  
  2.     streamed: SparkPlan, broadcast: SparkPlan, joinType: JoinType, condition: Option[Expression])  
  3.     (@transient sc: SparkContext)  
  4.   extends BinaryNode  

比较复杂的一次join操作,操作如下,
[java]  view plain  copy
  1. def execute() = {  
  2.   // 先将需要广播的SparkPlan执行后进行一次broadcast操作  
  3.   val broadcastedRelation =   
  4.   sc.broadcast(broadcast.execute().map(_.copy()).collect().toIndexedSeq)  
  5.   
  6.   val streamedPlusMatches = streamed.execute().mapPartitions { streamedIter =>  
  7.     val matchedRows = new mutable.ArrayBuffer[Row]  
  8.     val includedBroadcastTuples =    
  9.       new mutable.BitSet(broadcastedRelation.value.size)  
  10.     val joinedRow = new JoinedRow  
  11.       
  12.     streamedIter.foreach { streamedRow =>  
  13.       var i = 0  
  14.       var matched = false  
  15.   
  16.       while (i < broadcastedRelation.value.size) {  
  17.         // TODO: One bitset per partition instead of per row.  
  18.         val broadcastedRow = broadcastedRelation.value(i)  
  19.         if (boundCondition(joinedRow(streamedRow, broadcastedRow)).asInstanceOf[Boolean]) {  
  20.           matchedRows += buildRow(streamedRow ++ broadcastedRow)  
  21.           matched = true  
  22.           includedBroadcastTuples += i  
  23.         }  
  24.         i += 1  
  25.       }  
  26.   
  27.       if (!matched && (joinType == LeftOuter || joinType == FullOuter)) {  
  28.         matchedRows += buildRow(streamedRow ++ Array.fill(right.output.size)(null))  
  29.       }  
  30.     }  
  31.     Iterator((matchedRows, includedBroadcastTuples))  
  32.   }  
  33.   
  34.   val includedBroadcastTuples = streamedPlusMatches.map(_._2)  
  35.   val allIncludedBroadcastTuples =  
  36.     if (includedBroadcastTuples.count == 0) {  
  37.       new scala.collection.mutable.BitSet(broadcastedRelation.value.size)  
  38.     } else {  
  39.       streamedPlusMatches.map(_._2).reduce(_ ++ _)  
  40.     }  
  41.   
  42.   val rightOuterMatches: Seq[Row] =  
  43.     if (joinType == RightOuter || joinType == FullOuter) {  
  44.       broadcastedRelation.value.zipWithIndex.filter {  
  45.         case (row, i) => !allIncludedBroadcastTuples.contains(i)  
  46.       }.map {  
  47.         // TODO: Use projection.  
  48.         case (row, _) => buildRow(Vector.fill(left.output.size)(null) ++ row)  
  49.       }  
  50.     } else {  
  51.       Vector()  
  52.     }  
  53.   
  54.   // TODO: Breaks lineage.  
  55.   sc.union(  
  56.     streamedPlusMatches.flatMap(_._1), sc.makeRDD(rightOuterMatches))  
  57. }  

CartesianProduct

[java]  view plain  copy
  1. case class CartesianProduct(left: SparkPlan, right: SparkPlan) extends BinaryNode  

调用的是RDD的笛卡尔积操作,

[java]  view plain  copy
  1. def execute() =   
  2.   left.execute().map(_.copy()).cartesian(right.execute().map(_.copy())).map {  
  3.     case (l: Row, r: Row) => buildRow(l ++ r)  
  4.   }  

SparkEquiInnerJoin

[java]  view plain  copy
  1. case class SparkEquiInnerJoin(  
  2.     leftKeys: Seq[Expression],  
  3.     rightKeys: Seq[Expression],  
  4.     left: SparkPlan,  
  5.     right: SparkPlan) extends BinaryNode  

该join操作适用于left和right两部分partition一样大且提供各自keys的情况。

基本上看代码就可以了,没有什么可以说明的,做local join的时候借助的是PartitionLocalRDDFunctions里的方法。

[java]  view plain  copy
  1. def execute() = attachTree(this"execute") {  
  2.   val leftWithKeys = left.execute().mapPartitions { iter =>  
  3.     val generateLeftKeys = new Projection(leftKeys, left.output) // 传入了Schema  
  4.     iter.map(row => (generateLeftKeys(row), row.copy()))  
  5.   }  
  6.   
  7.   val rightWithKeys = right.execute().mapPartitions { iter =>  
  8.     val generateRightKeys = new Projection(rightKeys, right.output)  
  9.     iter.map(row => (generateRightKeys(row), row.copy()))  
  10.   }  
  11.   
  12.   // Do the join.  
  13.   // joinLocally是PartitionLocalRDDFunctions的方法  
  14.   val joined = filterNulls(leftWithKeys).joinLocally(filterNulls(rightWithKeys))  
  15.   // Drop join keys and merge input tuples.  
  16.   joined.map { case (_, (leftTuple, rightTuple)) => buildRow(leftTuple ++ rightTuple) }  
  17. }  
  18.   
  19. /** 
  20.  * Filters any rows where the any of the join keys is null, ensuring three-valued 
  21.  * logic for the equi-join conditions. 
  22.  */  
  23. protected def filterNulls(rdd: RDD[(Row, Row)]) =  
  24.   rdd.filter {  
  25.     case (key: Seq[_], _) => !key.exists(_ == null)  
  26.   }  

PartitionLocalRDDFunctions方法如下,该操作并不引入shuffle操作。两个RDD的partition数目需要相等。

[java]  view plain  copy
  1. def joinLocally[W](other: RDD[(K, W)]): RDD[(K, (V, W))] = {  
  2.   cogroupLocally(other).flatMapValues {  
  3.     case (vs, ws) => for (v <- vs.iterator; w <- ws.iterator) yield (v, w)  
  4.   }  
  5. }  

Other

Union

该操作直接继承SparkPlan

[java]  view plain  copy
  1. case class Union(children: Seq[SparkPlan])(@transient sc: SparkContext) extends SparkPlan  

用传入的SparkPlan集合各自的RDD执行结果生成一个UnionRDD

[java]  view plain  copy
  1. def execute() = sc.union(children.map(_.execute()))  




全文完 :)

### 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 执行计划的研究,不仅可以深入了解底层工作机制原理,而且还能有效实施全面细致的数据血缘追溯任务。这不仅有助于提升系统的可维护性和透明度,同时也为进一步改进性能提供了宝贵的参考资料。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值