[CALCITE-7116] Optimize queries with GROUPING SETS by converting them into equivalent UNION ALL of GROUP BY operations
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateGroupingSetsToUnionRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateGroupingSetsToUnionRule.java
new file mode 100644
index 0000000..ea3d9b2
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateGroupingSetsToUnionRule.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.rules;
+
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Values;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.ImmutableList;
+
+import org.immutables.value.Value;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Rule that converts a {@link org.apache.calcite.rel.core.Aggregate} with
+ * {@code GROUPING SETS} into a {@code UNION ALL} of simpler aggregates.
+ *
+ * <p>Example transformation:
+ * <pre>{@code
+ * SELECT a, b, c FROM t GROUP BY GROUPING SETS ((a,b), (a,c))
+ * }</pre>
+ *
+ * <p>Transformed to:
+ *
+ * <pre>{@code
+ * SELECT a, b, NULL AS c FROM t GROUP BY a, b
+ * UNION ALL
+ * SELECT a, NULL AS b, c FROM t GROUP BY a, c
+ * }</pre>
+ */
+@Value.Enclosing
+public class AggregateGroupingSetsToUnionRule
+ extends RelRule<AggregateGroupingSetsToUnionRule.Config>
+ implements SubstitutionRule {
+
+ /** Creates an AggregateGroupingSetsToUnionRule. */
+ protected AggregateGroupingSetsToUnionRule(Config config) {
+ super(config);
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public void onMatch(RelOptRuleCall call) {
+ final Aggregate aggregate = call.rel(0);
+
+ if (Aggregate.isSimple(aggregate)) {
+ return;
+ }
+
+ final RelBuilder relBuilder = call.builder();
+ final RexBuilder rexBuilder = aggregate.getCluster().getRexBuilder();
+
+ final RelNode input = aggregate.getInput();
+ final RelDataType rowType = aggregate.getRowType();
+ final ImmutableBitSet oriGroupSet = aggregate.getGroupSet();
+ final List<RelNode> unionInputs = new ArrayList<>();
+
+ for (ImmutableBitSet subGroupSet : aggregate.getGroupSets()) {
+ relBuilder.push(input);
+ final List<RexNode> subProjects = new ArrayList<>();
+
+ // Process aggregate group set
+ RelDataType subAggregateType =
+ Aggregate.deriveRowType(relBuilder.getTypeFactory(), relBuilder.peek().getRowType(),
+ false, subGroupSet, ImmutableList.of(subGroupSet), ImmutableList.of());
+ for (int i = 0; i < oriGroupSet.cardinality(); i++) {
+ int groupKey = oriGroupSet.nth(i);
+ if (subGroupSet.get(groupKey)) {
+ subProjects.add(
+ RexInputRef.of(
+ subGroupSet.indexOf(groupKey),
+ subAggregateType));
+ } else {
+ // If the groupKey is not in the GroupSet, use null as a placeholder.
+ subProjects.add(rexBuilder.makeNullLiteral(relBuilder.field(groupKey).getType()));
+ }
+ }
+
+ // Process aggregate calls
+ List<AggregateCall> subAggCalls = new ArrayList<>();
+ for (AggregateCall aggCall : aggregate.getAggCallList()) {
+ switch (aggCall.getAggregation().getKind()) {
+ case GROUPING:
+ int groupingValue = evaluateGroupingFunction(subGroupSet, aggCall.getArgList());
+ subProjects.add(
+ rexBuilder.makeLiteral(groupingValue, aggCall.getType(), true));
+ break;
+ case GROUP_ID:
+ // GROUP_ID is removed during RelNode conversion, no handling needed here.
+ return;
+ case GROUPING_ID:
+ // The GROUPING_ID aggregate function has been marked as deprecated
+ // and is no longer supported.
+ return;
+ default:
+ subProjects.add(
+ new RexInputRef(
+ subGroupSet.cardinality() + subAggCalls.size(),
+ aggCall.getType()));
+ subAggCalls.add(aggCall);
+ break;
+ }
+ }
+
+ relBuilder.aggregate(relBuilder.groupKey(subGroupSet), subAggCalls)
+ .project(subProjects, rowType.getFieldNames());
+
+ unionInputs.add(relBuilder.build());
+ }
+
+ relBuilder.pushAll(unionInputs)
+ .union(true, unionInputs.size());
+
+ call.transformTo(relBuilder.build());
+ }
+
+ private static int evaluateGroupingFunction(ImmutableBitSet groupSet, List<Integer> argIndices) {
+ final int argCount = argIndices.size();
+ if (argCount >= Integer.SIZE) {
+ throw new IllegalArgumentException(
+ "Too many grouping keys. Maximum is " + (Integer.SIZE - 1) + " for grouping functions.");
+ }
+
+ int result = 0;
+ for (int k = 0; k < argCount; k++) {
+ int index = argIndices.get(argCount - 1 - k);
+ if (!groupSet.get(index)) {
+ result |= 1 << k;
+ }
+ }
+ return result;
+ }
+
+
+ /** Rule configuration. */
+ @Value.Immutable
+ public interface Config extends RelRule.Config {
+ Config DEFAULT = ImmutableAggregateGroupingSetsToUnionRule.Config.of()
+ .withOperandFor(Aggregate.class, Values.class);
+
+ @Override default AggregateGroupingSetsToUnionRule toRule() {
+ return new AggregateGroupingSetsToUnionRule(this);
+ }
+
+ /** Defines an operand tree for the given classes. */
+ default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+ Class<? extends Values> valuesClass) {
+ return withOperandSupplier(b0 ->
+ b0.operand(aggregateClass)
+ .predicate(aggregate -> aggregate.getGroupType() != Aggregate.Group.SIMPLE)
+ .anyInputs())
+ .as(Config.class);
+ }
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
index 436a77a..938df46 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
@@ -940,4 +940,9 @@ private CoreRules() {}
/** Rule that remove duplicate {@link Sort} keys. */
public static final SortRemoveDuplicateKeysRule SORT_REMOVE_DUPLICATE_KEYS =
SortRemoveDuplicateKeysRule.Config.DEFAULT.toRule();
+
+ /** Rule that converts GROUPING SETS of {@link Aggregate}
+ * into equivalent {@link Union} ALL of GROUP BY operations. */
+ public static final AggregateGroupingSetsToUnionRule AGGREGATE_GROUPING_SETS_TO_UNION =
+ AggregateGroupingSetsToUnionRule.Config.DEFAULT.toRule();
}
diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 5f846ed..01a68fe 100644
--- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -34,6 +34,7 @@
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.rules.AggregateGroupingSetsToUnionRule;
import org.apache.calcite.rel.rules.AggregateJoinTransposeRule;
import org.apache.calcite.rel.rules.AggregateProjectMergeRule;
import org.apache.calcite.rel.rules.CoreRules;
@@ -10626,6 +10627,48 @@ private void checkLiteral2(String expression, String expected) {
relFn(relFn).ok(expected);
}
+ /** Test case of
+ * <a href="https://issues.apache.org/jira/browse/CALCITE-7116">[CALCITE-7116]
+ * Optimize queries with GROUPING SETS by converting them
+ * into equivalent UNION ALL of GROUP BY operations</a>. */
+ @Test void testAggregateGroupingSetsToUnionRule() {
+ final String query = "SELECT deptno, job, sal, SUM(comm),\n"
+ + " GROUPING(deptno) AS deptno_flag,\n"
+ + " GROUPING(job) AS job_flag,\n"
+ + " GROUPING(sal) AS sal_flag,\n"
+ + " GROUP_ID() AS group_id\n"
+ + "FROM emp\n"
+ + "GROUP BY GROUPING SETS ((deptno, job), (deptno, sal), (deptno, job))";
+ final String expected = "SELECT \"DEPTNO\", \"JOB\", \"SAL\", \"EXPR$3\", \"DEPTNO_FLAG\","
+ + " \"JOB_FLAG\", \"SAL_FLAG\", 0 AS \"GROUP_ID\"\nFROM (SELECT \"DEPTNO\", \"JOB\","
+ + " CAST(NULL AS DECIMAL(7, 2)) AS \"SAL\", SUM(\"COMM\") AS \"EXPR$3\","
+ + " 0 AS \"DEPTNO_FLAG\", 0 AS \"JOB_FLAG\", 1 AS \"SAL_FLAG\"\n"
+ + "FROM \"SCOTT\".\"EMP\"\n"
+ + "GROUP BY \"DEPTNO\", \"JOB\"\n"
+ + "UNION ALL\n"
+ + "SELECT \"DEPTNO\", CAST(NULL AS VARCHAR(9) CHARACTER SET \"ISO-8859-1\") AS \"JOB\","
+ + " \"SAL\", SUM(\"COMM\") AS \"EXPR$3\", 0 AS \"DEPTNO_FLAG\", 1 AS \"JOB_FLAG\","
+ + " 0 AS \"SAL_FLAG\"\n"
+ + "FROM \"SCOTT\".\"EMP\"\nGROUP BY \"DEPTNO\", \"SAL\") AS \"t5\"\n"
+ + "UNION ALL\nSELECT \"DEPTNO\", \"JOB\", CAST(NULL AS DECIMAL(7, 2)) AS \"SAL\","
+ + " SUM(\"COMM\"), GROUPING(\"DEPTNO\") AS \"DEPTNO_FLAG\","
+ + " GROUPING(\"JOB\") AS \"JOB_FLAG\", 1 AS \"SAL_FLAG\", 1 AS \"GROUP_ID\"\n"
+ + "FROM \"SCOTT\".\"EMP\"\n"
+ + "GROUP BY \"DEPTNO\", \"JOB\"";
+
+ HepProgramBuilder builder = new HepProgramBuilder();
+ builder.addRuleClass(AggregateGroupingSetsToUnionRule.class);
+ HepPlanner hepPlanner = new HepPlanner(builder.build());
+ RuleSet rules =
+ RuleSets.ofList(CoreRules.AGGREGATE_GROUPING_SETS_TO_UNION);
+
+ sql(query)
+ .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+ .withCalcite()
+ .optimize(rules, hepPlanner)
+ .ok(expected);
+ }
+
/** Fluid interface to run tests. */
static class Sql {
private final CalciteAssert.SchemaSpec schemaSpec;
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 32626d9..67320f6 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -11195,4 +11195,40 @@ private void checkLoptOptimizeJoinRule(LoptOptimizeJoinRule rule) {
.withRule(CoreRules.SORT_REMOVE_DUPLICATE_KEYS)
.check();
}
+
+ /** Test case of
+ * <a href="https://issues.apache.org/jira/browse/CALCITE-7116">[CALCITE-7116]
+ * Optimize queries with GROUPING SETS by converting them
+ * into equivalent UNION ALL of GROUP BY operations</a>. */
+ @Test void testAggregateGroupingSetsToUnionRule() {
+ final String sql = "SELECT deptno, job, sal, SUM(comm)\n"
+ + "FROM emp\n"
+ + "GROUP BY GROUPING SETS ((deptno, job), (deptno, sal))";
+ sql(sql)
+ .withRule(CoreRules.AGGREGATE_GROUPING_SETS_TO_UNION)
+ .check();
+ }
+
+ /** Test case of
+ * <a href="https://issues.apache.org/jira/browse/CALCITE-7116">[CALCITE-7116]
+ * Optimize queries with GROUPING SETS by converting them
+ * into equivalent UNION ALL of GROUP BY operations</a>. */
+ @Test void testAggregateGroupingSetsToUnionRuleGroupingGrouId() {
+ final String sql = "SELECT deptno, job, sal, SUM(comm),\n"
+ + " GROUPING(deptno) AS deptno_flag,\n"
+ + " GROUPING(job) AS job_flag,\n"
+ + " GROUPING(sal) AS sal_flag,\n"
+ + " GROUPING(deptno, sal) AS deptno_sal_flag,\n"
+ + " GROUP_ID() AS group_id\n"
+ + "FROM emp\n"
+ + "GROUP BY GROUPING SETS ((deptno, job),(deptno, job, sal),(deptno, sal))";
+
+ HepProgram program = new HepProgramBuilder()
+ .addRuleInstance(CoreRules.AGGREGATE_GROUPING_SETS_TO_UNION)
+ .build();
+
+ sql(sql)
+ .withProgram(program)
+ .check();
+ }
}
diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 708e612..a404c11 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -344,6 +344,71 @@
]]>
</Resource>
</TestCase>
+ <TestCase name="testAggregateGroupingSetsToUnionRule">
+ <Resource name="sql">
+ <![CDATA[SELECT deptno, job, sal, SUM(comm)
+FROM emp
+GROUP BY GROUPING SETS ((deptno, job), (deptno, sal))]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalAggregate(group=[{0, 1, 2}], groups=[[{0, 1}, {0, 2}]], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalUnion(all=[true])
+ LogicalProject(DEPTNO=[$0], JOB=[$1], SAL=[null:INTEGER], EXPR$3=[$2])
+ LogicalAggregate(group=[{0, 1}], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(DEPTNO=[$0], JOB=[null:VARCHAR(10)], SAL=[$1], EXPR$3=[$2])
+ LogicalAggregate(group=[{0, 2}], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testAggregateGroupingSetsToUnionRuleGroupingGrouId">
+ <Resource name="sql">
+ <![CDATA[SELECT deptno, job, sal, SUM(comm),
+ GROUPING(deptno) AS deptno_flag,
+ GROUPING(job) AS job_flag,
+ GROUPING(sal) AS sal_flag,
+ GROUPING(deptno, sal) AS deptno_sal_flag,
+ GROUP_ID() AS group_id
+FROM emp
+GROUP BY GROUPING SETS ((deptno, job),(deptno, job, sal),(deptno, sal))]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], JOB=[$1], SAL=[$2], EXPR$3=[$3], DEPTNO_FLAG=[$4], JOB_FLAG=[$5], SAL_FLAG=[$6], DEPTNO_SAL_FLAG=[$7], GROUP_ID=[0:BIGINT])
+ LogicalAggregate(group=[{0, 1, 2}], groups=[[{0, 1, 2}, {0, 1}, {0, 2}]], EXPR$3=[SUM($3)], DEPTNO_FLAG=[GROUPING($0)], JOB_FLAG=[GROUPING($1)], SAL_FLAG=[GROUPING($2)], DEPTNO_SAL_FLAG=[GROUPING($0, $2)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], JOB=[$1], SAL=[$2], EXPR$3=[$3], DEPTNO_FLAG=[$4], JOB_FLAG=[$5], SAL_FLAG=[$6], DEPTNO_SAL_FLAG=[$7], GROUP_ID=[0:BIGINT])
+ LogicalUnion(all=[true])
+ LogicalProject(DEPTNO=[$0], JOB=[$1], SAL=[$2], EXPR$3=[$3], DEPTNO_FLAG=[0:BIGINT], JOB_FLAG=[0:BIGINT], SAL_FLAG=[0:BIGINT], DEPTNO_SAL_FLAG=[0:BIGINT])
+ LogicalAggregate(group=[{0, 1, 2}], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(DEPTNO=[$0], JOB=[$1], SAL=[null:INTEGER], EXPR$3=[$2], DEPTNO_FLAG=[0:BIGINT], JOB_FLAG=[0:BIGINT], SAL_FLAG=[1:BIGINT], DEPTNO_SAL_FLAG=[1:BIGINT])
+ LogicalAggregate(group=[{0, 1}], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(DEPTNO=[$0], JOB=[null:VARCHAR(10)], SAL=[$1], EXPR$3=[$2], DEPTNO_FLAG=[0:BIGINT], JOB_FLAG=[1:BIGINT], SAL_FLAG=[0:BIGINT], DEPTNO_SAL_FLAG=[0:BIGINT])
+ LogicalAggregate(group=[{0, 2}], EXPR$3=[SUM($3)])
+ LogicalProject(DEPTNO=[$7], JOB=[$2], SAL=[$5], COMM=[$6])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
<TestCase name="testAggregateJoinRemove1">
<Resource name="sql">
<![CDATA[select distinct e.deptno from sales.emp e
diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq
index 3852b29..f563e0b 100644
--- a/core/src/test/resources/sql/agg.iq
+++ b/core/src/test/resources/sql/agg.iq
@@ -4012,4 +4012,118 @@
EnumerableTableScan(table=[[scott, EMP]])
!plan
+# [CALCITE-7116] Optimize queries with GROUPING SETS by converting them into equivalent UNION ALL of GROUP BY operations
+# Before SQL
+SELECT deptno, job, sal, SUM(comm),
+ GROUPING(deptno) AS deptno_flag,
+ GROUPING(job) AS job_flag,
+ GROUPING(sal) AS sal_flag,
+ GROUP_ID() AS group_id
+FROM emp
+GROUP BY GROUPING SETS ((deptno, job), (deptno, sal), (deptno, job));
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+| DEPTNO | JOB | SAL | EXPR$3 | DEPTNO_FLAG | JOB_FLAG | SAL_FLAG | GROUP_ID |
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+| 10 | CLERK | | | 0 | 0 | 1 | 0 |
+| 10 | CLERK | | | 0 | 0 | 1 | 1 |
+| 10 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 10 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 10 | PRESIDENT | | | 0 | 0 | 1 | 0 |
+| 10 | PRESIDENT | | | 0 | 0 | 1 | 1 |
+| 10 | | 1300.00 | | 0 | 1 | 0 | 0 |
+| 10 | | 2450.00 | | 0 | 1 | 0 | 0 |
+| 10 | | 5000.00 | | 0 | 1 | 0 | 0 |
+| 20 | ANALYST | | | 0 | 0 | 1 | 0 |
+| 20 | ANALYST | | | 0 | 0 | 1 | 1 |
+| 20 | CLERK | | | 0 | 0 | 1 | 0 |
+| 20 | CLERK | | | 0 | 0 | 1 | 1 |
+| 20 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 20 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 20 | | 1100.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 2975.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 3000.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 800.00 | | 0 | 1 | 0 | 0 |
+| 30 | CLERK | | | 0 | 0 | 1 | 0 |
+| 30 | CLERK | | | 0 | 0 | 1 | 1 |
+| 30 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 30 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 30 | SALESMAN | | 2200.00 | 0 | 0 | 1 | 0 |
+| 30 | SALESMAN | | 2200.00 | 0 | 0 | 1 | 1 |
+| 30 | | 1250.00 | 1900.00 | 0 | 1 | 0 | 0 |
+| 30 | | 1500.00 | 0.00 | 0 | 1 | 0 | 0 |
+| 30 | | 1600.00 | 300.00 | 0 | 1 | 0 | 0 |
+| 30 | | 2850.00 | | 0 | 1 | 0 | 0 |
+| 30 | | 950.00 | | 0 | 1 | 0 | 0 |
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+(30 rows)
+
+!ok
+
+# After SQL
+# This SQL comes from RelToSqlConverterTest#testAggregateGroupingSetsToUnionRule(). This result is the same as the result of Before SQL.
+SELECT DEPTNO, JOB, SAL, EXPR$3, DEPTNO_FLAG, JOB_FLAG, SAL_FLAG, 0 AS GROUP_ID
+FROM (
+ SELECT DEPTNO, JOB, CAST(NULL AS DECIMAL(7, 2)) AS SAL,
+ SUM(COMM) AS EXPR$3,
+ 0 AS DEPTNO_FLAG,
+ 0 AS JOB_FLAG,
+ 1 AS SAL_FLAG
+ FROM EMP
+ GROUP BY DEPTNO, JOB
+ UNION ALL
+ SELECT DEPTNO, CAST(NULL AS VARCHAR(9)) AS JOB, SAL,
+ SUM(COMM) AS EXPR$3,
+ 0 AS DEPTNO_FLAG,
+ 1 AS JOB_FLAG,
+ 0 AS SAL_FLAG
+ FROM EMP
+ GROUP BY DEPTNO, SAL
+) t5
+UNION ALL
+SELECT DEPTNO, JOB, CAST(NULL AS DECIMAL(7, 2)) AS SAL,
+ SUM(COMM),
+ GROUPING(DEPTNO) AS DEPTNO_FLAG,
+ GROUPING(JOB) AS JOB_FLAG,
+ 1 AS SAL_FLAG,
+ 1 AS GROUP_ID
+FROM EMP
+GROUP BY DEPTNO, JOB;
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+| DEPTNO | JOB | SAL | EXPR$3 | DEPTNO_FLAG | JOB_FLAG | SAL_FLAG | GROUP_ID |
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+| 10 | CLERK | | | 0 | 0 | 1 | 0 |
+| 10 | CLERK | | | 0 | 0 | 1 | 1 |
+| 10 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 10 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 10 | PRESIDENT | | | 0 | 0 | 1 | 0 |
+| 10 | PRESIDENT | | | 0 | 0 | 1 | 1 |
+| 10 | | 1300.00 | | 0 | 1 | 0 | 0 |
+| 10 | | 2450.00 | | 0 | 1 | 0 | 0 |
+| 10 | | 5000.00 | | 0 | 1 | 0 | 0 |
+| 20 | ANALYST | | | 0 | 0 | 1 | 0 |
+| 20 | ANALYST | | | 0 | 0 | 1 | 1 |
+| 20 | CLERK | | | 0 | 0 | 1 | 0 |
+| 20 | CLERK | | | 0 | 0 | 1 | 1 |
+| 20 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 20 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 20 | | 1100.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 2975.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 3000.00 | | 0 | 1 | 0 | 0 |
+| 20 | | 800.00 | | 0 | 1 | 0 | 0 |
+| 30 | CLERK | | | 0 | 0 | 1 | 0 |
+| 30 | CLERK | | | 0 | 0 | 1 | 1 |
+| 30 | MANAGER | | | 0 | 0 | 1 | 0 |
+| 30 | MANAGER | | | 0 | 0 | 1 | 1 |
+| 30 | SALESMAN | | 2200.00 | 0 | 0 | 1 | 0 |
+| 30 | SALESMAN | | 2200.00 | 0 | 0 | 1 | 1 |
+| 30 | | 1250.00 | 1900.00 | 0 | 1 | 0 | 0 |
+| 30 | | 1500.00 | 0.00 | 0 | 1 | 0 | 0 |
+| 30 | | 1600.00 | 300.00 | 0 | 1 | 0 | 0 |
+| 30 | | 2850.00 | | 0 | 1 | 0 | 0 |
+| 30 | | 950.00 | | 0 | 1 | 0 | 0 |
++--------+-----------+---------+---------+-------------+----------+----------+----------+
+(30 rows)
+
+!ok
+
# End agg.iq