wie zwei Tabellen wirklich zusammenfinden
Ein falsch gewaehlter Join-Typ liefert entweder zu wenige Zeilen, weil unpassende Datensaetze stillschweigend verschwinden, oder zu viele Zeilen, weil eine fehlende Join-Bedingung ein kartesisches Produkt erzeugt. Inner Join, Left Join, Right Join und Full Join unterscheiden sich genau darin, welche Zeilen aus den beteiligten Tabellen in der Ergebnismenge landen, wenn eine Bedingung nicht erfuellt ist, und wer das nicht versteht, baut Reports mit stillschweigend falschen Zahlen.
Inhaltsverzeichnis
- 1. Das Grundprinzip: was ein Join wirklich tut
- 2. Inner Join: nur die Schnittmenge
- 3. Left Join: alle Zeilen der linken Tabelle behalten
- 4. Right Join: die gespiegelte Variante
- 5. Full Join: nichts geht verloren
- 6. Das kartesische Produkt: der klassische Join-Fehler
- 7. Mehrere Joins und Join-Reihenfolge
- 8. NULL-Werte in Join-Ergebnissen richtig behandeln
- 9. Join-Typen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundprinzip: was ein Join wirklich tut
Ein Join kombiniert Zeilen aus zwei oder mehr Tabellen anhand einer Bedingung, meist eines Gleichheitsvergleichs zwischen einem Fremdschluessel und einem Primaerschluessel. Konzeptionell bildet die Datenbank zunaechst das kartesische Produkt aller moeglichen Zeilenkombinationen und filtert danach mit der ON-Bedingung heraus, welche Kombinationen tatsaechlich in der Ergebnismenge bleiben. In der Praxis optimiert der Query-Planer diesen Ablauf mit Hash-Joins, Merge-Joins oder Nested-Loop-Joins, aber das logische Ergebnis bleibt dasselbe, unabhaengig vom gewaehlten Ausfuehrungsplan.
Der entscheidende Unterschied zwischen den vier Join-Typen liegt nicht in der Schnittmenge selbst, sondern in der Behandlung der Zeilen, fuer die keine passende Gegenzeile existiert. Ein Inner Join verwirft diese Zeilen komplett, ein Left Join oder Right Join behaelt sie aus einer der beiden Tabellen und fuellt die fehlenden Spalten mit NULL, ein Full Join behaelt sie aus beiden Tabellen gleichzeitig. Wer diesen einen Unterschied verinnerlicht hat, kann jeden der vier Join-Typen aus der Definition ableiten, statt sie auswendig zu lernen.
Fuer die folgenden Beispiele nutzen wir zwei einfache Tabellen: customers mit Kundendaten und orders mit Bestellungen, verknuepft ueber customers.id = orders.customer_id. Nicht jeder Kunde hat eine Bestellung, nicht jede Bestellung ist zwangslaeufig einem existierenden Kunden zugeordnet, das macht die Unterschiede zwischen den Join-Typen sichtbar.
2. Inner Join: nur die Schnittmenge
Der Inner Join ist der restriktivste der vier Join-Typen und gleichzeitig der am haeufigsten verwendete. Er liefert ausschliesslich Zeilenkombinationen, bei denen die Join-Bedingung fuer beide Tabellen erfuellt ist. Ein Kunde ohne Bestellung erscheint nicht in der Ergebnismenge, eine Bestellung mit einer ungueltigen Kundenreferenz ebenfalls nicht. Das macht den Inner Join zur richtigen Wahl, wenn nur vollstaendige, referenziell konsistente Datensaetze relevant sind, etwa fuer eine Rechnungsliste, bei der jede Zeile zwingend einen Kunden und eine Bestellung braucht.
Syntaktisch ist INNER JOIN und das kurze JOIN ohne Praefix identisch, beide erzeugen einen Inner Join. Manche Teams verwenden trotzdem konsequent das ausgeschriebene INNER JOIN, weil es in Code-Reviews sofort erkennbar macht, dass hier bewusst gefiltert wird, waehrend ein blosses JOIN leicht mit einem versehentlich vergessenen Praefix verwechselt werden kann.
-- Sample tables
-- customers: id, name
-- orders: id, customer_id, amount
SELECT c.name, o.id AS order_id, o.amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
ORDER BY c.name;
-- customers table
-- id | name
-- 1 | Anna Berger
-- 2 | Tom Keller
-- 3 | Lisa Wolf <- has no orders
-- orders table
-- id | customer_id | amount
-- 10 | 1 | 120.00
-- 11 | 1 | 45.00
-- 12 | 2 | 300.00
-- 13 | 99 | 15.00 <- orphaned, no matching customer
-- Result: only rows where both sides match
-- name | order_id | amount
-- Anna Berger | 10 | 120.00
-- Anna Berger | 11 | 45.00
-- Tom Keller | 12 | 300.00
-- Lisa Wolf and order 13 are both missing from the result
3. Left Join: alle Zeilen der linken Tabelle behalten
Der Left Join, ausgeschrieben LEFT OUTER JOIN, behaelt jede Zeile der linken, zuerst genannten Tabelle, unabhaengig davon, ob eine passende Zeile in der rechten Tabelle existiert. Findet sich keine Uebereinstimmung, werden alle Spalten der rechten Tabelle mit NULL aufgefuellt. Das ist der Join-Typ der Wahl fuer die typische Frage "zeige mir alle X, auch wenn sie kein zugehoeriges Y haben", etwa "alle Kunden, auch die ohne Bestellung" oder "alle Produkte, auch die ohne Bewertung".
Ein haeufiger Anwendungsfall fuer den Left Join ist die Suche nach fehlenden Zuordnungen: Man joint links nach rechts und filtert anschliessend mit WHERE rechte_tabelle.id IS NULL auf genau die Zeilen, fuer die keine Gegenzeile existiert. Dieses Muster findet beispielsweise Kunden ohne jede Bestellung, ohne dafuer eine separate Subquery mit NOT IN zu benoetigen.
SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
ORDER BY c.name;
-- Result: every customer appears, even without orders
-- name | order_id | amount
-- Anna Berger | 10 | 120.00
-- Anna Berger | 11 | 45.00
-- Lisa Wolf | NULL | NULL
-- Tom Keller | 12 | 300.00
-- Common pattern: find customers with zero orders
SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;
-- name
-- Lisa Wolf
4. Right Join: die gespiegelte Variante
Der Right Join, ausgeschrieben RIGHT OUTER JOIN, verhaelt sich exakt spiegelbildlich zum Left Join: er behaelt jede Zeile der rechten Tabelle und fuellt fehlende Werte der linken Tabelle mit NULL auf. Jeder RIGHT JOIN laesst sich vollstaendig durch Vertauschen der Tabellenreihenfolge in einen LEFT JOIN umschreiben, deshalb verzichten viele Style-Guides bewusst auf RIGHT JOIN und verwenden ausschliesslich LEFT JOIN, um die Lesbarkeit konsistent zu halten und nicht zwischen zwei mentalen Modellen wechseln zu muessen.
Trotzdem taucht der Right Join in generiertem SQL, in ORM-Ausgaben oder in Migrationsskripten regelmaessig auf, und wer ihn nicht liest wie einen umgedrehten Left Join, verliert beim Debugging Zeit. Wichtig: PostgreSQL, MySQL und SQL Server unterstuetzen RIGHT JOIN alle gleichermassen, es handelt sich nicht um eine proprietaere Erweiterung, auch wenn er in der Praxis seltener geschrieben wird als sein linkes Gegenstueck.
-- Right join: keep every row from orders, even orphaned ones
SELECT c.name, o.id AS order_id, o.customer_id
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id
ORDER BY o.id;
-- name | order_id | customer_id
-- Anna Berger | 10 | 1
-- Anna Berger | 11 | 1
-- Tom Keller | 12 | 2
-- NULL | 13 | 99 <- orphaned order, kept because it's on the right
-- Equivalent rewrite using LEFT JOIN, swapped table order
SELECT c.name, o.id AS order_id, o.customer_id
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
ORDER BY o.id;
-- produces the identical result set
5. Full Join: nichts geht verloren
Der Full Join, ausgeschrieben FULL OUTER JOIN, kombiniert das Verhalten von Left und Right Join: er behaelt Zeilen aus beiden Tabellen, unabhaengig davon, ob eine Gegenzeile existiert, und fuellt jeweils die fehlende Seite mit NULL auf. Das Ergebnis enthaelt sowohl Kunden ohne Bestellung als auch Bestellungen ohne gueltigen Kunden in derselben Abfrage, was ihn zum passenden Werkzeug fuer Datenqualitaets-Checks macht, bei denen beide Richtungen der Inkonsistenz gleichzeitig sichtbar werden sollen.
Ein wichtiger Praxis-Hinweis: MySQL unterstuetzt FULL OUTER JOIN bis einschliesslich Version 8.0 nicht nativ, waehrend PostgreSQL, SQL Server und Oracle ihn direkt anbieten. In MySQL emuliert man den Full Join ueber eine Kombination aus LEFT JOIN, RIGHT JOIN und UNION. Diese vendor-spezifische Luecke ist einer der Faelle, in denen Standard-SQL-Kenntnis allein nicht ausreicht und man die Zieldatenbank kennen muss, bevor man einen Full Join im Produktionscode einsetzt.
-- Full join: PostgreSQL, SQL Server, Oracle
SELECT c.name, o.id AS order_id
FROM customers c
FULL OUTER JOIN orders o ON c.id = o.customer_id;
-- name | order_id
-- Anna Berger | 10
-- Anna Berger | 11
-- Tom Keller | 12
-- Lisa Wolf | NULL <- customer without any order
-- NULL | 13 <- order without a valid customer
-- MySQL emulation, no native FULL OUTER JOIN before 8.0
SELECT c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
UNION
SELECT c.name, o.id AS order_id
FROM customers c
RIGHT JOIN orders o ON c.id = o.customer_id;
6. Das kartesische Produkt: der klassische Join-Fehler
Der haeufigste Fehler bei jedem Join ist eine fehlende oder unvollstaendige ON-Bedingung. Ohne Join-Bedingung, etwa bei SELECT * FROM a, b oder CROSS JOIN, kombiniert die Datenbank jede Zeile von a mit jeder Zeile von b. Bei zwei Tabellen mit jeweils tausend Zeilen ergibt das eine Million Ergebniszeilen, ein kartesisches Produkt, das in einer echten Anwendung fast nie beabsichtigt ist und Reports mit vervielfachten, falschen Summen produziert.
Ein subtilerer, aber genauso haeufiger Fall entsteht bei mehreren Join-Bedingungen, wenn eine davon vergessen wird. Verknuepft man beispielsweise orders mit order_items nur ueber customer_id statt zusaetzlich ueber order_id, entstehen fuer jeden Kunden mit mehreren Bestellungen und mehreren Positionen unzaehlige falsche Kombinationen, ohne dass ein offensichtlicher Fehler auftritt, die Query laeuft einfach durch und liefert plausibel aussehende, aber falsche Zahlen.
-- WRONG: missing join condition creates a cartesian product
SELECT c.name, o.amount
FROM customers c, orders o;
-- every customer combined with every order: 3 customers x 4 orders = 12 rows
-- WRONG: incomplete join condition on multiple keys
SELECT o.id, oi.product_id, oi.quantity
FROM orders o
JOIN order_items oi ON o.customer_id = oi.customer_id;
-- missing order_id condition duplicates rows across unrelated orders
-- RIGHT: explicit and complete join condition
SELECT o.id, oi.product_id, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id;
-- Defensive check: row count sanity check after any join
SELECT COUNT(*) AS row_count FROM orders; -- baseline
SELECT COUNT(*) AS after_join
FROM orders o JOIN order_items oi ON o.id = oi.order_id;
-- after_join should be >= row_count, never a wild multiple
7. Mehrere Joins und Join-Reihenfolge
Reale Abfragen verknuepfen selten nur zwei Tabellen. Bei drei oder mehr Tabellen entscheidet die Reihenfolge der JOIN-Klauseln zwar nicht ueber die logische Korrektheit, wohl aber ueber die Lesbarkeit und manchmal ueber die Performance, wenn der Optimizer die Reihenfolge nicht selbst umstellt. Eine bewaehrte Konvention ist, mit der "zentralen" Tabelle zu beginnen, etwa orders, und von dort aus schrittweise ueber customers, order_items und products zu joinen, so bleibt die Query von oben nach unten nachvollziehbar.
Bei mehreren aufeinanderfolgenden LEFT JOIN-Klauerseln ist besondere Vorsicht geboten: jeder weitere Left Join auf eine Tabelle mit mehreren passenden Zeilen vervielfacht die vorherige Ergebnismenge. Ein Kunde mit drei Bestellungen, gejoint mit einer Tabelle mit zwei Adressen, ergibt sechs Zeilen statt der erwarteten drei, wenn die Adress-Join-Bedingung nicht zusaetzlich eingeschraenkt wird. Solche Multiplikationseffekte sind eine mildere Form desselben kartesischen Produkts aus Abschnitt sechs und faellt oft erst auf, wenn Summen in einem Report ploetzlich zu hoch sind.
Aggregationen nach mehreren Joins muessen diese Vervielfachung beruecksichtigen. Ein SUM(o.amount) nach einem Join mit order_items zaehlt den Bestellbetrag pro Position statt pro Bestellung, wenn nicht vorher dedupliziert oder die Summe auf der richtigen Aggregationsebene gebildet wird. Hier hilft oft, den Betrag vor dem Join in einer Subquery vorzuaggregieren, statt ihn nach dem Join zu summieren.
8. NULL-Werte in Join-Ergebnissen richtig behandeln
Jeder Outer Join, ob Left, Right oder Full, erzeugt NULL-Werte in den Spalten der Tabelle ohne Gegenzeile. Diese NULL-Werte muessen in der weiteren Verarbeitung explizit behandelt werden, sonst verhalten sich Vergleiche unerwartet. WHERE o.amount > 100 nach einem Left Join filtert Zeilen mit NULL in amount automatisch heraus, weil jeder Vergleich mit NULL in SQL zu UNKNOWN und nicht zu TRUE auswertet, das entfernt versehentlich genau die Zeilen, die der Left Join eigentlich behalten sollte.
Funktionen wie COALESCE(o.amount, 0) ersetzen NULL durch einen definierten Standardwert, bevor Aggregationen oder Vergleiche stattfinden, und IS NULL beziehungsweise IS NOT NULL sind die einzigen korrekten Operatoren, um explizit auf NULL zu pruefen. Wer stattdessen = NULL schreibt, erhaelt in der Regel eine leere Ergebnismenge ohne Fehlermeldung, ein Stolperstein, der besonders nach einem Left Join haeufig uebersehen wird.
-- WRONG: filtering after a LEFT JOIN accidentally turns it into an INNER JOIN
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.amount > 100;
-- Lisa Wolf disappears again, NULL > 100 evaluates to UNKNOWN
-- RIGHT: move the condition into the join, or handle NULL explicitly
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.amount > 100;
-- RIGHT: default missing amounts to zero for aggregation
SELECT c.name, COALESCE(SUM(o.amount), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;
9. Join-Typen im direkten Vergleich
Die folgende Tabelle fasst zusammen, welche Zeilen jeder Join-Typ in der Ergebnismenge behaelt, wenn keine Gegenzeile existiert. Diese Uebersicht ersetzt kein tiefes Verstaendnis der einzelnen Faelle, ist aber eine schnelle Referenz fuer die Wahl des richtigen Join-Typs in einer konkreten Abfrage.
| Join-Typ | Zeilen ohne Match links | Zeilen ohne Match rechts | Typischer Einsatz |
|---|---|---|---|
| INNER JOIN | verworfen | verworfen | Nur vollstaendige, konsistente Datensaetze |
| LEFT JOIN | behalten | verworfen | Alle X, auch ohne zugehoeriges Y |
| RIGHT JOIN | verworfen | behalten | Spiegelbild von LEFT JOIN |
| FULL JOIN | behalten | behalten | Datenqualitaets-Checks in beide Richtungen |
In der Praxis dominieren INNER JOIN und LEFT JOIN den Alltag deutlich, waehrend RIGHT JOIN selten manuell geschrieben wird und FULL JOIN auf Datenqualitaets- und Migrationsszenarien beschraenkt bleibt. Wer bei jedem Join bewusst entscheidet, ob Zeilen ohne Gegenpart erhalten bleiben sollen oder nicht, vermeidet die meisten der oben beschriebenen Fehlerquellen von vornherein.
Mironsoft
Datenbankdesign, Query-Optimierung und SQL-Reviews
Queries, die konsistente Zahlen liefern statt stiller Fehler?
Wir pruefen bestehende Abfragen auf fehlerhafte Join-Bedingungen, ungewollte kartesische Produkte und falsche NULL-Behandlung und bringen eure Reports auf einen belastbaren Stand.
Query-Review
Systematische Pruefung auf fehlende oder unvollstaendige Join-Bedingungen
Reporting-Fehler beheben
Vervielfachte Summen durch falsche Joins identifizieren und korrigieren
Schema-Beratung
Fremdschluessel, Indizes und Join-Pfade fuer performante Abfragen
10. Zusammenfassung
Die vier Join-Typen Inner, Left, Right und Full unterscheiden sich ausschliesslich darin, wie sie mit Zeilen ohne Gegenpart umgehen. Der Inner Join verwirft sie auf beiden Seiten, der Left Join behaelt sie links, der Right Join behaelt sie rechts, der Full Join behaelt sie auf beiden Seiten. Wer diese Regel verinnerlicht, muss die vier Varianten nicht auswendig lernen, sondern kann sie aus der Grundlogik ableiten und den passenden Join-Typ fuer jede konkrete Frage gezielt auswaehlen.
Die groessten praktischen Risiken liegen nicht in der Wahl des Join-Typs selbst, sondern in fehlenden oder unvollstaendigen Join-Bedingungen, die kartesische Produkte erzeugen, und in falscher NULL-Behandlung nach Outer Joins, die einen Left Join versehentlich in einen Inner Join zurueckverwandelt. Wer nach jedem Join die Zeilenzahl prueft und WHERE-Bedingungen auf Outer-Join-Spalten bewusst formuliert, vermeidet die meisten Fehler, die in der Praxis zu falschen Reports fuehren.
Join-Typen im Vergleich, das Wichtigste auf einen Blick
Inner Join
Nur Zeilen, bei denen die Bedingung auf beiden Seiten erfuellt ist. Der haeufigste und restriktivste Join-Typ.
Left und Right Join
Behalten alle Zeilen einer Seite, fuellen fehlende Werte der anderen Seite mit NULL. Right Join ist der gespiegelte Left Join.
Full Join
Behalten Zeilen aus beiden Tabellen unabhaengig von einem Match. In MySQL vor 8.0 nur ueber UNION emulierbar.
Haeufigster Fehler
Fehlende oder unvollstaendige Join-Bedingung erzeugt ein kartesisches Produkt mit vervielfachten Zeilen.