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:

SQL customers + orders
-- 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 Bestellung

Die 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:

SQL So sieht der Bug aus
-- 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:

SQL
 name  | total
-------+-------
 Alice | 99.95

Carol 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:

SQL Korrekt — Filter in ON
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o
    ON o.customer_id = c.id
   AND o.total > 50;

Output:

SQL
 name  | total
-------+-------
 Alice | 99.95
 Bob   |  NULL    <- Bob hat keine Bestellung > 50
 Carol |  NULL    <- Carol hat überhaupt keine Bestellungen

Jetzt 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:

KlauselWirkung
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:

SQL Kunden ohne Bestellung finden
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;

Output:

SQL
 id | name
----+-------
  3 | Carol

Die 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):

SQL
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:

SQL Inkonsistenzen finden
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:

SQL
 customer_id | name  | order_id | total
-------------+-------+----------+-------
           3 | Carol |     NULL |  NULL
        NULL | NULL  |        4 |  9.95

Beide 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:

SQL
-- 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):

SQL
ON c.id IS NOT DISTINCT FROM o.customer_id

IS 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:

SQL Identische Resultate
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 ordersorders 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

/ Weiter

Zurück zu Joins & Subqueries

Zur Übersicht