Self-Join: praktische Anwendungsfaelle jenseits der Theorie
AI generated
SELECT
JOIN
SQL · Datenbanken · Self-Join
Self-Join: praktische Anwendungsfaelle
jenseits der reinen Theorie

Ein Self-Join verknuepft eine Tabelle mit sich selbst und wirkt auf den ersten Blick wie eine akademische Uebung, ist in der Praxis aber das Werkzeug fuer drei sehr konkrete Probleme: hierarchische Beziehungen wie Mitarbeiter und Vorgesetzte abbilden, doppelte Datensaetze anhand fachlicher Kriterien aufspueren und aufeinanderfolgende Zeilen, etwa Ereignisse oder Preise, direkt miteinander vergleichen, ohne dafuer prozedurale Schleifen zu benoetigen.

13 Min. Lesezeit Self-Join · Hierarchien · Duplikate · Zeilenvergleiche Standard-SQL · MySQL · PostgreSQL · SQL Server

1. Die Grundidee: eine Tabelle mit sich selbst verknuepfen

Ein Self-Join ist technisch kein eigener Join-Typ, sondern ein normaler INNER JOIN oder LEFT JOIN, bei dem die Quell- und die Zieltabelle identisch sind. Der einzige Unterschied zu einem gewoehnlichen Join ist, dass beide Tabellenverweise mit unterschiedlichen Aliassen versehen werden muessen, weil die Datenbank sonst nicht unterscheiden kann, welche Instanz der Tabelle in welcher Spalte gemeint ist. Ein SELECT ohne eindeutige Aliasse auf eine selbst-verknuepfte Tabelle fuehrt entweder zu einem Syntaxfehler oder zu einer mehrdeutigen Spaltenreferenz.

Die konzeptionelle Huerde beim Self-Join ist selten die Syntax, sondern das mentale Modell: man muss sich vorstellen, zwei vollstaendig unabhaengige Kopien derselben Tabelle vor sich liegen zu haben, etwa employees AS e und employees AS m, und dann eine Beziehung zwischen einer Zeile aus der ersten Kopie und einer Zeile aus der zweiten Kopie zu formulieren. Sobald dieses Bild sitzt, unterscheidet sich ein Self-Join in der Anwendung nicht von einem Join zwischen zwei unterschiedlichen Tabellen.

Drei Anwendungsfaelle decken in der Praxis den grossen Teil aller Self-Join-Einsaetze ab: hierarchische Strukturen, bei denen eine Zeile auf eine andere Zeile derselben Tabelle verweist, fachliche Duplikaterkennung, bei der Zeilen mit gleichen inhaltlichen Merkmalen, aber unterschiedlichen Primaerschluesseln gefunden werden sollen, und der direkte Vergleich benachbarter Zeilen in einer sortierten Reihenfolge, etwa fuer Zeitreihen oder Rangfolgen.

2. Mitarbeiter und Vorgesetzte: die klassische Hierarchie

Der bekannteste Anwendungsfall fuer einen Self-Join ist eine Tabelle wie employees mit einer Spalte manager_id, die auf die id-Spalte derselben Tabelle verweist. Um fuer jeden Mitarbeiter den Namen seines Vorgesetzten in derselben Zeile anzuzeigen, verknuepft man die Tabelle mit sich selbst ueber e.manager_id = m.id. Ohne diese Technik muesste man entweder mit einer Anwendungslogik nachladen oder eine umstaendliche Subquery pro Zeile schreiben, was bei groesseren Tabellen deutlich langsamer ist.

Ein wichtiger Unterschied liegt in der Wahl zwischen INNER JOIN und LEFT JOIN beim Self-Join: Verwendet man INNER JOIN, verschwinden Mitarbeiter ohne Vorgesetzten, typischerweise die Geschaeftsfuehrung an der Spitze der Hierarchie, komplett aus dem Ergebnis, weil manager_id dort NULL ist und kein Match findet. Ein LEFT JOIN behaelt diese Zeilen und zeigt stattdessen NULL als Vorgesetztennamen, was in den meisten Reporting-Szenarien das gewuenschte Verhalten ist.


-- Sample table: employees
-- id | name          | manager_id
-- 1  | Sarah Fuchs    | NULL       <- top of the hierarchy
-- 2  | Jonas Herrmann | 1
-- 3  | Mia Brandt     | 1
-- 4  | Paul Reimann   | 2

-- Self-join: show each employee alongside their manager's name
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id
ORDER BY e.id;

-- employee        | manager
-- Sarah Fuchs      | NULL
-- Jonas Herrmann    | Sarah Fuchs
-- Mia Brandt        | Sarah Fuchs
-- Paul Reimann      | Jonas Herrmann

3. Mehrstufige Hierarchien und ihre Grenzen

Ein einzelner Self-Join loest genau eine Hierarchieebene. Um zwei Ebenen aufzuloesen, etwa den Vorgesetzten des Vorgesetzten zu ermitteln, muss man einen weiteren Self-Join auf dieselbe Tabelle mit einem dritten Alias hinzufuegen. Das funktioniert fuer eine feste, bekannte Anzahl an Ebenen zuverlaessig, wird aber schnell unhandlich, sobald die Hierarchietiefe variabel ist, etwa in einer Organisationsstruktur mit unterschiedlich vielen Fuehrungsebenen je Abteilung.

Fuer beliebig tiefe, nicht im Voraus bekannte Hierarchien ist ein mehrfacher Self-Join nicht mehr die richtige Loesung, sondern eine rekursive Common Table Expression mit WITH RECURSIVE, die in PostgreSQL, SQL Server, Oracle und seit Version 8.0 auch in MySQL verfuegbar ist. Der Self-Join bleibt trotzdem die richtige Wahl fuer alle Faelle mit fester, bekannter Tiefe, weil er einfacher zu lesen und meist auch schneller auszufuehren ist als eine rekursive Abfrage.


-- Two levels: employee, direct manager, and manager's manager
SELECT
  e.name AS employee,
  m1.name AS manager,
  m2.name AS manager_of_manager
FROM employees e
LEFT JOIN employees m1 ON e.manager_id = m1.id
LEFT JOIN employees m2 ON m1.manager_id = m2.id
ORDER BY e.id;

-- employee        | manager       | manager_of_manager
-- Sarah Fuchs      | NULL          | NULL
-- Jonas Herrmann    | Sarah Fuchs   | NULL
-- Mia Brandt        | Sarah Fuchs   | NULL
-- Paul Reimann      | Jonas Herrmann | Sarah Fuchs

4. Duplikate finden: fachliche statt technischer Gleichheit

Ein zweiter praktischer Anwendungsfall fuer den Self-Join ist das Aufspueren von Duplikaten, die sich nicht ueber den Primaerschluessel definieren, sondern ueber fachliche Kriterien, etwa zwei Kundeneintraege mit derselben E-Mail-Adresse, aber unterschiedlicher id. Ein Self-Join ueber das Duplikat-Kriterium mit einer zusaetzlichen Bedingung a.id < b.id findet genau solche Paare, ohne dass eine Zeile mit sich selbst verglichen wird und ohne dass jedes Paar doppelt, einmal in jeder Reihenfolge, erscheint.

Die Bedingung a.id < b.id ist hier entscheidend und wird haeufig vergessen. Ohne sie liefert der Self-Join jede Zeile gepaart mit sich selbst, weil a.email = b.email auch fuer a.id = b.id wahr ist, und zusaetzlich jedes echte Duplikat-Paar zweimal, einmal als (a, b) und einmal als (b, a). Mit dem Ungleichheitsvergleich statt eines reinen <> wird beides in einem Schritt geloest: Selbstvergleiche verschwinden und jedes Paar erscheint genau einmal.


-- Sample table: customers
-- id | name          | email
-- 1  | Anna Berger    | anna@example.com
-- 2  | A. Berger      | anna@example.com   <- duplicate email
-- 3  | Tom Keller     | tom@example.com
-- 4  | Thomas Keller  | tom@example.com    <- duplicate email

-- Self-join to find duplicate emails, each pair listed once
SELECT a.id AS id_a, a.name AS name_a, b.id AS id_b, b.name AS name_b, a.email
FROM customers a
JOIN customers b ON a.email = b.email AND a.id < b.id;

-- id_a | name_a       | id_b | name_b        | email
-- 1    | Anna Berger  | 2    | A. Berger     | anna@example.com
-- 3    | Tom Keller   | 4    | Thomas Keller | tom@example.com

5. Duplikate bereinigen, ohne alle Zeilen zu verlieren

Nach dem Finden von Duplikaten mit einem Self-Join folgt in der Praxis oft die Bereinigung, also das Loeschen aller bis auf eine Zeile je Duplikatgruppe. Auch hier eignet sich ein Self-Join ausgezeichnet: man loescht jede Zeile a, fuer die eine Zeile b mit gleichem Duplikat-Kriterium und kleinerer id existiert, so bleibt jeweils die aelteste Zeile je Gruppe erhalten und alle neueren Duplikate werden entfernt.

Dieses Muster ist deutlich sicherer als ein naiver DISTINCT-Ansatz, weil es explizit steuert, welche Zeile ueberlebt, statt sich auf eine implizite Reihenfolge zu verlassen. Vor dem tatsaechlichen Loeschen empfiehlt es sich immer, dieselbe Bedingung zuerst als SELECT auszufuehren, um die betroffenen Zeilen zu pruefen, bevor man sie unwiderruflich entfernt.


-- Preview before deleting: rows that would be removed
SELECT a.*
FROM customers a
JOIN customers b ON a.email = b.email AND a.id > b.id;

-- Delete newer duplicates, keep the row with the lowest id per email
DELETE a FROM customers a
JOIN customers b ON a.email = b.email AND a.id > b.id;

-- Portable version without vendor-specific DELETE ... JOIN syntax
DELETE FROM customers a
WHERE EXISTS (
  SELECT 1 FROM customers b
  WHERE b.email = a.email AND b.id < a.id
);

6. Aufeinanderfolgende Zeilen vergleichen

Der dritte grosse Anwendungsfall fuer den Self-Join ist der direkte Vergleich benachbarter Zeilen in einer sortierten Reihenfolge, etwa um zu pruefen, ob ein Preis von einem Tag zum naechsten gestiegen ist oder wie viel Zeit zwischen zwei aufeinanderfolgenden Ereignissen eines Nutzers vergangen ist. Man verknuepft die Tabelle mit sich selbst so, dass die zweite Kopie die jeweils naechste oder vorherige Zeile nach einem Sortierkriterium repraesentiert.

Bei fortlaufenden Ganzzahl-IDs ist die Bedingung b.id = a.id + 1 ausreichend, bei Datumswerten oder luechenhaften IDs braucht man stattdessen eine Bedingung, die die naechstgroessere Zeile ueber ein Minimum oder eine Rangfolge findet. Diese Technik loest Probleme, die ohne Self-Join oft in einer prozeduralen Schleife mit Cursor abgebildet wuerden, direkt als deklarative, mengenbasierte Abfrage.


-- Sample table: daily_prices
-- id | trade_date | price
-- 1  | 2026-07-01 | 100.00
-- 2  | 2026-07-02 | 104.00
-- 3  | 2026-07-03 | 98.00
-- 4  | 2026-07-04 | 98.00

-- Self-join on consecutive integer ids to compute day-over-day change
SELECT
  curr.trade_date,
  curr.price AS today_price,
  prev.price AS yesterday_price,
  curr.price - prev.price AS change
FROM daily_prices curr
JOIN daily_prices prev ON curr.id = prev.id + 1
ORDER BY curr.trade_date;

-- trade_date | today_price | yesterday_price | change
-- 2026-07-02 | 104.00      | 100.00          | 4.00
-- 2026-07-03 | 98.00       | 104.00          | -6.00
-- 2026-07-04 | 98.00       | 98.00           | 0.00

7. Luecken und Serien in Zeitreihen erkennen

Eine Erweiterung des Zeilenvergleichs aus Abschnitt sechs ist die Erkennung von Luecken in einer fortlaufenden Serie, etwa fehlende Tage in einem taeglichen Messwertprotokoll oder fehlende Rechnungsnummern in einer eigentlich luecklosen Nummerierung. Ein Self-Join, der jede Zeile mit der naechsten nach Sortierkriterium verknuepft und die Differenz berechnet, macht Luecken sichtbar, sobald die Differenz groesser ist als der erwartete Schritt.

Umgekehrt laesst sich mit demselben Grundmuster auch die laengste zusammenhaengende Serie identischer Werte finden, etwa mehrere aufeinanderfolgende Tage mit identischem Preis wie im Beispiel oben am 3. und 4. Juli. Man vergleicht jede Zeile mit der vorherigen, markiert Zeilen mit gleichem Wert und gruppiert anschliessend zusammenhaengende markierte Abschnitte, ein Muster, das haeufig als Vorstufe zu einer Window-Funktion mit LAG() dient, bevor man auf die kompaktere Window-Syntax wechselt.


-- Find gaps: missing invoice numbers in an otherwise sequential series
SELECT a.invoice_no + 1 AS gap_start, MIN(b.invoice_no) - 1 AS gap_end
FROM invoices a
JOIN invoices b ON b.invoice_no > a.invoice_no
GROUP BY a.invoice_no
HAVING MIN(b.invoice_no) - a.invoice_no > 1;

-- Find consecutive days with an unchanged price using a self-join
SELECT curr.trade_date, curr.price
FROM daily_prices curr
JOIN daily_prices prev
  ON curr.id = prev.id + 1 AND curr.price = prev.price;

-- trade_date | price
-- 2026-07-04 | 98.00   <- same price as the previous day

8. Performance-Aspekte und Alternativen mit Window-Funktionen

Ein Self-Join vervielfacht faktisch die Groesse der beteiligten Tabelle in der Verarbeitung, weil die Datenbank effektiv zwei logische Kopien derselben Daten zusammenfuehrt. Ohne passenden Index auf der Join-Spalte, etwa auf manager_id oder email, entsteht schnell ein vollstaendiger Table-Scan auf beiden Seiten, was bei grossen Tabellen spuerbar langsam wird. Ein Index auf der Verknuepfungsspalte ist deshalb bei jedem produktiv genutzten Self-Join Pflicht, nicht optional.

Fuer den Zeilenvergleichs-Anwendungsfall aus Abschnitt sechs bieten moderne Datenbanken mit LAG() und LEAD() eine Window-Funktion-Alternative, die denselben Vergleich ohne physischen Join realisiert und dadurch oft schneller ist, weil die Datenbank die Daten nur einmal sortiert liest, statt zwei logische Kopien zu verknuepfen. Fuer Hierarchien und Duplikaterkennung bleibt der Self-Join aber meist die klarste und direkteste Loesung, Window-Funktionen loesen dort kein aequivalentes Problem.


-- Same day-over-day comparison, using LAG() instead of a self-join
SELECT
  trade_date,
  price AS today_price,
  LAG(price) OVER (ORDER BY trade_date) AS yesterday_price,
  price - LAG(price) OVER (ORDER BY trade_date) AS change
FROM daily_prices
ORDER BY trade_date;

-- Index required for a performant self-join on a large table
CREATE INDEX idx_employees_manager_id ON employees (manager_id);
CREATE INDEX idx_customers_email ON customers (email);

9. Self-Join-Anwendungsfaelle im Vergleich

Die drei praktischen Einsatzbereiche unterscheiden sich deutlich in der Join-Bedingung und im gewaehlten Join-Typ. Die folgende Tabelle fasst zusammen, wann welches Muster passt und worauf jeweils zu achten ist.

Anwendungsfall Typische Join-Bedingung Empfohlener Join-Typ Zu beachten
Hierarchie e.manager_id = m.id LEFT JOIN INNER JOIN verliert die Wurzel der Hierarchie
Duplikate finden a.email = b.email AND a.id < b.id INNER JOIN Ohne id-Ungleichung doppelte und Selbst-Paare
Zeilenvergleich b.id = a.id + 1 INNER oder LEFT JOIN Bei Datumsluecken LAG() oft klarer
Luecken erkennen b.no > a.no mit MIN() INNER JOIN Index auf der Nummerierungsspalte wichtig

In allen vier Faellen bleibt die zugrunde liegende Mechanik dieselbe: eine Tabelle wird zweimal referenziert, jedes Mal mit einem eigenen Alias, und die Beziehung zwischen den beiden Instanzen wird ueber die ON-Bedingung definiert. Der Unterschied liegt ausschliesslich darin, welche fachliche Beziehung diese Bedingung ausdrueckt.

Mironsoft

Datenmodellierung, Datenqualitaet und SQL-Beratung

Hierarchien, Duplikate oder Zeitreihen sauber in SQL abbilden?

Wir entwickeln performante Self-Join- und Window-Funktion-Loesungen fuer Hierarchien, Duplikatserkennung und Zeitreihenanalysen und pruefen bestehende Abfragen auf fehlende Indizes.

Datenqualitaet

Duplikate systematisch finden und sicher bereinigen

Hierarchie-Modelle

Von einfachem Self-Join bis zu rekursiven CTEs

Query-Optimierung

Indizes und Ausfuehrungsplaene fuer selbst-verknuepfte Abfragen

10. Zusammenfassung

Der Self-Join ist kein exotisches Konstrukt, sondern ein gewoehnlicher Join, bei dem eine Tabelle mit zwei unterschiedlichen Aliassen gegen sich selbst gejoint wird. Drei Anwendungsfaelle decken die Praxis fast vollstaendig ab: hierarchische Beziehungen wie Mitarbeiter und Vorgesetzte, fachliche Duplikaterkennung ueber inhaltliche statt technische Gleichheit, und der direkte Vergleich benachbarter Zeilen in einer sortierten Serie fuer Zeitreihen und Ranganalysen.

Fuer feste Hierarchietiefen und Duplikaterkennung bleibt der Self-Join die klarste Loesung, waehrend beliebig tiefe Hierarchien eine rekursive CTE brauchen und Zeilenvergleiche mit Window-Funktionen wie LAG() und LEAD() oft performanter sind. Ein Index auf der Join-Spalte ist bei jedem Self-Join in Produktion Pflicht, weil sonst zwei vollstaendige Table-Scans der gleichen Tabelle noetig werden.

Self-Join, das Wichtigste auf einen Blick

Hierarchien

LEFT JOIN ueber manager_id = id behaelt die Wurzel der Hierarchie. Fuer variable Tiefe rekursive CTE nutzen.

Duplikate

a.id < b.id verhindert Selbst-Paare und doppelte Ergebnisse bei fachlicher Duplikaterkennung.

Zeilenvergleiche

Self-Join auf id + 1 fuer aufeinanderfolgende Zeilen, alternativ LAG() und LEAD() als Window-Funktion.

Performance

Index auf der Join-Spalte ist Pflicht, sonst entstehen zwei vollstaendige Scans derselben Tabelle.

11. FAQ: Self-Join praktische Anwendungsfaelle

1Was ist ein Self-Join genau?
Ein gewoehnlicher Join, bei dem Quell- und Zieltabelle identisch sind, mit zwei unterschiedlichen Aliassen.
2Oberster Mitarbeiter fehlt bei INNER JOIN?
manager_id ist NULL, kein Match. LEFT JOIN behaelt die Zeile mit NULL als Vorgesetztenname.
3Wozu a.id < b.id bei Duplikaten?
Verhindert Selbst-Matches und doppelte Paare in beiden Reihenfolgen.
4Wie tief Hierarchien aufloesen?
So tief wie manuell hinzugefuegt, praktikabel zwei bis drei Ebenen. Sonst rekursive CTE nutzen.
5Ist Self-Join langsamer?
Ohne Index auf der Join-Spalte entstehen zwei vollstaendige Scans, das wird bei grossen Tabellen langsam.
6Duplikate direkt loeschen?
Ja, mit DELETE ... JOIN und a.id > b.id. Vorher als SELECT pruefen.
7Wann LAG() statt Self-Join?
Bei Vergleichen mit der direkt vorherigen oder naechsten Zeile, meist performanter als ein Join.
8Funktioniert Self-Join mit LEFT JOIN?
Ja, bei Hierarchien meist sogar die richtige Wahl, damit die Wurzel nicht verloren geht.
9Luecken in Nummerierung finden?
Jede Zeile mit groesseren Nummern joinen, Minimum bilden, Abstand pruefen. Index auf der Spalte wichtig.
10Immer einen Index anlegen?
Bei produktiv genutzten Abfragen auf nicht trivial kleinen Tabellen ja, sonst leidet die Performance deutlich.