Padrões de Paginação no MongoDB
Você está construindo um feed, uma lista de produtos ou uma página de resultados de busca. A coleção cresceu para milhões de documentos e, de repente, suas consultas de paginação levam segundos em vez de milissegundos. O culpado é quase sempre o mesmo: você está usando skip e limit em grande escala.
Este artigo aborda os principais padrões de paginação do MongoDB—baseados em offset e baseados em cursor/keyset—explicando quando cada um funciona bem e quando não funciona. Sem conselhos específicos de frameworks, apenas padrões que permanecerão válidos conforme seus dados crescem.
Principais Conclusões
- A paginação por offset com
skip()elimit()é intuitiva, mas degrada linearmente à medida que os valores de offset aumentam—reserve-a para paginação superficial ou conjuntos de dados pequenos. - A paginação por keyset (baseada em cursor) usa consultas de intervalo para desempenho consistente independentemente da posição, tornando-a ideal para grandes conjuntos de dados.
- Sempre use um campo de desempate único (tipicamente
_id) com paginação por keyset para evitar duplicatas ou documentos ignorados. - Escolha seu padrão de paginação com base no tamanho dos dados, padrões de acesso e se os usuários precisam de navegação sequencial ou saltos arbitrários de página.
Paginação MongoDB Skip Limit: A Abordagem Familiar
A paginação baseada em offset usa skip() e limit() para dividir resultados em páginas. É intuitiva e mapeia diretamente para padrões de UI “página 1, página 2, página 3”.
db.posts.find()
.sort({ createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
Este padrão funciona bem para paginação superficial—as primeiras páginas de resultados, painéis administrativos com dados limitados ou ferramentas internas onde o desempenho não é crítico.
Por Que Skip Degrada em Escala
Os índices do MongoDB são estruturas B-tree, não arrays. Embora o banco de dados possa buscar eficientemente em um índice, ele ainda precisa avançar através das entradas ignoradas para alcançar o offset solicitado. Ignorar 10 documentos avança por 10 entradas de índice. Ignorar 100.000 avança por 100.000 entradas.
Isso significa que a página 1 é rápida, a página 100 é mais lenta e a página 10.000 é significativamente mais lenta ainda. O uso de CPU aumenta linearmente com o valor do offset, independentemente do tamanho da sua página.
Use paginação por offset quando:
- Os usuários raramente navegam além das primeiras páginas
- O conjunto de dados total é pequeno (menos de 10.000 documentos)
- Você precisa da funcionalidade “pular para a página X” e aceita o compromisso
- Está construindo ferramentas internas onde o tempo de consulta é menos crítico
Paginação Baseada em Cursor do MongoDB: Desempenho Consistente
A paginação por keyset do MongoDB (também chamada de paginação baseada em cursor) usa consultas de intervalo em vez de offsets posicionais. Em vez de dizer “ignore 1.000 documentos”, você diz “me dê documentos após este ponto específico”.
db.posts.find({ createdAt: { $lt: lastSeenDate } })
.sort({ createdAt: -1 })
.limit(pageSize)
O banco de dados realiza uma busca eficiente no índice para localizar o ponto de partida, depois lê apenas os documentos que você precisa. A página 1 e a página 10.000 têm características de desempenho idênticas.
O Requisito do Desempate
Um único campo de ordenação cria problemas quando os valores não são únicos. Se múltiplos documentos compartilham o mesmo timestamp createdAt, alguns podem ser ignorados ou duplicados entre páginas.
A solução é uma ordenação composta com um desempate único—tipicamente _id:
db.posts.find({
$or: [
{ createdAt: { $lt: lastDate } },
{ createdAt: lastDate, _id: { $lt: lastId } }
]
})
.sort({ createdAt: -1, _id: -1 })
.limit(pageSize)
Isso requer um índice composto correspondente:
db.posts.createIndex({ createdAt: -1, _id: -1 })
A ordem dos campos do índice deve corresponder à sua ordem de classificação para desempenho ideal.
Discover how at OpenReplay.com.
Ressalvas de Consistência
A paginação baseada em cursor é mais estável que a paginação por offset, mas não é magicamente consistente. Se documentos são inseridos, deletados ou se campos de ordenação mutáveis mudam entre requisições, os usuários ainda podem ver duplicatas ou perder itens. A diferença é que a paginação por keyset ancora em valores específicos em vez de posições, então escritas concorrentes tipicamente causam menos anomalias visíveis.
Para feeds e listas onde consistência perfeita não é necessária, esse compromisso geralmente é aceitável.
Paginação do Atlas Search: Um Mecanismo Diferente
Se você está usando o MongoDB Atlas Search com o estágio $search, a paginação funciona de forma diferente. O Atlas Search usa seu próprio sistema baseado em tokens—via searchSequenceToken nos bastidores—com parâmetros searchAfter e searchBefore em vez de cursores _id. Não misture essas abordagens—use o método de paginação que corresponde ao seu tipo de consulta.
Escolhendo o Padrão Correto
| Cenário | Padrão Recomendado |
|---|---|
| Feeds de rolagem infinita | Paginação por keyset |
| Botões “Carregar mais” | Paginação por keyset |
| Tabelas administrativas com números de página | Offset (apenas páginas superficiais) |
| Grandes conjuntos de dados com navegação sequencial | Paginação por keyset |
| Conjuntos de dados pequenos e estáticos | Qualquer um funciona |
A decisão geralmente se resume aos requisitos de UI. Se os usuários precisam pular para páginas arbitrárias, você está limitado à paginação por offset ou abordagens híbridas. Se a navegação sequencial é suficiente—próximo, anterior, carregar mais—a paginação por keyset escala melhor.
Conclusão
A paginação por offset com skip e limit é simples, mas degrada linearmente com o tamanho do offset. Reserve-a para paginação superficial ou conjuntos de dados pequenos.
A paginação por keyset mantém desempenho consistente independentemente da posição, mas requer ordenação determinística com um desempate único. É a melhor escolha para feeds, listas e qualquer interface onde os usuários navegam sequencialmente através de grandes conjuntos de resultados.
Nenhum padrão é universalmente errado. Escolha com base no tamanho dos seus dados, padrões de acesso e requisitos de UI.
Perguntas Frequentes
Sim, você pode usar múltiplos campos de ordenação com paginação por keyset. O requisito chave é que seu campo final deve ser único para servir como desempate. Seu índice composto deve corresponder à ordem e direção exatas de todos os campos de ordenação. A lógica da consulta se torna mais complexa com cada campo adicional, pois você precisa de condições OR aninhadas para lidar com casos de igualdade para cada campo precedente.
Para paginação reversa, uma abordagem comum é inverter seus operadores de comparação e direção de ordenação. Se avançar usa $lt com ordenação descendente, retroceder usa $gt com ordenação ascendente. Busque os resultados e então inverta-os em sua aplicação para manter a ordem de exibição. Armazene os cursores do primeiro e último documento de cada página para habilitar navegação bidirecional.
Se o campo de ordenação de um documento muda enquanto um usuário está paginando, ele pode ver esse documento duas vezes ou perdê-lo completamente, dependendo se ele se moveu para frente ou para trás em relação à posição do cursor. Para campos frequentemente atualizados, considere usar valores imutáveis como _id como sua chave de ordenação primária, ou aceite isso como uma limitação inerente de dados em tempo real.
A paginação por keyset não suporta naturalmente contagens totais porque não rastreia posição. Você pode executar uma consulta de contagem separada, mas isso adiciona sobrecarga em coleções grandes. Considere se os usuários realmente precisam de contagens exatas—frequentemente contagens aproximadas ou simplesmente indicar que existem mais resultados fornece contexto suficiente sem o custo de desempenho.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.