MWCodebymw.de ↗
SQL

EXISTS statt IN – Unterabfragen, die auch mit NULL richtig rechnen

"Zeig mir alle Kunden, die schon mal bestellt haben" – dafür greifen die meisten zu IN (SELECT …). Das funktioniert, bis eine einzige NULL in der Unterabfrage steht: dann liefert NOT IN plötzlich gar nichts mehr. EXISTS hat dieses Problem nicht.

Wenn ich prüfen will, ob es zu einer Zeile irgendwo eine passende andere Zeile gibt, greife ich fast immer zu EXISTS. Nicht aus Gewohnheit, sondern weil IN (SELECT …) eine Falle hat, in die ich selbst schon getappt bin.

Nehmen wir zwei Tabellen: kunden und bestellungen, verbunden über bestellungen.kunde_id.

Der einfache Fall: wer hat schon bestellt?

-- Variante A: IN
SELECT name FROM kunden
WHERE id IN (SELECT kunde_id FROM bestellungen);

-- Variante B: EXISTS
SELECT name FROM kunden k
WHERE EXISTS (SELECT 1 FROM bestellungen b WHERE b.kunde_id = k.id);

Beide liefern dasselbe. EXISTS liest sich anfangs sperriger, weil es die Verbindung selbst herstellt (b.kunde_id = k.id) statt sie über die Spaltenliste zu implizieren. Dafür sagt es genau, was gemeint ist: „gibt es dazu mindestens eine Zeile?"

Das SELECT 1 ist übrigens kein Trick, sondern nur Deutlichkeit: Die Datenbank interessiert bei EXISTS gar nicht, was die Unterabfrage zurückgibt, sondern nur, ob sie etwas zurückgibt. Sie hört beim ersten Treffer auf zu suchen.

Die Falle: NOT IN und ein einziges NULL

Jetzt die Umkehrung – „welche Kunden haben noch nie bestellt?":

-- ⚠️ gefährlich
SELECT name FROM kunden
WHERE id NOT IN (SELECT kunde_id FROM bestellungen);

Solange bestellungen.kunde_id niemals NULL ist, geht das gut. Sobald aber eine einzige Zeile dort NULL stehen hat, liefert diese Abfrage null Ergebnisse – auch wenn es massenhaft Kunden ohne Bestellung gibt.

Der Grund ist die Dreiwertigkeit von SQL. x NOT IN (1, 2, NULL) wird zu x <> 1 AND x <> 2 AND x <> NULL. Der letzte Vergleich ist nicht wahr und nicht falsch, sondern unbekannt – und wahr AND unbekannt ergibt unbekannt. Die WHERE-Klausel lässt aber nur durch, was eindeutig wahr ist. Ergebnis: leere Liste, ohne Fehlermeldung.

NOT EXISTS hat das Problem nicht:

-- ✅ verlässlich, auch mit NULL in kunde_id
SELECT name FROM kunden k
WHERE NOT EXISTS (
    SELECT 1 FROM bestellungen b WHERE b.kunde_id = k.id
);

Hier wird nichts verglichen, sondern gezählt: Entweder es gibt eine passende Zeile oder nicht. NULL = 5 ist zwar auch hier unbekannt und damit kein Treffer – aber das macht die Unterabfrage nur leer, und leer heißt bei NOT EXISTS korrekt „wahr".

Wer bei NOT IN bleiben will, muss die NULLs selbst aussortieren:

SELECT name FROM kunden
WHERE id NOT IN (
    SELECT kunde_id FROM bestellungen WHERE kunde_id IS NOT NULL
);

Das funktioniert – aber man muss daran denken. Bei NOT EXISTS muss man an nichts denken.

Praxisbeispiel: aussortieren, was schon erledigt ist

Eine Abfrage, die ich fast wörtlich so einsetze – offene Rechnungen, zu denen noch keine Zahlung verbucht ist:

SELECT r.nummer, r.betrag, r.faellig_am
FROM rechnungen r
WHERE r.storniert = 0
  AND NOT EXISTS (
      SELECT 1 FROM zahlungen z
      WHERE z.rechnung_id = r.id
        AND z.betrag >= r.betrag
  )
ORDER BY r.faellig_am;

Schön daran: In die Unterabfrage passen beliebig viele Zusatzbedingungen (z.betrag >= r.betrag), ohne dass die äußere Abfrage komplizierter wird. Mit IN müsste man dafür eine kombinierte Werteliste basteln – schnell unleserlich.

Was ist mit der Geschwindigkeit?

Die alte Faustregel „EXISTS ist schneller als IN" stimmt heute so nicht mehr. Moderne Planer – auch der von SQLite und PostgreSQL – schreiben beide Formen oft in dieselbe interne Operation um. Verlass dich nicht auf Gerüchte, sondern schau nach:

EXPLAIN QUERY PLAN
SELECT name FROM kunden k
WHERE NOT EXISTS (SELECT 1 FROM bestellungen b WHERE b.kunde_id = k.id);

Wichtiger als die Wahl zwischen IN und EXISTS ist fast immer ein Index auf der verbundenen Spalte – hier bestellungen(kunde_id). Ohne ihn liest die Datenbank für jeden Kunden die ganze Bestelltabelle durch.

Meine Regel

Ich nehme IN, wenn ich gegen eine feste, kurze Werteliste prüfe (WHERE status IN ('offen','geprüft')). Sobald eine Unterabfrage im Spiel ist – und erst recht bei der verneinten Form – nehme ich EXISTS beziehungsweise NOT EXISTS. Das ist ein Reflex, der mich schon zweimal vor einer stillen Fehlauswertung bewahrt hat, und stille Fehlauswertungen sind die schlimmsten: Sie sehen aus wie ein Ergebnis.

Du hast eine Abfrage, die manchmal seltsam wenig zurückgibt, und findest den Grund nicht? Das ist erstaunlich oft genau dieses NULL-Thema. Schreib mir, wenn du bei so etwas mal ein zweites Paar Augen brauchst.

Quellen

#SQL#EXISTS#NOT EXISTS#Unterabfrage#NULL

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →