-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-53618][SQL] Turn a FULL JOIN with a false condition into a UNION ALL #58506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,8 +28,8 @@ import org.apache.spark.sql.catalyst.TableIdentifier | |
| import org.apache.spark.sql.catalyst.analysis.UnresolvedRelation | ||
| import org.apache.spark.sql.catalyst.expressions.{Ascending, GenericRow, SortOrder} | ||
| import org.apache.spark.sql.catalyst.optimizer.{BuildLeft, BuildRight, JoinSelectionHelper} | ||
| import org.apache.spark.sql.catalyst.plans.logical.{Filter, HintInfo, Join, JoinHint, NO_BROADCAST_AND_REPLICATION} | ||
| import org.apache.spark.sql.execution.{BinaryExecNode, FilterExec, ProjectExec, SortExec, SparkPlan, WholeStageCodegenExec} | ||
| import org.apache.spark.sql.catalyst.plans.logical.{Filter, HintInfo, Join, JoinHint, NO_BROADCAST_AND_REPLICATION, Union} | ||
| import org.apache.spark.sql.execution.{BinaryExecNode, FilterExec, ProjectExec, SortExec, SparkPlan, UnionExec, WholeStageCodegenExec} | ||
| import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper | ||
| import org.apache.spark.sql.execution.exchange.{ShuffleExchangeExec, ShuffleExchangeLike} | ||
| import org.apache.spark.sql.execution.joins._ | ||
|
|
@@ -1838,6 +1838,29 @@ class JoinSuite extends SharedSparkSession with AdaptiveSparkPlanHelper | |
| cached.unpersist() | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-53618: full outer join with a false condition is rewritten to a union") { | ||
| val df = sql( | ||
| """ | ||
| |SELECT t1.id AS a, t2.id AS b | ||
| |FROM range(0, 2) t1 FULL OUTER JOIN range(10, 12) t2 ON 1 = 0 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. range().id is non-nullable; a full outer join must produce nullable columns. checkAnswer does not verify schema. Something like assert(df.schema.fields.forall(_.nullable)) would catch a Union.mergeChildOutputs / padding regression.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| |""".stripMargin) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, ON 1 = 0 is rewritten late. The early PropagateEmptyRelation batch runs before ConstantFolding, so ON false rewrites early and ON 1 = 0 waits until the later LocalRelation batch (after join reorder). Fine for a single MERGE join; only relevant if this full outer sits in a larger CBO join graph.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true, but I don't think we can change anything about that, right? Seems like it doesn't have a practical impact at the moment. |
||
|
|
||
| val optimized = df.queryExecution.optimizedPlan | ||
| assert(!optimized.exists(_.isInstanceOf[Join])) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test asserts the logical shape, not the physical win. A cheap extra check on executedPlan (UnionExec present, BroadcastNestedLoopJoinExec absent) would match that claim. Catalyst already covers the Union shape.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| assert(optimized.exists(_.isInstanceOf[Union])) | ||
|
|
||
| // `range` produces a non-nullable column, but a full outer join makes both sides | ||
| // nullable and the union has to keep it that way | ||
| assert(df.schema.fields.forall(_.nullable)) | ||
|
|
||
| checkAnswer(df, Row(0, null) :: Row(1, null) :: Row(null, 10) :: Row(null, 11) :: Nil) | ||
|
|
||
| // the point of the rewrite: the nested loop join and its broadcast are gone | ||
| val executed = df.queryExecution.executedPlan | ||
| assert(find(executed)(_.isInstanceOf[UnionExec]).isDefined) | ||
| assert(find(executed)(_.isInstanceOf[BroadcastNestedLoopJoinExec]).isEmpty) | ||
| } | ||
| } | ||
|
|
||
| class ThreadLeakInSortMergeJoinSuite | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description is about MERGE ... ON 1 = 0 with WHEN NOT MATCHED / WHEN NOT MATCHED BY SOURCE. However, tests only cover a standalone FULL OUTER JOIN. A DSv2 MergeIntoTable test (or even a logical-plan assertion that MergeRows.child is a Union) would lock in the case users cannot rewrite by hand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added another test for MERGE case