Spark中Join实现原理

Spark中5种join策略

原文

Join算子是数据处理中最常用的操作,Spark作为通用数据处理引擎,提供了丰富的Join操作场景。这篇文章将介绍Spark中5种Join策略,希望对你有所帮助。文章中主要内容有以下几个方面:

  • 影响Join的因素
  • Spark中5种Join执行策略
  • Spark如何选择Join策略

影响Join的因素

输入数据的大小

Join处理数据的大小会直接影响Join的执行效率,同时也会影响选择何种Join执行策略。

Join条件

Join条件是指Join列之间的逻辑比较类型,根据连接条件可分为等值连接和不等值连接,等值连接是指一个或多个等值条件被同时满足,每个等值条件作用于两个输入数据集的列上。其他不是使用=作为连接条件Join都是非等值连接。

Join类型

在Join条件作用于输入数据集后,Join类型决定Join算子输出结果。主要有以下几种Join类型:

  • 内连接Inner Join:只有满足连接条件的输入数据才会作为结果
  • 外连接Outer Join:又分为左外连接、右外连接、全连接
  • 半连接:右边表只是用来过滤左边表中数据,并不会出现在结果集中,类似in/exists语义
  • 交叉连接:又称笛卡尔乘积,结果包含左边表中每一行与右边表中每一行组合

五种Join执行策略

spark提供如下五中Join执行策略实现Join操作:

  • Shuffle Hash Join
  • Broadcast Hash Join
  • Sort Merge Join
  • Cartesian Join
  • Broadcast Nested Loop Joi

Shuffle Hash Join

简介

当Join的表数据量很大时,可使用Shuffle Hash Join. 这样一张大表就可根据join key分区,同时相同join key被划分到同样的分区,如下图:
在这里插入图片描述

上图所示,shuffle hash join主要包含以下两步:

  1. 参与join的两张表通过join key重分区,这即是shuffle过程,目的就是把相同join key数据发送到同一分区进行分区内join
  2. 对于shuffle后的分区,来自小表的分区数据构建成Hashtable, 然后与来自大表的partition数据根据join key进行匹配。
条件和特点
  • 只支持等值连接,无须根据join key排序
  • 除全连接外join类型都支持
  • 对小表中数据构建Hash map是一种耗内存操作,如果某一边构建的Hash table过大,可能会导致OOM
  • 设置参数spark.sql.join.prefersortmergeJoin=false, 默认为true

BroadCast Hash Join

简介

又称Map side Join。当有表比较小时,经常选择Broadcash Hash Join,这样可以避免shuffle压力和提高性能。例如,当事实表与维表关联时,维表通常比较小,此时可使用broadcast hash join广播维表,这样可避免shuffle提高join效率,spark中shuffle是一种很耗时操作。执行broadcast join之前,Spark先将Executor上维表数据发送给Driver,再由Driver广播到Executor上执行Join。如果需要广播的数据比较大,可能导致Driver OOM,详细过程如下图所示:
在这里插入图片描述

Broadcast Hash Join主要包括以下两步:

  • 广播阶段:将小表缓存到executor上
  • Hash Join阶段:在executor上执行Hash Join
条件和特点
  • 只支持等值连接,无须根据join key排序

  • 除全连接外Join类型都支持

  • 相比其他Join策略,Braodcast Hash Join更高效。Braodcast Hash Join属于网络密集型操作,有很多冗余数据传输,此外由于Driver要缓存数据,当缓存的小表数据比较大时,可能发生OOM。

  • 广播的小表数据量应该比较少,spark.sql.autoBroadcastJoinThreshold默认上限是10M

  • 广播表大小不能超过8G。spark 2.4源码BroadcastExchangeExec.scala中:

longMetric("dataSize") += dataSize
 if (dataSize >= (8L << 30)) {
    throw new SparkException(s"Cannot broadcast the table that is larger than 8GB: ${dataSize >> 30} GB")
}
  • 基表不能被广播,例如左连接时,只有右表能被广播,不能这样fact_table.join(broadcast(dimension_table)显示提示使用broadcast,当满足条件时会自动切换成broadcast join

Sort Merge Join

简介

sort merge join是spark默认join策略,通过参数spark.sql.join.preferSortMergeJoin配置,默认是true,即优先选择Sort Merge Join。两张大表Join经常使用此方式,Sort Merge Join可减少集群里数据传输量,也不需将所有数据都加载进内存再进行hashjoin,但在join最后阶段需按join key排序,如下图所示:
在这里插入图片描述

Sort Merge Join主要包括三个步骤:

  1. Shuffle阶段:两张大表通过join key进行分区
  2. Sort阶段:每个分区内数据进行排序
  3. Merge阶段:来自不同表的排序分区进行连接操作,遍历记录进行数据合并,相同Join key进行连接
条件和特性
  • 只支持等值连接
  • 支持所有join类型
  • 按join列排序
  • 参数spark.sql.join.prefersortmergeJoin设置为true, 默认即是true

笛卡尔连接

简介

如果两张表连接条件中未指定join列,则将产生笛卡尔乘积,这时连接结果数量为两张表记录数乘积。

条件
  • 只支持内连接
  • 支持等值和不等值连接
  • 设置 spark.sql.crossJoin.enabled=true

Broadcast Nested Loop Join

简介

当没有合适的join策略可选择事,最终将会选择Broadcast Nested Loop Join。这些策略优先级:

Broadcast hash join > sort merge join > shuffle hash join > Cartesian join > broadcast nested loop join

如果是内连接或非等值连接,笛卡尔和Broadcast Nested Loop Join都可以时优先选择Broadcast Nested Loop策略。如果是非等值连接并且一张表能广播,则使用笛卡尔连接。

条件和特点
  • 支持等值和不等值连接
  • 支持所有join类型. 主要优化点如下:
  • 右外连接时广播左表,左外连接时广播右表
  • 内连接时左右表都广播

Spark如何选择Join策略

等值连接

如果有join提示, 按以下顺序:

  1. Broadcast Hint: 如果支持此种join, 则使用broadcast hash join
  2. Sort merge hint: 如果join列已排序, 则使用 sort merge join
  3. Shuffle hash hint: 如果支持此种join, 则使用shuffle hash join
  4. Shuffle replicate NL hint: 如果是内连接, 则使用 Cartesian product

如果无join hints, 依次检查以下规则:

  1. 如果join类型可使用broad cast hash join,并且一张表可被广播,即大小小于spark.sql.autoBroadcastJoinThreshold value(默认10MB), 则使用
    broadcast hash join
  2. 如果spark.sql.join.preferSortMergeJoin=false,并且一张表小到可以构建为hash map则使用shuffle hash join
  3. 如果join列可排序,则使用sort merge join
  4. 如果是内连接,则使用笛卡尔连接
  5. 如果可能发生OOM或者无可选择的执行策略,则使用broadcast nested loop join

非等值连接

有join提示, 按以下顺序:

  1. broadcast hint: 使用broadcast nested loop join
  2. shuffle replicate NL hint: 如果是内连接, 使用笛卡尔连接

无Join提示, 则按以下顺序:

  1. 一个表足够小可广播,使用 broadcast nested loop join
  2. 如果是内连接,使用笛卡尔连接
  3. 如果可能发生OOM或者无可选择的执行策略,则使用broadcast nested loop join

join策略选择源码:

 object JoinSelection extends Strategy
    with PredicateHelper
    with JoinSelectionHelper {
    def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {

      case j @ ExtractEquiJoinKeys(joinType, leftKeys, rightKeys, nonEquiCond, left, right, hint) =>
        def createBroadcastHashJoin(onlyLookingAtHint: Boolean) = {
          getBroadcastBuildSide(left, right, joinType, hint, onlyLookingAtHint, conf).map {
            buildSide =>
              Seq(joins.BroadcastHashJoinExec(
                leftKeys,
                rightKeys,
                joinType,
                buildSide,
                nonEquiCond,
                planLater(left),
                planLater(right)))
          }
        }

        def createShuffleHashJoin(onlyLookingAtHint: Boolean) = {
          getShuffleHashJoinBuildSide(left, right, joinType, hint, onlyLookingAtHint, conf).map {
            buildSide =>
              Seq(joins.ShuffledHashJoinExec(
                leftKeys,
                rightKeys,
                joinType,
                buildSide,
                nonEquiCond,
                planLater(left),
                planLater(right)))
          }
        }

        def createSortMergeJoin() = {
          if (RowOrdering.isOrderable(leftKeys)) {
            Some(Seq(joins.SortMergeJoinExec(
              leftKeys, rightKeys, joinType, nonEquiCond, planLater(left), planLater(right))))
          } else {
            None
          }
        }

        def createCartesianProduct() = {
          if (joinType.isInstanceOf[InnerLike]) {
            Some(Seq(joins.CartesianProductExec(planLater(left), planLater(right), j.condition)))
          } else {
            None
          }
        }

        def createJoinWithoutHint() = {
          createBroadcastHashJoin(false)
            .orElse {
              if (!conf.preferSortMergeJoin) {
                createShuffleHashJoin(false)
              } else {
                None
              }
            }
            .orElse(createSortMergeJoin())
            .orElse(createCartesianProduct())
            .getOrElse {
              val buildSide = getSmallerSide(left, right)
              Seq(joins.BroadcastNestedLoopJoinExec(
                planLater(left), planLater(right), buildSide, joinType, nonEquiCond))
            }
        }

        createBroadcastHashJoin(true)
          .orElse { if (hintToSortMergeJoin(hint)) createSortMergeJoin() else None }
          .orElse(createShuffleHashJoin(true))
          .orElse { if (hintToShuffleReplicateNL(hint)) createCartesianProduct() else None }
          .getOrElse(createJoinWithoutHint())

    
          if (canBuildLeft(joinType)) BuildLeft else BuildRight
        }

        def createBroadcastNLJoin(buildLeft: Boolean, buildRight: Boolean) = {
          val maybeBuildSide = if (buildLeft && buildRight) {
            Some(desiredBuildSide)
          } else if (buildLeft) {
            Some(BuildLeft)
          } else if (buildRight) {
            Some(BuildRight)
          } else {
            None
          }

          maybeBuildSide.map { buildSide =>
            Seq(joins.BroadcastNestedLoopJoinExec(
              planLater(left), planLater(right), buildSide, joinType, condition))
          }
        }

        def createCartesianProduct() = {
          if (joinType.isInstanceOf[InnerLike]) {
            Some(Seq(joins.CartesianProductExec(planLater(left), planLater(right), condition)))
          } else {
            None
          }
        }

        def createJoinWithoutHint() = {
          createBroadcastNLJoin(canBroadcastBySize(left, conf), canBroadcastBySize(right, conf))
            .orElse(createCartesianProduct())
            .getOrElse {
              Seq(joins.BroadcastNestedLoopJoinExec(
                planLater(left), planLater(right), desiredBuildSide, joinType, condition))
            }
        }

        createBroadcastNLJoin(hintToBroadcastLeft(hint), hintToBroadcastRight(hint))
          .orElse { if (hintToShuffleReplicateNL(hint)) createCartesianProduct() else None }
          .getOrElse(createJoinWithoutHint())
      case _ => Nil
    }
  }

