Window Functions sind eine der mächtigsten SQL-Erweiterungen — und für viele Reporting-Aufgaben unverzichtbar. Anders als GROUP BY reduzieren sie die Zeilenzahl nicht: jede Zeile bleibt erhalten, bekommt aber zusätzlich einen Wert berechnet, der über ein Window anderer Zeilen rechnet. Hier das Konzept mit Beispielen.
Das Problem ohne Window Functions
Aufgabe: pro Bestellung den Gesamtumsatz des Kunden anzeigen — also „Bestellung 1 von Alice ist 99.95, ihr Gesamtumsatz ist 169.85".
Mit klassischem GROUP BY geht das nicht — du kollabierst auf eine Zeile pro Kunde und verlierst die Einzel-Bestellungen. Mit Subquery oder JOIN aufwendig:
SELECT
o.id,
o.customer_id,
o.total,
(SELECT sum(total) FROM orders WHERE customer_id = o.customer_id) AS customer_total
FROM orders o;Eine korrelierte Subquery pro Zeile — funktioniert, ist aber tippfehler-anfällig und nicht immer effizient.
Mit Window Function
SELECT
o.id,
o.customer_id,
o.total,
sum(o.total) OVER (PARTITION BY o.customer_id) AS customer_total
FROM orders o;Output (Beispiel):
id | customer_id | total | customer_total
----+-------------+--------+----------------
1 | 1 | 99.95 | 169.85
2 | 1 | 49.95 | 169.85
3 | 1 | 19.95 | 169.85
4 | 2 | 199.95 | 199.95
5 | 3 | 9.95 | 39.90
6 | 3 | 29.95 | 39.90Drei Bestellungen pro Alice (customer_id = 1) — alle behalten ihre Einzel-Werte, plus dieselbe Customer-Summe 169.85 pro Zeile. Pro Kunde ist die Customer-Summe konstant — sie wird über das Window „alle Zeilen mit derselben customer_id" berechnet.
Die OVER-Klausel
agg(...) OVER (window_def) macht aus einem normalen Aggregat eine Window Function. window_def definiert, welche Zeilen das Aggregat sehen soll.
| Element | Wirkung |
|---|---|
OVER () | Window = alle Zeilen des Resultats |
OVER (PARTITION BY col) | Window = alle Zeilen mit gleichem col-Wert |
OVER (ORDER BY col) | Window = alle vorherigen Zeilen bis zur aktuellen (Default-Frame) |
OVER (PARTITION BY a ORDER BY b) | Pro Gruppe a, in Reihenfolge b, von Anfang bis aktuell |
PARTITION BY — Window pro Gruppe
PARTITION BY ist das Window-Äquivalent zu GROUP BY. Jede „Partition" ist eine Gruppe, die Window-Function rechnet pro Partition:
SELECT
id,
customer_id,
total,
count(*) OVER (PARTITION BY customer_id) AS orders_per_customer,
avg(total) OVER (PARTITION BY customer_id)::numeric(10,2) AS avg_per_customer
FROM orders;Output:
id | customer_id | total | orders_per_customer | avg_per_customer
----+-------------+--------+---------------------+------------------
1 | 1 | 99.95 | 3 | 56.62
2 | 1 | 49.95 | 3 | 56.62
3 | 1 | 19.95 | 3 | 56.62
4 | 2 | 199.95 | 1 | 199.95
5 | 3 | 9.95 | 2 | 19.95
6 | 3 | 29.95 | 2 | 19.95Wichtig: jede Zeile ist erhalten — keine Kollaps wie bei GROUP BY. Du siehst die Einzel-Bestellung und die Aggregate parallel.
ORDER BY in der Window-Klausel — laufende Aggregate
Mit ORDER BY im Window wird das Window gerichtet — pro Zeile werden nur die vorherigen Zeilen (plus aktuelle) im Aggregat berücksichtigt. Damit baust du laufende Summen:
SELECT
id,
customer_id,
created_at,
total,
sum(total) OVER (
PARTITION BY customer_id
ORDER BY created_at
) AS running_total
FROM orders;Output (mit drei Bestellungen für Kunde 1, sortiert nach Datum):
id | customer_id | created_at | total | running_total
----+-------------+------------------------+--------+---------------
1 | 1 | 2026-04-01 10:00:00+02 | 99.95 | 99.95
2 | 1 | 2026-04-15 14:00:00+02 | 49.95 | 149.90
3 | 1 | 2026-05-01 09:00:00+02 | 19.95 | 169.85
4 | 2 | 2026-04-10 11:00:00+02 | 199.95 | 199.95Pro Kunde startet die laufende Summe wieder bei Null — das ist die Wirkung von PARTITION BY. Innerhalb der Partition läuft die Summe nach Datum.
Klassischer Anwendungsfall: Kontostand über Zeit, kumulative Verkaufszahlen, Fortschritts-Bars.
Aggregate als Window vs. GROUP BY
| Aspekt | GROUP BY | Window Function |
|---|---|---|
| Zeilen-Anzahl | reduziert auf eine pro Gruppe | unverändert |
| Detail-Daten sichtbar | nein | ja |
| Pro Zeile mehrere Aggregate | nicht direkt | trivial |
| Laufende Berechnungen | sehr umständlich | natürlich (mit ORDER BY) |
| Geschwindigkeit | sehr schnell für reine Aggregate | etwas langsamer |
Wann was?
- Reine Zusammenfassung („eine Zeile pro Kunde mit Total") →
GROUP BY - Detail + Aggregat in einer Zeile → Window
- Laufende/Kumulative Werte → Window mit
ORDER BY - Ranglisten, Vergleich mit Vorgänger/Nachfolger → Window-Functions wie
ROW_NUMBER,LAG(eigene Artikel)
Mehrere Windows in einer Query
Du kannst mehrere Windows nebeneinander einsetzen — auch mit unterschiedlichen PARTITION BY/ORDER BY:
SELECT
id,
customer_id,
total,
sum(total) OVER () AS grand_total,
sum(total) OVER (PARTITION BY customer_id) AS customer_total,
count(*) OVER (PARTITION BY customer_id) AS customer_orders,
ROUND(
100.0 * total / sum(total) OVER (PARTITION BY customer_id),
1
) AS share_of_customer
FROM orders;Pro Zeile: Gesamtumsatz, Customer-Umsatz, Anzahl Bestellungen pro Kunde, Anteil dieser Bestellung am Customer-Umsatz. Alles in einer Query.
Named Windows mit WINDOW
Wenn du dieselbe Window-Definition mehrfach brauchst, kannst du sie benennen:
SELECT
id,
customer_id,
total,
sum(total) OVER w AS customer_total,
count(*) OVER w AS customer_orders,
avg(total) OVER w AS avg_order
FROM orders
WINDOW w AS (PARTITION BY customer_id);Praktisch in Reports mit vielen Window-Aggregaten — DRY-Prinzip auch in SQL.
Interessantes
OVER () ohne Klauseln = ganzes Resultat als Window.
count(*) OVER () zählt alle Zeilen des Resultats. Praktisch in Pagination: pro Zeile auch die Gesamtzahl ausgeben (statt eine separate count(*)-Query). Postgres berechnet das einmal — kein Performance-Drama.
ORDER BY im Window ändert das Default-Frame.
Ohne ORDER BY: Window = ganze Partition (alle Zeilen). Mit ORDER BY: Window = von Anfang bis zur aktuellen Zeile (laufendes Aggregat). Wer das nicht weiß, wundert sich, warum eine Summe plötzlich pro Zeile anders ist. Mehr im Frame-Artikel.
Window Functions sind nicht in WHERE nutzbar.
WHERE row_number() OVER (...) = 1 schlägt fehl — Window Functions werden nach WHERE ausgewertet. Workaround: Subquery oder CTE drumherum. WHERE filtert die Zeilen, die ins Window kommen — danach wird das Window berechnet, dann kann erst gefiltert werden (mit zweiter Query-Schicht).
Aggregate werden zu Window Functions durch OVER.
Jedes Aggregat (count, sum, avg, array_agg, string_agg, bool_or, …) kann als Window verwendet werden. Anders als spezielle Window-only-Funktionen (row_number, rank, lag) sind die Aggregate doppel-gesichtig — mit OVER Window, ohne OVER klassisch.
Performance: Hash-Aggregate vs. Window.
Window Functions brauchen meistens einen Sort, dann einen Streaming-Plan. GROUP BY kann oft Hash-Aggregate ohne Sort. Bei großen Datenmengen merklich. Wenn beide Lösungen funktionieren und Performance kritisch ist: GROUP BY bevorzugen.
Window Functions sind SQL-Standard.
SQL:2003. Funktionieren auf Postgres, Oracle, SQL Server, DB2, MariaDB 10.2+, MySQL 8+, SQLite 3.25+. Kompatibel über Plattformen — anders als manche andere Postgres-Spezialitäten.
Weiterführende Ressourcen
Externe Quellen
- Window Functions – PostgreSQL Documentation
- Window Function Calls – Syntax
- Window Functions – Built-in