Advanced Aggregations & Pivot/Unpivot
Master GROUPING SETS, ROLLUP, CUBE, pivot tables, and the FILTER clause for BI-grade reports.
๐ฏ What You'll Learn
- GROUPING SETS for multiple aggregation levels in one query
- ROLLUP for hierarchical subtotals (region โ country โ city)
- CUBE for every possible subtotal combination
- Pivot and unpivot techniques for cross-tab reports
- FILTER clause for clean conditional aggregation
๐๏ธ GROUPING SETS
Think of GROUPING SETS like asking the database to run multiple GROUP BY queries at once and stack the results. Instead of three separate queries with UNION ALL (three table scans), GROUPING SETS does it in a single scan.
GROUPING SETS
Multiple aggregation levels in one efficient query
-- GROUPING SETS: multiple GROUP BY levels in one query
-- Instead of running 3 separate queries and UNIONing them:
-- Traditional approach (3 queries):
SELECT department, NULL AS job_title, SUM(salary) FROM employees GROUP BY department
UNION ALL
SELECT NULL, job_title, SUM(salary) FROM employees GROUP BY job_title
UNION ALL
SELECT NULL, NULL, SUM(salary) FROM employees;
-- GROUPING SETS (one query, one table scan!):
SELECT department, job_title, SUM(salary) AS total_salary,
COUNT(*) A
...๐ ROLLUP โ Hierarchical Reports
ROLLUP is the most common in business reports โ it creates subtotals that "roll up" from the most detailed level to a grand total. Perfect for: region โ country โ city breakdowns.
๐ก Pro Tip โ Column Order Matters
ROLLUP(a, b, c) gives subtotals for (a,b,c), (a,b), (a), (). Reversing to ROLLUP(c, b, a) gives completely different subtotals. Put the broadest category first.
ROLLUP
Hierarchical subtotals for regional reports
-- ROLLUP: hierarchical subtotals (most common in reports)
-- Generates subtotals from right to left + grand total
SELECT
region,
country,
city,
SUM(revenue) AS total_revenue,
COUNT(*) AS store_count
FROM stores
GROUP BY ROLLUP (region, country, city)
ORDER BY region, country, city;
-- This produces:
-- region | country | city | revenue (detail)
-- NA | US | NYC | 500,000
-- NA | US | LA | 400,000
-- NA | US | NULL | 900,000
...๐ง CUBE โ All Combinations
CUBE is ROLLUP's powerful sibling โ it generates subtotals for every possible combination of grouped columns. Use it when you need a full cross-dimensional analysis.
โ ๏ธ Common Mistake
Using CUBE with too many columns. CUBE(a,b,c,d,e) generates 2โต = 32 grouping sets. The result set explodes. Use GROUPING SETS to pick only the combinations you actually need.
CUBE
Every possible subtotal combination
-- CUBE: every possible combination of subtotals
-- Like ROLLUP but crosses ALL dimensions
SELECT
COALESCE(category, 'ALL CATEGORIES') AS category,
COALESCE(region, 'ALL REGIONS') AS region,
COALESCE(quarter, 'ALL QUARTERS') AS quarter,
SUM(revenue) AS total_revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM sales
GROUP BY CUBE (category, region, quarter)
ORDER BY category, region, quarter;
-- CUBE(a, b, c) generates ALL combinations:
-- (a,b,c), (a,b), (a,c), (b
...๐ Pivot & Unpivot
Pivoting turns row values into columns โ transforming a list of monthly sales into a spreadsheet-style grid. Unpivoting does the reverse. Standard SQL uses CASE WHEN; some databases add PIVOT/UNPIVOT syntax.
Pivot & Unpivot
Transform rows to columns and back
-- PIVOT: turn row values into columns
-- (Standard SQL uses CASE WHEN; some DBs have PIVOT syntax)
-- Raw data: rows per month
-- | product | month | sales |
-- | Widget | Jan | 100 |
-- | Widget | Feb | 150 |
-- | Gadget | Jan | 200 |
-- PIVOT with CASE WHEN (works everywhere):
SELECT
product,
SUM(CASE WHEN month = 'Jan' THEN sales END) AS jan,
SUM(CASE WHEN month = 'Feb' THEN sales END) AS feb,
SUM(CASE WHEN month = 'Mar' THEN sales END) AS mar,
SUM(CASE W
...๐ฏ FILTER Clause
The FILTER clause (PostgreSQL) is a cleaner alternative to CASE WHEN inside aggregates. It's more readable, and the optimizer can sometimes execute it more efficiently.
FILTER Clause
Clean conditional aggregation in PostgreSQL
-- FILTER clause: conditional aggregation (PostgreSQL)
-- Cleaner than CASE WHEN inside aggregates
-- Traditional CASE WHEN approach:
SELECT
department,
COUNT(*) AS total_employees,
COUNT(CASE WHEN salary > 100000 THEN 1 END) AS high_earners,
SUM(CASE WHEN status = 'active' THEN salary ELSE 0 END) AS active_payroll
FROM employees
GROUP BY department;
-- FILTER clause (PostgreSQL 9.4+) โ much cleaner:
SELECT
department,
COUNT(*) AS total_employees,
COUNT(*) FILTER (W
...๐ Quick Reference
| Feature | What It Does |
|---|---|
| GROUPING SETS | Custom set of aggregation levels |
| ROLLUP(a,b,c) | (a,b,c), (a,b), (a), () subtotals |
| CUBE(a,b,c) | All 2โฟ subtotal combinations |
| GROUPING(col) | Returns 1 if col is a subtotal NULL |
| FILTER(WHERE) | Conditional aggregate (PG only) |
๐ Lesson Complete!
You've mastered advanced aggregations that power business intelligence reports. Next, explore recursive CTEs for graph and hierarchy traversal!
Sign up for free to track which lessons you've completed and get learning reminders.