Inhaltsverzeichnis24 Abschnitte
- TL;DR
- Was sind SQL Window Functions?
- Anatomie eines Window-Function-Aufrufs
- Die drei Familien der Window Functions
- 1. Ranking-Funktionen
- 2. Aggregate Window Functions
- 3. Value-Funktionen: LAG, LEAD, FIRSTVALUE, LASTVALUE
- Mehrere Window Functions kombinieren
- Häufige Fehler und Fallen
- 1. Default-Frame vergessen
- 2. Window Functions in WHERE
- 3. Nicht-deterministisches ROWNUMBER
- 4. Performance: Fehlende Indizes
- 5. LASTVALUE liefert aktuelle Zeile
- Window Functions über SQL-Engines hinweg
- Wann Window Functions nicht das richtige Tool sind
- Quick-Reference-Cheatsheet
- Nächste Schritte
- FAQ
- Was ist der Unterschied zwischen RANK und DENSERANK?
- Kann ich Window Functions in dbt-Models nutzen?
- Wann nehme ich GROUP BY statt einer Window Function?
- Warum liefert LASTVALUE manchmal den aktuellen Wert statt den letzten?
- Weiterlesen
SQL Window Functions Tutorial: Rank, Aggregate, Vergleich
Jede:r Analyst:in läuft gegen dieselbe Wand: Du brauchst eine laufende Summe, eine Zeilennummer oder einen Vergleich zur Vorzeile — und GROUP BY kollabiert das Detail, das du brauchst. SQL Window Functions lösen das ohne Subqueries oder Self-Joins. Hast du sie einmal gelernt, fragst du dich, wie du je ohne sie überlebt hast.
Dieses Tutorial deckt die nützlichsten Window Functions mit ausführbaren Beispielen in PostgreSQL-Syntax ab (das meiste funktioniert identisch in Spark SQL, BigQuery Standard SQL und Snowflake SQL). Am Ende behandelst du Rankings, laufende Aggregate und Zeile-zu-Zeile-Vergleiche souverän.
TL;DR
- Window Function: Berechnung über eine Zeilenmenge, ohne sie zu kollabieren
- Drei Familien: Ranking (RANK, DENSE_RANK, ROW_NUMBER), Aggregate (SUM, AVG OVER), Value (LAG, LEAD, FIRST_VALUE)
OVER(...)definiert das Fenster —PARTITION BYundORDER BYals HauptbausteineQUALIFYin BigQuery und Snowflake ist die saubere Alternative zu CTE-Filterung- Häufigste Falle:
LAST_VALUEohne expliziten Frame liefert die aktuelle Zeile
Was sind SQL Window Functions?
Eine Window Function rechnet über eine Menge von Zeilen, die mit der aktuellen Zeile in Beziehung stehen — ohne sie wie GROUP BY zu einer einzelnen Output-Zeile zu kollabieren. Die Schlüssel-Klausel ist OVER(), die das "Fenster" definiert, das jede Zeile sieht.
| Konzept | GROUP BY | Window Function |
|---|---|---|
| Zeilen zurück | Eine pro Gruppe | Jede Originalzeile |
| Aggregation | Kollabiert Zeilen | Fügt Spalte neben Daten hinzu |
| Use-Case | Summary-Reports | Rankings, laufende Summen, Vergleiche |
| Syntax-Marker | GROUP BY col | function() OVER(...) |
Anatomie eines Window-Function-Aufrufs
-- PostgreSQL
SELECT
employee_id,
department,
salary,
RANK() OVER (
PARTITION BY department -- defines the group (like GROUP BY, but no collapse)
ORDER BY salary DESC -- defines the order within the partition
) AS dept_salary_rank
FROM employees;
- PARTITION BY teilt Zeilen in Gruppen (optional — weglassen, und das Fenster ist das ganze Resultset)
- ORDER BY bestimmt die Sequenz innerhalb jeder Partition
- Frame-Klausel (optional) schränkt weiter ein, welche Zeilen innerhalb der Partition die Function sieht
Die drei Familien der Window Functions
1. Ranking-Funktionen
Ranking-Functions weisen jeder Zeile eine Position innerhalb ihrer Partition zu.
| Funktion | Bei Gleichstand | Lücken nach Ties | Beispiel (Werte 100, 100, 90) |
|---|---|---|---|
ROW_NUMBER() | Bricht arbiträr | Keine Lücken | 1, 2, 3 |
RANK() | Gleicher Rang | Lücken nach Ties | 1, 1, 3 |
DENSE_RANK() | Gleicher Rang | Keine Lücken | 1, 1, 2 |
NTILE(n) | Verteilt auf n Buckets | N/A | Abhängig von n |
Praxisbeispiel: Top 3 Vertriebler:innen pro Region
-- PostgreSQL
WITH ranked AS (
SELECT
salesperson_name,
region,
total_revenue,
DENSE_RANK() OVER (
PARTITION BY region
ORDER BY total_revenue DESC
) AS revenue_rank
FROM sales_summary
)
SELECT *
FROM ranked
WHERE revenue_rank <= 3
ORDER BY region, revenue_rank;
Warum hier DENSE_RANK? Wenn zwei Personen auf Platz 2 gleichstehen, willst du Platz 3 trotzdem sehen. RANK() würde nach einem Tie auf 4 springen und potenziell weniger als 3 Personen pro Region zurückgeben.
2. Aggregate Window Functions
Jedes Standard-Aggregat — SUM, AVG, COUNT, MIN, MAX — kann mit OVER() als Window Function genutzt werden.
Laufende Summe
-- PostgreSQL
SELECT
order_date,
order_amount,
SUM(order_amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM orders
ORDER BY order_date;
Gleitender Durchschnitt (7-Tage)
-- PostgreSQL
SELECT
metric_date,
daily_value,
AVG(daily_value) OVER (
ORDER BY metric_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM daily_metrics
ORDER BY metric_date;
Frame-Klauseln zählen. ROWS BETWEEN steuert exakt, welche Zeilen ins Aggregat fließen. Häufige Frames:
| Frame | Bedeutung |
|---|---|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | Alle Zeilen von Start bis Aktuell (laufende Summe) |
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | Aktuelle Zeile + 6 davor (7-Zeilen-Fenster) |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | Aktuelle Zeile bis Ende |
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW | Datums-basierter Bereich (PostgreSQL) |
ROWSvsRANGE:ROWSzählt physische Zeilen.RANGEnutzt logische Werte — nützlich für Datums-Fenster, aber nicht einheitlich über Engines unterstützt. Spark SQL unterstütztRANGEmit Numerik/Datum, BigQuery Standard SQL unterstützt es für Datums-Expressions. Im Zweifel:ROWS.
3. Value-Funktionen: LAG, LEAD, FIRST_VALUE, LAST_VALUE
Diese erreichen Werte aus anderen Zeilen ohne Self-Join.
| Funktion | Liefert |
|---|---|
LAG(col, n, default) | Wert n Zeilen davor |
LEAD(col, n, default) | Wert n Zeilen danach |
FIRST_VALUE(col) | Erster Wert im Window-Frame |
LAST_VALUE(col) | Letzter Wert im Window-Frame |
NTH_VALUE(col, n) | n-ter Wert im Window-Frame |
Monatswachstumsrate (Month-over-Month)
-- PostgreSQL
SELECT
report_month,
revenue,
LAG(revenue, 1) OVER (ORDER BY report_month) AS prev_month_revenue,
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY report_month))::numeric
/ NULLIF(LAG(revenue, 1) OVER (ORDER BY report_month), 0)
* 100, 1
) AS mom_growth_pct
FROM monthly_revenue
ORDER BY report_month;
Gib immer einen Default für LAG/LEAD an, wenn NULLs Probleme nach unten verursachen könnten:
-- PostgreSQL
LAG(revenue, 1, 0) OVER (ORDER BY report_month)
Erstes Kaufdatum pro Kunde
-- PostgreSQL
SELECT DISTINCT
customer_id,
FIRST_VALUE(order_date) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS first_purchase_date
FROM orders;
Aufpassen bei LAST_VALUE: Der Default-Frame ist ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, also liefert LAST_VALUE die aktuelle Zeile, wenn du nicht explizit den Frame auf ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING setzt.
Mehrere Window Functions kombinieren
Du kannst mehrere Window Functions in derselben Query nutzen. Wiederholst du dieselbe OVER()-Klausel, nutz ein Named Window, um DRY zu bleiben:
-- PostgreSQL
SELECT
employee_id,
department,
salary,
RANK() OVER dept_window AS salary_rank,
AVG(salary) OVER dept_window AS dept_avg_salary,
salary - AVG(salary) OVER dept_window AS diff_from_avg
FROM employees
WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)
ORDER BY department, salary_rank;
Engine-Support für Named Windows: PostgreSQL unterstützt
WINDOW-Klauseln nativ. Spark SQL 3.x auch. BigQuery Standard SQL unterstützt Named Windows nicht — du musst dieOVER()-Klausel wiederholen.
Häufige Fehler und Fallen
1. Default-Frame vergessen
Wenn ORDER BY innerhalb OVER() vorhanden ist, ist der Default-Frame RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — nicht die gesamte Partition. Das überrascht bei SUM() und AVG():
-- PostgreSQL — This is a running total, NOT a partition total
SELECT SUM(amount) OVER (PARTITION BY dept ORDER BY hire_date) FROM employees;
-- This IS a partition total (no ORDER BY = entire partition as frame)
SELECT SUM(amount) OVER (PARTITION BY dept) FROM employees;
2. Window Functions in WHERE
Window Functions laufen nach WHERE. Du kannst nicht direkt darauf filtern:
-- This fails
SELECT * FROM employees WHERE RANK() OVER (ORDER BY salary DESC) <= 5;
-- Use a CTE or subquery
WITH ranked AS (
SELECT *, RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
)
SELECT * FROM ranked WHERE rnk <= 5;
3. Nicht-deterministisches ROW_NUMBER
Wenn dein ORDER BY Zeilen nicht eindeutig identifiziert, weist ROW_NUMBER() Positionen unter Ties arbiträr zu — und die Reihenfolge kann sich zwischen Ausführungen ändern:
-- Non-deterministic: ties on salary get arbitrary row numbers
ROW_NUMBER() OVER (ORDER BY salary DESC)
-- Add a tiebreaker
ROW_NUMBER() OVER (ORDER BY salary DESC, employee_id ASC)
4. Performance: Fehlende Indizes
Window Functions mit ORDER BY profitieren von Indizes auf den Sortierspalten, besonders bei großen Tabellen. PARTITION BY + ORDER BY-Kombinationen funktionieren am besten mit einem Composite-Index, der beide abdeckt.
5. LAST_VALUE liefert aktuelle Zeile
Wie oben — setz immer den Frame explizit bei LAST_VALUE:
-- Correct way to get the actual last value in the partition
LAST_VALUE(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)
Window Functions über SQL-Engines hinweg
| Feature | PostgreSQL | Spark SQL | BigQuery Standard SQL | Snowflake SQL |
|---|---|---|---|---|
Named Windows (WINDOW) | Ja | Ja (3.x+) | Nein | Ja |
RANGE mit Datum | Ja | Ja (num/date) | Ja | Ja |
NTH_VALUE | Ja | Ja | Ja | Ja |
QUALIFY (Filter auf Window) | Nein | Nein | Ja | Ja |
NTILE | Ja | Ja | Ja | Ja |
BigQuery und Snowflake bieten QUALIFY als saubere Alternative zur CTE-Filterung:
-- BigQuery Standard SQL / Snowflake SQL
SELECT *
FROM sales_summary
QUALIFY DENSE_RANK() OVER (PARTITION BY region ORDER BY total_revenue DESC) <= 3;
Das eliminiert das CTE-Muster komplett — wissenswert, wenn du in diesen Engines arbeitest.
Wann Window Functions nicht das richtige Tool sind
- Einfache Aggregation ohne Zeilendetail — nutz einfach
GROUP BY. - Cross-Table-Lookups — das ist ein
JOIN, keine Window Function. - Rekursive Muster (z. B. Tree-Traversal) — rekursive CTEs.
- Sehr große Partitionen mit komplexen Frames — Memory-intensiv. Bei Millionen Zeilen pro Partition mit
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGlohnt es sich, einen Pre-Aggregations-Ansatz zu erwägen.
Quick-Reference-Cheatsheet
| Aufgabe | Funktion | Wichtiges Detail |
|---|---|---|
| Ranking mit Lücken | RANK() | Ties teilen Rang, nächster wird übersprungen |
| Ranking ohne Lücken | DENSE_RANK() | Ties teilen Rang, nächster ist +1 |
| Eindeutige Zeilennummer | ROW_NUMBER() | Tiebreaker in ORDER BY ergänzen |
| Laufende Summe | SUM() OVER (ORDER BY ...) | Default-Frame passt |
| Gleitender Durchschnitt | AVG() OVER (ROWS BETWEEN n PRECEDING AND CURRENT ROW) | Frame explizit setzen |
| Wert der Vorzeile | LAG(col, 1) | Default angeben, um NULL zu vermeiden |
| Wert der Nachzeile | LEAD(col, 1) | Default angeben, um NULL zu vermeiden |
| In Buckets aufteilen | NTILE(n) | Gleichmäßige Verteilung auf n Gruppen |
Nächste Schritte
Bist du mit Window Functions vertraut, erkunde diese Muster:
- Sessionization — nutz
LAG, um Lücken in Event-Timestamps zu erkennen und Session-IDs zuzuweisen - Deduplication —
ROW_NUMBER()nach Natural Key partitioniert, gefiltert aufrnk = 1 - Kumulative Verteilung —
CUME_DIST()undPERCENT_RANK()für Percentile-Berechnungen - Gap-and-Island-Probleme —
ROW_NUMBERmit Datumsarithmetik kombinieren, um aufeinanderfolgende Sequenzen zu finden
Arbeitest du mit Daten aus APIs oder CSVs und willst Window-Function-Queries direkt im Browser laufen lassen ohne Datenbank-Setup, lässt Harbinger Explorer dich Datenquellen laden und mit DuckDB-SQL abfragen — inklusive vollem Window-Function-Support — direkt aus einem Browser-Tab.
FAQ
Was ist der Unterschied zwischen RANK und DENSE_RANK?
RANK lässt Lücken nach Ties (1, 1, 3, 4), DENSE_RANK nicht (1, 1, 2, 3). Wähle DENSE_RANK, wenn du "Top N" auch bei Gleichstand zuverlässig haben willst.
Kann ich Window Functions in dbt-Models nutzen?
Ja, problemlos. Window Functions kompilieren zu Standard-SQL, und dbt führt das gegen dein Warehouse aus. Achte nur darauf, dass dein Ziel-Warehouse die benutzten Features unterstützt (z. B. QUALIFY in BigQuery/Snowflake, nicht in PostgreSQL).
Wann nehme ich GROUP BY statt einer Window Function?
Wenn du tatsächlich nur eine aggregierte Zeile pro Gruppe willst. Brauchst du Detailzeilen plus eine Aggregation (z. B. "Pro Bestellung den Anteil am Tagesumsatz"), nimm Window Functions.
Warum liefert LAST_VALUE manchmal den aktuellen Wert statt den letzten?
Weil der Default-Frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ist. Setz explizit ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, damit das Fenster die ganze Partition umfasst.
Weiterlesen
Stand: 15. Mai 2026.
Geschrieben von
Harbinger Team
Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastrukturkritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.
Hat dir das geholfen?
Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.
Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.