Wir wissen schon, dass CTEs den Code lesbarer machen. Aber sollte man sie immer nutzen? Manchmal reicht eine einfache Unterabfrage – die ist dann sogar schneller. Lass uns anschauen, wann welches Tool im Vorteil ist und wie du bewusst entscheidest.
Unterabfragen: schnell und easy
Du erinnerst dich bestimmt: Eine Unterabfrage ist einfach SQL in SQL. Sie steckt direkt im Hauptquery und wird "on the fly" ausgeführt. Perfekt für einfache, einmalige Operationen:
-- Finde Produkte teurer als der Durchschnittspreis
SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
Hier berechnet die Unterabfrage einmal den Durchschnittspreis – fertig. Keine unnötigen Konstrukte.
Performance: Wer ist schneller?
Unterabfragen sind oft schneller bei einfachen Sachen. PostgreSQL kann sie "on the fly" optimieren, vor allem wenn die Unterabfrage nur einmal läuft:
-- Schnell: Unterabfrage wird einmal ausgeführt
SELECT customer_id, order_total
FROM orders
WHERE order_date = (SELECT MAX(order_date) FROM orders);
CTEs werden standardmäßig materialisiert – PostgreSQL berechnet das Ergebnis der CTE, speichert es als temporäre Tabelle und nutzt es dann. Das kann einfache Queries langsamer machen:
-- Langsamer: CTE wird als temporäre Tabelle materialisiert
WITH latest_date AS (
SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;
Aber! Ab PostgreSQL 12 kannst du die Materialisierung steuern:
-- Erzwinge KEINE Materialisierung
WITH latest_date AS NOT MATERIALIZED (
SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;
Mehrfachnutzung: Hier rockt die CTE
Wenn du das gleiche Zwischenergebnis mehrmals brauchst, ist die CTE unschlagbar:
-- Mit Unterabfrage: Logik wird zweimal wiederholt
SELECT
(SELECT COUNT(*) FROM orders WHERE status = 'abgeschlossen') AS abgeschlossene_bestellungen,
(SELECT COUNT(*) FROM orders WHERE status = 'abgeschlossen') * 100.0 / COUNT(*) AS abschlussrate
FROM orders;
-- Mit CTE: Einmal berechnen, zweimal nutzen
WITH abgeschlossene_bestellungen AS (
SELECT COUNT(*) AS anzahl FROM orders WHERE status = 'abgeschlossen'
)
SELECT
ab.anzahl AS abgeschlossene_bestellungen,
ab.anzahl * 100.0 / (SELECT COUNT(*) FROM orders) AS abschlussrate
FROM abgeschlossene_bestellungen ab;
Komplexe Analysen: CTE punktet
Für mehrstufige Analysen bringt die CTE Ordnung ins Chaos. Schau dir mal diesen Verkaufsreport an:
Mit Unterabfragen (Kuddelmuddel):
SELECT
category,
revenue,
revenue * 100.0 / (
SELECT SUM(p.price * oi.quantity)
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE EXTRACT(year FROM o.order_date) = 2024
) AS umsatzanteil
FROM (
SELECT
p.category,
SUM(p.price * oi.quantity) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE EXTRACT(year FROM o.order_date) = 2024
GROUP BY p.category
) category_revenue;
Mit CTE (alles schön sortiert):
WITH jahresumsatz AS (
SELECT
p.category,
p.price * oi.quantity AS verkaufsbetrag
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE EXTRACT(year FROM o.order_date) = 2024
),
kategorie_umsatz AS (
SELECT
category,
SUM(verkaufsbetrag) AS revenue
FROM jahresumsatz
GROUP BY category
),
gesamtumsatz AS (
SELECT SUM(verkaufsbetrag) AS gesamt FROM jahresumsatz
)
SELECT
ku.category,
ku.revenue,
ku.revenue * 100.0 / gu.gesamt AS umsatzanteil
FROM kategorie_umsatz ku, gesamtumsatz gu;
Rekursion: CTE hat das Monopol
Für hierarchische Strukturen sind Unterabfragen machtlos.
Nur rekursive CTEs schaffen Aufgaben wie "alle Untergebenen eines Managers finden":
WITH RECURSIVE mitarbeiter_hierarchie AS (
-- Start mit CEO
SELECT employee_id, manager_id, name, 1 AS ebene
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Füge Untergebene jeder Ebene hinzu
SELECT e.employee_id, e.manager_id, e.name, mh.ebene + 1
FROM employees e
JOIN mitarbeiter_hierarchie mh ON e.manager_id = mh.employee_id
)
SELECT * FROM mitarbeiter_hierarchie ORDER BY ebene, name;
Debugging und Codepflege
CTEs kannst du easy Schritt für Schritt debuggen:
-- Erster Schritt prüfen
WITH aktive_kunden AS (
SELECT customer_id FROM customers WHERE status = 'aktiv'
)
SELECT COUNT(*) FROM aktive_kunden; -- Sicherstellen, dass die Logik stimmt
-- Zweiten Schritt hinzufügen
WITH aktive_kunden AS (...),
neue_bestellungen AS (
SELECT customer_id, COUNT(*) as bestellanzahl
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
)
SELECT COUNT(*) FROM neue_bestellungen; -- Auch diesen Schritt checken
Unterabfragen sind schwerer zu debuggen – du musst sie aus dem Kontext rausziehen.
Praxistipps
Nimm Unterabfragen, wenn:
- Die Logik simpel ist und in eine Zeile passt
- Du maximale Performance für einfache Sachen willst
- Das Zwischenergebnis nur einmal gebraucht wird
- Du mit kleinen Datenmengen arbeitest
Nimm CTEs, wenn:
- Der Query komplex ist und sich logisch aufteilen lässt
- Du Zwischenergebnisse mehrfach brauchst
- Lesbarkeit und Wartbarkeit wichtig sind
- Du mit Hierarchien arbeitest (rekursive CTEs)
- Du komplexe Logik Schritt für Schritt debuggen willst
Goldene Regel
Starte mit einer Unterabfrage. Wenn es unübersichtlich wird oder sich Logik wiederholt – wechsel zu CTE. Dein zukünftiges Ich (oder dein Kollege) wird dir danken!
GO TO FULL VERSION