spark的shuffle hash join对Full Outer Join的支持

文章介绍了Spark3.1.0版本中ShuffleHashJoin(SHJ)对全外连接(fullouterjoin)的支持,相比于之前的版本,SHJ在处理大数据时能减少排序所需CPU和IO,特别是在streamside远大于buildside的情况下。文中还提到了与SortMergeJoin的性能对比和BroadcastHashJoin对fullouterjoin的限制。
摘要由CSDN通过智能技术生成

变迁历史

Spark3.1.0版本之前,shuffle hash join(SHJ)支持除了full outer join之外的类型,此时对于一个full outer join的等值join的sql,会使用smj,smj缺点就是在表很大的时候会增加排序所需的cpu和io,而SHJ可以排序所需的CPU和IO,特别是表比较大的时候。

而从3.1.0版本开始, Shuffle-Hash Join (SHJ) 支持所有的 join 类型(SPARK-32399),同时支持相应的 codegen execution(SPARK-32421)。

3.1.0版本怎么支持Full Outer Join

具体处理方式

  1. 通过哈希关系查找处理流侧的行,并通过以下方式标记构建侧( build side)的匹配行:
    1. 对于具有唯一键的连接,使用 BitSet 记录构建侧的匹配行(key index表示每一行)。
    2. 对于具有非唯一键的连接,使用 HashSet[Long] 记录构建侧的匹配行(key index+value index表示每一行)。

key index定义为BytesToBytesMap中longArray的index key

(is defined as the index into key addressing array longArray in BytesToBytesMap.)

value index 定义为相同键的值的 iterator index

  1. 通过迭代哈希关系处理构建侧的行,并过滤已经查找过的构建侧行(在  ShuffledHashJoinExec.fullOuterJoin 中完成)。

可以看到full outer shuffled hash join将迭代 两次 build side(一次用于构建hash map,另一次用于输出不匹配行)和迭代一次 stream side  

而full outer sort merge join需要两边都迭代两次,并且对大表进行排序可能会更加消耗CPU和IO。因此,当 stream side比build side大得多时,full outer shuffled hash join比full outer sort merge join更有效。

相关源码

在org.apache.spark.sql.execution.joins.ShuffledHashJoinExec中:

 private def fullOuterJoin(
      streamIter: Iterator[InternalRow],
      hashedRelation: HashedRelation,
      numOutputRows: SQLMetric): Iterator[InternalRow] = {
    val joinKeys = streamSideKeyGenerator()
    val joinRow = new JoinedRow
    val (joinRowWithStream, joinRowWithBuild) = {
      buildSide match {
        case BuildLeft => (joinRow.withRight _, joinRow.withLeft _)
        case BuildRight => (joinRow.withLeft _, joinRow.withRight _)
      }
    }
    val buildNullRow = new GenericInternalRow(buildOutput.length)
    val streamNullRow = new GenericInternalRow(streamedOutput.length)
    lazy val streamNullJoinRowWithBuild = {
      buildSide match {
        case BuildLeft =>
          joinRow.withRight(streamNullRow)
          joinRow.withLeft _
        case BuildRight =>
          joinRow.withLeft(streamNullRow)
          joinRow.withRight _
      }
    }

    val iter = if (hashedRelation.keyIsUnique) {
      fullOuterJoinWithUniqueKey(streamIter, hashedRelation, joinKeys, joinRowWithStream,
        joinRowWithBuild, streamNullJoinRowWithBuild, buildNullRow, streamNullRow)
    } else {
      fullOuterJoinWithNonUniqueKey(streamIter, hashedRelation, joinKeys, joinRowWithStream,
        joinRowWithBuild, streamNullJoinRowWithBuild, buildNullRow, streamNullRow)
    }

    val resultProj = UnsafeProjection.create(output, output)
    iter.map { r =>
      numOutputRows += 1
      resultProj(r)
    }
  }

BHJ和SMJ在full outer join上的性能对比

例如下面的查询,与full outer sort merge join相比,full outer shuffled hash join节省了 30% 的wall clock time 。注意,当构建端(build side)很大时,SHJ 可能会导致 OOM,因为构建 hashmap 是内存密集型的。

"wall clock time"是指实际时间,也称为挂钟时间或墙上时间。这是指从一个事件的开始到结束所经过的实际时间,包括所有时间延迟和等待时间。

