Zum Inhalt springen
aviral gupta

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

Joins, GROUP BY und NULL: vorhersagen, was SQL liefert

Nach dieser Lektion sagen Sie voraus, wie viele Zeilen ein Join liefert, verhindern, dass ein LEFT JOIN still zum Inner Join wird, und zählen und filtern Gruppen ohne NULL-Überraschungen.

Lektion 1 von 6 in B3 Joins und Aggregate

Anfang des Moduls

Danach können Sie

  • Die Zeilenzahl eines INNER JOIN und eines LEFT JOIN auf kleinen Tabellen vorhersagen
  • Einen Filter auf die rechte Tabelle in ON statt in WHERE setzen, wenn jede linke Zeile bleiben muss
  • Zwischen COUNT(*) und COUNT(Spalte), WHERE und HAVING sowie = und IS NULL wählen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen: In einer Tabelle orders enthält die Spalte customer_id die id einer Zeile der Tabelle customers. Wie nennt man diese Spalte?

    -- customers                 -- orders
    -- id | name | city           -- id  | customer_id | amount | status
    --  1 | Ana  | Berlin         -- 101 |      1      |   50   | paid
    --  2 | Ben  | Munich         -- 102 |      1      |   30   | refunded
    --  3 | Cleo | NULL           -- 103 |      2      |   20   | paid
    --  4 | Dev  | Berlin
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: Wie viele Zeilen liefert diese Abfrage?

    -- customers                 -- orders
    -- id | name | city           -- id  | customer_id | amount | status
    --  1 | Ana  | Berlin         -- 101 |      1      |   50   | paid
    --  2 | Ben  | Munich         -- 102 |      1      |   30   | refunded
    --  3 | Cleo | NULL           -- 103 |      2      |   20   | paid
    --  4 | Dev  | Berlin
    
    SELECT c.name, o.id
    FROM customers c
    LEFT JOIN orders o ON o.customer_id = c.id;
  3. Üben · Aufgabe 3 von 7

    Der Bericht soll jeden Kunden auflisten, mit seinen bezahlten Bestellungen, falls es welche gibt. Dieselben Tabellen. Wie viele Zeilen liefert die Abfrage, und warum?

    SELECT c.name, o.id
    FROM customers c
    LEFT JOIN orders o ON o.customer_id = c.id
    WHERE o.status = 'paid';
  4. Üben · Aufgabe 4 von 7

    Ergänzen Sie die Abfrage so, dass sie für Kunden ohne Bestellung 0 statt 1 zeigt. Tragen Sie das Argument von COUNT ein.

    SELECT c.name, COUNT(____) AS order_count
    FROM customers c
    LEFT JOIN orders o ON o.customer_id = c.id
    GROUP BY c.name;
    COUNT() AS order_count
  5. Üben · Aufgabe 5 von 7

    Bringen Sie die Klauseln in die Reihenfolge, in der PostgreSQL ein SELECT logisch verarbeitet. Diese Reihenfolge erklärt, warum WHERE COUNT(*) nicht nutzen kann, HAVING aber schon.

    1. 1.GROUP BY: Gruppen bilden und Aggregate berechnen
    2. 2.ORDER BY: das Ergebnis sortieren
    3. 3.SELECT: die Ausgabespalten berechnen
    4. 4.LIMIT: nur die ersten Zeilen behalten
    5. 5.FROM und JOIN: die verbundenen Zeilen bilden
    6. 6.HAVING: Gruppen verwerfen, die die Bedingung nicht erfüllen
    7. 7.WHERE: Zeilen verwerfen, die die Bedingung nicht erfüllen
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe: Sie wollen jeden Kunden, der nicht in Berlin wohnt. Dieselbe Tabelle customers (Cleos city ist NULL). Welche Namen liefert die Abfrage?

    SELECT name
    FROM customers
    WHERE city <> 'Berlin';
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie mit denselben Tabellen customers und orders eine Abfrage, die jeden Kunden auflistet, auch die ohne Bestellung, mit der Zahl der bezahlten Bestellungen und der bezahlten Gesamtsumme. Kunden ohne Bezahltes müssen 0 und 0 zeigen, nicht NULL. Führen Sie sie im Editor des Beispiels aus, dessen setup.sql die beiden Tabellen anlegt; erwartet wird Ana 1 50, Ben 1 20, Cleo 0 0, Dev 0 0.

    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

