Offset Pagination — paginazione con offset — metodo di caricamento paginato dei dati tramite HTTP API. Il cliente invia i parametri offset (spostamento dall'inizio) e limit (dimensione della pagina), e il server restituisce i record a partire dalla posizione offset. Secondo il REST API Tutorial, questo approccio è ampiamente utilizzato nei servizi RESTful grazie alla semplicità di implementazione. Tuttavia, su grandi volumi di dati, la paginazione offset perde prestazioni a causa della scansione completa della tabella fino alla posizione desiderata.
Punti chiave
Offset Pagination è un metodo di paginazione dei dati in cui la richiesta del client contiene due parametri: offset (quanti record saltare) e limit (quanti record restituire). Il server esegue una query SQL con OFFSET e LIMIT, salta il numero specificato di righe e restituisce un set di risultati di dimensione fissa.
Il metodo è nato nei database relazionali come il modo più semplice per organizzare la navigazione tra le pagine ed è stato trasferito alle API HTTP con lo sviluppo dell'architettura REST. Offset Pagination non richiede memorizzazione dello stato sul server — ogni richiesta è indipendente e contiene tutte le informazioni necessarie per la query.
Secondo il rapporto di progettazione API di Postman (2025), la paginazione offset è utilizzata nel 72% delle API REST pubbliche, rendendola lo standard dominante nonostante le note limitazioni delle prestazioni su grandi insiemi di dati.
Una tipica richiesta REST con Offset Pagination include i parametri di query offset e limit. La risposta contiene l'elenco dei record della pagina richiesta e metadati per costruire l'interfaccia di navigazione.
Il parametro limit limita il numero di record restituiti e protegge il server e il client da un carico eccessivo. I valori tipici di limit vanno da 10 a 50 record per pagina a seconda della complessità dei dati.
data class PageRequest(
val offset: Int,
val limit: Int
)
data class PageResponse<T>(
val items: List<T>,
val total: Int,
val hasMore: Boolean
)
fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>
Offset Pagination si traduce in una query SQL con i costrutti OFFSET e FETCH NEXT (o LIMIT in MySQL/SQLite). Il server del database scansiona la tabella, salta il numero di righe pari all'offset e restituisce le successive limit righe. Maggiore è l'offset, più lunga è la query.
Il problema delle prestazioni deriva dal fatto che il database non può saltare direttamente alla posizione dell'offset — deve leggere e scartare tutte le righe precedenti. Con offset = 100000 e limit = 20, il DBMS legge 100.020 righe e ne restituisce solo 20.
SQL — il linguaggio in cui il server esegue la paginazione offset. PostgreSQL e MySQL usano LIMIT, mentre SQL Server e Oracle usano OFFSET...FETCH. Diversi DBMS ottimizzano questa query in modo diverso, ma il problema fondamentale della scansione rimane lo stesso.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
La consistenza dei dati è il principale svantaggio di Offset Pagination quando si lavora con insiemi dinamici. Se un nuovo record viene aggiunto all'inizio della tabella tra due richieste utente, tutti i record esistenti si spostano. L'utente vede duplicati o lacune.
Consideriamo una tabella di 100 record con limit = 20. Sulla pagina 1, l'utente vede i record 1-20. Un amministratore aggiunge 5 nuovi record. Sulla pagina 2, l'utente vede i record 26-45 invece degli attesi 21-40 — i record 21-25 vengono saltati e i record 21-25 dell'insieme precedente vengono duplicati sulla pagina 1.
La paginazione Cursor-based è un'alternativa a Offset Pagination che utilizza un puntatore all'ultimo record della pagina corrente. Invece di un offset numerico, il client invia l'identificatore dell'ultimo record ricevuto e il server restituisce i successivi N record dopo di esso.
L'approccio cursor-based risolve il problema di consistenza: la posizione del cursore non cambia con inserimenti o eliminazioni perché il cursore si riferisce a un record specifico, non a una posizione. Tuttavia, è più complesso da implementare — richiede un campo unico ordinabile (di solito ID o timestamp).
| Parametro | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Semplicità | Alta — due parametri numerici | Media — richiede codifica del cursore |
| Consistenza | Bassa — duplicati negli inserimenti | Alta — cursore non influenzato dalle modifiche |
| Prestazioni | Degrada con offset grande | Stabili a qualsiasi volume |
| Salto alla pagina | Sì — può navigare a qualsiasi pagina | No — solo navigazione sequenziale |
| Ideale per | Tabelle <10K record, UI con numeri di pagina | Feed, scroll infinito, grandi insiemi |
La scelta tra gli approcci dipende dai requisiti dell'interfaccia utente. Se è necessaria una navigazione con numeri di pagina e salto diretto — Offset Pagination è più semplice. Per lo scroll infinito o i feed di notizie, i cursori sono preferibili.
Keyset pagination è una variante dell'approccio cursor-based in cui il filtraggio viene effettuato su una chiave univoca utilizzando WHERE invece di OFFSET. La query SQL utilizza una condizione come WHERE id > lastId, consentendo al database di utilizzare un indice senza scansionare le righe scartate.
Secondo il Wiki di PostgreSQL, keyset pagination viene eseguita 100-1000 volte più velocemente delle query offset a grandi offset perché la scansione dell'indice sostituisce la scansione completa della tabella. Lo svantaggio è l'impossibilità di saltare a una pagina arbitraria senza scansione sequenziale.
Offset Pagination è ottimale per insiemi di dati piccoli e medi (fino a 10.000 record) dove l'utente necessita di un'interfaccia con numeri di pagina. Gli scenari tipici includono pannelli di amministrazione, elenchi di ordini e cataloghi filtrati con paginazione per pagine.
Per le applicazioni mobili, la paginazione offset è adatta quando si caricano dati storici dove nuovi inserimenti sono rari o impossibili — ad esempio, cronologia ordini utente, elenchi di attività completate o archivi di transazioni. In questi scenari, il problema di consistenza non si presenta.
Sconsigliata per feed di social media, elenchi di commenti, chat e altri insiemi dinamici con inserimenti frequenti. In questi casi, lacune e record duplicati degradano l'esperienza utente e richiedono logica aggiuntiva di deduplicazione sul client.
La paginazione ibrida combina offset e cursore: la prima richiesta utilizza offset per mostrare la pagina iniziale, mentre le richieste successive utilizzano il cursore per il caricamento a scorrimento infinito. Questo approccio è utilizzato su Instagram e Twitter, dove la prima pagina viene caricata tramite cursore, ma l'offset viene utilizzato per calcolare la posizione quando si torna a una vista precedente.
Implementare un approccio ibrido richiede la memorizzazione della posizione virtuale dell'utente sul client e il coordinamento di due meccanismi di paginazione sul server. Secondo il blog di Instagram Engineering, il loro team utilizza la paginazione cursor-based con un campo aggiuntivo startCursor che sostituisce l'offset per il caricamento iniziale.
Le applicazioni mobili utilizzano Offset Pagination insieme a Retrofit/OkHttp su Android e URLSession/Combine su iOS. Il pattern tipico è il caricamento della pagina successiva quando si scorre fino alla fine dell'elenco tramite RecyclerView.OnScrollListener o il prefetching di UICollectionView.
L'implementazione della paginazione offset su un client mobile include tre componenti: un gestore di paginazione (memorizza l'offset corrente e hasMore), un adattatore di elenco (mostra gli elementi e l'indicatore di caricamento) e un repository (esegue le richieste e gestisce gli errori). Android Jetpack offre la libreria Paging 3, che supporta sia la paginazione offset che cursor-based pronta all'uso.
Paging 3 è una libreria Android Jetpack per il caricamento paginato dei dati. Incapsula la logica di paginazione, inclusi il tracciamento dell'offset, la gestione dello stato di caricamento e il precaricamento automatico durante lo scorrimento. PagingSource definisce le chiavi per le pagine successiva e precedente.
class OffsetPagingSource(
private val api: ApiService,
private val limit: Int = 20
) : PagingSource<Int, Item>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Item> {
val offset = params.key ?: 0
return try {
val response = api.getItems(offset, limit)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = if (response.hasMore) offset + limit else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource definisce le chiavi prevKey e nextKey per la navigazione tra le pagine. Con la paginazione offset, prevKey è sempre null (non è possibile andare alla pagina precedente senza salvare la cronologia), mentre nextKey aumenta di limit a ogni caricamento finché il server non restituisce hasMore = false. Questo è un modello semplice e prevedibile per gli elenchi mobili.
Primo errore — affidarsi all'ordine dei record senza ordinamento. Offset Pagination richiede un ordinamento ORDER BY stabile su un campo univoco. Senza di esso, il DBMS può restituire i record in ordine arbitrario, causando duplicati e lacune casuali tra le pagine.
Secondo errore — utilizzare l'offset per calcolare il numero di pagina nell'interfaccia. La formula page = offset / limit + 1 funziona solo se nessun record è stato eliminato o aggiunto tra i caricamenti. Con dati dinamici, il numero di pagina diventa impreciso e l'utente vede informazioni errate.
Terzo errore — ignorare i timeout delle query con offset grande. Con offset superiore a 100.000, la query può richiedere decine di secondi, bloccando l'interfaccia e consumando risorse del server. Si consiglia di impostare un valore massimo di offset a livello di API (ad esempio, 10.000) e utilizzare la paginazione cursor-based per grandi volumi.
Quarto errore — non includere il total count nella risposta. Senza il numero totale di record, il client non può visualizzare il conteggio delle pagine né implementare la paginazione numerata. Tuttavia, COUNT(*) su tabelle grandi è costoso — per insiemi con più di 100.000 record, utilizzare stime approssimative o limitare il valore massimo del totale.
Domande frequenti
Offset utilizza uno spostamento numerico per saltare i record, mentre cursor utilizza un puntatore all'ultimo record della pagina precedente. Offset è più semplice da implementare ma soffre di duplicati negli inserimenti e perdita di prestazioni a grandi offset. Il cursore è stabile indipendentemente dalle modifiche ai dati.
La paginazione offset è inefficiente con offset superiori a 10.000 record a causa della scansione completa della tabella. È inoltre inadatta per insiemi dinamici (feed, chat) dove nuovi record appaiono tra le richieste — l'utente vede lacune e record duplicati durante la navigazione.
Il limit ottimale dipende dalla dimensione del record e dalla velocità della rete — da 10 a 50 elementi per pagina. Per elenchi con immagini grandi, utilizzare limit = 10-15; per dati testuali, 20-50. Consentire sempre al client di specificare il proprio limit con un massimo lato server (di solito 100).
Per gestire i duplicati, utilizzare la deduplicazione lato client per ID univoco, applicare un ordinamento stabile su un campo univoco o passare alla paginazione cursor-based. Android Paging 3 supporta la key per la deduplicazione automatica degli elementi dell'elenco.
Sì, GraphQL supporta la paginazione offset tramite gli argomenti offset e limit nella query, sebbene la specifica Relay raccomandi l'approccio cursor-based. Le librerie Apollo GraphQL e Relay offrono supporto integrato per la paginazione offset con gestione automatica dello stato delle pagine.
Riepilogo
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.
Leggi anche