1、SparkSQL
SparkSQL是架构在Spark计算框架之上的分布式SQL引擎,使用DataFrame和DataSet承载结构化和半结构化数据来实现数据复杂查询处理,提供的DSL可以直接使用scala语言完成Sql查询,同时也使用thriftserver提供服务化的Sql查询功能。SparkSql提供了DataSource API,用户通过这套API可以自己开发一套Connector,直接查询各类数据源;数据源包括NoSql、RDBMS、搜索引擎以及HDFS等分布式文件系统上的文件等。
2、连接查询和连接条件
Sql中的连接查询(join),主要分为内连接查询(inner join)、外连接查询(outter join)和半连接查询(semi join)。连接条件(join condition)是指当这个条件满足两表的两行数据才能join在一起被返回,如
select a.value, b.value
from leftTable a left join rightTable b
on a.id = b.id and a.id > 10
where b.id > 2
其中的“a.id = b.id and a.id > 10”这部分条件被称为“join中条件”,直接用来判断被join两表的两行记录能否被join在一起,如果不满足这个条件,两表的两行记录并非全部被踢出局,而是根据连接查询类型的不同有不同的处理,所以这并非一个单表的过滤过程或者两个表的“联合过滤”过程;而where后的“b.id > 2”这部分被称为“join后条件”,这里虽然成为“join后条件”,但是并非一定要join后才能过过滤数据,只是说明如果join后进行过滤,肯定可以得到一个正确的结果,这也是我们后边分析问题时得到正确结果的基准方法。
3、谓词下推
所谓谓词,即返回值是true或者false的函数,scala或者spark中都有个filter方法,该高阶函数传入的参数就是一个返回tre或者false的函数。但在sql语言中没有方法,只有表达式。where后边的表达式起的作用正是过滤作用,而这部分亮语句被sql层解析处理后,在数据库内部正是以谓词的形式呈现的。
那谓词为什么要下推?sparkSql中的谓词下推有两层含义,第一层含义是指由谁来完成数据过滤,第二层含义是指何时完成数据过滤。下面先看下SparkSql中sql查询处理流程:
如上图,SparkSql会先对输入的Sql语句进行一系列的分析(Analyse),包括词法解析、语法分析以及语义分析;然后是执行计划的生成,包括逻辑计划和物理计划。其中在逻辑计划阶段会有很多的优化,对谓词的处理就在这个阶段完成;而物理计划则Spark core的RDD DAG图生成过程;这两步完成之后则是具体的执行了(也就是各种重量级的计算逻辑,如join、groupby、filter以及distinct等)。能够完成数据过滤的主体有两个,第一是分布式Sql层(在execute阶段),第二个是数据源。那么谓词下推的第一层含义就是指由Sql层的Filter操作符来完成过滤,还是由Scan操作在扫描阶段完成过滤。上边提到,我们可以通过封装SparkSql的DataSoource API完成各类数据源的查询,那么如果底层数据源无法高效完成数据的过滤,就会执行全扫描,把每条相关的数据都交给SparkSql的Filter操作符完成过滤,虽然SparkSql使用Code Generation技术极大地提高了数据过滤的效率,但是这个过程无法避免磁盘读取大量数据,甚至在某些情况下会涉及网络IO(例如数据非本地化存储时);如果底层数据源在进行扫描时能非常快速地完成数据的过滤,那么就会把过滤交给底层数据源来完成。
那么谓词下推第二层含义,即何时完成数据过滤则一般是在指连接查询中,是先对单表进行过滤再和其它表连接还是在先把多表进行连接再对连接后的临时表进行过滤。
4、内连接查询中的谓词下推规则
假设有两条张表:lefttable、righttable
4.1 Join后条件通过AND连接
select
*
from lefttable a
inner join righttable b
on a.id = b.id
where a.value = 'two' and b.value = 'two'
这个查询是一个内连接查询,join后条件是用and连接的两个表的过滤条件,假设不下推,而先做内连接判断,这时是可以得到正确结果的,步骤如下:
- 1)左表id为1的行在右表中可以找到,即这两行数据可以“join”在一起
- 2)左表id为2的行在右表中可以找到,这两行也可以“join”在一起
至此,join的临时结果表(之所以是临时表,因为还没有进行过滤)如下:
然后使用where条件进行过滤,显然临时表中的第一行不满足条件,被过滤掉,最后结果如下:
如果先进行谓词下推的情况。先对两表进行过滤,过滤的结果分别如下:
然后再对这两个过滤后的表进行内连接处理,结果如下:
可见,这和先进行join再过滤得到的结果一致
4.2 Join后条件通过OR连接
select
*
from lefttable a
inner join right b
on a.id = b.id
where a.value = 'two' or b.value = 'one'
先进行join处理,临时表的结果如下:
a.value | b.value | ||
1 | 1 | one | one |
2 | 2 | two | two |
然后使用where条件进行过滤,最张查询结果如下:
a.value | b.value | ||
1 | 1 | one | one |
2 | 2 | two | two |
如果先使用where条件对每个表进行过滤,两表的过滤结果如下:
然后对两个临时表进行内连接处理,结果如下:
那么为什么where条件中两表的条件被or连接就会出现错误的查询结果呢?是因为对于or两侧的过滤条件,任何一个满足条件即可以返回true,那么对于a.value = 'two' or v.value = 'two'这个查询条件,如果使用a.value = 'two'的左表记录过滤出来,那么一起又看流对于左表中a.value不为two的行,他们可能在跟右表使用id字段连接上之后,右表的b.value恰好为two,也满足a.value = 'two' or b.value = 'two',但因之前的过滤处理已经被处理掉,所以这种情况下谓词是不能下推的
但or连接两表join后条件也有两个例外。第一个例外是过滤条件字段恰好为join字段,比如:
select
*
from lefttable a
inner join righttable b
on a.value = b.value
where a.value = 'two' or b.value = 'two'
在这个查询中,join后条件依然是使用or连接两表的过滤条件,不同的是,join中条件不再是id相等,而是value字段相等,也就是过滤条件字段恰好就是join条件字段。此时,谓词下推和不下推产生的结果是相同的。
4.3 分区表使用or连接过滤条件
select
*
from lefttable a
inner join righttable b
on a.value = b.value
where a.pt = '20171101' and a.value = 'two'
or b.pt = '20171102' and b.value = 'two'
此时左表和右表都不再是普通的表,而是分区表,分区字段是pt,按照日期进行数据分区。同时两表查询条件依然使用OR进行连接。如果不能提前对两表进行过滤,那么会有非常巨量的数据要首先进行连接处理,这个代价是非常大的。但是如果按照我们在2中的分析,使用OR连接的过滤条件,又不能随意的进行谓词下推,那要如何处理?SparkSql在这里使用了一种叫做“分区裁剪”的优化手段,即把分区并不看做普通的过滤条件,而是使用了“一刀切”的方法,把不符合查询分区条件的目录直接排除在待扫描的目录之外。我们知道分区表在HDFS上是按照目录来存储一个分区的数据的,那么在进行分区裁剪时,直接把要扫描的HDFS目录通过Spark的Scan操作符,这样,Spark在进行扫描时,就可以直接过滤到其它分区的数据。但是,要完成这种优化,需要SparkSql的主义分析逻辑能够正确地分析出Sql语句所要表达的精确目的,所以分区字段在SparkSql的元数据中也是独立于其它普通字段,进行了单独的标示,就是为了方便语义分析逻辑能区别处理Sql语句中where条件里的这种特殊情况。