Cursor Pagination nello sviluppo mobile — cos’è, principio e realizzazione

Autore: IT Sectr Pubblicato: 2026-03-11 Tempo di lettura: 9 min

Cursor Pagination è un metodo di caricamento paginato dei dati che utilizza un cursore univoco per navigare attraverso un insieme ordinato di record. Secondo la Specifica GraphQL (2025), la paginazione basata su cursore è lo standard raccomandato per le API che lavorano con dati dinamici. Cursor pagination elimina i principali svantaggi dell’approccio Offset: instabilità durante le inserzioni e degrado delle prestazioni su grandi offset.

Punti chiave

  • Cursor Pagination è un metodo di paginazione dove ogni record ha un identificatore cursore univoco per la navigazione.
  • Cursore è un marcatore di posizione univoco in un insieme di dati (solitamente ID, UUID, timestamp) che non cambia con le inserzioni.
  • Stabilità — i nuovi record aggiunti tra le richieste non spostano il cursore, eliminando duplicati e lacune.
  • Prestazioni — la query WHERE id > cursor utilizza efficientemente l’indice senza perdere velocità su grandi insiemi di dati.
  • Limitazione — la paginazione tramite cursore non supporta la navigazione per numero di pagina (non si può saltare alla pagina 5).

Cos’è Cursor Pagination?

Cursor Pagination è un metodo di caricamento paginato in cui il server restituisce insieme ai dati un puntatore speciale — un cursore. Il client utilizza questo cursore nella richiesta successiva per ottenere il lotto successivo di record. Il cursore è un identificatore univoco dell’ultimo elemento della pagina corrente.

A differenza della paginazione Offset, dove il client dice “dammi la pagina 5 con 20 record,” la paginazione tramite cursore funziona diversamente: “dammi 20 record dopo il record con ID = 83.” Il server esegue una query con WHERE id > 83 e LIMIT 20. Questo approccio garantisce che ogni record cada esattamente in una pagina indipendentemente dalle inserzioni.

Il concetto di paginazione tramite cursore ha ottenuto ampia adozione grazie alla specifica Relay Connection (GraphQL), che ha reso la paginazione basata su cursore lo standard per le API moderne. Relay definisce il formato di risposta: edges (array di record con cursori), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Storia

La paginazione tramite cursore non è una tecnica nuova — veniva utilizzata nei database molto prima del web. In SQL si chiama keyset pagination o seek method. Il metodo è diventato popolare nelle API dopo la pubblicazione della specifica Relay nel 2015, che ha formalizzato il formato del cursore come stringa codificata in base64 per uniformità sul trasporto HTTP.

Come funziona la paginazione tramite cursore

Il principio base della paginazione tramite cursore è che la query utilizza una condizione WHERE su un campo indicizzato per il posizionamento, non un offset. Per la direzione in avanti si usa WHERE id > last_id; per la direzione indietro, WHERE id < first_id. L’indice B-tree trova il primo record dopo il cursore in O(log n), fornendo un tempo di risposta stabile.

sql
-- Ottieni 20 record dopo il cursore '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Ottieni 20 record PRIMA del cursore '83' (indietro)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Formato del cursore

Un cursore può essere semplice (un valore ID) o complesso (composto da più campi). I cursori semplici sono la chiave primaria di un record, ad esempio, id autoincrementale o UUID. I cursori compositi vengono utilizzati per ordinare per campi non univoci, ad esempio (created_at, id), dove id garantisce l’unicità quando i timestamp sono identici.

Un formato API tipico è un cursore come stringa codificata in base64. Il server decodifica il cursore, estrae il valore e costruisce la query SQL. La codifica Base64 nasconde la struttura interna del cursore al client e consente di modificare il formato senza rompere la compatibilità all’indietro. Il client riceve i cursori nel campo endCursor della risposta e li passa come stringa nella richiesta successiva.

Navigazione avanti e indietro

La paginazione tramite cursore supporta la navigazione bidirezionale. Per il movimento in avanti (next), si usa il cursore dell’ultimo elemento della pagina corrente; per indietro (previous), il cursore del primo elemento. I parametri after e before nella richiesta determinano la direzione: after prende i record dopo il cursore, before prende i record prima del cursore.

Cursor vs Offset Pagination

La scelta tra paginazione tramite cursore e Offset è una delle decisioni architettoniche chiave nella progettazione di un’API. Ogni metodo ha punti di forza e debolezza che determinano la sua applicabilità. Cursor pagination vince negli scenari con dati dinamici; Offset vince negli scenari con navigazione arbitraria.

CaratteristicaCursorOffset
Stabilità nelle inserzioniAlta (nessun duplicato)Bassa (spostamento pagine)
Prestazioni su grandi insiemiO(log n) — stabileO(n) — degrada con la crescita
Navigazione per numero di paginaNoSì (page=5)
Complessità di implementazioneMediaBassa
Supporto RESTcursor/before/afterpage/offset
Supporto GraphQLStandard RelayNon raccomandato

Perché Offset fallisce su larga scala

La paginazione Offset esegue una scansione completa della tabella fino alla posizione OFFSET. Con offset=100000, il database legge e salta 100000 righe, anche se LIMIT è 20. MySQL e PostgreSQL non possono ottimizzare OFFSET — questa è una caratteristica implementativa di LIMIT/OFFSET in SQL. La paginazione tramite cursore utilizza un indice B-tree che trova la posizione in O(log n).

Un problema aggiuntivo di Offset è il “salto” di record durante la paginazione all’indietro. Se un utente ha caricato la pagina 5 e in quel momento sono stati aggiunti nuovi record, quando richiede la pagina 6 vedrà di nuovo il record dalla pagina 5 o perderà quelli nuovi. Cursor pagination elimina completamente questo scenario: il cursore punta a un punto specifico nell’insieme e le inserzioni non cambiano la posizione.

Implementazione di Cursor Pagination

Vediamo l’implementazione della paginazione tramite cursore sul backend (Kotlin + Spring) e sul client (Android + Retrofit). Il server accetta i parametri after, before, limit e restituisce un elenco di record con cursori e pageInfo. Una risposta tipica contiene hasNextPage e hasPreviousPage per gestire l’interfaccia di paginazione.

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

Implementazione lato client su Android

Sul client, la paginazione tramite cursore viene implementata tramite PagingSource di Paging 3, dove la chiave è un cursore (Long). PagingSource.load riceve LoadParams.key — il cursore dell’ultimo record caricato. LoadResult.Page restituisce i dati e nextKey — il cursore per la pagina successiva. Quando nextKey = null, la paginazione è completa.

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

Implementazione GraphQL tramite Relay

In GraphQL, la paginazione tramite cursore viene implementata tramite il pattern Relay Connection. Ogni tipo ha una Connection (con pageInfo e edges) e un Edge (node + cursor). La query passa i parametri first, after, last, before. Il server restituisce un array di edges con cursori e pageInfo con hasNextPage/hasPreviousPage.

Quando usare Cursor Pagination

La paginazione tramite cursore è raccomandata per le API che lavorano con dati dinamici dove i record vengono frequentemente aggiunti o eliminati. Esempi classici: feed di notizie in un social network, messaggi di chat, cronologia transazioni, commenti ai post. In tutti questi scenari, la coerenza e l’assenza di duplicati sono importanti.

  • Chat e messaggeria — ogni nuovo messaggio viene aggiunto in cima all’elenco. La paginazione Offset viene interrotta ad ogni nuovo messaggio.
  • Social network e feed — i post vengono pubblicati continuamente. La paginazione tramite cursore garantisce che l’utente non perda nessun post.
  • Cronologia ordini e transazioni — i dati cambiano meno frequentemente, ma la coerenza è fondamentale per i report finanziari.
  • API con grandi volumi di dati — milioni di record. Cursor pagination mantiene le prestazioni dove Offset inizia a rallentare.
  • API GraphQL — lo standard Relay richiede la paginazione basata su cursore per la conformità alla specifica.
  • App mobili con scroll infinito — l’utente scorre verso il basso caricando nuovi lotti. L’approccio tramite cursore offre un’esperienza utente fluida senza duplicati.

