Spark从入门到精通30:Spark SQL:核心源码深度剖析

在前面一节我们讲解了SparkSQL的工作原理,接下来在这一节,我们对SparkSQL工作原理进一步地深入和加强,这一节主要讲解SparkSQL核心源码导读和剖析

首先,我们看SQLContext.scala源码:

执行sql方法的解析

找到我们执行sql语句的方法,如下图所示:



这个方法上面的注释大概意思:
使用spark执行一条SQL查询语句,将结果作为DataFrame返回。SQL解析使用的方言,可以通过spark.sql.dialect参数来进行设置
接下来,我们看代码逻辑:
首先,查看我们通过SQLContext.setConf方法设置的参数,spark.sql.dialect,如果是SQL方言,那么就进入接下来的执行,这里,可以通过源码清晰地看到,如果方言不是SQL,那么就直接报错。
我们再点击追踪conf,发现这个conf是SQLConf类,如果要给SparkSQL设置一些参数,那么要使用SQLContext.setConf方法,我们在SQLConf类中找到setConf方法,底层是会将配置信息放入SQLConf对象中的。
判断如果方言是sql,那么继续下面的代码执行,这里,实际上,首先,SparkSQL也是有lazy特性的,SparkSQL的lazy特性,我们在这里通过源码剖析,其实,调用sql()方法去执行一条SQL语句的时候,默认只会执行我们之前剖析的SparkSQL原理的第一步,即调用Sqlpparser组件针对SQL语句生成一个Unresolved LogicalPlan,然后将Unresolved LogicalPlan和SQLContext自身的实例(this)封装为一个DataFrame返回给用户,其中仅仅封装了SQL语句的Unresolved LogicalPlan,在用户拿到DataFrame之后,可以执行一些show(),select().show(),groupBy()这样的操作,或者拿到DataFrame对应的RDD,执行一系列transformation操作,最后执行一个action操作。在执行了DataFrame的上述要求返回结果数据的操作之后,才会实际出发SparkSQL后续的SQL执行流程,包括Analyzer、Optimizer、SparkPlan、execute PhysicalPlan
这里,我们首先看第一步,parseSql方法,传入SQL语句,调用SqlParser解析SQL,获取Unresolved LogicalPlan,我们看看SqlParser具体上如何把SQL语句解析成Unresolved LogicalPlan的:
点击追踪parseSql方法,如下:



这里,实际上会调用SqlParser的apply方法,来获取一个对SQL语句解析后的LogicalPlan
再点击追踪sqlParser方法,如下:

SqlParser是实际上是SparkSqlParser的实例,SparkSqlParser里面又封装了catalyst中的SqlParser,而SqlParser类其实是在spark-catalyst下的org.apache.spark.sql.catalyst下的类。和SQLContext不在同一包下。然而它的apply方法还不在它自己的类里,而是在它的父类AbstractSparkSQLParser下。



点击追踪AbstractSparkSQLParser类,找到apply方法,如下图所示:

所以,实际上,调用SqlParser的apply方法,将sql语句解析成LogicalPlan时会调用SqlParser的父类的apply方法。
这个方法从第38行开始,大概意思就是说,用lexical.Scanner,针对SQL语句,来进行语法检查、分析,满足语法检查结果的话,就是用SQL解析器,针对SQL进行解析,包括词法解析(将SQL语句解析成一个一个的短语,token)、语法解析,最后生成一个Unresolved LogicalPlan,该LogicalPlan仅仅针对SQL语句本身生成,纯语法不涉及任何关联的数据源等信息。
我们点击追踪第38行的lexical,发现override val lexical = new SqlLexical,再点击SqlLexical,如下:

这里的意思,就是说用SqlLexical,对SQL语句,执行一个检查,如果满足检查的话,那么才去分析,否则,说明SQL语句本身的语法就有问题。这些代码就是一些底层表达式,针对表达式生成一个短语,我们不用关心它具体的用法,只要明白大概意思即可。
接着,我们回到AbstractSparkSQLParser的apply方法方法,继续往下执行,开始解析我们的SQL语法,在SqlParser的解析代码如下:

首先从start方法开始,这里可以看出来,SparkSQL是支持至少两种主要的SQL语法,包括select语句和insert语句。
往下,select方法,这里就是对select语句执行解析,select里面包括from、where、group、having、sort、limit。
insert方法解析insert overwrite这种语法
下面的lazy方法,会将你的SQL语句里面解析出来的各种token或者TreeNode给关联起来最后形成一课语法树,语法树即封装在LogicalPlan中,但是要注意,此时的LogicalPlan还是Unresolved LogicalPlan。
至此,parsesql方法讲完了,总结一下,其实就是调用了SqlParser的apply方法,即由SqlParser将SQL语句通过内部的各种select、insert这种词法、语法解析器,去进行解析,然后将SQL语句的各个部分,组装成一个LogicalPlan,但是这里的LogicalPlan只是一颗语法树,还不知道自己具体执行的时候,那些数据从哪里来。所以叫做Unresolved LogicalPlan。
那么解析到了SQL,拿到了Unresolved LogicalPlan之后,会封装一个DataFrame,返回给用户,用户此时就可以用DataFrame做各种操作了。
执行queryExecution方法的解析

我们拿到了Unresolved LogicalPlan之后,到后面就出发Analyzer、Optimizer、SparkPlan这些东西在哪里呢,我们接下来就开始下面的研究

实际上我们在后面操作DataFrame的时候,不可避免地要执行SQL语句,真正针对数据进行查询,并返回结果的时候,就会触发SQLContext的executeSql方法的执行,该方法,实际上会返回一个QueryExecution,实际上就会触发整个后续的流程。
我们在SQLContext.scala中搜索QueryExecution,在137行,可以看到如下代码:



点进QueryExecution,源码如下:



从方法可以看到,只要传一个参数,这个参数就是我们之前生成的Unresolved LogicalPlan。
随即,我们看到1071行的analyzer,点击进入,看到analyzer的源码如下:

QueryExecution实际执行SQL语句的时候,第一步就是用之前的SqlParser解析出来的纯逻辑的封装了语法树的Unresolved LogicalPlan,去调用Analyzer的apply方法,来将Unresolved LogicalPlan来生成一个Resolved LogicalPlan。
Analyzer这个类也是在spark-catalyst下的org.apache.spark.sql.catalyst.analysis包下的类。和SQLContext不在同一包下。
点击进入Analyzer,如下:



可以看到Analyzer的父类是RuleExecutor,所以,调用Analyzer的apply方法时,实际上会调用RuleExecutor的apply方法中,并传入一个Unresolved LogicalPlan。
点击进入父类,看到apply方法:

在这个方法中,实际上做的最重要的一件事情,就是讲LogicalPlan与它要查询的数据源绑定起来,从而让Unresolved LogicalPlan变成一个Resolved LogicalPlan,最重要的就是将我们的LogicalPlan与SQL语句中的数据源绑定起来。
举例:这里Unresolved LogicalPlan中,只是针对select * from studetns where age<=18这条SQL语句生成了一个树的结果:
PROJECT name
||
SELECT
students

||
WHERE age <=18
但是,实际上此时最关键的一点是,不知道students表,是哪个表,表在哪里,mysql?hive?临时表?临时表又在哪里?
那么,Analyzer的apply()方法调用后,生成的Resolved LogicalPlan,就与SQL语句中的数据源,students临时表(studentDF.registerTempTable(‘students’))进行绑定。此时,Resolved LogicalPlan中,就知道了,自己要从哪个数据源中进行查询。
到此为止analyzer方法执行结束,得到一个Resolved LogicalPlan。
接着下一行,通过cacheManager,执行一个缓存的操作,这里有一个CacheManager,调用了其userCacheData方法,跟踪源码如下:



