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:

SQL Klassische Lösung — umständlich
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

SQL
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):

SQL
 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.90

Drei 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.

ElementWirkung
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:

SQL Pro Kunde Anzahl Bestellungen
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:

SQL
 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.95

Wichtig: 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:

SQL Running Total pro Kunde
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):

SQL
 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.95

Pro 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

AspektGROUP BYWindow Function
Zeilen-Anzahlreduziert auf eine pro Gruppeunverändert
Detail-Daten sichtbarneinja
Pro Zeile mehrere Aggregatenicht direkttrivial
Laufende Berechnungensehr umständlichnatürlich (mit ORDER BY)
Geschwindigkeitsehr schnell für reine Aggregateetwas 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:

SQL
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:

SQL Named Windows reduzieren Wiederholung
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

/ Weiter

Zurück zu Aggregation & Window Functions

Zur Übersicht