SQL-Engineering-Handbook

04 · ROLLUP, CUBE, and GROUPING SETS

🏠 Module Home · 🗂️ Handbook Home · ← 03 Conditional Aggregation · Next → 05 Business KPI Reports

ROLLUP, CUBE and GROUPING SETS

Module: 02 — Advanced Aggregations Domain used in this file: Retail (sales, stores, regions, products) Companion file: 04_ROLLUP_CUBE_GROUPING_SETS.sql


Introduction

Every finance report you’ve ever seen with subtotal rows and a grand total at the bottom was built with one of three tools: ROLLUP, CUBE, or GROUPING SETS. Without them, producing subtotals means running several separate GROUP BY queries at different levels and manually stacking the results — slow, error-prone, and impossible to keep consistent as data changes between runs. This topic is where multi-column GROUP BY (Topic 01) grows into full hierarchical reporting.


Concept Overview

ROLLUP(a, b) is equivalent to GROUPING SETS ((a, b), (a), ()). CUBE(a, b) is equivalent to GROUPING SETS ((a, b), (a), (b), ()). GROUPING SETS is the general-purpose tool; ROLLUP and CUBE are convenient shorthand for its two most common shapes.


Business Motivation

A retail finance report needs: revenue per store per month, a subtotal per store across all months, and a company-wide grand total — all in one downloadable table, because that is exactly the shape a finance stakeholder expects to see in a spreadsheet or BI export. Building this by hand means three separate queries (detail, store subtotal, grand total) unioned together, with a real risk that the numbers drift out of sync if the underlying sales table is written to between queries. ROLLUP computes all three levels in a single, internally consistent aggregation pass.


Why This Feature Exists

SQL’s standard GROUP BY intentionally returns only one grain per query. ROLLUP, CUBE, and GROUPING SETS extend GROUP BY specifically to support the very common real-world need for multiple grains, with totals, in one result set — because that is the shape nearly every finance and executive report is expected to take, and recomputing it as several separate queries is both slower and harder to keep consistent.


Real Company Examples


Business Problems Solved


Visual Explanation

ROLLUP(region, store)                       Hierarchy: region ▸ store ▸ (grand total)

┌────────┬──────────┬─────────┐
│ region │ store     │ revenue │
├────────┼──────────┼─────────┤
│ East    │ Store A   │ 40,000  │  ◀ detail
│ East    │ Store B   │ 35,000  │  ◀ detail
│ East    │ NULL      │ 75,000  │  ◀ subtotal for East (GROUPING(store) = 1)
│ West    │ Store C   │ 28,000  │  ◀ detail
│ West    │ NULL      │ 28,000  │  ◀ subtotal for West
│ NULL    │ NULL      │ 103,000 │  ◀ grand total (GROUPING(region) = 1)
└────────┴──────────┴─────────┘

CUBE(region, store) would additionally include a subtotal grouped by store alone (across all regions), which ROLLUP does not produce because it follows a strict hierarchy.


Syntax

-- ROLLUP: hierarchical subtotals following column order
SELECT region, store, SUM(revenue) AS total_revenue
FROM sales
GROUP BY ROLLUP(region, store);

-- CUBE: every possible subtotal combination
SELECT region, store, SUM(revenue) AS total_revenue
FROM sales
GROUP BY CUBE(region, store);

-- GROUPING SETS: hand-picked combinations only
SELECT region, store, SUM(revenue) AS total_revenue
FROM sales
GROUP BY GROUPING SETS ((region, store), (region), ());

-- GROUPING(): identify which columns are "rolled up" (NULL) in a given row
SELECT
    region, store, SUM(revenue) AS total_revenue,
    GROUPING(region) AS is_region_total,
    GROUPING(store)   AS is_store_total
FROM sales
GROUP BY ROLLUP(region, store);

Detailed Walkthrough

SELECT
    r.region_name,
    s.store_name,
    SUM(sa.revenue)                    AS total_revenue,
    GROUPING(r.region_name)            AS is_region_subtotal,
    GROUPING(s.store_name)             AS is_store_subtotal
FROM sales   AS sa
JOIN stores  AS s ON sa.store_id  = s.store_id
JOIN regions AS r ON s.region_id  = r.region_id
GROUP BY ROLLUP(r.region_name, s.store_name)
ORDER BY r.region_name, s.store_name;
  1. The engine first computes the full detail grain: one row per (region, store).
  2. It then adds a subtotal row per region, with store_name set to NULL — this is what ROLLUP adds beyond a plain GROUP BY.
  3. Finally, it adds one grand-total row with both region_name and store_name set to NULL.
  4. GROUPING(column) returns 1 on rows where that column has been “rolled up” into NULL for subtotal purposes, and 0 on genuine detail rows — this is how the presentation layer distinguishes a real NULL value from a subtotal marker.

Production Workflow

ROLLUP/CUBE/GROUPING SETS queries commonly feed directly into finance export tables, scheduled PDF/Excel report generation, or BI tools that expect pre-built subtotal rows (many BI tools can also compute subtotals client-side, but pushing it into SQL keeps the report reproducible and consistent regardless of which tool renders it).


Analytics Engineering Perspective


Performance Considerations


Edge Cases


Common Mistakes


Best Practices


Interview Questions

  1. What is the difference between ROLLUP(a, b) and CUBE(a, b)? ROLLUP produces subtotals following the hierarchy (a,b) → (a) → (). CUBE produces every combination: (a,b), (a), (b), ().
  2. How do you tell a genuine NULL value in the source data apart from a ROLLUP-generated subtotal NULL? Use the GROUPING() function, which returns 1 for rolled-up (subtotal) columns and 0 for real data rows.
  3. Why might GROUPING SETS outperform CUBE for the same reporting need? GROUPING SETS computes only the explicitly listed combinations, while CUBE computes every possible combination (2ⁿ for n columns), many of which may not be needed.
  4. What’s a risk of applying HAVING to a ROLLUP query without considering GROUPING()? The HAVING condition also applies to subtotal and grand-total rows, potentially filtering out a legitimate subtotal that happens to fail the same threshold as a detail row.
  5. Is ROLLUP(region, store) the same as ROLLUP(store, region)? No — ROLLUP’s subtotal hierarchy follows column order, so the two produce different subtotal levels.

Summary

ROLLUP, CUBE, and GROUPING SETS extend GROUP BY to produce subtotals and grand totals in a single aggregation pass — exactly the shape financial and executive reports require. ROLLUP follows a hierarchy, CUBE produces every combination, and GROUPING SETS lets you specify exactly what you need by hand. GROUPING() is the tool that safely distinguishes a computed subtotal from a genuine NULL in the underlying data.


Practice Challenges

  1. Rewrite the walkthrough query using CUBE instead of ROLLUP and explain the extra row(s) it produces.
  2. Rewrite the walkthrough query using GROUPING SETS to produce only (region, store) and the grand total — skipping the region-level subtotal entirely.
  3. Add a COALESCE-based label column that reads 'All Stores' on region subtotal rows and 'Company Total' on the grand-total row, using GROUPING().
  4. Explain what would go wrong if HAVING SUM(revenue) > 50000 were added to the walkthrough query without accounting for GROUPING().
  5. Design a GROUPING SETS query for a three-dimensional report (region, store, product category) that includes only region-level and store-level subtotals, skipping category-level and any two-way combinations.

Further Reading


◀ Previous: 03_CONDITIONAL_AGGREGATION.md · Next ▶ 05_BUSINESS_KPI_REPORTS.md


⬆ Back to top · 🏠 Module Home · 🗂️ Handbook Home