OUTER JOINs sehen einfach aus, haben aber ein paar Details, die in der Praxis regelmäßig stolpern lassen — vor allem die Frage „warum wird mein LEFT JOIN plötzlich zum INNER JOIN?". Der Artikel klärt das NULL-Verhalten, den Unterschied zwischen Filter in ON und WHERE, und das Standard-Pattern für „nur Zeilen ohne Match“.
Schnell-Wiederholung
Mit denselben Daten wie im Inner/Outer-Grundlagen-Artikel:
-- Carol hat keine Bestellung, eine Bestellung hat NULL als customer_id
SELECT c.name, o.id AS order_id, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
name | order_id | total
-------+----------+-------
Alice | 1 | 99.95
Alice | 2 | 49.95
Bob | 3 | 19.95
Carol | NULL | NULL <- Carol ohne BestellungDie häufigste Falle: WHERE macht aus LEFT ein INNER
Wer einen LEFT JOIN hat und dann eine Bedingung auf die rechte Tabelle in WHERE schreibt, verliert die NULL-Zeilen — und damit den OUTER-Effekt:
-- Versuch: alle Kunden, mit Bestellungen über 50 Euro
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.total > 50;Output:
name | total
-------+-------
Alice | 99.95Carol ist verschwunden! Bob auch. Der Grund: für Carol war o.total ja NULL. Und NULL > 50 ist nicht TRUE, also fällt sie aus dem WHERE raus. Effektiv ist der LEFT JOIN zum INNER JOIN geworden.
Lösung: die Filter-Bedingung in die ON-Klausel verschieben:
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.total > 50;Output:
name | total
-------+-------
Alice | 99.95
Bob | NULL <- Bob hat keine Bestellung > 50
Carol | NULL <- Carol hat überhaupt keine BestellungenJetzt zeigt das Resultat, was wir wollten: alle Kunden, mit ihrer großen Bestellung — falls vorhanden.
ON vs. WHERE — die Regel
Der Unterschied ist subtil aber zentral:
| Klausel | Wirkung |
|---|---|
ON … | Bedingung gilt während des Joins. Zeilen, die nicht matchen, werden bei OUTER JOINs trotzdem mit NULL ergänzt. |
WHERE … | Bedingung filtert das Resultat nach dem Join. NULL-Zeilen aus OUTER können dadurch wieder rausfallen. |
Faustregel:
- Filter auf die rechte Tabelle eines LEFT JOIN → in ON.
- Filter auf die linke Tabelle eines LEFT JOIN → in WHERE (oder ON, beides funktioniert).
- Filter auf NULL-Status (
WHERE o.id IS NULL) → in WHERE (siehe nächster Abschnitt).
Pattern: „nur Zeilen ohne Match“
Wenn du wissen willst, welche Zeilen kein Match haben, ist das LEFT-JOIN-mit-NULL-Filter-Pattern der Standard:
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;Output:
id | name
----+-------
3 | CarolDie Logik: LEFT JOIN ergänzt Carol mit NULL für o.id. Das WHERE o.id IS NULL filtert dann auf genau diese Nicht-Match-Zeilen. Carol hat keine id aus orders bekommen → sie kommt durch.
Wichtig: filter auf eine NOT-NULL-Spalte der rechten Tabelle (z. B. den Primary Key o.id). Wenn man auf eine optional-NULL-Spalte filtert, verwechselt man „kein Match" mit „NULL-Wert in der Spalte".
Alternative: NOT EXISTS (oft sauberer und etwa gleich schnell):
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);Mehr dazu im Artikel EXISTS vs. IN vs. JOIN.
FULL OUTER JOIN für Datenqualitäts-Reports
FULL OUTER JOIN zeigt beide Arten von Nicht-Matches gleichzeitig:
SELECT
c.id AS customer_id,
c.name,
o.id AS order_id,
o.total
FROM customers c
FULL OUTER JOIN orders o ON o.customer_id = c.id
WHERE c.id IS NULL OR o.id IS NULL;Output:
customer_id | name | order_id | total
-------------+-------+----------+-------
3 | Carol | NULL | NULL
NULL | NULL | 4 | 9.95Beide Probleme sichtbar in einer Query: Kunden ohne Bestellung und Bestellungen ohne Kunde. Für ETL-Validierung oder „warum stimmt mein Report nicht?"-Analysen sehr nützlich.
NULL-Werte im Join — wann matcht NULL?
Wichtige Eigenheit: NULL = NULL ist nicht TRUE in SQL. Wer also einen FK auf eine Spalte mit NULL-Werten joint:
-- orders mit customer_id NULL match NICHT auf c.id NULL
-- (auch wenn beide Spalten NULL wären)
SELECT c.name, o.id
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;NULL-Werte fallen aus jeder normalen Equality-Bedingung. Wer NULL-Werte als „gleich" behandeln will (selten gewollt):
ON c.id IS NOT DISTINCT FROM o.customer_idIS NOT DISTINCT FROM behandelt zwei NULLs als gleich. Funktioniert, ist aber kaum index-fähig — Performance leidet auf großen Tabellen. Mehr im Artikel NULL und Three-Valued Logic.
RIGHT JOIN ist (fast) immer als LEFT umschreibbar
A RIGHT JOIN B ist semantisch identisch zu B LEFT JOIN A — nur die Tabellen sind getauscht:
SELECT c.name, o.total
FROM customers c
RIGHT JOIN orders o ON o.customer_id = c.id;
-- Identisch zu:
SELECT c.name, o.total
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id;In großen Codebasen siehst du fast nie RIGHT JOIN — alle formulieren um auf LEFT JOIN. Lesbar einheitlicher.
Häufige Stolperfallen
WHERE auf rechter Tabelle bei LEFT JOIN — der Klassiker.
LEFT JOIN orders o ON … WHERE o.status = 'paid' filtert die Carol-Zeilen (mit NULL-status) raus und macht aus dem LEFT effektiv ein INNER. Lösung: Bedingung in die ON-Klausel: LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid'.
WHERE col IS NULL mit fehlenden NOT NULL-Constraints.
Das „nur Zeilen ohne Match“-Pattern (WHERE o.id IS NULL) funktioniert nur sicher, wenn o.id eine NOT-NULL-Spalte ist (typisch: Primary Key). Filter auf eine optional-NULL-Spalte würde echte NULLs in der Datenbank mit „kein Match" verwechseln. Immer auf einen NOT-NULL-Schlüssel filtern.
Reihenfolge der Tabellen ist bei OUTER JOIN bedeutsam.
customers LEFT JOIN orders ≠ orders LEFT JOIN customers. Erstere behält alle Kunden, letztere alle Bestellungen. Beim Refactoring (z. B. Spalten umsortieren in der Auswahl) ja nicht versehentlich die Tabellen vertauschen.
NULL = NULL matcht NICHT.
Auch in OUTER JOINs nicht. Bestellungen mit customer_id = NULL matchen NIE einen Kunden — auch keinen mit id = NULL. Wenn du das willst, brauchst du IS NOT DISTINCT FROM (NULL-tolerantes Vergleichen). Performance leidet aber meist.
Mehrere LEFT JOINs hintereinander = Multiplikation.
customers LEFT JOIN orders LEFT JOIN order_items kann pro Kunde sehr viele Zeilen produzieren. Wenn man pro Kunde eine Zeile will, hilft GROUP BY mit Aggregaten oder DISTINCT ON (siehe SELECT und WHERE). Oder Subqueries im SELECT.
FULL OUTER JOIN ist teurer als INNER und LEFT.
Postgres muss für FULL OUTER beide Seiten komplett scannen — kein Streaming-Plan möglich. Bei riesigen Tabellen lohnt es sich, FULL OUTER nur für gezielte Datenqualitäts-Reports zu nutzen, nicht im laufenden Betrieb.
Cloud-Postgres-Kompatibilität.
Alle behandelten Join-Typen sind Standard-SQL und funktionieren auf RDS, Cloud SQL, Azure Database, Supabase, Neon, Crunchy etc. Kein Anbieter-spezifisches Verhalten zu beachten — anders als bei Schema-Owner oder Backup-Themen.
Weiterführende Ressourcen
Externe Quellen
- Joined Tables – PostgreSQL Documentation
- Don't Do This: bare column reference – PostgreSQL Wiki
- Comparison Operators – IS NOT DISTINCT FROM