A primeira decisão é a ordem
Uma lista de tarefas ordenada apenas por instante pode ter empates. Se a aplicação divide o resultado em páginas, precisa de uma ordem total, por exemplo occurred_at e um id único. A existência de um índice não dispensa ORDER BY na query que entrega a página. Ordenar depois no browser também não corrige quais as linhas que o servidor escolheu para cada página. Escreve um conjunto com três tarefas no mesmo instante e verifica se o desempate é explícito. Esta garantia vale para o conjunto observado: uma ordem bem definida não congela automaticamente os dados entre dois pedidos HTTP. O contrato de consistência deve ser discutido separadamente.
Posição e continuação não são a mesma coisa
OFFSET conta posições no resultado da query atual. Se a primeira página de 10,20,30,40 entrega 10,20 e depois entra 5, OFFSET 2 pode devolver 20,30. Se for eliminado 10, o mesmo deslocamento pode saltar 30. Uma continuação id>20 usa o último valor entregue, evitando estes deslocamentos do prefixo quando a chave se mantém. Não significa que todas as alterações posteriores serão vistas: um novo id=15 fica antes do cursor. Escolhe entre uma lista dinâmica, uma exportação fixa e uma consulta histórica antes de prometer ausência de repetições ou cobertura completa. Cada produto pode precisar de uma política diferente.
O predicado deve reproduzir a ordenação
Para t ASC,id ASC sem NULL, a comparação (t,id)>(ultimo_t,ultimo_id) descreve a continuação lexicográfica. Se t empata, o id decide. Numa ordem mista score DESC,id ASC, usa uma condição equivalente a score menor ou score igual com id maior. Uma comparação simples dos dois campos na mesma direção não representa essa ordem. Valores NULL precisam de atenção adicional: colocá-los no fim com NULLS LAST não faz uma comparação desconhecida tornar-se verdadeira. Define explicitamente o grupo de NULL e como continuar dentro e depois dele, ou escolhe uma chave obrigatória adequada. O cursor deve guardar os valores necessários sem perder precisão ou alterar a representação.
Cursor, filtros e autorização
O cursor de uma lista filtrada por tenant A não deve ser reutilizado silenciosamente como continuação de tenant B. Define o contexto do cursor: filtros relevantes, direção de ordenação e última chave apresentada. A aplicação deve verificar compatibilidade e autorização em cada pedido. Mesmo um cursor assinado não prova que o utilizador atual continua autorizado; a assinatura apenas pode proteger integridade segundo o desenho adotado. Esta é uma regra de desenho da aplicação, não uma propriedade automática da comparação SQL. Para detetar outra página, podes pedir N+1 linhas e apresentar N. O cursor seguinte deve usar a última linha apresentada, preservando a linha extra para a próxima leitura.
Laboratório: desempatar sem perder linhas
O exemplo usa instantes inteiros sintéticos e quatro tarefas. O cursor (10,2) já foi apresentado; prevê que a próxima página contenha (10,3) e (11,1). Altera o predicado para t>10 e observa que perdes a tarefa empatada. Depois usa >= na comparação composta e observa a repetição do cursor. Estes ensaios isolam erros de fronteira sem depender de uma aplicação web. Noutra variante, insere uma linha antes do cursor entre duas leituras e compara a continuação com OFFSET. Os resultados são executados num subconjunto SQLite portátil. Não demonstram planos, custo, isolamento ou desempenho do PostgreSQL em produção, que exigem ensaios próprios.
Resumo: definir o conjunto observado
Quando um utilizador relata linhas repetidas, recolhe a ordem, os filtros, os cursores e as mudanças ocorridas entre pedidos. Uma chave mutável pode deslocar uma tarefa já entregue para depois do cursor, fazendo-a reaparecer. Para uma exportação de conjunto fixo, considera uma observação consistente ou um resultado materializado com ciclo de vida e recursos definidos. Não mantenhas uma transação aberta indefinidamente só para simplificar a interface. Em PostgreSQL, o nível de isolamento e a fronteira da transação influenciam a observação; transações separadas não partilham automaticamente um snapshot. Liga esta aula a reconciliação, isolamento e índices: correção da seleção e eficiência da execução precisam de evidência diferente.
WITH tasks(t,id) AS (
VALUES (10,1), (10,2), (10,3), (11,1)
)
SELECT t,id
FROM tasks
WHERE (t,id) > (10,2)
ORDER BY t,id
LIMIT 2;Uma linha inserida antes da posição anterior pode repetir resultados com OFFSET; a chave de continuação evita esse deslocamento específico.
Armadilhas comuns
Não omitir desempates, usar a linha extra como cursor, tratar NULL apenas na ordenação ou prometer snapshot através de um cursor isolado.
Tópicos relacionados: Contar e agrupar com cuidado · Índices e planos de execução · Reconciliação, métricas e diagnóstico de consultas
A ordem, o predicado e o cursor devem concordar; a estabilidade do conjunto exige um contrato de consistência separado.
Referência: Pagination and limits · PostgreSQL 18 reference semantics; DR SQL 2026.4; synthetic plan metrics and portable SQLite 3.51.2 examples