5-sparksql-join

本文详细探讨了Spark SQL中的join操作,包括笛卡尔积、SortMergeJoin、Shuffle Hash Join和Broadcast Hash Join。重点讲解了各种join类型的适用场景和条件,如连接键的排序、数据量大小对比等,帮助理解Spark SQL在处理大数据连接时的策略选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

join 操作 --TODO

join 笛卡尔积

代码流程

spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1) // 关闭 broadcast hit
//1 加载数据成DataFrame
val df = spark.read.json("examples/src/main/resources/people.json")
//2 调用dataframe的join操作
val rs = df.join(df)
/**
2.1  首先封装成Join类型的新逻辑计划 :Join(planWithBarrier, right.planWithBarrier, joinType = Inner, None),
2.1.1 当前df为left,被join的为right,逻辑计划类型为BinaryNode,包含2个节点
2.1.2 planWithBarrier = AnalysisBarrier(logicalPlan),给已经解析过的逻辑计划设置解析屏障,避免重复解析
2.2 封装成新的rs:DataFrame =》Dataset.ofRows(sparkSession, logicalPlan),dataframe通常流程,解析,优化,生成物理计划,生成可执行计划
2.2.1  logical 计划:Join 
2.2.2  sparkPlan 计划 : CartesianProductExec (根据join 类型匹配生成),left,right为FileSourceScanExec,执行拉取数据逻辑
2.2.3  executedPlan 计划 : CartesianProductExec,left,right为 wholeStageCodegenExec,已经插入全局生成代码节点
2.2.3.1 wholeStageCodegenExec的子节点为FileSourceScanExec
*/

// 3 执行count,触发job
rs.count()
/**
3.1 最终会执行:CartesianProductExec# doExecute
3.1.1 分别执行left,right的executre
    val leftResults = left.execute().asInstanceOf[RDD[UnsafeRow]]
        => MapPartitionRDD  =>prev = FileScanRDD
    val rightResults = right.execute().asInstanceOf[RDD[UnsafeRow]]
3.1.2 封装成UnsafeCartesianRDD,
    val pair = new UnsafeCartesianRDD(
    leftResults,
    rightResults,
    right.output.size,
    sqlContext.conf.cartesianProductExecBufferInMemoryThreshold,
    sqlContext.conf.cartesianProductExecBufferSpillThreshold)
3.1.2.1 spark.sql.cartesianProductExec.buffer.in.memory.threshold:保存在数组中内存的阈值,超过后刷新到disk,值太大容易引起OOM
默认4096
3.1.2.2 spark.sql.cartesianProductExec.buffer.spill.threshold:
值太小,引起频繁刷新disk
3.1.2.3 ExternalAppendOnlyUnsafeRowArray 
   专门给UnsafeRows的数组,带阈值刷新到disk

CartesianRDD#getPartitions 分区是如何处理的
    // 总分区数是 = 左表的分区数 * 右表的分区数
    val array = new Array[Partition](rdd1.partitions.length * rdd2.partitions.length)
    // 笛卡尔积分区
    for (s1 <- rdd1.partitions; s2 <- rdd2.partitions) {
          val idx = s1.index * numPartitionsInRdd2 + s2.index
          array(idx) = new CartesianPartition(idx, rdd1, rdd2, s1.index, s2.index)
        }
UnsafeCartesianRDD#compute 数据连接处理
    // 准备一个数组,内存中只保存inMemoryBufferThreshold行,超过的会分隔到文件
    val rowArray = new ExternalAppendOnlyUnsafeRowArray(inMemoryBufferThreshold, spillThreshold)
    // 获取当前笛卡尔积分区
    val partition = split.asInstanceOf[CartesianPartition]
    // 先计算rdd2也就是右表,实际计算过程就是加载数据的过程,添加到数组缓存 《== 加载右表分区的数据
    rdd2.iterator(partition.s2, context).foreach(rowArray.add)
    // 转化成迭代器
    def createIter(): Iterator[UnsafeRow] = rowArray.generateIterator()
    // 将当前分区rdd1的每行数据和rdd2分区的每行数据连接,组成新的行,分装成迭代器返回,注册了清理数组缓存的函数
    val resultIter =
      for (x <- rdd1.iterator(partition.s1, context);
           y <- createIter()) yield (x, y)
    CompletionIterator[(UnsafeRow, UnsafeRow), Iterator[(UnsafeRow, UnsafeRow)]](
      resultIter, rowArray.clear())

CartesianProductExec#doExecute 加载数据执行过滤
    初始化RDD,val pair = new UnsafeCartesianRDD()

*/