Dieselben Kunden, auf zwei Arten verbunden

setup.sql legt die beiden Tabellen dieser Lektion an; main.sql verbindet sie zweimal und sucht dann einen unbekannten Ort. Vergleichen Sie die ersten beiden Ergebnisse Zeile für Zeile: Der LEFT JOIN liefert alles, was der Inner Join liefert, und dazu eine mit NULL gefüllte Zeile für jeden Kunden ohne Bestellung. Auf Ihrem Rechner führen Sie es mit psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql aus.

main.sql

-- 1. Inner join: only customers who have an order.
SELECT c.name, o.id AS order_id, o.amount
FROM customers c
JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.id;

-- 2. Left join: every customer, NULL where there is no order.
SELECT c.name, o.id AS order_id, o.amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.id;

-- 3. Customers whose city is unknown.
SELECT name FROM customers WHERE city IS NULL;

setup.sql

CREATE TABLE customers (
  id integer PRIMARY KEY,
  name text,
  city text
);
INSERT INTO customers VALUES
  (1, 'Ana', 'Berlin'),
  (2, 'Ben', 'Munich'),
  (3, 'Cleo', NULL),
  (4, 'Dev', 'Berlin');

CREATE TABLE orders (
  id integer PRIMARY KEY,
  customer_id integer REFERENCES customers,
  amount integer,
  status text
);
INSERT INTO orders VALUES
  (101, 1, 50, 'paid'),
  (102, 1, 30, 'refunded'),
  (103, 2, 20, 'paid');

Ausführen mit

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

Ausgabe

 name | order_id | amount
------+----------+--------
 Ana  |      101 |     50
 Ana  |      102 |     30
 Ben  |      103 |     20
(3 rows)

 name | order_id | amount
------+----------+--------
 Ana  |      101 |     50
 Ana  |      102 |     30
 Ben  |      103 |     20
 Cleo |          |
 Dev  |          |
(5 rows)

 name
------
 Cleo
(1 row)
  • JOIN allein bedeutet INNER JOIN: Cleo und Dev haben keine Bestellung und fehlen daher im ersten Ergebnis.
  • Ana hat zwei Bestellungen und erscheint daher in beiden Ergebnissen zweimal: Ein Join liefert eine Zeile je passendem Paar.
  • Im LEFT JOIN erscheinen Cleo und Dev je einmal, die Spalten von orders leer: psql zeigt NULL als nichts.
  • Die dritte Abfrage nutzt IS NULL; city = NULL lieferte gar keine Zeile.
Ä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 3

Null, nicht eins

setup.sql hat customers und orders angelegt. Die Abfrage soll jeden Kunden mit der Zahl seiner Bestellungen zeigen, meldet aber 1 für Cleo und Dev, die keine haben. Ändern Sie, was COUNT zählt, sodass beide 0 zeigen. LEFT JOIN, GROUP BY und ORDER BY bleiben.

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 Cleo noch eine Zeile, mit NULL in jeder Spalte von orders. COUNT(*) zählt diese Zeile.

  2. Hinweis 2

    COUNT(Spalte) zählt nur die Zeilen, in denen diese Spalte nicht NULL ist.

  3. Hinweis 3

    Zählen Sie eine Spalte von orders, die jede echte Bestellung hat, etwa ihren Primärschlüssel: COUNT(o.id).

Eine Lösung zeigen

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

SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.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

-- Cleo and Dev have no orders, yet this reports 1 for each.
SELECT c.name, COUNT(*) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;

test.sql

-- test: Jeder Kunde erscheint, mit 0 für die ohne Bestellung
-- output:
--  name | order_count
-- ------+-------------
--  Ana  |           2
--  Ben  |           1
--  Cleo |           0
--  Dev  |           0
-- (4 rows)

setup.sql

