Cursor Pagination in der mobilen Entwicklung — was es ist, Prinzip und Umsetzung

Autor: IT Sectr Veröffentlicht: 2026-03-11 Lesezeit: 9 Min.

Cursor Pagination ist eine Methode zum seitenweisen Laden von Daten, die einen eindeutigen Cursor zur Navigation durch einen geordneten Datensatz verwendet. Laut der GraphQL-Spezifikation (2025) ist die cursor-basierte Paginierung der empfohlene Standard für APIs, die mit dynamischen Daten arbeiten. Cursor pagination beseitigt die Hauptnachteile des Offset-Ansatzes: Instabilität bei Einfügungen und Leistungsabfall bei großen Offsets.

Wichtige Punkte

  • Cursor Pagination ist eine Paginierungsmethode, bei der jeder Datensatz einen eindeutigen Cursor-Identifikator zur Navigation hat.
  • Cursor ist ein eindeutiger Positionsmarker in einem Datensatz (in der Regel ID, UUID, Timestamp), der sich bei Einfügungen nicht ändert.
  • Stabilität — neue Datensätze, die zwischen Anfragen hinzugefügt werden, verschieben den Cursor nicht und vermeiden so Duplikate und Lücken.
  • Leistung — die WHERE id > cursor-Abfrage nutzt den Index effizient, ohne bei großen Datensätzen an Geschwindigkeit zu verlieren.
  • Einschränkung — die Cursor-Paginierung unterstützt keine Navigation nach Seitennummer (man kann nicht zu Seite 5 springen).

Was ist Cursor Pagination?

Cursor Pagination ist eine seitenweise Lademethode, bei der der Server zusammen mit den Daten einen speziellen Zeiger — einen Cursor — zurückgibt. Der Client verwendet diesen Cursor in der nächsten Anfrage, um den nächsten Batch von Datensätzen abzurufen. Der Cursor ist ein eindeutiger Identifikator des letzten Elements der aktuellen Seite.

Im Gegensatz zur Offset-Paginierung, bei der der Client sagt: „Gib mir Seite 5 mit 20 Datensätzen,“ funktioniert die Cursor-Paginierung anders: „Gib mir 20 Datensätze nach dem Datensatz mit ID = 83.“ Der Server führt eine Abfrage mit WHERE id > 83 und LIMIT 20 aus. Dieser Ansatz garantiert, dass jeder Datensatz unabhängig von Einfügungen genau in eine Seite fällt.

Das Konzept der Cursor-Paginierung wurde dank der Relay Connection (GraphQL)-Spezifikation weit verbreitet übernommen, die die cursor-basierte Paginierung zum Standard für moderne APIs gemacht hat. Relay definiert das Antwortformat: edges (Array von Datensätzen mit Cursorn), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Geschichte

Die Cursor-Paginierung ist keine neue Technik — sie wurde lange vor dem Web in Datenbanken verwendet. In SQL nennt man sie keyset pagination oder seek method. Die Methode wurde in APIs nach der Veröffentlichung der Relay-Spezifikation im Jahr 2015 populär, die das Cursor-Format als base64-kodierten String zur Vereinheitlichung über HTTP-Transport formalisierte.

Wie funktioniert die Cursor-Paginierung

Das Grundprinzip der Cursor-Paginierung ist, dass die Abfrage für die Positionierung eine WHERE-Bedingung auf einem indizierten Feld verwendet, nicht einen Offset. Für die Vorwärtsrichtung wird WHERE id > last_id verwendet, für die Rückwärtsrichtung WHERE id < first_id. Der B-tree-Index findet den ersten Datensatz nach dem Cursor in O(log n) und bietet so eine stabile Antwortzeit.

sql
-- 20 Datensätze nach dem Cursor '83' abrufen
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- 20 Datensätze VOR dem Cursor '83' abrufen (zurück)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Cursor-Format

Ein Cursor kann einfach (ein ID-Wert) oder komplex (aus mehreren Feldern zusammengesetzt) sein. Einfache Cursor sind der Primärschlüssel eines Datensatzes, z.B. Auto-Increment-ID oder UUID. Zusammengesetzte Cursor werden zum Sortieren nach nicht-eindeutigen Feldern verwendet, z.B. (created_at, id), wobei id die Eindeutigkeit bei identischen Zeitstempeln garantiert.

Ein typisches API-Format ist ein Cursor als base64-kodierter String. Der Server dekodiert den Cursor, extrahiert den Wert und erstellt die SQL-Abfrage. Base64-Kodierung verbirgt die interne Cursor-Struktur vor dem Client und ermöglicht eine Formatänderung ohne Unterbrechung der Abwärtskompatibilität. Der Client erhält die Cursor im endCursor-Feld der Antwort und übergibt sie als String in der nächsten Anfrage.

Vorwärts- und Rückwärtsnavigation

Die Cursor-Paginierung unterstützt bidirektionale Navigation. Für die Vorwärtsbewegung (next) wird der Cursor des letzten Elements der aktuellen Seite verwendet, für die Rückwärtsbewegung (previous) der Cursor des ersten Elements. Die Parameter after und before in der Anfrage bestimmen die Richtung: after holt Datensätze nach dem Cursor, before holt Datensätze vor dem Cursor.

Cursor vs. Offset-Paginierung

Die Wahl zwischen Cursor- und Offset-Paginierung ist eine der wichtigsten architektonischen Entscheidungen beim Entwurf einer API. Jede Methode hat Stärken und Schwächen, die ihre Anwendbarkeit bestimmen. Cursor pagination gewinnt in Szenarien mit dynamischen Daten; Offset gewinnt in Szenarien mit willkürlicher Navigation.

EigenschaftCursorOffset
Stabilität bei EinfügungenHoch (keine Duplikate)Niedrig (Seitenverschiebung)
Leistung bei großen DatenmengenO(log n) — stabilO(n) — nimmt mit Wachstum ab
Navigation nach SeitennummerNeinJa (page=5)
ImplementierungskomplexitätMittelNiedrig
REST-Unterstützungcursor/before/afterpage/offset
GraphQL-UnterstützungRelay-StandardNicht empfohlen

Warum Offset im großen Maßstab versagt

Die Offset-Paginierung führt einen vollständigen Tabellenscan bis zur OFFSET-Position durch. Bei offset=100000 liest und überspringt die Datenbank 100000 Zeilen, selbst wenn LIMIT 20 ist. MySQL und PostgreSQL können OFFSET nicht optimieren — dies ist eine Implementierungseigenschaft von LIMIT/OFFSET in SQL. Die Cursor-Paginierung verwendet einen B-tree-Index, der die Position in O(log n) findet.

Ein weiteres Offset-Problem ist das „Überspringen“ von Datensätzen beim Rückwärtspaginieren. Wenn ein Benutzer Seite 5 geladen hat und in diesem Moment neue Datensätze hinzugefügt werden, sieht er bei der Anforderung von Seite 6 entweder den Datensatz von Seite 5 erneut oder verpasst die neuen. Cursor pagination eliminiert dieses Szenario vollständig: Der Cursor zeigt auf eine bestimmte Stelle im Set, und Einfügungen ändern die Position nicht.

Implementierung von Cursor Pagination

Betrachten wir die Implementierung der Cursor-Paginierung im Backend (Kotlin + Spring) und im Client (Android + Retrofit). Der Server akzeptiert die Parameter after, before, limit und gibt eine Liste von Datensätzen mit Cursorn und pageInfo zurück. Eine typische Antwort enthält hasNextPage und hasPreviousPage zur Verwaltung der Paginierungs-UI.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

Client-Implementierung auf Android

Im Client wird die Cursor-Paginierung über PagingSource aus Paging 3 implementiert, wobei der Schlüssel ein Cursor (Long) ist. PagingSource.load erhält LoadParams.key — den Cursor des zuletzt geladenen Datensatzes. LoadResult.Page gibt die Daten und nextKey — den Cursor für die nächste Seite — zurück. Wenn nextKey = null ist, ist die Paginierung abgeschlossen.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource with cursor key
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

GraphQL-Implementierung über Relay

In GraphQL wird die Cursor-Paginierung über das Relay Connection-Muster implementiert. Jeder Typ hat eine Connection (mit pageInfo und edges) und einen Edge (node + cursor). Die Abfrage übergibt die Parameter first, after, last, before. Der Server gibt ein Array von Edges mit Cursorn und pageInfo mit hasNextPage/hasPreviousPage zurück.

Wann man Cursor Pagination verwendet

Die Cursor-Paginierung wird für APIs empfohlen, die mit dynamischen Daten arbeiten, bei denen Datensätze häufig hinzugefügt oder gelöscht werden. Klassische Beispiele: Nachrichtenfeed in sozialen Netzwerken, Chat-Nachrichten, Transaktionsverlauf, Kommentare zu Beiträgen. In all diesen Szenarien sind Konsistenz und das Fehlen von Duplikaten wichtig.

  • Chats und Messenger — jede neue Nachricht wird oben in der Liste hinzugefügt. Die Offset-Paginierung wird bei jeder neuen Nachricht gestört.
  • Soziale Netzwerke und Feeds — Beiträge werden kontinuierlich veröffentlicht. Die Cursor-Paginierung stellt sicher, dass der Benutzer keinen einzigen Beitrag verpasst.
  • Bestell- und Transaktionsverlauf — Daten ändern sich seltener, aber Konsistenz ist für Finanzberichte entscheidend.
  • APIs mit großen Datenmengen — Millionen von Datensätzen. Cursor pagination erhält die Leistung, wo Offset zu verlangsamen beginnt.
  • GraphQL-APIs — der Relay-Standard erfordert cursor-basierte Paginierung zur Einhaltung der Spezifikation.
  • Mobile Apps mit unendlichem Scrollen — der Benutzer scrollt nach unten und lädt neue Batches. Der Cursor-Ansatz bietet eine reibungslose UX ohne Duplikate.

Wann die Cursor-Paginierung nicht geeignet ist

Es gibt Szenarien, in denen die Offset-Paginierung bequemer ist: Admin-Panels, bei denen eine Navigation nach Seitennummer erforderlich ist; Suche mit Paginierung, bei der sich Ergebnisse ändern können; Berichte und Analysen, bei denen ein fester Link zu Seite 5 benötigt wird. In diesen Fällen überwiegen die Vorteile des Cursors nicht die Implementierungskomplexität.

Die Cursor-Paginierung unterstützt kein „Springen“ zu einer beliebigen Seite — der Benutzer kann nicht auf „Seite 5“ klicken und dorthin gelangen. Dies ist eine architektonische Einschränkung: das Zählen der Gesamtzahl der Seiten erfordert eine separate COUNT-Abfrage, die für große Tabellen teuer sein kann. In solchen Fällen ein hybrider Ansatz: Cursor für Daten + Count für die Paginierung.

Häufig gestellte Fragen

Was ist ein Cursor in Cursor Pagination?

Ein Cursor ist ein eindeutiger Datensatzidentifikator, der auf eine Position im Datensatz zeigt. Er kann einfach (eine Datensatz-ID) oder zusammengesetzt (mehrere Felder) sein. Der Client erhält den Cursor des letzten Datensatzes der Seite und übergibt ihn in der nächsten Anfrage, um den nächsten Batch zu erhalten.

Warum ist Cursor Pagination besser als Offset?

Cursor pagination unterliegt keiner Verschiebung beim Hinzufügen neuer Datensätze — jedes Element fällt genau in eine Seite. Es behält auch die Geschwindigkeit bei großen Volumina bei, indem es Indizes anstelle des Scannens der ersten n Zeilen verwendet. Offset ist einfacher, aber instabil für dynamische Daten.

Kann man die Cursor-Paginierung ohne GraphQL implementieren?

Ja, die Cursor-Paginierung ist nicht an GraphQL gebunden. Sie kann in jeder REST-API implementiert werden, indem der Cursor als Abfrageparameter ?after=83&limit=20 übergeben wird. Die Antwort sollte pageInfo mit endCursor und hasNextPage enthalten — dies ermöglicht dem Client, das Laden zu verwalten, ohne die interne Cursor-Struktur zu kennen.

Welchen Cursor verwenden — ID, UUID oder Timestamp?

Auto-Increment-ID ist die optimale Wahl: monoton steigend, ändert sich nicht, wird effizient indiziert. UUID v7 (zeitgeordnet) funktioniert ebenfalls. Timestamps können bei gleicher Zeit Duplikate erzeugen, daher kombinieren Sie es mit ID: (created_at, id) zur Gewährleistung der Cursor-Eindeutigkeit.

Wie findet man die Gesamtzahl der Seiten bei der Cursor-Paginierung?

Die Cursor-Paginierung liefert keine Gesamtzahl der Seiten — dies ist ihre Einschränkung. Wenn Sie Gesamtinformationen benötigen, führen Sie eine separate COUNT-Abfrage mit denselben Filtern durch. Verwenden Sie für große Tabellen eine ungefähre Zählung via EXPLAIN oder einen zwischengespeicherten Gesamtwert aus Analysen.

Zusammenfassung

  • Cursor Pagination ist eine Paginierungsmethode mit Navigation durch einen eindeutigen Datensatzidentifikator anstelle eines Offsets.
  • Der Cursor garantiert Set-Stabilität bei Einfügungen: neue Datensätze verschieben bereits geladene Seiten nicht.
  • Die Leistung bleibt bei großen Datenmengen dank der Verwendung des B-tree-Index hoch (O(log n)).
  • Cursor pagination eignet sich für dynamische Daten: Chats, Newsfeeds, Transaktionen, Kommentare.
  • Die Haupteinschränkung ist das Fehlen der Navigation nach Seitennummer und die Unmöglichkeit, zu einer beliebigen Seite zu springen.
  • Die Implementierung verwendet WHERE id nach dem Cursor, after/before-Parameter und pageInfo in der Antwort.
  • Standard — Relay Connection GraphQL, aber REST-APIs mit Cursor-Parametern sind ebenfalls weit verbreitet.

Wir entwickeln eine mobile Applikation schlüsselfertig

IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.

Projekt besprechen

Lesen Sie auch