目录:
1.左表 join 后条件下推
2.左表join中条件不下推
3.右表join中条件下推
4.右表join中条件不下推
5.总结

在《SparkSql连接查询中的谓词下推处理(一)》中,我们介绍了一些基本的概念,并对内连接查询时的一些基本下推规则进行了分析。

本篇文章要介绍的是--外连接查询中的谓词下推规则,这相比内连接中的规则要复杂一些,不过使用简单的表格来进行分析也是可以分析清楚的。先上表:

spark select in spark select into_连接查询

我们以左外连接查询为例,先总结规矩如下:

spark select in spark select into_子查询_02

接下来对这个表格中的规则进行详细的分析。

1.左表join后条件下推

查询语句如下:

spark select in spark select into_连接查询_03

前文有提到,对于join后条件,如果放在join操作后执行,是可以作为正确结果进行比对的。那么先对两表进行左连接,结果如下:

spark select in spark select into_数据_04

然后使用LT.id>1这个join后条件进行过滤,结果如下:

spark select in spark select into_连接查询_05

来分析一下LT.id>1下推到左表进行数据过滤的结果,经过LT.id>1过滤后,左表变为:

spark select in spark select into_数据_06

此时再和右表进行左连接,左表id为2的行,在右表中能找到id为2的行,则连接结果如下:

spark select in spark select into_spark select in_07

可见,两种处理方法结果一致。条件下推过滤了左表整整50%的数据(相当牛,虽然只过滤了一条)。究其原因,是因为在SparkSQL中,把以上的查询解析成了如下的子查询:

spark select in spark select into_子查询_08

这是一个非相关子查询,即完全可以先完成子查询,再完成父查询,子查询在查询过程中和外部查询没有关联关系。

2.左表join中条件不下推

查询语句如下:

spark select in spark select into_子查询_09

来看看不下推的情况下计算出的正确结果,join过程如下:

第一步:左表id为1的行在右表中能找到相等的id,但是左表的id为1,是不满足第二个join条件(LT.id>1)的,所以左表这一条相当于没有和右表join上,所以左表的值value保留,而右表的value为null(你没满足join中条件没join上还把你的值保留,给我搞个空值?没办法,就是这么任性)。

第二步:左表id为2的行在右表中能找到,而且左表id为2的行的id大于1,两个join条件都满足,所以算是和右表join上了,所以左表和右表的value都保留。最终的查询结果如下:

spark select in spark select into_spark select in_10

那么如果把"LT.id>1"这个条件下推到做表,会得到什么结果呢?

首先左表经过"LT.id>1"过滤后,如下:

spark select in spark select into_子查询_11

此时再和右表连接,左表id为2的行在右表中能找到,且满足"LT.id = RT.id AND LT.id > 1"这个join中条件,所以两表的value都被保留。左表中已经没有数据了,查询结束,查询结果如下:

spark select in spark select into_连接查询_12

这个查询结果和不下推的正确结果不一致,是个错误的结果,所以左表join中条件是不能下推进行数据过滤的。分析原因:主要是因为join中条件和join后条件对结果的处理方式不同,前者在不满足join条件时会保留一部分结果,而后者在不满足条件时任何东西都不保留。

3.右表join中条件下推

查询语句如下:

spark select in spark select into_子查询_13

现在把RT.id>1这个右表join后条件下推,来过滤右表,过滤后如下:

spark select in spark select into_连接查询_14

然后左表再和右表进行左连接,流程如下:

第一步:左表id为1的行在右表中没有,此时左表值保留,右表为null;

第二步:左表id位2的行在右表中有,并且RT.id大于1,两个join条件都满足,则左表和右表的值都保留。查询结果如下:

spark select in spark select into_数据_15

那么如果不下推(为了得到正确结果),来看看结果,流程如下:

第一步:左表id为1的行在右表中有,但是不满足第二个join条件,所以这行算是没join上,所以左表数据保留,右表为null;

第二步:左表id为2的行在右表中有,也满足第二个join条件,所以左右表的数据都保留。

spark select in spark select into_数据_16

可见,右表join中条件下推不下推,结果一样,所以,干吗不下推?可以过滤掉一半的数据呢。SparkSQL中的等价处理语句是:

spark select in spark select into_数据_17

可以看出,也是解析成了一个非相关子查询来处理的。

4.右表join中条件不下推

这个应该是最违反常规理解的查询了,查询语句如下:

spark select in spark select into_连接查询_18

首先来看,join后条件不下推的情况,流程如下:

(gdb) disass
Dump of assembler code for function main.start:
 0x000000000044fc90 <+0>:mov %fs:0xfffffffffffffff8,%rcx
 0x000000000044fc99 <+9>:cmp 0x10(%rcx),%rsp
 0x000000000044fc9d <+13>:jbe 0x44fcfa <main.start+106>
 0x000000000044fc9f <+15>:sub $0x20,%rsp
 0x000000000044fca3 <+19>:mov %rbp,0x18(%rsp)
 0x000000000044fca8 <+24>:lea 0x18(%rsp)www.yuntianyuL.cn,%rbp
 0x000000000044fcad <+29>:xor %eax,%eax
 0x000000000044fcaf <+31>:jmp 0x44fcd0 <main.start+64>
 0x000000000044fcb1 <+33>:mov %rax,0x10(%rsp)
 0x000000000044fcb6 <+38>:nop
 0x000000000044fcb7 <+39>:nop
