Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Índices no MongoDB: medir COLLSCAN e IXSCAN

Crie índices simples, compostos e únicos no MongoDB e compare a mesma consulta antes e depois com explain, documentos e chaves examinados.

Rodolfo Mori5 min de leitura

Um índice no MongoDB é uma estrutura ordenada que ajuda o banco a encontrar documentos sem testar toda a coleção. Nesta lição, você vai medir a mesma consulta em 80 mil pedidos: primeiro com COLLSCAN, depois com IXSCAN.

O método de prova é explain("executionStats"). Ele mostra plano escolhido, quantos resultados voltaram, quantos documentos foram examinados e quantas chaves do índice foram percorridas.

O índice no fim do catálogo não guarda os livros

Pense no índice alfabético de um catálogo. Para encontrar pedidos do cliente 42, você consulta a lista ordenada e segue os apontamentos, em vez de abrir 80 mil fichas. No MongoDB, valores indexados e referências aos registros cumprem essa função; o estágio IXSCAN percorre o índice.

A comparação tem um limite importante: índice não é uma cópia gratuita. Ele ocupa disco e memória, e toda inserção, remoção ou mudança nos campos indexados precisa mantê-lo. Um catálogo com índice para qualquer detalhe ficaria caro de atualizar. No banco, o problema é o mesmo em outra escala.

Também não basta ver IXSCAN e declarar vitória. O plano pode percorrer muitas chaves, buscar muitos documentos ou ordenar depois. Por isso vamos comparar contagens, não apenas o nome do estágio.

Se filtros e projeções ainda forem novos, revise o CRUD MongoDB com Node.js.

Prepare um conjunto grande o bastante para enxergar

Instale o driver oficial em uma pasta vazia:

bash
npm init -y
npm install mongodb@7.5.0
added 12 packages

Crie indices.mjs, conecte e limpe somente o database do laboratório:

js
import { MongoClient } from "mongodb";

const uri = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017";
const client = new MongoClient(uri);
await client.connect();

const db = client.db("devclub_indices");
await db.dropDatabase();
const pedidos = db.collection("pedidos");

console.log({ database: db.databaseName, estado: "limpo" });
{ database: 'devclub_indices', estado: 'limpo' }

Gere 80 mil pedidos em lotes de 4 mil. O padrão é determinístico: mil clientes se repetem; o status muda a cada bloco; a data avança um minuto por documento. Isso não simula toda distribuição de uma loja real, mas permite repetir a comparação sob a mesma forma de dados.

js
const total = 80_000;
const tamanhoLote = 4_000;
const inicio = Date.parse("2025-01-01T00:00:00.000Z");

for (let primeiro = 0; primeiro < total; primeiro += tamanhoLote) {
  const lote = [];

  for (let i = primeiro; i < Math.min(primeiro + tamanhoLote, total); i += 1) {
    lote.push({
      _id: i,
      clienteId: `cliente-${String(i % 1000).padStart(3, "0")}`,
      status: ["pago", "pendente", "enviado", "cancelado"]
        [Math.floor(i / 1000) % 4],
      criadoEm: new Date(inicio + i * 60_000),
      total: (i % 200) + 20,
    });
  }

  await pedidos.insertMany(lote, { ordered: false });
}

console.log({ documentos: await pedidos.countDocuments() });
{ documentos: 80000 }

O laboratório foi executado em Apple Silicon dentro de Docker. Tempos e tamanho de índice variam por máquina, cache e armazenamento; as contagens examinadas abaixo são a evidência comparável para este conjunto controlado.

Defina uma consulta antes de inventar um índice

A tela quer pedidos pagos do cliente 42, do mais recente para o mais antigo, e mostra somente cliente, status e data. Escreva primeiro o padrão real:

js
const filtro = {
  clienteId: "cliente-042",
  status: "pago",
};
const projecao = {
  _id: 0,
  clienteId: 1,
  status: 1,
  criadoEm: 1,
};

const amostra = await pedidos
  .find(filtro, { projection: projecao })
  .sort({ criadoEm: -1 })
  .limit(2)
  .toArray();

console.log({ encontrados: await pedidos.countDocuments(filtro), amostra: amostra.length });
{ encontrados: 20, amostra: 2 }

Criar índice antes de conhecer filtro, ordenação e projeção é como produzir um mapa sem saber qual caminho alguém percorre. O índice deve apoiar uma forma de consulta, não apenas um campo que parece importante.

Meça o plano sem índice adequado

O plano completo é grande. Esta função percorre a árvore e coleta os nomes de estágio, sem alterar as estatísticas que vieram do servidor:

js
function etapasDoPlano(no, etapas = []) {
  if (!no || typeof no !== "object") return etapas;
  if (typeof no.stage === "string") etapas.push(no.stage);

  for (const valor of Object.values(no)) {
    if (valor && typeof valor === "object") etapasDoPlano(valor, etapas);
  }

  return [...new Set(etapas)];
}

console.log(etapasDoPlano({ stage: "COLLSCAN" }));
[ 'COLLSCAN' ]

Agora rode explain("executionStats") sobre a consulta inteira, sem limit, e extraia as contagens:

js
const antes = await pedidos
  .find(filtro, { projection: projecao })
  .sort({ criadoEm: -1 })
  .explain("executionStats");

console.log({
  momento: "antes",
  etapas: etapasDoPlano(antes.queryPlanner.winningPlan),
  retornados: antes.executionStats.nReturned,
  documentosExaminados: antes.executionStats.totalDocsExamined,
  chavesExaminadas: antes.executionStats.totalKeysExamined,
});
{ momento: 'antes', etapas: [ 'SORT', 'PROJECTION_SIMPLE', 'COLLSCAN' ], retornados: 20, documentosExaminados: 80000, chavesExaminadas: 0 }

O banco devolveu 20 documentos depois de examinar 80 mil. SORT também mostra que a ordenação não veio pronta de uma estrutura adequada. Isso não prova que toda COLLSCAN é um bug: ler grande parte de uma coleção pequena ou gerar um relatório eventual pode justificar varredura. Aqui, a diferença entre lidos e retornados é o sinal concreto.

Crie um índice composto na ordem da consulta

O filtro usa igualdade em clienteId e status; a saída ordena por criadoEm decrescente. Crie um índice composto nessa sequência e dê um nome legível:

js
const nomeDoIndice = await pedidos.createIndex(
  { clienteId: 1, status: 1, criadoEm: -1 },
  { name: "cliente_status_data" },
);

console.log({ indiceCriado: nomeDoIndice });
{ indiceCriado: 'cliente_status_data' }

Os campos de igualdade vêm antes; a ordenação vem depois. Essa decisão segue o padrão Equality, Sort, Range como ponto de partida. Não copie a ordem para toda consulta: se houver intervalo, outra ordenação ou collation, confirme com o plano real.

Um índice composto também tem prefixos. Este começa por clienteId, então pode apoiar filtros por clienteId e por clienteId + status. Ele não é equivalente a um índice que começa apenas por status ou criadoEm.

Meça novamente a mesma consulta

Sem alterar filtro, projeção nem ordenação, execute outro explain:

js
const depois = await pedidos
  .find(filtro, { projection: projecao })
  .sort({ criadoEm: -1 })
  .explain("executionStats");

console.log({
  momento: "depois",
  indice: nomeDoIndice,
  etapas: etapasDoPlano(depois.queryPlanner.winningPlan),
  retornados: depois.executionStats.nReturned,
  documentosExaminados: depois.executionStats.totalDocsExamined,
  chavesExaminadas: depois.executionStats.totalKeysExamined,
});
{ momento: 'depois', indice: 'cliente_status_data', etapas: [ 'PROJECTION_COVERED', 'IXSCAN' ], retornados: 20, documentosExaminados: 0, chavesExaminadas: 20 }

IXSCAN percorreu 20 chaves. PROJECTION_COVERED e zero documentos examinados indicam consulta coberta: todos os campos necessários ao filtro, à ordenação e à resposta estavam no índice. Se a projeção incluísse total, que não está nele, o banco precisaria buscar documentos; isso pode continuar sendo um bom plano, apenas deixa de ser coberto.

O ganho observado pertence a esta consulta e a esta massa. Índices apoiam também $match, $sort e combinações dentro de pipelines; veja o relatório em aggregation pipeline.

Índice único transforma duplicação em erro

Índice também pode expressar unicidade. Para login, queremos um e-mail por cliente. Crie o índice antes dos dados duplicados:

js
const clientes = db.collection("clientes");

console.log(await clientes.createIndex(
  { email: 1 },
  { unique: true, name: "email_unico" },
));

await clientes.insertOne({ nome: "Ana", email: "ana@exemplo.com" });
email_unico

O índice garante unicidade do valor armazenado. Ele não normaliza letras maiúsculas, espaços ou acentos segundo a regra do produto. Decida se a aplicação guarda um campo normalizado e considere collation quando a comparação exigir.

Tente agora repetir o e-mail:

js
try {
  await clientes.insertOne({
    nome: "Outra Ana",
    email: "ana@exemplo.com",
  });
} catch (erro) {
  console.log({
    erro: "email duplicado",
    codigo: erro.code,
    indice: "email_unico",
  });
}
{ erro: 'email duplicado', codigo: 11000, indice: 'email_unico' }

O código 11000 é a evidência do conflito. Faça a API traduzir isso para uma mensagem útil, mas preserve o índice: uma verificação findOne() antes da inserção sofre corrida quando duas requisições chegam juntas.

Missão: quebre a cobertura e explique o novo plano

Repita o explain de “depois”, acrescentando total: 1 à projeção. Colete as mesmas estatísticas:

js
const comTotal = await pedidos
  .find(filtro, {
    projection: { ...projecao, total: 1 },
  })
  .sort({ criadoEm: -1 })
  .explain("executionStats");

console.log({
  etapas: etapasDoPlano(comTotal.queryPlanner.winningPlan),
  retornados: comTotal.executionStats.nReturned,
  documentosExaminados: comTotal.executionStats.totalDocsExamined,
  chavesExaminadas: comTotal.executionStats.totalKeysExamined,
});

await client.close();
{ etapas: [ 'PROJECTION_SIMPLE', 'FETCH', 'IXSCAN' ], retornados: 20, documentosExaminados: 20, chavesExaminadas: 20 }

A missão termina quando você explica a mudança sem chamar o plano de “ruim”: IXSCAN ainda encontra as 20 entradas, FETCH busca os 20 documentos para ler total, e a consulta deixa de ser coberta. Incluir total no índice só faz sentido se a economia de leitura compensar espaço e manutenção nas escritas.

Compare essa medição com índices no PostgreSQL: os nomes do plano mudam, mas a disciplina é a mesma — medir a consulta real antes e depois, e não colecionar índices por intuição.

  • mongodb
  • indices
  • explain
  • performance
  • banco de dados

Perguntas frequentes

O que significa COLLSCAN no MongoDB?
Significa collection scan. O plano percorre documentos da coleção para testar o filtro. Em coleção pequena pode ser aceitável; em consulta frequente sobre muitos dados, é sinal para investigar um índice adequado.
O que significa IXSCAN?
Significa index scan. O plano percorre chaves de um índice para localizar resultados. Ainda pode precisar buscar documentos, por isso confira também totalKeysExamined e totalDocsExamined.
Quanto mais índices, melhor?
Não. Cada índice ocupa espaço e precisa ser atualizado nas escritas. Crie índices para padrões reais de filtro e ordenação, depois monitore uso e remova os que não justificam o custo.
A ordem dos campos em um índice composto importa?
Sim. Índices compostos suportam prefixos que começam pelos primeiros campos. Igualdade, ordenação e intervalo da consulta orientam a ordem; use explain para confirmar o plano no seu conjunto de dados.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em MongoDB 8.0.28, MongoDB Node.js Driver 7.5.0, Node 26.3.0 e Docker 29.5.3 em Apple Silicon, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MongoDB Manual — Indexes — mongodb.com
  2. MongoDB Manual — Create Indexes to Support Queries — mongodb.com
  3. MongoDB Manual — Explain Results — mongodb.com
  4. MongoDB Node.js Driver — Indexes — mongodb.com

Continue por aqui