这里的意思是说如果之前已经缓存过这个执行计划,又再次执行的话就可以使用缓存中的数据。
调用Optimizer的apply()方法

继续往下,到第1076行,针对Resolved LogicalPlan调用Optimizer,进行优化,获得Optimizer LogicalPlan,获得优化后的逻辑执行计划。点击进入Optimizer ,发现Optimizer 类在spark-catalyst下的org.apache.spark.sql.catalyst.Optimizer包下的类,Optimizer如下:



这里的batches是非常重要的,这里封装了每一个SparkSQL版本中,可以对逻辑执行计划执行的优化策略,我们在这里,对Optimizer重点是理解它的各种优化策略,从而心里才能清楚,SparkSQL内部是如何对我们写的SQL语句进行优化的,那么有一点是明了的,如果我们清楚了这些东西,其实在编写SQL语句的时候,脑子里想着这些优化策略,直接用优化策略建议的方式来编写SQL语句让我们传递给SparkSQL的SQL语句,本身就已经是最优的,这样的话,就可以避免在执行SQL解析的时候,进行大量的SparkSQL的内部的优化,那么在某种程度上也可以提升性能。
接下来,具体看下面的优化源码:
CombineLimits:其实就是合并limit语句,比如你的SQL语句中,有多个limit子句,那么这里会进行合并,取一个并集就可以了。这样的话,在后面SQL执行时,limit就执行一次就好。所以,我们就在写SQL的时候,尽量写一个limit。
NullPropagation:针对Null的优化,尽量避免值出现null的情况,否则Null是很容易产生数据倾斜的。
ConstantFolding:针对常量的优化,在这里会直接可以获得的常量;所以我们自己对可能出现的常量尽量直接给出
LikeSimplification,:like的简化优化
UnionPushdown:将union下推,意思和filter pushdown,就是说,将union、where这种子句,下推到子查询中进行,尽量早的执行union操作和where操作,避免在外层查询中,针对大量的数据,两张大表,执行where操作。

CombineFilters:合并filter,就是合并where子句,比如子查询中,有针对某个字段的where子句,外层查询中也有针对同样一个字段的where子句,那么,此时是可以合并where子句的,只保留一个即可,取并集即可,所以我们自己写SQL的时候,也要注意这个where的使用,如果针对一个字段,写一次就好。

ColumnPruning) ::
列剪裁,就是针对你要查询的列进行剪裁,对于我们自己来说,最重要的就是,如果表中有n个字段,但是你只要查询一个字段,那么就用select x from from(select * from table)的查询语句。
明白了这些优化以后,我们平常写SQL就这样写就可以在解析SQL中提升性能。
至此,Optimizer优化完成。

创建SparkPlan

接着,回来QueryExecution这个方法,继续往下,就是创建SparkPlan了,这里用Optimizer针对Resolved LogicalPlan生成的Optimized LogicalPlan,用SparkPlanner,创建一个SparkPlan。点击进入planner,在点击SparkPlanner,进入此类,如下:



这里,会用一些策略,比如说DataSourceStrategy,针对逻辑执行计划,执行进一步的具体化和物化。
然后,往下1085行,这里使用SparkPlan生成一个可以执行的SparkPlan,此时就是PhysicalPlan,物理执行计划,直接就是可以执行了,此时,已经绑定到了物理的数据源,而且,知道对各个表的join,如果进行join,包括join的时候,默认spark内部是会对小表进行广播的。
最后一步,调用SparkPlan(封装了PysicalPlan的SparkPlan)的execute方法,execute()方法,实际上就会去执行物理执行计划
最后一个重要的点是execute方法,返回的是什么?返回的是RDD(Row),就是一个元素类型为Row的RDD。

你可能感兴趣的:(Spark从入门到精通30:Spark SQL:核心源码深度剖析)