Skip to content

Commit e861b0d

Browse files
c21cloud-fan
authored andcommitted
[SPARK-36794][SQL] Ignore duplicated join keys when building relation for SEMI/ANTI shuffled hash join
### What changes were proposed in this pull request? This is a re-proposal of #34034. Ignore duplicated join keys when building hash relation for LEFT SEMI / LEFT ANTI shuffled hash join. The original PR was also covering broadcast hash join, and caused regression for broadcast shuffle exchange reuse in #34034 (comment) . So here we do not covert broadcast hash join. Shuffled hash join do not have the shuffle reuse problem. Make the deduplication work with broadcast exchange is kind of non-trivial for me now, because: For two `BroadcastExchangeExec`, one with `ignoreDuplicatedKey` being true (called `exchange_A`), and the other with `ignoreDuplicatedKey` being false (called `exchange_B`): * If we decide these two `BroadcastExchangeExec`s are not equal, then we have regression that we reuse fewer exchange after the change, as #34034 (comment) . * If we decide these two `BroadcastExchangeExec`s are equal, then we might have correctness issue when `exchange_B` reuses result of `exchange_A`. Also these two `BroadcastExchangeExec`s should not be equal as they already have different results in underlying hash relations. I still see it's valuable to have the optimization for shuffled hash join only. So leave the optimization for broadcast hash join as a followup as it needs more thoughts. ### Why are the changes needed? Help reduce the hash table size of join for LEFT SEMI and LEFT ANTI, reduce OOM possibility of shuffled hash join. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? Added unit test in `JoinSuite.scala`. Closes #34247 from c21/shj-fix. Authored-by: Cheng Su <chengsu@fb.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
1 parent 92caa75 commit e861b0d

3 files changed

Lines changed: 70 additions & 12 deletions

File tree

