Dubletten durch JOINs vermeiden: Wann EXISTS in Oracle SQL die bessere Wahl ist

Doppelte Ergebniszeilen gehören zu den häufigsten Problemen bei komplexeren SQL-Abfragen. Besonders oft treten sie auf, sobald mehrere Tabellen miteinander verknüpft werden.

Eine Abfrage liefert zunächst genau einen Datensatz pro Kunde, Vertrag oder Auftrag. Nach dem Hinzufügen eines weiteren JOIN erscheinen einzelne Datensätze plötzlich doppelt, dreifach oder noch häufiger.

Der erste Reflex besteht häufig darin, ein DISTINCT vor die ausgewählten Spalten zu setzen. Dadurch verschwinden sichtbare Duplikate zwar möglicherweise, die eigentliche Ursache bleibt jedoch bestehen.

In vielen Fällen wird die verbundene Tabelle gar nicht benötigt, um zusätzliche Daten auszugeben. Es soll lediglich geprüft werden, ob dort mindestens ein passender Datensatz existiert. Für solche Prüfungen ist EXISTS meistens präziser als ein normaler JOIN.

Dieser Artikel zeigt, warum JOINs Ergebnisse vervielfachen, wie sich die Ursache systematisch feststellen lässt und wann EXISTS die bessere Lösung darstellt.

1. Warum entstehen durch JOINs doppelte Datensätze?

Ein JOIN verbindet nicht einfach zwei Tabellen miteinander. Er erzeugt für jede passende Kombination der beteiligten Datensätze eine Ergebniszeile.

Angenommen, eine Kundentabelle enthält einen Datensatz pro Kunde:

SELECT k.kunden_id, k.name
FROM kunde k;

Beispieldaten:

kunden_idname
1001Beispiel GmbH
1002Muster AG

Nun existiert zusätzlich eine Tabelle mit Kundenkontakten:

kontakt_idkunden_idkontaktart
11001E-Mail
21001Telefon
31002E-Mail

Werden beide Tabellen verbunden, entsteht für jeden passenden Kontakt eine eigene Zeile:

SELECT k.kunden_id, k.name
FROM kunde k
JOIN kundenkontakt kk ON kk.kunden_id = k.kunden_id;

Das Ergebnis lautet:

kunden_idname
1001Beispiel GmbH
1001Beispiel GmbH
1002Muster AG

Der Kunde mit der ID 1001 erscheint zweimal, weil in der Tabelle kundenkontakt zwei passende Zeilen vorhanden sind.

SQL hat hier keinen Fehler gemacht. Das Ergebnis entspricht exakt der definierten Verknüpfung.

Der entscheidende Punkt lautet:

Ein JOIN liefert nicht automatisch eine Zeile pro Datensatz der Haupttabelle, sondern eine Zeile pro gefundener Kombination.

Die Kardinalität einer Beziehung verstehen

Vor dem Hinzufügen eines JOINs sollte geklärt werden, welche Beziehung zwischen den Tabellen besteht.

Typische Beziehungen sind:

Eins zu eins

Zu jedem Datensatz der Haupttabelle existiert höchstens ein Datensatz in der verbundenen Tabelle.

Beispiel:

kunde 1 : 1 kundenprofil

Ein solcher JOIN führt normalerweise nicht zu einer Vervielfachung.

Eins zu viele

Zu einem Datensatz der Haupttabelle können mehrere Datensätze in der verbundenen Tabelle existieren.

Beispiel:

kunde 1 : n kundenkontakt

Ein JOIN kann den Kundendatensatz dadurch mehrfach ausgeben.

Viele zu viele

Mehrere Datensätze der ersten Tabelle können mit mehreren Datensätzen der zweiten Tabelle verbunden sein.

Beispiel:

benutzer n : m rolle

Solche Beziehungen werden meistens über eine Zwischentabelle umgesetzt und können besonders starke Vervielfachungen verursachen.

Ein typischer Fehler aus der Praxis

Es sollen alle aktiven Kunden ausgegeben werden, für die mindestens eine gültige Zustimmung vorliegt.

