【Spark】Spark Join类型及Join实现方式
Spark Join类型
1. Inner Join (内连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "inner")
- 执行逻辑:只返回那些在两个表中都有匹配的行。
2. Left Join (左外连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "left")
- 执行逻辑:返回左表的所有记录,并且右表的匹配行,若右表没有匹配行则返回
null
。
3. Right Join (右外连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "right")
- 执行逻辑:返回右表的所有记录,并且左表的匹配行,若左表没有匹配行则返回
null
。
4. Full Join (全外连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "outer")
- 执行逻辑:返回左表和右表的所有记录,若某一方没有匹配,另一方则填充
null
。
5. Left Semi Join (左半连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "left_semi")
- 执行逻辑:返回左表中与右表匹配的行,只返回左表数据,不返回右表。
6. Left Anti Join (左反连接)
- 示例:
val result = df1.join(df2, df1("id") === df2("id"), "left_anti")
- 执行逻辑:返回左表中不与右表匹配的行。
7. Cross Join (笛卡尔积连接)
- 示例:
val result = df1.crossJoin(df2)
- 执行逻辑:返回两表的笛卡尔积,左表的每一行与右表的每一行组合。
Spark Join实现方式
在 Spark 中,
Join
操作有多种实现方式,每种方式的实现原理、适用场景和执行性能有所不同。接下来我们详细讨论以下几种常见的
Join
实现方式:
1. CPJ(Cartesion Product Join)笛卡尔积连接
工作原理:
- 笛卡尔积连接是最基础的连接方式,它将两个数据集的每一条记录与另一个数据集的每一条记录进行配对,从而生成一个新的结果集。这个操作是非常低效的,因为它会产生
N * M
条记录(N
和M
分别是两个数据集的行数)。- 这种方式不需要连接条件,因此通常不是我们期望的连接类型。
执行性能:
- 效率低:当两个数据集的大小很大时,计算量将急剧增加。通常,笛卡尔积连接仅在明确需要时使用(例如,计算所有可能的配对)。
Spark 选择笛卡尔积的情况:
- 笛卡尔积连接在 Spark 中通常是显式调用
crossJoin()
时使用。2. SMJ(Shuffle Sort Merge Join)排序归并连接
工作原理:
- 排序归并连接首先对两个数据集按照连接键进行排序,然后使用
merge
操作将排序后的数据集进行合并。数据集会被按连接键进行shuffle
,然后在每个分区内执行归并操作。- 这种方法非常适合处理大规模的分布式数据,尤其是当两个数据集都很大并且有良好的分区时。
执行性能:
- 效率较高:适合大数据量的连接,尤其当连接键有排序特性时。
- 由于需要对数据进行排序和
shuffle
,这会增加网络和磁盘的 I/O 成本。Spark 选择 SMJ 的情况:
- 当数据集较大并且 Spark 能够进行有效的
shuffle
操作时,Spark 会选择SMJ
。- 如果连接的表已经分区或有排序字段,则 Spark 会优先选择该方式。
3. SHJ(Shuffle Hash Join)哈希连接
工作原理:
- 哈希连接(SHJ) 是一种基于哈希表的连接方式。其基本思想是将一个表(通常是较小的表)哈希到内存中,然后通过哈希表查找另一个表的匹配记录。该方法特别适合处理大规模的数据集,尤其是当连接的两个数据集都比较大时,或者当连接键不具有顺序或排序特性时。
- 执行步骤:
- 分区阶段(Shuffle):首先,Spark 会将两个数据集根据连接键进行
shuffle
(重分区),确保具有相同连接键的记录被发送到同一个节点。此时,数据会按照连接键进行重分区。- 构建哈希表:选择较小的表(通常是内表),在每个节点上对该表进行哈希,构建哈希表。哈希表存储连接键及其对应的记录。
- 匹配查找:然后,在同一个节点上扫描较大的表(外表),对于每一条记录,使用相同的连接键查找哈希表中的匹配项。如果匹配,则生成结果。
执行性能:
- 高效:相比传统的嵌套循环连接(
NLJ
),哈希连接通常在处理大数据集时更为高效,特别是当连接条件是等值连接时。Spark 选择 SHJ 的情况:
- 外表大小至少是内表的3倍且内表的数据分片平均大小要小于广播变量阈值,Spark 会选择 Shuffle Hash Join。
4. BNLJ(Broadcast Nested Loop Join)广播嵌套循环连接
工作原理:
- 广播嵌套循环连接是嵌套循环连接的一种优化形式,针对连接的一个表较小的情况。它首先将较小的表(通常是内表)广播到所有执行节点,然后对大表(通常是外表)进行扫描。在每个节点上,将小表加载到内存中,并在每个分区上与外表进行连接。
执行性能:
- 高效:相比于传统的嵌套循环连接(
Nested Loop Join
),广播嵌套循环连接的效率较高,因为它通过将小表广播到每个节点,避免了全局的shuffle
操作,减少了数据传输的延迟。- 适合当一个表非常小(例如,
broadcast()
小表时)时,执行性能特别好。Spark 选择 BNLJ 的情况:
- Spark 会自动选择 Broadcast Nested Loop Join,当数据集中的一个表较小(可以放入内存)时,Spark 会选择该表进行广播,从而提高连接操作的性能。通常,Spark
会根据表的大小和内存限制来决定是否使用广播join
。5. BHJ(Broadcast Hash Join)广播哈希连接
工作原理:
- 广播哈希连接通过将一个小表广播到所有执行节点,从而避免了全局的
shuffle
操作。大的数据集会被分配到多个节点,而小的数据集会被广播到每个节点。- 这种方式非常高效,适用于连接一个大表和一个小表的情况。
执行性能:
- 效率非常高:适用于大表和小表连接,避免了大规模的
shuffle
操作。- 适合当一个表非常小(例如,
broadcast()
小表时)时,执行性能特别好。Spark 选择 BHJ 的情况:
- 如果其中一个表很小,Spark 会选择
BHJ
,因为将小表广播到所有节点可以大大减少shuffle
的开销。Spark 如何选择
Join
策略?1. 等值 Join
在等值数据关联中,Spark 会尝试按照以下顺序选择最优的连接策略:
- BHJ(Broadcast Hash Join)
- SMJ(Shuffle Sort Merge Join)
- SHJ(Shuffle Hash Join)
适用场景:
- BHJ(Broadcast Hash Join): 连接类型不能是全连接(Full Outer Join),基表需要足够小,能够放入内存并通过广播发送到所有节点。
- SMJ(Shuffle Sort Merge Join)与 SHJ(Shuffle Hash Join):支持所有连接类型,如Full Outer Join,Anti join
为什么SHJ比SMJ执行效率高,排名却不如SMJ靠前
- 相比 SHJ,Spark优先选择SMJ的原因在于,SMJ的实现方式更加稳定,更不容易OOM
- 在 Spark 中,SHJ(Shuffle Hash Join) 策略要想被选中,需要满足以下两个先决条件:
- a. 外表大小至少是内表的 3 倍:只有当内外表的尺寸悬殊到一定程度时,SHJ 的性能优势才会明显超过 SMJ。
- b. 内表的数据分片平均大小要小于广播变量阈值:内表的数据分片必须足够小,以便能够通过广播传递到各个节点,而不引起内存溢出或性能问题。
- 相比 SHJ,SMJ没有这么多的附加条件,无论是单表排序,还是两表做归并关联,都可以借助磁盘来完成。内存中放不下的数据,可以临时溢出到磁盘
2. 非等值 Join
- 在非等值数据关联中,Spark可选的Join策略只有BNLJ(Broadcast Nested Loop Join)和CPJ(Cartesion Product Join),BNLJ适合内表满足广播情况,否则只能用CPJ兜底