Aufwärmen · Aufgabe 1 von 7
// B3.6 · ca. 30 Min. · Einstieg
Projekt: ein Ausleihbericht
Nach diesem Projekt schreiben Sie einen Bericht, der jedes Mitglied behält, zählt, was es ausgeliehen hat und noch hat, und dessen Summen Sie mit den Daten prüfen.
Lektion 6 von 6 in B3 Joins und Aggregate
Danach können Sie
- Jedes Mitglied mit LEFT JOIN im Bericht behalten und seine Ausleihen mit count(l.id) zählen
- Offene Ausleihen mit FILTER zählen und das letzte Ausleihdatum je Mitglied mit max finden
- Je Genre berichten, nie ausgeliehene Bücher per Anti-Join finden und die Summen prüfen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es voraus, bevor Sie weiterlesen. Margaret hat nie ein Buch ausgeliehen. Was zeigt die Spalte loans in ihrer Zeile?
SELECT m.name, count(*) AS loans FROM members m LEFT JOIN loans l ON l.member_id = m.id GROUP BY m.id, m.name;Üben · Aufgabe 3 von 7
Ergänzen Sie das Argument von count, damit Margarets Zeile 0 Ausleihen zeigt.
SELECT m.name, count(____) AS loans FROM members m LEFT JOIN loans l ON l.member_id = m.id GROUP BY m.id, m.name ORDER BY m.name;SELECT m.name, count() AS loans FROM members m LEFT JOIN loans l ON l.member_id = m.id GROUP BY m.id, m.name ORDER BY m.name;Üben · Aufgabe 4 von 7
Ordnen Sie jeder Spalte des Berichts den Ausdruck zu, der sie berechnet.
Üben · Aufgabe 5 von 7
Bringen Sie die Zeilen des Berichts je Mitglied in die Reihenfolge, die SQL verlangt.
- 1.ORDER BY m.name;
- 2.GROUP BY m.id, m.name
- 3.FROM members m
- 4.SELECT m.name, count(l.id) AS loans
- 5.LEFT JOIN loans l ON l.member_id = m.id
Denksport · Aufgabe 6 von 7
Knobelaufgabe. Margaret hat überhaupt keine Ausleihen. Was zeigt die Spalte open in ihrer Zeile?
SELECT m.name, count(*) FILTER (WHERE l.returned_on IS NULL) AS open FROM members m LEFT JOIN loans l ON l.member_id = m.id GROUP BY m.id, m.name;Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie den Bericht je Stadt: für jede Stadt die Zahl der Mitglieder, ihre Ausleihen und ihre offenen Ausleihen, wobei jedes Mitglied zählt, auch Margaret, deren Stadt NULL ist (zeigen Sie sie als unknown). Sagen Sie zuerst voraus, was in der Zeile Berlin steht, und prüfen Sie dann, dass die Spalte der Ausleihen 5 ergibt.
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
Ein Bericht je Buch und seine Probe
setup.sql lädt die Bibliothek aus Modul B1: Bücher, Mitglieder und Ausleihen. main.sql baut den Bericht je Buch: wie oft jedes Buch ausgeliehen wurde, wie viele Exemplare noch unterwegs sind und das letzte Ausleihdatum. Danach listet es die Bücher, die niemand ausgeliehen hat, und die Summen, an denen Sie den Bericht prüfen. Auf Ihrem Computer: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.
main.sql
-- 1. Every book, lent or not: times lent, still out, last loan.
SELECT b.title,
count(l.id) AS times_lent,
count(l.id) FILTER (WHERE l.returned_on IS NULL) AS still_out,
max(l.loaned_on) AS last_loan
FROM books b
LEFT JOIN loans l ON l.book_id = b.id
GROUP BY b.id, b.title
ORDER BY b.id;
-- 2. The books nobody has borrowed yet.
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;
-- 3. The totals, to check the report against.
SELECT count(*) AS loans,
count(*) FILTER (WHERE returned_on IS NULL) AS open,
count(DISTINCT member_id) AS borrowers
FROM loans;
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);
Ausführen mit
psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sqlAusgabe
title | times_lent | still_out | last_loan
-------------+------------+-----------+------------
Dune | 2 | 1 | 2026-09-15
Emma | 1 | 0 | 2026-09-05
Kindred | 1 | 1 | 2026-09-12
Beloved | 0 | 0 |
The Hobbit | 0 | 0 |
Neuromancer | 1 | 1 | 2026-09-18
(6 rows)
title
------------
Beloved
The Hobbit
(2 rows)
loans | open | borrowers
-------+------+-----------
5 | 3 | 3
(1 row)- FROM books LEFT JOIN loans behält Beloved und The Hobbit, und count(l.id) gibt ihnen 0 statt 1.
- GROUP BY b.id, b.title bildet eine Zeile je Buch; ORDER BY b.id behält die Reihenfolge des Katalogs.
- max(l.loaned_on) ist das späteste Ausleihdatum; ein Buch ohne Ausleihen bekommt NULL, eine leere Zelle.
- Die Probe: times_lent ergibt zusammen 5 und still_out 3, die Summen der dritten Abfrage.
Ä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
Schritt 1: der Bericht je Mitglied
Die Vorlage verbindet loans und members mit einem Inner Join, also fehlt Margaret, und sie zählt mit count(*). Machen Sie daraus den Bericht je Mitglied: jedes Mitglied, mit name, loans (Zahl der Ausleihen), open (noch nicht zurückgegebene Ausleihen) und last_loan (Datum der letzten Ausleihe), nach Namen sortiert.
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
Hinweis 1
Beginnen Sie FROM members m und holen Sie die Ausleihen mit LEFT JOIN loans l ON l.member_id = m.id dazu, damit Margaret bleibt.
Hinweis 2
Zählen Sie eine Spalte von loans, count(l.id), nicht count(*): Margarets NULL-Zeile muss als 0 zählen.
Hinweis 3
open ist count(l.id) FILTER (WHERE l.returned_on IS NULL); last_loan ist max(l.loaned_on).
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
SELECT m.name,
count(l.id) AS loans,
count(l.id) FILTER (WHERE l.returned_on IS NULL) AS open,
max(l.loaned_on) AS last_loan
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
GROUP BY m.id, m.name
ORDER BY m.name;
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
-- setup.sql has loaded books, members and loans.
SELECT m.name, count(*) AS loans
FROM loans l
JOIN members m ON m.id = l.member_id
GROUP BY m.id, m.name
ORDER BY m.name;
test.sql
-- test: Jedes Mitglied erscheint, Margaret mit 0 Ausleihen, 0 offenen und ohne letzte Ausleihe
-- output:
-- name | loans | open | last_loan
-- ----------+-------+------+------------
-- Ada | 2 | 1 | 2026-09-12
-- Grace | 2 | 1 | 2026-09-18
-- Linus | 1 | 1 | 2026-09-15
-- Margaret | 0 | 0 |
-- (4 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);
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.sqlFü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
Schritt 2: der Bericht je Genre
Schreiben Sie zwei Abfragen. Erstens den Bericht je Genre: jedes Genre, mit genre, books (Zahl der Bücher, jedes einmal gezählt), loans und open, nach Genre sortiert; beginnen Sie bei books und holen Sie die Ausleihen mit LEFT JOIN dazu. Zweitens die Genres, deren Bücher nie ausgeliehen wurden, nur mit genre, nach Genre sortiert.
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
Hinweis 1
FROM books b LEFT JOIN loans l ON l.book_id = b.id, dann GROUP BY b.genre.
Hinweis 2
Dune hat zwei Ausleihen und erscheint nach dem Join zweimal: count(DISTINCT b.id) zählt es einmal.
Hinweis 3
Ein Genre ohne Ausleihen hat count(l.id) = 0; diese Prüfung betrifft eine Gruppe, gehört also in HAVING.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
SELECT b.genre,
count(DISTINCT b.id) AS books,
count(l.id) AS loans,
count(l.id) FILTER (WHERE l.returned_on IS NULL) AS open
FROM books b
LEFT JOIN loans l ON l.book_id = b.id
GROUP BY b.genre
ORDER BY b.genre;
SELECT b.genre
FROM books b
LEFT JOIN loans l ON l.book_id = b.id
GROUP BY b.genre
HAVING count(l.id) = 0
ORDER BY b.genre;
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
-- setup.sql has loaded books, members and loans.
SELECT genre, count(*) AS books
FROM books
GROUP BY genre
ORDER BY genre;
test.sql
-- test: Der Bericht je Genre, dann fantasy und fiction als nie ausgeliehene Genres
-- output:
-- genre | books | loans | open
-- ---------+-------+-------+------
-- classic | 1 | 1 | 0
-- fantasy | 1 | 0 | 0
-- fiction | 1 | 0 | 0
-- sci-fi | 3 | 4 | 3
-- (4 rows)
--
-- genre
-- ---------
-- fantasy
-- fiction
-- (2 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);
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.sqlFü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
Eine ausgewählte Spalte fehlt in GROUP BY
SELECT m.name, count(l.id) AS loans
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
GROUP BY m.id;
Was psql ausgibt
ERROR: column "m.name" must appear in the GROUP BY clause or be used in an aggregate functionWarum, und die Lösung
Jede Ausgabezeile steht für eine Gruppe, und PostgreSQL weiß hier nicht, dass eine Mitglieds-id nur einen Namen hat. Führen Sie jede ausgewählte Spalte, die nicht in einem Aggregat steht, in GROUP BY auf: GROUP BY m.id, m.name. Behalten Sie m.id dort, damit zwei Mitglieder mit gleichem Namen getrennt bleiben.
Eine Spalte ohne Tabellenangabe, die beide Tabellen haben
SELECT m.name, count(id) AS loans
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
GROUP BY m.id, m.name;
Was psql ausgibt
ERROR: column reference "id" is ambiguousWarum, und die Lösung
members und loans haben beide eine Spalte id, also könnte ein bloßes id beide meinen. Stellen Sie den Tabellen-Alias davor. Für den Bericht muss es die Spalte von loans sein, count(l.id); count(m.id) würde Margarets Zeile wieder als 1 zählen.
Ein Aggregat in WHERE, um Mitglieder ohne Ausleihen zu finden
SELECT m.name
FROM members m
LEFT JOIN loans l ON l.member_id = m.id
WHERE count(l.id) = 0
GROUP BY m.id, m.name;
Was psql ausgibt
ERROR: aggregate functions are not allowed in WHEREWarum, und die Lösung
WHERE läuft über einzelne Zeilen, bevor es die Gruppen und ihre Zählwerte gibt. Prüfen Sie die Zahl in HAVING: GROUP BY m.id, m.name HAVING count(l.id) = 0. Oder verzichten Sie auf das Gruppieren und nehmen den Anti-Join: LEFT JOIN loans l … WHERE l.id IS NULL.
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.