Pagination-Syntax im Datenbankvergleich
Eine begrenzte Anzahl Zeilen aus einer Abfrage zurueckzugeben klingt nach einer trivialen Aufgabe, doch MySQL, PostgreSQL, SQL Server und Oracle schreiben diese Begrenzung mit vier vollstaendig unterschiedlichen Syntaxformen. Wer Pagination-Code zwischen diesen Systemen portabel halten will, braucht den Ueberblick ueber LIMIT, TOP, ROWNUM und den gemeinsamen ANSI-Standard-Nenner OFFSET FETCH.
Inhaltsverzeichnis
- 1. Warum Pagination-Syntax zwischen Datenbanken so stark variiert
- 2. MySQL und PostgreSQL: LIMIT und OFFSET
- 3. SQL Server: TOP und OFFSET FETCH NEXT
- 4. Oracle: ROWNUM, ROW_NUMBER und FETCH FIRST
- 5. Der ANSI SQL Standard als gemeinsamer Nenner
- 6. Row-Number-basierte Pagination als portable Fallback-Strategie
- 7. Warum grosse OFFSET-Werte in jeder Datenbank teuer werden
- 8. ORM- und Query-Builder-Abstraktionen fuer portable Pagination
- 9. Pagination-Syntax im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Pagination-Syntax zwischen Datenbanken so stark variiert
Fast jede Anwendung mit Listenansichten braucht Pagination, also die Begrenzung einer Abfrage auf eine bestimmte Anzahl Zeilen ab einer bestimmten Position. Dieses Bedarf ist so alt wie relationale Datenbanken selbst, doch als die grossen Hersteller ihre Systeme entwickelten, gab es noch keinen SQL-Standard, der diese Aufgabe regelte. Jede Datenbank erfand daher ihre eigene Syntax, lange bevor der ANSI-Standard mit SQL:2008 die OFFSET ... FETCH-Klausel als einheitliche Loesung nachreichte.
Das Ergebnis ist bis heute eine fragmentierte Landschaft: MySQL und PostgreSQL nutzen die kompakte LIMIT ... OFFSET-Syntax, SQL Server historisch TOP und seit SQL Server 2012 zusaetzlich das standardnaehere OFFSET ... FETCH NEXT, und Oracle vor Version 12c ausschliesslich die semantisch tricky ROWNUM-Pseudospalte. Wer Anwendungen fuer mehrere Datenbanken schreibt oder eine Migration plant, muss diese vier Ansaetze und ihre subtilen Unterschiede in der Auswertungsreihenfolge kennen, denn Pagination-Bugs durch falsche Annahmen ueber die Ausfuehrungsreihenfolge gehoeren zu den haeufigsten Migrationsfehlern.
2. MySQL und PostgreSQL: LIMIT und OFFSET
MySQL und PostgreSQL teilen sich die gleiche, sehr kompakte Syntax LIMIT n OFFSET m, die die Ergebnismenge nach der Sortierung durch ORDER BY auf n Zeilen begrenzt und dabei die ersten m Zeilen ueberspringt. Diese Aehnlichkeit ist kein Zufall, PostgreSQL hat die Syntax bewusst kompatibel zu MySQL gehalten, obwohl beide Systeme technisch unabhaengig voneinander sind. Wichtig ist die Reihenfolge der Klauseln: LIMIT und OFFSET stehen immer nach ORDER BY, eine Abfrage ohne explizite Sortierung liefert keine garantiert stabile Reihenfolge fuer die Pagination.
PostgreSQL erlaubt zusaetzlich die alternative, ANSI-naehere Schreibweise OFFSET m ROWS FETCH NEXT n ROWS ONLY, die inhaltlich identisch zu LIMIT ... OFFSET ist, aber besser zu Migrationen von oder zu SQL Server und Oracle passt. In der Praxis dominiert bei beiden Datenbanken trotzdem die kuerzere LIMIT-Schreibweise, da sie in nahezu jeder Codebasis, jedem Tutorial und jedem ORM als Standardfall vorausgesetzt wird.
-- MySQL and PostgreSQL: identical LIMIT / OFFSET syntax
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;
-- Returns rows 41 to 60, ordered by creation date descending
-- PostgreSQL also accepts the ANSI-closer alternative
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
3. SQL Server: TOP und OFFSET FETCH NEXT
SQL Server bot historisch nur TOP n an, eine Klausel direkt hinter SELECT, die ausschliesslich die ersten n Zeilen der sortierten Ergebnismenge zurueckgibt, aber kein natives Ueberspringen von Zeilen erlaubt. Fuer echte Pagination musste man frueher auf Umwege mit einer ROW_NUMBER()-Fensterfunktion in einer Subquery zurueckgreifen, kombiniert mit einem WHERE-Filter auf die berechnete Zeilennummer, ein deutlich umstaendlicheres Muster als in MySQL oder PostgreSQL.
Seit SQL Server 2012 gibt es mit OFFSET m ROWS FETCH NEXT n ROWS ONLY die deutlich einfachere, ANSI-konforme Alternative, die inzwischen der empfohlene Standardweg fuer Pagination ist. Eine wichtige Einschraenkung bleibt: OFFSET ... FETCH in SQL Server erfordert zwingend eine ORDER BY-Klausel, waehrend TOP theoretisch auch ohne Sortierung funktioniert, dann aber ein undefiniertes, nicht wiederholbares Ergebnis liefert.
-- SQL Server: legacy TOP, only limits, cannot skip rows natively
SELECT TOP 20 id, title, created_at
FROM articles
ORDER BY created_at DESC;
-- SQL Server 2012+: modern OFFSET FETCH, requires ORDER BY
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- Older pagination workaround before SQL Server 2012
SELECT id, title, created_at FROM (
SELECT id, title, created_at,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM articles
) AS numbered
WHERE rn BETWEEN 41 AND 60;
4. Oracle: ROWNUM, ROW_NUMBER und FETCH FIRST
Oracle hatte vor Version 12c keine dedizierte Pagination-Syntax, sondern nur die Pseudospalte ROWNUM, die jeder Zeile im Ergebnis eine fortlaufende Nummer zuweist. Der entscheidende Fallstrick: ROWNUM wird waehrend der Auswertung zugewiesen, bevor ORDER BY greift, weshalb eine direkte Bedingung wie WHERE ROWNUM BETWEEN 41 AND 60 in einer sortierten Abfrage falsche Ergebnisse liefert. Der korrekte, klassische Ansatz kapselt die sortierte Abfrage in einer Subquery und wendet ROWNUM erst auf das bereits sortierte Zwischenergebnis an.
Seit Oracle 12c gibt es mit OFFSET ... FETCH FIRST n ROWS ONLY endlich eine ANSI-konforme, deutlich weniger fehleranfaellige Syntax, die dasselbe Ergebnis ohne die ROWNUM-Falle liefert. Fuer neue Oracle-Projekte ab Version 12c ist diese Syntax uneingeschraenkt zu empfehlen, waehrend Legacy-Code mit ROWNUM bei jeder Migration oder Refaktorierung besonders sorgfaeltig auf die richtige Subquery-Verschachtelung geprueft werden muss.
-- Oracle before 12c: ROWNUM trap — this gives WRONG results
-- ROWNUM is assigned before ORDER BY, ordering happens on unnumbered rows
SELECT id, title, created_at
FROM articles
WHERE ROWNUM BETWEEN 41 AND 60
ORDER BY created_at DESC; -- incorrect: ROWNUM numbers unsorted rows first
-- Correct classic pattern: apply ROWNUM to an already sorted subquery
SELECT id, title, created_at FROM (
SELECT id, title, created_at, ROWNUM AS rn FROM (
SELECT id, title, created_at FROM articles ORDER BY created_at DESC
)
) WHERE rn BETWEEN 41 AND 60;
-- Oracle 12c+: modern, ANSI-compliant FETCH FIRST syntax
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 40 ROWS FETCH FIRST 20 ROWS ONLY;
5. Der ANSI SQL Standard als gemeinsamer Nenner
Mit SQL:2008 hat der ANSI-Standard die Klausel OFFSET m ROWS FETCH FIRST n ROWS ONLY beziehungsweise gleichbedeutend FETCH NEXT n ROWS ONLY als offizielle, herstellerunabhaengige Loesung fuer Pagination eingefuehrt. PostgreSQL, SQL Server ab Version 2012 und Oracle ab Version 12c unterstuetzen diese Syntax inzwischen alle, wenn auch mit geringfuegigen Unterschieden bei optionalen Zusaetzen wie WITH TIES, das zusaetzliche Zeilen mit demselben Sortierwert wie die letzte zurueckgegebene Zeile einschliesst.
MySQL ist die einzige der vier besprochenen Datenbanken, die diese Standard-Syntax bis heute nicht unterstuetzt und stattdessen ausschliesslich bei LIMIT ... OFFSET bleibt. Wer wirklich portablen SQL-Code fuer alle vier Systeme schreiben moechte, kommt daher nicht um eine bedingte Codegenerierung herum, die je nach Zieldatenbank entweder LIMIT ... OFFSET oder OFFSET ... FETCH FIRST erzeugt.
6. Row-Number-basierte Pagination als portable Fallback-Strategie
Eine Alternative, die auf allen vier Datenbanken identisch funktioniert, ist die manuelle Nummerierung ueber die Fensterfunktion ROW_NUMBER() OVER (ORDER BY ...), die seit SQL:2003 Teil des Standards ist und von MySQL ab Version 8.0, PostgreSQL, SQL Server und Oracle gleichermassen unterstuetzt wird. Diese Methode kapselt die sortierte Abfrage in einer Subquery, berechnet die Zeilennummer und filtert dann mit einer einfachen WHERE-Bedingung auf den gewuenschten Bereich.
Der Vorteil dieser Row-Number-Strategie ist vollstaendige Portabilitaet ohne bedingte Codegenerierung, der Nachteil ein etwas hoeherer Schreibaufwand gegenueber der nativen Kurzsyntax und in manchen Query-Plaenen eine geringfuegig schlechtere Optimierbarkeit, da der Optimizer die Fensterfunktion nicht immer so effizient mit dem zugrunde liegenden Index verknuepfen kann wie eine native LIMIT- oder FETCH FIRST-Klausel.
-- Portable pattern: works identically on MySQL 8+, PostgreSQL, SQL Server, Oracle
SELECT id, title, created_at FROM (
SELECT id, title, created_at,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM articles
) AS numbered
WHERE rn BETWEEN 41 AND 60
ORDER BY created_at DESC;
-- No LIMIT, TOP, ROWNUM, or FETCH FIRST needed — same syntax everywhere
7. Warum grosse OFFSET-Werte in jeder Datenbank teuer werden
Unabhaengig von der gewaehlten Syntax haben alle vier Datenbanken dasselbe strukturelle Problem bei grossen Offset-Werten: Die Datenbank muss intern die ersten m + n Zeilen der sortierten Ergebnismenge tatsaechlich durchlaufen, bevor sie die uebersprungenen m Zeilen verwerfen und die restlichen n Zeilen zurueckgeben kann. Bei Seite 1000 einer Ergebnisliste mit 20 Zeilen pro Seite bedeutet das intern das Lesen von fast 20.000 Zeilen, unabhaengig davon, ob die Syntax LIMIT, TOP mit Subquery, ROWNUM oder FETCH FIRST lautet.
Dieses Verhalten ist syntaxunabhaengig und liegt in der relationalen Ausfuehrungslogik selbst begruendet, weshalb Keyset-Pagination mit einer WHERE-Bedingung auf den letzten gesehenen Sortierwert bei tiefen Seiten die deutlich schnellere Alternative ist. Dieser Artikel konzentriert sich bewusst auf die Syntaxunterschiede der klassischen Offset-Pagination zwischen den Datenbanken, waehrend die Performance-Strategie und der Vergleich mit Keyset-Pagination ausfuehrlich in einem eigenen Artikel dieses Blogs behandelt wird.
8. ORM- und Query-Builder-Abstraktionen fuer portable Pagination
In der Praxis schreiben die wenigsten Teams Pagination-Syntax von Hand fuer mehrere Datenbanken, sondern verlassen sich auf die Abstraktion ihres ORMs oder Query-Builders. Doctrine, Eloquent, Sequelize und SQLAlchemy bieten alle eine einheitliche limit()/offset()-API, die intern je nach konfiguriertem Datenbanktreiber die passende native Syntax erzeugt, ohne dass Anwendungscode LIMIT, TOP oder ROWNUM direkt kennen muss.
Wichtig ist trotzdem, bei kritischen Abfragen den tatsaechlich generierten SQL-Code zu pruefen, insbesondere bei aelteren Oracle-Zielsystemen, bei denen manche ORM-Versionen historisch die fehleranfaellige ROWNUM-Subquery-Verschachtelung statt der moderneren FETCH FIRST-Syntax generiert haben. Ein automatisierter Test, der fuer jede unterstuetzte Datenbank eine Beispielabfrage mit Offset ausfuehrt und das Ergebnis gegen eine erwartete Zeilenmenge prueft, faengt solche ORM-Regressionen zuverlaessig ab.
9. Pagination-Syntax im direkten Vergleich
Die folgende Tabelle stellt die vier nativen Syntaxformen sowie den ANSI-Standard-Ansatz nebeneinander.
| Datenbank | Native Syntax | ANSI OFFSET FETCH | Besonderheit |
|---|---|---|---|
| MySQL | LIMIT n OFFSET m |
Nicht unterstuetzt | Einzige Datenbank ohne Standard-Syntax |
| PostgreSQL | LIMIT n OFFSET m |
Unterstuetzt | Beide Schreibweisen inhaltlich identisch |
| SQL Server | TOP n (kein Skip) |
Unterstuetzt (ab 2012) | OFFSET FETCH erfordert zwingend ORDER BY |
| Oracle | ROWNUM |
Unterstuetzt (ab 12c) | ROWNUM vor ORDER BY zugewiesen, Subquery-Falle |
Wer heute neuen Code schreibt und mindestens PostgreSQL, SQL Server 2012+ oder Oracle 12c+ als Ziel hat, sollte die ANSI-Syntax OFFSET ... FETCH FIRST bevorzugen. Nur wenn MySQL Teil der unterstuetzten Systeme ist, bleibt entweder LIMIT ... OFFSET als datenbankspezifischer Zweig oder die vollstaendig portable Row-Number-Strategie aus Abschnitt sechs die richtige Wahl.
Mironsoft
Datenbank-Portabilitaet, Query-Optimierung und Migrationen
Pagination-Code, der auf jeder Zieldatenbank korrekt laeuft?
Wir pruefen bestehende Pagination-Abfragen auf ROWNUM-Fallen und Portabilitaetsluecken und bauen eine saubere, ANSI-nahe Loesung, die auch bei tiefen Seiten performant bleibt.
Code-Review
Pruefung bestehender Pagination-Abfragen auf ROWNUM- und Sortier-Fallen
Migration
Umbau von TOP oder ROWNUM auf ANSI-konforme FETCH-FIRST-Syntax
Performance
Keyset-Pagination fuer tiefe Seiten statt teurem Offset-Scan
10. Zusammenfassung
Die Pagination-Syntax unterscheidet sich zwischen MySQL, PostgreSQL, SQL Server und Oracle erheblich: MySQL und PostgreSQL teilen sich die kompakte LIMIT ... OFFSET-Syntax, SQL Server bietet historisch TOP und modern OFFSET ... FETCH NEXT, und Oracle wechselte von der fehleranfaelligen ROWNUM-Pseudospalte zur ANSI-konformen FETCH FIRST-Klausel ab Version 12c. Der ANSI-Standard OFFSET ... FETCH FIRST wird von drei der vier Systeme unterstuetzt, nur MySQL bleibt aussen vor.
Fuer produktiven, portablen Code lohnt sich entweder eine bedingte Syntax-Generierung je nach Zieldatenbank oder die vollstaendig portable Row-Number-Strategie mit ROW_NUMBER() OVER (ORDER BY ...). Unabhaengig von der gewaehlten Pagination-Syntax bleibt das strukturelle Performance-Problem grosser Offset-Werte bestehen und sollte bei tiefen Seiten durch Keyset-Pagination geloest werden.
Pagination-Syntax im Datenbankvergleich — Das Wichtigste auf einen Blick
MySQL / PostgreSQL
LIMIT n OFFSET m, PostgreSQL zusaetzlich mit ANSI-Alternative.
SQL Server
Modern OFFSET ... FETCH NEXT, erfordert immer ein ORDER BY.
Oracle
ROWNUM-Falle vor 12c, danach ANSI-konformes FETCH FIRST.
Portable Loesung
ROW_NUMBER() OVER (ORDER BY ...) funktioniert identisch auf allen vier Systemen.