Tabellen verbinden: JOIN

Kunden, Bestellungen und Produkte liegen meist in getrennten Tabellen. JOIN liest die zusammengehörenden Zeilen anhand ihrer Schlüssel gemeinsam.

Der einfache Join

Bestellungen speichern customer_id, Kunden den Namen. Um beides zusammen zu sehen, sagen Sie der Datenbank, wie die zwei Tabellen zueinander passen.

1

SELECT o.order_number,
       o.ordered_at,
       o.total,
       c.first_name || ' ' || c.last_name AS customer,
       c.country
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.ordered_at DESC
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

order_numberordered_attotalcustomercountry
ORD-2025-0024802025-12-28 23:52:146333.77Elena MeyerCA
ORD-2025-0005052025-12-28 11:16:392276.51Diego RossiIT
ORD-2025-0009402025-12-28 07:28:4611000.68Freya ChoiBR
ORD-2025-0015812025-12-28 06:12:382645.06Jonas LeeJP
ORD-2025-0009952025-12-28 05:47:532584.02Leo HaddadUS

Öffnet Planche in einem eigenen Browser-Tab, mit geladener Beispieldatenbank und dieser Abfrage im Editor. Alle Beispiele der Seite verwenden denselben Tab weiter, und ausgeführt wird erst, wenn Sie F9 drücken.

Drei Dinge verdienen einen Namen. JOIN allein bedeutet INNER JOIN. ON c.id = o.customer_id ist die Join-Bedingung – wie eine Zeile links einer Zeile rechts zugeordnet wird. Und orders o gibt der Tabelle einen kurzen Alias, wodurch Sie o.total und c.country schreiben können, ohne den vollen Tabellennamen zu wiederholen.

Aliase sind spätestens dann nicht mehr optional, wenn zwei Tabellen eine gleichnamige Spalte haben. Beide Tabellen hier haben eine id; unqualifiziert wäre id mehrdeutig, und die Datenbank würde die Abfrage ablehnen.

Mehr als zwei Tabellen

Joins lassen sich aneinanderhängen. Die Produkte einer Bestellung sind zwei Schritte entfernt: orders → order_items → products.

2

SELECT o.order_number,
       p.name AS product,
       oi.quantity,
       oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'delivered'
ORDER BY o.ordered_at DESC
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

order_numberproductquantityunit_price
ORD-2025-000505Zen Boots1700.65
ORD-2025-000505Echo Backpack 40L Plus11570.96
ORD-2025-001581Atlas Phone Pro Max11546.38
ORD-2025-001581Pulse Headphones2548.09
ORD-2025-000995Aria Chrono21289.56

Jedes JOIN hängt eine weitere Tabelle an das an, was Sie bisher haben, ein Join über vier Tabellen ist also dasselbe mit einer Zeile mehr. Von oben nach unten gelesen: "Bestellungen, mit ihren Positionen, mit dem Produkt je Position".

Zeilen ohne Gegenstück behalten

Ein Inner Join wirft alles ohne Partner weg. Ein Kunde, der nie bestellt hat, verschwindet einfach aus dem Ergebnis. LEFT JOIN behält jede Zeile der linken Tabelle und füllt die rechte Seite mit NULL, wo es nichts zuzuordnen gab.

3

SELECT c.id,
       c.first_name,
       c.last_name,
       o.order_number,
       o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.id
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

idfirst_namelast_nameorder_numbertotal
1LiamFischerORD-2022-0015961317.56
1LiamFischerORD-2025-0017232664.56
1LiamFischerORD-2025-0031424609.77
2MateoCostaORD-2025-0024733760.6
3YukiJangORD-2025-0035277569.3

Damit wird die Abfrage "finde, was fehlt" möglich: alles behalten und dann nur die Zeilen behalten, bei denen die Zuordnung fehlschlug.

4

-- The 95 customers who have never ordered anything.
SELECT c.id, c.first_name, c.last_name, c.country
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL
ORDER BY c.id
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

idfirst_namelast_namecountry
14MayaMoreauUS
17NoraMüllerIT
20SanaParkKR
30RaviParkKR
33OliverParkIN

WHERE o.id IS NULL liest sich zunächst seltsam – orders.id ist ein Primärschlüssel und kann in der Tabelle nie null sein. In diesem Ergebnis kann es das aber, weil der LEFT JOIN diese NULLs für die nicht zugeordneten Zeilen erfunden hat. Diese Zeilen sind die 95 Kunden, die nie bestellt haben.

ON oder WHERE: nicht dieselbe Frage

Hier ist die Falle. Beide Abfragen erwähnen status = 'cancelled'. Nur eine davon ist in der Praxis ein LEFT JOIN.

5

-- The condition is part of the join: every customer is kept,
-- and only cancelled orders are attached to them.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

idfirst_nameorder_numberstatus
1LiamNULLNULL
2MateoNULLNULL
3YukiORD-2025-003530cancelled
4InesNULLNULL
5LeoNULLNULL