Wall clock time包括了计算机系统中的各种时间消耗,比如CPU执行时间、I/O操作等等。它可以用来评估一个程序或操作的整体执行时间,包括了所有可能的延迟和等待时间,反映了程序在实际环境中的整体执行效率。

与之相对的,CPU时间只计算CPU实际执行代码的时间,不包括等待I/O操作、线程切换等操作所消耗的时间。 Wall clock time更能真实地反映一个任务的总体耗时,因为它包含了所有可能的时间开销,而不仅仅是CPU的执行时间。

def shuffleHashJoin(): Unit = {
    val N: Long = 4 << 22
    withSQLConf(
      SQLConf.SHUFFLE_PARTITIONS.key -> "2",
      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "20000000") {
      codegenBenchmark("shuffle hash join", N) {
        val df1 = spark.range(N).selectExpr(s"cast(id as string) as k1")
        val df2 = spark.range(N / 10).selectExpr(s"cast(id * 10 as string) as k2")
        val df = df1.join(df2, col("k1") === col("k2"), "full_outer")
        df.noop()
    }
  }
}
Running benchmark: shuffle hash join
  Running case: shuffle hash join off
  Stopped after 2 iterations, 16602 ms
  Running case: shuffle hash join on
  Stopped after 5 iterations, 31911 ms

Java HotSpot(TM) 64-Bit Server VM 1.8.0_181-b13 on Mac OS X 10.15.4
Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
shuffle hash join:                        Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
shuffle hash join off                              7900           8301         567          2.1         470.9       1.0X
shuffle hash join on                               6250           6382          95          2.7         372.5       1.3X

3.1.0版本之前为什么不支持full outer join

3.1.0之前的版本中,之所以SHJ不支持full outer join,主要是因为在执行shuffle hash join时,Spark会将两个输入数据集按照指定的连接键进行分区,然后在每个分区节点上的数据单独执行单机hash join算法。

在执行join操作时,Spark会将相同连接键的数据进行合并。如果使用hash join来实现全连接,那么可能会导致一些连接键只出现在一个数据集中而没有出现在另一个数据集中,这样就无法准确地将两个数据集进行合并。

例如,本来是table_a={id:1,id:2,id:3,id:4},table_b={id:1,id:2,id:3,id:4},则table_a和table_b中id=1/id=4的数据发到节点1,id=2的数据发到节点2,id=3的数据发到节点3,那样在各自节点上进行table_a full outer join table_b on table_a.id=table_b.id就会分别得到:

节点1:{1,1},{4,4}  -- 缺少了{id=2, null}{id=3, null},因为根本该节点上根本不知道有id=2,3

节点2:{2,2}  -- 缺少了{id=1, null}{id=3, null}{id=4, null},因为根本该节点上根本不知道有id=1,3,4

节点3:{3,3} -- 缺少了{id=1, null}{id=2, null}{id=4, null},因为根本该节点上根本不知道有id=1,2,4

Broadcast hash join支持full outer join吗?

目前还不支持。

从源码可以看到,支持full outer join的有BNLJ,SHJ,SMJ

Cartesian product join适合没有指定连接条件,支持等值和不等值 Join,只支持内连接(因为笛卡尔积没有啥实际意义,只有使用on语句形成内连接才有意义),下面代码就写死了inner

跟上面SHJ的例子一样,如果BHJ支持full outer join,则也会遇到以下情况,小表只能跟各个节点上拥有的个别id集合进行join,因而不能支持full outer join

大表table_a={id:1,id:2,id:3,id:4},广播的小表table_b={id:1,id:4},则table_b中id=1/id=4的数据发到各个节点上,其中节点1中table_a的数据是{id:1,id:4},节点2中table_a的数据是{id:2},节点3中table_a的数据是{id:3},那样在各自节点上进行table_a full outer join table_b on table_a.id=table_b.id就会分别得到:

节点1:{1,1},{4,4}  -- 缺少了{id=2, null}{id=3, null},因为根本该节点上根本不知道有id=2,3

节点2:{1,null},{2,null},{4,null}  -- 缺少了{id=3, null},因为根本该节点上根本不知道有id=3

