Zum Inhalt springen
aviral gupta

// B3.3 · ca. 30 Min. · Einstieg

Outer Joins

Nach dieser Lektion behalten Sie Zeilen, die ein Join verwirft, lesen ihre leeren Spalten, setzen Filter so, dass sie bleiben, und finden Mitglieder oder Bücher ohne Partner.

Lektion 3 von 6 in B3 Joins und Aggregate

Danach können Sie

  • Zeilen ohne Treffer mit LEFT, RIGHT oder FULL JOIN behalten und die mit NULL gefüllten Spalten lesen
  • Eine Bedingung an die optionale Tabelle in ON oder in WHERE setzen und wissen, welche Zeilen jeweils bleiben
  • Zeilen ohne Partner per Anti-Join finden: LEFT JOIN … WHERE key IS NULL
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion. Ada und Grace haben je zwei Ausleihen, Linus eine, Margaret keine. Welches Mitglied fehlt im Ergebnis dieses Inner Joins?

    SELECT m.name, l.id
    FROM members m
    JOIN loans l ON l.member_id = m.id;
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. Der Inner Join von members und loans lieferte 5 Zeilen, und Margaret hat keine Ausleihe. Wie viele Zeilen liefert der LEFT JOIN?

    SELECT m.name, l.id
    FROM members m
    LEFT JOIN loans l ON l.member_id = m.id;
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie das Schlüsselwort, damit jedes Buch aufgelistet wird, auch die nie ausgeliehenen.

    SELECT b.title, l.id
    FROM books b
    ____ JOIN loans l ON l.book_id = b.id
    ORDER BY b.id, l.id;
    SELECT b.title, l.id FROM books b JOIN loans l ON l.book_id = b.id ORDER BY b.id, l.id;
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Join die Zeilen zu, die er liefert.

  5. Üben · Aufgabe 5 von 7

    SELECT m.name, l.id FROM … Welche FROM-Klausel liefert dieselben Zeilen wie loans l RIGHT JOIN members m ON l.member_id = m.id?

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Sie suchen die Mitglieder, die nie ein Buch ausgeliehen haben. Die Ausleihen 2, 4 und 5 sind noch offen (returned_on ist NULL). Welche Namen liefert dies?

    -- books: 1 Dune, 2 Emma, 3 Kindred, 4 Beloved, 5 The Hobbit, 6 Neuromancer
    -- members: 1 Ada, 2 Grace, 3 Linus, 4 Margaret
    -- loans (id, book_id, member_id, returned_on):
    --   (1, 1, 1, 2026-09-10), (2, 3, 1, NULL), (3, 2, 2, 2026-09-20),
    --   (4, 1, 3, NULL), (5, 6, 2, NULL)
    
    SELECT m.name
    FROM members m
    LEFT JOIN loans l ON l.member_id = m.id
    WHERE l.returned_on IS NULL;
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie im Editor des Beispiels eine Ausleihgeschichte für den ganzen Katalog: jedes Buch mit dem Namen jedes Mitglieds, das es ausgeliehen hat, und dem Ausleihdatum, sortiert nach Buch-id und dann nach Ausleih-id. Nie ausgeliehene Bücher erscheinen einmal, mit leerem Namen und Datum. Ändern Sie dann ein LEFT JOIN in JOIN und sagen Sie voraus, welche Zeilen verschwinden, bevor Sie es ausführen.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Mitglieder ohne Ausleihen, Bücher ohne Regal

setup.sql lädt die Bibliothek aus Modul B1 (books, members, loans) und die Tabelle shelves aus der letzten Lektion. main.sql listet jedes Mitglied mit seinen Ausleihen, findet die Mitglieder, die nie etwas ausgeliehen haben, und verbindet books und shelves so, dass die Zeilen ohne Partner auf beiden Seiten bleiben. Auf Ihrem Rechner: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.

main.sql

-- 1. Every member, with their loans if they have any.
SELECT m.name, l.id AS loan_id, l.loaned_on
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
ORDER BY m.name, l.id;

-- 2. Members who have never borrowed a book (anti-join).
SELECT m.name
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
WHERE l.id IS NULL;

-- 3. Books and shelves: unmatched rows from both sides.
SELECT genre, b.title, s.floor
FROM books b
FULL JOIN shelves s USING (genre)
ORDER BY genre, b.title;

setup.sql

