Spark sql是spark内部最核心,也是社区最活跃的组件。Spark SQL支持在Spark中执行SQL,或者HiveQL的关系查询表达式。列式存储的类RDD(DataSet/DataFrame)数据类型以及对sql语句的支持使它更容易上手,同时,它对数据的抽取、清洗的特性,使它广泛的用于etl,甚至是机器学习领域。因此,saprk sql较其他spark组件,获得了更多的使用者。
下文,我们首先通过查看一个简单的sql的执行计划,对sql的执行流程有一个简单的认识,后面将通过对sql优化器catalyst的每个部分的介绍,来让大家更深入的了解sql后台的执行流程。由于此模板中代码较多,我们在此仅就执行流程中涉及到的主要代码进行介绍,方便大家更快地浏览spark sql的源码。本文中涉及到的源码spark 2.1.1的。
1. catalyst整体执行流程介绍
1.1 catalyst整体执行流程
说到spark sql不得不提的当然是Catalyst了。Catalyst是spark sql的核心,是一套针对spark sql 语句执行过程中的查询优化框架。因此要理解spark sql的执行流程,理解Catalyst的工作流程是理解spark sql的关键。而说到Catalyst,就必须得上下面这张图1了,这张图描述了spark sql执行的全流程。其中,长方形框内为catalyst的工作流程。
图1 spark sql 执行流程图
SQL语句首先通过Parser模块被解析为语法树,此棵树称为Unresolved Logical Plan;Unresolved Logical Plan通过Analyzer模块借助于Catalog中的表信息解析为Logical Plan;此时,Optimizer再通过各种基于规则的优化策略进行深入优化,得到Optimized Logical Plan;优化后的逻辑执行计划依然是逻辑的,并不能被Spark系统理解,此时需要将此逻辑执行计划转换为Physical Plan。
1.2 一个简单sql语句的执行
为了更好的对整个过程进行理解,下面通过一个简单的sql示例来查看采用catalyst框架执行sql的过程。示例代码如下:
object TestSql {
case class Student(id:Long,name:String,chinese:String,math:String,English:String,age:Int)
case class Score(sid:Long,weight1:String,weight2:String,weight3:String)
def main(args: Array[String]): Unit = {
//使用saprkSession初始化spark运行环境
val spark=SparkSession.builder().appName("Spark SQL basic example").config("spark.some.config.option", "some-value").getOrCreate()
//引入spark sql的隐式转换
import sqlContext.implicits._
//读取第一个文件的数据并转换成DataFrame
val testP1 = spark.sparkContext.textFile("/home/dev/testP1").map(_.split(" ")).map(p=>Student(p(0).toLong,p(1),p(2),p(3),p(4),p(5).trim.toInt)).toDF()
//注册成表
testP1.registerTempTable("studentTable")
//读取第二个文件的数据并转换成DataFrame
val testp2 = spark.sparkContext.textFile("/home/dev/testP2").map(_.split(" ")).map(p=>Score(p(0).toLong,p(1),p(2),p(3))).toDF()
//注册成表
testp2.registerTempTable("scoreTable")
//查看sql的逻辑执行计划
val dataframe = spark.sql("select sum(chineseScore) from " +
"(select x.id,x.chinese+20+30 as chineseScore,x.math from studentTable x inner join scoreTable y on x.id=y.sid)z" +
" where z.chineseScore <100").map(p=>(p.getLong(0))).collect.foreach(println)
此例也是针对spark2.1.1版本的,程序的入口是SparkSession。由于此例超级简单,做过spark的人一眼就能看出,而且每行代码都带有中文注释,所以在这里,我们就不做具体的介绍了。
这里涉及到的sql查询就是最后一句,通过spark shell可以看到该sql查询的逻辑执行计划和物理执行计划。进入sparkshell后,输入一下代码即可显示此sql查询的执行计划。
spark.sql("select sum(chineseScore) from " +
"(select x.id,x.chinese+20+30 as chineseScore,x.math from studentTable x inner join scoreTable y on x.id=y.sid)z" +
" where z.chineseScore <100").explain(true)
这里,是使用DataSet的explain函数实现逻辑执行计划和物理执行计划的打印,调用explain的源码如下:
/**
* Prints the plans (logical and physical) to the console for debugging purposes.
*
* @group basic
* @since 1.6.0
*/
def explain(extended: Boolean): Unit = {
val explain = ExplainCommand(queryExecution.logical, extended = extended)
sparkSession.sessionState.executePlan(explain).executedPlan.executeCollect().foreach {
// scalastyle:off println
r => println(r.getString(0))
// scalastyle:on println
}
}
显示在spark shell中的unresolved logical plan、resolved logical plan、optimized logical plan和physical plan如下图2所示: