SQL-JOINs verbinden Zeilen aus zwei Tabellen über eine gemeinsame Spalte, zum Beispiel die author_id eines Buchs und die id eines Autors. Die Art des Joins entscheidet, was mit den Zeilen ohne Treffer passiert. Die Beispiele funktionieren in PostgreSQL, MySQL, SQLite und SQL Server.
So verwendest du SQL-JOINs
Schreib die zweite Tabelle hinter JOIN und gib in ON an, wie ihre Zeilen zu denen der ersten Tabelle passen:
SELECT columns
FROM table_a
JOIN table_b ON table_a.key = table_b.key;
Alle Beispiele in diesem Abschnitt verbinden dieselben zwei Tabellen: Butler hat zwei der Bücher geschrieben, Asimov keines davon, und wer Beowulf geschrieben hat, weiß niemand.
INNER JOIN
Ein INNER JOIN liefert nur die Zeilen, die in beiden Tabellen einen Treffer haben. JOIN allein bedeutet INNER JOIN.
-- Input: authors
-- id | name
-- ---+--------
-- 1 | Le Guin
-- 2 | Butler
-- 3 | Asimov
-- Input: books
-- title | author_id
-- ---------------------+----------
-- The Dispossessed | 1
-- Kindred | 2
-- Parable of the Sower | 2
-- Beowulf | NULL
SELECT a.name, b.title
FROM authors a
INNER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
Asimov hat keine Bücher und Beowulf keinen Autor, also taucht keiner von beiden auf.
LEFT JOIN
Ein LEFT JOIN (oder LEFT OUTER JOIN) liefert jede Zeile der linken Tabelle, also der hinter FROM. Wo die rechte Tabelle keinen Treffer hat, sind ihre Spalten NULL.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- Asimov | NULL
RIGHT JOIN
Ein RIGHT JOIN (oder RIGHT OUTER JOIN) ist das Spiegelbild: jede Zeile der rechten Tabelle, mit NULLs, wo die linke Tabelle keinen Treffer hat.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
FULL OUTER JOIN
Ein FULL OUTER JOIN liefert jede Zeile beider Tabellen, verbunden, wo es geht, und mit NULLs aufgefüllt, wo nicht. MySQL kennt ihn nicht: siehe „Alternative Syntax“ weiter unten.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
FULL OUTER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
-- Asimov | NULL
CROSS JOIN
Ein CROSS JOIN kombiniert jede Zeile der einen Tabelle mit jeder Zeile der anderen, deshalb hat er kein ON. Zwei Größen und zwei Farben ergeben vier Kombinationen; 1.000 mal 1.000 Zeilen ergeben eine Million, also nutz ihn nur bei kleinen Tabellen.
-- Input: sizes
-- size
-- ----
-- S
-- M
-- Input: colors
-- color
-- -----
-- Red
-- Blue
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- Output:
-- size | color
-- -----+------
-- S | Red
-- S | Blue
-- M | Red
-- M | Blue
LEFT ANTI JOIN
Ein Left Anti Join liefert die Zeilen der linken Tabelle, die in der rechten Tabelle keinen Treffer haben. Ein eigenes Schlüsselwort gibt es nicht: Mach einen LEFT JOIN und behalte die Zeilen, bei denen die rechte Seite NULL ist.
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.author_id IS NULL;
-- Output:
-- name
-- ------
-- Asimov
RIGHT ANTI JOIN
Ein Right Anti Join liefert die Zeilen der rechten Tabelle, die in der linken Tabelle keinen Treffer haben.
-- Input: the authors and books tables above
SELECT b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- title
-- -------
-- Beowulf
Wichtige Regeln
-
NULL passt zu nichts, nicht einmal zu einem anderen NULL. Beowulfs NULL-
author_idpasst zu keinem Autor, daher liefern ihn nur die Joins, die Bücher ohne Treffer behalten. - Eine Zeile erscheint einmal pro passender Zeile. Butler erscheint oben zweimal, weil sie zu zwei Büchern passt. Summierst du dann eine Spalte aus der Autorentabelle, zählen ihre Werte doppelt.
-
Bei einem Outer Join gehört eine Bedingung auf die andere Tabelle in
ON. InWHEREgreift sie erst nach dem Join und wirft die NULL-Zeilen weg, die der Join gerade behalten hat:
-- Input: the authors and books tables above
-- Only Butler is left: the WHERE drops every author whose title is NULL
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.title LIKE 'K%';
-- Output:
-- name | title
-- -------+--------
-- Butler | Kindred
-- Every author is kept, with a book only where it starts with K
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id AND b.title LIKE 'K%';
-- Output:
-- name | title
-- --------+--------
-- Le Guin | NULL
-- Butler | Kindred
-- Asimov | NULL
Alternative Syntax
MySQL hat keinen FULL OUTER JOIN. Kombinier stattdessen einen LEFT JOIN mit den Zeilen ohne Treffer aus einem RIGHT JOIN:
-- MySQL
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a LEFT JOIN books b ON a.id = b.author_id
UNION ALL
SELECT a.name, b.title
FROM authors a RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Parable of the Sower
-- Butler | Kindred
-- Asimov | NULL
-- NULL | Beowulf
SQLite vor Version 3.39 kennt weder RIGHT JOIN noch FULL OUTER JOIN: Tausch die Tabellen und nimm einen LEFT JOIN.
Einen Anti Join kannst du auch mit NOT EXISTS schreiben. Verzichte dafür auf NOT IN: Liefert die Unterabfrage auch nur ein einziges NULL, wie hier Beowulfs author_id, gibt NOT IN überhaupt nichts zurück:
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
WHERE NOT EXISTS (SELECT 1 FROM books b WHERE b.author_id = a.id);
-- Output:
-- name
-- ------
-- Asimov
SELECT a.name
FROM authors a
WHERE a.id NOT IN (SELECT author_id FROM books);
-- Output:
-- name
-- ----
-- (0 rows)
Wenn die verbindenden Spalten in beiden Tabellen gleich heißen, kannst du statt der ON-Bedingung JOIN books USING (author_id) schreiben, außer in SQL Server.
Älterer Code listet die Tabellen oft mit Kommas auf und schreibt die Verknüpfung in WHERE. Das ist ein INNER JOIN in anderer Schreibweise. Lässt du das WHERE weg, wird daraus ein CROSS JOIN:
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a, books b
WHERE a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
Wann solltest du SQL-JOINs verwenden?
Nutz einen Join, wann immer die benötigten Daten auf mehrere Tabellen verteilt sind. Welcher Join der richtige ist, hängt davon ab, was mit den Zeilen ohne Treffer passieren soll – und die meisten Aufgaben fallen in einen dieser fünf Fälle.
Details aus einer verknüpften Tabelle nachschlagen
Eine Mitarbeiterzeile enthält eine Abteilungs-ID, der Bericht braucht aber den Namen der Abteilung. Ein INNER JOIN holt ihn und lässt jede Zeile weg, zu der es nichts nachzuschlagen gibt.
Zeilen ohne Treffer behalten
Wenn die verknüpften Daten optional sind, etwa bei Mitarbeitenden ohne zugewiesene Abteilung, würde ein INNER JOIN sie stillschweigend ausblenden. Ein LEFT JOIN behält jede Zeile der Haupttabelle und füllt die Lücken mit NULL.
Alle Einträge auflisten, auch leere
Berichte brauchen oft jede Abteilung, Kategorie oder jeden Kunden, auch die, zu denen es noch nichts gibt. Mach diese Tabelle zu der, deren Zeilen alle erhalten bleiben: an erster Stelle bei einem LEFT JOIN oder an zweiter bei einem RIGHT JOIN.
Zeilen ohne Treffer finden
Kunden, die nie bestellt haben, Produkte, die nie verkauft wurden, Mitarbeitende ohne Abteilung: Ein Anti Join findet, was fehlt – und eignet sich damit auch gut zur Prüfung der Datenqualität.
Alle Kombinationen erzeugen
Ein CROSS JOIN erzeugt Zeilen, die es noch nicht gibt: jede Größe in jeder Farbe, jeder Mitarbeitende mit jeder Schulung oder jede Filiale an jedem Tag eines Berichts.
Praxisbeispiele für SQL-JOINs
Studierende und ihre Kurse
Eine Many-to-many-Beziehung braucht zwei INNER JOINs über die Verknüpfungstabelle, die Studierende mit Kursen verbindet. Mei ist in keinem Kurs eingeschrieben, also taucht sie nicht auf:
-- Input: students
-- id | name
-- ---+------
-- 1 | Amara
-- 2 | Lucas
-- 3 | Mei
-- Input: enrollments
-- student_id | course_id | grade
-- -----------+-----------+------
-- 1 | 10 | A
-- 1 | 20 | B
-- 2 | 10 | B
-- Input: courses
-- id | course_name
-- ---+------------
-- 10 | Databases
-- 20 | Statistics
SELECT s.name, c.course_name, e.grade
FROM students s
INNER JOIN enrollments e ON s.id = e.student_id
INNER JOIN courses c ON e.course_id = c.id;
-- Output:
-- name | course_name | grade
-- ------+-------------+------
-- Amara | Databases | A
-- Amara | Statistics | B
-- Lucas | Databases | B
Nutzer mit optionalem Profil
Nicht jeder Nutzer hat ein Profil ausgefüllt. Ein LEFT JOIN behält sie alle:
-- Input: users
-- id | username
-- ---+---------
-- 1 | rosa_m
-- 2 | devon
-- 3 | kai_l
-- Input: user_profiles
-- user_id | bio
-- --------+------------------------
-- 1 | Data engineer in Lisbon
-- 3 | Loves SQL puzzles
SELECT u.username, p.bio
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id;
-- Output:
-- username | bio
-- ---------+------------------------
-- rosa_m | Data engineer in Lisbon
-- devon | NULL
-- kai_l | Loves SQL puzzles
Anzahl der Bestellungen pro Kunde
Um Kunden ohne Bestellungen einzubeziehen, mach einen LEFT JOIN ausgehend von customers. Zähl dann eine Spalte aus der Bestelltabelle: COUNT(*) zählt die Zeile mit, die der LEFT JOIN für Omar erzeugt hat, sodass er 1 statt 0 bekäme:
-- Input: customers
-- id | name
-- ---+------------
-- 1 | Harper Lee
-- 2 | Omar Haddad
-- 3 | Yuki Tanaka
-- Input: orders
-- id | customer_id | total
-- ----+-------------+-------
-- 101 | 1 | 59.90
-- 102 | 3 | 120.00
-- 103 | 1 | 14.50
-- Wrong: COUNT(*) counts Omar's NULL row
SELECT c.name, COUNT(*) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 1
-- Yuki Tanaka | 1
-- Right: COUNT(o.id) skips it
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 0
-- Yuki Tanaka | 1
Rechnungen und Zahlungen, die nicht zusammenpassen
Ein FULL OUTER JOIN, der nur die Zeilen ohne Treffer behält, gleicht zwei Listen in einer Abfrage ab: Rechnung 502 ist unbezahlt, und Zahlung 9003 gehört zu einer Rechnung, die es nicht gibt:
-- Input: invoices
-- invoice_id | amount
-- -----------+-------
-- 501 | 120.00
-- 502 | 75.50
-- 503 | 300.00
-- Input: payments
-- payment_id | invoice_id | paid
-- -----------+------------+-------
-- 9001 | 501 | 120.00
-- 9002 | 503 | 300.00
-- 9003 | 504 | 42.00
-- MySQL has no FULL OUTER JOIN: use the UNION ALL form under "Alternative syntax"
SELECT i.invoice_id, i.amount, p.payment_id, p.invoice_id AS paid_invoice
FROM invoices i
FULL OUTER JOIN payments p ON p.invoice_id = i.invoice_id
WHERE i.invoice_id IS NULL OR p.payment_id IS NULL;
-- Output:
-- invoice_id | amount | payment_id | paid_invoice
-- -----------+--------+------------+-------------
-- 502 | 75.50 | NULL | NULL
-- NULL | NULL | 9003 | 504
Tagesumsatz jeder Filiale, auch an Tagen ohne Verkäufe
Verbinde die Filialen per CROSS JOIN mit den Tagen, um eine Zeile pro Filiale und Tag zu bekommen, und hänge dann die Verkäufe per LEFT JOIN an dieses Raster. York hat am 6. Oktober nichts verkauft und bekommt trotzdem eine Zeile:
-- Input: stores
-- store
-- -----
-- Leeds
-- York
-- Input: days
-- day
-- ----------
-- 2026-10-05
-- 2026-10-06
-- Input: sales
-- store | day | amount
-- ------+------------+-------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
SELECT st.store, d.day, COALESCE(SUM(s.amount), 0.00) AS takings
FROM stores st
CROSS JOIN days d
LEFT JOIN sales s ON s.store = st.store AND s.day = d.day
GROUP BY st.store, d.day
ORDER BY st.store, d.day;
-- Output:
-- store | day | takings
-- ------+------------+--------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
-- York | 2026-10-06 | 0.00
Weiterführende Artikel zu SQL-JOINs
In diesem Blog:
- SQL-JOIN-Spickzettel: alle Join-Arten mit Diagramm auf einer Seite
- MySQL FULL OUTER JOIN: weitere Wege, den fehlenden FULL OUTER JOIN in MySQL zu umgehen
- MySQL-Joins-Guide: Joins in MySQL, mit Beispielen
- SQL-Server-Joins-Guide: Joins in T-SQL
- PostgreSQL CROSS JOIN: weitere Einsatzmöglichkeiten für Abfragen mit allen Kombinationen
- Guide zu Datenbankbeziehungen: wie die Tabellen, die du verbindest, zueinander in Beziehung stehen
- One-to-one-Beziehungen, Guide zu One-to-many-Beziehungen und Guide zu Many-to-many-Beziehungen: welchen Join jede Beziehung braucht
- SQL-COALESCE-Guide: die NULLs ersetzen, die ein Outer Join hinterlässt
Offizielle Dokumentation:
- PostgreSQL: Joined Tables
- MySQL: JOIN-Klausel
- SQLite: die FROM-Klausel und Joins
- SQL Server: FROM-Klausel plus JOIN
Beekeeper Studio ist eine kostenlose & quelloffene Datenbank-GUI
Das beste SQL-Abfrage- & Editor-Tool, das ich je benutzt habe. Es bietet alles, was ich zum Verwalten meiner Datenbank brauche. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio ist schnell, intuitiv und einfach zu bedienen. Beekeeper unterstützt jede Menge Datenbanken und läuft hervorragend auf Windows, Mac und Linux.
Was Nutzer über Beekeeper Studio sagen
„Beekeeper Studio hat meinen alten SQL-Workflow komplett ersetzt. Es ist schnell, intuitiv und macht die Arbeit mit Datenbanken wieder zum Vergnügen.“
„Ich habe viele Datenbank-GUIs ausprobiert, aber Beekeeper trifft die perfekte Balance zwischen Funktionsumfang und Einfachheit. Es funktioniert einfach.“