Klassisches Reporting-Problem: du brauchst Summen pro Kategorie und Gesamtsummen und Sub-Summen pro Sub-Kategorie — alles in einem Resultat. Ohne GROUPING SETS würdest du das mit mehreren UNION ALL-Queries bauen. Mit GROUPING SETS, ROLLUP oder CUBE geht's in einer einzigen Query, in einem Pass über die Daten — und ist deutlich lesbarer.
Beispiel-Daten
CREATE TABLE orders (
id bigserial PRIMARY KEY,
country text NOT NULL,
category text NOT NULL,
total numeric(10, 2) NOT NULL
);
INSERT INTO orders (country, category, total) VALUES
('DE', 'Books', 99.95),
('DE', 'Books', 49.95),
('DE', 'Software',199.95),
('AT', 'Books', 29.95),
('AT', 'Software', 79.95),
('CH', 'Books', 19.95);GROUPING SETS — explizite Aggregations-Mengen
SELECT
country,
category,
sum(total) AS revenue
FROM orders
GROUP BY GROUPING SETS (
(country), -- Aggregat pro Land
(category), -- Aggregat pro Kategorie
() -- Gesamtsumme
)
ORDER BY country NULLS LAST, category NULLS LAST;Output:
country | category | revenue
---------+----------+----------
AT | NULL | 109.90
CH | NULL | 19.95
DE | NULL | 349.85
NULL | Books | 199.80
NULL | Software | 279.90
NULL | NULL | 479.70Drei verschiedene Aggregations-Ebenen in einer Query:
(country)— Summe pro Land (3 Zeilen,categoryist NULL)(category)— Summe pro Kategorie (2 Zeilen,countryist NULL)()— die leere Klammer = Gesamtsumme (1 Zeile, beide NULL)
Das NULL in den Spalten signalisiert: „diese Spalte war kein Gruppierungs-Kriterium für diese Zeile".
ROLLUP — Hierarchische Zwischensummen
ROLLUP ist eine Kurzschreibweise für hierarchische Aggregation: jede Ebene plus alle übergeordneten:
SELECT country, category, sum(total) AS revenue
FROM orders
GROUP BY ROLLUP(country, category)
ORDER BY country NULLS LAST, category NULLS LAST;Output:
country | category | revenue
---------+----------+----------
AT | Books | 29.95
AT | Software | 79.95
AT | NULL | 109.90 <- Zwischensumme AT
CH | Books | 19.95
CH | NULL | 19.95 <- Zwischensumme CH
DE | Books | 149.90
DE | Software | 199.95
DE | NULL | 349.85 <- Zwischensumme DE
NULL | NULL | 479.70 <- GesamtsummeROLLUP(a, b) ist äquivalent zu GROUPING SETS ((a, b), (a), ()). Der Name kommt von „auf-rollen": von Detail (Land + Kategorie) → Zwischensumme (Land) → Gesamt.
Klassisch in Finanz-Reports: pro Quartal/Monat/Tag mit Zwischensummen pro Quartal/Monat und Gesamtsumme.
CUBE — Alle Kombinationen
CUBE(a, b) macht die Kreuztabelle: jede mögliche Teilmenge der Spalten:
SELECT country, category, sum(total) AS revenue
FROM orders
GROUP BY CUBE(country, category)
ORDER BY country NULLS LAST, category NULLS LAST;Output:
country | category | revenue
---------+----------+----------
AT | Books | 29.95
AT | Software | 79.95
AT | NULL | 109.90
CH | Books | 19.95
CH | NULL | 19.95
DE | Books | 149.90
DE | Software | 199.95
DE | NULL | 349.85
NULL | Books | 199.80 <- Gesamt pro Kategorie
NULL | Software | 279.90
NULL | NULL | 479.70CUBE(a, b) = GROUPING SETS ((a, b), (a), (b), ()). Jede mögliche Kombination — nützlich für Pivot-Reports, in denen man von jeder Seite zugreifen können will.
Bei N Spalten produziert CUBE 2^N Aggregations-Mengen. Bei 5 Spalten sind das 32 — schnell viel. Sparsam einsetzen.
GROUPING()-Funktion: NULL unterscheiden
Problem: in den obigen Outputs sind die NULL-Werte gemischt — manchmal bedeutet NULL „diese Spalte war Aggregations-Ebene", manchmal echtes NULL aus der Tabelle. Wie unterscheiden?
SELECT
country,
category,
GROUPING(country) AS country_grouped,
GROUPING(category) AS category_grouped,
sum(total) AS revenue
FROM orders
GROUP BY ROLLUP(country, category)
ORDER BY country_grouped, country, category_grouped, category;GROUPING(col) liefert 1, wenn die Spalte in dieser Zeile aggregiert wurde (also „NULL bedeutet hier 'alles aggregiert'"); 0, wenn die Spalte konkret war.
Praktisch zum Markieren von Zwischensummen-Zeilen im UI:
SELECT
CASE
WHEN GROUPING(country) = 1 THEN 'TOTAL'
ELSE country
END AS country_label,
CASE
WHEN GROUPING(category) = 1 THEN 'subtotal'
ELSE category
END AS category_label,
sum(total) AS revenue
FROM orders
GROUP BY ROLLUP(country, category)
ORDER BY country_label, category_label;Damit kannst du deine Reports so formatieren, dass die Zwischensummen-Zeilen klar erkennbar sind.
Wann was?
| Pattern | Use-Case |
|---|---|
GROUP BY a, b | klassische Aggregation, eine Ebene |
GROUP BY GROUPING SETS (...) | mehrere konkrete Aggregations-Mengen — flexibel |
GROUP BY ROLLUP(a, b) | hierarchische Reports mit Zwischensummen |
GROUP BY CUBE(a, b) | Pivot/Kreuztabelle, alle Kombinationen |
Faustregel: für „Total am Ende" → ROLLUP. Für „Detail + bestimmte Aggregate" → GROUPING SETS (am flexibelsten). Für „Pivot in alle Richtungen" → CUBE.
Besonderheiten
Eine Query, ein Pass — performance-mäßig ein Gewinn.
Ohne GROUPING SETS müsstest du mehrere Queries mit UNION ALL schreiben — jede liest die Tabelle separat. Mit GROUPING SETS macht Postgres alles in einem Scan. Bei großen Tabellen merklich schneller, vor allem mit Aggregat-Plänen wie HashAggregate.
NULL-Bedeutung wird zweideutig.
In den Resultaten kann NULL „Aggregations-Ebene" oder „echtes NULL aus Daten" bedeuten. Bei Tabellen, die echte NULLs haben, ist das verwirrend. Lösung: GROUPING(col) als zusätzliche Spalte oder COALESCE(col, '<all>') im SELECT, um die zwei Fälle zu unterscheiden.
CUBE wird schnell groß.
Für N Spalten produziert CUBE 2^N Aggregations-Mengen. Bei 6 Spalten = 64 — und entsprechend viele Resultat-Zeilen. Im Reporting-Workflow okay, im Frontend selten direkt zeigbar. Lieber gezielt GROUPING SETS mit den 4-5 wirklich gebrauchten Kombinationen.
Verschachtelte Klauseln möglich.
GROUPING SETS ((country, category), ROLLUP(country)) mischt verschiedene Konstrukte. Mächtig, aber selten nötig — meistens reicht eine der drei Formen pur. In komplexen BI-Reports kann's helfen.
ORDER BY mit NULLS LAST ist meist gewollt.
Aggregations-Zeilen haben NULL in den nicht-gruppierten Spalten. Standard-ORDER BY setzt NULLs ans Ende bei ASC. Wer Zwischensummen UNTER den Detail-Zeilen will, schreibt ORDER BY country NULLS LAST explizit — sonst landen sie je nach DB-Default oben oder unten.
Cross-DB-Kompatibilität: SQL-Standard.
GROUPING SETS, ROLLUP, CUBE sind alle SQL:1999/2003-Standard. Funktionieren auf Postgres, Oracle, SQL Server, DB2 — nicht aber auf MySQL (das hat nur WITH ROLLUP-Suffix als eingeschränkte Variante). Bei plattformübergreifenden Code-Bases prüfen.