When producing sales reports, operational analyses, or market surveys, you often need a single query that returns both the “subtotals at the detail dimension” and the “totals across different dimension combinations” along with the “grand total.” Stitching together multiple GROUP BY queries with UNION ALL not only makes the SQL verbose but also scans the base table multiple times, which is inefficient.
Doris provides three syntactic extensions to the GROUP BY clause for multi-dimensional aggregation. They produce summary results at multiple levels in a single SQL statement, and are semantically equivalent to using UNION ALL to join multiple aggregate queries:
| Syntax | Applicable scenario | Output summary combinations |
|---|---|---|
| ROLLUP | Aggregate level by level along dimensions with a natural hierarchy, such as time, geography, or category | Aggregate progressively along the specified column order, from the finest granularity up to the grand total |
| CUBE | Multiple independent dimensions that need full cross-dimensional analysis | All combinations of all dimension subsets |
| GROUPING SETS | Only a few specific dimension combinations are of interest, avoiding the cost of a full CUBE | The grouping sets that the user explicitly specifies |
This article introduces these three syntaxes in the order of “scenario -> syntax -> example,” and explains how the companion GROUPING and GROUPING_ID functions identify subtotal rows and distinguish between two kinds of NULL values.
ROLLUP is suitable for scenarios that aggregate hierarchical dimensions level by level. It aggregates along the specified column order, summarizing data progressively from the finest granularity up to the highest level. For example:
ROLLUP(year, month, day)ROLLUP(country, province, city)For sales data, you can use ROLLUP to aggregate by region and time, producing the monthly sales for each region, the total sales for each region, and the overall total sales.
SELECT … GROUP BY ROLLUP(grouping_column_reference_list)
The following query analyzes sales by year and month:
SELECT YEAR(d_date), MONTH(d_date), SUM(ss_net_paid) AS total_sum FROM store_sales, date_dim d1 WHERE d1.d_date_sk = ss_sold_date_sk AND YEAR(d_date) IN (2001, 2002) AND MONTH(d_date) IN (1, 2, 3) GROUP BY ROLLUP(YEAR(d_date), MONTH(d_date)) ORDER BY YEAR(d_date), MONTH(d_date);
The query aggregates by time level by level, computing the monthly sales subtotal for each year, the annual sales subtotal for each year, and the overall sales grand total. The query result is as follows:
+--------------+---------------+-------------+ | YEAR(d_date) | MONTH(d_date) | total_sum | +--------------+---------------+-------------+ | NULL | NULL | 54262669.17 | | 2001 | NULL | 26640320.46 | | 2001 | 1 | 9982165.83 | | 2001 | 2 | 8454915.34 | | 2001 | 3 | 8203239.29 | | 2002 | NULL | 27622348.71 | | 2002 | 1 | 11260654.35 | | 2002 | 2 | 7722750.61 | | 2002 | 3 | 8638943.75 | +--------------+---------------+-------------+ 9 rows in set (0.08 sec)
CUBE is best suited for queries that involve multiple independent dimension columns rather than columns that represent different levels of a single dimension. A common use case is summarizing all combinations of month, region, and product. These are three mutually independent dimensions, and analyzing every possible subtotal combination is very common.
By contrast, cross-tabulating year, month, and day produces many unnecessary values, because the time dimension itself has a natural hierarchy. In most analyses, subtotals such as “profit by month-day” are not needed, and relatively few users ask “what is the total sales on the 16th of every month across the year.” Hierarchical dimensions like this are better handled with ROLLUP.
SELECT … GROUP BY CUBE(grouping_column_reference_list)
SELECT YEAR(d_date), i_category, ca_state, SUM(ss_net_paid) AS total_sum FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ("Books", "Electronics") AND YEAR(d_date) IN (1998, 1999) AND ca_state IN ("LA", "AK") GROUP BY CUBE(YEAR(d_date), i_category, ca_state) ORDER BY YEAR(d_date), i_category, ca_state;
The query result computes:
+--------------+-------------+----------+------------+ | YEAR(d_date) | i_category | ca_state | total_sum | +--------------+-------------+----------+------------+ | NULL | NULL | NULL | 8690374.60 | | NULL | NULL | AK | 2675198.33 | | NULL | NULL | LA | 6015176.27 | | NULL | Books | NULL | 4238177.69 | | NULL | Books | AK | 1310791.36 | | NULL | Books | LA | 2927386.33 | | NULL | Electronics | NULL | 4452196.91 | | NULL | Electronics | AK | 1364406.97 | | NULL | Electronics | LA | 3087789.94 | | 1998 | NULL | NULL | 4369656.14 | | 1998 | NULL | AK | 1402539.19 | | 1998 | NULL | LA | 2967116.95 | | 1998 | Books | NULL | 2213703.82 | | 1998 | Books | AK | 719911.29 | | 1998 | Books | LA | 1493792.53 | | 1998 | Electronics | NULL | 2155952.32 | | 1998 | Electronics | AK | 682627.90 | | 1998 | Electronics | LA | 1473324.42 | | 1999 | NULL | NULL | 4320718.46 | | 1999 | NULL | AK | 1272659.14 | | 1999 | NULL | LA | 3048059.32 | | 1999 | Books | NULL | 2024473.87 | | 1999 | Books | AK | 590880.07 | | 1999 | Books | LA | 1433593.80 | | 1999 | Electronics | NULL | 2296244.59 | | 1999 | Electronics | AK | 681779.07 | | 1999 | Electronics | LA | 1614465.52 | +--------------+-------------+----------+------------+ 27 rows in set (0.21 sec)
When using ROLLUP and CUBE, the result set raises two issues that need to be addressed:
ROLLUP or CUBE operations, you need a way to tell them apart.The GROUPING and GROUPING_ID functions (paired with GROUPING SETS) solve these problems effectively.
GROUPING takes a single column as its argument:
1 when the value is a NULL produced by a ROLLUP or CUBE operation (that is, the row is a subtotal row).0 for any other value, including NULL values that already exist in the table.Example:
SELECT year(d_date), month(d_date), sum(ss_net_paid) AS total_sum, grouping(year(d_date)), grouping(month(d_date)) FROM store_sales, date_dim d1 WHERE d1.d_date_sk = ss_sold_date_sk AND year(d_date) IN (2001, 2002) AND month(d_date) IN (1, 2, 3) GROUP BY ROLLUP(year(d_date), month(d_date)) ORDER BY year(d_date), month(d_date);
The values of the GROUPING function at different aggregation levels:
(year(d_date), month(d_date)) group: result is (0, 0), the detail rows aggregated by year and month.(year(d_date)) group: result is (0, 1), the subtotal rows aggregated by year.() group: result is (1, 1), the grand total row.The query result is as follows:
+--------------+---------------+-------------+------------------------+-------------------------+ | year(d_date) | month(d_date) | total_sum | Grouping(year(d_date)) | Grouping(month(d_date)) | +--------------+---------------+-------------+------------------------+-------------------------+ | NULL | NULL | 54262669.17 | 1 | 1 | | 2001 | NULL | 26640320.46 | 0 | 1 | | 2001 | 1 | 9982165.83 | 0 | 0 | | 2001 | 2 | 8454915.34 | 0 | 0 | | 2001 | 3 | 8203239.29 | 0 | 0 | | 2002 | NULL | 27622348.71 | 0 | 1 | | 2002 | 1 | 11260654.35 | 0 | 0 | | 2002 | 2 | 7722750.61 | 0 | 0 | | 2002 | 3 | 8638943.75 | 0 | 0 | +--------------+---------------+-------------+------------------------+-------------------------+ 9 rows in set (0.06 sec)
The GROUPING function can filter results to a specific aggregation level. The following example keeps only “the grand total of sales,” “sales aggregated by year,” and “sales aggregated by state”:
SELECT year(d_date), i_category, ca_state, sum(ss_net_paid) AS total_sum FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ("Books", "Electronics") AND year(d_date) IN (1998, 1999) AND ca_state IN ("LA", "AK") GROUP BY CUBE(year(d_date), i_category, ca_state) HAVING grouping(year(d_date)) = 1 AND grouping(i_category) = 1 AND grouping(ca_state) = 1 OR grouping(year(d_date)) = 0 AND grouping(i_category) = 1 AND grouping(ca_state) = 1 OR grouping(year(d_date)) = 1 AND grouping(i_category) = 1 AND grouping(ca_state) = 0 ORDER BY year(d_date), i_category, ca_state;
The query result is as follows:
+---------------------+------------+----------+------------+ | year(`d1`.`d_date`) | i_category | ca_state | total_sum | +---------------------+------------+----------+------------+ | NULL | NULL | NULL | 8690374.60 | | NULL | NULL | AK | 2675198.33 | | NULL | NULL | LA | 6015176.27 | | 1998 | NULL | NULL | 4369656.14 | | 1999 | NULL | NULL | 4320718.46 | +---------------------+------------+----------+------------+ 5 rows in set (0.13 sec)
Replacing the NULL values in subtotal rows with more intuitive strings makes the result easier to read:
SELECT IF(grouping(year(d_date)) = 1, "Multi-year sum", year(d_date)) AS year, IF(grouping(i_category) = 1, "Multi-category sum", i_category) AS category, sum(ss_net_paid) AS total_sum FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ("Books", "Electronics") AND year(d_date) IN (1998, 1999) AND ca_state IN ("LA", "AK") GROUP BY CUBE(year(d_date), i_category)
The query result is as follows:
+----------------+--------------------+------------+ | year | category | total_sum | +----------------+--------------------+------------+ | 1998 | Books | 2213703.82 | | 1998 | Electronics | 2155952.32 | | 1999 | Electronics | 2296244.59 | | 1999 | Books | 2024473.87 | | 1998 | Multi-category sum | 4369656.14 | | 1999 | Multi-category sum | 4320718.46 | | Multi-year sum | Books | 4238177.69 | | Multi-year sum | Electronics | 4452196.91 | | Multi-year sum | Multi-category sum | 8690374.60 | +----------------+--------------------+------------+ 9 rows in set (0.09 sec)
GROUPING_ID and GROUPING are both used to assist with multi-dimensional aggregation queries (such as ROLLUP and CUBE), helping you distinguish between aggregation results at different levels.
To identify the aggregation level a row belongs to with GROUPING, you must compute the function on every GROUP BY column individually (a single-column result is not enough to distinguish levels), which makes the SQL verbose. GROUPING_ID is more powerful than GROUPING: it accepts multiple columns as arguments and returns an integer whose binary bits represent the aggregation states of all those columns at once.
When the computed results are stored in a table or materialized view, using the GROUPING function to represent different aggregation levels takes up more storage. GROUPING_ID is more appropriate in this scenario.
Taking CUBE(a, b) as an example, the correspondence between GROUPING_ID and GROUPING is:
| Aggregation level | Bit Vector | GROUPING_ID | GROUPING(a) | GROUPING(b) |
|---|---|---|---|---|
| a, b | 0 0 | 0 | 0 | 0 |
| a | 0 1 | 1 | 0 | 1 |
| b | 1 0 | 2 | 1 | 0 |
| Grand Total | 1 1 | 3 | 1 | 1 |
SELECT year(d_date), i_category, SUM(ss_net_paid) AS total_sum, GROUPING(year(d_date)), GROUPING(i_category), GROUPING_ID(year(d_date), i_category) FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ('Books', 'Electronics') AND year(d_date) IN (1998, 1999) AND ca_state IN ('LA', 'AK') GROUP BY CUBE(year(d_date), i_category);
The query result is as follows:
+--------------+-------------+------------+------------------------+----------------------+---------------------------------------+ | year(d_date) | i_category | total_sum | GROUPING(year(d_date)) | GROUPING(i_category) | GROUPING_ID(year(d_date), i_category) | +--------------+-------------+------------+------------------------+----------------------+---------------------------------------+ | 1998 | Electronics | 2155952.32 | 0 | 0 | 0 | | 1998 | Books | 2213703.82 | 0 | 0 | 0 | | 1999 | Electronics | 2296244.59 | 0 | 0 | 0 | | 1999 | Books | 2024473.87 | 0 | 0 | 0 | | 1998 | NULL | 4369656.14 | 0 | 1 | 1 | | 1999 | NULL | 4320718.46 | 0 | 1 | 1 | | NULL | Electronics | 4452196.91 | 1 | 0 | 2 | | NULL | Books | 4238177.69 | 1 | 0 | 2 | | NULL | NULL | 8690374.60 | 1 | 1 | 3 | +--------------+-------------+------------+------------------------+----------------------+---------------------------------------+ 9 rows in set (0.12 sec)
When you need to aggregate only over a few specified grouping combinations rather than compute a full CUBE, use GROUPING SETS in the GROUP BY clause. It lets you specify exactly which combinations across multiple dimensions to compute, avoiding unnecessary overhead.
Because CUBE queries usually consume more resources, GROUPING SETS improves execution efficiency when only a few dimensions are of interest.
SELECT … GROUP BY GROUPING SETS(grouping_column_reference_list)
Suppose you need:
You can use GROUPING SETS to specify these dimension combinations explicitly:
SELECT YEAR(d_date), i_category, ca_state, SUM(ss_net_paid) AS total_sum FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ('Books', 'Electronics') AND YEAR(d_date) IN (1998, 1999) AND ca_state IN ('LA', 'AK') GROUP BY GROUPING SETS( (YEAR(d_date), i_category), (YEAR(d_date), ca_state), (YEAR(d_date), ca_state, i_category) ) ORDER BY YEAR(d_date), i_category, ca_state;
The query result is as follows:
+--------------+-------------+----------+------------+ | YEAR(d_date) | i_category | ca_state | total_sum | +--------------+-------------+----------+------------+ | 1998 | NULL | AK | 1402539.19 | | 1998 | NULL | LA | 2967116.95 | | 1998 | Books | NULL | 2213703.82 | | 1998 | Books | AK | 719911.29 | | 1998 | Books | LA | 1493792.53 | | 1998 | Electronics | NULL | 2155952.32 | | 1998 | Electronics | AK | 682627.90 | | 1998 | Electronics | LA | 1473324.42 | | 1999 | NULL | AK | 1272659.14 | | 1999 | NULL | LA | 3048059.32 | | 1999 | Books | NULL | 2024473.87 | | 1999 | Books | AK | 590880.07 | | 1999 | Books | LA | 1433593.80 | | 1999 | Electronics | NULL | 2296244.59 | | 1999 | Electronics | AK | 681779.07 | | 1999 | Electronics | LA | 1614465.52 | +--------------+-------------+----------+------------+ 16 rows in set (0.11 sec)
The query above is equivalent to running CUBE and then using grouping_id to keep only the specified aggregation combinations, which avoids unnecessary computation:
SELECT SUM(ss_net_paid) AS total_sum, YEAR(d_date), i_category, ca_state FROM store_sales, date_dim d1, item, customer_address ca WHERE d1.d_date_sk = ss_sold_date_sk AND i_item_sk = ss_item_sk AND ss_addr_sk = ca_address_sk AND i_category IN ('Books', 'Electronics') AND YEAR(d_date) IN (1998, 1999) AND ca_state IN ('LA', 'AK') GROUP BY CUBE(YEAR(d_date), ca_state, i_category) HAVING grouping_id(YEAR(d_date), ca_state, i_category) = 0 OR grouping_id(YEAR(d_date), ca_state, i_category) = 2 OR grouping_id(YEAR(d_date), ca_state, i_category) = 1;
:::info Note CUBE computes every possible aggregation level (eight in this example), but in practice you may only be interested in a few of them. :::
GROUPING SETS is the more fundamental form of multi-dimensional aggregation. Both ROLLUP and CUBE can be expanded into GROUPING SETS.
The following GROUPING SETS statement:
SELECT k1, k2, SUM(k3) FROM t GROUP BY GROUPING SETS ((k1, k2), (k1), (k2), ());
Is equivalent to multiple GROUP BY queries joined by UNION ALL:
SELECT k1, k2, SUM(k3) FROM t GROUP BY k1, k2 UNION ALL SELECT k1, NULL, SUM(k3) FROM t GROUP BY k1 UNION ALL SELECT NULL, k2, SUM(k3) FROM t GROUP BY k2 UNION ALL SELECT NULL, NULL, SUM(k3) FROM t;
The query joined by UNION ALL is longer and scans the base table multiple times, so it is less efficient both to write and to execute.
ROLLUP is an extension of GROUPING SETS. For example:
SELECT a, b, c, SUM(d) FROM tab1 GROUP BY ROLLUP(a, b, c);
Is equivalent to the following GROUPING SETS:
GROUPING SETS ( (a, b, c), (a, b), (a), () );
CUBE(a, b, c) is equivalent to the following GROUPING SETS:
GROUPING SETS ( (a, b, c), (a, b), (a, c), (a), (b, c), (b), (c), () );
For the table creation statements and data files, see the appendix of Analytic Functions (Window Functions).