Cursor gegen Offset und wann welche Strategie zählt
Paginierung wirkt zunächst wie ein triviales Detail, entscheidet in API Platform aber über Antwortzeiten bei großen Tabellen, Konsistenz während paralleler Schreibzugriffe und Ergonomie für Client Entwickler. Wer nur die Standard Offset Paginierung kennt, verschenkt Performance und riskiert doppelte oder fehlende Datensätze bei stark frequentierten Endpunkten.
Inhaltsverzeichnis
- 1. Warum Paginierung mehr ist als eine Detailfrage
- 2. Offset Paginierung: der Standardfall in API Platform
- 3. Wo Offset Paginierung an ihre Grenzen stößt
- 4. Cursor basierte Paginierung aktivieren
- 5. Keyset Paginierung mit eigenem Index verstehen
- 6. Einen eigenen Paginator für externe Quellen bauen
- 7. Partielle Paginierung für sehr große Collections
- 8. Client Ergonomie: Hydra Links und OpenAPI Parameter
- 9. Paginierungsstrategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Paginierung mehr ist als eine Detailfrage
Jede Collection Operation in API Platform liefert standardmäßig eine paginierte Antwort zurück, weil eine ungebremste Liste mit hunderttausend Datensätzen weder Server noch Client sinnvoll verarbeiten könnten. Die Wahl der Paginierungsstrategie entscheidet aber über mehr als nur die Seitengröße: Sie bestimmt, wie stabil Ergebnisse bleiben, wenn zwischen zwei Requests neue Datensätze eingefügt werden, und wie teuer eine Abfrage auf tiefen Seiten wird.
Die häufigste Strategie, die Offset Paginierung, ist einfach zu verstehen und für kleine bis mittlere Tabellen ausreichend performant. Bei Millionen von Zeilen oder bei Endpunkten mit hoher Schreibfrequenz zeigt sich jedoch schnell, dass eine andere Paginierungsstrategie nötig ist. API Platform bietet dafür sowohl eingebaute Cursor basierte Paginierung als auch die Möglichkeit, einen komplett eigenen Paginator zu implementieren.
Wer die richtige Paginierungsstrategie von Anfang an wählt, spart sich später eine schmerzhafte Migration der API Verträge. Ein Wechsel von Offset zu Cursor Paginierung verändert nämlich die Struktur der zurückgegebenen Navigationslinks, was bei bereits produktiven Clients zu Breaking Changes führen kann, wenn er nicht sorgfältig geplant wird.
2. Offset Paginierung: der Standardfall in API Platform
Bei der Offset Paginierung übersetzt API Platform die Query Parameter page und itemsPerPage intern in ein OFFSET und LIMIT auf der Datenbankabfrage. Das ist die Standardeinstellung jeder Collection Operation und benötigt keine zusätzliche Konfiguration. Die Antwort enthält über Hydra oder JSON:API zusätzlich Links zur ersten, letzten, vorherigen und nächsten Seite, sodass Clients ohne eigene Logik durch die Ergebnismenge navigieren können.
Für die meisten administrativen Oberflächen und kleinere Datensätze ist die Offset Paginierung die richtige Wahl, weil sie beliebigen Sprung zu einer Seitenzahl erlaubt. Ein Nutzer kann direkt zur Seite 42 springen, was mit reiner Cursor Paginierung nicht ohne Weiteres möglich ist. Diese Eigenschaft macht Offset Paginierung zur bevorzugten Strategie für Backoffice Oberflächen mit Seitenzahlen Navigation.
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
/**
* Standard offset pagination configuration for a resource.
*/
#[ApiResource(
operations: [
new GetCollection(
paginationEnabled: true,
paginationClientItemsPerPage: true,
paginationItemsPerPage: 20,
paginationMaximumItemsPerPage: 100,
),
],
)]
final class Invoice
{
public int $id;
public string $number;
public \DateTimeImmutable $issuedAt;
}
3. Wo Offset Paginierung an ihre Grenzen stößt
Das grundlegende Problem der Offset Paginierung: Die Datenbank muss bei OFFSET 100000 zunächst hunderttausend Zeilen zählen und verwerfen, bevor die eigentlich gewünschte Seite geliefert wird. Je tiefer die Seite, desto langsamer die Abfrage, weil der Aufwand linear mit dem Offset wächst. Bei einer Tabelle mit mehreren Millionen Zeilen wird das schnell spürbar, selbst mit passendem Index auf der Sortierspalte.
Das zweite Problem betrifft Konsistenz: Wird zwischen zwei aufeinanderfolgenden Requests ein neuer Datensatz eingefügt, verschiebt sich der komplette Offset um eine Position, was dazu führt, dass ein Client beim Blättern denselben Datensatz doppelt sieht oder einen anderen komplett überspringt. Bei einem Feed, der sich ständig ändert, etwa Bestellungen oder Log Einträge, wird dieses Verhalten schnell zum sichtbaren Bug für den Client.
4. Cursor basierte Paginierung aktivieren
API Platform unterstützt Cursor basierte Paginierung nativ über paginationViaCursor. Statt eines Offsets navigiert der Client über den Wert eines eindeutigen, sortierbaren Feldes, meist die ID oder ein Zeitstempel. Jede Antwort enthält einen Cursor, der auf das letzte Element der aktuellen Seite verweist, der nächste Request nutzt diesen Cursor als Filter, um direkt an dieser Stelle fortzusetzen, ohne vorherige Zeilen zu zählen.
Diese Paginierung skaliert linear unabhängig von der Position in der Gesamtmenge, weil die Datenbank direkt über den Index auf die Startposition springt, statt Zeilen zu überspringen. Der Nachteil: Ein direkter Sprung zu einer beliebigen Seitenzahl ist nicht mehr möglich, der Client kann nur vorwärts oder rückwärts vom aktuellen Cursor aus navigieren. Für Feeds, Aktivitätslisten und API Konsumenten, die systematisch alle Datensätze durchlaufen, ist das der klar überlegene Ansatz.
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
/**
* Cursor based pagination keyed on the immutable, ordered id column.
*/
#[ApiResource(
operations: [
new GetCollection(
paginationViaCursor: [
['field' => 'id', 'direction' => 'DESC'],
],
paginationPartial: true,
),
],
order: ['id' => 'DESC'],
)]
final class ActivityLogEntry
{
public int $id;
public string $action;
public \DateTimeImmutable $occurredAt;
}
5. Keyset Paginierung mit eigenem Index verstehen
Cursor basierte Paginierung in API Platform ist im Kern eine Umsetzung von Keyset Paginierung: Die Datenbankabfrage bekommt eine WHERE id < :cursor Bedingung statt eines Offsets, kombiniert mit ORDER BY id DESC LIMIT :size. Damit das performant bleibt, muss auf der sortierenden Spalte ein Index existieren, sonst führt auch Keyset Paginierung zu vollständigen Tabellenscans.
Bei zusammengesetzten Sortierungen, etwa zuerst nach Status, dann nach Erstellungsdatum, braucht die Keyset Bedingung entsprechend mehrere Felder im Cursor, damit die Reihenfolge stabil bleibt. API Platform unterstützt mehrere Sortierfelder im paginationViaCursor Array, wichtig ist aber, dass ein zusammengesetzter Index in Doctrine dieselbe Feldreihenfolge abbildet, damit die Datenbank den Index tatsächlich nutzen kann statt eine zusätzliche Sortierung im Speicher durchzuführen.
6. Einen eigenen Paginator für externe Quellen bauen
Sobald Daten nicht aus Doctrine, sondern über einen State Provider aus einer externen API oder einem Suchindex kommen, muss die Paginierung manuell nachgebaut werden. API Platform erwartet dafür ein Objekt, das PaginatorInterface implementiert und Methoden wie getCurrentPage(), getItemsPerPage() und getTotalItems() bereitstellt. Diese Metadaten fließen automatisch in die Hydra oder JSON:API Antwort ein, ohne dass der Client etwas von der internen Quelle merkt.
Bei Cursor basierter Paginierung auf einer externen Quelle implementiert man stattdessen PartialPaginatorInterface, das keine Gesamtanzahl kennen muss, was besonders bei Suchindizes wie Elasticsearch relevant ist, wo eine exakte Gesamtzahl teuer zu berechnen wäre. Der eigene Paginator reicht in diesem Fall nur den nächsten Cursor Wert durch, den die externe Quelle selbst zurückgibt.
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
/**
* Wraps results from an external search index that returns its own cursor.
*/
final class SearchResultPaginator implements PartialPaginatorInterface, \IteratorAggregate
{
private array $items;
public function __construct(
array $items,
private readonly int $itemsPerPage,
private readonly ?string $nextCursor,
) {
$this->items = $items;
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->items);
}
public function count(): int
{
return count($this->items);
}
public function getItemsPerPage(): float
{
return (float) $this->itemsPerPage;
}
public function getCurrentPage(): float
{
return 1.0;
}
}
7. Partielle Paginierung für sehr große Collections
Neben Cursor basierter Paginierung bietet API Platform mit paginationPartial eine leichtgewichtige Option, die auf Doctrine Ebene die teure COUNT(*) Abfrage überspringt. Statt der exakten Gesamtanzahl liefert die Antwort nur, ob eine nächste Seite existiert. Für Endlos Scroll Oberflächen reicht diese Information völlig aus und spart bei sehr großen Tabellen eine zusätzliche, oft teure Zählabfrage.
Diese Option lässt sich sowohl mit Offset als auch mit Cursor Paginierung kombinieren und ist besonders bei Tabellen mit häufigen Schreibzugriffen sinnvoll, weil eine exakte Gesamtanzahl in einem solchen Szenario ohnehin nur für den Moment der Abfrage gilt und sich Sekunden später schon wieder geändert haben kann. Der Verzicht auf die exakte Zahl ist hier kein Funktionsverlust, sondern eine ehrliche Abbildung der tatsächlichen Datenlage.
8. Client Ergonomie: Hydra Links und OpenAPI Parameter
Unabhängig von der gewählten Paginierungsstrategie dokumentiert API Platform die verfügbaren Query Parameter automatisch in der generierten OpenAPI Spezifikation, sodass Client Entwickler ohne Rückfrage erkennen, ob page, itemsPerPage oder ein Cursor Parameter erwartet wird. Diese automatische Dokumentation ist einer der größten Vorteile gegenüber handgeschriebenen REST Endpunkten, bei denen Paginierungsverhalten oft nur in einem separaten Wiki dokumentiert ist.
Bei Hydra Antworten liefert API Platform zusätzlich hydra:view mit direkten Links zur nächsten und vorherigen Seite, sodass ein gut gebauter Client überhaupt keine eigene URL Konstruktion mehr braucht, sondern einfach dem gelieferten Link folgt. Diese Selbstbeschreibung reduziert die Kopplung zwischen Client und Server erheblich, weil sich das interne Paginierungsformat ändern kann, ohne dass der Client seine URL Logik anpassen muss.
9. Paginierungsstrategien im Vergleich
Die folgende Tabelle stellt die drei wichtigsten Paginierungsstrategien in API Platform gegenüber und zeigt, wann welche Variante die richtige Wahl ist.
| Kriterium | Offset Paginierung | Cursor Paginierung | Empfehlung |
|---|---|---|---|
| Sprung zu Seite N | Direkt möglich | Nicht direkt möglich | Offset für Backoffice mit Seitenzahlen |
| Performance bei tiefen Seiten | Fällt mit steigendem Offset | Konstant über Index | Cursor bei großen Tabellen |
| Konsistenz bei Schreibzugriff | Duplikate oder Lücken möglich | Stabil pro Cursor | Cursor für Feeds und Logs |
| Gesamtanzahl bekannt | Ja, über COUNT | Optional, via paginationPartial | Partial bei sehr großen Tabellen |
| Setup Aufwand | Standard, keine Konfiguration | Sortierfeld und Index nötig | Offset als sicherer Startpunkt |
In der Praxis beginnen die meisten API Platform Projekte mit der Standard Offset Paginierung und wechseln gezielt zu Cursor basierter Paginierung, sobald konkrete Performance oder Konsistenzprobleme auftreten. Diese pragmatische Reihenfolge verhindert vorzeitige Optimierung, ohne die Möglichkeit zu verlieren, später auf eine Cursor Strategie umzusteigen, sobald die Datenmenge es erfordert.
Mironsoft
Symfony und API Platform Architektur für anspruchsvolle APIs
Paginierung, die auch bei Millionen Datensätzen performant bleibt?
Wir analysieren eure API Platform Endpunkte, identifizieren teure Offset Abfragen und implementieren Cursor basierte Paginierung oder eigene Paginatoren für externe Datenquellen.
Performance Audit
Query Analyse für Paginierung auf großen Tabellen
Cursor Migration
Sichere Umstellung von Offset auf Cursor Paginierung ohne Breaking Changes
Custom Paginator
Eigene Paginatoren für externe APIs und Suchindizes
10. Zusammenfassung
Offset Paginierung ist der einfache Standardfall in API Platform und für kleinere Tabellen mit direkter Seitenzahl Navigation die richtige Wahl. Sobald Tabellen wachsen oder Endpunkte hoher Schreibfrequenz ausgesetzt sind, zeigt Cursor basierte Paginierung ihre Stärken: konstante Performance unabhängig von der Position und stabile Ergebnisse auch bei parallelen Schreibzugriffen. Für externe Datenquellen jenseits von Doctrine braucht es einen eigenen Paginator, der PaginatorInterface oder PartialPaginatorInterface implementiert.
Die Entscheidung für eine Paginierungsstrategie sollte nie pauschal für ein ganzes Projekt getroffen werden, sondern pro Endpunkt anhand der tatsächlichen Zugriffsmuster: Backoffice Listen profitieren von Offset, öffentliche Feeds und Aktivitätslisten von Cursor Paginierung. Wer diese Unterscheidung früh trifft, vermeidet spätere Breaking Changes an den API Verträgen.
Paginierung in API Platform: Das Wichtigste auf einen Blick
Offset Paginierung
Standard in API Platform, erlaubt direkten Sprung zu Seitenzahlen, wird bei großen Offsets langsamer.
Cursor Paginierung
Über paginationViaCursor aktiviert, konstante Performance über den Index, kein direkter Seitensprung.
Partielle Paginierung
paginationPartial spart die teure COUNT Abfrage, ideal für Endlos Scroll und sehr große Tabellen.
Eigene Paginatoren
PaginatorInterface oder PartialPaginatorInterface für externe Quellen jenseits von Doctrine.