SQL: Abfragen über mehrere Tabellen mit JOIN

https://bildung.social/@oerinformatik/112633708392604185

https://oer-informatik.de/sql-join

SQL, JOIN, INNER JOIN, LEFT JOIN, Kreuzprodukt, Anti-Join, Self Join, Fremdschlüssel, MariaDB, Aggregation

tl/dr; (ca. 26 min Lesezeit): In relationalen Datenbanken sind zusammengehörige Daten auf mehrere Tabellen verteilt. Mit JOIN fügst Du sie in einer Abfrage wieder zusammen: Der innere Verbund liefert nur Zeilen mit Partner, die äußeren Verbünde (LEFT, RIGHT, FULL) auch Zeilen ohne Partner. Der Artikel zeigt Kreuzprodukt, Join-Bedingungen über mehrere Tabellen und Spalten, Selbstverknüpfung, Anti-Join, die Falle „Bedingung in WHERE statt ON“, Vervielfachung beim Zählen sowie UPDATE und DELETE mit JOIN in MariaDB [AI-Assisted]. (Zuletzt geändert am 26.10.2026)

Dieser Artikel ist Teil einer Artikelreihe über SQL-Abfragen: Selektion und Projektion / Sortieren und Begrenzen (ORDER BY, LIMIT) / Funktionen im SELECT (Single-Row-Functions) / Fallunterscheidungen und NULL (CASE, COALESCE) / Aggregatfunktionen und Gruppierung / Abfragen über mehrere Tabellen (JOIN) / Übungsaufgaben: einfache Abfragen, Aggregatfunktionen, JOIN, Rechnungsdatenbank (gemischt)

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste der Artikel Selektion und Projektion und Der logische Datenbankentwurf mit dem Relationenmodell auf.

Als SQL-Grundlagen kann ich nach Durchlesen des Artikels…

  • … erklären, warum Daten auf mehrere Tabellen verteilt sind und wie Primär- und Fremdschlüssel sie verbinden.

  • … das Kreuzprodukt zweier Tabellen erläutern und die Zahl seiner Zeilen berechnen.

  • … Abfragen mit INNER JOIN … ON über zwei und mehr Tabellen (auch über Zwischentabellen) notieren und die implizite Schreibweise mit Komma und WHERE lesen.

  • … Tabellen- und Spaltenaliase verwenden und mehrdeutige Spaltennamen qualifizieren.

  • … LEFT JOIN und RIGHT JOIN unterscheiden und anhand einer vorgegebenen Ergebnistabelle die passende Join-Art wählen.

  • … Verbünde mit WHERE, GROUP BY, HAVING und ORDER BY kombinieren.

  • … erkennen, dass eine Bedingung an die rechte Tabelle eines LEFT JOIN in die ON-Klausel gehört, und beim Zählen COUNT(spalte) statt COUNT(*) anwenden.

  • … das Konzept eines Verbunds erklären und mit DISTINCT oder EXISTS ermitteln, welche Datensätze überhaupt einen Partner haben.

Darüber hinaus kann ich als SQL-Expertenwissen…

  • … einen vollständigen äußeren Verbund in MariaDB mit UNION nachbilden.

  • … Selbstverknüpfungen, Tabellen in mehreren Rollen, zusammengesetzte Schlüssel und Bedingungen mit Bereichen im Verbund anwenden.

  • … Datensätze ohne Partner mit einem Anti-Join finden und die Vervielfachung von Zeilen beim Zählen erkennen und beheben.

  • … Equi- und Non-Equi-Joins unterscheiden und Zeilen ohne Partner auf beiden Seiten mit zwei Anti-Joins und UNION finden.

  • … Datensätze mit UPDATE und DELETE über einen Verbund ändern bzw. löschen.

  • … Zeitspannen über einen Verbund berechnen und je Gruppe mitteln.

  • … Verbünde in der Datenanalyse mit pandas (merge) umsetzen und DataFrames mit mehreren Bedingungen filtern.

Ein Onlineshop speichert Kund*innen, Bestellungen, Bestellpositionen und Artikel in getrennten Tabellen. Das ist kein Zufall, sondern das Ergebnis der Normalisierung: Jede Information steht nur einmal in der Datenbank. Für fast jede sinnvolle Auswertung („Wer hat was bestellt?“) müssen die Teile aber wieder zusammengesetzt werden. Genau das leistet der Verbund (join). Kaum eine SQL-Aufgabe kommt ohne ihn aus.

Das Konzept in Kürze: Ein Verbund fügt die Datensätze von mindestens zwei Tabellen zu den Zeilen einer Ergebnistabelle zusammen. Welche Datensätze zusammengehören, legt die Verbundbedingung fest – meist der Vergleich eines Fremdschlüssels mit dem Primärschlüssel, auf den er verweist (ON b.kunde_id = k.kunde_id). Die Join-Art bestimmt, was mit Datensätzen ohne Partner geschieht: Der innere Verbund lässt sie weg, die äußeren Verbünde behalten sie und füllen die fehlenden Spalten mit NULL.

Das Beispiel: Abteilungen, Mitarbeitende und Schulungen

