← CS Lab Databases & Data Modeling / SQL Subqueries & CTEs Open standalone ↗

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

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 IN with a subquery that can return NULL silently returns no rows. Prefer NOT EXISTS when 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

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 IN mit einer Subquery, die NULL liefern kann, gibt stillschweigend keine Zeilen zurück. Bei möglichen Nulls besser NOT EXISTS.

Mehr dazu

Next up
Databases & Data ModelingSQL Transactions & ACID Databases & Data ModelingSQL Indexes All concepts →