Common Table Expressions in SQL: Abfragen mit WITH strukturieren
Verschachtelte Subqueries machen SQL schnell unlesbar. Mit Common Table Expressions (WITH) strukturierst du komplexe Abfragen sauber – inklusive rekursiver CTEs für Hierarchien.
Sobald deine SQL-Abfragen komplexer werden, stapeln sich schnell verschachtelte Subqueries: eine Abfrage in der Abfrage in der Abfrage. Das funktioniert zwar, ist aber schwer zu lesen und noch schwerer zu warten. Genau hier kommen Common Table Expressions – kurz CTEs – ins Spiel. Mit dem Schlüsselwort WITH gibst du einer Zwischenabfrage einen Namen und verwendest sie danach wie eine ganz normale Tabelle. In diesem Beitrag schauen wir uns an, wie CTEs funktionieren, warum sie deinen Code lesbarer machen und wie du mit rekursiven CTEs sogar Hierarchien abfragst.
Was ist eine Common Table Expression?
Eine CTE ist eine benannte, temporäre Ergebnismenge, die nur für die Dauer einer einzigen Abfrage existiert. Du definierst sie mit WITH, gefolgt von einem Namen und der eigentlichen Abfrage in Klammern. Danach kannst du diesen Namen im SELECT so nutzen, als wäre er eine echte Tabelle.
WITH aktive_kunden AS (
SELECT id, name, stadt
FROM kunden
WHERE letzter_login >= '2026-01-01'
)
SELECT name, stadt
FROM aktive_kunden
WHERE stadt = 'Berlin';Die CTE aktive_kunden filtert zunächst alle Kunden, die sich seit Jahresbeginn eingeloggt haben. In der Hauptabfrage greifst du auf dieses Zwischenergebnis zu und schränkst weiter auf Berlin ein. Der große Vorteil: Die Abfrage liest sich von oben nach unten wie eine Geschichte, statt von innen nach außen wie bei einer Subquery.
CTE statt verschachtelter Subquery
Vergleichen wir beide Ansätze direkt. Angenommen, du möchtest alle Produkte finden, deren Preis über dem Durchschnitt liegt. Mit einer Subquery sieht das so aus:
SELECT name, preis
FROM produkte
WHERE preis > (SELECT AVG(preis) FROM produkte);Das ist noch überschaubar. Sobald aber mehrere Zwischenschritte dazukommen, wird es unübersichtlich. Mit einer CTE bleibt jeder Schritt benannt und nachvollziehbar:
WITH durchschnitt AS (
SELECT AVG(preis) AS avg_preis
FROM produkte
)
SELECT p.name, p.preis
FROM produkte p, durchschnitt d
WHERE p.preis > d.avg_preis;Der Name durchschnitt macht sofort klar, worum es geht. Du gibst der Zwischenberechnung eine Bedeutung, statt sie in Klammern zu verstecken.
Mehrere CTEs verketten
Richtig stark werden CTEs, wenn du mehrere davon hintereinander definierst. Nach dem ersten WITH trennst du weitere CTEs einfach mit einem Komma. Jede darf auf die vorher definierten zugreifen. So zerlegst du eine komplexe Auswertung in klar getrennte, aufeinander aufbauende Schritte.
WITH bestellungen_pro_kunde AS (
SELECT kunde_id, COUNT(*) AS anzahl, SUM(betrag) AS umsatz
FROM bestellungen
GROUP BY kunde_id
),
top_kunden AS (
SELECT kunde_id, umsatz
FROM bestellungen_pro_kunde
WHERE anzahl >= 5
)
SELECT k.name, t.umsatz
FROM top_kunden t
JOIN kunden k ON k.id = t.kunde_id
ORDER BY t.umsatz DESC;Hier passiert dreierlei: Die erste CTE aggregiert Bestellungen pro Kunde, die zweite filtert auf Kunden mit mindestens fünf Bestellungen, und die Hauptabfrage verknüpft das Ergebnis mit den Kundennamen. Jeder Block macht genau eine Sache – das ist deutlich einfacher zu lesen und zu debuggen als eine einzige, tief verschachtelte Abfrage.
Rekursive CTEs: Hierarchien abfragen
Der vielleicht mächtigste Anwendungsfall sind rekursive CTEs. Damit fragst du Baum- oder Hierarchiestrukturen ab – etwa eine Mitarbeitertabelle, in der jeder Datensatz auf seinen Vorgesetzten verweist. Eine rekursive CTE besteht aus zwei Teilen, die mit UNION ALL verbunden werden: dem Ankerteil (der Startpunkt) und dem rekursiven Teil, der sich selbst aufruft, bis keine neuen Zeilen mehr dazukommen.
WITH RECURSIVE org AS (
-- Ankerteil: der Chef ganz oben ohne Vorgesetzten
SELECT id, name, chef_id, 1 AS ebene
FROM mitarbeiter
WHERE chef_id IS NULL
UNION ALL
-- Rekursiver Teil: jeweils die direkten Untergebenen
SELECT m.id, m.name, m.chef_id, o.ebene + 1
FROM mitarbeiter m
JOIN org o ON m.chef_id = o.id
)
SELECT name, ebene
FROM org
ORDER BY ebene, name;Der Ankerteil wählt die Person an der Spitze. Der rekursive Teil verknüpft die Mitarbeitertabelle wieder mit der CTE selbst und findet so Ebene für Ebene alle Untergebenen. Die Spalte ebene zählt dabei mit, wie tief in der Hierarchie sich jemand befindet. Beachte das Schlüsselwort RECURSIVE – in PostgreSQL und SQLite ist es Pflicht, im SQL Server lässt du es weg.
Wann CTEs sinnvoll sind – und wann nicht
CTEs sind kein Allheilmittel, aber in vielen Situationen die klar bessere Wahl. Ihre Stärken zeigen sich vor allem, wenn du:
- eine komplexe Abfrage in lesbare, benannte Schritte zerlegen willst,
- ein Zwischenergebnis mehrfach in derselben Abfrage brauchst,
- oder eine Hierarchie bzw. Baumstruktur mit einer rekursiven CTE durchlaufen musst.
Ein wichtiger Hinweis zur Performance: In manchen Datenbanken (früher etwa in PostgreSQL bis Version 11) wurden CTEs als sogenannte Optimization Fence behandelt, also getrennt vom Rest der Abfrage optimiert. Bei sehr großen Datenmengen kann eine simple Subquery deshalb gelegentlich schneller sein. Für die meisten Alltagsabfragen ist der Unterschied aber vernachlässigbar – und der Gewinn an Lesbarkeit wiegt schwerer. Im Zweifel hilft ein Blick in den Ausführungsplan mit EXPLAIN.
Fazit
Common Table Expressions machen aus verschachtelten, schwer lesbaren SQL-Abfragen eine klare Abfolge benannter Schritte. Du definierst sie mit WITH, verkettest bei Bedarf mehrere davon mit Komma und greifst mit RECURSIVE sogar auf hierarchische Daten zu. Fang klein an: Nimm dir das nächste Mal eine deiner verschachtelten Subqueries vor und schreib sie in eine CTE um. Du wirst schnell merken, wie viel angenehmer sich solche Abfragen lesen – für dich und für alle, die deinen Code später warten müssen.