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.
Inhaltsverzeichnis
- 1. Die Grundidee: eine Tabelle mit sich selbst verknuepfen
- 2. Mitarbeiter und Vorgesetzte: die klassische Hierarchie
- 3. Mehrstufige Hierarchien und ihre Grenzen
- 4. Duplikate finden: fachliche statt technischer Gleichheit
- 5. Duplikate bereinigen, ohne alle Zeilen zu verlieren
- 6. Aufeinanderfolgende Zeilen vergleichen
- 7. Luecken und Serien in Zeitreihen erkennen
- 8. Performance-Aspekte und Alternativen mit Window-Funktionen
- 9. Self-Join-Anwendungsfaelle im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.