Die Bedingung steht in ON, ist also Teil der Zuordnung: Jeder Kunde bleibt erhalten, und eine stornierte Bestellung wird angehängt, wo es eine gibt. Kunden ohne stornierte Bestellung erscheinen weiterhin, mit NULLs.

6

-- The same condition in WHERE runs after the join and throws away
-- every row where it is not true — including the NULLs the LEFT JOIN made.
SELECT c.id, c.first_name, o.order_number, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'cancelled'
ORDER BY c.id
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

idfirst_nameorder_numberstatus
3YukiORD-2025-003530cancelled
6NoraORD-2023-001551cancelled
6NoraORD-2022-002224cancelled
7PriyaORD-2022-003262cancelled
10ElenaORD-2025-000182cancelled

Hier steht die Bedingung in WHERE, das nach dem Join läuft und jede Zeile wegwirft, für die sie nicht wahr ist. Die NULL-Zeilen, die der LEFT JOIN gerade erzeugt hat, fallen durch diesen Test – NULL = 'cancelled' ist nicht wahr – und werden alle verworfen. Sie haben einen LEFT JOIN geschrieben und einen INNER JOIN bekommen.

Die Regel ist kurz: Eine Bedingung auf die rechte Tabelle eines LEFT JOIN gehört in ON. Eine Bedingung auf die linke Tabelle gehört in WHERE. Bei einem INNER JOIN macht es tatsächlich keinen Unterschied, weshalb man sich die Gewohnheit leicht falsch angewöhnt.

Eine Tabelle mit sich selbst verbinden

Nirgends steht, dass die beiden Seiten verschiedene Tabellen sein müssen. Mitarbeiter haben eine manager_id, die auf einen anderen Mitarbeiter zeigt, also verbindet sich die Tabelle unter zwei verschiedenen Aliasen mit sich selbst.

7

SELECT e.first_name || ' ' || e.last_name AS employee,
       e.position,
       m.first_name || ' ' || m.last_name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY manager, employee
LIMIT 25;
In Planche ausführen

Ergebnis — erste 5 von 25 Zeilen

employeepositionmanager
Ava MüllerDirectorNULL
Hana TanakaDirectorNULL
Hugo AhmedDirectorNULL
Lucas RomeroDirectorNULL
Maya MüllerDirectorNULL

e und m sind dieselbe Tabelle, zweimal betrachtet, und erst die Aliase machen das möglich. Es ist ein LEFT JOIN, weil die zehn Personen an der Spitze keine Führungskraft haben und ein Inner Join sie still weglassen würde.

Was Sie jetzt können

Sie können Fragen über das ganze Schema stellen. Was noch nicht geht, ist die Antwort zusammenzufassen: Bestellungen zählen, Umsatz summieren und Durchschnitte bilden folgen in der nächsten Lektion.

Selbst ausprobieren

  1. Die 15 neuesten Bewertungen mit Zeitpunkt, Produkt, Verfasser und Note. Das sind drei Tabellen.

    Lösung anzeigen

    Lösung anzeigen

    SELECT r.created_at,
           p.name AS product,
           c.first_name || ' ' || c.last_name AS reviewer,
           r.rating
    FROM reviews r
    JOIN products p ON p.id = r.product_id
    JOIN customers c ON c.id = r.customer_id
    ORDER BY r.created_at DESC
    LIMIT 15;
    In Planche ausführen

    Ergebnis — erste 5 von 15 Zeilen

    created_atproductreviewerrating
    2025-12-28 21:12:12Sol ChronoHugo Park2
    2025-12-28 16:46:22Ember Catan 2Minjun Novak4
    2025-12-28 14:00:00Quill SpeakerOmar Smith3
    2025-12-28 03:41:42Learning React (Deluxe ed.)Ines Romero4
    2025-12-28 03:40:06Zen Sheet Set 2Kenji Ahmed4

    Zwei Joins hintereinander: Bewertungen zu Produkten für den Namen, dann Bewertungen zu Kunden für den Verfasser. Beide sind Inner Joins, weil eine Bewertung immer beides hat.

  2. Zu welchen Bestellungen ist überhaupt keine Sendung erfasst?

    Lösung anzeigen

    Lösung anzeigen

    SELECT o.order_number, o.status, o.ordered_at
    FROM orders o
    LEFT JOIN shipments s ON s.order_id = o.id
    WHERE s.id IS NULL
    ORDER BY o.ordered_at DESC
    LIMIT 20;
    In Planche ausführen

    Ergebnis — erste 5 von 20 Zeilen

    order_numberstatusordered_at
    ORD-2025-000940cancelled2025-12-28 07:28:46
    ORD-2025-003297processing2025-12-28 01:40:38
    ORD-2025-002729cancelled2025-12-27 23:10:11
    ORD-2025-002019paid2025-12-27 09:26:48
    ORD-2025-000337pending2025-12-27 05:29:40

    Ein LEFT JOIN behält jede Bestellung, und WHERE s.id IS NULL behält nur die ohne Zuordnung. 1.681 der 4.000 Bestellungen erfüllen das – plausibel, denn offene und stornierte Bestellungen werden nie versandt.