Zum Inhalt springen
SQL Anfänger 35 min

Window Functions

Lerne Window Functions: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD und mehr - für fortgeschrittene Analysen und Rankings.

Aktualisiert:

Window Functions sind eines der maechtigsten Features in SQL. Sie ermoeglichen Berechnungen ueber eine Menge von Zeilen, ohne die Ergebnisse zu gruppieren. Das bedeutet: Du kannst Aggregationen durchfuehren UND gleichzeitig alle einzelnen Zeilen sehen.

Was sind Window Functions?

Vergleiche GROUP BY mit Window Functions:

-- GROUP BY: Fasst Zeilen zusammen (du siehst nur die Gruppen)
SELECT kategorie, AVG(preis) AS avg_preis
FROM produkte
GROUP BY kategorie;
kategorie   | avg_preis
------------+----------
Accessoires |     27.49
Kleidung    |     66.66
Schuhe      |     79.99
-- Window Function: Zeigt JEDE Zeile + den Durchschnitt der Kategorie
SELECT
    name,
    kategorie,
    preis,
    AVG(preis) OVER (PARTITION BY kategorie) AS avg_kategorie
FROM produkte;
name           | kategorie   | preis  | avg_kategorie
---------------+-------------+--------+--------------
Muetze Winter  | Accessoires |  14.99 |         27.49
Rucksack Urban | Accessoires |  39.99 |         27.49
T-Shirt Basic  | Kleidung    |  19.99 |         66.66
Jeans Classic  | Kleidung    |  49.99 |         66.66
Jacke Outdoor  | Kleidung    | 129.99 |         66.66
Sneaker Sport  | Schuhe      |  79.99 |         79.99

Jede Zeile bleibt erhalten, aber du siehst trotzdem den Durchschnitt!

Die OVER-Klausel

Jede Window Function braucht eine OVER-Klausel:

funktion() OVER (
    PARTITION BY spalte    -- Optional: Gruppierung
    ORDER BY spalte        -- Optional: Sortierung innerhalb der Gruppe
)

PARTITION BY

PARTITION BY teilt die Daten in Gruppen (aehnlich wie GROUP BY, aber ohne Zusammenfassung):

SELECT
    name,
    kategorie,
    preis,
    SUM(preis) OVER (PARTITION BY kategorie) AS summe_kategorie,
    COUNT(*) OVER (PARTITION BY kategorie) AS anzahl_kategorie
FROM produkte;

ORDER BY in OVER

ORDER BY innerhalb von OVER definiert die Reihenfolge fuer laufende Berechnungen:

-- Laufende Summe der Bestellbetraege nach Datum
SELECT
    id,
    bestelldatum,
    betrag,
    SUM(betrag) OVER (ORDER BY bestelldatum) AS laufende_summe
FROM bestellungen;

Ergebnis:

id | bestelldatum | betrag | laufende_summe
---+--------------+--------+---------------
 1 | 2026-01-15   |  69.98 |          69.98
 2 | 2026-02-20   | 129.99 |         199.97
 3 | 2026-03-01   |  94.98 |         294.95
 4 | 2026-03-05   |  19.99 |         314.94
 5 | 2026-03-08   | 184.97 |         499.91

Ranking-Funktionen

ROW_NUMBER() - Fortlaufende Nummer

-- Produkte innerhalb jeder Kategorie nummerieren (nach Preis)
SELECT
    name,
    kategorie,
    preis,
    ROW_NUMBER() OVER (PARTITION BY kategorie ORDER BY preis) AS rang
FROM produkte;

Ergebnis:

name           | kategorie   | preis  | rang
---------------+-------------+--------+-----
Muetze Winter  | Accessoires |  14.99 |    1
Rucksack Urban | Accessoires |  39.99 |    2
T-Shirt Basic  | Kleidung    |  19.99 |    1
Jeans Classic  | Kleidung    |  49.99 |    2
Jacke Outdoor  | Kleidung    | 129.99 |    3
Sneaker Sport  | Schuhe      |  79.99 |    1

RANK() - Rang mit Luecken

-- Bei gleichen Werten: Gleicher Rang, naechster Rang wird uebersprungen
SELECT
    name,
    preis,
    RANK() OVER (ORDER BY preis DESC) AS rang
FROM produkte;

DENSE_RANK() - Rang ohne Luecken

-- Bei gleichen Werten: Gleicher Rang, naechster Rang folgt direkt
SELECT
    name,
    preis,
    DENSE_RANK() OVER (ORDER BY preis DESC) AS rang
FROM produkte;

Vergleich der Ranking-Funktionen

Angenommen, drei Produkte haben den gleichen Preis:

PreisROW_NUMBERRANKDENSE_RANK
100111
80222
80322
50443
  • ROW_NUMBER: Immer eindeutig (2, 3 bei gleichen Werten)
  • RANK: Gleiche Werte bekommen gleichen Rang, naechster wird uebersprungen (2, 2, 4)
  • DENSE_RANK: Gleiche Werte bekommen gleichen Rang, naechster folgt direkt (2, 2, 3)

LAG() - Vorherige Zeile

-- Bestellung mit dem Betrag der vorherigen Bestellung vergleichen
SELECT
    id,
    bestelldatum,
    betrag,
    LAG(betrag) OVER (ORDER BY bestelldatum) AS vorherige_bestellung,
    betrag - LAG(betrag) OVER (ORDER BY bestelldatum) AS differenz
FROM bestellungen;

Ergebnis:

id | bestelldatum | betrag | vorherige_bestellung | differenz
---+--------------+--------+----------------------+----------
 1 | 2026-01-15   |  69.98 |                 NULL |      NULL
 2 | 2026-02-20   | 129.99 |                69.98 |     60.01
 3 | 2026-03-01   |  94.98 |               129.99 |    -35.01
 4 | 2026-03-05   |  19.99 |                94.98 |    -74.99
 5 | 2026-03-08   | 184.97 |                19.99 |    164.98

LEAD() - Naechste Zeile

-- Naechste Bestellung anzeigen
SELECT
    id,
    bestelldatum,
    betrag,
    LEAD(betrag) OVER (ORDER BY bestelldatum) AS naechste_bestellung
FROM bestellungen;

FIRST_VALUE() und LAST_VALUE()

-- Guenstigstes Produkt pro Kategorie anzeigen
SELECT
    name,
    kategorie,
    preis,
    FIRST_VALUE(name) OVER (
        PARTITION BY kategorie ORDER BY preis
    ) AS guenstigstes_in_kategorie
FROM produkte;

Aggregat-Window-Functions

Alle Standard-Aggregatfunktionen koennen als Window Functions verwendet werden:

SELECT
    name,
    kategorie,
    preis,
    -- Aggregationen pro Kategorie (ohne GROUP BY!)
    SUM(preis) OVER (PARTITION BY kategorie) AS summe_kat,
    AVG(preis) OVER (PARTITION BY kategorie) AS avg_kat,
    MIN(preis) OVER (PARTITION BY kategorie) AS min_kat,
    MAX(preis) OVER (PARTITION BY kategorie) AS max_kat,
    COUNT(*) OVER (PARTITION BY kategorie) AS anzahl_kat
FROM produkte;

Laufende Berechnungen

-- Laufende Summe und laufender Durchschnitt
SELECT
    bestelldatum,
    betrag,
    SUM(betrag) OVER (ORDER BY bestelldatum) AS laufende_summe,
    ROUND(AVG(betrag) OVER (ORDER BY bestelldatum), 2) AS laufender_avg,
    COUNT(*) OVER (ORDER BY bestelldatum) AS bestellung_nr
FROM bestellungen;

NTILE() - In Gruppen aufteilen

NTILE teilt die Ergebnisse in N gleich grosse Gruppen:

-- Produkte in 3 Preisgruppen teilen
SELECT
    name,
    preis,
    NTILE(3) OVER (ORDER BY preis) AS preisgruppe
FROM produkte;

Ergebnis:

name           | preis  | preisgruppe
---------------+--------+------------
Muetze Winter  |  14.99 |           1
T-Shirt Basic  |  19.99 |           1
Rucksack Urban |  39.99 |           2
Jeans Classic  |  49.99 |           2
Sneaker Sport  |  79.99 |           3
Jacke Outdoor  | 129.99 |           3

