James Xu created SPARK-57928:
--------------------------------

             Summary: [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


---
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:

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

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