Skip to main content

GROUPING SETS

GROUPING SETS, ROLLUP and CUBE can be used in the GROUP BY clause to perform a grouping over multiple dimensions within the same query. Note that this syntax is not compatible with GROUP BY ALL.

Examples

Compute the average income along the provided four different dimensions:

-- the syntax () denotes the empty set (i.e., computing an ungrouped aggregate)
SELECT city, street_name, avg(income)
FROM addresses
GROUP BY GROUPING SETS ((city, street_name), (city), (street_name), ());

Compute the average income along the same dimensions:

SELECT city, street_name, avg(income)
FROM addresses
GROUP BY CUBE (city, street_name);

Compute the average income along the dimensions (city, street_name), (city) and ():

SELECT city, street_name, avg(income)
FROM addresses
GROUP BY ROLLUP (city, street_name);

Description

GROUPING SETS perform the same aggregate across different GROUP BY clauses in a single query.

CREATE TABLE students (course VARCHAR, type VARCHAR);
INSERT INTO students (course, type)
VALUES
('CS', 'Bachelor'), ('CS', 'Bachelor'), ('CS', 'PhD'), ('Math', 'Masters'),
('CS', NULL), ('CS', NULL), ('Math', NULL);
SELECT course, type, count(*)
FROM students
GROUP BY GROUPING SETS ((course, type), course, type, ());
coursetypecount_star()
MathNULL1
NULLNULL7
CSPhD1
CSBachelor2
MathMasters1
CSNULL2
MathNULL2
CSNULL5
NULLNULL3
NULLMasters1
NULLBachelor2
NULLPhD1

In the above query, we group across four different sets: course, type, course, type and () (the empty group). The result contains NULL for a group which is not in the grouping set for the result, i.e., the above query is equivalent to the following statement of UNION ALL clauses:

-- Group by course, type:
SELECT course, type, count(*)
FROM students
GROUP BY course, type
UNION ALL
-- Group by type:
SELECT NULL AS course, type, count(*)
FROM students
GROUP BY type
UNION ALL
-- Group by course:
SELECT course, NULL AS type, count(*)
FROM students
GROUP BY course
UNION ALL
-- Group by nothing:
SELECT NULL AS course, NULL AS type, count(*)
FROM students;

CUBE and ROLLUP are syntactic sugar to easily produce commonly used grouping sets.

The ROLLUP clause will produce all “sub-groups” of a grouping set, e.g., ROLLUP (country, city, zip) produces the grouping sets (country, city, zip), (country, city), (country), (). This can be useful for producing different levels of detail of a group by clause. This produces n+1 grouping sets where n is the amount of terms in the ROLLUP clause.

CUBE produces grouping sets for all combinations of the inputs, e.g., CUBE (country, city, zip) will produce (country, city, zip), (country, city), (country, zip), (city, zip), (country), (city), (zip), (). This produces 2^n grouping sets.

Identifying Grouping Sets with GROUPING_ID()

The super-aggregate rows generated by GROUPING SETS, ROLLUP and CUBE can often be identified by NULL-values returned for the respective column in the grouping. But if the columns used in the grouping can themselves contain actual NULL-values, then it can be challenging to distinguish whether the value in the resultset is a “real” NULL-value coming out of the data itself, or a NULL-value generated by the grouping construct. The GROUPING_ID() or GROUPING() function is designed to identify which groups generated the super-aggregate rows in the result.

GROUPING_ID() is an aggregate function that takes the column expressions that make up the grouping(s). It returns a BIGINT value. The return value is 0 for the rows that are not super-aggregate rows. But for the super-aggregate rows, it returns an integer value that identifies the combination of expressions that make up the group for which the super-aggregate is generated. At this point, an example might help. Consider the following query:

WITH days AS (
SELECT
year("generate_series") AS y,
quarter("generate_series") AS q,
month("generate_series") AS m
FROM generate_series(DATE '2023-01-01', DATE '2023-12-31', INTERVAL 1 DAY)
)
SELECT y, q, m, GROUPING_ID(y, q, m) AS "grouping_id()"
FROM days
GROUP BY GROUPING SETS (
(y, q, m),
(y, q),
(y),
()
)
ORDER BY y, q, m;

These are the results:

yqmgrouping_id()
2023110
2023120
2023130
20231NULL1
2023240
2023250
2023260
20232NULL1
2023370
2023380
2023390
20233NULL1
20234100
20234110
20234120
20234NULL1
2023NULLNULL3
NULLNULLNULL7

In this example, the lowest level of grouping is at the month level, defined by the grouping set (y, q, m). Result rows corresponding to that level are simply aggregate rows and the GROUPING_ID(y, q, m) function returns 0 for those. The grouping set (y, q) results in super-aggregate rows over the month level, leaving a NULL-value for the m column, and for which GROUPING_ID(y, q, m) returns 1. The grouping set (y) results in super-aggregate rows over the quarter level, leaving NULL-values for the m and q column, for which GROUPING_ID(y, q, m) returns 3. Finally, the () grouping set results in one super-aggregate row for the entire resultset, leaving NULL-values for y, q and m and for which GROUPING_ID(y, q, m) returns 7.

To understand the relationship between the return value and the grouping set, you can think of GROUPING_ID(y, q, m) writing to a bitfield, where the first bit corresponds to the last expression passed to GROUPING_ID(), the second bit to the one-but-last expression passed to GROUPING_ID(), and so on. This may become clearer by casting GROUPING_ID() to BIT:

WITH days AS (
SELECT
year("generate_series") AS y,
quarter("generate_series") AS q,
month("generate_series") AS m
FROM generate_series(DATE '2023-01-01', DATE '2023-12-31', INTERVAL 1 DAY)
)
SELECT
y, q, m,
GROUPING_ID(y, q, m) AS "grouping_id(y, q, m)",
right(GROUPING_ID(y, q, m)::BIT::VARCHAR, 3) AS "y_q_m_bits"
FROM days
GROUP BY GROUPING SETS (
(y, q, m),
(y, q),
(y),
()
)
ORDER BY y, q, m;

Which returns these results:

yqmgrouping_id(y, q, m)y_q_m_bits
2023110000
2023120000
2023130000
20231NULL1001
2023240000
2023250000
2023260000
20232NULL1001
2023370000
2023380000
2023390000
20233NULL1001
20234100000
20234110000
20234120000
20234NULL1001
2023NULLNULL3011
NULLNULLNULL7111

Note that the number of expressions passed to GROUPING_ID(), or the order in which they are passed is independent from the actual group definitions appearing in the GROUPING SETS-clause (or the groups implied by ROLLUP and CUBE). As long as the expressions passed to GROUPING_ID() are expressions that appear somewhere in the GROUPING SETS-clause, GROUPING_ID() will set a bit corresponding to the position of the expression whenever that expression is rolled up to a super-aggregate.

Syntax