Quando la paginazione tramite cursore non è adatta

Esistono scenari in cui la paginazione Offset è più conveniente: pannelli di amministrazione dove è necessaria la navigazione per numero di pagina; ricerca con paginazione dove i risultati possono cambiare; report e analisi dove è necessario un link fisso alla pagina 5. In questi casi, i vantaggi del cursore non superano la complessità di implementazione.

La paginazione tramite cursore non supporta il “salto” a una pagina arbitraria — l’utente non può cliccare su “Pagina 5” e andarci. Questa è una limitazione architettonica: contare il numero totale di pagine richiede una query COUNT separata, che può essere costosa per tabelle grandi. In questi casi, un approccio ibrido: cursore per i dati + count per la paginazione.

Domande frequenti

Cos’è un cursore in Cursor Pagination?

Un cursore è un identificatore univoco di record che punta a una posizione nell’insieme di dati. Può essere semplice (un ID di record) o composito (più campi). Il client riceve il cursore dell’ultimo record della pagina e lo passa nella richiesta successiva per ottenere il lotto successivo.

Perché Cursor Pagination è migliore di Offset?

Cursor pagination non è soggetto a spostamento quando vengono aggiunti nuovi record — ogni elemento cade esattamente in una pagina. Mantiene inoltre la velocità su grandi volumi utilizzando indici invece di scansionare le prime n righe. Offset è più semplice ma instabile per dati dinamici.

Si può implementare la paginazione tramite cursore senza GraphQL?

Sì, la paginazione tramite cursore non è legata a GraphQL. Può essere implementata in qualsiasi API REST passando il cursore come parametro di query ?after=83&limit=20. La risposta deve contenere pageInfo con endCursor e hasNextPage — questo consente al client di gestire il caricamento senza conoscere la struttura interna del cursore.

Quale cursore usare — ID, UUID o timestamp?

ID autoincrementale è la scelta ottimale: aumenta monotonamente, non cambia, viene indicizzato efficientemente. Anche UUID v7 (ordinato per tempo) funziona. I timestamp possono produrre duplicati allo stesso momento, quindi combinalo con ID: (created_at, id) per garantire l’unicità del cursore.

Come ottenere il numero totale di pagine con la paginazione tramite cursore?

La paginazione tramite cursore non fornisce il numero totale di pagine — questa è la sua limitazione. Se hai bisogno di informazioni totali, esegui una query COUNT separata con gli stessi filtri. Per tabelle grandi, usa un conteggio approssimativo tramite EXPLAIN o un totale memorizzato nella cache dalle analisi.

Riepilogo

  • Cursor Pagination è un metodo di paginazione con navigazione tramite identificatore univoco di record invece di offset.
  • Il cursore garantisce la stabilità dell’insieme nelle inserzioni: i nuovi record non spostano le pagine già caricate.
  • Le prestazioni su grandi volumi di dati rimangono elevate (O(log n)) grazie all’uso dell’indice B-tree.
  • Cursor pagination è adatto per dati dinamici: chat, feed di notizie, transazioni, commenti.
  • La limitazione principale è l’assenza di navigazione per numero di pagina e l’impossibilità di saltare a una pagina arbitraria.
  • L’implementazione utilizza WHERE id dopo il cursore, i parametri after/before e pageInfo nella risposta.
  • Standard — Relay Connection GraphQL, ma le API REST con parametri cursore sono anche ampiamente utilizzate.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche