GH GambleHub

Paginación y cursores

1) Por qué se necesita paginación

La paginación limita la cantidad de datos transmitidos y renderizados por el cliente, reduce la carga de almacenamiento/redes y establece una forma determinista de «caminar» por la colección. En sistemas reales, la paginación no es solo 'page = 1 & limit = 50', sino un conjunto de contratos protocolarios e invariantes de consistencia.

Objetivos modelo:
  • Control de latencia y memoria por solicitud.
  • Navegación estable cuando se cambia el conjunto de datos (inserción/eliminación).
  • Posibilidad de reanudar desde el asiento (resumen).
  • Caché y precarga (prefetch).
  • Protección contra el abuso (rate limiting, backpressure).

2) Modelos de paginación

2. 1 OFFSET/LIMIT (página)

La idea es: «saltar N líneas, volver M».
Pros: simplicidad, compatible con casi cualquier SQL/NoSQL.

Contras:
  • Degradación lineal: grandes OFFSET conducen al escaneo completo/skip-cost.
  • Inestabilidad en las inserciones/eliminaciones entre consultas (los desplazamientos «flotan»).
  • Es difícil garantizar una «renovabilidad» precisa.
Ejemplo SQL:
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;

2. 2 Cursor/Keyset/Seek-paginación

La idea es «continuar con la clave K». Un cursor es una posición en un conjunto ordenado.

Ventajas:
  • O (1) acceder a la continuación si hay un índice.
  • Estabilidad en los cambios de colección.
  • Mejor latencia en «páginas» profundas.
Contras:
  • Se necesitan llaves de clasificación estrictamente definidas, únicas y monótonas.
  • Más difícil de implementar y depurar.
Ejemplo SQL (seek):
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 (tokens opacos)

Idea: el servidor devuelve un token opaque en el que se codifica la «posición» (y posiblemente el estado de los shardos/filtros). El cliente no entiende las vísceras y simplemente devuelve el token para la página siguiente.
Ventajas: flexibilidad, posibilidad de cambiar el circuito sin romper la API.
Contras: gestión de la vida útil de los tokens, compatibilidad con los deployes.

2. 4 Cursores temporales y lógicos

Tiempo-basado: «todas las entradas hasta T», el cursor es una marca de tiempo (adecuado para subprocesos append-only).
Log-sequence/offset-based: cursor - offset (Kafka offset, journal seq).
Global monotonic IDs: Snowflake/UUIDv7 como claves ordenables para un seek estable.

3) Diseño de cursos y tokens

3. 1 Propiedades de un buen cursor

Opacidad (opaque): el cliente es independiente del formato.
Autoría/integridad: firma HMAC para evitar la sustitución/manipulación.
Contexto: incluye ordenamiento, filtros, versión del esquema, tenant/shard.
Vida útil: TTL e «inapelable» (no-replay) cuando se cambian los índices/derechos de acceso.
Tamaño: compacto (<= 1-2 KB), adecuado para URL.

3. 2 Formato de token

Pila recomendada: JSON → compresión (zstd/deflate) → Base64URL → HMAC.

Estructura de la carga útil (ejemplo):
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
}

En la parte superior se añade 'mac = HMAC (secret, payload)' y todo está codificado en un solo token de cadena.

3. 3 Seguridad

Firmar (HMAC/SHA-256).
Cifrado opcional (AES-GCM) si hay valores sensibles (PII).
Validación en el servidor: versión, TTL, credenciales de usuario (RBAC/ABAC).

4) Coherencia e invariantes

4. 1 Clasificación estable

Utilice el determinismo completo: 'ORDER BY ts DESC, id DESC'.
La clave de ordenación debe ser única (añadir 'id' como tiebreaker).
El índice debe coincidir con la clasificación (índice covering).

4. 2 Imágenes (snapshot) y aislamiento

Para páginas «sin saltar», utilice el snapshot de lectura consistente (MVCC/txid).
Si el snapshot no es práctico (datos caros/muchos), formula el contrato: «el cursor devuelve los elementos, estrictamente anteriores a las posiciones». Es natural para las noticias.

4. 3 Inserciones/eliminaciones entre páginas

