9 Advanced Aggregations
9.1 Module Introduction
In this module, we will discuss some powerful ways to calculate aggregates across groups. You will do this by using ROLLUP, CUBE, and the GROUPING function.
Reference: Oracle SQL by Example, Chapter 6 · Oracle SQL Language Reference, Aggregate and Group Functions
9.2 Explanation
9.2.1 Motivation: Queries with Subtotals
GROUP BY gets you a single level of summary — one row per group. But reporting often calls for subtotals and grand totals alongside the detail, the way a spreadsheet pivot table shows a total at the bottom of each group and a grand total at the very end.
You could get there by writing several separate queries and UNION ALL-ing them together, but Oracle gives you three extensions to GROUP BY that do this in a single query:
ROLLUPCUBE- The
GROUPINGfunction
9.2.2 ROLLUP: Subtotals Up a Hierarchy
ROLLUP produces subtotal rows as it “rolls up” through the columns you list, in order, finishing with a grand total row.
Think of it as, “totals per innermost group, then totals per next group up, then one overall total.”
SELECT
section_id,
grade_type_code,
AVG(numeric_grade) AS avg_grade
FROM grade
GROUP BY
ROLLUP(section_id, grade_type_code)
ORDER BY
section_id,
grade_type_code;This returns:
- One row per
(section_id, grade_type_code)combination - A subtotal row per
section_id(withgrade_type_codeasNULL) - A single grand-total row (with both columns
NULL)
The column order inside ROLLUP() matters! It defines the hierarchy being subtotaled, from left to right.
You can also do partial ROLLUP’s. In the example below, we only roll up the instructor ID and the section ID. There won’t be a grand total for capacity across all courses.
SELECT
COURSE_NO,
INSTRUCTOR_ID,
SECTION_ID,
SUM(CAPACITY)
FROM
SECTION
GROUP BY
COURSE_NO,
ROLLUP(
INSTRUCTOR_ID,
SECTION_ID
)
ORDER BY
COURSE_NO,
INSTRUCTOR_ID,
SECTION_ID9.2.3 CUBE: Every Combination of Subtotals
CUBE goes further: it produces subtotals for every possible combination of the grouping columns, not just a strict hierarchy.
SELECT
section_id,
grade_type_code,
AVG(numeric_grade) AS avg_grade
FROM grade
GROUP BY
CUBE(section_id, grade_type_code)
ORDER BY
section_id,
grade_type_code;With two columns, CUBE gives you:
- Detail rows (subtotals for specific values across all groups)
- Subtotals by
section_idalone - Subtotals by
grade_type_codealone - The grand total
You get four levels of summary instead of ROLLUP’s three.
As you add more columns,
CUBEgrows quickly (\(2^n\) combinations for \(n\) columns), so it’s best reserved for a handful of grouping columns at a time.
9.2.4 Telling Subtotal Rows Apart with GROUPING
Both ROLLUP and CUBE mark subtotal rows by putting NULL in the columns that were “rolled up.” But what if your data might actually contain NULL values?
In other words, how do you tell a real
NULLapart from a subtotal row?
The GROUPING() function solves this. For a given column, it returns:
1on subtotal/grand-total rows (where that column was aggregated away)0on normal detail rows.
SELECT section_id,
grade_type_code,
AVG(numeric_grade) AS avg_grade,
GROUPING(section_id) AS is_section_subtotal,
GROUPING(grade_type_code) AS is_type_subtotal
FROM grade
GROUP BY
ROLLUP(section_id, grade_type_code);This is especially handy paired with DECODE or CASE. Use it to relabel subtotal rows with something more readable than NULL. For example:
SELECT
DECODE(GROUPING(grade_type_code), 1, 'All Types', grade_type_code)
...9.3 Exercises
Using ENROLLMENT, SECTION, and COURSE:
- Write a query that shows the number of enrollments per
section_idwithin eachcourse_no, with a subtotal per course and a grand total. Which clause do you need? - Modify your query to replace the
NULLplaceholders in subtotal rows with the label'Course Total'and'Grand Total', usingGROUPING. - Suppose a manager wants enrollment counts broken out by
course_noalone, bysection_idalone, and by both together — all in one result set. Which clause fits best, and why wouldn’tROLLUPbe sufficient here? - Pivot the student count by
STATE(fromZIPCODE) into separate columns for GA and AL.
SELECT *
FROM (
SELECT z.state, s.student_id
FROM student s
JOIN zipcode z ON s.zip = z.zip
)
--- Complete the PIVOT operation here9.4 Q&A
- Why might a report writer prefer
ROLLUPoverCUBEeven when both would technically answer the question? - If a
GROUP BY ROLLUP(a, b)query returns a row where bothaandbareNULL, what does that row represent? - What happens to query cost/performance as you add more columns to a
CUBE? Why?
9.5 Additional Resources
9.5.1 Further Reading
- Oracle SQL Language Reference, ROLLUP, CUBE, and GROUPING
- Oracle SQL by Example (Rischert 2009), Chapter 6
9.6 Answers
GROUP BY ROLLUP(c.course_no, s.section_id)rolls up section-level detail into course-level subtotals and then a grand total.- Wrap each grouped column:
DECODE(GROUPING(s.section_id), 1, 'Course Total', TO_CHAR(s.section_id)), and similarly forcourse_nowith'Grand Total'where bothGROUPINGvalues equal 1. CUBE(c.course_no, s.section_id)— it produces subtotals for course alone, section alone, and both together, whereasROLLUPonly produces one direction of subtotal (course, then course+section), not section alone.
SELECT *
FROM (
SELECT z.state, s.student_id
FROM student s
JOIN zipcode z ON s.zip = z.zip
)
PIVOT (
COUNT(student_id) FOR state IN ('GA' AS GA, 'AL' AS AL)
);