Die Tabellenstruktur könnte folgendermaßen aussehen:

kunde-----kunden_idnamestatuszustimmung----------zustimmung_idkunden_idzustimmungsartgueltig_vongueltig_bis

Eine erste Abfrage wird mit einem JOIN geschrieben:

SELECT    k.kunden_id,    k.nameFROM kunde kJOIN zustimmung z    ON z.kunden_id = k.kunden_idWHERE k.status = 'AKTIV'  AND z.zustimmungsart = 'WERBUNG'  AND z.gueltig_von <= SYSDATE  AND (        z.gueltig_bis IS NULL        OR z.gueltig_bis >= SYSDATE      );

Diese Abfrage funktioniert nur dann ohne Dubletten, wenn für jeden Kunden höchstens eine passende Zustimmung existiert.

Besitzt ein Kunde mehrere gültige Zustimmungsdatensätze, erscheint er mehrfach.

Das kann beispielsweise durch folgende Situationen entstehen:

  • mehrere historische Datensätze,
  • sich überschneidende Gültigkeitszeiträume,
  • verschiedene technische Versionen,
  • doppelte Datenerfassung,
  • mehrere passende Zustimmungstypen,
  • fehlende Einschränkungen in der JOIN-Bedingung.

Die fachliche Anforderung lautet jedoch nicht:

Gib jede gültige Zustimmung mit dem zugehörigen Kunden aus.

Sie lautet:

Gib jeden aktiven Kunden aus, für den mindestens eine gültige Zustimmung existiert.

Das ist eine Existenzprüfung. Dafür ist EXISTS vorgesehen.

Die gleiche Prüfung mit EXISTS

Die Abfrage kann folgendermaßen formuliert werden:

SELECT    k.kunden_id,    k.name
FROM kunde k
WHERE k.status = 'AKTIV'  
AND EXISTS (        
SELECT 1        
FROM zustimmung z        
WHERE z.kunden_id = k.kunden_id          
AND z.zustimmungsart = 'WERBUNG'          
AND z.gueltig_von <= SYSDATE          
AND (z.gueltig_bis IS NULL OR z.gueltig_bis >= SYSDATE)
);

Die äußere Abfrage liest ausschließlich aus der Tabelle kunde.

Für jeden Kunden wird geprüft, ob in zustimmung mindestens ein passender Datensatz vorhanden ist.

Sobald Oracle einen Treffer findet, ist die Bedingung erfüllt. Es spielt keine Rolle, ob ein oder zehn passende Zustimmungsdatensätze existieren.

Dadurch bleibt das gewünschte Ergebnis erhalten:

eine Ergebniszeile pro Kunde

Was bedeutet SELECT 1 innerhalb von EXISTS?

Innerhalb einer EXISTS-Abfrage wird häufig SELECT 1 verwendet:

EXISTS (    SELECT 1    FROM zustimmung z    WHERE z.kunden_id = k.kunden_id)

Die Zahl 1 hat keine fachliche Bedeutung. Sie macht lediglich deutlich, dass keine Spalte aus der Unterabfrage benötigt wird.

Folgende Varianten wären logisch ebenfalls möglich:

SELECT *
SELECT z.kunden_id
SELECT NULL

Für EXISTS zählt ausschließlich, ob die Unterabfrage mindestens eine Zeile liefert.

Die verbreitete Schreibweise SELECT 1 ist deshalb sinnvoll, weil sie die Absicht der Abfrage deutlich macht:

Es soll nur die Existenz eines Datensatzes geprüft werden.

JOIN und EXISTS erfüllen unterschiedliche Aufgaben

Ein JOIN ist nicht grundsätzlich falsch. JOIN und EXISTS lösen unterschiedliche fachliche Probleme.

JOIN verwenden

Ein JOIN ist passend, wenn Spalten aus der verbundenen Tabelle benötigt werden.

Beispiel:

SELECT    k.kunden_id,    k.name,    z.zustimmungsart,    z.gueltig_von
FROM kunde k
JOIN zustimmung z    ON z.kunden_id = k.kunden_id
WHERE z.zustimmungsart = 'WERBUNG';

