Dominando a Indexação no MongoDB para Desempenho Ótimo de Consultas
Desbloqueie o desempenho ideal de consultas no MongoDB dominando técnicas de indexação. Este guia abrangente cobre índices de campo único, compostos e multichave, explica o poder das consultas de cobertura e orienta você sobre o uso de `explain()` para análise de desempenho. Aprenda as melhores práticas para acelerar suas operações de leitura, reduzir a carga do banco de dados e construir uma aplicação mais responsiva. Leitura essencial para qualquer desenvolvedor MongoDB focado em ajuste de desempenho.
Dominando a Indexação no MongoDB para Desempenho Ótimo de Consultas
A indexação no MongoDB se torna interessante quando o banco de dados não é mais pequeno o suficiente para palpites de sorte. Uma consulta que parecia instantânea em desenvolvimento pode se tornar dolorosa em produção assim que uma coleção tem milhões de documentos, um painel adiciona ordenação ou um endpoint de API começa a filtrar por vários campos ao mesmo tempo.
O objetivo não é indexar todos os campos. Isso geralmente torna as escritas mais lentas, consome memória e disco, e ainda deixa consultas importantes descobertas. O objetivo é entender o punhado de formas de consulta das quais sua aplicação realmente depende e, em seguida, construir índices que correspondam a essas formas.
Entendendo os Índices do MongoDB
Em sua essência, um índice é como um índice em um livro. Em vez de ler o livro inteiro para encontrar um tópico, você consulta uma referência ordenada e salta para perto da página certa. Os índices do MongoDB ajudam o planejador de consultas a localizar documentos correspondentes sem escanear toda a coleção. Sem um índice útil, o MongoDB pode realizar uma varredura de coleção, examinando documentos um por um até encontrar as correspondências.
As varreduras de coleção nem sempre são ruins. Escanear uma coleção minúscula pode ser aceitável. Executar um relatório administrativo uma vez por mês pode ser aceitável. Mas uma varredura de coleção dentro de um caminho de requisição de alto tráfego é diferente. Ela compete com leituras e escritas normais, piora à medida que os dados crescem e geralmente se manifesta como latência imprevisível.
Como os Índices Funcionam
O MongoDB comumente usa índices do tipo B-tree para índices de campo normais. O detalhe prático importante é que os valores indexados são armazenados em ordem. Essa ordenação ajuda o MongoDB com filtros de igualdade, filtros de intervalo e ordenações quando a forma da consulta se alinha com o índice.
Por exemplo, um índice em { email: 1 } é perfeito para:
db.users.findOne({ email: "[email protected]" })
Não é útil para:
db.users.find({ lastLoginAt: { $lt: ISODate("2025-01-01") } })
Essa segunda consulta precisa de um índice que comece com lastLoginAt, ou precisa escanear.
Quando Usar Índices
Os índices são mais benéficos para campos que são frequentemente usados em:
- Critérios de consulta (
find(),findOne()): Campos usados no documentofilterde suas consultas. - Critérios de ordenação (
sort()): Campos usados para ordenar os resultados de suas consultas. - Campo
_id: Por padrão, o MongoDB cria um índice no campo_id, garantindo unicidade e buscas rápidas por ID.
No entanto, os índices também têm um custo:
- Espaço de armazenamento: Índices consomem espaço em disco.
- Desempenho de escrita: Os índices precisam ser atualizados sempre que documentos são inseridos, atualizados ou excluídos, o que pode tornar as operações de escrita mais lentas.
- Pressão de memória: Páginas de índice frequentemente usadas competem por cache. Muitos índices grandes podem dificultar a manutenção do conjunto de trabalho na memória.
Portanto, é crucial criar índices estrategicamente, focando em campos que trarão os ganhos de desempenho mais significativos para suas operações de leitura comuns.
Criando e Gerenciando Índices
O MongoDB fornece o método createIndex() para criar índices e getIndexes() para visualizar os existentes. O método dropIndex() é usado para removê-los.
Criação Básica de Índice
Para criar um índice de campo único, você especifica o nome do campo e o tipo de índice (geralmente 1 para ordem ascendente ou -1 para descendente).
db.collection.createIndex( { fieldName: 1 } );
Exemplo: Indexando um campo username em ordem ascendente:
db.users.createIndex( { username: 1 } );
Visualizando Índices
Para ver os índices em uma coleção:
db.collection.getIndexes();
Exemplo: Visualizando índices na coleção users:
db.users.getIndexes();
Isso retornará um array de definições de índice, incluindo o índice _id padrão.
Em uma coleção movimentada, crie índices deliberadamente. As versões modernas do MongoDB suportam construções de índice online em muitos casos comuns, mas as construções de índice ainda consomem CPU, E/S de disco e memória. Em sistemas de produção, agende grandes construções de índice durante períodos mais calmos e monitore o atraso de replicação se você executar um conjunto de réplicas.
Removendo Índices
Para remover um índice:
db.collection.dropIndex( "indexName" );
Você pode encontrar o indexName na saída de getIndexes(). Alternativamente, você pode remover um índice especificando o(s) campo(s) indexado(s) no mesmo formato de createIndex():
db.collection.dropIndex( { fieldName: 1 } );
Exemplo: Removendo o índice username:
db.users.dropIndex( "username_1" ); // Usando o nome do índice
// OU
db.users.dropIndex( { username: 1 } ); // Usando a definição do índice
Antes de remover um índice, verifique se algo ainda o utiliza:
db.users.aggregate([{ $indexStats: {} }])
Isso mostra contadores de acesso desde que o servidor foi iniciado. Um contador zero é uma pista, não uma prova absoluta. O servidor pode ter reiniciado recentemente, ou a consulta pode ser executada apenas durante um trabalho semanal. Para sistemas importantes, combine $indexStats, pesquisa no código da aplicação, logs de consulta e um curto período de observação.
Índices Compostos
Índices compostos envolvem múltiplos campos. A ordem dos campos em um índice composto é crítica. O MongoDB usa índices compostos para consultas que envolvem múltiplos campos nas cláusulas filter ou sort.
Quando Usar Índices Compostos
Índices compostos são mais eficazes quando suas consultas frequentemente filtram ou ordenam por uma combinação de campos. O índice pode satisfazer consultas que correspondem aos campos na mesma ordem em que são definidos no índice ou no prefixo do índice.
Exemplo: Considere uma coleção de orders com campos como userId, orderDate e status. Se você consulta frequentemente pedidos por um usuário específico e os ordena por data, um índice composto em { userId: 1, orderDate: 1 } seria altamente benéfico.
db.orders.createIndex( { userId: 1, orderDate: 1 } );
Este índice pode suportar eficientemente consultas como:
db.orders.find( { userId: "user123" } ).sort( { orderDate: -1 } )db.orders.find( { userId: "user123", orderDate: { $lt: ISODate() } } )
No entanto, pode não ser tão eficaz para consultas que filtram apenas por orderDate se userId não for também especificado, ou se os campos estiverem em uma ordem diferente.
A Ordem dos Campos Importa
A ordem dos campos em um índice composto determina quais padrões de consulta ele pode suportar bem. Uma regra prática útil é campos de igualdade primeiro, depois campos de ordenação, depois campos de intervalo. Isso é frequentemente chamado de diretriz ESR: igualdade, ordenação, intervalo. É uma diretriz, não uma lei, mas evita muitos designs de índice ruins.
Suponha que sua página de pedidos execute esta consulta:
db.orders.find({
tenantId: "t1",
status: "paid",
createdAt: { $gte: ISODate("2025-01-01") }
}).sort({ createdAt: -1 })
Um índice razoável poderia ser:
db.orders.createIndex({ tenantId: 1, status: 1, createdAt: -1 })
tenantId e status são filtros de igualdade. createdAt suporta a ordenação e o intervalo. Se você criar { createdAt: -1, status: 1, tenantId: 1 }, o MongoDB ainda pode usá-lo em alguns casos, mas geralmente é menos alinhado com esta consulta.
Para consultas que ordenam resultados, a ordem dos campos no índice deve corresponder à ordem dos campos na operação sort() para desempenho ideal. Se uma consulta inclui tanto um filtro quanto uma ordenação, e o índice corresponde aos campos do filtro, ele também pode ser usado para ordenação sem uma varredura de coleção separada para ordenação.
Índices compostos também podem servir consultas de prefixo. Um índice em { tenantId: 1, status: 1, createdAt: -1 } pode ajudar uma consulta apenas em tenantId, ou tenantId mais status. Geralmente não pode ajudar muito com uma consulta apenas em status porque status não é o campo principal.
Consultas de Cobertura
Uma consulta de cobertura é uma consulta onde o MongoDB pode satisfazer toda a consulta usando apenas o índice. Isso significa que o índice contém todos os campos que estão sendo consultados e projetados. Consultas de cobertura evitam buscar documentos da própria coleção, tornando-as extremamente rápidas.
Como Alcançar Consultas de Cobertura
Para alcançar uma consulta de cobertura, garanta que:
- Você tenha um índice que inclua todos os campos usados no filtro da consulta.
- Você inclua apenas esses campos indexados (ou um subconjunto deles) em sua projeção.
Exemplo: Considere uma coleção employees com campos name, age e city. Se você tem um índice { city: 1, age: 1 } e deseja recuperar os nomes e idades dos funcionários em uma cidade específica, você pode criar uma consulta de cobertura:
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } ).explain()
Nesta consulta, city está no índice, e name e age estão incluídos na projeção. Se o índice também contivesse name e age, seria uma consulta de cobertura.
Vamos refinar o índice e a consulta para uma verdadeira consulta de cobertura:
// Crie um índice que inclua todos os campos necessários para a consulta e projeção
db.employees.createIndex( { city: 1, age: 1, name: 1 } );
// Agora, uma consulta que filtra por cidade e projeta nome e idade pode ser coberta
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } )
Quando você executa explain("executionStats") nesta consulta, um plano coberto deve examinar chaves de índice sem buscar documentos completos da coleção. Em muitos planos de explicação, isso significa que você verá um IXSCAN sem um estágio FETCH, e totalDocsExamined deve ser 0. A saída de explicação varia de acordo com a versão do MongoDB e a forma da consulta, então foque nos estágios reais do plano e nos contadores examinados, em vez de procurar por um rótulo exato.
Consultas de cobertura são úteis para caminhos de leitura intensa, como autocomplete, pequenas visualizações de lista ou verificações de permissão. Elas são menos úteis se a projeção incluir campos grandes, muitos campos ou campos que mudam constantemente. Adicionar muitos campos a um índice apenas para cobrir uma consulta pode criar um índice volumoso que prejudica o desempenho de escrita.
Outros Tipos Importantes de Índice
O MongoDB oferece vários tipos de índice para casos de uso específicos:
Índices Multichave
Índices multichave são criados automaticamente quando você indexa um campo de array. Eles permitem consultar elementos dentro de arrays.
Exemplo: Se você tem uma coleção products com um campo de array tags como ["electronics", "gadgets"]:
db.products.createIndex( { tags: 1 } );
Este índice suportará consultas como db.products.find( { tags: "electronics" } ).
Arrays exigem cuidado extra em índices compostos. Um índice multichave armazena entradas para elementos do array, o que pode aumentar rapidamente o tamanho do índice. O MongoDB também tem restrições em torno de índices compostos multichave quando mais de um campo indexado pode conter arrays no mesmo documento. Se seu modelo de dados tem vários arrays e filtros complexos, teste a consulta exata com dados representativos antes de assumir que um índice composto se comportará como um índice de campo escalar.
Índices de Texto
Índices de texto suportam pesquisa eficiente de conteúdo de string em documentos. Eles são usados para consultas de pesquisa de texto usando o operador $text.
db.articles.createIndex( { content: "text" } );
Isso permite pesquisas como: db.articles.find( { $text: { $search: "desempenho de banco de dados" } } ).
Índices de texto são úteis para pesquisa de texto básica, mas não são uma plataforma de pesquisa completa. Se você precisa de ajuste avançado de relevância, tolerância a erros de digitação, facetas, destaque ou comportamento de pesquisa específico de idioma, o MongoDB Atlas Search ou um mecanismo de busca dedicado pode ser uma escolha melhor.
Índices Geoespaciais
Índices geoespaciais são usados para consulta eficiente de dados geográficos usando os operadores $near, $geoWithin e $geoIntersects.
db.locations.createIndex( { loc: "2dsphere" } ); // Para índice 2dsphere
Índices Únicos
Índices únicos impõem unicidade para um campo ou uma combinação de campos. Se um valor duplicado for inserido ou atualizado, o MongoDB retornará um erro.
db.users.createIndex( { email: 1 }, { unique: true } );
Para tabelas de usuário em produção, normalize antes de impor a unicidade. Endereços de e-mail são um exemplo comum. Se sua aplicação trata [email protected] e [email protected] como o mesmo usuário, armazene um campo normalizado como emailLower e coloque o índice único lá. Não confie apenas no código da aplicação para prevenir duplicatas sob concorrência.
Índices Parciais
Índices parciais indexam apenas documentos que correspondem a uma expressão de filtro. Eles são úteis quando uma consulta se concentra em um subconjunto de uma coleção.
db.orders.createIndex(
{ tenantId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "open" } }
)
Isso pode ajudar se sua aplicação lê frequentemente pedidos abertos e pedidos fechados compõem a maior parte da coleção. O índice é menor porque exclui documentos que não correspondem ao filtro parcial. A consulta deve incluir uma condição compatível para que o MongoDB o use.
Índices TTL
Índices TTL removem automaticamente documentos após um tempo configurado. Eles são comumente usados para sessões, tokens temporários ou eventos de curta duração.
db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
)
A exclusão TTL não é instantânea no momento exato de expiração. O MongoDB remove documentos expirados em segundo plano. Use-o para limpeza, não para temporização de segurança precisa onde um token deve se tornar inválido imediatamente. Sua aplicação ainda deve verificar a expiração durante as leituras.
Análise de Desempenho com explain()
Entender como o MongoDB executa suas consultas é crucial para otimizá-las. O método explain() fornece insights sobre o plano de execução da consulta, incluindo se um índice foi usado e como.
db.collection.find( {...} ).explain( "executionStats" );
Campos-chave para procurar na saída de explain():
winningPlan.stage: Indica o estágio do plano de execução (por exemplo,COLLSCANpara varredura de coleção,IXSCANpara varredura de índice).executionStats.totalKeysExamined: O número de chaves de índice examinadas.executionStats.totalDocsExamined: O número de documentos examinados.
Um bom plano de execução terá totalDocsExamined próximo ou igual ao número de documentos retornados, e totalKeysExamined significativamente menor que o número total de documentos na coleção. Se totalDocsExamined for muito alto, ou COLLSCAN for usado, sugere que um índice está faltando ou não está sendo usado efetivamente.
Aqui está a maneira rápida como leio um plano de explicação:
- Procure por
COLLSCAN. Se este é um caminho intenso e a coleção é grande, esse é geralmente o primeiro problema. - Procure por
IXSCANseguido porFETCH. Uma busca é normal quando a consulta precisa de campos fora do índice, mas a examinação excessiva de documentos significa que o índice não é seletivo o suficiente. - Compare
nReturned,totalKeysExaminedetotalDocsExamined. Retornar 20 documentos após examinar 25 chaves é saudável. Retornar 20 documentos após examinar 500.000 chaves não é. - Fique atento a ordenações em memória. Se o MongoDB tiver que ordenar um grande conjunto de resultados após a filtragem, um índice composto que suporte a ordenação pode ajudar.
Use filtros realistas ao testar. Um plano de explicação para tenantId: "demo" pode não corresponder a um grande inquilino com milhões de documentos. A distribuição dos dados importa.
Um Passo a Passo Prático de Design de Índice
Imagine uma aplicação com uma coleção tickets. Agentes de suporte usam uma página de fila com estes filtros:
db.tickets.find({
tenantId: "acme",
status: "open",
assigneeId: "u123"
}).sort({ updatedAt: -1 }).limit(50)
Comece com a forma da consulta, não com a lista de campos. A coleção é multi-inquilino, agentes geralmente filtram por status e responsável, e a interface ordena as atualizações mais recentes primeiro. Um índice prático é:
db.tickets.createIndex({
tenantId: 1,
status: 1,
assigneeId: 1,
updatedAt: -1
})
Agora considere outra página: gerentes visualizam todos os tickets abertos, independentemente do responsável:
db.tickets.find({
tenantId: "acme",
status: "open"
}).sort({ updatedAt: -1 }).limit(100)
O índice anterior pode usar o prefixo { tenantId, status }, mas assigneeId está antes de updatedAt, então pode não suportar a ordenação tão bem para esta consulta de gerente. Você pode precisar de um segundo índice:
db.tickets.createIndex({
tenantId: 1,
status: 1,
updatedAt: -1
})
Isso é uma troca normal. Um índice raramente serve perfeitamente a todas as telas. O trabalho é suportar os caminhos importantes sem criar uma pilha de índices sobrepostos que todos custam escritas.
Melhores Práticas para Indexação no MongoDB
- Indexe apenas o que você precisa: Evite criar índices em campos que raramente são consultados ou ordenados. Cada índice adiciona sobrecarga.
- Use índices compostos com sabedoria: Ordene os campos corretamente com base nos padrões de consulta. Considere os campos mais seletivos primeiro.
- Busque consultas de cobertura: Se o desempenho de leitura é crítico, projete índices para cobrir operações de leitura comuns.
- Monitore o uso de índices: Revise regularmente o uso de índices usando
explain()edb.collection.aggregate([{ $indexStats: {} }])para identificar índices não utilizados ou ineficientes. - Considere a seletividade do índice: Índices em campos com baixa cardinalidade (poucos valores distintos) podem não ser tão eficazes quanto aqueles em campos com alta cardinalidade.
- Mantenha os índices pequenos: Evite incluir campos grandes ou arrays em índices, a menos que seja absolutamente necessário para consultas de cobertura.
- Teste seus índices: Sempre teste o impacto de novos índices tanto no desempenho de leitura quanto de escrita sob condições de carga realistas.
- Remova índices redundantes com cuidado: Se você tem
{ a: 1, b: 1 }, um índice separado{ a: 1 }pode ser redundante para muitas cargas de trabalho. Confirme o uso antes de excluir. - Projete em torno de telas e trabalhos reais: Os índices devem mapear para o comportamento da aplicação: consulta de login, página de fila, filtro de relatório, varredura de trabalhador em segundo plano.
- Reveja após mudanças de esquema: Um novo campo, nova ordem de ordenação ou novo modelo de inquilino pode tornar um índice antigo menos útil.
Como é a Boa Indexação
Uma boa indexação no MongoDB geralmente é silenciosa. As consultas importantes examinam aproximadamente a quantidade de dados que retornam. Ordenações não se transformam em trabalho caro. As escritas não são sobrecarregadas por uma dúzia de índices especulativos. Quando um novo recurso adiciona uma nova forma de consulta, você a testa com explain("executionStats") antes que se torne um incidente de produção.
O hábito prático é simples: colete a consulta real, projete o menor índice útil para essa forma de consulta, teste com dados representativos e continue verificando o uso do índice ao longo do tempo. Esse hábito fará mais pelo desempenho do MongoDB do que memorizar todos os tipos de índice.