El modelo Seek minimiza los «duplicados/pases».
Documentar el comportamiento al eliminar/cambiar: se permiten «agujeros» raros entre las páginas, pero no «hacia atrás en el tiempo».

5) Indexación y esquemas de identificación

Los índices compuestos están estrictamente en orden de clasificación: '(created_at DESC, id DESC)'.
ID monótono: Snowflake/UUIDv7 dan orden en el tiempo → aceleran seek.
Teclas de acceso rápido: Distribuya la clave de shard (por ejemplo, 'tenant _ id', 'region') y ordene dentro de la barra.
Generadores ID: evite colisiones y «relojes del futuro» (clock skew) - sincronización de tiempo, «regresión» en saltos NTP.

6) Paginación cruzada-charda

6. 1 Esquemas

Scatter-Gather: consultas paralelas a todas las chardas, cursos de búsqueda locales, luego k-way merge en el agregador.
Por-Shard Cursors: el token contiene posiciones para cada chard.
Bounded fan-out: limite el número de chardos en un solo paso (rate limiting/timeout budget).

6. 2 Tokens para multi-shard

Almacene la matriz '{shard _ id, last_pos}'. En el siguiente paso, renueve por cada shard activo y vuelva a pellizcar, dando una página ordenada globalmente.

7) Contratos de protocolo

7. 1 REST

Consulta:

GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
Respuesta:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
Recomendaciones:
  • 'limit' con el límite superior (por ejemplo, max = 200).
  • 'next _ cursor' falta si 'has _ more = false'.
  • Idempotencia GET, capacidad de respuesta en caché sin 'next _ cursor' (primera página con filtros fijos y snapshot).

7. 2 GraphQL (enfoque Relay)

Contrato típico de 'conexión':
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' debe ser opaco y estar firmado; no use «Base64 crudo (id)» sin HMAC.

7. 3 gRPC

Utilice 'page _ size' y '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 Subprocesos y WebSockets

Para cintas continuas: el cursor como "el último offset/ts' visto.

Admita 'resume _ from' en el reconnect:
json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }

8) Caché, pre-inicio, CDN

ETag/If-None-Match para la primera página con filtros estables.
Cache-Control con TTL corto (por ejemplo, 5-30 s) para listas públicas.
Prefetch: devuelve 'next _ cursor' y sugerencias ('Link: rel =' next'), el cliente puede preinstalar la siguiente página.
Variaciones: considere 'filter/sort/locale/tenant' como la clave de la pieza de caché.

9) Gestión de carga y límite

El límite superior es 'limit', por ejemplo 200.
Backpressure server-side: si el tiempo de consulta es> budget, reduzca el 'limit' en la respuesta (y notifique explícitamente al cliente el tamaño real de la página).
Rate limits por usuario/token/tenant.
Timeout/Retry: pausa exponencial, consultas repetidas idempotentes.

10) Aspectos UX

Skroll versus numeración: desplazamiento infinito → cursores; páginas numeradas → offset (pero explique la inexactitud al actualizar los datos).
Botón «volver al lugar»: almacena la pila de cursores del cliente.
Páginas en blanco: si 'has _ more = false', no muestre el botón 'Más'.
Límites estables: muestre el 'total' exacto sólo si es barato (de lo contrario, el 'approx _ total' aproximado).

11) Pruebas y casos de edge

Listas de cheques:
  • Ordenación estable: los elementos con el mismo 'ts' no «parpadean».
  • Inserciones/desinstalaciones: no aparecen repeticiones en la unión de páginas.
  • Cambio de filtros entre páginas: el token debe ser rechazado como «obsoleto/incompatible».
  • TTL token: error correcto después de la fecha de vencimiento.
  • Mayor profundidad: la latencia no crece linealmente.
  • Multishard: orden merge correcto, sin starvation de los «lentos» chardos.
Prueba basada en propiedades de ejemplo (pseudocódigo):
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch

12) Observabilidad y SLO

Métricas:
  • 'list _ request _ latency _ ms' (P50/P95/P99) a lo largo de la página.
  • 'seek _ index _ hit _ ratio' (porcentaje de solicitudes que han salido en el índice de cobertura).
  • 'next _ cursor _ invalid _ rate' (errores de validación/TTL/firma).
  • 'merge _ fanout' (cole-in de las chardas involucradas por página).
  • 'duplicates _ on _ boundary' y 'gaps _ on _ boundary' (un detalle en telemetría de cliente).
