[SPARK-58210][SQL][FOLLOWUP] Extend CombineAdjacentAggregation to partial merge - #57859
[SPARK-58210][SQL][FOLLOWUP] Extend CombineAdjacentAggregation to partial merge#57859cloud-fan wants to merge 4 commits into
Conversation
|
Thank you for extending this rule, @cloud-fan. The 1. Weakening the required distribution of the existing
Concretely, with 2. The
3. The combined Minor comments:
|
| } | ||
| } | ||
|
|
||
| test("Combine adjacent partial merge and final hash aggregates") { |
There was a problem hiding this comment.
Combine PartialMerge - Final to one FInal is sound good to me. But is there a valid end to end test ? I can not find a real world query can reach this pattern that all agg functions are PartialMerge and the adjacent agg functions are Final.
There was a problem hiding this comment.
Thanks for raising this. OSS does not currently produce a pure adjacent PartialMerge -> Final HashAggregate pair from a built-in query plan. The rule is intended to handle plans produced by physical-plan extensions that add an aggregation stage, and I updated the class documentation to say that explicitly.
The test now constructs a valid executable Partial -> PartialMerge -> Final pipeline, applies the rule to the upper pair, and compares the uncombined and combined execution results. This replaces the previous plan-shape-only coverage. I also added a separate end-to-end query test for the AQE rebalance/skew distribution regression.
|
Thanks for the detailed review. Addressed in 359d3dc:
I also updated the class documentation. The sort/object-hash paths continue retaining the final node distribution through For execution coverage, the PartialMerge test now builds a valid executable Partial -> PartialMerge -> Final pipeline and compares results before and after combining, in addition to its metadata and filter checks. OSS does not currently generate this pure adjacency from a built-in query; the documented use case is a physical-plan extension that introduces an extra aggregation stage. I attempted |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
Thank you, @cloud-fan. This extension looks correct to me — I verified the key correctness points:
- Buffer binding:
Final-mode aggregates bind buffers positionally frominitialInputBufferOffset, and the combined node takes the lower aggregate's offset while its child keeps the samegrouping + bufferslayout. Correct. - AQE safety: since this rule runs after
EnsureRequirements, keeping the distribution requirement on the combined node is what letsValidateRequirementsblockOptimizeSkewInRebalancePartitionsfrom splitting rebalance partitions (which would produce wrong results). The second test covers exactly this scenario. - Streaming: real streaming aggregation plans have
StateStoreSaveExecbetweenFinalandPartialMerge, so this pattern cannot match them. - Mixed-mode (distinct) aggregates are excluded by the
forallchecks, and filteredPartialMergepairs are conservatively rejected with test coverage.
A few minor comments (none blocking):
- The PR description says "preserve the lower aggregate's child distribution", but the code keeps the upper (final) aggregate's
requiredChildDistributionExpressions(and the tests assert that). The code is right; the description could be updated. The described "requires the lower aggregate output to exactly match the final aggregate inputs" check also doesn't exist explicitly — it holds implicitly from the parent-child pattern. requiredChildDistributionExpressions = finalAgg.requiredChildDistributionExpressionsinsidefinalAgg.copy(...)is a no-op. If the intent is to document that the upper node's requirement is kept, a comment would be clearer — as written it may read as a typo forpartialAgg.….- The
Completepath now takesisStreaming/numShufflePartitionsfrom the lower aggregate (previously the final's were kept). Practically equivalent for plans Spark produces, butrequiredChildDistributionis now computed from fields of two different nodes (exprs from final, the rest from partial) — a short comment on why would help future readers. - The
IllegalArgumentExceptionbranch incombineHashAggregatesis unreachable sincecombinedModeonly returnsComplete/Final; returning the newaggregateExpressionsdirectly fromcombinedModewould remove the second match entirely. - The scaladoc reads as if
PartialMerge+Finalcombining applies generally, but onlyHashAggregateExechandles it;SortAggregateExec/ObjectHashAggregateExecremainPartial-only. Worth a note in the doc.
On tests: the new path is covered by a hand-assembled plan (understandable, since Spark core never produces this shape), and the AQE test is effectively a regression test for the pre-existing Complete path — asserting the shuffle actually had skewed partitions would make it non-vacuous, but that can be a follow-up.
Thank you again for the follow-up, @cloud-fan!
|
Thanks, @dongjoon-hyun. Addressed these comments in 296a850:
The focused suite (10 tests) and |
|
the failed streaming test is unrelated, thanks for review, merging to master/4.x/4.3 |
…tial merge ### What changes were proposed in this pull request? This is a follow-up to #57363. It extends `CombineAdjacentAggregation` to combine adjacent `PartialMerge` and `Final` hash aggregates into one `Final` aggregate. This pattern can be produced by physical-plan extensions that add an aggregation stage; Spark's built-in query planning does not currently produce it. The combined aggregate preserves the upper `Final` aggregate's child distribution requirement. Because the lower aggregate is removed, the combined aggregate takes its streaming and shuffle-partition metadata from that lower aggregate and reads its original child. Filtered `Partial` aggregation remains supported, while a `PartialMerge` pair with filters is not combined. ### Why are the changes needed? `CombineAdjacentAggregation` currently handles only `Partial` followed by `Final`. A compatible adjacent `PartialMerge` and `Final` hash aggregate pair can also be collapsed safely, avoiding an unnecessary hash aggregation stage. The upper aggregate's distribution requirement must remain after the rule runs so that later AQE optimization does not split partitions that the combined aggregate requires to remain clustered. The lower aggregate's streaming and shuffle-partition metadata remain child-facing properties and therefore move to the combined aggregate when the lower node is removed. ### Does this PR introduce _any_ user-facing change? Yes. Spark may produce a single final hash aggregate instead of adjacent partial-merge and final hash aggregates when their grouping and logical lineage are compatible. Query results are unchanged. ### How was this patch tested? Added coverage to `CombineAdjacentAggregationSuite` for: - combining an executable `PartialMerge` and `Final` hash aggregate pipeline and comparing the uncombined and combined results; - preserving the upper distribution requirement and lower streaming, shuffle-partition, grouping, child, and buffer-offset metadata; - refusing to combine a filtered `PartialMerge` pair; - retaining the distribution requirement under AQE and verifying that shuffle partition specs are present but are not partial reducer specs. Ran: ``` build/sbt 'sql/testOnly org.apache.spark.sql.execution.CombineAdjacentAggregationSuite' sql/scalastyle ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Codex (GPT-5) Closes #57859 from cloud-fan/cloud-fan/consolidate-adjacent-aggregation. Authored-by: Wenchen Fan <wenchen@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit f88a554) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…tial merge ### What changes were proposed in this pull request? This is a follow-up to #57363. It extends `CombineAdjacentAggregation` to combine adjacent `PartialMerge` and `Final` hash aggregates into one `Final` aggregate. This pattern can be produced by physical-plan extensions that add an aggregation stage; Spark's built-in query planning does not currently produce it. The combined aggregate preserves the upper `Final` aggregate's child distribution requirement. Because the lower aggregate is removed, the combined aggregate takes its streaming and shuffle-partition metadata from that lower aggregate and reads its original child. Filtered `Partial` aggregation remains supported, while a `PartialMerge` pair with filters is not combined. ### Why are the changes needed? `CombineAdjacentAggregation` currently handles only `Partial` followed by `Final`. A compatible adjacent `PartialMerge` and `Final` hash aggregate pair can also be collapsed safely, avoiding an unnecessary hash aggregation stage. The upper aggregate's distribution requirement must remain after the rule runs so that later AQE optimization does not split partitions that the combined aggregate requires to remain clustered. The lower aggregate's streaming and shuffle-partition metadata remain child-facing properties and therefore move to the combined aggregate when the lower node is removed. ### Does this PR introduce _any_ user-facing change? Yes. Spark may produce a single final hash aggregate instead of adjacent partial-merge and final hash aggregates when their grouping and logical lineage are compatible. Query results are unchanged. ### How was this patch tested? Added coverage to `CombineAdjacentAggregationSuite` for: - combining an executable `PartialMerge` and `Final` hash aggregate pipeline and comparing the uncombined and combined results; - preserving the upper distribution requirement and lower streaming, shuffle-partition, grouping, child, and buffer-offset metadata; - refusing to combine a filtered `PartialMerge` pair; - retaining the distribution requirement under AQE and verifying that shuffle partition specs are present but are not partial reducer specs. Ran: ``` build/sbt 'sql/testOnly org.apache.spark.sql.execution.CombineAdjacentAggregationSuite' sql/scalastyle ``` ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Codex (GPT-5) Closes #57859 from cloud-fan/cloud-fan/consolidate-adjacent-aggregation. Authored-by: Wenchen Fan <wenchen@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit f88a554) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
What changes were proposed in this pull request?
This is a follow-up to #57363. It extends
CombineAdjacentAggregationto combine adjacentPartialMergeandFinalhash aggregates into oneFinalaggregate. This pattern can beproduced by physical-plan extensions that add an aggregation stage; Spark's built-in query
planning does not currently produce it.
The combined aggregate preserves the upper
Finalaggregate's child distribution requirement.Because the lower aggregate is removed, the combined aggregate takes its streaming and
shuffle-partition metadata from that lower aggregate and reads its original child. Filtered
Partialaggregation remains supported, while aPartialMergepair with filters is not combined.Why are the changes needed?
CombineAdjacentAggregationcurrently handles onlyPartialfollowed byFinal. A compatibleadjacent
PartialMergeandFinalhash aggregate pair can also be collapsed safely, avoiding anunnecessary hash aggregation stage.
The upper aggregate's distribution requirement must remain after the rule runs so that later AQE
optimization does not split partitions that the combined aggregate requires to remain clustered.
The lower aggregate's streaming and shuffle-partition metadata remain child-facing properties and
therefore move to the combined aggregate when the lower node is removed.
Does this PR introduce any user-facing change?
Yes. Spark may produce a single final hash aggregate instead of adjacent partial-merge and final
hash aggregates when their grouping and logical lineage are compatible. Query results are
unchanged.
How was this patch tested?
Added coverage to
CombineAdjacentAggregationSuitefor:PartialMergeandFinalhash aggregate pipeline and comparing theuncombined and combined results;
grouping, child, and buffer-offset metadata;
PartialMergepair;are present but are not partial reducer specs.
Ran:
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Codex (GPT-5)