Chapter 06 · Advanced T-SQL: Windows, Aggregation, PIVOT, MERGE Alternatives, and JSON

GROUP BY, GROUPING SETS, ROLLUP, CUBE, and Advanced Aggregation

Produce explainable subtotal and multidimensional reports with GROUPING SETS, ROLLUP, CUBE, GROUPING, and GROUPING_ID while controlling grain and workspace growth.

Intermediate105–135 minutesAdvanced aggregation labSQL Server 2025 · compatibility 170Developer/Express · disposable lab06 tableLast reviewed: August 2026

Learning outcomes

ServiceHub finance receives three independently written reports: totals by region, totals by service category, and a grand total. The reports disagree because one query excludes rows with an unknown region while another turns NULL into the word “Total.” A fourth report uses several UNION ALL branches and scans the same fact table repeatedly. Advanced aggregation solves the shape problem only when you understand output grain, subtotal-generated NULL placeholders, and the combinatorial growth of grouping sets.

01

Build ordinary GROUP BY queries from a clearly stated output grain before adding subtotal operators.

02

Use GROUPING SETS, ROLLUP, and CUBE for exact subtotal shapes and predict the groups each construct creates.

03

Distinguish source NULL values from subtotal placeholders with GROUPING and GROUPING_ID.

04

Reason about cardinality, memory grants, sorts/hash aggregates, and spill risk without claiming one aggregation strategy is always faster.

05

Choose advanced aggregation only when its result contract is clearer than repeated UNION branches.

Mental model

GROUP BY changes grain. GROUPING SETS asks SQL Server to produce several grains in one logical query. ROLLUP is hierarchical; CUBE generates all combinations. The syntax is compact, but the result can be much larger than the base grouping.

1. Start from one explicit grain

sql · bootstrap invoice-line data including a real NULL
USE ServiceHubLab;GOIF SCHEMA_ID(N'lab06') IS NULL EXEC(N'CREATE SCHEMA lab06 AUTHORIZATION dbo;');DROP TABLE IF EXISTS lab06.InvoiceLine;GOCREATE TABLE lab06.InvoiceLine(  line_id bigint NOT NULL CONSTRAINT PK_lab06_InvoiceLine PRIMARY KEY,  region varchar(20) NULL,  service_category varchar(30) NOT NULL,  amount decimal(12,2) NOT NULL);INSERT lab06.InvoiceLine(line_id,region,service_category,amount) VALUES(1,'North','repair',125.00),(2,'North','install',300.00),(3,'South','repair',175.00),(4,'South','inspection',90.00),(5,NULL,'repair',80.00),(6,'North','repair',50.00);GOSELECT region, service_category, SUM(amount) AS revenueFROM lab06.InvoiceLineGROUP BY region, service_categoryORDER BY region, service_category;GO

The ordinary query produces one row per distinct (region, service_category). The NULL region is a real source value meaning “region not recorded,” not a grand-total marker. Preserve that distinction when subtotals are added.

2. GROUPING SETS expresses the exact grains you need

GROUPING SETS lets you name several grouping shapes, including the empty grouping set () for a grand total. It is logically similar to a UNION ALL of separate GROUP BY queries, but the optimizer can consider the entire request together. That does not guarantee one scan or one aggregate operator; inspect the plan rather than asserting an implementation.

sql · region, category, and grand total in one statement
USE ServiceHubLab;GOSELECT region, service_category,       SUM(amount) AS revenue,       GROUPING(region) AS g_region,       GROUPING(service_category) AS g_category,       GROUPING_ID(region, service_category) AS grouping_levelFROM lab06.InvoiceLineGROUP BY GROUPING SETS(   (region, service_category),   (region),   (service_category),   ())ORDER BY GROUPING_ID(region,service_category), region, service_category;GO

GROUPING(column) returns 1 when the column is being used as an aggregate placeholder for that output row and 0 when the value belongs to the grouping key. Therefore the real source region IS NULL detail row has g_region = 0, while a subtotal that aggregates all regions has g_region = 1. That bit is the safe way to label totals.

Wrong approach

COALESCE(region, All regions) cannot distinguish a real missing region from a subtotal placeholder. It silently collapses two different meanings into the same display label.

3. ROLLUP is hierarchical; CUBE is combinatorial

ROLLUP(a,b,c) produces detail groups plus progressively higher prefixes: (a,b,c), (a,b), (a), and (). This suits hierarchies such as year → month or region → category when the listed order expresses the drill-up path. CUBE(a,b) produces all combinations: (a,b), (a), (b), and (). With many dimensions, the number of grouping sets grows quickly; do not CUBE a dozen dimensions merely because the syntax is short.

