Zum Inhalt springen
aviral gupta

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

HAVING und FILTER

Nach dieser Lektion behalten Sie nur die Gruppen, auf die es ankommt, zählen offene und zurückgegebene Ausleihen nebeneinander und listen die Titel einer Gruppe in einer Zelle.

Lektion 5 von 6 in B3 Joins und Aggregate

Danach können Sie

  • WHERE für einzelne Zeilen und HAVING für Gruppen wählen und ein Aggregat in WHERE reparieren
  • Mit HAVING auf count oder sum nur die benötigten Gruppen behalten
  • Mehrere Teilmengen in einem Durchgang mit FILTER zählen und Werte mit string_agg verbinden
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen: Eine Abfrage gruppiert die Ausleihen nach Mitglied und zählt sie. Welche Klausel behält nur die Mitglieder mit mindestens zwei Ausleihen?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. Die sci-fi-Bücher haben 2, 1 und 1 Exemplare; classic, fiction und fantasy haben je ein Buch, mit 1, 1 und 3 Exemplaren. Was liefert dies?

    SELECT genre, count(*)
    FROM books
    WHERE copies = 1
    GROUP BY genre
    HAVING count(*) > 1;
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie das Schlüsselwort, damit die Abfrage nur die Genres mit mehr als einem Buch liefert.

    SELECT genre, count(*) FROM books GROUP BY genre ____ count(*) > 1;
    SELECT genre, count(*) FROM books GROUP BY genre count(*) > 1;
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Klausel zu, was sie filtert oder ordnet.

  5. Üben · Aufgabe 5 von 7

    Fünf Ausleihen, drei davon noch offen (returned_on ist NULL). Was liefert dies?

    SELECT count(*) FILTER (WHERE returned_on IS NULL) AS open,
           count(*) AS all_loans
    FROM loans;
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Ada und Linus wohnen in Berlin, Grace in München (Munich), Margarets Stadt ist NULL. Was liefert dies?

    SELECT string_agg(city, ', ' ORDER BY city)
    FROM members;
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie mit der Bibliothek des Beispiels eine Abfrage über loans, verbunden mit books: für jedes Genre die Zahl der Ausleihen, die Zahl der noch offenen und die ausgeliehenen Titel alphabetisch in einer Zelle. Behalten Sie nur die Genres mit mindestens zwei Ausleihen. Sagen Sie zuerst voraus, welche Genres übrig bleiben und warum ein Titel zweimal erscheint.

    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

Gruppen filtern und Teilmengen zählen

setup.sql lädt die Bibliothek aus Modul B1: Bücher, Mitglieder und Ausleihen. main.sql behält die Genres mit mindestens drei Exemplaren, kombiniert WHERE und HAVING, zählt offene und zurückgegebene Ausleihen je Mitglied in einem Durchgang und listet die Titel jedes Genres. Auf Ihrem Computer: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.

main.sql

-- 1. Genres with at least three copies in all.
SELECT genre, count(*) AS books, sum(copies) AS copies
FROM books
GROUP BY genre
HAVING sum(copies) >= 3
ORDER BY genre;

-- 2. WHERE picks loans, HAVING picks members.
SELECT member_id, count(*) AS september_loans
FROM loans
WHERE loaned_on >= '2026-09-05'
GROUP BY member_id
HAVING count(*) >= 2
ORDER BY member_id;

-- 3. Several counts in one pass with FILTER.
SELECT m.name,
       count(*) AS loans,
       count(*) FILTER (WHERE l.returned_on IS NULL) AS open,
       count(*) FILTER (WHERE l.returned_on IS NOT NULL) AS returned
FROM loans l
JOIN members m ON m.id = l.member_id
GROUP BY m.id, m.name
ORDER BY m.name;

-- 4. The titles of each genre in one cell.
SELECT genre, string_agg(title, ', ' ORDER BY title) AS titles
FROM books
GROUP BY genre
ORDER BY genre;

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.sql

Ausgabe

  genre  | books | copies
---------+-------+--------
 fantasy |     1 |      3
 sci-fi  |     3 |      4
(2 rows)

 member_id | september_loans
-----------+-----------------
         2 |               2
(1 row)

 name  | loans | open | returned
-------+-------+------+----------
 Ada   |     2 |    1 |        1
 Grace |     2 |    1 |        1
 Linus |     1 |    1 |        0
(3 rows)

  genre  |           titles
---------+----------------------------
 classic | Emma
 fantasy | The Hobbit
 fiction | Beloved
 sci-fi  | Dune, Kindred, Neuromancer
