DevGuideTK
← Trilha Backend

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=40
SELECT * 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 pedidaTempo da consulta
Deslocamento 0 (primeira página)0,06 ms
Deslocamento 10.0002,19 ms
Deslocamento 100.00022,78 ms
Deslocamento 1.000.000214,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:

MomentoPáginaItens recebidos
Primeira consulta1 (deslocamento 0)10, 9, 8
Chega o pedido 11
Segunda consulta2 (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.
  • LIMIT e OFFSET com 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.