=> 0x000000000044fcb8 <+40>:lea 0x241e1(%rip),%rax # 0x473ea0
 0x000000000044fcbf <+47>:mov %rax,(%rsp)
 0x000000000044fcc3 <+51>:callq 0x447380 <runtime.mcall>
 0x000000000044fcc8 <+56>:mov 0x10(%rsp)www.yuntianyugw.com,%rax
 0x000000000044fccd <+61>:inc %rax
 0x000000000044fcd0 <+64>:cmp $0x3b9aca00,%rax
 0x000000000044fcd6 <+70>:jl 0x44fcb1 www.shengdaptd.cn<main.start+33>
 0x000000000044fcd8 <+72>:nop
 0x000000000044fcd9 <+73>:mov 0x28(%rsp),%rax
 0x000000000044fcde <+78>:mov %rax,(%rsp)
 0x000000000044fce2 <+82>:movq $0xffffffffffffffff,0x8(%rsp)
 0x000000000044fceb <+91>:callq 0x44f8f0 <sync.(*WaitGroup).Add>
 0x000000000044fcf0 <+96>:mov 0x18(%rsp),%rbp
 0x000000000044fcf5 <+101>:add $0x20,%rsp
 0x000000000044fcf9 <+105>:retq 
 0x000000000044fcfa <+106>:callq 0x447550 <runtime.morestack_noctxt>
 0x000000000044fcff <+111>:jmp 0x44fc90 www.meiwanyule.cn <www.chengmingdL.com main.start>


复制代码
可以看到当前正在执行的函数是main.start而不是runtime.Gosched,在整个start函数中都找不到Gosched函数的身影,原来它被编译器优化了。程序现在停在了0x000000000044fcb8 <+40>: lea 0x241e1(%rip),%rax 这一指令处,该指令下面的第二条callq指令在调用runtime.mcall,我们首先使用si 2来执行2条汇编指令让程序停在下面这条指令处:

=> 0x000000000044fcc3 <+51>: callq 0x447380 <runtime.mcall>
然后使用i r rsp rbp rip记录一下CPU的rsp、rbp和rip寄存器的值备用:

(gdb) i r rsprbprip
rsp 0xc000031fb0 0xc000031fb0
rbp 0xc000031fc8 0xc000031fc8
rip 0x44fcc3 0x44fcc3 <main.start+51>
继续看0x000000000044fcc3位置的callq指令,它首先会把紧挨其后的下一条指令的地址0x000000000044fcc8放入g2的栈,然后跳转到mcall函数的第一条指令开始执行。回忆一下第二章我们详细分析过的mcall函数的执行流程,结合现在这个场景,mcall将依次完成下面几件事:

把上面call指令压栈的返回地址0x000000000044fcc8取出来保存在g2的sched.pc字段,把上面我们查看到的rsp(0xc000031fb0)和rbp(0xc000031fc8)分别保存在g2的sched.sp和sched.bp字段,这几个寄存器代表了g2的调度现场信息;

把保存在g0的sched.sp和sched.bp字段中的值分别恢复到CPU的rsp和rbp寄存器,这样完成从g2的栈到g0的栈的切换;

在g0栈执行gosched_m函数(gosched_m函数是runtime.Gosched函数调用mcall时传递给mcall的参数)。

继续看gosched_m函数

runtime/proc.go : 2623
// Gosched continuation on g0.
func gosched_m(gp *g) {
 if trace.enabled { //traceback 不关注
 traceGoSched()
 }
 goschedImpl(gp) //我们这个场景:gp = g2
}
gosched_m函数只是简单的在调用goschedImpl:runtime/proc.go : 260
func goschedImpl(gp *g) {
 ......
 casgstatus(gp, _Grunning, _Grunnable)
 dropg() //设置当前m.curg = nil, gp.m = nil
 lock(&sched.lock)
 globrunqput(gp) //把gp放入sched的全局运行队列runq
 unlock(&sched.lock)

 schedule() //进入新一轮调度

第一步:左表id为1的行在右表中可以找到,但是此时仅仅满足join条件,在使用where条件判断这条连接后数据时,发现右表的id不满足RT.id>1的条件,所以这条join结果不保留(注意:这里是不保留,全都不保留,左表右表都不保留,要跟上边的没join上而右表的值保留为null的情况区别开,这也是关键所在);

第二步:左表id为2的行和右表id为2的行join上了,同时也满足RT.id>1的where条件。

spark select in spark select into_数据_19

这是一条符合语义的正确的查询结果。

好了,接下来看看右表join后条件下推的情况:

第一步:使用RT.id>1过滤右表,过滤后右表只剩一行id为2的行;

第二步:左表id为1的行在过滤后的右表中没有,此时左表值保留,右表值为null;

第三步:左表id为2的行在右表中有,此时左表值保留,右表值也保留。

结果如下:

spark select in spark select into_数据_20

很明显这其实是一个错误的结果。

总结

至此,左连接查询的四条规则分析完了。可以看出,在SparkSQL中对于外连接查询时的过滤条件,并不能在所有情况下都用来进行数据源的过滤,如果使用得当会极大的提升查询性能,如果使用不当,则会产生错误的查询结果,而这种错误结果又不易发觉,所以使用时要格外小心。