(4 rows)
  • HAVING sum(copies) >= 3 behält fantasy (The Hobbit allein hat 3) und sci-fi (2 + 1 + 1); das Aggregat in HAVING ist ausgeschrieben, nicht aus dem Alias übernommen.
  • WHERE verwirft zuerst Adas Ausleihe vom 2026-09-01; von den vier übrigen Ausleihen hat nur Mitglied 2 zwei, also behält HAVING eine Gruppe.
  • Die drei Zählwerte der dritten Abfrage sehen jeweils andere Zeilen: FILTER schränkt nur die Eingabe seines eigenen count ein.
  • ORDER BY in string_agg sortiert die Titel innerhalb jeder Zelle; das ORDER BY am Ende sortiert die 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

Nur die Gruppen, auf die es ankommt

Schreiben Sie zwei Abfragen. Erstens: die Genres mit insgesamt mindestens zwei Exemplaren, mit genre und den Exemplaren insgesamt (copies), nach Genre sortiert. Zweitens: die Mitglieder, die mindestens zwei Bücher ausgeliehen haben, mit Name und der Zahl der Ausleihen (loans), nach Namen sortiert; verbinden Sie loans mit members.

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

    Die Bedingung betrifft die Summe einer Gruppe, gehört also in HAVING, zwischen GROUP BY und ORDER BY.

  2. Hinweis 2

    Schreiben Sie das Aggregat in HAVING aus: HAVING sum(copies) >= 2. Der Alias copies ist dort unbekannt.

  3. Hinweis 3

    Für die Mitglieder: JOIN members m ON m.id = l.member_id, GROUP BY m.id, m.name, HAVING count(*) >= 2.

Eine Lösung zeigen

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

SELECT genre, sum(copies) AS copies
FROM books
GROUP BY genre
HAVING sum(copies) >= 2
ORDER BY genre;

SELECT m.name, count(*) AS loans
FROM loans l
JOIN members m ON m.id = l.member_id
GROUP BY m.id, m.name
HAVING count(*) >= 2
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 genre, sum(copies) AS copies
FROM books
GROUP BY genre
ORDER BY genre;

test.sql