PERCENT_RANK() und CUME_DIST()

-- Prozentuale Position im Ranking
SELECT
    name,
    preis,
    ROUND(PERCENT_RANK() OVER (ORDER BY preis)::NUMERIC, 2) AS pct_rang,
    ROUND(CUME_DIST() OVER (ORDER BY preis)::NUMERIC, 2) AS kum_verteilung
FROM produkte;

Praxisbeispiele

Top-N pro Kategorie

-- Die 2 teuersten Produkte jeder Kategorie
SELECT name, kategorie, preis
FROM (
    SELECT
        name,
        kategorie,
        preis,
        ROW_NUMBER() OVER (PARTITION BY kategorie ORDER BY preis DESC) AS rang
    FROM produkte
) AS ranked
WHERE rang <= 2;

Kunden-Ranking nach Umsatz

SELECT
    k.name,
    SUM(b.betrag) AS umsatz,
    RANK() OVER (ORDER BY SUM(b.betrag) DESC) AS umsatz_rang
FROM kunden k
JOIN bestellungen b ON k.id = b.kunden_id
GROUP BY k.name;

Monats-ueber-Monat Vergleich

SELECT
    monat,
    umsatz,
    LAG(umsatz) OVER (ORDER BY monat) AS vormonat_umsatz,
    ROUND(
        (umsatz - LAG(umsatz) OVER (ORDER BY monat)) /
        LAG(umsatz) OVER (ORDER BY monat) * 100
    , 1) AS veraenderung_pct
FROM (
    SELECT
        DATE_TRUNC('month', bestelldatum) AS monat,
        SUM(betrag) AS umsatz
    FROM bestellungen
    GROUP BY DATE_TRUNC('month', bestelldatum)
) AS monatsumsatz;

Was kommt als Naechstes?

Im naechsten Tutorial wendest du alles Gelernte in einem praxisnahen Projekt an: Du baust eine komplette Bibliotheks-Datenbank.

Zusammenfassung

  • Window Functions berechnen ueber eine Menge von Zeilen, ohne sie zusammenzufassen
  • OVER() definiert das Fenster (PARTITION BY + ORDER BY)
  • ROW_NUMBER, RANK, DENSE_RANK erstellen Rankings
  • LAG, LEAD greifen auf vorherige/naechste Zeilen zu
  • SUM, AVG, COUNT ueber OVER fuer laufende Berechnungen
  • NTILE teilt Daten in gleich grosse Gruppen
  • Window Functions sind ideal fuer Rankings, Vergleiche und Trendanalysen

Uebungen

  1. ROW_NUMBER: Nummeriere alle Bestellungen jedes Kunden nach Datum.
  2. RANK: Erstelle ein Produkt-Ranking nach Preis innerhalb jeder Kategorie.
  3. LAG: Vergleiche den Betrag jeder Bestellung mit der vorherigen desselben Kunden.
  4. Laufende Summe: Berechne die laufende Umsatzsumme nach Datum.
  5. Top-N: Finde das meistverkaufte Produkt jeder Kategorie (nutze ROW_NUMBER und eine Subquery).
-- Loesung zu Uebung 1:
SELECT
    k.name,
    b.bestelldatum,
    b.betrag,
    ROW_NUMBER() OVER (PARTITION BY k.id ORDER BY b.bestelldatum) AS bestell_nr
FROM bestellungen b
JOIN kunden k ON b.kunden_id = k.id;

-- Loesung zu Uebung 4:
SELECT
    bestelldatum,
    betrag,
    SUM(betrag) OVER (ORDER BY bestelldatum) AS laufende_summe
FROM bestellungen;

Pro-Tipp: Window Functions werden in SQLite ab Version 3.25.0 unterstuetzt. In PostgreSQL sind sie seit vielen Versionen verfuegbar. Wenn du Window Functions beherrschst, hast du ein Werkzeug, das die meisten SQL-Anwender nicht kennen - das macht dich in Data Analytics und Reporting besonders wertvoll.

Zurück zum SQL Kurs