physischer Zeilenversatz vs. logischer Wertebereich
ROWS und RANGE definieren beide einen Window Frame, tun das aber auf grundlegend verschiedene Weise: ROWS zaehlt physische Zeilenpositionen, RANGE betrachtet logische Wertebereiche der Sortierspalte. Bei eindeutigen Sortierwerten liefern beide dasselbe Ergebnis, bei Duplikaten in der Sortierspalte koennen ROWS und RANGE fuer dieselbe Abfrage unterschiedliche Zahlen produzieren.
Inhaltsverzeichnis
- 1. Was ein Window Frame ueberhaupt ist
- 2. ROWS: der physische Zeilenrahmen
- 3. RANGE: der logische Wertebereich
- 4. Der entscheidende Unterschied: wie beide mit Duplikaten umgehen
- 5. Der unsichtbare Standard-Frame und seine Fallstricke
- 6. GROUPS: die dritte, seltener genutzte Option
- 7. RANGE mit Datums- und Zeitintervallen
- 8. Praxisbeispiel: wann die Wahl das Ergebnis tatsaechlich aendert
- 9. ROWS und RANGE im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was ein Window Frame ueberhaupt ist
Jede Window Function mit ORDER BY arbeitet innerhalb eines Window Frame, also einer Teilmenge der aktuellen Partition, die relativ zur aktuellen Zeile definiert ist. Aggregierende Window Functions wie SUM(), AVG() oder COUNT() beziehen nur die Zeilen innerhalb dieses Frames in ihre Berechnung ein, nicht die gesamte Partition. Ohne ein grundlegendes Verstaendnis des Window Frame bleibt das Verhalten dieser Funktionen bei Aenderungen der Sortierung oder bei Duplikaten schwer vorhersehbar.
SQL bietet drei verschiedene Modi, einen Window Frame zu definieren: ROWS, RANGE und, seltener genutzt, GROUPS. Alle drei folgen der Syntax ROWS/RANGE/GROUPS BETWEEN Start AND Ende, unterscheiden sich aber fundamental darin, wie sie festlegen, welche Zeilen zum Frame gehoeren. Dieser Unterschied ist keine akademische Feinheit, sondern hat in bestimmten Situationen direkten Einfluss auf das Abfrageergebnis.
Dieser Artikel klaert den Unterschied zwischen ROWS und RANGE im Detail, zeigt anhand konkreter Beispiele mit Duplikaten, wann beide dasselbe und wann sie unterschiedliche Ergebnisse liefern, und ordnet GROUPS als dritte Option ein.
2. ROWS: der physische Zeilenrahmen
ROWS definiert einen Window Frame rein anhand physischer Zeilenpositionen, unabhaengig vom tatsaechlichen Wert der Sortierspalte. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW bezieht immer exakt die zwei vorangegangenen Zeilen plus die aktuelle Zeile ein, egal welche Werte diese Zeilen in der Sortierspalte tragen und egal ob mehrere Zeilen denselben Sortierwert teilen. Die Zaehlung erfolgt strikt nach Position in der durch ORDER BY festgelegten Reihenfolge.
Diese Eigenschaft macht ROWS zur richtigen Wahl fuer alle Faelle, in denen eine feste Anzahl von Datensaetzen gemeint ist, unabhaengig von ihren Werten: ein 7-Tage-Moving-Average ueber die letzten sieben Datensaetze, eine laufende Summe ueber die letzten zehn Transaktionen, oder ein Vergleich mit den drei vorangegangenen Messwerten. ROWS ist deterministisch in Bezug auf die Anzahl der einbezogenen Zeilen, was die Berechnung fuer Entwickler intuitiv nachvollziehbar macht.
-- ROWS: exactly 3 physical rows in the frame (2 preceding + current)
SELECT
student_id,
exam_date,
score,
SUM(score) OVER (
ORDER BY exam_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rows_sum
FROM exam_scores
ORDER BY exam_date;
-- exam_date | score | rows_sum (always sums exactly 3 rows once available)
-- 2026-01-10 | 70 | 70
-- 2026-01-10 | 85 | 155 <- two rows share the same date, still just 2 rows
-- 2026-01-12 | 90 | 245 <- exactly the 3 preceding physical rows
3. RANGE: der logische Wertebereich
RANGE definiert einen Window Frame dagegen ueber einen Wertebereich der Sortierspalte, nicht ueber Zeilenpositionen. RANGE BETWEEN 2 PRECEDING AND CURRENT ROW bezieht alle Zeilen ein, deren Sortierwert innerhalb von zwei Einheiten unterhalb des aktuellen Werts liegt, unabhaengig davon, wie viele physische Zeilen das tatsaechlich sind. Bei einer numerischen Sortierspalte mit dem aktuellen Wert 100 wuerden bei RANGE BETWEEN 2 PRECEDING alle Zeilen mit Werten zwischen 98 und 100 einbezogen, egal ob das eine, drei oder zwanzig Zeilen sind.
Der entscheidende Unterschied zu ROWS: Bei RANGE werden alle Zeilen mit demselben Sortierwert wie die aktuelle Zeile automatisch als eine logische Gruppe behandelt, sogenannte Peers. Das CURRENT ROW-Ende eines RANGE-Frames bedeutet deshalb nicht "bis zur aktuellen physischen Zeile", sondern "bis einschliesslich aller Zeilen mit demselben Sortierwert wie die aktuelle Zeile". Genau dieser Unterschied ist die Wurzel der meisten Verwirrung rund um RANGE.
-- RANGE: frame boundary defined by value distance, not row count
SELECT
student_id,
exam_date,
score,
SUM(score) OVER (
ORDER BY exam_date
RANGE BETWEEN INTERVAL '2 day' PRECEDING AND CURRENT ROW
) AS range_sum
FROM exam_scores
ORDER BY exam_date;
-- exam_date | score | range_sum (includes ALL rows within the date range)
-- 2026-01-10 | 70 | 155 <- both same-date rows included together
-- 2026-01-10 | 85 | 155 <- identical result for both peer rows
-- 2026-01-12 | 90 | 245 <- all rows within 2 days back
4. Der entscheidende Unterschied: wie beide mit Duplikaten umgehen
Solange die Sortierspalte ausschliesslich eindeutige Werte enthaelt, liefern ROWS und RANGE mit denselben Grenzen fast immer identische Ergebnisse, weil jede Zeile genau eine physische Position und gleichzeitig einen eindeutigen Wertebereich einnimmt. Sobald aber mehrere Zeilen denselben Sortierwert teilen, etwa mehrere Bestellungen am selben Tag oder mehrere Messwerte zur selben Sekunde, trennen sich die Ergebnisse von ROWS und RANGE deutlich.
ROWS behandelt jede Zeile individuell nach ihrer physischen Position, selbst wenn zwei Zeilen denselben Sortierwert haben, koennen sie in unterschiedlichen Frame-Grenzen landen. RANGE hingegen weist allen Zeilen mit demselben Sortierwert zwingend denselben Frame und damit dasselbe Aggregationsergebnis zu, weil sie als Peers gelten. Das ist der Kern des Unterschieds: ROWS kann unterschiedliche Ergebnisse fuer Zeilen mit identischem Sortierwert liefern, RANGE garantiert fuer Peers immer dasselbe Ergebnis.
-- Side-by-side: ROWS vs. RANGE with duplicate order dates
SELECT
order_id,
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS rows_running_total,
SUM(amount) OVER (
ORDER BY order_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS range_running_total
FROM orders
ORDER BY order_date, order_id;
-- order_id | order_date | amount | rows_running_total | range_running_total
-- 1 | 2026-02-01 | 100 | 100 | 250 <- peers already merged
-- 2 | 2026-02-01 | 150 | 250 | 250 <- same peer group
-- 3 | 2026-02-03 | 80 | 330 | 330
-- Rows 1 and 2 share the same order_date and are RANGE peers
5. Der unsichtbare Standard-Frame und seine Fallstricke
Ist eine ORDER BY-Klausel vorhanden, aber keine explizite Frame-Klausel, wendet die Datenbank standardmaessig RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW an, nicht ROWS. Das ist ein haeufiger Stolperstein, weil viele Entwickler implizit ROWS-Semantik erwarten, aber tatsaechlich RANGE-Semantik mit ihren Peer-Regeln erhalten. Bei eindeutigen Sortierwerten faellt dieser Unterschied nicht auf, bei Duplikaten fuehrt er zu Ergebnissen, die auf den ersten Blick falsch wirken, aber exakt dem SQL-Standard entsprechen.
Aus genau diesem Grund empfiehlt es sich, die Frame-Klausel bei jeder produktiven Window Function explizit auszuschreiben, statt sich auf den Standard zu verlassen. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW statt der impliziten RANGE-Variante zu schreiben macht sofort sichtbar, welches Verhalten beabsichtigt ist, und verhindert, dass ein spaeterer Leser des Codes das falsche Verhalten annimmt.
6. GROUPS: die dritte, seltener genutzte Option
Neben ROWS und RANGE definiert der SQL-Standard mit GROUPS eine dritte Frame-Art, die in PostgreSQL ab Version 11 sowie in einigen anderen modernen Datenbanken verfuegbar ist. GROUPS BETWEEN 2 PRECEDING AND CURRENT ROW zaehlt nicht Zeilen und nicht Wertebereiche, sondern Peer-Gruppen: Alle Zeilen mit demselben Sortierwert bilden eine Gruppe, und die Zahl vor PRECEDING gibt an, wie viele solcher Gruppen einbezogen werden, unabhaengig davon, wie viele physische Zeilen jede Gruppe enthaelt.
GROUPS ist nuetzlich, wenn eine feste Anzahl unterschiedlicher Werte statt einer festen Anzahl von Zeilen oder eines Wertebereichs gebraucht wird, etwa "die letzten drei unterschiedlichen Bestelltage" statt "die letzten drei Bestellungen" oder "die letzten drei Tage im Kalender". In der Praxis wird GROUPS deutlich seltener verwendet als ROWS und RANGE, ist aber die praeziseste Wahl fuer genau diese spezielle Anforderung.
-- GROUPS: count 2 preceding PEER GROUPS, not rows or a value range
SELECT
order_id,
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
GROUPS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS groups_sum
FROM orders
ORDER BY order_date, order_id;
-- order_date has duplicates: two orders share 2026-02-01
-- The frame always covers exactly 3 distinct order_date groups
-- regardless of how many physical rows each date contains
-- PostgreSQL 11+, some other databases support GROUPS directly
7. RANGE mit Datums- und Zeitintervallen
Ein legitimer und haeufiger Anwendungsfall fuer RANGE ist die Arbeit mit echten Kalenderfenstern statt Zeilenfenstern. RANGE BETWEEN INTERVAL '7 day' PRECEDING AND CURRENT ROW bezieht alle Zeilen ein, deren Datum innerhalb der letzten sieben Kalendertage liegt, unabhaengig davon, wie viele Zeilen an einzelnen Tagen vorliegen. Bei Zeitreihen mit Luecken oder mit mehreren Ereignissen pro Tag liefert das genau das gewuenschte Ergebnis eines echten Sieben-Tage-Fensters, waehrend ROWS BETWEEN 6 PRECEDING je nach Datenlage mehr oder weniger als sieben Kalendertage abdecken kann.
Diese Datums- und Zeitintervall-Faehigkeit von RANGE ist nicht in jeder Datenbank identisch implementiert. PostgreSQL und SQL Server unterstuetzen INTERVAL-Ausdruecke direkt in der RANGE-Klausel, waehrend MySQL vor Version 8.0.28 hier eingeschraenkter war und teils numerische Offsets statt echter Intervalle verlangt. Vor dem produktiven Einsatz lohnt sich deshalb ein Blick in die Dokumentation der jeweiligen Datenbank, welche Ausdrucksformen fuer RANGE-Grenzen konkret unterstuetzt werden.
-- RANGE with a true 7-day calendar window, gaps and multiple
-- events per day handled correctly
SELECT
sensor_id,
reading_time,
temperature,
AVG(temperature) OVER (
PARTITION BY sensor_id
ORDER BY reading_time
RANGE BETWEEN INTERVAL '7 day' PRECEDING AND CURRENT ROW
) AS avg_last_7_calendar_days
FROM sensor_readings
ORDER BY sensor_id, reading_time;
-- Covers exactly 7 calendar days back, regardless of how many
-- readings exist on each individual day
8. Praxisbeispiel: wann die Wahl das Ergebnis tatsaechlich aendert
Ein konkretes Beispiel aus der Praxis: Ein Verkaufsreport soll eine laufende Summe der Tagesumsaetze zeigen. Werden mehrere Transaktionen desselben Tages als separate Zeilen gespeichert, liefert RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW fuer alle Zeilen desselben Tages dieselbe laufende Summe, naemlich inklusive aller Transaktionen dieses Tages, waehrend ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW die laufende Summe von Zeile zu Zeile innerhalb desselben Tages weiterzaehlt. Fuer einen Report, der "kumulierter Umsatz bis einschliesslich heute" zeigen soll, ist RANGE korrekt, fuer einen Report, der die transaktionsgenaue Reihenfolge abbilden soll, ist ROWS korrekt.
Die richtige Wahl haengt also nicht von einer generellen Empfehlung ab, sondern von der fachlichen Frage hinter der Abfrage. Wer unsicher ist, sollte sich fragen: "Sollen Zeilen mit demselben Sortierwert immer dasselbe Ergebnis erhalten?" Ist die Antwort ja, ist RANGE die richtige Wahl. Ist die Antwort nein, weil jede physische Zeile individuell behandelt werden soll, ist ROWS korrekt.
| Kriterium | ROWS | RANGE |
|---|---|---|
| Zaehlbasis | Physische Zeilenposition | Logischer Wertebereich der Sortierspalte |
| Umgang mit Duplikaten | Jede Zeile individuell, kann abweichen | Peers erhalten garantiert dasselbe Ergebnis |
| Typischer Einsatz | Feste Zeilenanzahl, z. B. Moving Average | Echte Kalenderfenster, kumulierte Tageswerte |
| Standard ohne Frame-Klausel | Nicht der Standard | Ist der implizite Standard bei ORDER BY |
9. ROWS und RANGE im direkten Vergleich
Zusammengefasst unterscheiden sich ROWS und RANGE in genau einem Punkt fundamental: der Definition dessen, was "innerhalb des Fensters" bedeutet. ROWS beantwortet das mit "die soundsovielte physische Zeile davor oder danach", RANGE beantwortet das mit "alle Zeilen, deren Sortierwert in diesem Bereich liegt, inklusive aller Zeilen mit identischem Wert wie die aktuelle Zeile". Bei eindeutigen Sortierwerten ist dieser Unterschied unsichtbar, bei Duplikaten wird er zum entscheidenden Faktor fuer die Korrektheit einer Abfrage.
Fuer die Praxis bedeutet das: Ist die Sortierspalte garantiert eindeutig, etwa ein Primaerschluessel oder ein Zeitstempel mit Mikrosekunden-Praezision, spielt die Wahl zwischen ROWS und RANGE meist keine Rolle fuer das Ergebnis, wohl aber fuer Lesbarkeit und Performance, weil ROWS in den meisten Datenbanken effizienter ausgefuehrt wird. Ist die Sortierspalte nicht eindeutig, etwa ein reines Datum ohne Uhrzeit, muss die Wahl bewusst anhand der fachlichen Anforderung getroffen werden.
10. Zusammenfassung
ROWS und RANGE definieren einen Window Frame auf zwei grundlegend verschiedene Arten: ROWS zaehlt physische Zeilenpositionen, RANGE betrachtet logische Wertebereiche der Sortierspalte und behandelt Zeilen mit identischem Sortierwert als Peers mit garantiert gleichem Ergebnis. Solange die Sortierspalte eindeutige Werte enthaelt, liefern beide dasselbe Ergebnis, bei Duplikaten koennen sie sich deutlich unterscheiden.
Der unsichtbare Standard-Frame bei vorhandenem ORDER BY ohne explizite Frame-Klausel ist RANGE, nicht ROWS, was ein haeufiger Stolperstein ist. Wer die Frame-Klausel bei jeder Window Function bewusst und explizit setzt, statt sich auf den impliziten Standard zu verlassen, vermeidet die meisten Ueberraschungen, die aus dem Unterschied zwischen ROWS und RANGE entstehen koennen.
ROWS vs. RANGE, das Wichtigste auf einen Blick
ROWS
Zaehlt physische Zeilenpositionen. Jede Zeile wird individuell behandelt, auch bei gleichem Sortierwert.
RANGE
Zaehlt Wertebereiche der Sortierspalte. Peers mit gleichem Sortierwert erhalten garantiert dasselbe Ergebnis.
Der unsichtbare Standard
Ohne explizite Frame-Klausel gilt RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, nicht ROWS.
Wann es wichtig wird
Bei Duplikaten in der Sortierspalte, etwa mehreren Ereignissen am selben Tag oder Zeitstempel.