SortMergeJoin

  • 匹配条件
    • 匹配到有表的连接条件
      • case ExtractEquiJoinKeys(joinType, leftKeys, rightKeys, condition, left, right)
    • 连接键可排序
      • RowOrdering.isOrderable(leftKeys),
  • 默认使用,两个都是大表并且key可排序的情况
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1) // 关闭 broadcast hit
//1 加载数据成DataFrame
val df = spark.read.json("examples/src/main/resources/people.json")
val df2 = spark.read.json("examples/src/main/resources/people.json")
val rs = df.join(df2,df.col("age") === df2("age")) 
println(rs.count())
/**
生成对应的executePlan为SortMergeJoinExec
    同样有两个内存相关的配置:都保存在内存中
        spark.sql.sortMergeJoinExec.buffer.spill.threshold :默认是整数最大值,无上限
        sortMergeJoinExecBufferInMemoryThreshold:默认是整数最大值,无上限
SortMergeJoinScanner#findNextInnerJoinRows,匹配left,和right的key相同的数据,基本逻辑:
    1. 以leftIter为基准,获取一行数据,并通过leftProject获取leftKey
        if (streamedIter.advanceNext()) {
          streamedRow = streamedIter.getRow
          streamedRowKey = streamedKeyGenerator(streamedRow)
          true
        }
    2.  在类似的方式获取rightIter的一行数据,和rightKey
    3.  比较leftKey 和 rightKey大小 ,直到left或right没有数据
        -   comp = 0,调用bufferMatchingRows保存
        -   comp > 0 则执行 2 , 获取下一行right
        -   comp <0 则执行 1 ,获取下一行left
总结:
    1. 两个表都只遍历一次
    
SortMergeJoinExec#doExecute ==> 数据读取 ==》 shuffle => sort => 匹配合并
//在executedPlan阶段,left和right为InputAdapter,其中在上层会插入SortExec节点,所以需要先shuffle,
//left和right执行execute()方法后返回排序后的RDD
//zipPartitions将left,right的partition,一一组合成一个新的partition
left.execute().zipPartitions(right.execute()) { (leftIter, rightIter) =>
    //初始化排序器,根据key的类型,使用相对应类型的增序排序器,用来比较left和right的key
    val keyOrdering = newNaturalAscendingOrdering(leftKeys.map(_.dataType))
   // 根据不同的连接类型匹配相对于的处理
   joinType match {
        // 内连接方式 , inner 和 cross,生成一个能处理内连接的iterator
        case _: InnerLike =>
            new RowIterator {
              // 初始化SortMergeJoinScanner 实现sortmerge的工具类
              val smjScanner =new SortMergeJoinScanner(
              createLeftKeyGenerator(),// 是一个left 表的连接key的Projection,也就是age字段的投影,可以是多个字段,如果连接有多个字段
              createRightKeyGenerator(),// 同理
              keyOrdering,  // 排序器
              RowIterator.fromScala(leftIter), // 左表一个分区的数据,
              RowIterator.fromScala(rightIter), // 右表一个分区的数据
              inMemoryThreshold,
              spillThreshold
            )
            //匹配生成部分,有迭代器处理
            override def advanceNext(): Boolean = {
                // 通过SortMergeJoinScanner扫描匹配的行,并保存
                smjScanner.findNextInnerJoinRows()
            }
            override def getRow: InternalRow = resultProj(joinRow)
            }
        // 左外连接
        case LeftOuter =>
            // 调用findNextOuterJoinRows ,以左表为基准,找到匹配行返回,找不到null行代替GenericInternalRow
            new LeftOuterIterator
        // 右外,全外类似
        // LeftSemi
        case LeftSemi =>
            区别:getRow: InternalRow = currentLeftRow 返回结果只包含匹配的左边的行
        case LeftAnti =>

   }
}

*/


shuffle hash join

  • 匹配条件
    • !conf.preferSortMergeJoin (没有指定使用SortMerge) && canBuildRight(joinType) && canBuildLocalHashMap(right) (判断单个parition是否足够小,可以建立hash table) && muchSmaller(right, left) (判断一个表是否是比另一个表大小的3倍,只有足够小的表建立hash table )
      • spark.sql.join.preferSortMergeJoin 默认为true (默认不使用shuffle,使用sort)
      • plan.stats.sizeInBytes < conf.autoBroadcastJoinThreshold * conf.numShufflePartitions
      • a.stats.sizeInBytes * 3 <= b.stats.sizeInBytes
    • !RowOrdering.isOrderable(leftKeys) // 或者 连接键不能排序
  • 一个大表,一个小表,小表足够小的情况
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", 10) // 关闭 broadcast hit
spark.conf.set("spark.sql.join.preferSortMergeJoin","false")
//1 加载数据成DataFrame
val df = spark.read.json("examples/src/main/resources/people.json")
var df2 = spark.read.json("examples/src/main/resources/people.json")
df2 = df2.union(df2).union(df2)
val rs = df.join(df2,df.col("age") === df2("age")) 
println(rs.count())

/**
生成对应的executePlan为ShuffledHashJoinExec
    left,right为ShuffleExchangeExec
        根据key 做 hashPartition
    buildSide=BuildLeft,right为大表
// 大表为streamedPlan 也就是right, 小表为buildPlan 也就是left
// streamedPlan.execute() 与 buildPlan.execute()为 安装 key hash后的rdd
// zipPartitions 将两个rdd 一对一组合
streamedPlan.execute().zipPartitions(buildPlan.execute()) { (streamIter, buildIter) =>
      val hashed = buildHashedRelation(buildIter) // 根据小表的一个partition的数据以及key,创建一个HashedRelation ,相当与一个hash表
      // hashedRelation.get(joinKeys(srow)),直接根据大表的key匹配
      join(streamIter, hashed, numOutputRows, avgHashProbe)
    }


*/

broadcast hash join

  • 匹配条件
    • canBroadcastByHints , join时指定broadcast(df)
    • canBroadcastBySizes, 数据量小于autoBroadcastJoinThreshold
/**
生成对应的executePlan为BroadcastHashJoinExec
BroadcastHashJoinExec#doExecute
    val broadcastRelation = buildPlan.executeBroadcast[HashedRelation]() // 将小表广播出去
    // 遍历每个大表的partition,直接通过广播地址获取hash表,用来匹配key
    // 而 shuffle hash join的hash表为小表的一个partion
    streamedPlan.execute().mapPartitions { streamedIter =>
      val hashed = broadcastRelation.value.asReadOnlyCopy()
      TaskContext.get().taskMetrics().incPeakExecutionMemory(hashed.estimatedSize)
      join(streamedIter, hashed, numOutputRows, avgHashProbe)
    }


*/
参考
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值