Cursor Pagination w rozwoju mobilnym — co to jest, zasada i implementacja

Autor: IT Sectr Opublikowano: 2026-03-11 Czas czytania: 9 min

Cursor Pagination — metoda stronicowania danych, która wykorzystuje unikalny kursor do nawigacji po uporządkowanym zbiorze rekordów. Według GraphQL Specification (2025), stronicowanie kursorem jest zalecanym standardem dla API pracujących z dynamicznymi danymi. Cursor Pagination eliminuje główne wady podejścia Offset: niestabilność przy wstawianiu i spadek wydajności przy dużych przesunięciach.

Najważniejsze

  • Cursor Pagination — metoda stronicowania, w której każdy rekord ma unikalny identyfikator-kursor do nawigacji.
  • Kursor — unikalny znacznik pozycji w zbiorze danych (zwykle ID, UUID, timestamp), który nie zmienia się przy wstawianiu.
  • Stabilność — nowe rekordy dodane między zapytaniami nie przesuwają kursora, eliminując duplikaty i pominięcia.
  • Wydajność — zapytanie WHERE id > cursor efektywnie wykorzystuje indeks, nie tracąc szybkości na dużych zbiorach.
  • Ograniczenie — stronicowanie kursorem nie obsługuje nawigacji po numerze strony (nie można przeskoczyć do strony 5).

Co to jest Cursor Pagination?

Cursor Pagination (stronicowanie kursorem) — metoda stronicowania, w której serwer wraz z danymi zwraca specjalny wskaźnik — kursor. Klient używa tego kursora w następnym zapytaniu, aby pobrać kolejną porcję rekordów. Kursor to unikalny identyfikator ostatniego elementu bieżącej strony.

W przeciwieństwie do Offset-pagination, gdzie klient mówi „daj mi stronę 5 po 20 rekordów”, stronicowanie kursorem działa inaczej: „daj mi 20 rekordów po rekordzie o ID = 83”. Serwer wykonuje zapytanie z warunkiem WHERE id > 83 i LIMIT 20. Takie podejście gwarantuje, że każdy rekord trafi dokładnie na jedną stronę niezależnie od wstawień.

Koncepcja stronicowania kursorem zyskała szerokie rozpowszechnienie dzięki specyfikacji Relay Connection (GraphQL), która uczyniła cursor-based pagination standardem dla nowoczesnych API. Relay określa format odpowiedzi: edges (tablica rekordów z kursorami), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Historia powstania

Cursor-pagination nie jest nową techniką — była używana w bazach danych na długo przed pojawieniem się sieci web. W SQL nazywa się to keyset pagination lub seek method. Metoda stała się popularna w API po opublikowaniu specyfikacji Relay w 2015 roku, która sformalizowała format kursora jako base64-encoded string dla ujednolicenia transmisji przez HTTP.

Jak działa stronicowanie kursorem

Podstawowa zasada stronicowania kursorem — zapytanie używa warunku WHERE na polu indeksowanym do pozycjonowania, a nie przesunięcia. Dla kierunku do przodu stosuje się WHERE id > last_id, dla kierunku wstecz — WHERE id < first_id. Indeks B-tree pozwala znaleźć pierwszy rekord po kursorze w O(log n), co daje stabilny czas odpowiedzi.

sql
-- Pobierz 20 rekordów po kursorem '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Pobierz 20 rekordów PRZED kursorem '83' (wstecz)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Format kursora

Kursor może być prosty (wartość ID) lub złożony (złożony z kilku pól). Proste kursory to klucz główny rekordu, na przykład autoinkrementowane id lub UUID. Kursory złożone są używane do sortowania po nieunikalnych polach, na przykład (created_at, id), gdzie id gwarantuje unikalność przy identycznych znacznikach czasu.

Typowy format API — kursor w postaci base64-encoded string. Serwer dekoduje kursor, wyodrębnia wartość i buduje zapytanie SQL. Base64-kodowanie ukrywa wewnętrzną strukturę kursora przed klientem i pozwala zmienić format bez utraty kompatybilności wstecznej. Klient otrzymuje kursory w polu endCursor odpowiedzi i przekazuje je jako string w następnym zapytaniu.

Nawigacja do przodu i do tyłu

Stronicowanie kursorem obsługuje nawigację dwukierunkową. Do ruchu do przodu (next) używany jest kursor ostatniego elementu bieżącej strony, do tyłu (previous) — kursor pierwszego elementu. Parametry after i before w zapytaniu określają kierunek: after pobiera rekordy po kursorze, before — przed kursorem.

Cursor vs Offset pagination

Wybór między stronicowaniem kursorem a Offset-pagination to jedno z kluczowych pytań architektonicznych przy projektowaniu API. Każda metoda ma mocne i słabe strony, które określają jej zastosowanie. Cursor-pagination wygrywa w scenariuszach z dynamicznymi danymi, Offset — w scenariuszach z dowolną nawigacją.

CechaCursorOffset
Stabilność przy wstawianiuWysoka (bez duplikatów)Niska (przesunięcie stron)
Wydajność na dużych zbiorachO(log n) — stabilnaO(n) — spada ze wzrostem
Nawigacja po numerze stronyNieTak (page=5)
Złożoność implementacjiŚredniaNiska
Wsparcie w RESTcursor/before/afterpage/offset
Wsparcie w GraphQLStandard RelayNie zalecane

Dlaczego Offset przegrywa na dużą skalę

Offset-pagination wykonuje pełne skanowanie tabeli do pozycji OFFSET. Przy offset=100000 baza danych czyta i pomija 100000 wierszy, nawet jeśli LIMIT wynosi 20. MySQL i PostgreSQL nie potrafią optymalizować OFFSET — to cecha implementacji LIMIT/OFFSET w SQL. Cursor-pagination używa indeksu B-tree, który znajduje pozycję w O(log n).

Dodatkowy problem Offset — „pomijanie” rekordów przy stronicowaniu wstecz. Jeśli użytkownik załadował stronę 5, a w tym momencie zostały dodane nowe rekordy, przy zapytaniu o stronę 6 zobaczy rekord ze strony 5 ponownie lub pominie nowe. Cursor-pagination całkowicie eliminuje ten scenariusz: kursor wskazuje konkretne miejsce w zbiorze, a wstawienia nie zmieniają pozycji.

Implementacja Cursor Pagination

Rozważmy implementację stronicowania kursorem na backendzie (Kotlin + Spring) i na kliencie (Android + Retrofit). Serwer przyjmuje parametry after, before, limit i zwraca listę rekordów z kursorami i pageInfo. Typowa odpowiedź zawiera hasNextPage i hasPreviousPage do zarządzania UI stronicowania.

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
        )
    )
}

Implementacja kliencka na Androidzie

Na kliencie stronicowanie kursorem jest implementowane przez PagingSource z Paging 3, gdzie kluczem jest kursor (Long). PagingSource.load otrzymuje LoadParams.key — kursor ostatniego załadowanego rekordu. LoadResult.Page zwraca dane i nextKey — kursor dla następnej strony. Gdy nextKey = null — stronicowanie zakończone.

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

// PagingSource z kluczem kursora
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)
    }
}

Implementacja GraphQL przez Relay

W GraphQL stronicowanie kursorem jest implementowane przez wzorzec Connection Relay. Każdy typ ma Connection (z pageInfo i edges) oraz Edge (node + cursor). Zapytanie przekazuje parametry first, after, last, before. Serwer zwraca tablicę edges z kursorami i pageInfo z hasNextPage/hasPreviousPage.

Kiedy stosować Cursor Pagination

Cursor-pagination jest zalecana dla API pracujących z dynamicznymi danymi, gdzie rekordy są często dodawane lub usuwane. Klasyczne przykłady: kanał newsów w sieci społecznościowej, wiadomości na czacie, historia transakcji, komentarze do posta. We wszystkich tych scenariuszach ważna jest spójność i brak duplikatów.

  • Czaty i komunikatory — każda nowa wiadomość dodawana jest na górze listy. Offset-pagination zaburza się przy każdej nowej wiadomości.
  • Sieci społecznościowe i kanały — posty publikowane są w sposób ciągły. Stronicowanie kursorem gwarantuje, że użytkownik nie pominie żadnego posta.
  • Historia zamówień i transakcji — dane zmieniają się rzadziej, ale spójność jest krytyczna dla raportowania finansowego.
  • API z dużymi wolumenami danych — miliony rekordów. Cursor-pagination zachowuje wydajność tam, gdzie Offset zaczyna zwalniać.
  • GraphQL API — standard Relay wymaga używania cursor-based pagination zgodnie ze specyfikacją.
  • Aplikacje mobilne z nieskończonym przewijaniem — użytkownik przewija w dół, ładując kolejne porcje. Podejście kursorem zapewnia płynne UX bez duplikatów.

