Skip to content

Commit aa8798c

Browse files
committed
[SQL] Rewrite UNION ALL of aggregates as ROLLUP
Add an optimizer rule, RewriteUnionAggregateAsRollup, that detects a UNION ALL of N aggregates whose GROUP BY clauses form a complete prefix-shrinking hierarchy ([c1..cN], [c1..cN-1], ..., [c1], []) and rewrites it into a single Aggregate with ROLLUP. Machine-generated SQL (BI tools, hand-translated queries) commonly emits this hand-expanded shape; collapsing it removes the redundant repeated scans and aggregations. Two patterns are recognized: - Inner-aggregate fold (CTE pattern): the branches fold a shared inner aggregate -- sum-of-sum, max-of-max, min-of-min, or sum-of-count. The rewrite rolls up OVER the inner aggregate, so the inner level's decimal width and overflow behavior are reproduced exactly. - Source-aggregate (direct pattern): each branch independently aggregates the same source, so any deterministic aggregate is allowed (SUM, MAX, MIN, COUNT, AVG, COUNT(DISTINCT), COLLECT_SET, FILTERed), provided every branch's per-position measure signature is identical. Correctness is preserved by reconstructing every rollup level from a single reference branch and binding to the Union output by position, with conservative guards that REJECT (never mis-apply) any branch the rewrite cannot reproduce exactly: per-position measure identity (matched by inner-output name, stable across CTE-copy ExprIds and coercion casts), grouping-key passthrough names, per-position role layout, eval-mode/timezone/FILTER/DISTINCT signatures, and value-changing projections above a branch aggregate. The grand-total (GROUP BY ()) level is unioned back verbatim so empty-input semantics (a global aggregate emits one row, a grouped ROLLUP zero) are preserved. Gated by spark.sql.optimizer.unionAggregateToRollup.enabled (internal, default true). Adds catalyst unit tests and end-to-end equivalence tests (each firing query is checked rule-on == rule-off) and regenerates the affected TPCDS v2_7 golden files (q5a, q14a, q18a, q67a, q77a, q80a).
1 parent cf2a6f5 commit aa8798c

31 files changed

Lines changed: 9356 additions & 7342 deletions

File tree

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ abstract class Optimizer(catalogManager: CatalogManager)
224224
ReplaceIntersectWithSemiJoin,
225225
ReplaceExceptWithFilter,
226226
ReplaceExceptWithAntiJoin,
227-
ReplaceDistinctWithAggregate),
227+
ReplaceDistinctWithAggregate,
228+
RewriteUnionAggregateAsRollup),
228229
Batch("Aggregate", fixedPoint,
229230
RemoveLiteralFromGroupExpressions,
230231
RemoveRepetitionFromGroupExpressions),

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteUnionAggregateAsRollup.scala

Lines changed: 1599 additions & 0 deletions
Large diffs are not rendered by default.

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/rules/RuleIdCollection.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ object RuleIdCollection {
183183
"org.apache.spark.sql.catalyst.optimizer.RewriteNearestByJoin" ::
184184
"org.apache.spark.sql.catalyst.optimizer.RewriteExceptAll" ::
185185
"org.apache.spark.sql.catalyst.optimizer.RewriteIntersectAll" ::
186+
"org.apache.spark.sql.catalyst.optimizer.RewriteUnionAggregateAsRollup" ::
186187
"org.apache.spark.sql.catalyst.optimizer.SimplifyBinaryComparison" ::
187188
"org.apache.spark.sql.catalyst.optimizer.SimplifyCaseConversionExpressions" ::
188189
"org.apache.spark.sql.catalyst.optimizer.SimplifyCasts" ::

sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,18 @@ object SQLConf {
595595
"for using switch statements in InSet must be non-negative and less than or equal to 600")
596596
.createWithDefault(400)
597597

598+
val UNION_AGGREGATE_TO_ROLLUP_ENABLED =
599+
buildConf("spark.sql.optimizer.unionAggregateToRollup.enabled")
600+
.internal()
601+
.doc("When true, the optimizer rewrites a UNION ALL of N aggregates whose GROUP BYs " +
602+
"form a strict prefix-shrinking hierarchy into a single Aggregate with ROLLUP. " +
603+
"Handles both the inner-aggregate fold pattern (sum-of-sum, max-of-max, etc.) and " +
604+
"the source-aggregate pattern (each branch independently aggregates the same source).")
605+
.version("4.3.0")
606+
.withBindingPolicy(ConfigBindingPolicy.SESSION)
607+
.booleanConf
608+
.createWithDefault(true)
609+
598610
private val VALID_LOG_LEVELS: Array[String] = Level.values.map(_.toString)
599611

600612
val PLAN_CHANGE_LOG_LEVEL = buildConf("spark.sql.planChangeLog.level")

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteUnionAggregateAsRollupSuite.scala

Lines changed: 1715 additions & 0 deletions
Large diffs are not rendered by default.

sql/core/src/test/resources/tpcds-plan-stability/approved-plans-v2_7/q14a.sf100/explain.txt

Lines changed: 362 additions & 627 deletions
Large diffs are not rendered by default.

sql/core/src/test/resources/tpcds-plan-stability/approved-plans-v2_7/q14a.sf100/simplified.txt

Lines changed: 236 additions & 314 deletions
Large diffs are not rendered by default.

sql/core/src/test/resources/tpcds-plan-stability/approved-plans-v2_7/q14a/explain.txt

Lines changed: 356 additions & 621 deletions
Large diffs are not rendered by default.

sql/core/src/test/resources/tpcds-plan-stability/approved-plans-v2_7/q14a/simplified.txt

Lines changed: 216 additions & 294 deletions
Large diffs are not rendered by default.

sql/core/src/test/resources/tpcds-plan-stability/approved-plans-v2_7/q18a.sf100/explain.txt

Lines changed: 241 additions & 624 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)