Como implementar paginação eficiente em grandes datasets
Implementar paginação eficiente em datasets volumosos não é só aplicar um LIMIT e OFFSET, essa abordagem simples pode derrubar a performance quando o volume cresce. O resultado esperado é uma navegação rápida e previsível, sem sobrecarregar o banco. Pré-requisito: ter um índice na coluna de ordenação (como ID ou data de criação).
Passo 1: Escolha a estratégia certa para o volume
Para datasets com poucas centenas de registros, OFFSET/LIMIT funciona bem. Mas para grandes volumes (milhares ou milhões), adote cursor-based pagination, também chamada de keyset pagination. Em vez de pular linhas com OFFSET, você filtra por um valor da última linha da página anterior (ex.: WHERE id > último_id_visto). Isso evita que o banco precise varrer e descartar linhas.
Dica importante: Use uma coluna única e indexada como cursor (chave primária, por exemplo). Evite colunas com valores repetidos, como status ou data sem milissegundos.
Erro comum a evitar: Usar OFFSET alto, quanto maior o OFFSET, mais linhas o banco varre e descarta. Para a página 100.000, o banco lê 100.000 linhas e joga 99.990 fora.
Passo 2: Implemente a paginação baseada em cursor
A lógica é simples: na primeira requisição, não passa cursor. A query retorna as N primeiras linhas ordenadas por uma coluna (ex.: id ASC). No final da resposta, inclua o valor do último id retornado. Na próxima requisição, filtre WHERE id > último_id ORDER BY id LIMIT N.
Dica: Garanta que a ordenação seja determinística. Se usar data, combine com ID para evitar que registros com mesmo timestamp pulem ou repitam.
Erro comum a evitar: Não esquecer de ordenar explicitamente. Sem ORDER BY, a ordem pode variar entre execuções, causando repetição ou falha de registros.
Passo 3: Otimize os índices e a consulta
Crie um índice composto na coluna de ordenação e nas colunas de filtro. Para paginação cursor, um índice em (id, coluna_filtro) pode acelerar a consulta. Use EXPLAIN para verificar se o índice está sendo usado.
Dica: Se o dataset tiver filtros (ex.: WHERE status = 'ativo'), inclua a coluna do filtro no início do índice composto.
Erro comum a evitar: Índice faltando na coluna de ordenação, sem ele, o banco fará full scan.
Checklist do que foi feito
- [ ] Estratégia definida: cursor-based para grandes volumes, OFFSET só para pequenos.
- [ ] Coluna de cursor única e indexada.
- [ ] Ordenação explícita e determinística.
- [ ] Índice composto criado para suportar filtros e ordenação.
- [ ] Teste de performance com volume real.
Perguntas frequentes
Qual a diferença entre OFFSET/LIMIT e cursor-based pagination?
OFFSET/LIMIT pula linhas no início do resultado, forçando o banco a ler e descartar registros. Cursor-based filtra por um valor conhecido (ex.: último ID), evitando essa varredura. Para grandes volumes, cursor é muito mais rápido.
Quando usar OFFSET/LIMIT ainda é aceitável?
Sim, para datasets pequenos (até algumas centenas de registros) ou quando o usuário precisa pular para uma página específica (ex.: página 5 de 10). Para milhões, evite.
Preciso de um índice específico para cursor-based pagination?
Sim, um índice na coluna de ordenação (e nas colunas de filtro, se houver) é essencial. Sem ele, a consulta fará varredura completa, anulando a vantagem.
Como lidar com dados que mudam entre páginas?
Cursor-based é sensível a inserções/remoções. Se um registro for inserido antes do cursor atual, ele pode não aparecer. Para consistência, considere snapshot isolation ou paginação baseada em data/hora imutável.
É possível combinar filtros com cursor-based pagination?
Sim, basta incluir a condição do filtro no WHERE e criar um índice composto que comece pela coluna do filtro. Exemplo: WHERE status = 'ativo' AND id > último_id ORDER BY id.
Qual é o limite prático para OFFSET antes de trocar de estratégia?
Não há número fixo, mas acima de 10.000 registros o OFFSET começa a mostrar lentidão perceptível. Teste com seu volume real e métricas de tempo de resposta.