Spark LogicalPlan 源码分析
原理
LogicalPlan
是Apache Spark中的抽象类,用于表示Spark SQL中的逻辑执行计划。是Spark SQL查询优化和执行过程的核心组件之一。
LogicalPlan
的由来机制主要基于以下两个原理:
-
解析器(Parser):在Spark SQL中,查询语句首先被解析器解析成一个抽象语法树(AST),这个AST表示了查询的语法结构。解析器将查询语句解析为一系列的语法规则,然后构建一个对应的AST。
-
转换器(Transformer):AST经过解析后,会进一步经过一系列的转换操作,将其转换为更高级别的逻辑计划。这些转换器将AST转换为
LogicalPlan
的实例,并且可以进行各种优化操作,例如谓词下推、投影消除等。
通过解析器和转换器的工作,LogicalPlan
最终得到了一个完整的逻辑执行计划,该计划以树状结构表示查询的逻辑操作序列。每个节点代表一个具体的逻辑操作,例如选择(Selection)、投影(Projection)、连接(Join)等。节点之间通过父子关系连接,形成了一棵树。
LogicalPlan
的由来机制充分利用了解析器和转换器的能力,将查询语句转换为可执行的逻辑计划,并提供了一种统一的方式来表示和操作这些计划。这使得Spark SQL可以对查询进行优化和执行,以提高查询性能和效率。
方法示例
在LogicalPlan类中,定义了以下方法:
-
isStreaming: Boolean
- 返回一个布尔值,指示该子树是否来自流式数据源。
示例:
val plan: LogicalPlan = ... val isStreaming: Boolean = plan.isStreaming
-
maxRows: Option[Long]
- 返回计划可能计算的最大行数。对于可以推迟Limit操作符的操作者(例如Union),应该重写此方法。
示例:
val plan: LogicalPlan = ... val maxRowCount: Option[Long] = plan.maxRows
-
maxRowsPerPartition: Option[Long]
- 返回计划在每个分区上可能计算的最大行数。
示例:
val plan: LogicalPlan = ... val maxRowCountPerPartition: Option[Long] = plan.maxRowsPerPartition
-
resolved: Boolean
- 返回一个布尔值,指示该表达式及其所有子表达式是否已解析为具体模式。
示例:
val plan: LogicalPlan = ... val isResolved: Boolean = plan.resolved
-
childrenResolved: Boolean
- 返回一个布尔值,指示该查询计划的所有子计划是否已解析。
示例:
val plan: LogicalPlan = ... val areChildrenResolved: Boolean = plan.childrenResolved
-
resolve(schema: StructType, resolver: Resolver): Seq[Attribute]
- 将给定的模式解析为该查询计划中的具体Attribute引用。
示例:
val plan: LogicalPlan = ... val schema: StructType = ... val resolvedAttributes: Seq[Attribute] = plan.resolve(schema, resolver)
-
resolveChildren(nameParts: Seq[String], resolver: Resolver): Option[NamedExpression]
- 根据所有子节点的输入,将给定的字符串转换为NamedExpression。
示例:
val plan: LogicalPlan = ... val nameParts: Seq[String] = ... val resolvedExpression: Option[NamedExpression] = plan.resolveChildren(nameParts, resolver)
-
resolve(nameParts: Seq[String], resolver: Resolver): Option[NamedExpression]
- 根据此逻辑计划的输出,将给定的字符串转换为NamedExpression。
示例:
val plan: LogicalPlan = ... val nameParts: Seq[String] = ... val resolvedExpression: Option[NamedExpression] = plan.resolve(nameParts, resolver)
-
resolveQuoted(name: String, resolver: Resolver): Option[NamedExpression]
- 将给定的属性名称按点号拆分为多个部分,并根据output中的属性进行解析。
示例:
val plan: LogicalPlan = ... val name: String = ... val resolvedExpression: Option[NamedExpression] = plan.resolveQuoted(name, resolver)
-
refresh(): Unit
- 刷新(或使无效)递归计划中缓存的任何元数据/数据。
示例:
val plan: LogicalPlan = ... plan.refresh()
-
outputOrdering: Seq[SortOrder]
- 返回该计划生成的输出排序。
示例:
val plan: LogicalPlan = ... val ordering: Seq[SortOrder] = plan.outputOrdering
这些方法用于对逻辑计划进行操作、解析和查询。你可以根据具体需求调用这些方法来获取计划的相关信息,解析模式,判断计划是否已解析,刷新缓存等。
中文源码
abstract class LogicalPlan
extends QueryPlan[LogicalPlan]
with AnalysisHelper
with LogicalPlanStats
with QueryPlanConstraints
with Logging {
/**
* 返回true,如果该子树包含来自流式数据源的数据。
*/
def isStreaming: Boolean = children.exists(_.isStreaming == true)
override def verboseStringWithSuffix: String = {
super.verboseString + statsCache.map(", " + _.toString).getOrElse("")
}
/**
* 返回该计划可能计算的最大行数。
*
* 可以推迟Limit操作符的操作者应重写此函数(例如Union)。
* 可以通过Limit操作进行推进的任何操作者都应重写此函数(例如Project)。
*/
def maxRows: Option[Long] = None
/**
* 返回该计划在每个分区上可能计算的最大行数。
*/
def maxRowsPerPartition: Option[Long] = maxRows
/**
* 如果该表达式及其所有子表达式已解析为具体模式,则返回true;
* 如果它仍然包含任何未解析的占位符,则返回false。
* LogicalPlan的实现可以重写此方法(例如UnresolvedRelation应返回`false`)。
*/
lazy val resolved: Boolean = expressions.forall(_.resolved) && childrenResolved
override protected def statePrefix = if (!resolved) "'" else super.statePrefix
/**
* 返回true,如果该查询计划的所有子计划都已解析。
*/
def childrenResolved: Boolean = children.forall(_.resolved)
/**
* 将给定的模式解析为该查询计划中的具体[[Attribute]]引用。由于未解析的[[Attribute]],
* 此函数只能在经过分析的计划上调用,否则将抛出[[AnalysisException]]。
*/
def resolve(schema: StructType, resolver: Resolver): Seq[Attribute] = {
schema.map { field =>
resolve(field.name :: Nil, resolver).map {
case a: AttributeReference => a
case _ => sys.error(s"can not handle nested schema yet... plan $this")
}.getOrElse {
throw new AnalysisException(
s"Unable to resolve ${field.name} given [${output.map(_.name).mkString(", ")}]")
}
}
}
private[this] lazy val childAttributes = AttributeSeq(children.flatMap(_.output))
private[this] lazy val outputAttributes = AttributeSeq(output)
/**
* 根据所有子节点的输入,将给定的字符串转换为[[NamedExpression]]。属性以以下形式的字符串表示:
* `[scope].AttributeName.[nested].[fields]...`。
*/
def resolveChildren(
nameParts: Seq[String],
resolver: Resolver): Option[NamedExpression] =
childAttributes.resolve(nameParts, resolver)
/**
* 根据此逻辑计划的输出,将给定的字符串转换为[[NamedExpression]]。属性以以下形式的字符串表示:
* `[scope].AttributeName.[nested].[fields]...`。
*/
def resolve(
nameParts: Seq[String],
resolver: Resolver): Option[NamedExpression] =
outputAttributes.resolve(nameParts, resolver)
/**
* 将给定的属性名称按点号拆分为多个部分,但不要拆分由反引号引起来的名称,
* 例如`ab.cd`.`efg`应该拆分为两个部分 "ab.cd" 和 "efg"。
*/
def resolveQuoted(
name: String,
resolver: Resolver): Option[NamedExpression] = {
outputAttributes.resolve(UnresolvedAttribute.parseAttributeName(name), resolver)
}
/**
* 刷新(或使无效)递归计划中缓存的任何元数据/数据。
*/
def refresh(): Unit = children.foreach(_.refresh())
/**
* 返回该计划生成的输出排序。
*/
def outputOrdering: Seq[SortOrder] = Nil
}
/**
* 没有子节点的逻辑计划节点。
*/
abstract class LeafNode extends LogicalPlan {
override final def children: Seq[LogicalPlan] = Nil
override def producedAttributes: AttributeSet = outputSet
/** 可以在分析过程中幸存下来的叶子节点必须定义自己的统计信息。*/
def computeStats(): Statistics = throw new UnsupportedOperationException
}
/**
* 具有单个子节点的逻辑计划节点。
*/
abstract class UnaryNode extends LogicalPlan {
def child: LogicalPlan
override final def children: Seq[LogicalPlan] = child :: Nil
/**
* 通过替换原始约束表达式中的相应别名,生成一组附加的别名约束
*/
protected def getAliasedConstraints(projectList: Seq[NamedExpression]): Set[Expression] = {
var allConstraints = child.constraints.asInstanceOf[Set[Expression]]
projectList.foreach {
case a @ Alias(l: Literal, _) =>
allConstraints += EqualNullSafe(a.toAttribute, l)
case a @ Alias(e, _) =>
// 对于`projectList`中的每个别名,将约束中的引用替换为其属性。
allConstraints ++= allConstraints.map(_ transform {
case expr: Expression if expr.semanticEquals(e) =>
a.toAttribute
})
allConstraints += EqualNullSafe(e, a.toAttribute)
case _ => // 不做任何改变。
}
allConstraints -- child.constraints
}
override protected def validConstraints: Set[Expression] = child.constraints
}
/**
* 具有左子节点和右子节点的逻辑计划节点。
*/
abstract class BinaryNode extends LogicalPlan {
def left: LogicalPlan
def right: LogicalPlan
override final def children: Seq[LogicalPlan] = Seq(left, right)
}
abstract class OrderPreservingUnaryNode extends UnaryNode {
override final def outputOrdering: Seq[SortOrder] = child.outputOrdering
}
源码分析
该源码是Scala中的逻辑计划(LogicalPlan)相关类的定义和实现。逻辑计划用于表示Spark SQL查询的逻辑结构,包括数据源、操作符、表达式等。以下是对源码的分析总结:
-
抽象类LogicalPlan是所有逻辑计划节点的基类,它继承了多个特质(trait),包括QueryPlan、AnalysisHelper、LogicalPlanStats、QueryPlanConstraints和Logging。
-
LogicalPlan中定义了一系列方法和属性,用于描述和操作逻辑计划节点。例如,isStreaming方法用于判断该子树是否来自流式数据源;maxRows方法用于返回计划可能计算的最大行数;resolved属性用于判断计划是否已解析等。
-
LogicalPlan还定义了一些辅助方法,如resolve方法用于解析模式(schema)到具体的Attribute引用;refresh方法用于刷新缓存的元数据/数据;outputOrdering方法用于返回计划生成的输出排序等。
-
LeafNode是没有子节点的逻辑计划节点的抽象类,继承自LogicalPlan。LeafNode必须定义自己的统计信息,并且没有子节点。
-
UnaryNode是具有单个子节点的逻辑计划节点的抽象类,继承自LogicalPlan。UnaryNode定义了一个名为child的抽象方法,并提供了相应的实现。UnaryNode还定义了getAliasedConstraints方法,用于生成一组附加的别名约束。
-
BinaryNode是具有左子节点和右子节点的逻辑计划节点的抽象类,继承自LogicalPlan。BinaryNode定义了left和right两个抽象方法,并提供了相应的实现。
-
OrderPreservingUnaryNode是保持顺序的一元逻辑计划节点的抽象类,继承自UnaryNode。OrderPreservingUnaryNode通过重写outputOrdering方法来保持输入和输出之间的排序顺序。
总的来说,该源码定义了逻辑计划相关的类和特质,提供了对逻辑计划的描述、操作和解析功能。它为Spark SQL查询的构建和优化提供了基础。