Cursor Pagination i mobilutveckling — vad är det, princip och implementering

Författare: IT Sectr Publicerad: 2026-03-11 Lästid: 9 min

Cursor Pagination — metod för datapaginering som använder en unik markör för navigering genom en ordnad uppsättning poster. Enligt GraphQL Specification (2025) är markörpaginering den rekommenderade standarden för API:er som arbetar med dynamisk data. Cursor Pagination eliminerar de största nackdelarna med Offset-metoden: instabilitet vid insättningar och prestandaförlust vid stora förskjutningar.

Huvudpunkter

  • Cursor Pagination — pagineringsmetod där varje post har en unik identifierare-markör för navigering.
  • Markör — unik positionsmarkör i datamängden (vanligtvis ID, UUID, timestamp), som inte ändras vid insättningar.
  • Stabilitet — nya poster som läggs till mellan förfrågningar flyttar inte markören, vilket eliminerar dubbletter och luckor.
  • Prestanda — frågan WHERE id > cursor använder indexet effektivt utan att förlora hastighet på stora mängder.
  • Begränsning — markörpaginering stöder inte navigering efter sidnummer (man kan inte hoppa till sidan 5).

Vad är Cursor Pagination?

Cursor Pagination (markörpaginering) — pagineringsmetod där servern tillsammans med data returnerar en speciell pekare — markör. Klienten använder denna markör i nästa förfrågan för att hämta nästa omgång poster. Markören är den unika identifieraren för det sista elementet på den aktuella sidan.

Till skillnad från Offset-paginering, där klienten säger “ge mig sidan 5 med 20 poster”, fungerar markörpaginering annorlunda: “ge mig 20 poster efter posten med ID = 83”. Servern utför frågan med villkoret WHERE id > 83 och LIMIT 20. Detta tillvägagångssätt garanterar att varje post hamnar exakt på en sida oavsett insättningar.

Konceptet med markörpaginering har blivit utbrett tack vare specifikationen Relay Connection (GraphQL), som gjorde cursor-based pagination till standard för moderna API:er. Relay definierar svarsformatet: edges (array av poster med markörer), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Historia

Cursor-pagination är inte en ny teknik — den användes i databaser långt innan webben uppstod. I SQL kallas det keyset pagination eller seek method. Metoden blev populär i API:er efter publiceringen av Relay-specifikationen 2015, som formaliserade markörformatet som en base64-kodad sträng för enhetlig överföring via HTTP.

Hur markörpaginering fungerar

Grundprincipen för markörpaginering — frågan använder WHERE-villkor på ett indexerat fält för positionering, inte förskjutning. För framåtriktning används WHERE id > last_id, för bakåtriktning — WHERE id < first_id. B-tree-indexet gör det möjligt att hitta den första posten efter markören på O(log n) tid, vilket ger stabil svarstid.

sql
-- Hämta 20 poster efter markören '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Hämta 20 poster FÖRE markören '83' (bakåt)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Markörformat

Markören kan vara enkel (ID-värde) eller komplex (sammansatt av flera fält). Enkla markörer är postens primärnyckel, till exempel autoinkrement id eller UUID. Komplexa markörer används för sortering efter icke-unika fält, till exempel (created_at, id), där id garanterar unikhet vid identiska tidsstämplar.

Typiskt API-format — markör som en base64-kodad sträng. Servern avkodar markören, extraherar värdet och bygger SQL-frågan. Base64-kodning döljer markörens interna struktur för klienten och gör det möjligt att ändra format utan att förlora bakåtkompatibilitet. Klienten får markörer i svarets endCursor-fält och skickar dem som en sträng i nästa förfrågan.

Navigering framåt och bakåt

Markörpaginering stöder tvåvägsnavigering. För framåtrörelse (next) används markören för det sista elementet på den aktuella sidan, för bakåtrörelse (previous) — markören för det första elementet. Parametrarna after och before i förfrågan bestämmer riktningen: after hämtar poster efter markören, before — före markören.

Cursor vs Offset pagination

Valet mellan markör- och Offset-paginering — en av de viktigaste arkitektoniska frågorna vid design av API:er. Varje metod har styrkor och svagheter. Cursor-pagination vinner i scenarier med dynamisk data, Offset — i scenarier med godtycklig navigering.

EgenskapCursorOffset
Stabilitet vid insättningHög (inga dubbletter)Låg (siddförskjutning)
Prestanda på stora mängderO(log n) — stabilO(n) — minskar med tillväxt
Navigering efter sidnummerNejJa (page=5)
ImplementeringskomplexitetMedelLåg
Stöd i RESTcursor/before/afterpage/offset
Stöd i GraphQLRelay-standardRekommenderas inte

Varför Offset förlorar i stor skala

Offset-paginering utför fullständig tabellsökning till OFFSET-positionen. Vid offset=100000 läser databasen och hoppar över 100000 rader, även om LIMIT är 20. MySQL och PostgreSQL kan inte optimera OFFSET — detta är en egenskap i LIMIT/OFFSET-implementeringen i SQL. Cursor-pagination använder B-tree-indexet, som hittar positionen på O(log n) tid.

Ytterligare ett problem med Offset — “överhoppning” av poster vid bakåtpaginering. Om användaren laddade sidan 5 och nya poster lades till i det ögonblicket, kommer hen vid begäran av sidan 6 att se posten från sidan 5 igen eller missa nya. Cursor-pagination eliminerar helt detta scenario: markören pekar på en specifik plats i mängden och insättningar ändrar inte positionen.

Implementering av Cursor Pagination

Låt oss titta på implementeringen av markörpaginering på backend (Kotlin + Spring) och på klienten (Android + Retrofit). Servern tar emot parametrarna after, before, limit och returnerar en lista med poster med markörer och pageInfo. Ett typiskt svar innehåller hasNextPage och hasPreviousPage för hantering av pagineringsgränssnittet.

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

Klientimplementering på Android

På klientsidan implementeras markörpaginering via PagingSource från Paging 3, där nyckeln är markören (Long). PagingSource.load får LoadParams.key — markören för den senast laddade posten. LoadResult.Page returnerar data och nextKey — markör för nästa sida. När nextKey = null — är pagineringen slutförd.

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

// PagingSource med markörnyckel
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-implementering via Relay

I GraphQL implementeras markörpaginering via Relay Connection-mönstret. Varje typ har Connection (med pageInfo och edges) och Edge (node + cursor). Förfrågan skickar parametrarna first, after, last, before. Servern returnerar en array av edges med markörer och pageInfo med hasNextPage/hasPreviousPage.

När ska Cursor Pagination användas

Cursor-pagination rekommenderas för API:er som arbetar med dynamisk data, där poster ofta läggs till eller tas bort. Klassiska exempel: nyhetsflöde i sociala nätverk, chattmeddelanden, transaktionshistorik, kommentarer på inlägg. I alla dessa scenarier är konsistens och frånvaro av dubbletter viktigt.

  • Chattar och meddelandetjänster — varje nytt meddelande läggs till överst i listan. Offset-paginering störs vid varje nytt meddelande.
  • Sociala nätverk och flöden — inlägg publiceras kontinuerligt. Markörpaginering garanterar att användaren inte missar något inlägg.
  • Order- och transaktionshistorik — data ändras mer sällan, men konsistens är kritisk för finansiell rapportering.
  • API:er med stora datavolymer — miljontals poster. Cursor-pagination bibehåller prestanda där Offset börjar sakta ner.
  • GraphQL API — Relay-standarden kräver användning av cursor-based pagination enligt specifikationen.
  • Mobilappar med oändlig scrollning — användaren scrollar neråt och laddar nya omgångar. Markörmetoden ger en smidig användarupplevelse utan dubbletter.

När Cursor-pagination inte är lämplig

Det finns scenarier där Offset-paginering är bekvämare: administrativa paneler där navigering efter sidnummer behövs; sökning med paginering där resultat kan ändras; rapporter och analys där en fast länk till sidan 5 behövs. I dessa fall väger markörens fördelar inte upp implementeringskomplexiteten.

Markörpaginering stöder inte “hopp” till en godtycklig sida — användaren kan inte klicka på “Sida 5” och komma dit. Detta är en arkitektonisk begränsning: för att beräkna det totala antalet sidor krävs en separat COUNT-fråga, som kan vara dyr för stora tabeller. I sådana fall en hybridmetod: cursor för data + count för paginering.

Vanliga frågor

Vad är en markör i Cursor Pagination?

En markör är unik identifierare för en post som anger positionen i datamängden. Den kan vara enkel (postens ID) eller komplex (flera fält). Klienten får markören för sidans sista post och skickar den i nästa förfrågan för att få nästa omgång.

Vad är bättre med Cursor Pagination än Offset?

Cursor-pagination påverkas inte av förskjutning när nya poster läggs till — varje element hamnar exakt på en sida. Den behåller också hastigheten på stora volymer tack vare användning av index istället för att skanna de första n raderna. Offset är enklare men instabil för dynamisk data.

Kan markörpaginering implementeras utan GraphQL?

Ja, markörpaginering är inte bunden till GraphQL. Den kan implementeras i vilket REST API som helst genom att skicka markören som en frågeparameter ?after=83&limit=20. Svaret bör innehålla pageInfo med endCursor och hasNextPage — detta gör det möjligt för klienten att hantera laddningen utan kännedom om markörens interna struktur.

Vilken markör ska man använda — ID, UUID eller timestamp?

Autoinkrement ID — optimalt val: ökar monotont, ändras inte, indexeras effektivt. UUID v7 (tidsordnad) är också lämplig. Timestamp kan ge dubbletter vid samma tid, därför kombineras den med ID: (created_at, id) för att garantera markörens unikhet.

Hur får man reda på det totala antalet sidor vid markörpaginering?

Markörpaginering ger inte det totala antalet sidor — detta är dess begränsning. Om information om total behövs, utför en separat COUNT-fråga med samma filter. För stora tabeller, använd ungefärlig räkning via EXPLAIN eller cachad total från analys.

Sammanfattning

  • Cursor Pagination — pagineringsmetod med navigering efter postens unika identifierare istället för förskjutning.
  • Markör garanterar stabilitet vid insättning: nya poster flyttar inte redan laddade sidor.
  • Prestanda på stora datavolymer förblir hög (O(log n)) tack vare användning av B-tree-index.
  • Cursor-pagination är lämplig för dynamisk data: chattar, nyhetsflöden, transaktioner, kommentarer.
  • Huvudsaklig begränsning — ingen navigering efter sidnummer och omöjlighet att hoppa till godtycklig sida.
  • Implementering använder WHERE id efter markör, parametrar after/before och pageInfo i svaret.
  • Standard — Relay Connection GraphQL, men REST API med markörparametrar är också utbrett.

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också