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主要包含以下两步:
- 参与join的两张表通过join key重分区,这即是shuffle过程,目的就是把相同join key数据发送到同一分区进行分区内join
- 对于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主要包括三个步骤:
- Shuffle阶段:两张大表通过join key进行分区
- Sort阶段:每个分区内数据进行排序
- 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提示, 按以下顺序:
- Broadcast Hint: 如果支持此种join, 则使用broadcast hash join
- Sort merge hint: 如果join列已排序, 则使用 sort merge join
- Shuffle hash hint: 如果支持此种join, 则使用shuffle hash join
- Shuffle replicate NL hint: 如果是内连接, 则使用 Cartesian product
如果无join hints, 依次检查以下规则:
- 如果join类型可使用broad cast hash join,并且一张表可被广播,即大小小于spark.sql.autoBroadcastJoinThreshold value(默认10MB), 则使用
broadcast hash join - 如果spark.sql.join.preferSortMergeJoin=false,并且一张表小到可以构建为hash map则使用shuffle hash join
- 如果join列可排序,则使用sort merge join
- 如果是内连接,则使用笛卡尔连接
- 如果可能发生OOM或者无可选择的执行策略,则使用broadcast nested loop join
非等值连接
有join提示, 按以下顺序:
- broadcast hint: 使用broadcast nested loop join
- shuffle replicate NL hint: 如果是内连接, 使用笛卡尔连接
无Join提示, 则按以下顺序:
- 一个表足够小可广播,使用 broadcast nested loop join
- 如果是内连接,使用笛卡尔连接
- 如果可能发生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策略相关代码。希望这篇文章能帮到你。