Chapter 06 · Advanced SQL: Analytic Functions, MODEL, PIVOT, MATCH_RECOGNIZE, and SQL Macros

PIVOT/UNPIVOT, GROUPING SETS, CUBE, ROLLUP, and Advanced Aggregation

Reshape and summarize ServiceHub data with PIVOT, UNPIVOT, GROUPING SETS, CUBE, and ROLLUP while distinguishing subtotal NULLs from real data NULLs.

Intermediate → Advanced110–130 minutesPivot + multi-level aggregation labPIVOT/UNPIVOT + GROUPING SETS/CUBE/ROLLUPFree · no paid packs/options requiredLast reviewed: August 2026

Learning outcomes

ServiceHub has row-oriented monthly revenue but reporting consumers want quarter columns, subtotal rows, and a grand total. A naive solution runs many separate queries and labels every null as “TOTAL,” accidentally merging genuine unknown dimensions with generated subtotal nulls. Oracle’s reshaping and grouping extensions can express the result in one statement—if their semantics are understood.

01

Use PIVOT to rotate category values into aggregate columns and explain its implicit grouping.

02

Use UNPIVOT to rotate columns back into rows and control NULL inclusion.

03

Build targeted subtotal levels with GROUPING SETS and hierarchical subtotals with ROLLUP/CUBE.

04

Use GROUPING and GROUPING_ID to distinguish generated subtotal NULLs from stored NULL values.

05

Compare advanced aggregation with simpler conditional aggregation and choose the clearest form.

Prerequisite connection

Chapter 05 set operators and Lesson 1 analytics established row-set and aggregation semantics. PIVOT is not just formatting: it aggregates while rotating.

Lab/version/licensing baseline

Mandatory labs target a disposable ServiceHub schema in Oracle AI Database Free 26ai. This chapter was reviewed against July 2026 RU 23.26.3, SQL Developer 26.2, and SQLcl 26.2.1. No Diagnostics/Tuning Pack, RAC, Data Guard, Exadata, GoldenGate, or paid cloud service is required. Free remains capped at two processing cores, 2 GB RAM for SGA+PGA, and 12 GB user data and receives no Oracle patches or Support SRs, so it is a learning baseline rather than a production recommendation.

1. PIVOT performs aggregation plus rotation

PIVOT implicitly groups by source columns not referenced by the pivot clause, applies the requested aggregate, and emits columns for values listed in the IN list. Use a narrow source subquery so accidental extra columns do not silently become grouping keys.

sql · quarter revenue pivot
SELECT *FROM (  SELECT region_code, quarter_code, amount  FROM servicehub_sql_revenue)PIVOT (  SUM(amount) AS revenue  FOR quarter_code IN (    'Q1' AS q1,    'Q2' AS q2,    'Q3' AS q3,    'Q4' AS q4  ))ORDER BY region_code;

Static non-XML PIVOT requires known pivot values. If categories are truly dynamic, decide whether the consumer should receive rows, generated SQL under strict identifier validation, or the XML form; do not concatenate untrusted labels into SQL text.

2. UNPIVOT rotates columns to rows—and excludes NULLs by default

sql · default and explicit null handling
SELECT region_code, quarter_code, amountFROM servicehub_sql_quarter_wideUNPIVOT (  amount FOR quarter_code IN (    q1 AS 'Q1', q2 AS 'Q2', q3 AS 'Q3', q4 AS 'Q4'  ))ORDER BY region_code, quarter_code;SELECT region_code, quarter_code, amountFROM servicehub_sql_quarter_wideUNPIVOT INCLUDE NULLS (  amount FOR quarter_code IN (    q1 AS 'Q1', q2 AS 'Q2', q3 AS 'Q3', q4 AS 'Q4'  ))ORDER BY region_code, quarter_code;

The first query omits null-valued source cells. That is data-shaping behavior, not proof that those columns never existed. Also ensure unpivoted value columns are in a compatible data-type group.

3. GROUPING SETS asks only for the subtotal levels you need

sql · detail, region subtotal, and grand total
SELECT region_code,       service_type,       SUM(amount) AS amount,       GROUPING(region_code) AS g_region,       GROUPING(service_type) AS g_type,       GROUPING_ID(region_code, service_type) AS grouping_idFROM servicehub_sql_revenueGROUP BY GROUPING SETS (  (region_code, service_type),  (region_code),  ())ORDER BY GROUPING_ID(region_code, service_type), region_code, service_type;

GROUPING SETS is often clearer than computing a full cube when only selected subtotal levels matter. Oracle conceptually combines the requested grouping results, and duplicate-looking rows can be legitimate if grouping sets overlap.

4. Deliberately wrong: NVL cannot distinguish a real NULL from a subtotal NULL

sql · wrong label
SELECT NVL(region_code, 'ALL REGIONS') AS region_label,       SUM(amount) AS amountFROM servicehub_sql_revenueGROUP BY ROLLUP(region_code);