CREATE TABLE customers (
  id integer PRIMARY KEY,
  name text,
  city text
);
INSERT INTO customers VALUES
  (1, 'Ana', 'Berlin'),
  (2, 'Ben', 'Munich'),
  (3, 'Cleo', NULL),
  (4, 'Dev', 'Berlin');

CREATE TABLE orders (
  id integer PRIMARY KEY,
  customer_id integer REFERENCES customers,
  amount integer,
  status text
);
INSERT INTO orders VALUES
  (101, 1, 50, 'paid'),
  (102, 1, 30, 'refunded'),
  (103, 2, 20, 'paid');

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 3

Nicht in Berlin, Unbekannte eingeschlossen

Listen Sie Name und Ort jedes Kunden auf, von dem nicht bekannt ist, dass er in Berlin wohnt, nach Namen sortiert. Ein Kunde mit city NULL gehört auch dazu: Wir wissen nicht, dass er in Berlin wohnt. Der Startcode vergisst ihn.

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

    Für Cleo ist NULL <> 'Berlin' nicht true, sondern NULL, also verwirft WHERE ihre Zeile.

  2. Hinweis 2

    Entweder ergänzen Sie den NULL-Fall selbst: city <> 'Berlin' OR city IS NULL.

  3. Hinweis 3

    Oder Sie nehmen den Vergleich, der NULL wie einen Wert behandelt: city IS DISTINCT FROM 'Berlin'.

Eine Lösung zeigen

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

SELECT name, city
FROM customers
WHERE city IS DISTINCT FROM 'Berlin'
ORDER BY 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

SELECT name, city
FROM customers
WHERE city <> 'Berlin'
ORDER BY name;

test.sql

-- test: Ben (Munich) und Cleo (Ort unbekannt) erscheinen, nach Namen sortiert
-- output:
--  name |  city
-- ------+--------
--  Ben  | Munich
--  Cleo |
-- (2 rows)

setup.sql

CREATE TABLE customers (
  id integer PRIMARY KEY,
  name text,
  city text
);
INSERT INTO customers VALUES
  (1, 'Ana', 'Berlin'),
  (2, 'Ben', 'Munich'),
  (3, 'Cleo', NULL),
  (4, 'Dev', 'Berlin');

CREATE TABLE orders (
  id integer PRIMARY KEY,
  customer_id integer REFERENCES customers,
  amount integer,
  status text
);
INSERT INTO orders VALUES
  (101, 1, 50, 'paid'),
  (102, 1, 30, 'refunded'),
  (103, 2, 20, 'paid');

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 3 von 3

Kunden mit mindestens 50 Umsatz

Die Abfrage addiert die Beträge der Bestellungen jedes Kunden. Behalten Sie nur die Kunden, deren Summe mindestens 50 ist. Der Test betrifft Gruppen und kann daher nicht in WHERE stehen.

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

    WHERE läuft, bevor es die Gruppen gibt, und sieht daher kein SUM.

  2. Hinweis 2

    HAVING kommt nach GROUP BY und filtert ganze Gruppen.

  3. Hinweis 3

    Fügen Sie HAVING SUM(o.amount) >= 50 zwischen GROUP BY und ORDER BY ein. Ein Alias aus SELECT wie total ist dort nicht nutzbar.

Eine Lösung zeigen

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

SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
HAVING SUM(o.amount) >= 50
ORDER BY c.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

-- Keep only the customers whose total is at least 50.
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;

test.sql

-- test: Nur Ana erscheint, deren Bestellungen zusammen 80 ergeben
-- output:
--  name | total
-- ------+-------
--  Ana  |    80
-- (1 row)

setup.sql

CREATE TABLE customers (
  id integer PRIMARY KEY,
  name text,
  city text
);
INSERT INTO customers VALUES
  (1, 'Ana', 'Berlin'),
  (2, 'Ben', 'Munich'),
  (3, 'Cleo', NULL),
  (4, 'Dev', 'Berlin');