Kiedy Cursor-pagination nie jest odpowiednia

Istnieją scenariusze, w których Offset-pagination jest wygodniejsza: panele administracyjne, gdzie potrzebna jest nawigacja po numerze strony; wyszukiwanie ze stronicowaniem, gdzie wyniki mogą się zmieniać; raporty i analityka, gdzie potrzebny jest stały link do strony 5. W tych przypadkach zalety kursora nie przewyższają złożoności implementacji.

Stronicowanie kursorem nie obsługuje „skoku” do dowolnej strony — użytkownik nie może kliknąć „Strona 5” i przejść do niej. To ograniczenie architektoniczne: do obliczenia całkowitej liczby stron wymagane jest osobne zapytanie COUNT, które może być kosztowne dla dużych tabel. W takich przypadkach hybrydowe podejście: cursor dla danych + count do stronicowania.

Często zadawane pytania

Co to jest kursor w Cursor Pagination?

Kursor to unikalny identyfikator rekordu, który wskazuje pozycję w zbiorze danych. Może być prosty (ID rekordu) lub złożony (kilka pól). Klient otrzymuje kursor ostatniego rekordu strony i przekazuje go w następnym zapytaniu, aby uzyskać kolejną porcję.

Czym Cursor Pagination jest lepsza od Offset?

Cursor-pagination nie podlega przesunięciu przy dodawaniu nowych rekordów — każdy element trafia dokładnie na jedną stronę. Zachowuje również szybkość na dużych wolumenach dzięki użyciu indeksów zamiast skanowania pierwszych n wierszy. Offset jest prostszy, ale niestabilny dla dynamicznych danych.

Czy można zaimplementować stronicowanie kursorem bez GraphQL?

Tak, stronicowanie kursorem nie jest związane z GraphQL. Można je zaimplementować w dowolnym REST API, przekazując kursor jako parametr zapytania ?after=83&limit=20. Odpowiedź powinna zawierać pageInfo z endCursor i hasNextPage — to pozwala klientowi zarządzać ładowaniem bez znajomości wewnętrznej struktury kursora.

Jaki kursor użyć — ID, UUID czy timestamp?

Autoinkrementowane ID — optymalny wybór: monotonicznie wzrasta, nie zmienia się, efektywnie indeksowane. UUID v7 (uporządkowany czasowo) również jest odpowiedni. Timestamp może dawać duplikaty przy identycznym czasie, dlatego łączy się go z ID: (created_at, id) dla gwarancji unikalności kursora.

Jak poznać całkowitą liczbę stron przy stronicowaniu kursorem?

Stronicowanie kursorem nie podaje całkowitej liczby stron — to jego ograniczenie. Jeśli potrzebna jest informacja o total, wykonaj osobne zapytanie COUNT z tymi samymi filtrami. Dla dużych tabel użyj przybliżonego zliczania przez EXPLAIN lub buforowanego total z analityki.

Podsumowanie

  • Cursor Pagination — metoda stronicowania z nawigacją po unikalnym identyfikatorze rekordu zamiast przesunięcia.
  • Kursor gwarantuje stabilność zbioru przy wstawianiu: nowe rekordy nie przesuwają już załadowanych stron.
  • Wydajność na dużych wolumenach danych pozostaje wysoka (O(log n)) dzięki użyciu indeksów B-tree.
  • Cursor-pagination nadaje się do dynamicznych danych: czaty, kanały newsów, transakcje, komentarze.
  • Główne ograniczenie — brak nawigacji po numerze strony i niemożliwość przeskoczenia do dowolnej strony.
  • Implementacja używa WHERE id po kursorze, parametry after/before i pageInfo w odpowiedzi.
  • Standard — Relay Connection GraphQL, ale REST API z parametrami kursora jest również szeroko rozpowszechniony.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również