Hier werden Daten aus der Tabelle zustimmung ausgegeben. Ein JOIN ist daher notwendig.

Mehrere Ergebniszeilen pro Kunde können fachlich korrekt sein, wenn jede Zustimmung einzeln angezeigt werden soll.

EXISTS verwenden

EXISTS ist passend, wenn ausschließlich geprüft werden soll, ob ein verbundener Datensatz vorhanden ist.

Beispiel:

SELECT    k.kunden_id,    k.name
FROM kunde k
WHERE EXISTS (    SELECT 1    FROM zustimmung z    WHERE z.kunden_id = k.kunden_id);

Hier werden keine Spalten aus zustimmung benötigt. Die Tabelle dient ausschließlich als Filter.

Warum DISTINCT häufig nur das Symptom verdeckt

Eine häufige Reaktion auf doppelte Zeilen lautet:

SELECT DISTINCT    k.kunden_id,    k.name
FROM kunde k
JOIN zustimmung z    ON z.kunden_id = k.kunden_id;

Bei identischen Ausgabespalten verschwinden die sichtbaren Dubletten.

Das bedeutet jedoch nicht, dass die Verknüpfung fachlich richtig ist.

DISTINCT wird erst nach der Erzeugung des Zwischenergebnisses angewendet. Oracle kann zunächst mehrere Kombinationen erzeugen und muss diese anschließend wieder zusammenfassen.

Problematisch wird es insbesondere dann, wenn später eine weitere Spalte ergänzt wird:

SELECT DISTINCT    k.kunden_id,    k.name,    z.zustimmung_id
FROM kunde k
JOIN zustimmung z    ON z.kunden_id = k.kunden_id;

Da sich die zustimmung_id unterscheidet, bleiben die Zeilen trotz DISTINCT mehrfach vorhanden.

DISTINCT sollte verwendet werden, wenn fachlich tatsächlich identische Ergebniszeilen entfernt werden sollen. Es sollte nicht als Standardreparatur für unklare JOIN-Vervielfachungen dienen.

Vorsicht bei NOT IN und NULL-Werten

Für Ausschlussprüfungen wird gelegentlich NOT IN verwendet:

SELECT    k.kunden_id
FROM kunde k
WHERE k.kunden_id NOT IN (    SELECT ks.kunden_id    FROM kundensperre ks);

Enthält die Unterabfrage einen NULL-Wert, kann das Ergebnis unerwartet ausfallen. Aufgrund der dreiwertigen SQL-Logik ist der Vergleich dann nicht eindeutig wahr oder falsch.

Robuster ist:

SELECT    k.kunden_id
FROM kunde kWHERE NOT EXISTS (    SELECT 1    FROM kundensperre ks    WHERE ks.kunden_id = k.kunden_id);

Bei korrelierten Ausschlussprüfungen ist NOT EXISTS deshalb meistens die sicherere Schreibweise.

EXISTS und Performance in Oracle

Bei EXISTS wird häufig angenommen, Oracle führe die Unterabfrage für jede Zeile der Haupttabelle vollständig neu aus.

Der Oracle Optimizer kann korrelierte Unterabfragen jedoch intern umformen. Häufig entsteht daraus ein sogenannter Semi Join.

Ein Semi Join liefert Datensätze der Haupttabelle, sobald mindestens ein passender Datensatz auf der Gegenseite gefunden wurde. Weitere Treffer müssen für die reine Existenzprüfung nicht als zusätzliche Ergebniszeilen ausgegeben werden.

Die tatsächliche Performance hängt unter anderem von folgenden Faktoren ab:

  • Anzahl der Datensätze,
  • Selektivität der Filterbedingungen,
  • vorhandenen Indizes,
  • Statistiken,
  • Datenverteilung,
  • JOIN-Reihenfolge,
  • gewähltem Ausführungsplan.

Ein sinnvoller Index könnte beispielsweise lauten:

CREATE INDEX ix_zustimmung_kunde_art    ON zustimmung (        kunden_id,        zustimmungsart    );

