Paginação e cursores
1) Por que precisa de paginação
A paginação limita a quantidade de dados transmitidos e rendidos pelo cliente, reduz a pressão sobre o armazenamento/rede e define uma forma determinada de «caminhar» na coleção. Em sistemas reais, a paginação não é apenas 'page = 1 & limit = 50', mas sim um conjunto de contratos protocolares e invariantes de coerência.
Alvos típicos:- Controle de latência e memória para consulta.
- Navegação estável quando você altera o conjunto de dados (inserir/remover).
- Possibilidade de reabrir do local.
- Acoplamento e pré-teste (prefetch).
- Proteção contra abuso (rate limiting, backpressure).
2) Modelos de paginação
2. 1 OFFSET/LIMIT (página)
A ideia é: «Deixa escapar as linhas N, devolve a M».
Vantagens: simplicidade, compatível com quase qualquer SQL/NoSQL.
- Degradação linear: grandes OFFSET resultam em digitalização completa/skip-cost.
- Instabilidade nas inserções ou remoções entre as solicitações (os deslocamentos «flutuam»).
- É difícil garantir a renovabilidade.
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;
2. 2 Cursor/Keyset/Seek-paginação
A ideia é «continua com a chave K». O cursor é uma posição em um conjunto ordenado.
Benefícios:- O (1) acesso à continuação quando o índice estiver disponível.
- Estabilidade nas alterações na coleção.
- Melhor latência em páginas profundas.
- Precisamos de chaves de triagem rigorosamente definidas, únicas e monótonas.
- Mais difícil de realizar e depurar.
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 Continuação tocens (tocantes opacos)
Ideia: o servidor devolve opaque-token que codifica «posição» (e possivelmente o estado dos chardes/filtros). O cliente não compreende as entranhas e apenas devolve o token para a próxima página.
Vantagens: flexibilidade, capacidade de alterar o padrão sem quebrar a API.
Contras: gerenciamento da vida dos tokens, compatibilidade com os depósitos.
2. 4 Cursores temporários e lógicos
Time-based: «Todas as entradas até T», o cursor é uma marca de tempo (adequada para os fluxos de append-only).
Jobs-sequence/offset-based: cursor - deslocamento no logue (Kafka offset, journal seq).
Global monotonic IDs: Snowflake/UUIDv7 como chave para seek estável.
3) Engenharia de cursos e tokens
3. 1 Propriedades de bom cursor
Opacidade (opaque): o cliente não depende do formato.
Autoria/integridade: assinatura HMAC para impedir a troca/manipulação.
Contexto: inclui triagem, filtros, versão de circuito, tenant/shard.
Tempo de vida: TTL e «não-usabilidade» (não-replay) quando você muda de índice/permissão.
Tamanho: compacto (<= 1-2 KB) adequado ao URL.
3. 2 Formato de token
A pilha recomendada é JSON → compressão (zstd/deflate) → Base64URL → HMAC.
Estrutura de carga útil (exemplo):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
}
Por cima, é adicionado 'mac = HMAC (segredo, payload)' e tudo é codificado em um tocador de linha.
3. 3 Segurança
Assinar (HMAC/SHA-256).
Criptografar opcionalmente (AES-GCM) com valores sensíveis (PII).
Validação no servidor: versão, TTL, permissões do usuário (RBAC/ABAC).
4) Coerência e invariantes
4. 1 Ordenação estável
Use o determinismo completo: 'ORDER BY ts DESC, id DESC'.
A chave de triagem deve ser única (adicione 'id' como tiebreaker).
O índice deve corresponder à triagem (covering index).
4. 2 Instantâneos (snapshot) e isolamento
Use o snapshot read-consistent (MVCC/txid) para as páginas «indefectíveis».
Se não for apropriado (caro/grande número de dados), formule o contrato: «O cursor devolve itens estritamente posição anterior». É natural para as notícias.
4. 3 Inserções/remoções entre páginas
O modelo seek minimiza «duplicados/omissões».
Documente o comportamento de remoção/alteração: os raros «buracos» entre as páginas são permitidos, mas não «para trás no tempo».
5) Indexação e esquemas de identificadores
Os índices compostos estão estritamente na ordem de triagem «(created _ at DESC, id DESC)».
ID monótono: Snowflake/UUIDv7 dá ordem de tempo → acelera seek.
Chaves quentes: Distribua em shard-key (por exemplo, «tenant _ id», «region») e arrume dentro do shard.
Geradores de ID: evite conflitos e «relógios do futuro» - sincronização do tempo, «regressão» em saltos NTP.
6) Paginação cross-chard
6. 1 Esquemas
Scatter-Gather: solicitações paralelas para todos os cursos de seek local, então k-way merge no agregador.
Per-Shard Cursors: O token contém posições para cada chard.
Bounded fan-out: limite o número de chardes por etapa (rate limiting/timeout budet).
6. 2 Tokens para multi-shard
Guarde a matriz ' 7) Contratos de protocolo 7. 1 REST 7. 2 GraphQL (Abordagem Relay) 'cursor' deve ser opaco e assinado; não use «Base64 cru (id)» sem HMAC. 7. 3 gRPC 7. 4 Fluxos e WebSockets Para as fitas contínuas, o cursor é o «último offset/ts visto». 8) Caçamento, pré-processamento, CDN ETag/If-None-Match para a primeira página com filtros estáveis. 9) Controle de carga e limitação Limite superior, por exemplo, 200. 10) Aspectos UX Scroll contra numeração: rolagem sem fim → cursores; páginas de matrícula → offset (mas explique a imprecisão na atualização de dados). 11) Testes e malas edge 12) Observabilidade e SLO 13) Migração e compatibilidade Inclua 'v' no token e suporte as versões antigas N semanas. 14) Exemplos de implementação 14. 1 Geração de token (pseudocode) 14. 2 Validação do token 14. 3 Consulta Seek com chave composta 15) Segurança e conformidade Não inclua campos crus no tocador, dos quais você pode tirar o PII. 16) Erros frequentes e anti-pattern Base64 (id) como um cursor: fácil de falsificar/selecionar, quebra o contrato ao mudar de ordem. 17) Miniclist de implementação 1. Defina a triagem e adicione um tie-breaker único. 18) Resumidas recomendações para a escolha da abordagem Diretórios/buscas onde o «número de página» e total aproximado são importantes: permitamos 'OFFSET/LIMIT' + dinheiro; informe que o total é aproximado. 19) Exemplo de acordo API (resumo) `GET /v1/items? limit=50&cursor=...` A resposta sempre inclui 'page. limit`, `page. ha _ more ', opcionalmente' page. next_cursor`. Este artigo fornece princípios arquitetônicos e pattern prontos para projetar uma paginação que permanece rápida, previsível e segura, mesmo em um ambiente de grandes dados, charding e conjuntos de registros que mudam ativamente.
Resposta:
GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
Recomendações:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
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
}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;
}json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }
Cachê-controle com TTL curto (por exemplo, 5-30 c) para listas públicas.
Prefetch: devolva 'next _ cursor' e dicas ('Link: rel =' next') e o cliente pode antecipar a seguinte página.
Variações: leve em conta 'filter/port/local/tenant' como a chave da parte do caju.
Server-side backpressure: Se o tempo de consulta> budget, reduza 'limit' na resposta (e comunique claramente ao cliente o tamanho real da página).
Rate limits por usuário/token/tenant.
Timeout/Retry: pausa exponencial, solicitações idepotentes.
Botão voltar ao local: guarde a pilha de cursores do cliente.
Páginas em branco: Se 'has _ more = falso', não mostre o botão 'Mais'.
Limites estáveis: mostre o exato 'total' apenas se for barato (senão, o aproximado 'approx _ total').
Exemplo de teste property-based (pseudocode):
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch
Logi/Training:
SLO-exemplo:
Ao alterar as chaves de triagem, envie um erro «macio» de '409 Conflict' com uma dica para fazer uma listagem recente sem o cursor.
Caso catastrófico (todos os tokens): mude 'signing _ key _ id' e rejeite os antigos.python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)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. filterssql
-- 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;
Assine e limite o TTL.
Tente tornar os tokens inaceitáveis entre os usuários (encaixe 'sub/tenant/roles' no payload e verifique na validação).
Só enxergue as hashtags de tokens.
Sem tie-breaker: 'ORDER BY ts DESC' sem 'id' → duplicados/saltados.
Altere os filtros entre páginas sem deficiência do token.
OFFSET profundo: lento e imprevisível.
Tokens sem versão ou TTL.
2. Crie um índice de cobertura para esta ordem.
3. Selecione seek + token opaco.
4. Execute a assinatura (e criptografia, se necessário) do token.
5. Coloque a TTL e a versionização.
6. Configure e documente os contratos 'has _ more', 'next _ cursor'.
7. Pense no padrão de cross-shard (se necessário) e k-way merge.
8. Adicione métricas, alertas e SLO.
9. Cubra com testes de borda de página property-based.
10. Descreva a estratégia migratória dos tokens.
Fitas, analista, listas profundas, RPS alto: apenas cursor/seek.
Coleções chardadas/distribuídas: per-shard cursors + merge token.
Threads/CDC: cursores como offsets/ts com retomada.
O cursor é opaco, assinado, com TTL.
ORDEM BY created _ at DESC, id DESC.
Comportamento das alterações de conjunto: os itens não «voltam» em relação ao cursor.
As métricas e erros são padrão: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.