A subquery is a query used as a building block inside another query. A CTE is the same idea with a name pinned in front, so you read the steps top-to-bottom instead of inside-out.
Where you meet it
- "Rows above the average" — compute the average in a subquery, filter on it.
- "Customers who placed at least one order" — an
EXISTSorINsubquery. - Multi-step reports: one CTE per stage, chained into a final
SELECT. - Walking a tree — an org chart, a parts list — with a recursive CTE.
What it does
It lets one statement express logic that depends on the result of another query. The inner query runs and hands its result to the outer one. A CTE changes nothing about the result — it just gives that inner query a name you can reuse and reference, which keeps deep nesting readable.
A CTE only renames your logic for readability — it makes nothing faster. A correlated subquery, meanwhile, quietly re-runs for every outer row.
How it works
A scalar subquery returns a single value and is evaluated once — like the average in the tool. A correlated subquery references a column from the outer row, so it conceptually re-runs for every outer row: the comparison below is recomputed per person, against the average of their own country.
-- Subquery (scalar): runs once
SELECT * FROM people
WHERE spend > (SELECT AVG(spend) FROM people);
-- Same logic as a named CTE
WITH avg_spend AS (
SELECT AVG(spend) AS a FROM people
)
SELECT * FROM people, avg_spend
WHERE spend > a;
-- Correlated: re-evaluated per outer row
SELECT * FROM people p
WHERE spend > (SELECT AVG(spend) FROM people
WHERE country = p.country);
A recursive CTE (WITH RECURSIVE) feeds its own output back in: a base SELECT seeds the rows, then a second SELECT references the CTE and repeats until it produces nothing new — that is how you walk hierarchies and graphs.
Watch out
- Correlated subqueries are a performance trap. Re-running per outer row can mean millions of executions; a join or window function is often far cheaper.
- A CTE is not automatically faster. It is a readability tool, not an optimization. Depending on the engine it may even be materialized — computed once into a temp result that blocks index pushdown.
NOT INwith a subquery that can returnNULLsilently returns no rows. PreferNOT EXISTSwhen nulls are possible.
Go deeper
Eine Subquery ist eine Query, die als Baustein in einer anderen Query steckt. Ein CTE ist dieselbe Idee mit vorangestelltem Namen — du liest die Schritte von oben nach unten statt von innen nach außen.
Wo es vorkommt
- "Zeilen über dem Durchschnitt" — Durchschnitt in einer Subquery berechnen, danach filtern.
- "Kunden mit mindestens einer Bestellung" — eine
EXISTS- oderIN-Subquery. - Mehrstufige Reports: ein CTE je Stufe, verkettet zu einem finalen
SELECT. - Einen Baum durchlaufen — Org-Chart, Stückliste — mit einem rekursiven CTE.
Was es tut
Es erlaubt einem Statement, Logik auszudrücken, die vom Ergebnis einer anderen Query abhängt. Die innere Query läuft und reicht ihr Ergebnis an die äußere weiter. Ein CTE ändert nichts am Ergebnis — es gibt der inneren Query nur einen Namen, den du wiederverwenden und referenzieren kannst, was tiefe Verschachtelung lesbar hält.
Ein CTE benennt deine Logik nur lesbarer — schneller macht es nichts. Eine korrelierte Subquery läuft derweil still für jede äußere Zeile neu.
Wie es funktioniert
Eine skalare Subquery liefert einen einzelnen Wert und wird einmal ausgewertet — wie der Durchschnitt im Tool. Eine korrelierte Subquery referenziert eine Spalte der äußeren Zeile und läuft daher konzeptionell für jede äußere Zeile neu: der Vergleich unten wird pro Person neu berechnet, gegen den Schnitt des jeweils eigenen Landes.
-- Subquery (skalar): läuft einmal
SELECT * FROM people
WHERE spend > (SELECT AVG(spend) FROM people);
-- Gleiche Logik als benanntes CTE
WITH avg_spend AS (
SELECT AVG(spend) AS a FROM people
)
SELECT * FROM people, avg_spend
WHERE spend > a;
-- Korreliert: pro äußerer Zeile neu ausgewertet
SELECT * FROM people p
WHERE spend > (SELECT AVG(spend) FROM people
WHERE country = p.country);
Ein rekursives CTE (WITH RECURSIVE) speist seine eigene Ausgabe zurück: ein Basis-SELECT liefert die Startzeilen, dann referenziert ein zweites SELECT das CTE und wiederholt, bis nichts Neues mehr entsteht — so durchläuft man Hierarchien und Graphen.
Worauf achten
- Korrelierte Subqueries sind eine Performance-Falle. Pro äußerer Zeile neu zu laufen kann Millionen Ausführungen bedeuten; ein Join oder eine Window-Funktion ist oft deutlich günstiger.
- Ein CTE ist nicht automatisch schneller. Es ist ein Lesbarkeits-Werkzeug, keine Optimierung. Je nach Engine wird es sogar materialisiert — einmal in ein Temp-Ergebnis berechnet, was Index-Pushdown blockiert.
NOT INmit einer Subquery, dieNULLliefern kann, gibt stillschweigend keine Zeilen zurück. Bei möglichen Nulls besserNOT EXISTS.