Paginazione e puntatori
1) Perché la paginazione
La paginazione limita la quantità di dati trasmessi e resi dal cliente, riduce il carico di storage/rete e definisce un metodo definito per «passeggiare» nella raccolta. Nei sistemi reali, la paginazione non è solo «page = 1 & limit = 50», ma una serie di contratti protocollari e invarianti di coerenza.
Obiettivi tipici:- Controllo della latitanza e della memoria per la richiesta.
- Navigazione stabile quando il set di dati (incolla/elimina) viene modificato.
- Possibilità di riprendere dalla posizione.
- Disconnessione e preordine (prefetch).
- Protezione dagli abusi (rate limiting, backpressure).
2) Modelli di paginazione
2. 1 OFFSET/LIMIT (pagina)
«Salta le righe N, restituisci la M».
I vantaggi sono semplici, compatibili con quasi tutte le SQL/NoSQL.
- Degrado lineare: i grandi OFFSET conducono alla scansione completa/skip-cost.
- Instabilità durante l'inserimento o l'eliminazione tra le query (offset fluttuanti).
- È difficile garantire una rinnovabile precisa.
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;
2. 2 Cursor/Keyset/Seek-paginazione
«Continua con la chiave K». Il cursore è una posizione in un set ordinato.
Vantaggi:- O (1) accesso al proseguimento in presenza di un indice.
- Stabilità delle modifiche alla raccolta.
- La miglior latitanza delle pagine profonde.
- Servono chiavi di ordinamento rigorosamente definite, uniche e monouso.
- Più difficile da realizzare e debugger.
sql
-- Resumption after steam (created_at, id) = (:last_ts,:last_id)
SELECT
FROM orders
WHERE (created_at, id) < (:last_ts,:last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;
2. 3 Continuation tokens (token opachi)
Idea: il server restituisce l'opache-token in cui è codificata la posizione (e forse lo stato dei chard/filtri). Il cliente non capisce le interiorità e restituisce il token per la pagina successiva.
I vantaggi sono la flessibilità, la possibilità di cambiare schema senza l'astinenza dell'API.
Contro: gestione della durata dei token, compatibilità con i depositi.
2. 4 Cursori temporanei e logici
Time-based: «tutti i record fino a T», il cursore è un'etichetta temporale (appropriata per i flussi append-only).
Log-sequence/offset-based - Puntatore - Offset (Kafka offset, journal seq).
Global monotonic IDs: Snowflake/UUIDv7 come chiave ordinabile per seek stabile.
3) Progettazione di corsi e token
3. 1 Proprietà buon cursore
Opacità - Il client non dipende dal formato.
Copyright/integrità: firma HMAC per impedire la sostituzione/manipolazione.
Il contesto include ordinamento, filtri, versione dello schema, tenant/shard.
Durata della vita: TTL e Non-Replay quando cambiano indice/privilegi di accesso.
Dimensioni compatte (<= 1-2 KB) adatte all'URL.
3. 2 Formato token
La pila consigliata è la compressione JSON (zstd/deflate) da Base64URL a HMAC.
Struttura del carico utile (esempio):json
{
"v": 3 ,//token version
"sort": ["created_at:desc","id:desc"],
"pos": {"created_at":"2025-10-30T12:34:56. 789Z","id":"987654321"},
"filters": {"user_id":"42","status":["paid","shipped"]},
"tenant": "eu-west-1",
"shards": [{"s ": "a, "" hi":"..."}] ,//optional: cross-shard context
"issued_at": "2025-10-31T14:00:00Z",
"ttl_sec": 3600
}
In alto viene aggiunto «mac = HMAC (secret, payload)» e tutto viene codificato in un tocco di stringa.
3. 3 Sicurezza
Firma (HMAC/SHA-256).
Crittografia opzionale (AES-GCM) in presenza di valori sensibili (PII).
Convalida sul server: versione, TTL, autorizzazioni utente (RBAC/ABAC).
4) Coerenza e invarianti
4. 1 Ordinamento stabile
Usa il determinismo completo: 'ORDER BY ts DESC, id DESC'.
La chiave di ordinamento deve essere univoca (aggiungi «id» come tiebreaker).
L'indice deve corrispondere all'ordinamento (covering index).
4. 2 Snapshot e isolamento
Per le pagine «inattive», utilizzare il read-consistent snapshot (MVCC/txid).
Se non è appropriato (costoso/pieno di dati), formulare il contratto: «Il cursore restituisce gli elementi, strettamente precedentemente posizioni». È naturale per i notiziari.
4. 3 Inserisci/rimuovi tra le pagine
Il modello seek minimizza i «duplicati/omessi».
Documentare il comportamento durante la rimozione/modifica: sono consentiti rari buchi tra le pagine, ma non indietro nel tempo.
5) Indicizzazione e schemi di identificatori
Gli indici compositi sono rigorosamente in ordine di ordinamento: '(created _ at DESC, id DESC)'.
ID monotonici: Snowflake/UUIDv7 dà ordine di tempo per accelerare il seek.
Chiavi hot: distribuisci shard-key (ad esempio tenant _ id, region) e ordina all'interno dello shard.
Generatori ID: evitare collusioni e «orologi dal futuro» - sincronizzazione del tempo, «regressione» nei salti NTP.
6) Paginazione cross-shard
6. 1 Schemi
Scatter-Gather - Richieste parallele a tutti gli shard, corsi di seek locali, quindi k-way merge sull'aggregatore.
Per-Shard Cursors - Il token contiene posizioni per ogni shard.
Bounded fan-out - Limitare il numero di sardi per passo (rate limiting/timeout budget et).
6. 2 Token per multi-shard
Memorizza l'array «{shard _ id, last _ pos}». Al passo successivo, riprendete per ogni shard attivo e mentite di nuovo, dando una pagina ordinata globalmente.
7) Contratti di protocollo
7. 1 REST
Richiesta:
GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
Risposta:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
Raccomandazioni:
- «limit» con limite superiore (ad esempio max = 200).
- «next _ cursor» non esiste se «ha _ more = false».
- Idampotenza GET, cache delle risposte senza «next _ cursor» (prima pagina per filtri fissi e snapshot).
7. 2 GraphQL (approccio Relay)
Tipico contratto «connection»:graphql type Query {
orders(first: Int, after: String, filter: OrderFilter): OrderConnection!
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
type OrderEdge {
node: Order!
cursor: String! // opaque
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
cursor deve essere opaco e firmato. non utilizzare il crudo Base64 (id) senza HMAC.
7. 3 gRPC
Usa «page _ size» e «page _ token»:proto message ListOrdersRequest {
string filter = 1;
int32 page_size = 2;
string page_token = 3; // opaque
}
message ListOrdersResponse {
repeated Order items = 1;
string next_page_token = 2; // opaque bool has_more = 3;
}
7. 4 Flussi e WebSockets
Per i nastri continui, il cursore è come «ultimo offset/ts visto».
Supporta «resume _ from» durante la riconnessione:json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }
8) Cache, pre-caricamento, CDN
ETAG/If-None-Match per la prima pagina con filtri stabili.
Cache-Control con TTL breve (ad esempio 5-30 c) per gli elenchi pubblici.
Prefetch: restituisci «next _ cursor» e suggerimenti («Link: rel =» next «») e il client può predisporre la pagina seguente.
Variazioni: considerate'filter/fort/locale/tenante'come chiave della parte della cache.
9) Controllo del carico e limitazione
Limite superiore, ad esempio 200.
Server-side backpressure: se il tempo di query> budget, ridurre il'limit'nella risposta (e chiaramente comunicare al cliente le dimensioni effettive della pagina).
Rate limits per utente/token/tenant.
Timeout/Retry: pausa esponenziale, ripetizioni idipotenti.
10) Aspetti UX
Scroll contro numerazione: scorrimento infinito dei cursori; pagine di targa offset (ma spieghi l'imprecisione durante l'aggiornamento dei dati).
Pulsante Ritorna alla posizione - Memorizza lo stack dei cursori del client.
Pagine vuote: se «ha _ more = false», non mostrare il pulsante «ancora».
Limiti stabili: mostra «total» preciso solo se è economico (altrimenti è approssimativamente «approx _ total»).
11) Test e valigette edge
Assegno fogli:- Ordinamento stabile: gli elementi con lo stesso «ts» non «lampeggiano».
- Inserisci/rimuovi - Non vengono visualizzate ripetizioni nel giunto delle pagine.
- Modifica dei filtri tra le pagine: il token deve essere rifiutato comè obsoleto/incompatibile ".
- TTL token: errore corretto alla scadenza.
- Grande profondità, la latitanza non cresce lineare.
- Ordine merge corretto, assenza di starvation «lenti».
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch
12) Osservabilità e SLO
Metriche:- 'list _ sollest _ latency _ ms' (P50/P95/P99) in base alla lunghezza della pagina.
- 'seek _ index _ hit _ ratio' (percentuale di richieste che sono uscite dall'indice di copertura).
- 'next _ cursor _ invalid _ rate' (errori di convalida/TTL/firme).
- «merge _ fanout» (le schede coinvolte alla pagina).
- «duplicates _ on _ boundary» e «gaps _ on _ boundary».
- Correlare «cursor _ id» nei logi, mascherare payload.
- Tag span: 'page _ size', 'source _ shards', 'db _ index _ used'.
- Disponibilità: 99. 9% su «List».
- Latitudine: P95 <200 ms per'page _ size <= 50 'con sciarpa locale.
- Errore del token: <0. 1% del totale delle chiamate.
13) Migrazioni e compatibilità
Abilita «v» nel token e supporta le versioni precedenti N settimane.
Se cambi le chiavi di ordinamento, invia l'errore «morbido» «409 Conflict» con il suggerimento di eseguire il listino fresco senza il cursore.
Caso catastrofico (ruota di tutti i token): cambia «signing _ key _ id» e rifiuta i vecchi.
14) Esempi di implementazione
14. 1 Generazione di token (pseudocode)
python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)
14. 2 Validazione token
python raw = base64url_decode(token)
mac, compressed = raw[:32], raw[32:]
assert mac == hmac_sha256(signing_key, compressed)
payload = json. loads(zlib. decompress(compressed))
assert now() - payload["issued_at"] < payload["ttl_sec"]
assert payload["filters"] == req. filters
14. 3 Seek query con chiave composita
sql
-- Page # 1
SELECT FROM feed
WHERE tenant_id =:t
ORDER BY ts DESC, id DESC
LIMIT:limit;
-- Next pages, continued after (ts0, id0)
SELECT FROM feed
WHERE tenant_id =:t
AND (ts <:ts0 OR (ts =:ts0 AND id <:id0))
ORDER BY ts DESC, id DESC
LIMIT:limit;
15) Sicurezza e conformità
Non includere nei token i campi crudi da cui è possibile estrarre il PII.
Firmare e limitare la TTL.
Cerca di rendere i token intolleranti tra gli utenti (inserisci «sub/tenant/roles» in payload e incrocia durante la validazione).
Logica solo gli hashtag dei token.
16) Errori frequenti e anti-pattern
Base64 (id) come cursore: facile da contraffazione/selezione, rompe il contratto quando cambia ordinamento.
Nessun tie-breaker: «ORDER BY ts DESC» senza «id» è duplicato/salto.
Cambia i filtri tra le pagine senza disabilità del token.
OFFSET profondo: lento e imprevedibile.
Token senza versione e TTL.
17) Mini-listino di implementazione
1. Definire l'ordinamento e aggiungere un tie-breaker univoco.
2. Creare un indice di copertura sotto questo ordine.
3. Selezionate seek + token opaco.
4. Firmare (e crittografare, se necessario) il token.
5. Piazzate TTL e versioning.
6. Formare e documentare i contratti «ha _ more», «next _ cursor».
7. Elaborare lo schema cross-shard (se necessario) e k-way merge.
8. Aggiungete metriche, alert e SLO.
9. Coprire con test property-based i bordi delle pagine.
10. Descrivete la strategia migratoria dei token.
18) Brevi suggerimenti per la scelta dell'approccio
Directory/ricerca in cui il numero di pagina e il totale approssimativo sono importanti: consentiamo OFFSET/LIMIT + cache; Mi dica che il totale è approssimativo.
Nastri, analisi, elenchi profondi, RPS alto: solo cursor/seek.
Raccolte sharded/distribuite: per-shard cursors + merge token.
Flusso/CDC: cursori come offsets/ts con ripresa.
19) Esempio di accordo API (curriculum)
`GET /v1/items? limit=50&cursor=...`
La risposta è sempre "page. limit`, `page. ha _ more ', opzionale'page'. next_cursor`.
Il puntatore è opaco, firmato, con TTL.
Ordinamento definito: 'ORDER BY created _ at DESC, id DESC'.
Comportamento delle modifiche al set: gli elementi non tornano indietro rispetto al cursore.
Le metriche e gli errori sono standardizzati: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.
Questo articolo fornisce principi architettonici e pattern pronti per progettare una paginazione che rimane veloce, prevedibile e sicura anche in condizioni di grandi dati, charding e set di record in forte evoluzione.