Alle Beispiele in diesem Artikel beziehen sich auf eine kleine Personaldatenbank eines Fahrradhändlers:

  • abteilung (abteilung_id, bezeichnung)

  • mitarbeiter (mitarbeiter_id, vorname, nachname, #abteilung_id, #vorgesetzt_id)

  • schulung (schulung_id, titel)

  • teilnahme (#mitarbeiter_id, #schulung_id)

Tabelle abteilung
abteilung_id bezeichnung
1 Vertrieb
2 Werkstatt
3 Buchhaltung
4 IT
Tabelle mitarbeiter
mitarbeiter_id vorname nachname abteilung_id vorgesetzt_id
1 Sophie Bertram 1 NULL
2 Bastian Ludwig 2 NULL
3 Selin Arslan 2 2
4 Paul Neubauer 2 2
5 Natalia Wróbel 3 1
6 Alex Kaiser NULL 2

Der Fremdschlüssel abteilung_id in mitarbeiter verweist auf den Primärschlüssel von abteilung. Alex Kaiser ist neu und noch keiner Abteilung zugeordnet (NULL), die Abteilung IT hat noch niemanden.

Ausgangslage: Zwei Tabellen mitarbeiter und abteilung mit Überschneidungen und fehlenden Überschneidungen
Ausgangslage: Zwei Tabellen mitarbeiter und abteilung mit Überschneidungen und fehlenden Überschneidungen

vorgesetzt_id verweist auf eine andere Zeile derselben Tabelle. Die Tabelle teilnahme löst die n:m-Beziehung zwischen Mitarbeitenden und Schulungen auf: Selin (3) und Paul (4) waren bei der E-Bike-Diagnose (1), Selin, Natalia (5) und Sophie (1) bei Erster Hilfe (2), Natalia außerdem bei der Schulung E-Rechnung (3).

Das Kreuzprodukt: jede Zeile mit jeder

Nennst Du zwei Tabellen in der FROM-Klausel, ohne zu sagen, wie sie zusammengehören, kombiniert die Datenbank jede Zeile der einen mit jeder Zeile der anderen Tabelle. Dieses kartesische Produkt oder Kreuzprodukt (cross join) hat so viele Zeilen wie das Produkt der Zeilenzahlen:

Die Abfrage liefert 6 \cdot 4 = 24 Zeilen. Nur fünf davon sind sinnvoll, nämlich die, in denen mitarbeiter.abteilung_id und abteilung.abteilung_id übereinstimmen. Ein Verbund ist also gedanklich ein Kreuzprodukt, aus dem die Zeilen mit passendem Schlüssel ausgewählt werden. Ein vergessenes Verknüpfungskriterium erkennst Du in der Praxis an plötzlich riesigen Ergebnissen mit Wiederholungen.

Bewusst eingesetzt wird das Kreuzprodukt selten, zum Beispiel um alle Kombinationen zu erzeugen (jede Schulung für jede Abteilung als leere Planungsliste).

Der innere Verbund: INNER JOIN

Der innere Verbund (inner join) liefert nur die Zeilen, für die in beiden Tabellen ein passender Partner existiert.

INNER JOIN liefert die Schnittmenge zwischen mitarbeiter und abteilung
INNER JOIN liefert die Schnittmenge zwischen mitarbeiter und abteilung

Die Verknüpfungsbedingung (Join-Bedingung) steht hinter ON; meist vergleicht sie Fremdschlüssel und Primärschlüssel:

Ergebnis des inneren Verbunds
vorname nachname abteilung
Sophie Bertram Vertrieb
Bastian Ludwig Werkstatt
Selin Arslan Werkstatt
Paul Neubauer Werkstatt
Natalia Wróbel Buchhaltung

Alex Kaiser fehlt (kein Partner, weil NULL mit nichts übereinstimmt), die Abteilung IT ebenfalls (niemand verweist auf sie). Das Schlüsselwort INNER ist optional, JOIN allein bedeutet dasselbe. Die Werkstatt erscheint dreimal, weil drei Mitarbeitende auf sie verweisen: Ist die Verknüpfungsspalte auf einer Seite nicht eindeutig, wiederholt der Verbund die Werte der anderen Seite für jeden Partner.

Aliase und eindeutige Spaltennamen

m und a sind Tabellenaliase (das AS davor darf fehlen, in Oracle muss es sogar fehlen). Sie sparen Schreibarbeit und sind nötig, sobald ein Spaltenname in mehreren Tabellen vorkommt: abteilung_id gibt es in beiden Tabellen. Schreibst Du die Spalte ohne Tabellenangabe, bricht MariaDB mit Column 'abteilung_id' in field list is ambiguous ab. Gewöhn Dir an, im Verbund alle Spalten zu qualifizieren (m.nachname), auch wenn sie eindeutig sind.

Spaltenaliase (a.bezeichnung AS abteilung) geben Ergebnisspalten einen sprechenden Namen. Das ist wichtig, wenn mehrere Tabellen gleichnamige Spalten haben (etwa bezeichnung in Artikel und Kategorie) oder eine Aufgabe die Spaltenüberschriften vorgibt.

Die implizite Schreibweise

Ältere Lehrbücher, manche Lösungsvorlagen und viele Bestandsanwendungen nutzen die implizite Schreibweise: Die Tabellen stehen mit Komma in FROM, die Join-Bedingung in WHERE:

Das Ergebnis ist dasselbe. Die explizite Schreibweise mit JOIN … ON hat aber Vorteile: Verknüpfung und Filter sind getrennt, eine vergessene Join-Bedingung fällt sofort auf, und nur sie erlaubt äußere Verbünde. Du solltest beide lesen können und JOIN … ON schreiben.

Ein Verbund, dessen Bedingung nur auf Gleichheit prüft (=), heißt Equi-Join; in der impliziten Schreibweise sieht man gut, dass er ein gefiltertes Kreuzprodukt ist. Vergleicht die Bedingung mit anderen Operatoren (<, BETWEEN …), spricht man von einem Non-Equi-Join (auch Theta-Join); ein Beispiel folgt im Abschnitt über zusammengesetzte Schlüssel und weitere Bedingungen.

Daneben gibt es Kurzformen: USING (abteilung_id) vergleicht gleichnamige Spalten, NATURAL JOIN sogar alle gleichnamigen Spalten automatisch. NATURAL JOIN ist tückisch: Kommt später eine Spalte wie bezeichnung in beiden Tabellen hinzu, ändert sich das Ergebnis still. Verwende besser ON.

Mehr als zwei Tabellen verknüpfen

Für jede weitere Tabelle folgt ein weiteres JOIN … ON. Bei n Tabellen brauchst Du (in der Regel) n - 1 Join-Bedingungen. Den Weg findest Du im Datenmodell, indem Du den Fremdschlüsselbeziehungen folgst. Eine n:m-Beziehung führt dabei immer über die Zwischentabelle:

Mitarbeitende mit ihren Schulungen
vorname nachname titel
Selin Arslan E-Bike-Diagnose
Selin Arslan Erste Hilfe
Sophie Bertram Erste Hilfe
Paul Neubauer E-Bike-Diagnose
Natalia Wróbel E-Rechnung
Natalia Wróbel Erste Hilfe

Die Reihenfolge der JOIN beeinflusst bei inneren Verbünden das Ergebnis nicht. Wichtig ist nur, dass jede Join-Bedingung auf Tabellen verweist, die vorher schon genannt wurden.

Zusammengesetzte Schlüssel und weitere Bedingungen

Besteht ein Schlüssel aus mehreren Spalten, müssen alle Spalten verglichen werden. Verweist zum Beispiel ein Fertigungsauftrag mit (bestellung_id, pos_nr) auf eine Bestellposition, lautet die Bedingung:

Vergleichst Du nur bestellung_id, passt der Auftrag zu allen Positionen der Bestellung, und die Zeilen vervielfachen sich.

Die Join-Bedingung muss außerdem kein reiner Schlüsselvergleich sein. Bei einer Mitfahr-App passt ein Angebot zu einem Gesuch, wenn Haltepunkt, Richtung und Datum übereinstimmen und die Abfahrt im gewünschten Zeitfenster liegt: ON an.haltepunkt_id = g.haltepunkt_id AND an.datum = g.datum AND an.abfahrt BETWEEN g.zeit_von AND g.zeit_bis. Auch Plan- und Ist-Zeiten (etwa für Verspätungen) werden so über mehrere Spalten verknüpft und dann mit Datumsfunktionen wie TIMESTAMPDIFF(MINUTE, plan, ist) verrechnet.

Äußere Verbünde: auch Zeilen ohne Partner

Oft sollen auch Zeilen erscheinen, die keinen Partner haben: alle Mitarbeitenden, auch die ohne Abteilung; alle Räume, auch die nicht belegten; alle Kund*innen, auch die ohne Bestellung. Dafür gibt es die äußeren Verbünde (outer joins). Fehlende Partner werden mit NULL aufgefüllt.

LEFT JOIN

Beim LEFT [OUTER] JOIN bleiben alle Zeilen der linken Tabelle (die vor JOIN steht) erhalten:

LEFT JOIN liefert alle mitarbeiter nur die zugeordneten Einträge von abteilung
LEFT JOIN liefert alle mitarbeiter nur die zugeordneten Einträge von abteilung
Ergebnis des LEFT JOIN
vorname nachname abteilung
Sophie Bertram Vertrieb
Bastian Ludwig Werkstatt
Selin Arslan Werkstatt
Paul Neubauer Werkstatt
Natalia Wróbel Buchhaltung
Alex Kaiser NULL

RIGHT JOIN

Der RIGHT [OUTER] JOIN behält entsprechend alle Zeilen der rechten Tabelle:

LEFT JOIN liefert alle Einträge von abteilung und nur die zugeordneten Einträge von mitarbeiter
LEFT JOIN liefert alle Einträge von abteilung und nur die zugeordneten Einträge von mitarbeiter

Jetzt erscheint die Abteilung IT mit NULL bei Vor- und Nachname, Alex Kaiser fehlt. A RIGHT JOIN B ist gleichwertig zu B LEFT JOIN A. Viele Teams schreiben deshalb nur LEFT JOIN und stellen die Tabelle, deren Zeilen vollständig erscheinen sollen, an den Anfang.

FULL JOIN und sein Ersatz in MariaDB

Der FULL [OUTER] JOIN behält die Zeilen beider Tabellen. PostgreSQL, SQL Server, Oracle und SQLite (ab Version 3.39) kennen ihn. MariaDB und MySQL unterstützen ihn in den aktuellen Versionen nicht (bei MariaDB ist er in Entwicklung, Stand Herbst 2026). Dort vereinigst Du einen LEFT JOIN und einen RIGHT JOIN mit UNION, das die doppelt gelieferten Zeilen mit Partner entfernt:

LEFT JOIN liefert alle mitarbeiter nur die zugeordneten Einträge von abteilung
LEFT JOIN liefert alle mitarbeiter nur die zugeordneten Einträge von abteilung

Das Ergebnis enthält die fünf Zeilen mit Partner, dazu Alex Kaiser ohne Abteilung und die Abteilung IT ohne Mitarbeitende. Mit UNION ALL stünden die fünf Zeilen mit Partner doppelt im Ergebnis.

Welche Join-Art passt? Die Ergebnistabelle verrät es

Viele Aufgaben geben eine Beispielausgabe vor. Daraus lässt sich die Join-Art ablesen:

Join-Art aus der Ergebnistabelle ableiten
Beobachtung in der Ergebnistabelle Join-Art
nur Zeilen, bei denen alle Spalten gefüllt sind INNER JOIN
Zeilen mit NULL in den Spalten der zweiten Tabelle oder Anzahl 0 LEFT JOIN (Haupttabelle links) bzw. RIGHT JOIN (Haupttabelle rechts)
NULL-Zeilen auf beiden Seiten FULL JOIN bzw. LEFT … UNION … RIGHT

Sollen zum Beispiel „alle Maschinentypen mit der Anzahl ihrer Maschinen“ ausgegeben werden und steht in der Beispielliste ein Typ mit 0, brauchst Du einen äußeren Verbund mit der Typ-Tabelle als Haupttabelle.

ON oder WHERE? Die häufigste Falle

Bei inneren Verbünden ist es egal, ob eine zusätzliche Bedingung in ON oder in WHERE steht. Bei äußeren Verbünden nicht. Die Datenbank wertet die Abfrage gedanklich so aus:

  1. FROM und JOIN … ON: Verbund bilden, fehlende Partner mit NULL auffüllen.

  2. WHERE: Zeilen filtern.

  3. GROUP BY, Aggregatfunktionen, HAVING.

  4. SELECT, ORDER BY.

Beispiel: Gesucht ist je Abteilung die Anzahl der Mitarbeitenden, die eine vorgesetzte Person haben, auch Abteilungen mit 0. Ein naheliegender, aber falscher Versuch:

Er liefert nur Werkstatt (2) und Buchhaltung (1). Für die IT erzeugt der LEFT JOIN zwar eine Zeile, aber mit m.vorgesetzt_id = NULL; dasselbe gilt für Sophie im Vertrieb. Das WHERE wirft diese Zeilen danach hinaus, und der äußere Verbund wirkt wie ein innerer. Richtig ist, die Bedingung an die rechte Tabelle in die ON-Klausel zu schreiben. Sie entscheidet dann nur, welcher Partner passt:

Ergebnis mit der Bedingung in ON
bezeichnung anzahl
Vertrieb 0
Werkstatt 2
Buchhaltung 1
IT 0

Als Faustregel gilt: Bedingungen an die Tabelle, deren Zeilen vollständig erhalten bleiben sollen (die linke beim LEFT JOIN), gehören in WHERE. Bedingungen an die optionale Tabelle gehören in ON. Die Ausnahme ist der Anti-Join (siehe unten), bei dem Du gerade nach den fehlenden Partnern filterst.

COUNT(*) oder COUNT(spalte)?

Zählst Du nach einem äußeren Verbund mit COUNT(*), wird die mit NULL aufgefüllte Zeile mitgezählt: Die IT hätte dann 1 Person statt 0. COUNT(m.mitarbeiter_id) zählt nur Werte, die nicht NULL sind, und liefert die richtige 0. Zähle deshalb immer eine Spalte der optionalen Tabelle, am besten ihren Primärschlüssel.

Bei SUM, AVG, MIN und MAX ergibt eine Gruppe ohne Partner NULL. Soll stattdessen 0 oder ein Text erscheinen, hilft COALESCE(SUM(…), 0) bzw. IFNULL(…).

Nur wissen, ob es einen Partner gibt: DISTINCT und EXISTS

Manche Fragen interessieren sich nicht für die Partnerzeilen selbst, sondern nur dafür, ob es welche gibt: „Welche Abteilungen haben schon Mitarbeitende zu Schulungen geschickt?“, „Welche Treffpunkte wurden schon für Aktivitäten genutzt?“. Ein gewöhnlicher Verbund liefert dann jede Abteilung so oft, wie sie Teilnahmen hat:

Das Ergebnis enthält sechs Zeilen: dreimal Werkstatt (Selin hat zwei Teilnahmen, Paul eine), zweimal Buchhaltung und einmal Vertrieb. Mit SELECT DISTINCT a.bezeichnung bleibt jede Abteilung nur einmal stehen. Gleichwertig ist ein sogenannter Semi-Join mit EXISTS, der die Partnerzeilen gar nicht erst in das Ergebnis holt:

DISTINCT wirkt immer auf die ganze Ergebniszeile: Nimmst Du zusätzlich den Namen der Mitarbeitenden in das SELECT auf, erscheinen die Abteilungen wieder mehrfach. Der Anti-Join im nächsten Abschnitt beantwortet die umgekehrte Frage „Wer hat keinen Partner?“.

Anti-Join: Datensätze ohne Partner finden

„Welche Mitarbeitenden haben noch an keiner Schulung teilgenommen?“, „Welche Betten sind heute frei?“, „Welche Haltepunkte werden nie genutzt?“ Solche Fragen beantwortet ein Anti-Join: ein LEFT JOIN, bei dem Du anschließend nur die Zeilen ohne Partner behältst:

Ergebnis: Bastian Ludwig und Alex Kaiser. Prüfe in WHERE eine Spalte, die beim echten Partner nie NULL sein kann, am besten den Primär- oder Fremdschlüssel der rechten Tabelle. Gleichwertig ist eine Unterabfrage mit WHERE NOT EXISTS (SELECT * FROM teilnahme AS t WHERE t.mitarbeiter_id = m.mitarbeiter_id).

Sollen die Zeilen ohne Partner auf beiden Seiten erscheinen (Mitarbeitende ohne Abteilung und Abteilungen ohne Mitarbeitende), vereinigst Du zwei Anti-Joins mit UNION. Das ist das Gegenstück zum FULL JOIN: Es fehlen gerade die Zeilen mit Partner.

Ergebnis: Alex Kaiser (ohne Abteilung) und die Abteilung IT (ohne Mitarbeitende). In DBMS mit FULL JOIN genügt … FULL JOIN … WHERE a.abteilung_id IS NULL OR m.mitarbeiter_id IS NULL.

Eine Tabelle in mehreren Rollen

Manchmal braucht eine Abfrage dieselbe Tabelle mehrmals. Dann bekommt jede Verwendung einen eigenen Alias.

Selbstverknüpfung (self join): vorgesetzt_id verweist auf mitarbeiter. Um zu jeder Person die vorgesetzte Person anzuzeigen, wird mitarbeiter einmal als „Mitarbeiter*in“ (m) und einmal als „Vorgesetzte*r“ (v) verwendet:

Mitarbeitende mit vorgesetzter Person
vorname nachname vg_vorname vg_nachname
Sophie Bertram NULL NULL
Bastian Ludwig NULL NULL
Selin Arslan Bastian Ludwig
Paul Neubauer Bastian Ludwig
Natalia Wróbel Sophie Bertram
Alex Kaiser Bastian Ludwig

Der LEFT JOIN sorgt dafür, dass auch die Geschäftsführung ohne Vorgesetzte erscheint. Nach demselben Muster werden Kategoriebäume (Kategorie und Oberkategorie) aufgelöst.

Mehrere Fremdschlüssel auf dieselbe Tabelle: Eine Bestellung hat eine Rechnungs- und eine Lieferadresse, beide in der Tabelle adresse; eine Mitfahrt hat eine Fahrerin und einen Mitfahrer, beide in nutzer. Auch hier wird die Tabelle je Rolle einmal mit eigenem Alias verbunden: JOIN adresse AS ra ON ra.adresse_id = b.rechnungsadresse_id JOIN adresse AS la ON la.adresse_id = b.lieferadresse_id. Ebenso lassen sich zwei Zeilen derselben Tabelle vergleichen, etwa die Zählerstände zweier Jahre (JOIN zaehlerstand AS z21 … JOIN zaehlerstand AS z22 …), um den Verbrauch als Differenz zu berechnen.

Verbund und Aggregation

Verbünde lassen sich mit allen Klauseln aus dem Artikel Aggregierende Funktionen kombinieren. Wichtig: Jede Spalte im SELECT, die nicht in einer Aggregatfunktion steht, gehört in GROUP BY. Gruppiere am besten zusätzlich nach dem Primärschlüssel, damit zwei Personen mit gleichem Namen nicht zusammenfallen.

Vorsicht, Vervielfachung

Jeder 1:n-Verbund kann Zeilen der „1-Seite“ vervielfachen. Das stört beim Auflisten nicht, beim Zählen und Summieren aber sehr. Beispiel: je Abteilung die Anzahl der Mitarbeitenden und die Anzahl der Schulungsteilnahmen:

Die Abfrage meldet für die Werkstatt 4 Mitarbeitende (es sind 3) und für die Buchhaltung 2 (es ist 1). Selin und Natalia haben je zwei Teilnahmen und stehen deshalb vor dem Gruppieren in zwei Zeilen. Abhilfen:

  • COUNT(DISTINCT m.mitarbeiter_id) zählt jede Person nur einmal (bei Summen hilft DISTINCT nicht).

  • Jede Kennzahl in einer eigenen (korrelierten) Unterabfrage berechnen, zum Beispiel (SELECT COUNT(*) FROM teilnahme AS t WHERE t.mitarbeiter_id = m.mitarbeiter_id). So zählt jede Unterabfrage unabhängig; mehr dazu im Artikel über Unterabfragen.

  • Zuerst in einer abgeleiteten Tabelle je Person aggregieren und dann verbinden.

Kontrolliere Zählergebnisse deshalb immer an einem Beispiel, dessen Ergebnis Du von Hand kennst.

Zeitspannen über einen Verbund auswerten

Liegen Start- und Endzeitpunkt in verschiedenen Tabellen oder soll die Dauer je Gruppe einer anderen Tabelle ausgewertet werden (durchschnittliche Lieferzeit je Dienstleister, Bearbeitungsdauer je Mitarbeiter*in), verbindest Du die Tabellen und rechnest die Differenz in einer festen Einheit aus. Am sichersten ist es, zuerst in Minuten zu rechnen und dann umzurechnen; Datums- und Zeitfunktionen unterscheiden sich allerdings je Datenbanksystem. Beispiel mit den Tabellen sendung (sendung_id, dienstleister_id, versandt_am, zugestellt_am) und dienstleister (dienstleister_id, name) – durchschnittliche Transportdauer in Stunden:

Achte auf drei Punkte: Datensätze ohne Endzeitpunkt (NULL) fallen aus der Differenz heraus und sollten ausdrücklich ausgeschlossen oder gesondert gezählt werden; die Differenz von zwei reinen Uhrzeiten (TIME) funktioniert nur, wenn beide am selben Tag liegen; und TIMESTAMPDIFF schneidet angefangene Einheiten ab (89 Minuten sind 1 Stunde), deshalb erst in Minuten rechnen. In MariaDB rechnen TIME_TO_SEC und SEC_TO_TIME Uhrzeiten in Sekunden um und zurück, wenn das Ergebnis wieder als Uhrzeit (hh:mm:ss) erscheinen soll.

Ändern und Löschen über einen Verbund

Auch Änderungen hängen oft von einer anderen Tabelle ab: „Erhöhe die Preise aller Artikel des Herstellers X“, „Lösche alle Buchungen von Nutzenden ohne Einwilligung“. Standard-SQL löst das mit einer Unterabfrage, die in jedem Datenbanksystem funktioniert:

MariaDB und MySQL erlauben zusätzlich einen Verbund direkt in UPDATE und DELETE (multi-table UPDATE/DELETE):

Bei DELETE steht vor FROM die Tabelle (bzw. ihr Alias), aus der gelöscht wird; die verbundenen Tabellen liefern nur die Bedingungen. PostgreSQL und SQLite verwenden für UPDATE stattdessen UPDATE … SET … FROM andere_tabelle WHERE …. Teste solche Anweisungen vorher als SELECT mit derselben FROM- und WHERE-Klausel, dann siehst Du, welche Zeilen betroffen sind.

Die Join-Arten im Überblick

Alle Verbünde folgen derselben Grundform; eckige Klammern kennzeichnen optionale Teile:

Join-Arten im Überblick
Join-Art liefert Schreibweise in MariaDB
innerer Verbund (INNER JOIN) nur Zeilen mit Partner A INNER JOIN B ON …
linker äußerer Verbund (LEFT JOIN) alle Zeilen von A, fehlende Partner als NULL A LEFT JOIN B ON …
rechter äußerer Verbund (RIGHT JOIN) alle Zeilen von B, fehlende Partner als NULL A RIGHT JOIN B ON …
vollständiger äußerer Verbund (FULL JOIN) alle Zeilen beider Tabellen A LEFT JOIN B ON … UNION A RIGHT JOIN B ON …
Anti-Join Zeilen von A ohne Partner A LEFT JOIN B ON … WHERE B.schluessel IS NULL
beidseitiger Anti-Join Zeilen beider Tabellen ohne Partner zwei Anti-Joins mit UNION
Kreuzprodukt (CROSS JOIN) jede Zeile von A mit jeder von B A CROSS JOIN B bzw. FROM A, B
Selbstverknüpfung Zeilen einer Tabelle mit Zeilen derselben Tabelle A AS a1 JOIN A AS a2 ON …

NATURAL JOIN und USING sind Kurzformen des inneren bzw. äußeren Verbunds über gleichnamige Spalten (siehe oben).

Schritt für Schritt zur JOIN-Abfrage

Bei Aufgaben hat sich folgendes Vorgehen bewährt:

  1. Ergebnis lesen: Welche Spalten sind gefordert, aus welchen Tabellen stammen sie? Gibt es Spaltenüberschriften, die Du per Alias nachbilden musst?

  2. Weg im Datenmodell suchen: Welche Tabellen liegen zwischen den benötigten Spalten (Zwischentabellen, Tabellen nur für eine Bedingung)? Folge den Fremdschlüsseln und schreibe für jeden Schritt ein JOIN … ON.

  3. Join-Art wählen: Sollen auch Zeilen ohne Partner (NULL, 0) erscheinen? Dann äußerer Verbund mit der vollständigen Tabelle links.

  4. Bedingungen einordnen: Filter auf die Haupttabelle in WHERE, Filter auf optionale Tabellen in ON. Bekannte Namen statt IDs über den Verbund filtern.

  5. Gruppieren und sortieren: GROUP BY mit allen nicht aggregierten Spalten, HAVING für Bedingungen an Aggregate, ORDER BY in der geforderten Reihenfolge.

  6. Kontrollieren: Stimmt die Zeilenzahl? Erscheinen Zeilen doppelt? Liefert COUNT für Zeilen ohne Partner 0?

Zum Üben findest Du drei umfangreiche Aufgaben in den Übungsaufgaben zu JOIN.

Exkurs: Verbünde in der Datenanalyse mit pandas

In der Datenanalyse liegen Daten oft nicht in einer Datenbank, sondern als CSV- oder JSON-Dateien vor und werden mit der Python-Bibliothek pandas ausgewertet. Auch dort werden Tabellen (DataFrames) verbunden – mit der Funktion merge, die einem SQL-Verbund entspricht:

Die wichtigsten Parameter von pandas.merge und ihre SQL-Gegenstücke
merge-Parameter Bedeutung SQL-Gegenstück
on="spalte" bzw. on=["a", "b"] Verbundspalte(n) mit gleichem Namen in beiden DataFrames ON l.spalte = r.spalte (mehrere Spalten mit AND)
left_on=…, right_on=… Verbundspalten mit unterschiedlichen Namen ON l.abteilung_id = r.id
how="inner" (Standard) nur Zeilen mit Partner INNER JOIN
how="left" / how="right" alle Zeilen des linken bzw. rechten DataFrames LEFT JOIN / RIGHT JOIN
how="outer" alle Zeilen beider DataFrames FULL OUTER JOIN

Fehlende Partner erscheinen in pandas als NaN. Danach wird gefiltert – das entspricht dem WHERE. Mehrere Bedingungen werden mit & (und) bzw. | (oder) verknüpft, jede Bedingung steht in eigenen Klammern; and und or funktionieren hier nicht:

Die Fallen sind dieselben wie in SQL: Verbindest Du nur über einen Teil eines zusammengesetzten Schlüssels, vervielfachen sich die Zeilen; ein innerer Verbund lässt Zeilen ohne Partner stillschweigend verschwinden. Kontrolliere deshalb nach jedem merge die Zeilenzahl (len(daten)).

Leitfragen

  1. Warum sind die Daten einer Datenbank auf mehrere Tabellen verteilt, und welche Rolle spielen Primär- und Fremdschlüssel beim Verbund?

Durch die Normalisierung steht jede Information nur einmal in der Datenbank; zusammengehörige Daten liegen deshalb in getrennten Tabellen. Ein Fremdschlüssel verweist auf den Primärschlüssel einer anderen Tabelle, und die Join-Bedingung vergleicht genau diese beiden Spalten, um die passenden Zeilen wieder zusammenzufügen; siehe Abschnitt „Das Beispiel: Abteilungen, Mitarbeitende und Schulungen“ und Abschnitt „Der innere Verbund: INNER JOIN“.

  1. Wie viele Zeilen liefert eine Abfrage über zwei Tabellen mit 8 und 7 Zeilen ohne Join-Bedingung, und warum?

Ohne Join-Bedingung entsteht das Kreuzprodukt: Jede Zeile der einen wird mit jeder Zeile der anderen Tabelle kombiniert, also 8 \cdot 7 = 56 Zeilen. Sinnvoll sind davon nur die Zeilen, in denen Fremd- und Primärschlüssel übereinstimmen; siehe Abschnitt „Das Kreuzprodukt: jede Zeile mit jeder“.

  1. Wie lautet eine Abfrage über eine n:m-Beziehung, und wie viele Join-Bedingungen braucht sie?

Eine n:m-Beziehung führt immer über die Zwischentabelle, z. B. FROM mitarbeiter AS m INNER JOIN teilnahme AS t ON t.mitarbeiter_id = m.mitarbeiter_id INNER JOIN schulung AS s ON s.schulung_id = t.schulung_id. Bei n Tabellen brauchst Du in der Regel n - 1 Join-Bedingungen, hier also zwei; siehe Abschnitt „Mehr als zwei Tabellen verknüpfen“.

  1. Woran erkennst Du an einer vorgegebenen Ergebnistabelle, ob ein INNER, LEFT, RIGHT oder FULL JOIN nötig ist, und wie bildest Du den FULL JOIN in MariaDB nach?

Sind alle Spalten gefüllt, reicht ein INNER JOIN; stehen NULL oder 0 in den Spalten der zweiten Tabelle, brauchst Du einen LEFT JOIN (Haupttabelle links) bzw. RIGHT JOIN (Haupttabelle rechts); NULL-Zeilen auf beiden Seiten verlangen einen FULL JOIN. In MariaDB bildest Du ihn nach, indem Du einen LEFT JOIN und einen RIGHT JOIN mit UNION vereinigst, das die doppelten Zeilen entfernt; siehe Abschnitt „Welche Join-Art passt? Die Ergebnistabelle verrät es“ und Abschnitt „FULL JOIN und sein Ersatz in MariaDB“.

  1. Warum macht eine Bedingung an die rechte Tabelle in WHERE aus einem LEFT JOIN praktisch einen INNER JOIN, und warum liefert COUNT(*) dort 1 statt 0?

WHERE wird erst nach dem Verbund ausgewertet: Die mit NULL aufgefüllten Zeilen ohne Partner erfüllen die Bedingung an die rechte Tabelle nicht und fliegen heraus, sodass nur noch Zeilen mit Partner übrig bleiben. Bedingungen an die optionale Tabelle gehören deshalb in ON. COUNT(*) zählt die aufgefüllte NULL-Zeile mit; COUNT(spalte) mit einer Spalte der optionalen Tabelle zählt nur Werte ungleich NULL und liefert 0; siehe Abschnitt „ON oder WHERE? Die häufigste Falle“ und Abschnitt „COUNT(*) oder COUNT(spalte)?“.

  1. Wie findest Du Datensätze, zu denen es keinen Partner gibt?

Mit einem Anti-Join: einem LEFT JOIN, nach dem Du in WHERE nur die Zeilen ohne Partner behältst, z. B. WHERE t.mitarbeiter_id IS NULL. Prüfe dabei eine Spalte, die beim echten Partner nie NULL ist, am besten den Schlüssel der rechten Tabelle; gleichwertig ist WHERE NOT EXISTS (…); siehe Abschnitt „Anti-Join: Datensätze ohne Partner finden“.

  1. Wann musst Du dieselbe Tabelle mehrfach mit unterschiedlichen Aliasen einbinden?

Immer dann, wenn eine Tabelle in mehreren Rollen gebraucht wird: bei einer Selbstverknüpfung (z. B. Mitarbeitende und ihre Vorgesetzten in derselben Tabelle), bei mehreren Fremdschlüsseln auf dieselbe Tabelle (Rechnungs- und Lieferadresse) oder wenn Du zwei Zeilen derselben Tabelle vergleichst (Zählerstände zweier Jahre). Jede Verwendung bekommt dann einen eigenen Alias; siehe Abschnitt „Eine Tabelle in mehreren Rollen“.

  1. Warum können Anzahlen nach mehreren 1:n-Verbünden zu groß sein, und was hilft dagegen?

Jeder 1:n-Verbund kann die Zeilen der 1-Seite vervielfachen; vor dem Gruppieren steht eine Person dann mehrfach im Ergebnis und wird mehrfach gezählt. Abhilfe schaffen COUNT(DISTINCT …) (nicht bei Summen), eine eigene (korrelierte) Unterabfrage je Kennzahl oder eine vorab aggregierte abgeleitete Tabelle; siehe Abschnitt „Vorsicht, Vervielfachung“.

  1. Wie änderst oder löschst Du Datensätze, deren Auswahl von einer anderen Tabelle abhängt?

In Standard-SQL nutzt Du eine Unterabfrage in SET oder WHERE. MariaDB und MySQL erlauben zusätzlich einen Verbund direkt in UPDATE … INNER JOIN … SET … WHERE … und DELETE alias FROM … INNER JOIN … WHERE …, wobei vor FROM die Tabelle steht, aus der gelöscht wird. Teste solche Anweisungen vorher als SELECT mit denselben Klauseln; siehe Abschnitt „Ändern und Löschen über einen Verbund“.

  1. Was ist das Konzept hinter einem JOIN, und wie vermeidest Du doppelte Zeilen, wenn nur gefragt ist, welche Datensätze einen Partner haben?

Ein JOIN fügt die Datensätze mehrerer Tabellen über eine Verbundbedingung – meist Fremdschlüssel gleich Primärschlüssel – zu Ergebniszeilen zusammen; die Join-Art entscheidet, ob Datensätze ohne Partner erhalten bleiben. Interessiert nur, ob es einen Partner gibt, entfernst Du die Mehrfachzeilen mit SELECT DISTINCT oder formulierst einen Semi-Join mit EXISTS; siehe Abschnitt „Nur wissen, ob es einen Partner gibt: DISTINCT und EXISTS“.

  1. Wie entspricht pandas.merge einem SQL-Verbund, und worauf musst Du beim Filtern eines DataFrames achten?

on bzw. left_on/right_on geben die Verbundspalten an (wie ON), how die Join-Art (inner, left, right, outer); fehlende Partner werden zu NaN. Beim Filtern verknüpfst Du mehrere Bedingungen mit & bzw. | und setzt jede Bedingung in Klammern, and/or funktionieren nicht; siehe Abschnitt „Exkurs: Verbünde in der Datenanalyse mit pandas“.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “SQL: Abfragen über mehrere Tabellen mit JOIN” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/sql-join veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/db-sql/dml-select-queries. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 26.10.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: