Cursor Pagination v mobilním vývoji — co to je, princip a implementace

Autor: IT Sectr Publikováno: 2026-03-11 Doba čtení: 9 min

Cursor Pagination — metoda stránkování dat, která používá unikátní kurzor pro navigaci v uspořádané sadě záznamů. Podle GraphQL Specification (2025) je kurzorové stránkování doporučeným standardem pro API pracující s dynamickými daty. Cursor Pagination odstraňuje hlavní nevýhody přístupu Offset: nestabilitu při vkládání a pokles výkonu při velkých posunech.

Hlavní body

  • Cursor Pagination — metoda stránkování, kde každý záznam má unikátní identifikátor-kurzor pro navigaci.
  • Kurzor — unikátní značka pozice v datové sadě (obvykle ID, UUID, timestamp), která se při vkládání nemění.
  • Stabilita — nové záznamy přidané mezi požadavky neposouvají kurzor, čímž eliminují duplicity a mezery.
  • Výkon — dotaz WHERE id > cursor efektivně využívá index, aniž by ztrácel rychlost na velkých sadách.
  • Omezení — kurzorové stránkování nepodporuje navigaci podle čísla stránky (nelze přeskočit na stránku 5).

Co je Cursor Pagination?

Cursor Pagination (kurzorové stránkování) — metoda stránkování, při které server spolu s daty vrací speciální ukazatel — kurzor. Klient použije tento kurzor v dalším požadavku k získání další dávky záznamů. Kurzor je unikátní identifikátor posledního prvku aktuální stránky.

Na rozdíl od Offset stránkování, kde klient říká „dej mi stránku 5 po 20 záznamech”, kurzorové stránkování funguje jinak: „dej mi 20 záznamů po záznamu s ID = 83”. Server provede dotaz s podmínkou WHERE id > 83 a LIMIT 20. Takový přístup zaručuje, že každý záznam skončí přesně na jedné stránce bez ohledu na vkládání.

Koncepce kurzorového stránkování se rozšířila díky specifikaci Relay Connection (GraphQL), která učinila cursor-based pagination standardem pro moderní API. Relay definuje formát odpovědi: edges (pole záznamů s kurzory), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Historie vzniku

Cursor-pagination není nová technika — používala se v databázích dávno před vznikem webu. V SQL se nazývá keyset pagination nebo seek method. Metoda se stala populární v API po zveřejnění specifikace Relay v roce 2015, která formalizovala formát kurzoru jako base64 kódovaný řetězec pro jednotnost přenosu přes HTTP.

Jak funguje kurzorové stránkování

Základní princip kurzorového stránkování — dotaz používá podmínku WHERE na indexovaném poli pro pozicování, nikoli posun. Pro směr vpřed se použije WHERE id > last_id, pro směr vzad — WHERE id < first_id. Index B-tree umožňuje najít první záznam po kurzoru v O(log n), což poskytuje stabilní dobu odezvy.

sql
-- Získat 20 záznamů po kurzoru '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Získat 20 záznamů PŘED kurzorem '83' (zpět)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Formát kurzoru

Kurzor může být jednoduchý (hodnota ID) nebo složený (z několika polí). Jednoduché kurzory jsou primární klíč záznamu, například autoinkrementované id nebo UUID. Složené kurzory se používají pro řazení podle neunikátních polí, například (created_at, id), kde id zaručuje unikátnost při stejných časových značkách.

Typický formát API — kurzor ve formě base64 kódovaného řetězce. Server dekóduje kurzor, extrahuje hodnotu a sestaví SQL dotaz. Base64 kódování skrývá vnitřní strukturu kurzoru před klientem a umožňuje změnit formát bez ztráty zpětné kompatibility. Klient získává kurzory v poli endCursor odpovědi a předává je jako řetězec v dalším požadavku.

Navigace vpřed a vzad

Kurzorové stránkování podporuje obousměrnou navigaci. Pro pohyb vpřed (next) se používá kurzor posledního prvku aktuální stránky, pro vzad (previous) — kurzor prvního prvku. Parametry after a before v požadavku určují směr: after bere záznamy po kurzoru, before — před kurzorem.

Cursor vs Offset pagination

Volba mezi kurzorovým a Offset stránkováním — jedna z klíčových architektonických otázek při navrhování API. Každá metoda má silné a slabé stránky. Cursor-pagination vítězí ve scénářích s dynamickými daty, Offset — ve scénářích s libovolnou navigací.

VlastnostCursorOffset
Stabilita při vkládáníVysoká (bez duplicit)Nízká (posun stránek)
Výkon na velkých sadáchO(log n) — stabilníO(n) — klesá s růstem
Navigace podle čísla stránkyNeAno (page=5)
Složitost implementaceStředníNízká
Podpora v RESTcursor/before/afterpage/offset
Podpora v GraphQLStandard RelayNedoporučuje se

Proč Offset prohrává ve velkém měřítku

Offset stránkování provádí úplné skenování tabulky do pozice OFFSET. Při offset=100000 databáze čte a přeskočí 100000 řádků, i když je LIMIT 20. MySQL a PostgreSQL neumějí OFFSET optimalizovat — to je vlastnost implementace LIMIT/OFFSET v SQL. Cursor-pagination používá index B-tree, který najde pozici v O(log n).

Další problém Offsetu — „přeskakování” záznamů při stránkování zpět. Pokud uživatel načetl stránku 5 a v tu chvíli byly přidány nové záznamy, při požadavku na stránku 6 uvidí záznam ze stránky 5 znovu nebo přeskočí nové. Cursor-pagination tento scénář zcela eliminuje: kurzor ukazuje na konkrétní místo v sadě a vkládání pozici nemění.

Implementace Cursor Pagination

Podívejme se na implementaci kurzorového stránkování na backendu (Kotlin + Spring) a na klientovi (Android + Retrofit). Server přijímá parametry after, before, limit a vrací seznam záznamů s kurzory a pageInfo. Typická odpověď obsahuje hasNextPage a hasPreviousPage pro správu UI stránkování.

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

Klientská implementace na Androidu

Na straně klienta se kurzorové stránkování implementuje přes PagingSource z Paging 3, kde klíčem je kurzor (Long). PagingSource.load obdrží LoadParams.key — kurzor posledního načteného záznamu. LoadResult.Page vrací data a nextKey — kurzor pro další stránku. Když je nextKey = null — stránkování je dokončeno.

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

// PagingSource s klíčem kurzoru
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 implementace přes Relay

V GraphQL se kurzorové stránkování implementuje pomocí vzoru Connection Relay. Každý typ má Connection (s pageInfo a edges) a Edge (node + cursor). Požadavek předává parametry first, after, last, before. Server vrací pole edges s kurzory a pageInfo s hasNextPage/hasPreviousPage.

Kdy použít Cursor Pagination

Cursor-pagination se doporučuje pro API pracující s dynamickými daty, kde se záznamy často přidávají nebo odstraňují. Klasické příklady: zpravodajský kanál v sociální síti, zprávy v chatu, historie transakcí, komentáře k příspěvkům. Ve všech těchto scénářích je důležitá konzistence a absence duplicit.

  • Chaty a messengery — každá nová zpráva se přidává na vrchol seznamu. Offset stránkování je při každé nové zprávě narušen.
  • Sociální sítě a kanály — příspěvky jsou publikovány nepřetržitě. Kurzorové stránkování zaručuje, že uživatel nepropásne žádný příspěvek.
  • Historie objednávek a transakcí — data se mění méně často, ale konzistence je kritická pro finanční výkaznictví.
  • API s velkými objemy dat — miliony záznamů. Cursor-pagination udržuje výkon tam, kde Offset začíná zpomalovat.
  • GraphQL API — standard Relay vyžaduje použití cursor-based pagination podle specifikace.
  • Mobilní aplikace s nekonečným scrollováním — uživatel scrolluje dolů a načítá nové dávky. Kurzorový přístup poskytuje plynulé UX bez duplicit.

Kdy Cursor-pagination není vhodná

Existují scénáře, kde je Offset stránkování pohodlnější: administrativní panely, kde je potřeba navigace podle čísla stránky; vyhledávání se stránkováním, kde se výsledky mohou měnit; zprávy a analytika, kde je potřeba pevný odkaz na stránku 5. V těchto případech výhody kurzoru nepřevyšují složitost implementace.

Kurzorové stránkování nepodporuje „skok” na libovolnou stránku — uživatel nemůže kliknout na „Stránka 5” a přejít na ni. Toto je architektonické omezení: pro výpočet celkového počtu stránek je vyžadován samostatný COUNT dotaz, který může být nákladný pro velké tabulky. V takových případech hybridní přístup: cursor pro data + count pro stránkování.

Často kladené otázky

Co je kurzor v Cursor Pagination?

Kurzor je unikátní identifikátor záznamu, který ukazuje pozici v datové sadě. Může být jednoduchý (ID záznamu) nebo složený (několik polí). Klient obdrží kurzor posledního záznamu stránky a předá ho v dalším požadavku pro získání další dávky.

V čem je Cursor Pagination lepší než Offset?

Cursor-pagination není vystaven posunu při přidávání nových záznamů — každý prvek skončí přesně na jedné stránce. Také udržuje rychlost na velkých objemech díky použití indexů namísto skenování prvních n řádků. Offset je jednodušší, ale nestabilní pro dynamická data.

Lze implementovat kurzorové stránkování bez GraphQL?

Ano, kurzorové stránkování není vázáno na GraphQL. Lze ho implementovat v libovolném REST API předáním kurzoru jako query parametru ?after=83&limit=20. Odpověď by měla obsahovat pageInfo s endCursor a hasNextPage — to umožňuje klientovi spravovat načítání bez znalosti vnitřní struktury kurzoru.

Jaký kurzor použít — ID, UUID nebo timestamp?

Autoinkrementované ID — optimální volba: monotónně roste, nemění se, efektivně se indexuje. UUID v7 (časově uspořádané) je také vhodné. Timestamp může vytvářet duplicity při stejném čase, proto se kombinuje s ID: (created_at, id) pro zaručení unikátnosti kurzoru.

Jak zjistit celkový počet stránek při kurzorovém stránkování?

Kurzorové stránkování neposkytuje celkový počet stránek — to je jeho omezení. Pokud je potřeba informace o total, proveďte samostatný COUNT dotaz se stejnými filtry. Pro velké tabulky použijte přibližné počítání přes EXPLAIN nebo cacheovaný total z analytiky.

Závěr

  • Cursor Pagination — metoda stránkování s navigací podle unikátního identifikátoru záznamu místo posunu.
  • Kurzor zaručuje stabilitu sady při vkládání: nové záznamy neposouvají již načtené stránky.
  • Výkon na velkých objemech dat zůstává vysoký (O(log n)) díky použití B-tree indexů.
  • Cursor-pagination se hodí pro dynamická data: chaty, zpravodajské kanály, transakce, komentáře.
  • Hlavní omezení — absence navigace podle čísla stránky a nemožnost přeskočit na libovolnou stránku.
  • Implementace používá WHERE id po kurzoru, parametry after/before a pageInfo v odpovědi.
  • Standard — Relay Connection GraphQL, ale REST API s kurzorovými parametry je také široce rozšířené.

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také