Paginação, filtros e ordenação
Paginação por deslocamento ou por cursor, e por que a primeira degrada com tabelas grandes e repete itens quando chegam dados novos.
Por que paginar
Uma rota GET /pedidos que devolve todos os pedidos funciona no primeiro mês, com 50 registros. Um ano depois, com 2 milhões, ela consome toda a memória do servidor para montar a resposta, leva minutos para trafegar e trava o cliente que tenta exibi-la.
Paginar é devolver a lista em partes de tamanho controlado. O cliente pede a primeira página e, se precisar, as seguintes. A pergunta é como ele diz qual página quer.
Paginação por deslocamento
A forma mais conhecida: o cliente informa quantos itens pular e quantos quer.
GET /pedidos?limite=20&deslocamento=40SELECT * FROM pedidos
ORDER BY criado_em DESC
LIMIT 20 OFFSET 40;É simples, permite pular direto para a página 37 e mostrar "página 3 de 120". Tem dois problemas, e os dois aparecem com o crescimento dos dados.
Problema 1: fica mais lenta a cada página
Para pular 40 itens, o banco precisa encontrar e descartar os 40 primeiros. Para pular um milhão, precisa percorrer um milhão. Medimos no PostgreSQL, numa tabela de 2 milhões de pedidos com índice na ordenação:
| Página pedida | Tempo da consulta |
|---|---|
| Deslocamento 0 (primeira página) | 0,06 ms |
| Deslocamento 10.000 | 2,19 ms |
| Deslocamento 100.000 | 22,78 ms |
| Deslocamento 1.000.000 | 214,07 ms |
O tempo cresce na proporção do deslocamento. Robôs de busca e integrações que percorrem a lista inteira chegam a essas páginas profundas, e cada consulta delas fica milhares de vezes mais cara que a primeira.
Problema 2: repete e pula itens
O deslocamento é contado no momento de cada consulta. Se um item novo chega entre uma página e outra, todos os itens "andam" uma posição, e o cliente vê um deles de novo.
Reproduzimos com dez pedidos, ordenados do mais recente para o mais antigo, em páginas de três:
| Momento | Página | Itens recebidos |
|---|---|---|
| Primeira consulta | 1 (deslocamento 0) | 10, 9, 8 |
| Chega o pedido 11 | ||
| Segunda consulta | 2 (deslocamento 3) | 8, 7, 6 |
O pedido 8 apareceu nas duas páginas. Com remoções, acontece o inverso: um item some da lista sem nunca ter sido exibido. Numa lista que muda o tempo todo, como pedidos, mensagens ou eventos, isso é a regra, e não a exceção.
Paginação por cursor
Em vez de "pule 40", o cliente diz "continue depois do último item que eu vi". O servidor devolve, junto com a página, um cursor que identifica esse ponto.
GET /pedidos?limite=20
→ { "itens": [...], "proximoCursor": "eyJjcmlhZG9FbSI6Ij..." }
GET /pedidos?limite=20&cursor=eyJjcmlhZG9FbSI6Ij...Por baixo, o cursor guarda os valores de ordenação do último item, e a consulta começa a partir deles:
SELECT * FROM pedidos
WHERE (criado_em, id) < ('2026-03-10 12:00:00.123456+00', 981)
ORDER BY criado_em DESC, id DESC
LIMIT 20;Com um índice em (criado_em, id), o banco vai direto ao ponto e lê só os 20 itens. No mesmo teste com 2 milhões de pedidos, a página depois de um milhão de itens levou 0,09 ms, contra 214 ms com deslocamento.
E como o ponto de partida é um item, e não uma contagem, itens novos não empurram nada: no teste de consistência, depois da chegada do pedido 11, a segunda página por cursor trouxe 7, 6 e 5, sem repetir o 8.
O desempate na ordenação
O cursor só funciona se a ordenação for única: dois itens nunca podem ocupar a mesma posição. Ordenar só por data falha quando dois pedidos têm o mesmo horário, o que acontece mais do que parece, em importações e em sistemas com muito volume.
A solução é acrescentar uma coluna única ao fim da ordenação, quase sempre o identificador: ORDER BY criado_em DESC, id DESC. E o cursor guarda as duas.
Uma armadilha de precisão
Testamos uma implementação em Node que montava o cursor com o horário lido do banco como Date do JavaScript. O PostgreSQL guarda horários com microssegundos; o Date, só com milissegundos. Numa tabela com 1.000 pedidos, 100 deles criados exatamente às 12:00:00.123456, o cliente percorreu todas as páginas e viu 904 pedidos: o cursor cortado em 12:00:00.123 pulou quase todos os pedidos daquele instante.
Lendo o horário do banco como texto, preservando os microssegundos, a mesma implementação viu os 1.000. Testamos essa versão nas cinco linguagens deste tópico, e todas percorreram os 1.000 pedidos sem repetir nem pular nenhum.
Total de itens
Mostrar "1.243 pedidos encontrados" exige contar. Na mesma tabela de 2 milhões, um COUNT(*) levou 67 ms, muito mais que buscar a página. Com filtros, o custo varia, mas raramente é desprezível. Por isso muitas APIs não devolvem o total, ou devolvem um valor aproximado, ou só quando o cliente pede.
Por que isso importa quando a IA escreve o código
Um assistente que cria uma rota de listagem costuma escolher a forma mais simples:
- Nenhuma paginação, devolvendo a tabela inteira.
LIMITeOFFSETcom o número da página, sem pensar nas páginas profundas.- Ordenação só por data, sem desempate.
- O cursor montado com valores convertidos pela linguagem, perdendo precisão.
- Nenhum limite máximo, permitindo
?limite=1000000. - Um
COUNT(*)em toda requisição, para mostrar o total.
O próximo nível compara as formas de paginar, filtrar e ordenar, e quando cada uma faz sentido.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Paginação em cinco minutos
Explique em no máximo cinco minutos de leitura como paginar listas numa API, para alguém que sempre usou LIMIT e OFFSET sem pensar nas consequências. Cubra só o essencial: por que paginar, a diferença entre paginação por deslocamento e por cursor, por que o deslocamento fica lento e repete itens, e por que a ordenação precisa de um desempate. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Deslocamento ou cursor, por dentro
Quero entender o que o banco de dados faz em cada forma de paginação. Aja como um mentor. Pergunte primeiro qual banco eu uso. Conduza assim: 1. Mostre o SQL de uma página com LIMIT e OFFSET e explique o que o banco precisa fazer para pular N linhas. 2. Peça que eu estime como o tempo cresce com o deslocamento, antes de explicar. 3. Mostre o SQL da paginação por cursor com comparação de tuplas e explique como o índice permite ir direto ao ponto. 4. Monte comigo uma linha do tempo em que um item novo chega entre duas páginas, e peça que eu diga o que o cliente vê em cada forma. Faça uma pergunta de verificação em cada etapa.
Ordenação estável e desempate
Quero entender por que a paginação precisa de uma ordenação estável. Explique: 1. O que acontece quando dois itens têm o mesmo valor na coluna de ordenação, em cada forma de paginação. 2. Por que acrescentar o identificador como desempate resolve. 3. Como montar a condição do cursor com duas colunas, e por que a comparação de tuplas é melhor que encadear condições com OR. 4. Como o índice precisa ser declarado para atender a ordenação com desempate. Depois me mostre três consultas de paginação e pergunte se cada uma pode pular ou repetir itens. Corrija as minhas respostas.
Os detalhes que fazem o cursor falhar
Quero conhecer os erros sutis que fazem uma paginação por cursor pular itens sem nenhum erro aparente. Explique, com um exemplo para cada um: 1. Perda de precisão de horários ao converter para o tipo de data da linguagem. 2. Números grandes convertidos para ponto flutuante. 3. Mudança de filtros ou de ordenação entre uma página e outra com o mesmo cursor. 4. Ordenação por um campo que pode mudar enquanto o cliente pagina. 5. Fusos horários diferentes entre a aplicação e o banco. Para cada um, diga como detectar com um teste que percorre todas as páginas.