GROUPING SETS

PostgreSQL GROUPING SETS lets you define multiple GROUP BY groupings in a single query, replacing multiple UNION ALL statements for multi-dimensional aggregation.

5 min read · Last updated: March 2026 · Back to overview

Quick Answer

PostgreSQL GROUPING SETS is a subclause of GROUP BY that lets you define multiple grouping combinations in a single query. Instead of writing separate queries and combining them with UNION ALL, GROUPING SETS scans the table once and produces subtotals for each specified grouping.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

Introduction to PostgreSQL GROUPING SETS

GROUPING SETS is a subclause of GROUP BY that lets you compute aggregates across multiple grouping combinations in a single query. Each grouping set is a parenthesized list of columns, and PostgreSQL produces a separate subtotal for each one.

General syntax:

SELECT c1, c2, aggregate_function(c3)
FROM table_name
GROUP BY GROUPING SETS (
  (c1, c2),
  (c1),
  (c2),
  ()
);

The empty set () computes the grand total across all rows.

Sample table setup

CREATE TABLE sales (
  brand    VARCHAR NOT NULL,
  segment  VARCHAR NOT NULL,
  quantity INT     NOT NULL,
  PRIMARY KEY (brand, segment)
);

INSERT INTO sales (brand, segment, quantity)
VALUES
  ('ABC', 'Premium', 100),
  ('ABC', 'Basic',   200),
  ('XYZ', 'Premium', 100),
  ('XYZ', 'Basic',   300);

Why GROUPING SETS replaces UNION ALL

Without GROUPING SETS, getting subtotals at four levels requires four separate queries joined with UNION ALL:

SELECT brand, segment, SUM(quantity) FROM sales GROUP BY brand, segment
UNION ALL
SELECT brand, NULL,    SUM(quantity) FROM sales GROUP BY brand
UNION ALL
SELECT NULL,  segment, SUM(quantity) FROM sales GROUP BY segment
UNION ALL
SELECT NULL,  NULL,    SUM(quantity) FROM sales;

This is verbose and scans the sales table four times. The GROUPING SETS equivalent is more concise and performs a single scan:

SELECT brand, segment, SUM(quantity)
FROM sales
GROUP BY GROUPING SETS (
  (brand, segment),
  (brand),
  (segment),
  ()
);

Output:

 brand | segment | sum
-------+---------+-----
 null  | null    | 700
 XYZ   | Basic   | 300
 ABC   | Premium | 100
 ABC   | Basic   | 200
 XYZ   | Premium | 100
 ABC   | null    | 300
 XYZ   | null    | 400
 null  | Basic   | 500
 null  | Premium | 200
(9 rows)

Rows where brand or segment is NULL represent subtotals for that level of aggregation.

Using the GROUPING() function

GROUPING(column) returns 0 if the column is part of the current grouping set and 1 if it is not. This lets you distinguish aggregation-introduced NULLs from real NULL data:

SELECT
  GROUPING(brand)   AS grouping_brand,
  GROUPING(segment) AS grouping_segment,
  brand,
  segment,
  SUM(quantity)
FROM sales
GROUP BY GROUPING SETS (
  (brand),
  (segment),
  ()
)
ORDER BY brand, segment;

Filter to only brand-level subtotals using GROUPING() in the HAVING clause:

SELECT
  GROUPING(brand)   AS grouping_brand,
  GROUPING(segment) AS grouping_segment,
  brand,
  segment,
  SUM(quantity)
FROM sales
GROUP BY GROUPING SETS (
  (brand),
  (segment),
  ()
)
HAVING GROUPING(brand) = 0
ORDER BY brand, segment;

GROUPING SETS tips

  • Use GROUPING() to distinguish NULLs introduced by the grouping operation from NULLs that exist in the actual data.
  • Include the empty set () in your grouping list to add a grand total row without a separate query.
  • When all combinations of columns are needed, consider CUBE as a shorthand — CUBE (brand, segment) is equivalent to GROUPING SETS ((brand, segment), (brand), (segment), ()).
  • For hierarchical subtotals (e.g., year → month → day), ROLLUP is a more concise alternative to a manually written GROUPING SETS.
  • Order results with ORDER BY brand NULLS LAST, segment NULLS LAST to push grand-total rows to the bottom of the output.

Continue in Group Data: CUBE.

Related in this section: PostgreSQL GROUP BY · HAVING · CUBE

Frequently Asked Questions

What problem does GROUPING SETS solve?

When you need aggregates at multiple levels — for example, total sales by brand, by segment, and by brand+segment — you would normally write separate GROUP BY queries and combine them with UNION ALL. This scans the table once per query and is verbose. GROUPING SETS does the same in a single query with a single table scan, making it both shorter and more efficient.

What does an empty grouping set () mean in GROUPING SETS?

An empty grouping set () computes the grand total across all rows — the same as running SELECT aggregate_function(col) FROM table with no GROUP BY. Including () in GROUPING SETS adds a grand total row to the result alongside the subtotals.

How do I tell which grouping a row belongs to in GROUPING SETS output?

Use the GROUPING() function. It accepts a column name and returns 0 if that column is part of the current grouping set, or 1 if it is not (meaning the column value is NULL because it is aggregated over). For example, GROUPING(brand) = 0 identifies rows grouped by brand, and GROUPING(brand) = 1 identifies rows where brand is NULL due to a higher-level subtotal.

What is the difference between GROUPING SETS, CUBE, and ROLLUP?

GROUPING SETS lets you explicitly list every grouping combination you want. ROLLUP generates a hierarchy of subtotals from most-detailed to grand total — useful for time hierarchies like (year, month, day). CUBE generates every possible combination of the listed columns, including the grand total — useful when you want all permutations. GROUPING SETS gives you full control; ROLLUP and CUBE are convenient shortcuts for common patterns.

Why do NULL values appear in GROUPING SETS output?

When a column is not part of the current grouping set, PostgreSQL sets it to NULL in that row to indicate the value is aggregated over. For example, if a row represents the subtotal for all segments of a brand, the segment column is NULL. Use GROUPING() to distinguish intentional NULL data from NULLs introduced by the grouping operation.