最近工作中SparkSQL写了也不少了,整天Join来Join去的,哈哈哈,所以来了解下Join的底层原理吧,不想把Join当个黑盒一样的去使用。Spark支持Inner、full outer、left join、right join 、leftsemi、leftanti、cross这几种Join方式,具体每种Join得到的结果是啥,这里就不一一解释了,可见"参考一"中的内容...本人一般常用的也就是Inner 、outer、 left join 和 leftsemi使用的多一点,其他几个暂时用的还不多。
等值Join VS 非等值Join:
SparkSQL和HiveSQL不同,HiveSQL只支持等值连接,但是SparkSQL非等值连接也是支持的。
等值连接和非等值连接的区别是:如果on语句中包含一个相等条件或多个需要同时满足的相等条件,那么称为等值连接,否则就称为非等值连接。
非等值连接例如下面这两种:A.x < B.x A.x == B.x or A.y == B.y,除非业务需求,否者尽量不要使用非等值连接,相对等值连接要慢。
Spark中的5种Join策略:
Spark提供了五种执行Join实现机制,分别是:
-
Broadcast Hash Join:只能用于等值Join,性能最好。Left Join时只能广播右表,不能广播左表,而且不支持Full outer join
-
Shuffle Hash Join:只能用于等值Join,性能次之。Join的Type和上面的一致,但是默认情况下不开启(说明Spark其实不太建议我们使用这种Join方式,个人觉得Join的其中一张表是大表,另外一张表不大时可以考虑开启该方式)
-
Sort Merge Join:只能用于等值Join,是最常被用的就是这种Join方式。
-
Cartesian Join:即笛卡尔积,如果两张表没有指定Join条件,且是内连接类型,回选用此方案。
-
Broadcast Nested Join:最后的保底手段,支持任意类型,任意条件的Join。
前三种Join的方式是在实际项目中经常被用到的,接下来会重点说明下。后面两种不常用,除非业务需要,否则也应当尽量避免使用。同时Shuffle Hash Join一般也不建议开启,后面会说到这种Join方式对内存还是有点要求的,算是一个不稳定的因素。
Broadcast Hash Join:
一般根据数据表的角色不同分为streamedTable流式表和BuildTable构建表,通常会把大表设定为流式表,将小表设定为构建表。在Join运算过程中,会遍历流式表的每条记录,然后在构建表中查找相匹配的记录进行匹配。BHJ这种Join方式会将小表Broadcast到各个Executor上,构建成“HashMap”,然后大表从“HashMap”中寻找对应的记录关联到一起,这样就规避掉了Shuffle。流程图如下所示:
Join代码如下图所示:hashed变量就是broadcast中获取到的广播的小表,被构造成了一个类似HashMap的结构。然后使用迭代器遍历流式表(单个Partition)中的每条记录,去从“HashMap”中找到对应的记录即可:
、
BroadcastHashJoin虽然速度快,但是它需要将构建表的数据广播出去,这要求在Driver端先获取到所有数据,然后Driver端还要把数据Broadcast到各个Executor上,所以小表不能太大,默认情况下是10M,个人建议不超过20M吧。本人曾经试过50个Executor,Driver配置了2G的内存,20M左右的小表进行Broadcast,Driver端内存溢出了。
Shuffle Hash Join:
和上面的差不多,也是对构建表构造一个“HashMap”。不过它是使用ZipPartitions的方式讲两个RDD中数据中相同Key的数据划分到同一个分区中,然后对小表中的数据构建HashMap,小表就是下面源码中的buildIter。如果数据量大的话,这个HashMap占用的内存也不小,所以说这种Join方式是对内存有要求的。源码以及流程图如下面两图所示:
Sort Merge Join:
当两个表的数据量都非常大时,会使用SortMergeJoin方式进行Join。也是对两张表进行ZipPartition操作,将相同Key的数据划分到同一个分区中,但是之后不会把构建表构造HashMap了,而是对分区中的数据按照Join Key 排序,然后对排序之后的数据不停迭代,按照Key的顺序逐个对比寻找匹配的记录。流程图如下所示:
源码如下所示,不同的Join方式执行的流程还有点差异,这里就以Inner Join为例,看下Join是如何运行的:
KeyGenerator用来作用在每条记录上,生成比较的Key;keyOrdering用于比较Key是否相同;RowIterator就是数据遍历的迭代器了;
join的工作都是在smjScanner类中实现的,这个类的作用就是遍历数据,然后进行Join,核心方法就是下面的findNextInnerJoinRows():
findNextInnerJoinRows()方法中前面一直都在判断是否碰到了null的记录(null意味表遍历结束或者构建表需要前移) ,核心匹配逻辑就是如果没有Key不匹配,就相应的移动流式表或者构建表的记录,如果匹配了,就把数据缓存到bufferedMatches中:
最终会像这样记录流式表和构建表Join之间的关系:
Join 选择策略:
Join策略选取的代码在SparkStrategy.scala中,代码很长就不贴了,主要逻辑如下:
如果是等值Join时:
用户如果指定了hint,那么优先使用hint方式进行Join,例如BROADCASTJOIN。
如果用户没有指定任何 Join hint,那根据 Join 的适用条件按照 Broadcast Hash Join -> Shuffle Hash Join -> Sort Merge Join顺序选择 Join 策略。BHJ要求广播的表大小小于"spark.sql.autoBroadcastJoinThreshold",默认情况下这个值是10M。SHJ则要求小表的大小得是大表的1/3一下,并且小表的大小要小于 conf.autoBroadcastJoinThreshold * conf.numShufflePartitions。以上两者都不满足的话,就使用SMJ了。
非等值Join,先暂不关心...
ps:spark支持再同一条SQL语句中写多个Hint,例如:
select /*+ REPARTITION(1) */ /*+ BROADCASTJOIN (a) */ /*
select /*+ REPARTITION(1), BROADCASTJOIN (a) */
两种写法都可以
参考:
https://www.cnblogs.com/yixianyixian/p/9336840.html(SQL中的各种Join图解)
https://xie.infoq.cn/article/612ccea5c1762c351d8d139c8(Spark Join介绍)
https://blog.csdn.net/chengujun7940/article/details/100837797(Spark中的Join图示)