-- test: fantasy und sci-fi haben mindestens zwei Exemplare, dann haben Ada und Grace je zwei Ausleihen
-- output:
--   genre  | copies
-- ---------+--------
--  fantasy |      3
--  sci-fi  |      4
-- (2 rows)
--
--  name  | loans
-- -------+-------
--  Ada   |     2
--  Grace |     2
-- (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.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

Offen, zurückgegeben und welche Titel

Die Vorlage zählt die Ausleihen jedes Mitglieds. Ersetzen Sie den einen Zählwert durch zwei: open (kein returned_on) und returned, mit FILTER. Ergänzen Sie dann titles: die Titel, die das Mitglied ausgeliehen hat, alphabetisch, getrennt durch Komma und Leerzeichen. Verbinden Sie books für die Titel. Behalten Sie die Sortierung nach Namen.

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

    count(*) FILTER (WHERE l.returned_on IS NULL) AS open zählt nur die offenen Ausleihen jedes Mitglieds.

  2. Hinweis 2

    Die zurückgegebenen Ausleihen sind die, bei denen l.returned_on IS NOT NULL gilt.

  3. Hinweis 3

    JOIN books b ON b.id = l.book_id, dann string_agg(b.title, ', ' ORDER BY b.title) AS titles; das ORDER BY steht nach dem Trennzeichen.

Eine Lösung zeigen

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

SELECT m.name,
       count(*) FILTER (WHERE l.returned_on IS NULL) AS open,
       count(*) FILTER (WHERE l.returned_on IS NOT NULL) AS returned,
       string_agg(b.title, ', ' ORDER BY b.title) AS titles
FROM loans l
JOIN members m ON m.id = l.member_id
JOIN books b ON b.id = l.book_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

-- Loans per member: one count so far.
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 mit offenen und zurückgegebenen Ausleihen und den ausgeliehenen Titeln in alphabetischer Reihenfolge
-- output:
--  name  | open | returned |      titles
-- -------+------+----------+-------------------
--  Ada   |    1 |        1 | Dune, Kindred
--  Grace |    1 |        1 | Emma, Neuromancer
--  Linus |    1 |        0 | Dune
-- (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);

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

Ein Aggregat in WHERE

SELECT genre, sum(copies)
FROM books
WHERE sum(copies) >= 3
GROUP BY genre;

Was psql ausgibt

ERROR:  aggregate functions are not allowed in WHERE

Warum, und die Lösung

WHERE entscheidet, welche Zeilen in die Gruppen kommen, läuft also, bevor es eine Summe gibt. Eine Bedingung über die Summe einer Gruppe gehört in HAVING, nach GROUP BY: GROUP BY genre HAVING sum(copies) >= 3. Bedingungen über einzelne Zeilen, etwa year > 1970, bleiben in WHERE.

Ein Ausgabe-Alias in HAVING

SELECT genre, count(*) AS n
FROM books
GROUP BY genre
HAVING n > 1;

Was psql ausgibt

ERROR:  column "n" does not exist

Warum, und die Lösung

Den Alias n vergibt die SELECT-Liste, und die wird nach HAVING berechnet, also kennt HAVING ihn nicht. Wiederholen Sie das Aggregat: HAVING count(*) > 1. ORDER BY n würde funktionieren, weil ORDER BY nach der SELECT-Liste läuft. Ein Alias, der zugleich ein Tabellenname ist, ist noch tückischer: HAVING books > 1 scheitert mit operator does not exist: books > integer, weil books dann die ganze Zeile der Tabelle meint.

string_agg über Zahlen

SELECT genre, string_agg(year, ', ')
FROM books
GROUP BY genre;

Was psql ausgibt

ERROR:  function string_agg(integer, unknown) does not exist

Warum, und die Lösung

string_agg verbindet Text (oder bytea), und year ist ein integer. Wandeln Sie die Werte zuerst in Text um: string_agg(year::text, ', ' ORDER BY year). Sortieren nach year selbst hält die Zahlen in numerischer Reihenfolge, also kommt 1965 vor 1979 und 1984.

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

WHERE filtert Zeilen, HAVING filtert Gruppen

Eine gruppierte Abfrage arbeitet in Schritten: FROM und die Joins bilden die Zeilen, WHERE verwirft einzelne Zeilen, GROUP BY bildet die Gruppen und berechnet die Aggregate, HAVING verwirft ganze Gruppen, danach laufen SELECT und ORDER BY. WHERE kann also kein Aggregat verwenden, weil es noch keine Gruppe gibt: aggregate functions are not allowed in WHERE. HAVING count(*) >= 2 behält die Gruppen mit mindestens zwei Zeilen. Eine Abfrage kann beides nutzen: WHERE copies = 1 entfernt zuerst die Bücher mit mehr Exemplaren, dann behält HAVING count(*) > 1 die Genres, die noch zwei haben. Eine Bedingung über einzelne Zeilen gehört in WHERE, auch wo HAVING sie annähme.

In HAVING schreiben Sie das Aggregat aus

HAVING läuft, bevor SELECT seine Ausgabespalten berechnet, deshalb ist ein Alias aus der SELECT-Liste dort unbekannt: HAVING n > 1 scheitert mit column "n" does not exist. Schreiben Sie das Aggregat erneut: HAVING count(*) > 1. ORDER BY läuft zuletzt und darf den Alias verwenden. Das Aggregat in HAVING muss nicht ausgewählt sein: SELECT genre … HAVING sum(copies) >= 3 ist in Ordnung. HAVING kann auch eine gruppierte Spalte prüfen, HAVING genre <> 'sci-fi', aber WHERE erledigt das günstiger. Ohne GROUP BY prüft HAVING die eine Gruppe der ganzen Tabelle, und scheitert die Prüfung, liefert die Abfrage gar keine Zeile, keine 0.

FILTER zählt Teilmengen, string_agg listet Werte

count(*) FILTER (WHERE returned_on IS NULL) zählt nur die offenen Ausleihen, während ein einfaches count(*) daneben weiterhin alle zählt: FILTER entfernt Zeilen nur aus der Eingabe seines eigenen Aggregats. So liefert ein Durchgang über die Ausleihen die Gesamtzahl, die offenen und die zurückgegebenen nebeneinander, je Mitglied oder je Genre. string_agg(title, ', ') verbindet die Textwerte einer Gruppe zu einem String, mit dem Trennzeichen dazwischen, und überspringt NULL. Ihre Reihenfolge ist unbestimmt, außer Sie schreiben ORDER BY in den Aufruf, nach allen Argumenten: string_agg(title, ', ' ORDER BY title). Es nimmt Text; wandeln Sie eine Zahl zuerst um, year::text.

Quellen

Zuletzt geprüft am 30. September 2026