sql/core/src/main/scala/org/apache/spark/sql/execution/joins/HashedRelation.scala

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,20 @@ private[execution] object HashedRelation {
126126
/**
127127
* Create a HashedRelation from an Iterator of InternalRow.
128128
*
129-
* @param allowsNullKey Allow NULL keys in HashedRelation.
130-
* This is used for full outer join in `ShuffledHashJoinExec` only.
129+
* @param allowsNullKey Allow NULL keys in HashedRelation.
130+
* This is used for full outer join in `ShuffledHashJoinExec` only.
131+
* @param ignoresDuplicatedKey Ignore rows with duplicated keys in HashedRelation.
132+
* This is only used for semi and anti join without join condition in
133+
* `ShuffledHashJoinExec` only.
131134
*/
132135
def apply(
133136
input: Iterator[InternalRow],
134137
key: Seq[Expression],
135138
sizeEstimate: Int = 64,
136139
taskMemoryManager: TaskMemoryManager = null,
137140
isNullAware: Boolean = false,
138-
allowsNullKey: Boolean = false): HashedRelation = {
141+
allowsNullKey: Boolean = false,
142+
ignoresDuplicatedKey: Boolean = false): HashedRelation = {
139143
val mm = Option(taskMemoryManager).getOrElse {
140144
new TaskMemoryManager(
141145
new UnifiedMemoryManager(
@@ -152,7 +156,8 @@ private[execution] object HashedRelation {
152156
// NOTE: LongHashedRelation does not support NULL keys.
153157
LongHashedRelation(input, key, sizeEstimate, mm, isNullAware)
154158
} else {
155-
UnsafeHashedRelation(input, key, sizeEstimate, mm, isNullAware, allowsNullKey)
159+
UnsafeHashedRelation(input, key, sizeEstimate, mm, isNullAware, allowsNullKey,
160+
ignoresDuplicatedKey)
156161
}
157162
}
158163
}
@@ -450,7 +455,8 @@ private[joins] object UnsafeHashedRelation {
450455
sizeEstimate: Int,
451456
taskMemoryManager: TaskMemoryManager,
452457
isNullAware: Boolean = false,
453-
allowsNullKey: Boolean = false): HashedRelation = {
458+
allowsNullKey: Boolean = false,
459+
ignoresDuplicatedKey: Boolean = false): HashedRelation = {
454460
require(!(isNullAware && allowsNullKey),
455461
"isNullAware and allowsNullKey cannot be enabled at same time")
456462

@@ -471,12 +477,14 @@ private[joins] object UnsafeHashedRelation {
471477
val key = keyGenerator(row)
472478
if (!key.anyNull || allowsNullKey) {
473479
val loc = binaryMap.lookup(key.getBaseObject, key.getBaseOffset, key.getSizeInBytes)
474-
val success = loc.append(
475-
key.getBaseObject, key.getBaseOffset, key.getSizeInBytes,
476-
row.getBaseObject, row.getBaseOffset, row.getSizeInBytes)
477-
if (!success) {
478-
binaryMap.free()
479-
throw QueryExecutionErrors.cannotAcquireMemoryToBuildUnsafeHashedRelationError()
480+
if (!(ignoresDuplicatedKey && loc.isDefined)) {
481+
val success = loc.append(
482+
key.getBaseObject, key.getBaseOffset, key.getSizeInBytes,
483+
row.getBaseObject, row.getBaseOffset, row.getSizeInBytes)
484+
if (!success) {
485+
binaryMap.free()
486+
throw QueryExecutionErrors.cannotAcquireMemoryToBuildUnsafeHashedRelationError()
487+
}
480488
}
481489
} else if (isNullAware) {
482490
binaryMap.free()

sql/core/src/main/scala/org/apache/spark/sql/execution/joins/ShuffledHashJoinExec.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ case class ShuffledHashJoinExec(
5959
case _ => super.outputOrdering
6060
}
6161

62+
// Exposed for testing
63+
@transient lazy val ignoreDuplicatedKey = joinType match {
64+
case LeftExistence(_) =>
65+
// For building hash relation, ignore duplicated rows with same join keys if:
66+
// 1. Join condition is empty, or
67+
// 2. Join condition only references streamed attributes and build join keys.
68+
val streamedOutputAndBuildKeys = AttributeSet(streamedOutput ++ buildKeys)
69+
condition.forall(_.references.subsetOf(streamedOutputAndBuildKeys))
70+
case _ => false
71+
}
72+
6273
/**
6374
* This is called by generated Java class, should be public.
6475
*/
@@ -72,7 +83,8 @@ case class ShuffledHashJoinExec(
7283
buildBoundKeys,
7384
taskMemoryManager = context.taskMemoryManager(),
7485
// Full outer join needs support for NULL key in HashedRelation.
75-
allowsNullKey = joinType == FullOuter)
86+
allowsNullKey = joinType == FullOuter,
87+
ignoresDuplicatedKey = ignoreDuplicatedKey)
7688
buildTime += NANOSECONDS.toMillis(System.nanoTime() - start)
7789
buildDataSize += relation.estimatedSize
7890
// This relation is usually used until the end of task.

sql/core/src/test/scala/org/apache/spark/sql/JoinSuite.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,4 +1402,42 @@ class JoinSuite extends QueryTest with SharedSparkSession with AdaptiveSparkPlan
14021402
assertJoin(sql, classOf[ShuffledHashJoinExec])
14031403
}
14041404
}
1405+
1406+
test("SPARK-36794: Ignore duplicated key when building relation for semi/anti hash join") {
1407+
withTable("t1", "t2") {
1408+
spark.range(10).map(i => (i.toString, i + 1)).toDF("c1", "c2").write.saveAsTable("t1")
1409+
spark.range(10).map(i => ((i % 5).toString, i % 3)).toDF("c1", "c2").write.saveAsTable("t2")
1410+
1411+
val semiJoinQueries = Seq(
1412+
// No join condition, ignore duplicated key.
1413+
(s"SELECT /*+ SHUFFLE_HASH(t2) */ t1.c1 FROM t1 LEFT SEMI JOIN t2 ON t1.c1 = t2.c1",
1414+
true),
1415+
// Have join condition on build join key only, ignore duplicated key.
1416+
(s"""
1417+
|SELECT /*+ SHUFFLE_HASH(t2) */ t1.c1 FROM t1 LEFT SEMI JOIN t2
1418+
|ON t1.c1 = t2.c1 AND CAST(t1.c2 * 2 AS STRING) != t2.c1
1419+
""".stripMargin,
1420+
true),
1421+
// Have join condition on other build attribute beside join key, do not ignore
1422+
// duplicated key.
1423+
(s"""
1424+
|SELECT /*+ SHUFFLE_HASH(t2) */ t1.c1 FROM t1 LEFT SEMI JOIN t2
1425+
|ON t1.c1 = t2.c1 AND t1.c2 * 100 != t2.c2
1426+
""".stripMargin,
1427+
false)
1428+
)
1429+
semiJoinQueries.foreach {
1430+
case (query, ignoreDuplicatedKey) =>
1431+
val semiJoinDF = sql(query)
1432+
val antiJoinDF = sql(query.replaceAll("SEMI", "ANTI"))
1433+
checkAnswer(semiJoinDF, Seq(Row("0"), Row("1"), Row("2"), Row("3"), Row("4")))
1434+
checkAnswer(antiJoinDF, Seq(Row("5"), Row("6"), Row("7"), Row("8"), Row("9")))
1435+
Seq(semiJoinDF, antiJoinDF).foreach { df =>
1436+
assert(collect(df.queryExecution.executedPlan) {
1437+
case j: ShuffledHashJoinExec if j.ignoreDuplicatedKey == ignoreDuplicatedKey => true
1438+
}.size == 1)
1439+
}
1440+
}
1441+
}
1442+
}
14051443
}

0 commit comments

Comments
 (0)