Window Functions
Acht Kostbarkeiten in T-SQL, Teil 5
Window Functions (Fensterfunktionen) sind keine Modeerscheinung, sondern Fundament moderner SQL-Analyse. Seit SQL Server 2012 gehören sie offiziell zum Werkzeugkasten – doch viele Entwickler greifen immer noch zu Umwegen: Self-Joins, CTEs, Zwischentabellen. Zwar können das alles Lösungen sein und sie haben natürlich auch ihre Berechtigungen, aber Window Functions können die elegantere Lösung sein.
Dabei gilt: Wer Daten versteht, muss sie nicht mehr gruppieren – er öffnet ein Fenster. Window Functions erweitern T-SQL um die Fähigkeit, Zeilen im Kontext ihrer Nachbarn zu betrachten. Sie kombinieren Aggregation, Sortierung und Vergleich in einem Schritt, ohne die Detaildaten zu verlieren.
Das ist nicht nur eleganter, sondern auch performanter: SQL Server kann damit intern optimieren, statt Logik in der Anwendung zu simulieren.
AdventureWorks
Einige der Abfragen laufen in der OLTP-Datenbank AdentureWorks. Die Download-Dateien dieser Beispiel-Datenbank sind, inklusive Details zur Installation, bei Microsoft Learn zu finden.
Was ist also ein „Fenster“?
Ein Fenster ist der logische Rahmen, in dem eine Funktion ihre Berechnung durchführt. Definiert wird es mit der OVER()-Klausel. Jede Zeile bleibt erhalten, bekommt aber Zugriff auf andere Zeilen, die im Fenster liegen.
SELECT CustomerID, OrderDate, SUM(TotalDue) OVER( PARTITION BY CustomerID ORDER BY OrderDate ROWS UNBOUNDED PRECEDING ) AS RunningTotal FROM Sales.SalesOrderHeader;
Diese Abfrage summiert Bestellungen je Kunde in chronologischer Reihenfolge. Das Ergebnis enthält weiterhin jede Bestellung – ergänzt um den bisherigen Gesamtumsatz.
Wichtig: Fensterfunktionen reduzieren keine Zeilen wie GROUP BY. Im Gegenteil: Sie erweitern das Ergebnis um Kontext.
Die Bausteine eines Fensters
SQL Server erlaubt die in Tabelle 1 gezeigten vier verschiedenen Bausteine, die ein Fenster definieren.
| Baustein | Zweck | Beispiel |
|---|---|---|
| ORDER BY | Definiert die Reihenfolge | chronologisch, alphabetisch |
| PARTITION BY | Gruppiert die Daten logisch | je Kunde, Abteilung, Jahr |
| ROWS | physische Zeilen | einzelne Zeilen |
| RANGE | Wertebereich des ORDER-BY-Ausdrucks | statistische Werte |
Ein häufiger Irrtum betrifft den Unterschied zwischen RANGE und ROWS. RANGE berücksichtigt alle Zeilen mit gleichem Sortierwert, ROWS dagegen eine feste Anzahl von Zeilen. Bei doppelten Sortierwerten kann das zu stark abweichenden Ergebnissen führen. Faustregel: Zeitreihen sind ROWS, statistische Werte sind RANGE.
Ranking und Statistik im SQL Server
Rankingfunktionen wie ROW_NUMBER(), RANK(), DENSE_RANK() und NTILE() ordnen Zeilen innerhalb einer Partition. Sie liefern Positionen, Kategorien oder Quartile – ideal für Reports.
SELECT TerritoryID, SalesPersonID, SUM(SalesYTD) AS TotalSales, RANK() OVER( PARTITION BY TerritoryID ORDER BY SUM(SalesYTD) DESC ) AS RankPerRegion FROM Sales.SalesPerson GROUP BY TerritoryID, SalesPersonID;
Unterschiede:
- ROW_NUMBER(): Eindeutig, auch bei Gleichstand.
- RANK(): Gleich hohe Werte teilen sich denselben Rang, nächste Position wird übersprungen.
- DENSE_RANK(): Keine Lücken, also eine keine leeren Positionen.
- NTILE(n): Unterteilt in n Gruppen, zum Beispiel Quartile, Dezile et cetera.
Alle diese Funktionen arbeiten selbstredend deterministisch. Das bedeutet: Gleiche Daten, gleiches Ergebnis.
Analytische Funktionen für Zeitreihen und Vergleiche
SQL Server bietet weitere Fensterfunktionen, die auf Nachbarzeilen zugreifen:
SELECT ProductID, OrderDate, Amount, LAG(Amount) OVER(PARTITION BY ProductID ORDER BY OrderDate) AS PrevAmount, Amount - LAG(Amount) OVER(PARTITION BY ProductID ORDER BY OrderDate) AS Diff FROM Sales.OrderLines;
LAG() und LEAD() greifen auf vorherige beziehungsweise folgende Zeilen zu. FIRST_VALUE() und LAST_VALUE() holen die Extrempunkte des Fensters. Damit lassen sich Abweichungen, Trends und Wachstumsraten direkt in T-SQL berechnen.
Solche Berechnungen erzeugten früher komplexe Joins oder Cursor. Heute reicht eine Zeile – und SQL Server erledigt den Rest im Planoperator „Sequence Project“.
Frames – Präzision statt Zufall
Ohne explizite Frame-Definition wählt SQL Server intern Default-Werte, die vom Datentyp abhängen. Das ist fehleranfällig. Ein RANGE UNBOUNDED PRECEDING auf DATETIME kann zum Beispiel alle Zeilen mit gleichem Timestamp einschließen – auch bei Millisekunden-Unterschieden.
Ein explizites ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW liefert dagegen konsistente Ergebnisse. Fenster können sich auch rückwärts und vorwärts ausdehnen: ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING ergibt einen gleitenden Durchschnitt – auch bei den ersten und letzten Zeilen und auch, wenn nicht genügend vorherige oder folgende Zeilen existieren. Damit sind Bewegungsdurchschnitte und Rolling Averages direkt in SQL berechenbar – ohne ETL.
Was im Ausführungsplan passiert
Fensterfunktionen erzeugen im Execution Plan die Operatoren „Sort“, „Segment“, „Sequence Project“ oder „Window Aggregate“. Die Daten werden sortiert, segmentiert (nach Partition) und anschließend sequenziell verarbeitet. Für jede Partition entsteht eine geordnete Worktable. Wichtige Leistungsfaktoren sind laut Erfahrung:
- Index Passform: Ein Index auf (PartitionSpalte, OrderSpalte) vermeidet Sortierungen.
- Sort Spills: Wenn der Arbeitsspeicher zu klein ist, landet die Sortierung in TempDB, das bedeutet Performanceverlust. Prüfen mit „Actual Execution Plan“ und nach „Sort Warnings“ suchen.
- Parallelität: Fensterfunktionen können parallel laufen, wenn Partitionierung das zulässt.
- Batch Mode on Rowstore: Ab SQL Server 2022 auch für Zeilen-Tabellen, bewirkt deutlich schnellere Verarbeitung.
Zwei Praxisregeln:
- Eine gute Sortierung spart Gigabytes an I/O.
- Eine falsche Frame-Definition kostet Sekunden.
Ein Beispiel aus dem Reporting-Alltag
Ein Unternehmen möchte den monatlichen Umsatzverlauf pro Kunde inklusive Veränderung zum Vormonat ermitteln.
SELECT CustomerID, YEAR(OrderDate) AS OrderYear, MONTH(OrderDate) AS OrderMonth, SUM(TotalDue) AS MonthlyTotal, LAG(SUM(TotalDue)) OVER( PARTITION BY CustomerID ORDER BY YEAR(OrderDate), MONTH(OrderDate) ) AS PrevMonthTotal, SUM(TotalDue) - LAG(SUM(TotalDue)) OVER( PARTITION BY CustomerID ORDER BY YEAR(OrderDate), MONTH(OrderDate) ) AS Diff FROM Sales.SalesOrderHeader GROUP BY CustomerID, YEAR(OrderDate), MONTH(OrderDate);
Ein wenig Praxiswissen für Entwickler
Fensterfunktionen ersetzen Subqueries und verbessern die Lesbarkeit. Sie sind deterministisch, wiederholbar und in Views problemlos einsetzbar. WINDOW w AS (...) erlaubt Wiederverwendung in einer Abfrage – ab SQL Server 2022.
Ein wenig Praxiswissen für Admins
Fensteroperationen erzeugen I/O in TempDB. Mehrere Dateien, Instant File Initialization und (mindestens) schnelle SSDs sind Pflicht. Überwachung über sys.dm_db_file_space_usage und sys.dm_exec_query_profiles.
Ein wenig Praxiswissen fürs Monitoring
Auch das Monitoring soll nicht zu kurz kommen:
- Actual Execution Plan auf Sort Spills und Worktables prüfen.
- sys.dm_exec_query_stats für CPU und I/O pro Query auswerten.
- SET STATISTICS IO ON ausführen oder SQL Server Profiler verwenden und physische Leseoperationen erkennen.
Tipp: kleine, homogene Partitionen liefern stabilere Performance als wenige große.
Fehler und Fallstricke
Fehler und Fallstricke aus der Praxis
- Fenster ohne ORDER BY: wenn möglich, kein Determinismus, unterschiedliche Ergebnisse nach Planänderung.
- RANGE auf Textspalten: nicht zulässig, ergo Fehler.
- Große Partitionen: unzählige Sort Spills in TempDB.
- Zu viele Fenster in einer Abfrage: SQL Server dupliziert Sortierungen, ergo Zeitverlust.
- CTEs mit Fenstern in Views: Recompile-Kosten, wenn Statistiken veraltet.
Jede dieser Fallen kostet messbar Performance – aber nie Funktionalität. Fensterfunktionen liefern immer das korrekte Ergebnis – nur die Laufzeit variiert.
Fortgeschrittene Einsatzmuster
Fensterfunktionen lassen sich stapeln. Beispiel: prozentualer Anteil am Gesamtumsatz innerhalb eines Rankings.
SELECT CustomerID, SUM(TotalDue) AS CustomerTotal, SUM(SUM(TotalDue)) OVER() AS GlobalTotal, SUM(TotalDue) * 100.0 / SUM(SUM(TotalDue)) OVER() AS PercentOfAll FROM Sales.SalesOrderHeader GROUP BY CustomerID;
Ein weiterer Spezialfall: kombinierte Fenster. Man kann mehrere unabhängige Fenster in einem SELECT verwenden – SQL Server führt sie sequenziell aus. Bei gleichen PARTITION BY-/ORDER BY-Spalten werden sie intern zusammengefasst, das bewirkt einen Performance-Gewinn ohne Änderung am Code.
Grenzen und Alternativen
Fensterfunktionen sind leistungsfähig, aber nicht magisch. Sie skalieren bis mehrere Millionen Zeilen, doch bei hundert Millionen oder mehr sind Columnstore-Indexe und Batch Mode die bessere Wahl.
Auch Aggregationstabellen oder Materialized Views können Entlastung bringen. Für regelmäßige Reports ist Pre-Aggregation oft effizienter, für Ad-hoc-Analysen dagegen Fensterlogik.
SQL Server 2025 verbessert intern den „Window Aggregate“-Operator – für mehr Parallelität, bessere Speichernutzung und weniger TempDB-Schreibvorgänge.
Fazit
Fensterfunktionen sind nicht nur ein Feature – sie können ein Paradigmenwechsel sein. Sie ermöglichen analytisches Denken direkt im T-SQL und machen separate ETL-Prozesse oft überflüssig. Wer sie versteht, kann Kontext und Detail in einem Ergebnis vereinen. Sie sind präzise, skalierbar und transparent – und damit eines der mächtigsten Werkzeuge des SQL Server.
- Was ist also ein „Fenster“?
- Die Bausteine eines Fensters
- Ranking und Statistik im SQL Server
- Analytische Funktionen für Zeitreihen und Vergleiche
- Frames – Präzision statt Zufall
- Was im Ausführungsplan passiert
- Ein Beispiel aus dem Reporting-Alltag
- Ein wenig Praxiswissen für Entwickler
- Ein wenig Praxiswissen für Admins
- Ein wenig Praxiswissen fürs Monitoring
- Fehler und Fallstricke
- Fortgeschrittene Einsatzmuster
- Grenzen und Alternativen
- Fazit