Zeilen zusammenfassen: GROUP BY

Aggregation fasst viele Zeilen zu Anzahlen, Summen oder Durchschnitten zusammen. So erhalten Sie Ergebnisse je Status, Kunde oder Kategorie statt je Datensatz.

Eine Zahl je Gruppe

GROUP BY sammelt Zeilen mit demselben Wert, und die Aggregatfunktionen reduzieren jeden Stapel auf eine einzelne Zahl.

1

SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
In Planche ausführen

Ergebnis — erste 5 von 7 Zeilen

statusorders
delivered1351
paid695
shipped641
pending349
refunded327

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

Sieben Status, sieben Zeilen. Jede Spalte der SELECT-Liste ist entweder etwas, wonach Sie gruppiert haben, oder etwas, das Sie aggregiert haben – das ist die Regel, die das Ergebnis wohldefiniert macht. Fragen Sie hier nach order_number, wird die Frage sinnlos: Welche der 1.351 zugestellten Bestellnummern sollte es sein?

COUNT(*) ist nicht COUNT(spalte)

2

SELECT COUNT(*) AS customers,
       COUNT(phone) AS with_phone,
       COUNT(*) - COUNT(phone) AS without_phone
FROM customers;
In Planche ausführen

Ergebnis — 1 Zeile

customerswith_phonewithout_phone
15001221279

COUNT(*) zählt Zeilen. COUNT(phone) zählt Zeilen, in denen phone nicht null ist. Der Unterschied sind die 279 Kunden aus der Filter-Lektion, und das ist ein wirklich nützlicher Trick: Die Differenz der beiden Zählungen ergibt ohne zweite Abfrage, wie viel fehlt.

Jedes andere Aggregat überspringt NULLs ebenfalls, was am meisten bei AVG zählt. Der Durchschnitt von 10, 20 und NULL ist 15, nicht 10 – die NULL wird nicht als Null behandelt, sie wird gar nicht mitgezählt. Wenn Sie sie doch als Null wollen, sagen Sie es mit COALESCE(spalte, 0).

Mehrere Aggregate auf einmal

3

SELECT cat.name AS category,
       COUNT(*) AS products,
       ROUND(MIN(p.price), 2) AS cheapest,
       ROUND(AVG(p.price), 2) AS average,
       ROUND(MAX(p.price), 2) AS dearest
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
ORDER BY average DESC
LIMIT 20;
In Planche ausführen

Ergebnis — erste 5 von 20 Zeilen

categoryproductscheapestaveragedearest
Wearables11417.31672.292433.73
Programming106.891585.672420.02
Tablets17314.761529.452412.93
Audio11593.81482.892334.28
Kitchen12670.541443.212293.8

Ein Durchgang durch die Tabelle beantwortet fünf Fragen je Kategorie. Dasselbe im Anwendungscode hieße, alle 400 Produktzeilen zu übertragen und darüber zu iterieren; die Datenbank rechnet die fünf Zahlen dort aus, wo die Zeilen ohnehin liegen, und schickt 20 zurück.

Über einen Join hinweg gruppieren

Aggregate und Joins lassen sich kombinieren. Erst joinen, dann die verbundenen Zeilen gruppieren.

4

SELECT cat.name AS category,
       COUNT(DISTINCT o.id) AS orders,
       SUM(oi.quantity) AS units,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
WHERE o.status IN ('paid', 'shipped', 'delivered')
GROUP BY cat.id
ORDER BY revenue DESC;
In Planche ausführen

Ergebnis — erste 5 von 27 Zeilen

categoryordersunitsrevenue
Watches395815933900.92
Board Games395801929534.55
Laptops366768900778.09
Science340709888542.78
Cycling405823858478.88

WHERE davor, HAVING danach

Beide filtern. Sie filtern zu unterschiedlichen Zeitpunkten, und das ist der einzige Unterschied.

5

SELECT cat.name AS category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
In Planche ausführen

Ergebnis — erste 5 von 8 Zeilen

categoryrevenue
Board Games1377454.99
Tablets1348572.94
Science1347046.09
Watches1325717.58
Cycling1288143.46

WHERE läuft vor dem Gruppieren und entscheidet, welche Zeilen teilnehmen. HAVING läuft danach und entscheidet, welche Gruppen übrig bleiben. Eine Bedingung auf eine rohe Spalte gehört also in WHERE, eine Bedingung auf ein Aggregat kann nur in HAVING stehen – während WHERE läuft, existiert das Aggregat noch nicht.

Legen Sie die Klauselreihenfolge aus der SELECT-Lektion daneben, und die Regel erklärt sich von selbst:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY  ->  LIMIT

Nach etwas gruppieren, das Sie berechnen

Sie können nach einem Ausdruck gruppieren, nicht nur nach einer Spalte. Die ersten sieben Zeichen eines Zeitstempels ergeben 2025-01 – einen Monat.

6

SELECT substr(ordered_at, 1, 7) AS month,
       COUNT(*) AS orders,
       ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
  AND ordered_at >= '2025-01-01'
GROUP BY month
ORDER BY month;
In Planche ausführen

Ergebnis — erste 5 von 12 Zeilen

monthordersrevenue
2025-0152311707.73
2025-0253350775.62
2025-0358351184.96
2025-0452374465.94
2025-0553381541.09

Nach einem abgeleiteten Wert zu gruppieren ist die Art, wie praktisch jeder Bericht "pro Monat" oder "pro Woche" geschrieben wird. Beachten Sie, dass GROUP BY month den SELECT-Alias wiederverwendet, was jede Engine in GROUP BY und ORDER BY erlaubt – streng sind nur WHERE und HAVING.

Was Sie jetzt können

Sie können einen Bericht erzeugen. Was noch nicht geht, ist eine Zeile mit einer Zusammenfassung zu vergleichen – "Produkte über dem Durchschnittspreis", "die Top drei je Kategorie" –, denn dafür braucht es zwei Abfragen gleichzeitig. Die nächste Lektion behandelt das.

Selbst ausprobieren

  1. Wie viele Kunden gibt es je Land? Zeigen Sie nur Länder mit mindestens 100, das größte zuerst.

    Lösung anzeigen

    Lösung anzeigen

    SELECT country, COUNT(*) AS customers
    FROM customers
    GROUP BY country
    HAVING COUNT(*) >= 100
    ORDER BY customers DESC;
    In Planche ausführen

    Ergebnis — erste 5 von 12 Zeilen

    countrycustomers
    KR155
    AU132
    IT130
    ES126
    GB126

    GROUP BY erzeugt eine Zeile je Land, und die Bedingung gilt der Anzahl, gehört also in HAVING statt in WHERE. Der Ausdruck wird dort wiederholt statt über den Alias angesprochen, weil das auf jeder Engine funktioniert.

  2. Die 15 bestbewerteten Produkte, gezählt werden nur Produkte mit mindestens 8 Bewertungen.

    Lösung anzeigen

    Lösung anzeigen

    SELECT p.name,
           COUNT(r.id) AS reviews,
           ROUND(AVG(r.rating), 2) AS avg_rating
    FROM products p
    JOIN reviews r ON r.product_id = p.id
    GROUP BY p.id
    HAVING COUNT(r.id) >= 8
    ORDER BY avg_rating DESC
    LIMIT 15;
    In Planche ausführen

    Ergebnis — erste 5 von 15 Zeilen

    namereviewsavg_rating
    Pulse Yoga Mat84.63
    A Winter Letter (3rd ed.)94.56
    Terra Planter84.5
    Nimbus Cosmos84.5
    Ivory Toaster94.33

    Zwei Aggregate über derselben Gruppe: eines zählt die Bewertungen, das andere mittelt die Note. Der HAVING-Filter verhindert, dass eine einzelne Fünf-Sterne-Bewertung die Liste anführt.