CREATE TABLE books (
  id integer,
  title text,
  author text,
  year integer,
  genre text,
  copies integer
);
INSERT INTO books VALUES
  (1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
  (2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
  (3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
  (4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
  (5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
  (6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);

CREATE TABLE members (
  id integer,
  name text,
  email text,
  city text,
  joined date
);
INSERT INTO members VALUES
  (1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
  (2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
  (3, 'Linus', NULL, 'Berlin', '2026-02-10'),
  (4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');

CREATE TABLE loans (
  id integer,
  book_id integer,
  member_id integer,
  loaned_on date,
  returned_on date
);
INSERT INTO loans VALUES
  (1, 1, 1, '2026-09-01', '2026-09-10'),
  (2, 3, 1, '2026-09-12', NULL),
  (3, 2, 2, '2026-09-05', '2026-09-20'),
  (4, 1, 3, '2026-09-15', NULL),
  (5, 6, 2, '2026-09-18', NULL);

CREATE TABLE shelves (
  genre text,
  floor integer
);
INSERT INTO shelves VALUES
  ('classic', 1),
  ('fantasy', 2),
  ('sci-fi', 2),
  ('poetry', 3);

Ausführen mit

psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql

Ausgabe

   name   | loan_id | loaned_on
----------+---------+------------
 Ada      |       1 | 2026-09-01
 Ada      |       2 | 2026-09-12
 Grace    |       3 | 2026-09-05
 Grace    |       5 | 2026-09-18
 Linus    |       4 | 2026-09-15
 Margaret |         |
(6 rows)

   name
----------
 Margaret
(1 row)

  genre  |    title    | floor
---------+-------------+-------
 classic | Emma        |     1
 fantasy | The Hobbit  |     2
 fiction | Beloved     |
 poetry  |             |     3
 sci-fi  | Dune        |     2
 sci-fi  | Kindred     |     2
 sci-fi  | Neuromancer |     2
(7 rows)
  • Margaret hat keine Ausleihe, also fügt der LEFT JOIN sie einmal hinzu, mit loan_id und loaned_on NULL; psql zeigt NULL als leere Zelle.
  • WHERE l.id IS NULL behält nur die Zeilen, die der LEFT JOIN hinzugefügt hat. Eine echte Ausleihe hat immer eine id, also bleiben nur Mitglieder ganz ohne Ausleihe.
  • FULL JOIN behält Beloved, dessen Genre fiction kein Regal hat, und das Regal poetry, das kein Buch hat. genre stammt von der Seite, die einen Wert hat.
  • Ein Inner Join von books und shelves lieferte nur die fünf gepaarten Zeilen.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser PostgreSQL herunter (bis zu 10.1 MB) und speichert es im Cache. Jeder Lauf beginnt mit einer leeren Datenbank. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Jedes Mitglied und seine Bücher

Listen Sie jedes Mitglied mit dem Titel jedes Buchs auf, das es ausgeliehen hat, sortiert nach Name und dann Titel. Mitglieder ohne Ausleihe erscheinen einmal, mit leerem Titel. Der Starter verwendet LEFT JOIN für loans, und doch fehlt Margaret. Finden Sie heraus, warum, und beheben Sie es.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser PostgreSQL herunter (bis zu 10.1 MB) und speichert es im Cache. Jeder Lauf beginnt mit einer leeren Datenbank. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Nach dem LEFT JOIN hat Margarets Zeile NULL in l.book_id.

  2. Hinweis 2

    Der nächste Join ist ein Inner Join: b.id = NULL ist nie wahr, also verwirft er ihre Zeile wieder.

  3. Hinweis 3

    Machen Sie auch den zweiten Join zu einem Outer Join: LEFT JOIN books b ON b.id = l.book_id.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

-- Every member, with the titles they borrowed.
SELECT m.name, b.title
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
LEFT JOIN books b ON b.id = l.book_id
ORDER BY m.name, b.title;
Auf dem eigenen Computer ausführen

Installieren Sie PostgreSQL 18 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.sql

-- Every member, with the titles they borrowed.
SELECT m.name, b.title
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
JOIN books b ON b.id = l.book_id
ORDER BY m.name, b.title;

test.sql

-- test: Alle vier Mitglieder stehen in der Liste, Margaret einmal mit leerem Titel
-- output:
--    name   |    title
-- ----------+-------------
--  Ada      | Dune
--  Ada      | Kindred
--  Grace    | Emma
--  Grace    | Neuromancer
--  Linus    | Dune
--  Margaret |
-- (6 rows)

setup.sql

CREATE TABLE books (
  id integer,
  title text,
  author text,
  year integer,
  genre text,
  copies integer
);
INSERT INTO books VALUES
  (1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
  (2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
  (3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
  (4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
  (5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
  (6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);

CREATE TABLE members (
  id integer,
  name text,
  email text,
  city text,
  joined date
);
INSERT INTO members VALUES
  (1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
  (2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
  (3, 'Linus', NULL, 'Berlin', '2026-02-10'),
  (4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');

CREATE TABLE loans (
  id integer,
  book_id integer,
  member_id integer,
  loaned_on date,
  returned_on date
);
INSERT INTO loans VALUES
  (1, 1, 1, '2026-09-01', '2026-09-10'),
  (2, 3, 1, '2026-09-12', NULL),
  (3, 2, 2, '2026-09-05', '2026-09-20'),
  (4, 1, 3, '2026-09-15', NULL),
  (5, 6, 2, '2026-09-18', NULL);

CREATE TABLE shelves (
  genre text,
  floor integer
);
INSERT INTO shelves VALUES
  ('classic', 1),
  ('fantasy', 2),
  ('sci-fi', 2),
  ('poetry', 3);

psql führt die Dateien auf Ihrem eigenen PostgreSQL-Server aus. Nehmen Sie eine leere Datenbank, die Sie wegwerfen können: Hängen Sie -d und ihren Namen an den Befehl an.

Programm ausführen:

psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql

Für die Prüfungen gibt es auf Ihrem Computer noch keinen Befehl. Sie stehen in test.sql: Jede Prüfabfrage gibt t aus, wenn sie besteht.

Übung 2 von 2

Bücher, die niemand ausgeliehen hat

Schreiben Sie zwei Abfragen, jede nach Titel sortiert. Erstens: die Titel der Bücher, die nie ausgeliehen wurden. Zweitens: die Titel der Bücher, die gerade im Regal stehen, also Bücher ohne offene Ausleihe (returned_on IS NULL). Verwenden Sie in beiden LEFT JOIN mit WHERE l.id IS NULL; in der zweiten gehört der Test auf offene Ausleihen in ON.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser PostgreSQL herunter (bis zu 10.1 MB) und speichert es im Cache. Jeder Lauf beginnt mit einer leeren Datenbank. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Ein Anti-Join: LEFT JOIN loans, dann die Zeilen behalten, in denen l.id IS NULL ist, die Bücher ohne Ausleihe.

  2. Hinweis 2

    In der zweiten Abfrage zählen nur offene Ausleihen als Treffer, also gehört l.returned_on IS NULL mit AND in ON.

  3. Hinweis 3

    Emma wurde ausgeliehen und zurückgegeben: Mit dem Test in ON findet sie keine offene Ausleihe und kommt zurück.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

SELECT b.title
FROM books b
LEFT JOIN loans l ON l.book_id = b.id
WHERE l.id IS NULL
ORDER BY b.title;

SELECT b.title
FROM books b
LEFT JOIN loans l
  ON l.book_id = b.id
 AND l.returned_on IS NULL
WHERE l.id IS NULL
ORDER BY b.title;
Auf dem eigenen Computer ausführen

Installieren Sie PostgreSQL 18 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.sql

-- The books that have been loaned.
SELECT b.title
FROM books b
JOIN loans l ON l.book_id = b.id
ORDER BY b.title;

test.sql

-- test: Zuerst Beloved und The Hobbit, dann Beloved, Emma und The Hobbit
-- output:
--    title
-- ------------
--  Beloved
--  The Hobbit
-- (2 rows)
--
--    title
-- ------------
--  Beloved
--  Emma
--  The Hobbit
-- (3 rows)

setup.sql

CREATE TABLE books (
  id integer,
  title text,
  author text,
  year integer,
  genre text,
  copies integer
);
INSERT INTO books VALUES
  (1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
  (2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
  (3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
  (4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
  (5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
  (6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);

CREATE TABLE members (
  id integer,
  name text,
  email text,
  city text,
  joined date
);
INSERT INTO members VALUES
  (1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
  (2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
  (3, 'Linus', NULL, 'Berlin', '2026-02-10'),
  (4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');

CREATE TABLE loans (
  id integer,
  book_id integer,
  member_id integer,
  loaned_on date,
  returned_on date
);
INSERT INTO loans VALUES
  (1, 1, 1, '2026-09-01', '2026-09-10'),
  (2, 3, 1, '2026-09-12', NULL),
  (3, 2, 2, '2026-09-05', '2026-09-20'),
  (4, 1, 3, '2026-09-15', NULL),
  (5, 6, 2, '2026-09-18', NULL);

CREATE TABLE shelves (
  genre text,
  floor integer
);
INSERT INTO shelves VALUES
  ('classic', 1),
  ('fantasy', 2),
  ('sci-fi', 2),
  ('poetry', 3);

psql führt die Dateien auf Ihrem eigenen PostgreSQL-Server aus. Nehmen Sie eine leere Datenbank, die Sie wegwerfen können: Hängen Sie -d und ihren Namen an den Befehl an.

Programm ausführen:

psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql

Für die Prüfungen gibt es auf Ihrem Computer noch keinen Befehl. Sie stehen in test.sql: Jede Prüfabfrage gibt t aus, wenn sie besteht.

Häufige Fehler

OUTER JOIN ohne LEFT, RIGHT oder FULL

SELECT m.name, l.id
FROM members m
OUTER JOIN loans l ON l.member_id = m.id;

Was psql ausgibt

ERROR:  syntax error at or near "OUTER"

Warum, und die Lösung

OUTER ist nach LEFT, RIGHT oder FULL ein optionales Füllwort und kann nicht allein stehen: PostgreSQL muss wissen, welche Seite es behalten soll. Schreiben Sie LEFT JOIN (oder LEFT OUTER JOIN), um jedes Mitglied zu behalten.

Die Join-Bedingung in WHERE statt in ON

SELECT m.name, l.id
FROM members m
LEFT JOIN loans l
WHERE l.member_id = m.id;

Was psql ausgibt

ERROR:  syntax error at or near "WHERE"

Warum, und die Lösung

Ein Outer Join braucht wie ein Inner Join seine Bedingung in ON (oder USING) direkt nach der Tabelle. Selbst mit einem CROSS JOIN liefe die Bedingung in WHERE erst nach dem Join und verwürfe Margaret. Schreiben Sie LEFT JOIN loans l ON l.member_id = m.id.

Ein ON, das eine später verbundene Tabelle verwendet

SELECT m.name, b.title
FROM members m
LEFT JOIN loans l ON l.book_id = b.id
LEFT JOIN books b ON b.id = l.book_id;

Was psql ausgibt

ERROR:  missing FROM-clause entry for table "b"

Warum, und die Lösung

Joins werden von links nach rechts aufgebaut, und jedes ON kann nur die bis dahin verbundenen Tabellen verwenden. loans muss über l.member_id = m.id mit members verbunden werden; books kommt danach und verwendet l.book_id.

PostgreSQL im Browser: PGlite 0.5.8 (PostgreSQL 18.3), Apache-2.0 und PostgreSQL License. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

LEFT JOIN behält jede Zeile der linken Tabelle

members LEFT JOIN loans ON l.member_id = m.id führt zuerst den Inner Join aus: 5 Zeilen, eine pro Ausleihe. Dann fügt er jedes Mitglied ohne Treffer einmal hinzu, mit NULL in jeder Spalte von loans: Margaret, also 6 Zeilen. psql zeigt diese NULLs als leere Zellen. Welche Tabelle links steht, zählt: loans LEFT JOIN members behält jede Ausleihe, und da jede Ausleihe ein Mitglied hat, sind das nur die 5 Treffer. In einer Kette führen Sie den Outer Join weiter: Nach members LEFT JOIN loans findet ein einfaches JOIN books ON b.id = l.book_id für Margarets NULL kein Buch und verwirft sie wieder; schreiben Sie LEFT JOIN books.

RIGHT und FULL JOIN

RIGHT JOIN behält stattdessen jede Zeile der rechten Tabelle: shelves s RIGHT JOIN books b ON b.genre = s.genre behält alle sechs Bücher, und Beloved, dessen Genre kein Regal hat, bekommt ein leeres floor. Das ist dasselbe wie books b LEFT JOIN shelves s, bis auf die Spaltenreihenfolge, weshalb die meisten nur LEFT JOIN schreiben. FULL JOIN behält die Zeilen ohne Treffer beider Seiten: books FULL JOIN shelves USING (genre) liefert die fünf Treffer, Beloved ohne Stockwerk und das Regal poetry ohne Buch, 7 Zeilen. Mit USING zeigt die gemeinsame Spalte genre den Wert der Seite, die einen hat.

ON oder WHERE, und der Anti-Join

Eine Bedingung in ON wird angewandt, bevor der Outer Join seine NULL-Zeilen hinzufügt, eine in WHERE danach. members LEFT JOIN loans ON l.member_id = m.id AND l.returned_on IS NOT NULL behält alle vier Mitglieder, mit zurückgegebenen Ausleihen, wo es sie gibt. Derselbe Test in WHERE entfernt Linus und Margaret, deren Zeilen dort NULL haben. WHERE lässt sich auch gezielt so nutzen: LEFT JOIN loans … WHERE l.id IS NULL behält nur die Mitglieder ganz ohne Ausleihe, ein Anti-Join. Prüfen Sie eine Spalte, die bei einem echten Treffer nie NULL ist, etwa l.id. returned_on ist auch bei offenen Ausleihen NULL und würde diese mit behalten.

Quellen

Zuletzt geprüft am 30. September 2026