CUBE in PostgreSQL GROUP BY

Learn how PostgreSQL CUBE generates all possible grouping combinations for multi-dimensional aggregate analysis, with examples and a comparison to ROLLUP and GROUPING SETS.

6 min read · PostgreSQL 9.5+ · Back to overview

Quick Answer

CUBE is a GROUP BY subclause that generates all 2^n possible grouping combinations for n columns, including every subtotal and a grand total. It is ideal for multi-dimensional pivot-style reports where every cross-section of the data matters.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The CUBE subclause of GROUP BY generates every possible combination of grouping sets for a list of columns. A single CUBE query replaces what would otherwise require multiple separate GROUP BY queries combined with UNION ALL, making it the standard tool for multi-dimensional aggregate reporting.

Syntax

SELECT c1, c2, c3, aggregate(c4)
FROM table_name
GROUP BY CUBE (c1, c2, c3);

For n columns, CUBE produces 2^n grouping sets. CUBE(c1, c2, c3) is equivalent to:

GROUPING SETS (
  (c1, c2, c3),
  (c1, c2),
  (c1, c3),
  (c2, c3),
  (c1),
  (c2),
  (c3),
  ()
)

The empty grouping set () always produces the grand total row. A NULL in a dimension column of the result means that row aggregates across all values of that dimension.

Practical Example

Create a three-dimensional fact table for event ticket sales:

CREATE TABLE ticket_sales (
  sale_id  SERIAL PRIMARY KEY,
  region   VARCHAR NOT NULL,
  venue    VARCHAR NOT NULL,
  genre    VARCHAR NOT NULL,
  tickets  INT     NOT NULL
);

INSERT INTO ticket_sales (region, venue, genre, tickets) VALUES
  ('East', 'Arena',   'Rock',     120),
  ('East', 'Arena',   'Jazz',      45),
  ('East', 'Theater', 'Jazz',      80),
  ('West', 'Arena',   'Rock',      95),
  ('West', 'Theater', 'Classical', 60),
  ('West', 'Theater', 'Jazz',      30),
  ('North','Arena',   'Rock',      70),
  ('North','Theater', 'Classical', 50);

Generate all subtotals across region and genre:

SELECT
  region,
  genre,
  SUM(tickets) AS total_tickets
FROM ticket_sales
GROUP BY CUBE (region, genre)
ORDER BY region NULLS LAST, genre NULLS LAST;

Sample output:

 region | genre     | total_tickets
--------+-----------+---------------
 East   | Jazz      |           125
 East   | Rock      |           120
 East   | NULL      |           245
 North  | Classical |            50
 North  | Rock      |            70
 North  | NULL      |           120
 West   | Classical |            60
 West   | Jazz      |            30
 West   | Rock      |            95
 West   | NULL      |           185
 NULL   | Classical |           110
 NULL   | Jazz      |           155
 NULL   | Rock      |           285
 NULL   | NULL      |           550

Using GROUPING() to label rows

Replace NULL subtotal markers with descriptive text:

SELECT
  CASE WHEN GROUPING(region) = 1 THEN 'All Regions' ELSE region END AS region,
  CASE WHEN GROUPING(genre)  = 1 THEN 'All Genres'  ELSE genre  END AS genre,
  SUM(tickets) AS total_tickets
FROM ticket_sales
GROUP BY CUBE (region, genre)
ORDER BY
  GROUPING(region),
  region,
  GROUPING(genre),
  genre;

Partial CUBE

A partial CUBE fixes one column and cubes only the others, reducing the number of output rows:

SELECT
  region,
  genre,
  SUM(tickets) AS total_tickets
FROM ticket_sales
GROUP BY region, CUBE (genre)
ORDER BY region, genre NULLS LAST;

Here region appears in every grouping; only genre is cubed. You get one detail row per region+genre combination and one subtotal per region — but no cross-region genre subtotals.

CUBE vs ROLLUP vs GROUPING SETS

CUBEROLLUPGROUPING SETS
Grouping sets produced2^n (all combinations)n+1 (strict hierarchy)Exactly what you list
Respects column orderNoYes (left-to-right)N/A
Best use caseCross-dimensional OLAPTime hierarchiesCustom selection
Grand total includedAlwaysAlwaysOnly if you add ()

Testing with Vela

Multi-dimensional CUBE queries can produce large result sets and consume significant memory on tables with many distinct dimension values. Use Vela’s database branching to run EXPLAIN ANALYZE on your CUBE query against a production-data branch before deploying it. This lets you confirm the planner’s chosen strategy and tune index coverage — for example, adding a composite index on (region, genre) — without any risk to live data.

Production Tips

  • The row count grows exponentially: 3 columns produce up to 2^3 = 8 grouping sets, so the output can be large for dimensions with many distinct values. Use LIMIT, pagination, or a partial CUBE to control result size.
  • NULL in a CUBE result column is a subtotal marker, not missing data. Use GROUPING(col) rather than IS NULL checks to distinguish the two cases reliably.
  • CUBE scans the table once, making it more efficient than running separate GROUP BY queries and using UNION ALL.
  • Combine CUBE with HAVING to filter out subtotal rows below a threshold — for example, HAVING SUM(tickets) > 50 removes low-volume combinations from the report.
  • For EXPLAIN ANALYZE output, look for HashAggregate or GroupAggregate with a Grouping Sets label to confirm the planner is handling CUBE correctly.

Continue in Group Data: ROLLUP.

Related in this section: PostgreSQL GROUP BY · HAVING · GROUPING SETS

Frequently Asked Questions

What does CUBE do in PostgreSQL GROUP BY?
CUBE generates all possible combinations of grouping sets for the listed columns. For n columns, CUBE produces 2^n grouping sets. GROUP BY CUBE(region, product, quarter) produces 8 grouping sets: all three together, each pair, each individual column, and the empty set for the grand total. NULL in a CUBE column indicates that row is a subtotal across all values of that dimension.
Does CUBE lock the table?
No. CUBE is part of a SELECT query and takes an ACCESS SHARE lock for the query's duration. It does not block concurrent reads or writes. Long-running CUBE queries on large tables can consume significant resources, so consider running them during off-peak hours on production.
How can I tell which rows in a CUBE result are subtotals vs detail rows?
Use the GROUPING() function. GROUPING(col) returns 1 when the column is aggregated across (subtotal or grand total row) and 0 for a regular grouped row. Wrap it in a CASE expression to replace NULL subtotal markers with descriptive labels like 'All Regions' or 'Grand Total'.
What is a partial CUBE, and when should I use it?
A partial CUBE applies CUBE to only a subset of columns while keeping others fixed. For example, GROUP BY region, CUBE(product, quarter) always groups by region but generates all CUBE combinations for product and quarter. Use a partial CUBE when you need subtotals across some dimensions but not all — it reduces the result row count and computation.
What is the safest way to use CUBE in production?
Index the CUBE columns to speed up grouping. Use a partial CUBE to limit the number of result rows if not every combination is needed. For frequently executed multi-dimensional reports, consider materializing the CUBE output into a materialized view rather than recomputing all 2^n combinations on every request.