Uma resposta paginada pode estar correta no momento em que é executada e, ainda assim, produzir uma navegação inconsistente. Se uma aplicação solicita uma página, o conjunto de dados muda e, em seguida, ela solicita a próxima, pode receber itens repetidos ou deixar de ver outros. Esse é um problema comum em listas de atividades, pedidos e registros que continuam crescendo.
A paginação por cursor em uma API PHP ajuda a controlar esse deslocamento, mas, por si só, não garante uma visão congelada dos dados. A decisão importante é definir o que significa avançar pela lista, quais mudanças podem ocorrer durante a navegação e de qual contrato o cliente precisa.
Por que os resultados mudam entre as páginas

Suponha que uma consulta ordene os registros por data decrescente. O cliente obtém os primeiros 20. Antes de solicitar os próximos, três registros recentes são inseridos. Se a segunda solicitação usar OFFSET 20, ela começará na posição 21 do conjunto atual, não na posição 21 que existia na primeira solicitação. Alguns itens da primeira página podem aparecer novamente.
Também podem ocorrer omissões. Se um registro situado antes do deslocamento for excluído, os itens seguintes avançam uma posição, e uma linha que o cliente esperava encontrar pode ficar para trás. A ordenação também não é necessariamente estável se várias linhas tiverem a mesma data: sem um critério adicional, não há garantia de que o banco de dados sempre retornará esses empates na mesma ordem.
Convém distinguir dois objetivos: evitar saltos causados por mudanças de posição e oferecer um snapshot exato de todo o conjunto. Uma paginação por cursor bem definida ajuda com o primeiro. O segundo exige uma estratégia explícita de consistência, que pode ser mais custosa e depender do banco de dados.
Offset ou cursor: escolha de acordo com o padrão de leitura
A paginação por deslocamento, normalmente expressa com LIMIT e OFFSET, é simples e permite ir diretamente a uma página conhecida. Pode ser adequada para conjuntos pequenos ou relativamente estáticos, interfaces com saltos frequentes entre páginas e casos em que inconsistências durante a navegação são aceitáveis. Em conjuntos grandes, deslocamentos elevados podem exigir que o banco de dados percorra ou descarte muitas linhas; o custo real depende do mecanismo, dos índices e da consulta.
A paginação por cursor retorna uma referência ao ponto a partir do qual continuar, por exemplo, o último valor de ordenação e sua chave única. A consulta seguinte busca registros posteriores ou anteriores a esse ponto, em vez de pular uma quantidade de linhas. É adequada para navegações sequenciais, feeds e listas que recebem inserções frequentes. Em contrapartida, não oferece naturalmente um salto para uma página arbitrária: o cliente precisa percorrer as páginas ou dispor de outra estratégia.
A escolha não precisa ser universal para toda a API. É possível oferecer offset em uma consulta administrativa com páginas numeradas e cursor em um fluxo de atividades. A interface deve refletir o que o servidor pode garantir, em vez de prometer navegação aleatória e estabilidade absoluta com um único mecanismo.
Defina uma ordenação total antes de criar o cursor
O cursor só identifica uma posição se a ordenação for determinística. Ordenar apenas por created_at não basta quando dois registros têm a mesma data. Adicione uma coluna única e imutável como critério de desempate, por exemplo, id:
ORDER BY created_at DESC, id DESCAssim, cada linha ocupa uma posição definida na ordenação. O cursor de continuação deve conter ambos os valores. Para o mesmo sentido decrescente, a consulta seguinte busca os pares menores que o último par retornado:
WHERE created_at < :cursor_date
OR (created_at = :cursor_date AND id < :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT :page_sizePara uma ordenação crescente, as comparações são invertidas. Com vários critérios, as condições devem respeitar toda a ordem lexicográfica: primeiro, compara-se o campo inicial e, em caso de empate, o seguinte. Se as direções forem diferentes — por exemplo, data decrescente e identificador crescente —, cada comparação deve corresponder à direção de sua coluna. Não basta inverter todos os operadores de uma só vez.
Os valores de ordenação também precisam de regras estáveis. Se uma coluna puder ser nula, defina como esses valores serão ordenados e codifique essa distinção na condição de continuação. É preferível usar critérios imutáveis durante a navegação: se uma data que determina a posição for alterada, uma linha pode passar de um lado do cursor para o outro.
Torne o cursor opaco, validado e vinculado à consulta
Um cursor pode serializar os valores de ordenação e codificá-los, por exemplo, com Base64URL. Ser opaco significa que o consumidor não precisa interpretá-lo nem construí-lo, não que o Base64 o proteja. Se a alteração de seus valores puder mudar o escopo da consulta, valide o formato e assine o conteúdo com um HMAC ou use um mecanismo equivalente de integridade. Não inclua segredos nem dados pessoais desnecessários.
Valide os tipos, os campos esperados, a versão do formato e os limites de tamanho antes de consultar o banco de dados. Use parâmetros SQL para os valores. Os nomes das colunas e as direções de ordenação não devem ser aceitos diretamente do cursor ou da solicitação: devem vir de uma lista permitida no servidor.
Um cursor de data e identificador não deve poder ser reutilizado acidentalmente com filtros diferentes se isso resultar em uma continuação enganosa. É possível incluir uma representação canônica dos filtros relevantes, do sentido da ordenação e, se for o caso, do tamanho da página, e assiná-los junto com o ponto de continuação. Se não corresponderem à solicitação atual, retorne um erro claro em vez de continuar silenciosamente com outra consulta. Em PHP, centralize a codificação, a validação e a assinatura para não duplicar regras entre controladores.
Decida qual consistência oferecer diante de mudanças concorrentes
Em uma navegação por dados dinâmicos, cada página consulta o estado disponível naquele momento. Um cursor baseado em uma ordenação imutável evita muitos deslocamentos provocados por inserções anteriores ao ponto alcançado. Mas não cria um snapshot: novas linhas podem aparecer depois do cursor, linhas ainda não visitadas podem ser excluídas e as permissões e os filtros aplicáveis podem mudar. Documente esse comportamento para que o cliente não o confunda com uma exportação fechada.
Se o produto precisar que todas as páginas representem um conjunto delimitado, uma opção é definir um limite, como uma data ou um identificador máximo no início da navegação, e adicioná-lo a cada consulta. Isso exclui inserções posteriores ao limite quando o critério escolhido permite, mas não preserva linhas excluídas nem garante um snapshot perfeito diante de modificações. Outra possibilidade é um snapshot transacional; manter uma transação aberta entre solicitações costuma ter implicações operacionais e de recursos e, portanto, não deve ser considerado a solução padrão.
O contrato pode estabelecer limites claros: ordenação suportada, filtros que devem ser mantidos, validade, se houver, comportamento diante de um cursor inválido e se mudanças concorrentes podem alterar o conjunto. Não prometa ausência absoluta de duplicações ou omissões se a estratégia não puder garanti-la.
Teste os limites e documente o contrato

Os testes devem verificar a navegação completa, não apenas o formato de uma resposta. Prepare linhas com valores de ordenação repetidos e confirme que várias páginas concatenadas produzem a ordem esperada, sem duplicações. Inclua casos em que o tamanho da página divide um grupo de empates e valide tanto o sentido crescente quanto o decrescente.
- Insira registros antes e depois do cursor entre duas solicitações e verifique o comportamento acordado.
- Exclua uma linha pendente e modifique uma coluna de ordenação, se o modelo permitir; documente as consequências.
- Altere um filtro, a ordenação ou o sentido da navegação e verifique se um cursor incompatível é rejeitado.
- Envie cursores malformados, adulterados, grandes demais ou com valores de tipos incorretos.
- Verifique os limites do tamanho da página e o caso sem resultados, inclusive a ausência de uma próxima página.
Para diagnósticos, registre métricas de duração da consulta, tamanho da página e erros de validação, sem despejar cursores sensíveis nem dados pessoais. Se ocorrerem repetições, verifique primeiro a ordenação total e a condição de continuação. Se o problema for o custo das consultas, inspecione o plano e os índices nos campos de ordenação e nas condições de filtro.
Uma paginação estável não depende de ocultar uma string em Base64: depende de uma ordenação determinística, uma comparação coerente, filtros controlados e expectativas explícitas sobre mudanças concorrentes. Com essas decisões, offset e cursor se tornam ferramentas que podem ser escolhidas de acordo com a navegação de que o cliente realmente precisa.