Logs/treising:
  • Correlacionar 'cursor _ id' en los logs, enmascarar el payload.
  • Tegire los durmientes: 'page _ size', 'source _ shards', 'db _ index _ used'.
Ejemplo de SLO:
  • Disponibilidad: 99. 9% en métodos 'List'.
  • Latencia: P95 <200 ms para 'page _ size <= 50' en una bola local.
  • Error de señal: <0. 1% del total de llamadas.

13) Migración e interoperabilidad

Incluya la 'v' en el token y mantenga las versiones anteriores de las semanas N.
Cuando cambie las claves de clasificación, envíe el error «suave» '409 Conflict' con una indicación para realizar un anuncio fresco sin cursor.
Caso catastrófico (revuelo de todos los tokens): cambia 'signing _ key _ id' y rechaza los antiguos.

14) Ejemplos de implementaciones

14. 1 Generación de token (pseudocódigo)

python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)

14. 2 Validación de 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 Consulta Seek con clave compuesta

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) Seguridad y cumplimiento

No incluya campos crudos en los tokens de los que pueda derivar PII.
Firme y limite la TTL.
Intenta que los tokens sean intolerantes entre los usuarios (encaja 'sub/tenant/roles' en el payload y verifica al validar).
Lógica sólo los hashes de tokens.

16) Errores frecuentes y anti-patrones

Base64 (id) como cursor: fácil de falsificar/recoger, rompe el contrato al cambiar la clasificación.
Ausencia de tie-breaker: 'ORDER BY ts DESC' sin 'id' → duplicados/saltos.
Cambio de filtros entre páginas sin discapacidad de token.
OFFSET profundo: lento e impredecible.
Tokens sin versión y TTL.

17) Mini checklist de implementación

1. Identifique la clasificación y agregue un tie-breaker único.
2. Cree un índice de cobertura bajo este orden.
3. Seleccione el modelo: seek + token opaco.
4. Implemente la firma (y, si es necesario, el cifrado) del token.
5. Plantar TTL y versionar.
6. Articular y documentar los contratos 'has _ more', 'next _ cursor'.
7. Piense en el esquema cruzado (si es necesario) y k-way merge.
8. Agregue métricas, alertas y SLO.
9. Cubra las pruebas de límite de página con property-based.
10. Describa la estrategia de migración de tokens.

18) Recomendaciones breves sobre la elección de un enfoque

Directorios/búsquedas donde el «número de página» y el total aproximado son importantes: permitamos 'OFFSET/LIMIT' + caché; Informe que el total es aproximado.
Cintas, análisis, listas profundas, RPS alto: sólo cursor/seek.
Colecciones chardeadas/distribuidas: por-shard cursors + merge token.
Flujos/CDC: cursores como offsets/ts con reanudación.

19) Ejemplo de acuerdo API (resumen)

`GET /v1/items? limit=50&cursor=...`

La respuesta siempre incluye 'page. limit`, `page. has_more', opcional 'page. next_cursor`.
El cursor es opaco, firmado, c TTL.
La clasificación es determinista: 'ORDER BY created_at DESC, id DESC'.
Comportamiento en cambios de conjunto: los elementos no «retroceden» con respecto al cursor.
Las métricas y los errores están estandarizados: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.

Este artículo proporciona principios arquitectónicos y patrones listos para diseñar una paginación que sigue siendo rápida, predecible y segura incluso en entornos de big data, charding y conjuntos de registros que cambian activamente.

Contact

Póngase en contacto

Escríbanos ante cualquier duda o necesidad de soporte.¡Siempre estamos listos para ayudarle!

Iniciar integración

El Email es obligatorio. Telegram o WhatsApp — opcionales.

Su nombre opcional
Email opcional
Asunto opcional
Mensaje opcional
Telegram opcional
@
Si indica Telegram, también le responderemos allí además del Email.
WhatsApp opcional
Formato: +código de país y número (por ejemplo, +34XXXXXXXXX).

Al hacer clic en el botón, usted acepta el tratamiento de sus datos.