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;
Ergebnis — erste 5 von 7 Zeilen
| status | orders |
|---|---|
| delivered | 1351 |
| paid | 695 |
| shipped | 641 |
| pending | 349 |
| refunded | 327 |
Ö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;
Ergebnis — 1 Zeile
| customers | with_phone | without_phone |
|---|---|---|
| 1500 | 1221 | 279 |
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;
Ergebnis — erste 5 von 20 Zeilen
| category | products | cheapest | average | dearest |
|---|---|---|---|---|
| Wearables | 11 | 417.3 | 1672.29 | 2433.73 |
| Programming | 10 | 6.89 | 1585.67 | 2420.02 |
| Tablets | 17 | 314.76 | 1529.45 | 2412.93 |
| Audio | 11 | 593.8 | 1482.89 | 2334.28 |
| Kitchen | 12 | 670.54 | 1443.21 | 2293.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;
Ergebnis — erste 5 von 27 Zeilen
| category | orders | units | revenue |
|---|---|---|---|
| Watches | 395 | 815 | 933900.92 |
| Board Games | 395 | 801 | 929534.55 |
| Laptops | 366 | 768 | 900778.09 |
| Science | 340 | 709 | 888542.78 |
| Cycling | 405 | 823 | 858478.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;
Ergebnis — erste 5 von 8 Zeilen
| category | revenue |
|---|---|
| Board Games | 1377454.99 |
| Tablets | 1348572.94 |
| Science | 1347046.09 |
| Watches | 1325717.58 |
| Cycling | 1288143.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;
Ergebnis — erste 5 von 12 Zeilen
| month | orders | revenue |
|---|---|---|
| 2025-01 | 52 | 311707.73 |
| 2025-02 | 53 | 350775.62 |
| 2025-03 | 58 | 351184.96 |
| 2025-04 | 52 | 374465.94 |
| 2025-05 | 53 | 381541.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.