CREATE TABLE orders (
  id integer PRIMARY KEY,
  customer_id integer REFERENCES customers,
  amount integer,
  status text
);
INSERT INTO orders VALUES
  (101, 1, 50, 'paid'),
  (102, 1, 30, 'refunded'),
  (103, 2, 20, 'paid');

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 Spaltenname, den beide Tabellen haben

SELECT id, name
FROM customers c
JOIN orders o ON o.customer_id = c.id;

Was psql ausgibt

ERROR:  column reference "id" is ambiguous

Warum, und die Lösung

customers und orders haben beide eine Spalte id, und PostgreSQL rät nicht, welche Sie meinen. Stellen Sie jeder solchen Spalte ihre Tabelle oder ihren Alias voran: SELECT o.id, c.name. Viele Entwickler qualifizieren in einem Join jede Spalte, damit die Abfrage eindeutig bleibt, wenn eine Tabelle später eine Spalte dazubekommt.

Eine Spalte, die weder gruppiert noch aggregiert ist

SELECT c.name, c.city, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.name;

Was psql ausgibt

ERROR:  column "c.city" must appear in the GROUP BY clause or be used in an aggregate function

Warum, und die Lösung

Nach GROUP BY steht jede Ergebniszeile für eine ganze Gruppe, also muss jede Spalte in SELECT entweder in GROUP BY stehen oder in einem Aggregat wie COUNT. Nehmen Sie die Spalte in GROUP BY auf (GROUP BY c.name, c.city) oder gruppieren Sie nach dem Schlüssel c.id, der den Rest bestimmt.

Ein Aggregat in WHERE

SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE SUM(o.amount) >= 50
GROUP BY c.name;

Was psql ausgibt

ERROR:  aggregate functions are not allowed in WHERE

Warum, und die Lösung

WHERE wählt einzelne Zeilen, bevor eine Gruppe gebildet ist, also gibt es noch keine Summe. Ein Test auf die Summe einer Gruppe gehört in HAVING, nach GROUP BY: HAVING SUM(o.amount) >= 50.

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 linke Zeile, bis WHERE sie entfernt

Ein Inner Join liefert nur Zeilen, die zusammenpassen. Ein LEFT JOIN macht zuerst den Inner Join und fügt dann jede linke Zeile ohne Partner einmal hinzu, mit NULL in jeder Spalte der rechten Tabelle. Die PostgreSQL-Dokumentation sagt es deutlich: Eine Bedingung in ON wirkt vor dem Join, eine Bedingung in WHERE danach. Bei Inner Joins ist das gleich, bei Outer Joins nicht: WHERE o.status = 'paid' wirft genau die NULL-Zeilen weg, die der LEFT JOIN eben hinzugefügt hat. Filter auf die rechte Tabelle gehören in ON.

Gruppen: welche Zeilen hineingehen, welche Gruppen herauskommen

Die logische Reihenfolge ist FROM (mit Joins), WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. WHERE wählt die Eingabezeilen, bevor ein Aggregat berechnet ist, und kann daher weder COUNT noch SUM nutzen; HAVING filtert danach ganze Gruppen. COUNT(*) zählt Zeilen; COUNT(Spalte) zählt Zeilen, in denen diese Spalte nicht NULL ist. Nach einem LEFT JOIN ist ein Kunde ohne Partner immer noch eine Zeile, also meldet COUNT(*) 1, wo COUNT(o.id) richtig 0 meldet. Andere Aggregate wie SUM liefern für keine Zeilen NULL: Umschließen Sie sie mit COALESCE(…, 0).

NULL ist kein Wert, den man mit = vergleicht

NULL bedeutet „unbekannt“, daher ergibt city = NULL nicht true, sondern NULL, und WHERE behält nur Zeilen, für die die Bedingung true ist. Dieselbe Falle steckt in city <> 'Berlin': Eine Zeile, deren city NULL ist, besteht auch diesen Test nicht. Prüfen Sie auf NULL mit IS NULL und IS NOT NULL, und nehmen Sie IS DISTINCT FROM, wenn ein Vergleich NULL wie einen gewöhnlichen, vergleichbaren Wert behandeln soll.

Quellen

Zuletzt geprüft am 30. September 2026