Ob dieser Index optimal ist, hängt von den konkreten Abfragen und Daten ab. Indizes sollten nicht ausschließlich aufgrund einer einzelnen Beispielabfrage angelegt werden.

Checkliste zur Fehleranalyse

Bei unerwarteten Dubletten sollte die Abfrage in dieser Reihenfolge geprüft werden:

  1. Welcher Schlüssel soll im Ergebnis eindeutig sein?
  2. Ist dieser Schlüssel bereits in der Haupttabelle eindeutig?
  3. Nach welchem JOIN steigt die Anzahl der Ergebniszeilen?
  4. Wie viele passende Detaildatensätze existieren pro Schlüssel?
  5. Ist die JOIN-Bedingung vollständig?
  6. Fehlen Gültigkeits-, Status- oder Versionsbedingungen?
  7. Werden Spalten aus der Detailtabelle tatsächlich benötigt?
  8. Soll nur geprüft werden, ob mindestens ein Datensatz existiert?
  9. Muss ein konkreter Detaildatensatz ausgewählt werden?
  10. Verdeckt DISTINCT möglicherweise nur eine fehlerhafte Verknüpfung?

Häufig gestellte Fragen

Verhindert EXISTS immer doppelte Datensätze?

EXISTS erzeugt durch die geprüfte Unterabfrage keine zusätzlichen Ergebniszeilen in der äußeren Abfrage. Sind die Datensätze der Haupttabelle selbst bereits doppelt oder wird sie durch andere JOINs vervielfacht, können weiterhin Dubletten auftreten.

Ist EXISTS immer schneller als JOIN?

Nein. Die Performance hängt vom Ausführungsplan, den Indizes, den Statistiken und der Datenverteilung ab. EXISTS ist vor allem dann fachlich passend, wenn ausschließlich die Existenz eines Datensatzes geprüft wird.

Ist DISTINCT grundsätzlich schlecht?

Nein. DISTINCT ist korrekt, wenn fachlich unterschiedliche Quellzeilen zu einer eindeutigen Ergebnismenge zusammengeführt werden sollen. Problematisch ist der Einsatz als pauschale Reparatur unbekannter JOIN-Dubletten.

Kann ich Spalten aus der EXISTS-Unterabfrage ausgeben?

Nein. Die Unterabfrage dient nur als Bedingung. Werden Spalten aus der Detailtabelle benötigt, ist ein JOIN, eine skalare Unterabfrage oder eine andere gezielte Auswahlmethode erforderlich.

Was ist der Unterschied zwischen EXISTS und IN?

Beide können ähnliche Prüfungen ausdrücken. EXISTS eignet sich besonders für korrelierte Unterabfragen und komplexe Bedingungen. Bei Ausschlussprüfungen ist NOT EXISTS in Verbindung mit möglichen NULL-Werten häufig robuster als NOT IN.

Warum liefert ein LEFT JOIN ebenfalls mehrere Zeilen?

Auch ein LEFT JOIN erzeugt für jeden passenden Datensatz der rechten Tabelle eine eigene Kombination. Der Unterschied besteht lediglich darin, dass Datensätze der linken Tabelle auch ohne Treffer erhalten bleiben.

Fazit

Dubletten nach einem JOIN sind meistens keine zufälligen SQL-Fehler. Sie entstehen aus der Kardinalität der verbundenen Tabellen und den definierten Verknüpfungsbedingungen.

Ein JOIN ist richtig, wenn Daten aus mehreren Tabellen gemeinsam ausgegeben werden sollen. Wird eine Tabelle jedoch nur verwendet, um das Vorhandensein eines passenden Datensatzes zu prüfen, beschreibt EXISTS die fachliche Anforderung meist genauer.

Die wichtigste Regel lautet:

JOIN für die Ausgabe verbundener Daten, EXISTS für reine Existenzprüfungen.

Statt unerwartete Mehrfachzeilen sofort mit DISTINCT zu entfernen, sollte zuerst untersucht werden, welcher JOIN die Ergebnismenge vergrößert. Dadurch entstehen verständlichere Abfragen, eindeutigere Fachlogik und häufig auch stabilere Ausführungspläne.