sql · compare ROLLUP and CUBE
USE ServiceHubLab;GOSELECT region, service_category, SUM(amount) AS revenue,       GROUPING_ID(region,service_category) AS gidFROM lab06.InvoiceLineGROUP BY ROLLUP(region, service_category)ORDER BY gid, region, service_category;GOSELECT region, service_category, SUM(amount) AS revenue,       GROUPING_ID(region,service_category) AS gidFROM lab06.InvoiceLineGROUP BY CUBE(region, service_category)ORDER BY gid, region, service_category;GO

The CUBE adds category-only totals that the ROLLUP does not. If the report does not consume those rows, generating them is wasted work and can enlarge memory, sorting, serialization, and network costs.

4. Label subtotal rows from metadata, not from NULL guessing

sql · produce unambiguous report labels
USE ServiceHubLab;GOSELECT  CASE WHEN GROUPING(region)=1 THEN 'ALL REGIONS'       WHEN region IS NULL THEN 'UNKNOWN REGION'       ELSE region END AS region_label,  CASE WHEN GROUPING(service_category)=1 THEN 'ALL CATEGORIES'       ELSE service_category END AS category_label,  SUM(amount) AS revenueFROM lab06.InvoiceLineGROUP BY GROUPING SETS ((region,service_category),(region),())ORDER BY GROUPING_ID(region,service_category), region, service_category;GO

The display now preserves the business difference between “unknown source value” and “subtotal over all values.” Keep the raw grouping flags in machine-facing outputs when downstream consumers need to identify levels programmatically.

5. Advanced aggregation can request real workspace

Large grouping operations commonly use Stream Aggregate or Hash Match (Aggregate), and they may require sorting or hashing memory. CUBE/large grouping-set requests can multiply output groups and increase memory pressure. A spill is not proven by the presence of GROUPING SETS; it is observed in an actual plan/runtime warning and related diagnostics. Compare estimated versus actual rows, memory grant information, and tempdb spill evidence at representative scale.

sql · capture your local aggregation evidence
USE ServiceHubLab;GOSET STATISTICS IO ON;SELECT region, service_category, SUM(amount) AS revenueFROM lab06.InvoiceLineGROUP BY GROUPING SETS ((region,service_category),(region),(service_category),());SET STATISTICS IO OFF;GO

On six rows this is intentionally tiny. Do not infer production speed from the lab. Later chapters scale the ServiceHub dataset for plan, memory-grant, and tempdb analysis.

6. Production judgment

Repair pattern

Write the list of required output grains first. Use GROUPING SETS for an exact list, ROLLUP for a true hierarchy, and CUBE only when every dimensional combination is a required result. Carry GROUPING/GROUPING_ID so downstream code can distinguish subtotal placeholders from real NULL values.

The mandatory lab needs only SQL Server 2025 Developer/Express and compatibility level 170. No restart or special edition feature is required. Monitor result cardinality, memory grants, spills, repeated scans, and client serialization costs when moving to large dimensions. Rollback is dropping lab06.InvoiceLine. If a simpler set of separate queries is operationally clearer—for example, independently cached dashboards—clarity can outweigh syntactic compactness.

7. Compare semantic alternatives, not just shorter syntax

Advanced aggregation is most valuable when it makes a multi-grain requirement easier to audit. Before replacing several existing report queries, compare not only execution plans but also security predicates, filters, currency/time-zone rules, NULL policies, and snapshot timing. Separate UNION branches may have drifted because each branch acquired slightly different logic; GROUPING SETS can centralize those shared predicates. The reverse is also possible: independently refreshed dashboards might intentionally have different freshness or permissions, in which case forcing them into one giant aggregation can couple unrelated workloads.

For a production review, write acceptance rows for each grouping level before looking at the plan. Verify detail totals reconcile to the requested subtotals, verify real NULL source values remain distinguishable, and test that adding a new region/category does not change the meaning of existing grouping IDs. Then inspect actual cardinality, grant size, spills, parallelism, and elapsed time under representative scale. A shorter query is useful only when its semantics and operating model are also clearer.

Check your understanding

  1. What grain does GROUP BY region, service_category produce?
  2. How does GROUPING(region) distinguish a subtotal NULL from a real NULL region?
  3. What extra grouping does CUBE(region,category) produce beyond ROLLUP(region,category)?
  4. Why can CUBE become expensive with many dimensions?
  5. Does GROUPING SETS guarantee a single physical scan?
Review the answers

One row per distinct region/category combination.

It returns 1 when region is a subtotal placeholder and 0 when region is part of the grouping key, even if the source value itself is NULL.

It additionally produces category-only totals.

The number of grouping combinations grows combinatorially, increasing output and possible workspace requirements.

No. It expresses logical groupings; the optimizer chooses the physical implementation.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.