总结

这篇文章介绍了spark中五种join策略,并且用图展示了最重要三种。文章开头梳理了影响join的因素,然后介绍五种join执行策略,同时详细解释了每种join策略原理及触发条件,最后展示了选择join策略相关代码。希望这篇文章能帮到你。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Sparkjoin操作有多种实现策略,包括Shuffle Hash Join(洗牌哈希连接)、Broadcast Hash Join(广播哈希连接)、Sort Merge Join(排序合并连接)、Cartesian Join(笛卡尔连接)和Broadcast Nested Loop Join(广播嵌套循环连接)。 Shuffle Hash JoinSpark的默认join策略,它将两个数据集根据连接键进行分区,并将相同连接键的数据发送到同一个分区。然后,通过哈希函数将两个数据集的相同连接键的数据分布到相同的节点上,并进行连接操作。这种策略适用于大规模数据集的连接操作,但可能会产生大量的网络传输和数据洗牌操作。 Broadcast Hash Join适用于一个小数据集和一个大数据集进行连接操作的场景。它将小数据集广播到所有的工作节点,然后使用哈希连接算法将大数据集与每个节点上的小数据集进行连接。由于小数据集被广播到每个节点,这种策略减少了数据洗牌的开销,提高了性能。 Sort Merge Join是一种通过对两个数据集进行排序后进行合并的连接策略。它适用于两个数据集都已经排序或者无法广播的情况。Sort Merge Join首先对两个数据集根据连接键进行排序,然后按照连接键进行合并操作。这种策略需要额外的排序操作,但可以避免数据洗牌。 Cartesian Join是一种比较低效的连接策略,它将两个数据集的每一条记录都与另一个数据集的所有记录进行连接。这种策略适用于小规模数据集的连接操作,但可能会导致数据爆炸和性能下降。 Broadcast Nested Loop Join是一种在嵌套循环连接的基础上使用广播的优化策略。它将一个小数据集广播到所有的工作节点,并在每个节点上执行嵌套循环连接操作。这种策略适用于一个小数据集和一个大数据集进行连接的场景,可以提高性能。 总的来说,Sparkjoin操作根据数据集的大小、排序状态和网络传输等因素选择合适的实现策略来进行连接操作,以提高性能和效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Spark SQL 之 Join原理](https://blog.csdn.net/weixin_42868529/article/details/104521847)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [SparkJoin实现原理](https://blog.csdn.net/jinjiating/article/details/127973403)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值