[SPARK-58549][SQL] Preserve key-grouped partitioning and ordering across a DSv2 scan merge - #57753
Conversation
…oss a DSv2 scan merge Follow-up to SPARK-40259. Instead of declining a DSv2 scan merge whenever either input reports key-grouped partitioning or ordering, allow the merge and re-derive the merged scan's own report, declining only if that would degrade what the inputs reported. - Drop the kGP/ordering conjuncts from the mergeable gate. - At the leaf, combine the two inputs' reports (remapped into the merged relation's attribute space) into the single report the merge must preserve: kGP must be equal, ordering is the stronger of the two. If the inputs are incompatible (differing non-empty kGP, or neither ordering satisfies the other) no rebuilt scan could keep both not-worse, so decline right there -- before rebuilding -- unless the matching config accepts degrading that dimension. - After rebuildScan, run V2ScanPartitioningAndOrdering on the single merged scan node to re-derive its partitioning/ordering, then check it against the combined required report (mergeDegradesReporting): decline if the merged scan's kGP does not match, or its ordering does not satisfy, what was required (carried through DSv2DeferredScan for the deferred build). Gated per dimension by two new configs, default false: spark.sql.optimizer.mergeSubplans.dsv2ScanMerge.allowKeyGroupedPartitioningDegradation and ...allowOrderingDegradation. With defaults it is a pure improvement (merge when not worse, decline on degradation). - Tests: default-decline-on-degradation, default-decline-on-incompatible-inputs, and config-allows-degradation in MergeSubplansSuite; a preserves-kGP end-to-end test in DSv2PlanMergingSuite with a new reporting SCAN_MERGING fixture.
|
cc @LuciferYang |
|
I’ll take a look at it tomorrow. |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Although I understand your intention, for new test cases, SPARK-58549 test prefix should be used according to this PR's JIRA instead of the previous one, @peter-toth .
| @@ -169,4 +172,39 @@ class DSv2PlanMergingSuite extends QueryTest with SharedSparkSession | |||
| } | |||
| } | |||
| } | |||
|
|
|||
| test("SPARK-40259: a scan merge preserves the sources' reported key-grouped partitioning") { | |||
There was a problem hiding this comment.
| test("SPARK-40259: a scan merge preserves the sources' reported key-grouped partitioning") { | |
| test("SPARK-58549: a scan merge preserves the sources' reported key-grouped partitioning") { |
| @@ -2618,6 +2619,109 @@ class MergeSubplansSuite extends PlanTest { | |||
| assertDeclines(s => s.copy(ordering = Some(Seq(SortOrder(s.output.head, Ascending))))) | |||
| } | |||
|
|
|||
| test("SPARK-40259: do not merge DSv2 scans reporting incompatible kGP/ordering") { | |||
There was a problem hiding this comment.
| test("SPARK-40259: do not merge DSv2 scans reporting incompatible kGP/ordering") { | |
| test("SPARK-58549: do not merge DSv2 scans reporting incompatible kGP/ordering") { |
| assertDeclines(s => s.copy(ordering = Some(Seq(SortOrder(s.output.head, Ascending))))) | ||
| } | ||
|
|
||
| test("SPARK-40259: merge DSv2 scans reporting kGP/ordering when the degradation config allows") { |
There was a problem hiding this comment.
| test("SPARK-40259: merge DSv2 scans reporting kGP/ordering when the degradation config allows") { | |
| test("SPARK-58549: merge DSv2 scans reporting kGP/ordering when the degradation config allows") { |
| SQLConf.MERGE_SUBPLANS_DSV2_ALLOW_ORDERING_DEGRADATION.key) | ||
| } | ||
|
|
||
| test("SPARK-40259: enforce the required report on the deferred under-Filter scan build") { |
There was a problem hiding this comment.
| test("SPARK-40259: enforce the required report on the deferred under-Filter scan build") { | |
| test("SPARK-58549: enforce the required report on the deferred under-Filter scan build") { |
| // stronger, which satisfies both). None from combine* means the inputs are INCOMPATIBLE -- no | ||
| // rebuilt scan could keep both not-worse -- so decline HERE, before rebuilding, unless the | ||
| // matching config accepts degrading that dimension. | ||
| val requiredKeyGroupedPartitioning = combineRequiredKeyGroupedPartitioning( |
There was a problem hiding this comment.
requiredKeyGroupedPartitioning / requiredOrdering seem to carry a double meaning here. These Options use .isEmpty to mean the inputs' reports are incompatible, while the same names are reused for the Seq parameters of tryBuildMergedDSv2Scan / mergeDegradesReporting, where empty means no requirement. Renaming the Options to combined* (and deriving the expected* Seqs from them) makes the early-decline condition read correctly at the use site. Maybe
requiredKeyGroupedPartitioning->combinedKeyGroupedPartitioningrequiredOrdering->combinedOrdering
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1 for the logic. LGTM with two minor issues in the above.
|
Thanks @dongjoon-hyun! Both applied — the |
What changes were proposed in this pull request?
Follow-up to SPARK-40259 (subquery plan merge for DataSource V2 scans).
Today the DSv2 scan merge declines whenever either input scan reports key-grouped partitioning or ordering. The rebuilt merged scan carries neither on its own, so fusing the two scans could drop a partitioning/ordering the original plan relied on and force an extra shuffle or sort. Declining is safe but leaves the merge unused for any partitioned/ordered source.
This PR lets the merge proceed and re-derives the merged scan's own report instead of declining up front:
keyGroupedPartitioning/orderingconjuncts from themergeablegate inPlanMerger.tryMergeScanRelations.combineRequiredKeyGroupedPartitioning);SortOrder.orderingSatisfies(combineRequiredOrdering).If the inputs are incompatible (differing non-empty kGP, or neither ordering satisfies the other), no rebuilt scan can keep both not-worse, so the merge is declined right there -- before rebuilding -- saving a scan rebuild.
V2ScanRelationPushDown.rebuildScan, re-derive the merged scan's partitioning/ordering by runningV2ScanPartitioningAndOrderingon the single merged scan node, then check it against the combined required report (mergeDegradesReporting): decline if the merged scan's kGP does not match, or its ordering does not satisfy, what was required. The required report is carried throughDSv2DeferredScanfor the deferred (under-Filter) build.Two new opt-in configs gate accepting a degradation instead of declining (both default
false), so with the defaults this is a pure improvement -- merge when not worse, decline on loss:spark.sql.optimizer.mergeSubplans.dsv2ScanMerge.allowKeyGroupedPartitioningDegradationspark.sql.optimizer.mergeSubplans.dsv2ScanMerge.allowOrderingDegradationOnly sources that declare the
SCAN_MERGINGtable capability are affected.Why are the changes needed?
The parent feature (SPARK-40259) conservatively declines a merge whenever an input reports key-grouped partitioning or ordering, to avoid forcing a shuffle/sort that the original plan avoided. That is correct but overly broad: when the rebuilt merged scan re-derives the same (or a stronger) report, fusing the scans loses nothing and still removes a duplicate scan -- which matters for partitioned/ordered sources, e.g. an SPJ join inside the merged subplan. This PR keeps the safety (decline on real degradation) while capturing the merge when it is genuinely not worse.
Does this PR introduce any user-facing change?
No. The two new configs are opt-in and default to
false, and no built-in source declaresSCAN_MERGING, so there is no behavior change for existing sources.How was this patch tested?
New and existing unit tests, all under
sql/core:MergeSubplansSuite:allow...Degradation=true;Filter) build path enforces the required report too: decline by default, merge with the config on;DSv2PlanMergingSuite: an end-to-end test that a scan merge preserves the sources' reported key-grouped partitioning, using a newSCAN_MERGINGfixture (InMemoryScanMergingReportingCatalog/InMemoryScanMergingReportingTable) that keeps its reported partitioning (noNonReportingScanwrapper), an identity-partitioned table underspark.sql.sources.v2.bucketing.enabled=true, and two scalar subqueries reading the partition column.build/sbt 'sql/testOnly *MergeSubplansSuite *DSv2PlanMergingSuite *PlanMergingSuite'-- 95 tests pass.dev/lint-scalaclean.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)