wann was verwenden
Die Wahl zwischen einer Subquery und einem Join ist selten eine reine Geschmacksfrage, sondern haengt von der Art der Subquery ab: skalare Subqueries liefern einen einzelnen Wert, korrelierte Subqueries verweisen auf die aeussere Abfrage, derived tables treten als vollstaendige Tabelle im FROM auf, und jede dieser Formen hat einen eigenen, oft aequivalenten Join, den der Optimizer moderner Datenbanken in vielen Faellen sogar automatisch erzeugt.
Inhaltsverzeichnis
- 1. Drei Arten von Subqueries im Ueberblick
- 2. Skalare Subqueries: ein Wert statt einer Tabelle
- 3. Derived Tables: Subquery als vollstaendige Tabelle
- 4. Korrelierte Subqueries und ihr Join-Aequivalent
- 5. Lesbarkeit: wann eine Subquery klarer ist als ein Join
- 6. Performance: wo sich Subquery und Join wirklich unterscheiden
- 7. Wann der Optimizer beides identisch behandelt
- 8. Common Table Expressions als dritter Weg
- 9. Subquery-Typen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Drei Arten von Subqueries im Ueberblick
Eine Subquery ist eine Abfrage innerhalb einer Abfrage, doch der Begriff fasst drei fachlich sehr unterschiedliche Konstrukte zusammen, die jeweils eigene Vor- und Nachteile gegenueber einem Join haben. Eine skalare Subquery liefert genau einen Wert und wird dort eingesetzt, wo eine Spalte einen einzelnen Ausdruck erwartet. Eine derived table steht als vollstaendige, temporaere Tabelle im FROM-Teil und kann selbst wieder gejoint werden. Eine korrelierte Subquery verweist auf Spalten der aeusseren Abfrage und wird logisch fuer jede aeussere Zeile neu ausgewertet.
Diese drei Formen unterscheiden sich fundamental in ihrem Verhaeltnis zu einem Join. Eine skalare Subquery hat oft kein direktes Join-Aequivalent, weil sie an einer Stelle steht, an der ein Join syntaktisch gar nicht moeglich waere. Eine derived table ist im Kern nichts anderes als ein vorgeschalteter Join-Partner. Eine korrelierte Subquery laesst sich haeufig, aber nicht immer, in einen Join oder eine Window-Funktion umschreiben. Wer diese drei Kategorien auseinanderhaelt, kann die Frage "Subquery oder Join" pro Fall gezielt beantworten, statt sie pauschal zu entscheiden.
Fuer die folgenden Beispiele verwenden wir wieder customers und orders, ergaenzt um order_items fuer Beispiele mit Aggregation auf mehreren Ebenen.
2. Skalare Subqueries: ein Wert statt einer Tabelle
Eine skalare Subquery gibt genau eine Zeile mit genau einer Spalte zurueck und kann ueberall dort stehen, wo ein einzelner Wert erwartet wird, etwa in der SELECT-Liste, in einer WHERE-Bedingung oder als Vergleichswert. Ein typisches Beispiel ist die Anzeige des Gesamtumsatzes eines Kunden direkt in der Kundenliste, ohne die Kundentabelle vorher zu gruppieren. Fuer diesen Fall gibt es kein einfaches Join-Aequivalent, weil ein Join mehrere Zeilen pro Kunde erzeugen wuerde, sobald der Kunde mehrere Bestellungen hat, das Ergebnis muesste danach ohnehin wieder aggregiert werden.
Eine skalare Subquery in der SELECT-Liste hat einen wichtigen Nachteil: sie wird pro Zeile der aeusseren Abfrage separat ausgefuehrt, wenn sie auf die aeussere Zeile Bezug nimmt, also korreliert ist. Bei tausend Kunden bedeutet das tausend Ausfuehrungen der inneren Abfrage, sofern der Optimizer sie nicht intern in einen Join umschreibt. Moderne Optimizer tun das haeufig automatisch, aber sich blind darauf zu verlassen ist riskant, gerade bei aelteren Datenbankversionen oder komplexeren Subqueries.
-- Scalar subquery in the SELECT list: total spent per customer
SELECT
c.name,
(SELECT SUM(o.amount) FROM orders o WHERE o.customer_id = c.id) AS total_spent
FROM customers c;
-- name | total_spent
-- Anna Berger | 165.00
-- Tom Keller | 300.00
-- Lisa Wolf | NULL
-- Equivalent using a LEFT JOIN with pre-aggregation
SELECT c.name, agg.total_spent
FROM customers c
LEFT JOIN (
SELECT customer_id, SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id
) agg ON agg.customer_id = c.id;
3. Derived Tables: Subquery als vollstaendige Tabelle
Eine derived table ist eine Subquery, die im FROM-Teil einer Abfrage steht und dort wie eine ganz normale Tabelle behandelt wird, inklusive eigenem Alias und der Moeglichkeit, sie mit anderen Tabellen zu joinen. Das obige Beispiel im vorherigen Abschnitt mit agg ist bereits eine derived table: die innere Abfrage aggregiert orders vorab pro Kunde, und die aeussere Abfrage joint diese vorberechnete Tabelle regulaer mit customers. Konzeptionell ist eine derived table also kein Gegensatz zum Join, sondern eine Vorstufe dazu.
Der grosse Vorteil einer derived table gegenueber einer direkten Aggregation nach dem Join liegt in der korrekten Aggregationsebene: aggregiert man orders zuerst in der derived table und joint erst danach, entstehen keine durch nachfolgende Joins vervielfachten Summen, ein Problem, das bei mehreren aufeinanderfolgenden Joins mit anschliessender Aggregation regelmaessig auftritt. Diese Technik loest damit direkt das Multiplikationsproblem, das bei Joins mit mehreren Tabellen auf 1:n-Beziehungen entsteht.
-- WRONG: aggregating after joining two 1:n relations multiplies the sum
SELECT c.name, SUM(oi.quantity * oi.price) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.name;
-- inflates totals when a customer has multiple orders with multiple items
-- RIGHT: pre-aggregate per order first in a derived table
SELECT c.name, SUM(order_totals.total) AS total
FROM customers c
JOIN (
SELECT o.customer_id, o.id AS order_id, SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.customer_id, o.id
) order_totals ON order_totals.customer_id = c.id
GROUP BY c.name;
4. Korrelierte Subqueries und ihr Join-Aequivalent
Eine korrelierte Subquery verweist innerhalb ihrer WHERE-Bedingung auf eine Spalte der aeusseren Abfrage und kann deshalb nicht unabhaengig von dieser ausgewertet werden. Der klassische Fall ist eine EXISTS-Bedingung, die prueft, ob zu einer aeusseren Zeile mindestens eine passende innere Zeile existiert. Fachlich entspricht das oft demselben Ergebnis wie ein INNER JOIN mit anschliessendem DISTINCT, syntaktisch und im Ausfuehrungsplan koennen sich beide aber deutlich unterscheiden.
Der wichtigste Unterschied zwischen einer korrelierten Subquery mit EXISTS und einem entsprechenden Join zeigt sich, sobald die innere Tabelle mehrere passende Zeilen pro aeusserer Zeile hat: Der Join vervielfacht die aeussere Zeile pro Treffer, waehrend EXISTS nur prueft, ob ueberhaupt ein Treffer existiert, und die aeussere Zeile deshalb genau einmal liefert. Wer nur wissen will, ob eine Beziehung existiert, nicht aber die Details der inneren Zeilen braucht, ist mit EXISTS oft sowohl korrekter als auch performanter unterwegs als mit einem Join plus DISTINCT.
-- Correlated subquery with EXISTS: customers who have placed at least one order
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- Join-based equivalent requires DISTINCT to avoid duplicated rows
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id;
-- both return the same names, but the join can produce intermediate
-- duplicate rows before DISTINCT removes them again
5. Lesbarkeit: wann eine Subquery klarer ist als ein Join
Neben Performance ist Lesbarkeit ein legitimes Kriterium fuer die Wahl zwischen Subquery und Join. Eine EXISTS-Bedingung transportiert die fachliche Absicht "hat mindestens eine passende Zeile" direkter als ein Join mit anschliessendem DISTINCT, dessen Zweck sich erst beim genaueren Lesen erschliesst. Aehnlich verhaelt es sich mit IN bei einer statischen oder einfachen Subquery-Liste, die oft leichter zu ueberfliegen ist als ein aequivalenter Join mit Filter.
Umgekehrt wird ein Join lesbarer, sobald mehrere Spalten aus der verknuepften Tabelle im Ergebnis benoetigt werden. Eine Subquery liefert typischerweise nur einen Wert oder eine Ja/Nein-Information, waehrend ein Join beliebig viele Spalten der zweiten Tabelle in derselben Ergebniszeile bereitstellt. Braucht man mehr als eine Information aus der verknuepften Tabelle, ist ein Join fast immer die klarere und direktere Wahl, weil sich sonst mehrere Subqueries fuer dieselbe logische Beziehung haeufen wuerden.
Eine gute Faustregel: Subqueries fuer Existenzpruefungen, einzelne Aggregatwerte und Filterlisten, Joins fuer alles, wo mehrere Spalten der verknuepften Tabelle im Ergebnis erscheinen sollen. Diese Regel deckt die meisten Praxisfaelle ab, ohne dass man bei jeder Abfrage neu abwaegen muss.
6. Performance: wo sich Subquery und Join wirklich unterscheiden
In modernen relationalen Datenbanken ist die Performance-Differenz zwischen einer gut geschriebenen Subquery und einem aequivalenten Join in vielen Faellen gering bis nicht vorhanden, weil der Query-Optimizer beide in denselben internen Ausfuehrungsplan uebersetzt. Relevante Unterschiede entstehen vor allem in drei Situationen: bei nicht korrelierten Subqueries in einer IN-Bedingung mit sehr grossen Ergebnismengen, bei korrelierten Subqueries, die der Optimizer nicht automatisch dekorrelieren kann, und bei alten oder schwaecheren Optimizern, die bestimmte Subquery-Formen grundsaetzlich schlechter behandeln.
Eine korrelierte Subquery, die der Optimizer nicht dekorrelieren kann, wird im schlimmsten Fall tatsaechlich einmal pro aeusserer Zeile ausgefuehrt, was bei grossen Tabellen zu quadratischer statt linearer Laufzeit fuehrt. Der zuverlaessigste Weg, dieses Risiko auszuschliessen, ist ein Blick in den tatsaechlichen Ausfuehrungsplan mit EXPLAIN beziehungsweise EXPLAIN ANALYZE, statt sich auf Annahmen ueber das Optimizer-Verhalten zu verlassen.
-- Check whether the optimizer decorrelates a subquery into a join
EXPLAIN ANALYZE
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- Look for "Hash Join" or "Semi Join" in the plan output.
-- A "Nested Loop" re-executed per outer row signals a non-decorrelated
-- subquery and is worth investigating for large tables.
7. Wann der Optimizer beides identisch behandelt
PostgreSQL, MySQL ab Version 8.0 und SQL Server verfuegen alle ueber Optimizer, die viele Subqueries automatisch in semantisch aequivalente Joins umschreiben, ein Prozess, der als Subquery-Dekorrelation oder Subquery-Unnesting bezeichnet wird. Eine IN-Subquery mit statischer Werteliste wird in aller Regel als Semi-Join ausgefuehrt, unabhaengig davon, ob man sie als IN (SELECT ...) oder als expliziten Join mit DISTINCT schreibt, das Ergebnis im Ausfuehrungsplan ist praktisch identisch.
Diese Optimierungsfaehigkeit hat Grenzen: sehr komplexe korrelierte Subqueries mit mehreren verschachtelten Bedingungen, Aggregatfunktionen in der korrelierten Bedingung oder Subqueries in OR-verknuepften Bedingungen werden von manchen Optimizern nicht dekorrelieren. In solchen Faellen liefert eine manuelle Umschreibung in einen Join oder eine derived table oft messbar bessere Ergebnisse als die automatische Optimierung. Der einzige verlaessliche Weg, das fuer eine konkrete Abfrage zu wissen, ist ein Vergleich der Ausfuehrungsplaene beider Varianten auf realistischen Datenmengen.
8. Common Table Expressions als dritter Weg
Neben Subquery und Join gibt es mit der Common Table Expression, eingeleitet durch WITH, einen dritten Weg, der syntaktisch einer benannten derived table entspricht, aber am Anfang der Abfrage einmal definiert und danach mehrfach referenziert werden kann. Fuer die reine Frage "Subquery oder Join" ist eine CTE meist aequivalent zu einer entsprechenden derived table im FROM, verbessert aber die Lesbarkeit erheblich, sobald mehrere aufeinander aufbauende Zwischenschritte noetig sind.
In manchen Datenbanken, insbesondere in aelteren PostgreSQL-Versionen vor 12, wirkte eine CTE als sogenannte Optimization Fence und wurde separat materialisiert, was die Performance gegenueber einer aequivalenten Subquery verschlechtern konnte. Seit PostgreSQL 12 wird eine CTE standardmaessig inline behandelt, wenn moeglich, wodurch sich der Performance-Unterschied zu einer Subquery meist aufloest. Wer eine aeltere Datenbankversion einsetzt, sollte das Verhalten der eigenen Version explizit pruefen, bevor man sich auf identisches Verhalten verlaesst.
-- CTE version of the pre-aggregated derived table from section 3
WITH order_totals AS (
SELECT o.customer_id, o.id AS order_id, SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.customer_id, o.id
)
SELECT c.name, SUM(order_totals.total) AS total
FROM customers c
JOIN order_totals ON order_totals.customer_id = c.id
GROUP BY c.name;
9. Subquery-Typen im direkten Vergleich
Die folgende Tabelle fasst die drei Subquery-Formen zusammen und zeigt, wie sie sich in Bezug auf ihr Join-Aequivalent, ihre typische Verwendung und ihr Performance-Risiko unterscheiden.
| Subquery-Typ | Join-Aequivalent | Typische Verwendung | Performance-Risiko |
|---|---|---|---|
| Skalar | Meist keins, sonst LEFT JOIN + Aggregation | Einzelwert in SELECT-Liste | Hoch bei Korrelation ohne Dekorrelation |
| Derived Table | Direktes Aequivalent, ist im Kern ein Join | Vor-Aggregation vor dem Join | Niedrig |
| Korreliert (EXISTS) | INNER JOIN + DISTINCT, semantisch aehnlich | Existenzpruefung ohne Detaildaten | Mittel, abhaengig vom Optimizer |
Diese Uebersicht zeigt: die Frage "Subquery oder Join" ist keine einzelne Entscheidung, sondern haengt vom konkreten Subquery-Typ ab. Eine derived table ist praktisch immer unproblematisch, eine skalare korrelierte Subquery verdient dagegen immer einen Blick in den Ausfuehrungsplan, bevor sie in Produktionscode landet.
Mironsoft
Query-Optimierung, Ausfuehrungsplan-Analyse und SQL-Reviews
Langsame Subqueries identifizieren und gezielt umbauen?
Wir analysieren Ausfuehrungsplaene, finden nicht dekorrelierte Subqueries und schreiben sie gezielt in performante Joins oder Window-Funktionen um.
Execution-Plan-Analyse
EXPLAIN ANALYZE systematisch auswerten und Engpaesse finden
Query-Refactoring
Subqueries gezielt in Joins, CTEs oder Window-Funktionen ueberfuehren
Datenbankberatung
Optimizer-Verhalten je Datenbankversion einschaetzen
10. Zusammenfassung
Die Entscheidung zwischen Subquery und Join haengt stark vom konkreten Subquery-Typ ab. Skalare Subqueries loesen Faelle, in denen ein einzelner Wert benoetigt wird und ein Join gar nicht sinnvoll waere. Derived tables sind im Grunde vorbereitete Join-Partner und loesen zuverlaessig das Problem vervielfachter Summen nach mehrfachen Joins. Korrelierte Subqueries mit EXISTS pruefen Existenz ohne unnoetige Duplikate, waehrend ein Join mit DISTINCT denselben Zweck oft umstaendlicher erreicht.
Moderne Optimizer dekorrelieren viele Subqueries automatisch zu Joins, sodass der Performance-Unterschied in der Praxis oft gering ausfaellt. Verlassen sollte man sich darauf trotzdem nicht blind: ein Blick in den tatsaechlichen Ausfuehrungsplan mit EXPLAIN ANALYZE zeigt zuverlaessig, ob eine konkrete Subquery dekorreliert wird oder pro aeusserer Zeile neu ausgefuehrt werden muss.
Subqueries vs. Joins, das Wichtigste auf einen Blick
Skalare Subquery
Fuer einzelne Werte in der SELECT-Liste. Bei Korrelation pro Zeile ausgefuehrt, sofern der Optimizer nicht dekorreliert.
Derived Table
Im Kern ein vorbereiteter Join-Partner. Loest das Problem vervielfachter Summen bei mehreren 1:n-Joins.
Korrelierte Subquery
EXISTS prueft Existenz ohne Duplikate. Alternative zu Join plus DISTINCT.
Faustregel
Subquery fuer Einzelwerte und Existenzpruefungen, Join fuer mehrere benoetigte Spalten der verknuepften Tabelle.