- 1 Section
- 10 Lessons
- unbegrenzt
- SQL Fortgeschritten10
- 1.1Alle JOIN-Typen: RIGHT, FULL OUTER, CROSS, SELF
- 1.2Unterabfragen (Subqueries)
- 1.3Common Table Expressions (CTEs)
- 1.4Window Functions: ROW_NUMBER, RANK, PARTITION BY
- 1.5Views erstellen und nutzen
- 1.6Stored Procedures und Funktionen
- 1.7Trigger
- 1.8Datenbankindizes und Query-Optimierung
- 1.9Datenbankreporte und komplexe Auswertungen
- 1.10SQL-Fortgeschrittenenaufgaben
Datenbankreporte und komplexe Auswertungen
In den vorigen Lektionen hast du alle wichtigen Bausteine kennengelernt: JOINs, Subqueries, CTEs, Window Functions und Views. Diese Lektion zeigt, wie du diese Werkzeuge zu echten Datenbankreporten kombinierst – also Auswertungen, wie sie in Unternehmen täglich gebraucht werden.
Wir behandeln klassische Report-Muster: Pivot-Tabellen (Zeilen zu Spalten umdrehen), Rollup-Summen (Zwischen- und Gesamtsummen in einer Abfrage), Top-N-Auswertungen, ABC-Analyse und Datumsreihen-Reports, bei denen auch Tage ohne Bestellungen erscheinen müssen.
1) Was ist ein Datenbankreport?
Ein Report ist eine aggregierte Auswertung, die typischerweise:
- Daten aus mehreren Tabellen zusammenführt
- Gruppiert und mit Kennzahlen versieht (Summe, Durchschnitt, Anzahl, Anteil)
- Eine strukturierte Form hat (oft mit Zwischensummen, Sortierung, Filter)
- Periodisch oder auf Anfrage erstellt wird
Reports sind selten reine SQL-Übungen. Ein guter Report beantwortet eine konkrete Fachfrage: „Welche Produkte machen 80 % unseres Umsatzes aus?" oder „Wie hat sich der Kundenstamm pro Monat entwickelt?". Genau solche Fragen schauen wir uns hier an.
2) Beispiel-Daten
Für diese Lektion erweitern wir das vertraute Schema:
| id | bezeichnung | kategorie |
|---|---|---|
| 1 | Laptop | Hardware |
| 2 | Maus | Hardware |
| 3 | Office-Lizenz | Software |
| 4 | Antivirus | Software |
| id | artikel | region | summe | monat |
|---|---|---|---|---|
| 1 | 1 | Nord | 800 | 01 |
| 2 | 1 | Süd | 900 | 01 |
| 3 | 2 | Nord | 30 | 01 |
| 4 | 2 | Süd | 50 | 02 |
| 5 | 3 | Nord | 500 | 02 |
| 6 | 3 | Süd | 400 | 02 |
| 7 | 4 | Nord | 200 | 03 |
| 8 | 1 | Nord | 850 | 03 |
3) Klassiker: Umsatz pro Kategorie und Monat
Eine der häufigsten Report-Fragen: Umsatz je Kategorie und Monat. Mit normalem GROUP BY:
SELECT a.kategorie, b.monat, SUM(b.summe) AS umsatz FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.kategorie, b.monat ORDER BY a.kategorie, b.monat;
| kategorie | monat | umsatz |
|---|---|---|
| Hardware | 01 | 1730 |
| Hardware | 02 | 50 |
| Hardware | 03 | 850 |
| Software | 02 | 900 |
| Software | 03 | 200 |
4) Pivot-Tabelle mit CASE
Standard-SQL kennt kein eingebautes „PIVOT" (außer in SQL Server und Oracle mit besonderer Syntax). In MySQL/PostgreSQL erreicht man das gleiche Ergebnis mit CASE in der SELECT-Liste:
SELECT a.kategorie, SUM(CASE WHEN b.monat = '01' THEN b.summe ELSE 0 END) AS jan, SUM(CASE WHEN b.monat = '02' THEN b.summe ELSE 0 END) AS feb, SUM(CASE WHEN b.monat = '03' THEN b.summe ELSE 0 END) AS mar, SUM(b.summe) AS gesamt FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.kategorie;
| kategorie | jan | feb | mar | gesamt |
|---|---|---|---|---|
| Hardware | 1730 | 50 | 850 | 2630 |
| Software | 0 | 900 | 200 | 1100 |
CASE, der nur die passenden Werte aufaddiert. Wenn der Monat nicht passt, kommt 0. So entstehen die Pivot-Spalten. Funktioniert in jeder modernen DB ohne Sondersyntax.5) WITH ROLLUP – Zwischen- und Gesamtsummen
Im Pivot oben standen Gesamtsummen als zusätzliche Spalte. Häufig will man auch eine Gesamtzeile. Dafür gibt es WITH ROLLUP (MySQL, Oracle) bzw. GROUPING SETS (Standard-SQL):
SELECT COALESCE(a.kategorie, 'GESAMT') AS kategorie, COALESCE(b.region, '(alle)') AS region, SUM(b.summe) AS umsatz FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.kategorie, b.region WITH ROLLUP;
| kategorie | region | umsatz |
|---|---|---|
| Hardware | Nord | 1680 |
| Hardware | Süd | 950 |
| Hardware | (alle) | 2630 |
| Software | Nord | 200 |
| Software | Süd | 900 |
| Software | (alle) | 1100 |
| GESAMT | (alle) | 3730 |
NULL (über COALESCE in (alle)/GESAMT umbenannt) sind die Zwischensummen. Sehr nützlich für Reports, in denen Detail- und Aggregatzeilen zusammenstehen sollen.6) GROUPING SETS für gezielte Aggregationen
Wenn dir WITH ROLLUP zu viel oder zu wenig aggregiert, kannst du mit GROUPING SETS (in MySQL ab Version 8.0 verfügbar, in PostgreSQL und SQL Server ebenfalls) genau die gewünschten Gruppierungen erzeugen:
SELECT a.kategorie, b.region, SUM(b.summe) AS umsatz FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY GROUPING SETS ( (a.kategorie, b.region), -- Detail (a.kategorie), -- Kategorie-Summe () -- Gesamt );
Damit bekommst du gezielt drei Ebenen: Detail, Zwischensummen pro Kategorie und Gesamtsumme – ohne überflüssige Kombinationen. Praktisch für mehrdimensionale Reports.
7) Top-N pro Gruppe
Eine klassische Frage: „Die zwei umsatzstärksten Artikel pro Kategorie". Mit Window Functions und einer CTE elegant lösbar:
WITH umsatz_je_artikel AS ( SELECT a.kategorie, a.bezeichnung, SUM(b.summe) AS umsatz FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.id, a.kategorie, a.bezeichnung ), ranked AS ( SELECT kategorie, bezeichnung, umsatz, RANK() OVER (PARTITION BY kategorie ORDER BY umsatz DESC) AS rang FROM umsatz_je_artikel ) SELECT kategorie, bezeichnung, umsatz, rang FROM ranked WHERE rang <= 2 ORDER BY kategorie, rang;
| kategorie | bezeichnung | umsatz | rang |
|---|---|---|---|
| Hardware | Laptop | 2550 | 1 |
| Hardware | Maus | 80 | 2 |
| Software | Office-Lizenz | 900 | 1 |
| Software | Antivirus | 200 | 2 |
8) ABC-Analyse
Eine ABC-Analyse klassifiziert Daten nach ihrer Bedeutung: A-Artikel machen typischerweise die Top 80 % vom Umsatz aus, B-Artikel die nächsten 15 %, C-Artikel den Rest. So erkennst du, auf welche wenigen Artikel sich der Großteil des Umsatzes konzentriert – ein Klassiker in der Materialwirtschaft.
WITH umsatz AS ( SELECT a.bezeichnung, SUM(b.summe) AS u FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.id, a.bezeichnung ), mit_anteil AS ( SELECT bezeichnung, u, SUM(u) OVER (ORDER BY u DESC) / SUM(u) OVER () AS kum_anteil FROM umsatz ) SELECT bezeichnung, u, ROUND(kum_anteil * 100, 1) AS kum_pct, CASE WHEN kum_anteil <= 0.80 THEN 'A' WHEN kum_anteil <= 0.95 THEN 'B' ELSE 'C' END AS abc FROM mit_anteil ORDER BY u DESC;
| bezeichnung | u | kum_pct | abc |
|---|---|---|---|
| Laptop | 2550 | 68,4 | A |
| Office-Lizenz | 900 | 92,5 | A |
| Antivirus | 200 | 97,9 | B |
| Maus | 80 | 100,0 | C |
SUM OVER ORDER BY), 3) Klassifikation per CASE. Mit den Schwellen 80 %/95 % kann je nach Branche variiert werden.9) Anteil am Gesamtumsatz
Häufige Frage: „Wie viel Prozent macht jeder Artikel am Gesamtumsatz aus?" Wieder mit einer Window Function, diesmal ohne ORDER BY:
SELECT a.bezeichnung, SUM(b.summe) AS umsatz, ROUND( 100.0 * SUM(b.summe) / SUM(SUM(b.summe)) OVER (), 1 ) AS anteil_pct FROM bestellungen b JOIN artikel a ON b.artikel = a.id GROUP BY a.id, a.bezeichnung ORDER BY umsatz DESC;
Der doppelte SUM wirkt verwirrend: der innere aggregiert pro Artikel, der äußere mit OVER () bildet die Gesamtsumme. Genau das ist der Trick – Window Functions dürfen auf Aggregaten aus dem GROUP BY aufbauen.
10) Datumsreihe mit Lücken füllen
Ein häufiges Report-Problem: ein Tagesumsatz-Bericht. Was, wenn an manchen Tagen gar keine Bestellungen liefen? Eine naive Abfrage liefert nur Tage mit Bestellungen – die Lücken bleiben unsichtbar:
SELECT bestelldatum, SUM(summe) FROM bestellungen GROUP BY bestelldatum; -- Tage ohne Bestellungen fehlen komplett
Die Lösung: eine Datumsreihe per rekursiver CTE erzeugen und per LEFT JOIN verbinden:
WITH RECURSIVE tage AS ( SELECT DATE('2026-01-01') AS tag UNION ALL SELECT tag + INTERVAL 1 DAY FROM tage WHERE tag < '2026-01-31' ) SELECT t.tag, COALESCE(SUM(b.summe), 0) AS tagesumsatz FROM tage t LEFT JOIN bestellungen b ON b.bestelldatum = t.tag GROUP BY t.tag ORDER BY t.tag;
Jetzt erscheint jeder Tag im Januar – mit Umsatz, oder mit 0 wenn keine Bestellung lief. Genau das, was ein Bericht braucht.
11) Vergleich Vorperiode
Viele Reports wollen aktuelle Werte mit denen aus einer früheren Periode vergleichen. LAG hilft dabei, den Vormonatswert in derselben Zeile bereitzustellen:
WITH monatsumsatz AS ( SELECT monat, SUM(summe) AS umsatz FROM bestellungen GROUP BY monat ) SELECT monat, umsatz, LAG(umsatz) OVER (ORDER BY monat) AS vormonat, umsatz - LAG(umsatz) OVER (ORDER BY monat) AS differenz FROM monatsumsatz ORDER BY monat;
| monat | umsatz | vormonat | differenz |
|---|---|---|---|
| 01 | 1730 | NULL | NULL |
| 02 | 950 | 1730 | −780 |
| 03 | 1050 | 950 | +100 |
12) Kohorten-Analyse
Eine Kohorten-Analyse gruppiert Kunden nach dem Zeitpunkt ihres ersten Kontakts und beobachtet, wie sich diese Gruppen über die Zeit entwickeln – ein wichtiges Werkzeug im Marketing. Vereinfachtes Schema:
WITH erst_bestellung AS ( SELECT kunde_id, MIN(monat) AS kohorte FROM bestellungen GROUP BY kunde_id ) SELECT e.kohorte, b.monat, COUNT(DISTINCT b.kunde_id) AS aktive_kunden FROM erst_bestellung e JOIN bestellungen b ON e.kunde_id = b.kunde_id GROUP BY e.kohorte, b.monat ORDER BY e.kohorte, b.monat;
Pro „Kohorte" (= Monat der ersten Bestellung) zeigt der Report, wie viele Kunden in jedem späteren Monat noch aktiv waren. Daraus lassen sich Bindungsquoten ableiten („50 % der Januar-Kunden haben auch im März bestellt").
13) Reports operationalisieren
Eine Report-Abfrage ist erst dann ein „Report", wenn sie regelmäßig läuft und das Ergebnis irgendwo landet. Typische Operationalisierungen:
14) Performance-Tipps für Reports
Reports laufen oft über große Datenmengen. Ein paar Hinweise:
- Indizes auf Filterspalten sind Pflicht – siehe L8
- Reports getrennt vom OLTP-System fahren: schwere Aggregat-Abfragen können das produktive System bremsen. Lösungen: Read-Replica, Data Warehouse, Materialized View
- Vorberechnung: tägliche Aggregate in einer separaten Tabelle, statt jedes Mal aus den Detaildaten zu rechnen
- Filter vor JOINs: Datensätze früh reduzieren, nicht erst nach mehreren JOINs
- Zeitraum begrenzen:
WHERE datum >= … AND datum < …statt offener Bereiche EXPLAINeinsetzen, bevor ein Report in Produktion geht
15) Daten-Qualitäts-Checks im Report
Bevor Zahlen rausgehen, lohnt ein paar Plausibilitäts-Prüfungen. Klassische Anti-Muster, die du erkennen solltest:
- Mehrfach gezählte Zeilen: ein JOIN, der unbeabsichtigt Multiplikationen erzeugt (1 Kunde × 3 Bestellungen × 2 Adressen = 6 Zeilen). Symptom: Summen sind plötzlich viel zu hoch
- NULL-Werte ignoriert:
SUMignoriert NULL still – wenn das nicht beabsichtigt ist, mitCOALESCEauf 0 setzen - Falsche Datumsfilter:
BETWEEN '2026-01-01' AND '2026-01-31'schließt den 31.01 mit ein, aber nicht den 31.01 23:59 Uhr (wenn datetime). Besser:>= '2026-01-01' AND < '2026-02-01' - Stichtag vs. Periode: „Aktive Kunden" zum Stichtag ist etwas anderes als „aktive Kunden im Monat"
- Soft-Deletes vergessen: gelöscht markierte Daten sollten meist nicht in Reports auftauchen
Zusammenfassung
Datenbankreporte kombinieren die fortgeschrittenen SQL-Werkzeuge zu nutzbaren Auswertungen. Die wichtigsten Muster: Pivot-Tabellen via CASE in der SELECT-Liste, Zwischen- und Gesamtsummen mit WITH ROLLUP oder GROUPING SETS, Top-N pro Gruppe mit Window Functions in einer CTE, ABC-Analyse über kumulative Summen, Lücken in Datumsreihen per rekursiver CTE + LEFT JOIN, Vorperioden-Vergleich mit LAG. Für die Produktion gilt: Reports laufen am besten gegen Read-Replicas oder Data Warehouses, kritische Aggregate werden in Materialized Views oder Vorberechnungs-Tabellen gehalten, und vor dem Rollout immer mit EXPLAIN prüfen.
