逻辑计划的创建

无论通过任何方式来创建Dataset,都会在创建Dataset时生成一个QueryExecution对象和创建Dataset的逻辑计划。完成Dataset的创建后,可以对Dataset进行各种操作,SparkSQL不会立即执行这些操作,而是会根据这些操作添加对应的逻辑计划节点,从而形成逻辑计划操作树。

本文介绍Dataset逻辑计划创建的实现原理。

查看逻辑计划

先通过一个实战的例子来查看一下创建Dataset的逻辑计划。下面的代码通过一个对象数组来创建Dataset:

import spark.implicits._

// 创建一个DataSet,并设置schema
case class Person(name: String, age: Long)
val data = Seq(Person("Michael", 29), Person("Andy", 30), Person("Justin", 19))
val ds = spark.createDataset(data)

然后可以通过下面的操作来查看该Dataset的逻辑计划。

查看创建DataSet的逻辑计划

查看完成Dataset的创建但并没有对该Dataset进行任何操作时的逻辑计划。可以看到,此时创建了一个LocalRelation的逻辑计划节点。

scala> ds.explain(true)
== Parsed Logical Plan ==
LocalRelation [name#2, age#3L]
查看添加操作后的逻辑执计划

通过以下调用来对Dataset进行过滤和列的选取操作,并查看逻辑计划。从以下输出可以看到,当执行filter和select操作时,在原来的逻辑计划树上添加了过滤器(Filter)和Project节点,并且Project的name字段是unresolvedalias的。

scala> ds.filter("age>10").select("name").explain(true)
== Parsed Logical Plan ==
'Project [unresolvedalias('name, None)]
+- Filter (age#3L > cast(10 as bigint))
   +- LocalRelation [name#2, age#3L]

也可以通过下面的函数调用来查看详细的逻辑计划对应的类和属性的信息:

scala> ds.filter("age>10").queryExecution.logical.prettyJson
res60: String =
[ {
  "class" : "org.apache.spark.sql.catalyst.plans.logical.Filter",
  "num-children" : 1,
  "condition" : [ {
    "class" : "org.apache.spark.sql.catalyst.expressions.GreaterThan",
    "num-children" : 2,
    "left" : 0,
    "right" : 1
  }, {
    "class" : "org.apache.spark.sql.catalyst.analysis.UnresolvedAttribute",
    "num-children" : 0,
    "nameParts" : "[age]"
  }, {
    "class" : "org.apache.spark.sql.catalyst.expressions.Literal",
    "num-children" : 0,
    "value" : "10",
    "dataType" : "integer"
  } ],
  "child" : 0
}, {
  "class" : "org.apache.spark.sql.catalyst.plans.logical.LocalRelation",
  "num-children" : 0,
  "output" : [ [ {
    "class" : "org.apache.spark.sql.catalyst.expressions.AttributeReference",
    "num-children" : 0,
    "name" : "name",
    ...

通过prettyJson函数会打印各个逻辑计划和属性更加详细的信息。

读取本地文件生成Dataset的逻辑计划
scala> var ds2 = spark.read.csv("file:///home/xh/data.csv")
scala> ds2.explain(true)
== Parsed Logical Plan ==
Relation[_c0#15,_c1#16,_c2#17,_c3#18,_c4#19,_c5#20,_c6#21] csv

== Analyzed Logical Plan ==
_c0: string, _c1: string, _c2: string, _c3: string, _c4: string, _c5: string, _c6: string
Relation[_c0#15,_c1#16,_c2#17,_c3#18,_c4#19,_c5#20,_c6#21] csv

逻辑计划生成的实现

Dataset的构造函数

Dataset的构造函数的代码实现大致如下:

class Dataset[T] private[sql](
    val sparkSession: SparkSession,
    val queryExecution: QueryExecution,
    encoder: Encoder[T])
  extends Serializable {
	...
  // 在创建DataSet时,先创建QueryExecution对象
  def this(sparkSession: SparkSession, logicalPlan: LogicalPlan, encoder: Encoder[T]) =     
  {
     this(sparkSession, sparkSession.sessionState.executePlan(logicalPlan), encoder)
  }
  
  // 构造函数2
  def this(sqlContext: SQLContext, logicalPlan: LogicalPlan, encoder: Encoder[T]) = 
  {
    this(sqlContext.sparkSession, logicalPlan, encoder)
  }

  //...
}

构造函数需要传入一个逻辑计划,然后通过executePlan函数来创建一个QueryExecution对象。创建Dataset的方式不同,传入的逻辑计划也不相同。比如:若是通过range函数来创建的,就会生成Range的逻辑计划。

可见,创建Dataset时需要传入一个QueryExecution和SparkSession对象。

通过数组来创建Dataset

如前面的例子,通过一个scala数组来创建Dataset时,会创建一个LocalRelation逻辑计划,该逻辑计划表示从本地数组作为数据的来源。通过数组来创建Dataset的代码如下:

def createDataset[T : Encoder](data: Seq[T]): Dataset[T] = {
  // 返回一个编码器,可以用来对spark sql的行数据进行序列化/反序列化操作。
  val enc = encoderFor[T]
  // 获取逻辑计划节点的属性
  val attributes = enc.schema.toAttributes
  // 使用编码器对数据行进行编码
  val encoded = data.map(d => enc.toRow(d).copy())
  // 创建一个从本地计划扫描数据的逻辑计划节点
  val plan = new LocalRelation(attributes, encoded)
  // 通过Dataset的伴生对象作为工厂来创建Dataset。把第一个逻辑计划
  Dataset[T](self, plan)
}
通过RDD来创建Dataset

根据已有RDD来创建Dataset时会创建一个ExternalRDD逻辑计划节点,代码如下:

def createDataset[T : Encoder](data: RDD[T]): Dataset[T] = {
  // 创建一个ExternalRDD逻辑计划
  Dataset[T](self, ExternalRDD(data, self))
}
未解析(unresolved)的表达式

当我们不知道类型或未将其与输入表(或别名)匹配,则该属性被称为未解析(Unresolved)。 Spark SQL 使用 Catalyst 规则和一个 Catalog 对象来跟踪所有数据源中的表来解析这些属性。

例如,在 SQL 查询语句SELECT col1 FROM sales 中,col1 的类型,或它是否是有效的列名,直到我们查找sales表时才真正知道。

再比如:查询语句:SELECT * FROM ...中,会创建一个UnresolvedStar逻辑计划。

这些未解析的逻辑计划(unresolved logical plan)在包:org.apache.spark.sql.catalyst.analysis中的unresolved.scala文件中定义。

逻辑计划树的创建

从以上分析可以,从Dataset创建开始,就开始创建逻辑计划树。每个操作对应逻辑计划树中的一个或多个节点。在构建逻辑计划树时,越早执行的操作越是在数据的下层节点,那么,创建Dataset的各种操作,就是逻辑计划树的叶子节点。当Dataset构建完成后进行的各种操作会作为父节点,也就是说,操作越多逻辑计划树高度越高。

通过这种方式就得到了一颗最初的逻辑计划树,等待进行后面的分析和优化。

小结

从以上分析可知,在创建Dataset时,会根据创建方式生成一个逻辑计划,并会生成一个QueryExecution对象。在创建逻辑计划时,由于有些属性和表我们并不知道其详情,所以会生成很多unresolved的逻辑计划,而这些unresolved的逻辑计划就是需要在逻辑计划分析阶段进行分析和处理的。