CodeGym /Kurse /SQL SELF /CTE vs Unterabfragen: Wann was nehmen?

CTE vs Unterabfragen: Wann was nehmen?

SQL SELF
Level 28 , Lektion 1
Verfügbar

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!

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION