SemaTrain Logo Ein Fachportal von SemaTrain

Aggregation: COUNT, SUM, AVG, GROUP BY, HAVING

Jetzt wird aus „Daten abfragen“ echte Analyse: Sie zählen, summieren und gruppieren – und lernen, wann WHERE vs. HAVING richtig ist.

Merksatz: WHERE filtert Zeilen vor der Gruppierung, HAVING filtert Gruppen nach der Aggregation.

SQL Grundlagen Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur SQL Grundlagen Schulung. Termine & Buchung laufen über SemaTrain.de.

Fokus: COUNT, SUM, AVG, GROUP BY, HAVING – mit Kurs-/Terminbeispielen.

Worum geht’s?

Lehr-/Lernziele

Merksatz: WHERE = Zeilenfilter, HAVING = Gruppenfilter.

COUNT: Wie viele Termine gibt es?

COUNT(*) – alle Zeilen zählen (SQL)
-- Anzahl aller Termine
SELECT COUNT(*) AS anzahl_termine
FROM termine;
COUNT(*) vs COUNT(spalte)
COUNT(spalte) ignoriert NULL (SQL)
-- COUNT(spalte) zählt NUR nicht-NULL Werte
SELECT
  COUNT(*)          AS alle_zeilen,
  COUNT(trainer_id) AS trainer_gesetzt
FROM termine;

Beispiel: Termine ohne Trainer haben trainer_id = NULL und werden bei COUNT(trainer_id) nicht gezählt.

GROUP BY: Termine pro Ort / Format

GROUP BY ort (SQL)
-- Termine pro Ort
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
ORDER BY termine DESC, ort ASC;
GROUP BY format (SQL)
-- Termine pro Format (Online/Präsenz)
SELECT
  format,
  COUNT(*) AS termine
FROM termine
GROUP BY format
ORDER BY termine DESC, format ASC;

SUM & AVG: Kennzahlen berechnen (konsistent zum Schema)

In eurem Lernpfad liegen Dauer und Preis in termine (dauer_tage, preis_eur). Damit können wir ohne Umwege summieren und mitteln.

AVG: Preis/Dauer pro Format (SQL)
-- Ø-Preis und Ø-Dauer pro Format
SELECT
  format,
  COUNT(*)          AS termine,
  AVG(preis_eur)    AS preis_avg,
  AVG(dauer_tage)   AS tage_avg
FROM termine
GROUP BY format
ORDER BY termine DESC, format ASC;
SUM: Umsatz pro Ort (SQL)
-- Umsatz (Summe Preis) pro Ort
SELECT
  ort,
  COUNT(*)       AS termine,
  SUM(preis_eur) AS umsatz_summe
FROM termine
GROUP BY ort
ORDER BY umsatz_summe DESC, ort ASC;

HAVING: Gruppen filtern

HAVING COUNT(*) >= 2 (SQL)
-- Nur Orte mit mindestens 2 Terminen
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
HAVING COUNT(*) >= 2
ORDER BY termine DESC, ort ASC;
Häufige Falle: Aggregat in WHERE
WHERE vs HAVING (SQL)
-- FALSCH: Aggregatfunktionen in WHERE sind nicht erlaubt
-- SELECT ort, COUNT(*) FROM termine WHERE COUNT(*) >= 2 GROUP BY ort;

-- RICHTIG: Gruppenfilter in HAVING
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
HAVING COUNT(*) >= 2;

Aggregation + JOIN: Termine & Umsatz pro Kurs

Typisches Reporting: Wie oft findet welcher Kurs statt – und was ist die Umsatzsumme aus Terminpreisen?

LEFT JOIN + GROUP BY: Termine pro Kurs (SQL)
-- Termine pro Kurs (inkl. Kurse ohne Termine)
SELECT
  k.kurs_id,
  k.kurs_name,
  COUNT(t.termin_id) AS termine
FROM kurse AS k
LEFT JOIN termine AS t
  ON t.kurs_id = k.kurs_id
GROUP BY k.kurs_id, k.kurs_name
ORDER BY termine DESC, k.kurs_name ASC;
JOIN + SUM/AVG: Umsatz/Ø-Preis pro Kurs (SQL)
-- Umsatzsumme pro Kurs (termin-preis_eur wird aggregiert)
-- Hinweis: SUM(NULL) ist NULL, daher COALESCE(...)
SELECT
  k.kurs_id,
  k.kurs_name,
  COUNT(t.termin_id)                 AS termine,
  COALESCE(SUM(t.preis_eur), 0)      AS umsatz_summe,
  COALESCE(AVG(t.preis_eur), 0)      AS preis_avg
FROM kurse AS k
LEFT JOIN termine AS t
  ON t.kurs_id = k.kurs_id
GROUP BY k.kurs_id, k.kurs_name
ORDER BY umsatz_summe DESC, k.kurs_name ASC;
Warum LEFT JOIN + COUNT(t.termin_id)?
  • LEFT JOIN zeigt auch Kurse ohne Termin.
  • COUNT(t.termin_id) zählt nur echte Termine (NULL nicht).
  • Für SUM/AVG nutzen wir COALESCE, damit „0“ statt NULL angezeigt wird.

Typische Stolperfallen

Praxisaufgabe

  1. Zähle Termine pro Ort (GROUP BY).
  2. Zeige nur Orte mit mindestens 2 Terminen (HAVING).
  3. Zähle Termine pro Kurs (JOIN + GROUP BY) – nutze LEFT JOIN, damit Kurse ohne Termine sichtbar bleiben.
  4. (Bonus) Berechne Umsatz pro Kurs als SUM(preis_eur).
Lösungsvorschlag anzeigen
Lösung: GROUP BY + HAVING + JOIN (+ Umsatz) (SQL)
-- 1+2) Orte mit mindestens 2 Terminen
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
HAVING COUNT(*) >= 2
ORDER BY termine DESC, ort ASC;

-- 3) Termine pro Kurs (inkl. 0)
SELECT
  k.kurs_id,
  k.kurs_name,
  COUNT(t.termin_id) AS termine
FROM kurse AS k
LEFT JOIN termine AS t
  ON t.kurs_id = k.kurs_id
GROUP BY k.kurs_id, k.kurs_name
ORDER BY termine DESC, k.kurs_name ASC;

-- Bonus) Umsatz pro Kurs
SELECT
  k.kurs_id,
  k.kurs_name,
  COALESCE(SUM(t.preis_eur), 0) AS umsatz_summe
FROM kurse AS k
LEFT JOIN termine AS t
  ON t.kurs_id = k.kurs_id
GROUP BY k.kurs_id, k.kurs_name
ORDER BY umsatz_summe DESC, k.kurs_name ASC;

Kurz-Takeaways

Quiz: Aggregation & GROUP BY

1. (LZ1) Was zählt COUNT(spalte)?

2. (LZ3) Wann nutzt man HAVING statt WHERE?

3. (LZ2) Welche Aussage ist korrekt?

4. (LZ4) Warum ist im Reporting "Termine pro Kurs" oft LEFT JOIN sinnvoll?

Praxisaufgabe

Mini-Projekt: KPI-Board (Aggregation für Reporting)

Sie bauen ein kleines „Dashboard“ aus SQL-Abfragen: Kennzahlen pro Ort, Format und Kurs. Fokus: COUNT/SUM/AVG, GROUP BY und der Unterschied WHERE vs HAVING.

Beitrag zu den Lehr-/Lernzielen: LZ1 (COUNT/SUM/AVG), LZ2 (GROUP BY-Regel), LZ3 (WHERE vs HAVING), LZ4 (JOIN + Aggregation).

Stufe A – Basis-KPIs (ohne JOIN)

Stufe B – WHERE vs HAVING

Stufe C – Reporting pro Kurs (JOIN + GROUP BY)

Bonus – Top-N & Vorfilter

Musterlösung (KPI-Queries)
Projektlösung: KPI-Board (Aggregation) (SQL)
-- ==========================================================
-- Stufe A1: Termine pro Ort
-- ==========================================================
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
ORDER BY termine DESC, ort ASC;

-- ==========================================================
-- Stufe A2: Termine pro Format (Online/Präsenz)
-- ==========================================================
SELECT
  format,
  COUNT(*)        AS termine,
  AVG(preis_eur)  AS preis_avg,
  AVG(dauer_tage) AS tage_avg
FROM termine
GROUP BY format
ORDER BY termine DESC, format ASC;

-- ==========================================================
-- Stufe B: HAVING (Gruppenfilter) – nur Orte mit >= 2 Terminen
-- ==========================================================
SELECT
  ort,
  COUNT(*) AS termine
FROM termine
GROUP BY ort
HAVING COUNT(*) >= 2
ORDER BY termine DESC, ort ASC;

-- ==========================================================
-- Stufe B (Vorfilter): WHERE vor GROUP BY (z. B. nur Online)
-- ==========================================================
SELECT
  ort,
  COUNT(*) AS online_termine
FROM termine
WHERE format = 'Online'
GROUP BY ort
ORDER BY online_termine DESC, ort ASC;

-- ==========================================================
-- Stufe C: Termine + Umsatz pro Kurs (JOIN + Aggregation)
-- ==========================================================
SELECT
  k.kurs_id,
  k.kurs_name,
  COUNT(t.termin_id)            AS termine,
  COALESCE(SUM(t.preis_eur), 0) AS umsatz_summe
FROM kurse AS k
LEFT JOIN termine AS t
  ON t.kurs_id = k.kurs_id
GROUP BY k.kurs_id, k.kurs_name
ORDER BY umsatz_summe DESC, k.kurs_name ASC;

-- ==========================================================
-- Bonus: Top-3 Orte für Online-Termine
-- ==========================================================
SELECT
  ort,
  COUNT(*) AS online_termine
FROM termine
WHERE format = 'Online'
GROUP BY ort
ORDER BY online_termine DESC, ort ASC
LIMIT 3;

Ausblick: Im nächsten Kapitel (DML) ändern Sie Daten (INSERT/UPDATE/DELETE) – und sehen danach, wie sich die KPIs durch neue Termine verändern.