ROLLUP handles hierarchies stored as columns; a parent-child tree of varying depth needs a recursive CTE. BookNest groups its genres in mart.genre_tree (All books, then Fiction & Lit and Nonfiction, then Practical). The recursion pairs each leaf genre with every ancestor, so one join and one GROUP BY total every level:
WITH RECURSIVE up AS (
SELECT node AS genre, node AS ancestor, 0 AS hops FROM mart.genre_tree
WHERE node NOT IN (SELECT parent FROM mart.genre_tree WHERE parent IS NOT NULL)
UNION ALL
SELECT up.genre, t.parent, up.hops + 1
FROM up JOIN mart.genre_tree t ON t.node = up.ancestor
WHERE t.parent IS NOT NULL
)
SELECT up.ancestor AS node, sum(s.gross_amount) AS gross, count(DISTINCT up.genre) AS genres
FROM up JOIN mart.sales s ON s.genre = up.genre AND s.status <> 'cancelled'
GROUP BY up.ancestor
ORDER BY gross DESC;Output
node | gross | genres -----------------+------------+-------- All books | 3303427.30 | 6 Nonfiction | 2076598.85 | 4 Fiction & Lit | 1226828.45 | 2 ... Travel | 90131.25 | 1
The anchor member selects the leaves; the recursive member climbs one level per iteration until no parent remains, so a deeper tree needs no query change. If bad data makes a node its own ancestor, the recursion never ends: guard it with the CYCLE clause (PostgreSQL 14 1,289 ) or WHERE up.hops < 20. The tree's INSERT is in demos/ch04/sql/443-tree.sql.