节点3:{1,null},{3,null},{4,null}  -- 缺少了{id=2, null},因为根本该节点上根本不知道有id=2

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spark SQL底层join实现有三种方式:broadcast hash joinshuffle hash join和sort merge join。其中broadcast hash join适用于小数据量的join操作,可以将一个小的表复制到所有的Executor上,然后和其他的表进行join操作;shuffle hash join适用于大数据量的join操作,可以通过Hash函数将数据分区,然后通过网络进行数据交换,再将分区的数据进行join操作;sort merge join适用于两个表都有序的情况下进行join操作,可以将两个表按照join字段进行排序,然后按顺序进行join操作。 ### 回答2: Spark SQL是Apache Spark的一个组件,提供了一个基于SQL的编程接口,支持分布式数据处理。其底层实现了三种Join操作,分别是Broadcast Hash JoinShuffle Hash Join和Sort Merge Join。 1.Broadcast Hash Join是在一个表比较小的情况下使用的Join算法。具体流程是,将小表广播给集群中的每个Executor,然后对大表进行Join操作。该算法需要把小表数据拷贝到内存中,可能会导致OOM异常,因此需要在实际使用中谨慎选择。 2.Shuffle Hash Join适用于两个表都比较大的情况下。具体流程是,在两个表都进行Shuffle操作,将Join Key相同的数据放到同一个分区。然后将每个分区的数据交给一个Executor进行Join操作。该算法的缺点是Shuffle会增加网络开销以及I/O操作的负担,因此需要注意调整参数大小。 3.Sort Merge Join适用于两个表都比较大且Join Key有序的情况下。该算法的流程是,在两个表进行Sort操作,将Join Key相同的数据放到同一个分区。然后将每个分区的数据交给一个Executor进行Join操作。该算法的优点是Join Key有序,不需要进行Shuffle操作,因此可以避免Shuffle操作的网络损耗和I/O操作的负担。 综上所述,Spark SQL底层Join的实现使用了三种Join算法,Broadcast Hash Join适用于小表JoinShuffle Hash Join适用于两个表都比较大的情况下,Sort Merge Join适用于两个表都比较大且Join Key有序的情况下。我们在使用时需要根据实际情况选择合适的Join算法,避免OOM和网络开销等问题。 ### 回答3: Spark SQL是一种针对结构化和半结构化数据处理的高性能分布式计算框架。在使用Spark SQL进行数据处理时,很多情况下需要对数据进行join操作。Spark SQL的join操作有三种实现方式,分别是Broadcast Hash JoinShuffle Hash Join和Sort Merge Join。 Broadcast Hash Join是一种在内存中进行的join操作,当一个表的大小可以运用内存进行分布并广播到所有节点时,可以采用Broadcast Hash Join。这种join的实现方式是先在driver端对较小的表进行哈希操作,然后将其哈希表广播到所有worker节点上,同时另外一个较大的表再进行哈希操作,将其切分成多个小表,然后将每个小表发到worker上去跟广播的哈希表进行join,最终将所有小表的join结果汇总即可。Broadcast Hash Join的优点是可以减少数据的运输,缩短查询时间。缺点是只能适用于对于较小表以及对于等值join场景,而且如果数据量过大,广播查询也会耗费大量的网络资源,无法解决内存不足的问题。 Shuffle Hash Join是一种使用网络进行数据传输的join操作方式。当一个表的大小无法运用内存进行分布并广播到所有节点时,可以采用Shuffle Hash Join。它的实现方式是将两张表的数据都进行哈希分区,将相同哈希值的分区数据放到同一个节点上,然后在每个节点进行join操作。不同节点之间进行数据交换,需要通过Shuffle进行数据传送。Shuffle Hash Join适用于较大的表,可以支持任何join,但效率较低,因为需要网络传输。 Sort Merge Join是一种对两个表进行排序后再进行join操作的方式。Sort Merge Join的实现方式是对两张表按照join key 进行排序,然后进行合并操作。当然这个过程支持Inner、Full、Left、Right的多种Join操作。Sort Merge Join的优点是适用于超大表的join操作,缺点是需要对两张表进行排序操作,代价较高,且仅适用于等值join,而且排序操作必须保证内存能够承受。 在Spark SQL的JOIN操作中,Broadcast Hash Join适用于大表关联小表的情况;Shuffle Hash Join是对大表关联大表,或者把数据分散在集群节点上的表进行JOIN操作的时候的方法;Sort Merge Join通常用于数据量较大而无法全部载入内存的情况下进行JOIN操作。不同的JOIN操作应根据数据量以及具体的情况来选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值