[ 
https://issues.apache.org/jira/browse/SPARK-57928?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated SPARK-57928:
-----------------------------------
    Labels: pull-request-available  (was: )

> [SQL] Collapse redundant Partial+Final aggregate pair when shuffle is skipped
> -----------------------------------------------------------------------------
>
>                 Key: SPARK-57928
>                 URL: https://issues.apache.org/jira/browse/SPARK-57928
>             Project: Spark
>          Issue Type: Improvement
>          Components: Optimizer
>    Affects Versions: 4.3.0
>            Reporter: James Xu
>            Priority: Major
>              Labels: pull-request-available
>
> —
> *Problem:*
> Spark plans aggregations in two phases: a Partial aggregate that 
> pre-aggregates within each partition, followed by a shuffle and a Final 
> aggregate that merges across partitions. EnsureRequirements inserts the 
> shuffle only when the child's output partitioning does not already satisfy 
> ClusteredDistribution(groupingKeys). When the shuffle is skipped — for 
> example, because the aggregate sits directly on top of a SortMergeJoin keyed 
> on exactly the grouping columns — the Final aggregate receives data that is 
> already fully aggregated within each partition. The Final pass provides no 
> cross-partition merging, yet it still materialises intermediate buffers and 
> processes every row a second time.
> A representative pattern is an aggregation over a join where the join keys 
> match the GROUP BY columns:
> {code:java}
> SELECT t1.user_id, t1.region, SUM(t1.amount) AS total
> FROM events t1
> JOIN users t2 ON t1.user_id = t2.user_id AND t1.region = t2.region
> GROUP BY t1.user_id, t1.region{code}
> When the join is a SortMergeJoin on (user_id, region), the output is already 
> hash-partitioned on those keys. EnsureRequirements skips the shuffle before 
> the Final aggregate, but leaves both Partial and Final nodes in the plan. On 
> large inputs with high row counts per partition key, the redundant aggregate 
> pair causes severe disk spill and widespread fallback to sort-based 
> aggregation.
> —
> *Root Cause:*
> AggUtils.planAggregateWithoutDistinct unconditionally emits a Partial+Final 
> pair, expecting EnsureRequirements to insert an Exchange between them. When 
> EnsureRequirements instead finds that the child's partitioning already 
> satisfies ClusteredDistribution(groupingKeys) and skips the shuffle, no rule 
> subsequently detects that the Final node is now doing redundant work. The two 
> nodes remain in the physical plan and both execute in full.
> —
> *Solution:*
> Add a new physical rule, RemoveRedundantAggregates, that runs after 
> EnsureRequirements in both the standard preparations sequence and the AQE 
> queryStagePreparationRules. The rule pattern-matches a Final-mode aggregate 
> sitting directly over a Partial-mode aggregate of the same concrete type with 
> no Exchange between them, and replaces both with a single Complete-mode node. 
> The absence of an Exchange node between the two aggregates is structural 
> proof that no shuffle was inserted — if one had been, it would appear as the 
> direct child of the Final aggregate and the match would not fire.
> Complete mode drives the same updateExpressions path as Partial, evaluating 
> aggregate functions directly on raw input rows and emitting final results in 
> one pass. This is semantically equivalent to Partial→Final on the same 
> partition without an intermediate shuffle, but eliminates the buffer 
> materialisation between the two phases. The replacement node preserves the 
> same output attributes as the original Final aggregate, so all downstream 
> operators bind correctly.
> The rule applies to HashAggregateExec, ObjectHashAggregateExec, and 
> SortAggregateExec. It must run after EnsureRequirements; running it before 
> would collapse pairs that still need a shuffle.
> —
> *Expected Impact:*
> For queries that aggregate over a SortMergeJoin (or any operator providing 
> ClusteredDistribution) on the grouping keys, with large input sizes and low 
> group cardinality relative to input volume:
>  - Disk spill from the redundant Final aggregate pass is eliminated entirely, 
> since Complete mode processes raw input in a single pass with no intermediate 
> buffer materialisation.
>  - Tasks that previously fell back to sort-based aggregation due to spill 
> pressure are no longer subject to that pressure from the second pass.
>  - Overall stage wall-clock time is reduced proportionally to the elimination 
> of the redundant pass and its associated spill I/O.
> The optimisation applies to any query where a SortMergeJoin, bucketed scan, 
> or other operator providing ClusteredDistribution on the grouping keys sits 
> below a two-phase aggregate.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to