每天一个小例子:
spark中,
1.left semi join (左半连接)的意思就是,
JOIN 子句中右边的表只能在 ON 子句中设置过滤条件,右表中有重复的数据会自动跳过,具体详见:
2. left anti join
含义:left anti join 是 not in/not exists 子查询的一种更高效的实现,相当于not in或者not exists,left anti join只会取左表的字段,右边的字段只用来做判断,不会取出来
select id
from t1
where t1.id not in (select id from t2);
进入正题
broadcast join
其实这个是最简单的, spark.sql.autoBroadcastJoinThreshold 所配置的值,默认是10M,当某一张表的大小小于这个值时,将这张表收集到driver,然后广播到每一个executor上,这样的好处就是,大表进行join的时候,按照分区划分为多个partition,然后每一个partition与executor上的小表进行连接,小表全程都是存放在内存中,没有进行磁盘io的,速度就快。注意:将 spark.sql.autoBroadcastJoinThreshold 参数设置为 -1,可以关闭这种连接方式;只能用于等值 Join,不要求参与 Join 的 keys 可排序 。
shuffle hash join
因为被广播的表首先被collect到driver段,然后被冗余分发到每个executor上,所以当表比较大时,采用broadcast join会对driver端和executor端造成较大的压力。这个使用场景就是大表连接小表,比上面的那张表要大一些,具体的操作就是两张表,一张为streamlter(迭代器的形式)也是大表(左边),buildlter就是小表,这里的话,分区内的数据无需排序,因为右边的分区的数据都会放在hashmap中,其实就是内存里(如果内存放不下会dump到磁盘中),然后也是会和左边大表合并,遇见相同的join key就合并成一条新数据。那么为神马要进行分区呢,主要是有分区的话就可以进行并行计算,和广播的方式对比,其实就是小表也进行了分区,根据分区,大表和小表有相同分区的分配到同一个executor上,然后和将小表的数据进行hash到一个hashtable中,然后与大表分区的join key进行关联
不难发现,要将来自buildIter的记录放到hash表中,那么每个分区来自buildIter的记录不能太大,否则就存不下,默认情况下hash join的实现是关闭状态,如果要使用hash join,必须满足以下四个条件:
要启用 Shuffle Hash Join策略,必须满足以下条件:
1.仅支持等值 Join,不要求参与 Join 的 Keys 可排序(这点是和sort-merge join相对应)
2.spark.sql.join.preferSortMergeJoin 参数必须设置为 false,参数是从 Spark 2.0.0 版本引入的,默认值为true,也就是默认情况下选择 Sort Merge Join;
3.小表的大小(plan.stats.sizeInBytes)必须小于 spark.sql.autoBroadcastJoinThreshold *spark.sql.shuffle.partitions(默认值200)其实就是让每一个小表的分区都类似于广播变量的小表;
4.而且小表大小(stats.sizeInBytes)的三倍必须小于等于大表的大小(stats.sizeInBytes),也就是a.stats.sizeInBytes * 3 < = b.stats.sizeInBytes
sort merge join
这种sort-merge join是spark2.0默认采用的方式(对于有聚合算子count,sum这些,底层其实就是rdd的sort-base的shuffle算法,就是可以在map端进行预聚合,采用PartitionAppendOnlyMap数据结构的),最适合大表连接大表,和上面的hash join相比,其实就是按照重分区之后(图中的4个),会对分区中的数据进行排序(比如buildlter中p0,p1,p2三个分区有重分区之后的p0的数据时,会一边进行收集一边排序),原因就是,这种情况下的右表比较大,无法完全的放入到内存中(放不下的数据需要spill到磁盘中),所以进行排序,按照join key顺序,在各个分区中(图中为4个分区),比如左表和右表p0分区的数据一条一条地进行匹配,由于我们是通过右表去匹配左表,所以右表所采用的数据结构的性能必须要择优。
由于两个表都是排序的,每次处理完streamIter的一条记录后,对于streamIter的下一条记录,只需从buildIter中上一次查找结束的位置开始查找,所以说每次在buildIter中查找不必重头开始,整体上来说,查找性能还是较优的。简单来说,两张表都是顺序从上往下找,如图,左表第一条数据在右表中没找到,跳过,第二条数据(3,r3),发现右表有,匹配上,生成新纪录。table1到第三条数据,table2到第二条数据,这样顺序地往下找。
注意:要启用 Shuffle Sort Merge Join 必须满足的条件是仅支持等值 Join,并且要求参与 Join 的 Keys 可排序。
额外的:如果 Join 的 Key 为不等值Join 或者没有指定 Join 条件,则会选择 Broadcast nested loop join 或 Shuffle-and-replicate nested loop join。
AE
这个是sparkSQL进行shuffle时的两个stage,在stage0中,根据join key分成了5个分区,各个分区大小总和分别为60m,40m,1m,2m,50m,ae会通过shuffle write时统计各个分区的大小,通过 ExchangeCoordinator(这个是spark的协调器,只在shuffleExchangeExec的物理执行计划注册到coordinator时才会有这个协调器) 计算出合适的 post-shuffle Partition 个数(即 Reducer)个数(本例中 Reducer 个数设置为 3)启动相应个数的 Reducer ,每个 Reducer 读取一个或多个 Shuffle Write Partition 数据(如下图所示,Reducer 0 读取 Partition 0,Reducer 1 读取 Partition 1、2、3,Reducer 2 读取 Partition 4),那么为什么是3个reducer?
我们先做个预习,稍微讲一下coordinator,一个coordinator有三个参数:
- numExchanges
- targetPostShuffleInputSize
- minNumPostShufflePartitions
第一个参数是用于表示有多少个ShuffleExchangeExec需要注册到这个coordinator里面。因此,当我们要开始真正执行(doExecute)时,我们需要知道到底有多少个ShuffleExchangeExec
第二个参数是表示后面shuffle阶段每个partition的输入数据大小,这是用于adaptive-execution的,用于推测后面的shuffle阶段需要多少个partition(具体方法就是统计shuffle write之后各个分区的数据量),可以通过spark.sql.adaptive.shuffle.targetPostShuffleInputSize来配置
第三个参数是一个可选参数,表示之后shuffle阶段最小的partition数量。如果这个参数被定义,那么之后的shuffle阶段的partition数量不能小于这个值
有了coordinator的基本概念,下面可以解释一下为什么上面的例子是3个reducer?
协调器中的参数targetPostShuffleInputSize 默认为 64MB,每个 Reducer 读取数据量不超过 64MB,如果 Partition 0 与 Partition 2 结合,Partition 1 与 Partition 3 结合,虽然也都不超过 64 MB。但读完 Partition 0 再读 Partition 2,对于同一个 Mapper 而言,如果每个 Partition 数据比较少,跳着读多个 Partition 相当于随机读,在 HDD 上性能不高
目前的做法是只结合相临的 Partition,从而保证顺序读,提高磁盘 IO 性能
该方案只会合并多个小的 Partition,不会将大的 Partition 拆分,因为拆分过程需要引入一轮新的 Shuffle。基于上面的原因,默认 Partition 个数(本例中为 5)可以大一点,然后由 ExchangeCoordinator 合并。如果设置的 Partition 个数太小,Adaptive Execution 在此场景下无法发挥作用
除此之外,我们在上述例子中,虽然说是顺序读取Partition 1到Partition 3,但是也是发起了3次请求才读到第一个stage0 task0的三个分区,于是乎,spark也新增接口,可以实现一次请求读取3个分区,减少了网络通信的代价。
通过spark.sql.adaptive.shuffle.targetPostShuffleInputSize 可设置每个 Reducer 读取的目标数据量,其单位是字节,默认值为 64 MB。上文例子中,如果将该值设置为 50 MB,最终效果仍然如上文所示,而不会将 Partition 0 的 60MB 拆分。