If a stored row legitimately has region_code IS NULL, this labels it exactly like the generated grand-total row. Use GROUPING(region_code), which returns 1 for a null generated by a grouping extension and 0 for a stored null.

sql · repair with GROUPING
SELECT CASE         WHEN GROUPING(region_code) = 1 THEN 'ALL REGIONS'         WHEN region_code IS NULL THEN 'UNKNOWN REGION'         ELSE region_code       END AS region_label,       SUM(amount) AS amountFROM servicehub_sql_revenueGROUP BY ROLLUP(region_code)ORDER BY GROUPING(region_code), region_code;

5. ROLLUP and CUBE have different combinatorics

ROLLUP(a,b,c) generates a hierarchy of grouping prefixes: detail, then progressively higher subtotals, then grand total. CUBE(a,b,c) generates every combination of those dimensions. That can be exactly right for multidimensional analysis—or excessive work and output if the consumer needs only a few levels.

sql · compare focused rollup and full cube
SELECT region_code, service_type, SUM(amount) AS amountFROM servicehub_sql_revenueGROUP BY ROLLUP(region_code, service_type);SELECT region_code, service_type, SUM(amount) AS amountFROM servicehub_sql_revenueGROUP BY CUBE(region_code, service_type);

Do not choose CUBE because it sounds more advanced. Choose it because every dimension combination has a defined consumer.

6. Conditional aggregation can be clearer than PIVOT

sql · portable alternative for a small fixed category set
SELECT region_code,       SUM(CASE WHEN quarter_code='Q1' THEN amount END) AS q1,       SUM(CASE WHEN quarter_code='Q2' THEN amount END) AS q2,       SUM(CASE WHEN quarter_code='Q3' THEN amount END) AS q3,       SUM(CASE WHEN quarter_code='Q4' THEN amount END) AS q4FROM servicehub_sql_revenueGROUP BY region_codeORDER BY region_code;

Compare readability, output-column contracts, and measured plans. A specialized operator is not automatically faster than an equivalent simpler formulation.

7. Hands-on lab

sql · setup
DROP TABLE servicehub_sql_quarter_wide IF EXISTS PURGE;DROP TABLE servicehub_sql_revenue IF EXISTS PURGE;CREATE TABLE servicehub_sql_revenue (  region_code VARCHAR2(20),  service_type VARCHAR2(20) NOT NULL,  quarter_code VARCHAR2(2) NOT NULL,  amount NUMBER(12,2) NOT NULL);INSERT INTO servicehub_sql_revenue VALUES ('NORTH','INSTALL','Q1',1000);INSERT INTO servicehub_sql_revenue VALUES ('NORTH','REPAIR','Q1',600);INSERT INTO servicehub_sql_revenue VALUES ('NORTH','INSTALL','Q2',1200);INSERT INTO servicehub_sql_revenue VALUES ('SOUTH','REPAIR','Q1',700);INSERT INTO servicehub_sql_revenue VALUES (NULL,'REPAIR','Q2',150);CREATE TABLE servicehub_sql_quarter_wide (  region_code VARCHAR2(20), q1 NUMBER, q2 NUMBER, q3 NUMBER, q4 NUMBER);INSERT INTO servicehub_sql_quarter_wide VALUES ('NORTH',1600,1200,NULL,NULL);INSERT INTO servicehub_sql_quarter_wide VALUES ('SOUTH',700,NULL,NULL,NULL);COMMIT;
sql · plan observation
EXPLAIN PLAN SET STATEMENT_ID='SH06L2'FORSELECT region_code, service_type, SUM(amount)FROM servicehub_sql_revenueGROUP BY GROUPING SETS ((region_code,service_type),(region_code),());SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL,'SH06L2','BASIC'));-- Estimated-plan evidence only; exact operators are not a semantic contract.
sql · cleanup
DROP TABLE servicehub_sql_quarter_wide PURGE;DROP TABLE servicehub_sql_revenue PURGE;

8. Production judgment

Use PIVOT when a stable cross-tab shape improves clarity; use rows or conditional aggregation when it does not. Use GROUPING SETS to request explicit subtotal levels, ROLLUP for natural hierarchies, and CUBE only when all combinations are valuable. Generated subtotal nulls must be identified with GROUPING/GROUPING_ID, not text substitution. No paid pack, restart, or COMPATIBLE change is required for the mandatory examples.

Check your understanding

  1. What hidden grouping does PIVOT perform?
  2. What happens to NULL source cells in UNPIVOT by default?
  3. Why is NVL(region_code,'ALL') unsafe in a ROLLUP report?
  4. When is GROUPING SETS preferable to CUBE?
  5. Does PIVOT automatically mean a cheaper plan than conditional aggregation?
Review the answers

PIVOT implicitly groups by source columns not referenced inside the pivot clause, then aggregates and rotates.

They are excluded unless INCLUDE NULLS is specified.

A real stored NULL becomes indistinguishable from a generated subtotal NULL. GROUPING identifies the generated case.

When only selected subtotal combinations are needed rather than every combination.

No. Compare semantics, readability, and measured execution evidence.

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.