O NoSQL nº 1 do mercado

MongoDB do primeiro documento ao cluster em produção

Esta apostila é o aprofundamento aplicado do banco de documentos mais pedido em vagas de backend e full-stack. O módulo 4 da apostila de NoSQL foi a degustação; aqui você constrói o pacote completo: modelagem profissional, o aggregation pipeline por inteiro, índices que passam no explain(), replica sets, sharding, o ecossistema Atlas e a operação de nível sênior.

10 módulosModelagem & padrõesAggregation PipelineReplica Set & ShardingAtlas Search + VectorExercícios com gabarito
💡 Como esta apostila se relaciona com a de NoSQL

A apostila de NoSQL responde quando usar documento e como ele se compara às outras famílias. Esta responde o como, ponta a ponta, só com MongoDB. Onde a teoria de sistemas distribuídos (CAP, quóruns, LSM-Tree) aparecer aqui, será um resumo — aprofunde nos módulos 3 e 9 de lá. E toda a parte de modelagem por padrões de acesso tem tratamento dedicado na apostila de Modelagem de Dados NoSQL.

MÓDULO 01 · BÁSICO

Modelo de documento, BSON e CRUD

Objetivo: entender o que é um documento, como o MongoDB o guarda (BSON), e dominar o CRUD real — incluindo o driver e o pool de conexões, que iniciantes ignoram e produção cobra.

1.1 O documento: a unidade de tudo

Um documento é um objeto tipo JSON: pares campo-valor, onde o valor pode ser um escalar, um array ou outro objeto aninhado. A coleção agrupa documentos; o database agrupa coleções. Não há esquema declarado — dois documentos da mesma coleção podem ter campos diferentes (o que é liberdade e perigo em doses iguais).

// Um documento de pedido — o que em SQL seriam 3-4 tabelas com JOIN
{
  "_id": ObjectId("665f1b2c9d3e4a0012ab34cd"),
  "numero": 10432,
  "cliente": { "id": "u1", "nome": "Ana Souza" },
  "itens": [
    { "sku": "CAM-01", "qtd": 2, "preco": 79.90 },
    { "sku": "MEIA-03", "qtd": 1, "preco": 19.90 }
  ],
  "status": "pago",
  "criadoEm": ISODate("2026-06-04T13:20:00Z")
}

1.2 BSON: o que o MongoDB realmente grava

Você escreve JSON, mas o MongoDB armazena e trafega BSON (Binary JSON): um formato binário que adiciona tipos que JSON não tem e torna a varredura eficiente (cada campo é prefixado por tipo e tamanho).

⚠️ Dinheiro em double é bug garantido

0.1 + 0.2 não dá 0.3 em ponto flutuante. Para valores monetários, use Decimal128 (NumberDecimal("79.90") no shell) ou guarde centavos como inteiro. Esse erro passa em teste e aparece na conciliação financeira.

1.3 CRUD com tradução SQL

SQLMongoDB (mongosh / driver)
INSERT INTO pedidos ...db.pedidos.insertOne({...}) / insertMany([...])
SELECT * FROM pedidos WHERE status='pago'db.pedidos.find({ status: "pago" })
... ORDER BY criadoEm DESC LIMIT 20.sort({ criadoEm: -1 }).limit(20)
SELECT numero, status FROM ....find({}, { numero: 1, status: 1, _id: 0 }) (projeção)
UPDATE pedidos SET status='enviado' WHERE ...db.pedidos.updateOne({...}, { $set: { status: "enviado" } })
DELETE FROM pedidos WHERE ...db.pedidos.deleteMany({...})
INSERT ... ON CONFLICT UPDATE (upsert)updateOne(filtro, update, { upsert: true })
// Operadores de consulta que você usa todo dia
db.pedidos.find({ valor: { $gte: 100, $lt: 500 } })          // faixa
db.pedidos.find({ status: { $in: ["pago", "enviado"] } })       // lista
db.pedidos.find({ "cliente.nome": "Ana Souza" })              // campo aninhado por dot notation
db.pedidos.find({ itens: { $elemMatch: { sku: "CAM-01", qtd: { $gt: 1 } } } })  // array de objetos
db.pedidos.find({ cupom: { $exists: false } })                // campo ausente

// Operadores de update
{ $set: {...} }  { $inc: { estoque: -1 } }  { $push: { itens: {...} } }
{ $pull: { tags: "promo" } }  { $addToSet: { tags: "vip" } }  { $unset: { cupom: "" } }

1.4 O driver e o pool de conexões o que produção cobra

No shell tudo parece mágico. Na aplicação, você usa um driver (Node, Python/PyMongo, Java, Go...) e ele mantém um pool de conexões — um conjunto de sockets reutilizados. Erros clássicos:

// Node — UMA instância de MongoClient para todo o processo, criada no boot
const client = new MongoClient(uri, {
  maxPoolSize: 20,          // teto de conexões simultâneas por instância da app
  minPoolSize: 2,
  serverSelectionTimeoutMS: 5000,
  retryWrites: true    // reexecuta escrita idempotente uma vez em falha de rede transitória
});
await client.connect();   // no startup, não a cada request
export const db = client.db("loja");
⚠️ Conectar por request derruba o cluster

Criar um MongoClient a cada requisição HTTP abre milhares de conexões e esgota o servidor (connection storm). Uma instância, criada no boot, compartilhada. Em serverless (Lambda/Cloud Functions), guarde o client em escopo global do módulo para reaproveitar entre invocações "quentes" e limite maxPoolSize (ex.: 5) — cada instância multiplica o pool.

💼 Mercado de trabalho

Perguntas de triagem: "O que é o _id e o que é um ObjectId?", "Qual o limite de tamanho de um documento?", "Como você guardaria um valor em reais?" A que separa júnior de pleno: "Por que não abrir uma conexão por request?". Saber falar de pool, retryWrites e serverless já sinaliza experiência real.

✏️ Exercício 1 — Leitura de documento e tipos

(a) Você precisa dos 10 pedidos mais recentes do cliente u1, trazendo só numero, valor e status. Escreva a consulta. (b) Por que criadoEm deve ser ISODate e não string? (c) O preço unitário deve ser que tipo?

Gabarito: (a) db.pedidos.find({ "cliente.id": "u1" }, { numero:1, valor:1, status:1, _id:0 }).sort({ criadoEm:-1 }).limit(10). (b) Com Date, comparações de faixa e ordenação são corretas e o índice funciona como intervalo; string ordena lexicograficamente e quebra fusos. (c) Decimal128 (ou centavos em Int) — nunca double.

MÓDULO 02 · BÁSICO

Modelagem: embedding vs referencing

Objetivo: tomar a decisão que define o sucesso de um projeto MongoDB — aninhar ou referenciar — a partir de cardinalidade e padrão de acesso, e reconhecer os antipadrões antes que cheguem à produção.

2.1 A pergunta central

No relacional você normaliza por reflexo. No MongoDB, a pergunta é: estes dois dados são lidos juntos quase sempre? Se sim, aninhe (embedding) — uma leitura resolve. Se não, ou se o lado "muitos" cresce sem limite, referencie (guardar o _id do outro, como uma FK que você resolve na aplicação ou com $lookup).

// EMBEDDING — endereços vivem dentro do usuário
{ _id: "u1", nome: "Ana", enderecos: [ { rotulo: "casa", cidade: "Recife" } ] }

// REFERENCING — pedidos apontam para o usuário
{ _id: "p1", clienteId: "u1", valor: NumberDecimal("250.00") }

2.2 A regra da cardinalidade

RelaçãoExemploPadrão recomendado
1:1usuário ↔ perfilEmbedding (ou mesmo documento)
1:poucos (limite conhecido e pequeno)usuário ↔ endereços (≤ ~10)Embedding em array
1:muitos (cresce, mas cabe)post ↔ comentáriosReferencing, ou subset (embutir os N recentes + referenciar o resto)
1:milhões / ilimitadosensor ↔ leituras; canal ↔ mensagensReferencing sempre; considerar bucket pattern
muitos:muitosproduto ↔ tags; aluno ↔ turmasArray de referências no lado que consulta mais; às vezes nos dois

2.3 Os três antipadrões que explodem em produção

⚠️ 1 — Arrays sem limite (unbounded arrays)

Embutir "todos os comentários do post" funciona no protótipo. Com o tempo o documento passa de 16 MB (erro), e antes disso cada $push reescreve o array inteiro e pode mover o documento no disco. Regra: se o array pode crescer indefinidamente, ele não pode ser embutido.

⚠️ 2 — Documentos que só incham

Um campo historicoDeStatus que só recebe $push ao longo de meses transforma um documento pequeno em um monstro que ocupa cache e trafega inteiro a cada leitura. Mova histórico para coleção própria.

⚠️ 3 — Normalizar como se fosse SQL

Cinco coleções (usuarios, enderecos, telefones, preferencias, fotos) que sempre são lidas juntas viram cinco consultas ou um $lookup encadeado. Se nada disso faz sentido sozinho, é um documento.

2.4 Extended Reference: o meio-termo que o mercado usa

Referenciar puro obriga a um segundo fetch para exibir "o nome do cliente no pedido". Extended Reference duplica 2–3 campos estáveis do documento referenciado:

{ _id: "p1", clienteId: "u1",
  cliente: { nome: "Ana Souza", cidade: "Recife" },   // cópia para exibição rápida
  itens: [ { sku: "CAM-01", nome: "Camiseta Preta", preco: NumberDecimal("79.90"), qtd: 2 } ] }

O nome do produto e o preço dentro do item do pedido são dado histórico congelado: se o produto mudar de nome ou preço amanhã, o pedido de ontem não deve mudar. Aqui a duplicação não é um mal necessário — é a modelagem correta.

💼 Mercado de trabalho

O exercício de entrevista mais comum de MongoDB: "modele um blog / e-commerce / chat". O avaliador quer ouvir o raciocínio: liste as consultas da aplicação primeiro, decida embedding/referencing por cardinalidade + acesso, cite o limite de 16 MB e o antipadrão de array ilimitado, e justifique cada denormalização como histórico ou performance. Resultado sem raciocínio não pontua.

✏️ Exercício 2 — Modele um sistema de cursos

Entidades: cursos, aulas (dezenas por curso), alunos (milhares por curso), progresso do aluno por aula. Telas: página do curso com a lista de aulas; "meus cursos" do aluno; marcar aula como concluída.

Gabarito (uma boa resposta): cursos com aulas embutidas em array (dezenas, limite conhecido, sempre lidas com o curso) — cada aula com _id próprio. matriculas como coleção separada (1:milhares) com { alunoId, cursoId, cursoNome (extended ref), progresso: [ { aulaId, concluidaEm } ] } — o progresso é 1:dezenas, cabe embutido na matrícula. Índices: { alunoId: 1 } em matrículas (para "meus cursos"), { cursoId: 1, alunoId: 1 } único (evita matrícula dupla). Marcar concluída = $push/$addToSet no array progresso da matrícula.

MÓDULO 03 · INTERMEDIÁRIO

Aggregation Pipeline a fundo

Objetivo: dominar o pipeline de agregação — a ferramenta que faz do MongoDB um motor analítico, não só um armário de documentos. Estágios, $lookup, $facet, window functions e materialização.

3.1 O modelo mental: uma esteira de estágios

O aggregate() recebe um array de estágios. Cada estágio recebe o fluxo de documentos do anterior, transforma e passa adiante — como | no shell Unix. É GROUP BY + subqueries + JOIN + window functions, tudo encadeável.

db.pedidos.aggregate([
  { $match:   { criadoEm: { $gte: ISODate("2026-01-01") }, status: "pago" } },  // WHERE — o mais cedo possível
  { $unwind:  "$itens" },                                                // explode o array em 1 doc por item
  { $group:   { _id: "$itens.sku",
               receita: { $sum: { $multiply: ["$itens.preco", "$itens.qtd"] } },
               unidades: { $sum: "$itens.qtd" } } },
  { $sort:    { receita: -1 } },
  { $limit:   10 },
  { $project: { sku: "$_id", receita: 1, unidades: 1, _id: 0 } }
])

3.2 Os estágios que você vai usar

EstágioFazCuidado
$matchFiltra (WHERE)Ponha primeiro — só assim usa índice
$groupAgrupa e agrega ($sum, $avg, $min/$max, $push, $addToSet, $first/$last)Não usa índice; roda em memória (limite de 100 MB por estágio, senão allowDiskUse)
$project / $set / $unsetMolda campos, cria calculados$set (ex-$addFields) adiciona sem apagar o resto
$unwind1 documento por elemento de arrayMultiplica a contagem; use preserveNullAndEmptyArrays se precisar manter vazios
$lookupLEFT JOIN com outra coleçãoCaro; a coleção "de fora" precisa de índice no campo de junção
$facetVários sub-pipelines na mesma passadaÓtimo para "resultados + contagem total + agregados de filtro" numa request só
$merge / $outGrava o resultado numa coleção$merge atualiza incrementalmente (base de rollups); $out substitui tudo
$setWindowFieldsWindow functions (running total, rank, média móvel)Desde 5.0 — substitui gambiarras com $group + $unwind

3.3 $lookup: o JOIN que você deve evitar (mas às vezes precisa)

{ $lookup: {
    from: "clientes",
    localField: "clienteId",
    foreignField: "_id",
    as: "cliente"          // vira um ARRAY (mesmo que 0 ou 1) — some com $unwind depois
} }

Funciona, mas cada documento do fluxo dispara uma busca na coleção externa. Em escala, é o gargalo. As saídas de mercado: Extended Reference (módulo 2 — você já tem o nome do cliente no pedido, não precisa do $lookup), ou aceitar o $lookup só em telas de baixo volume (admin, relatório) com índice em clientes._id (que já existe) e no localField.

3.4 $facet: a tela de listagem com filtros numa request

db.produtos.aggregate([
  { $match: { categoria: "calcados" } },
  { $facet: {
      pagina:        [ { $sort: { vendas: -1 } }, { $skip: 0 }, { $limit: 20 } ],
      total:         [ { $count: "n" } ],
      porMarca:      [ { $group: { _id: "$marca", n: { $sum: 1 } } }, { $sort: { n: -1 } } ],
      faixaDePreco:  [ { $bucket: { groupBy: "$preco", boundaries: [0, 100, 300, 1000],
                        default: "1000+", output: { n: { $sum: 1 } } } } ]
  } }
])

Um único aggregate devolve os itens da página, a contagem para a paginação e as facetas da barra lateral — o que seriam 3–4 idas ao banco.

3.5 Materializar: rollups com $merge avançado

Dashboards que recalculam "vendas por dia dos últimos 2 anos" a cada carregamento são caros. Rode a agregação uma vez por hora (job agendado ou Atlas Trigger) e grave numa coleção vendas_diarias com $merge — as telas leem dela instantaneamente. É o padrão Computed/CQRS aplicado dentro do próprio MongoDB.

💼 Mercado de trabalho

Vaga de pleno/sênior quase sempre pede: "dado este schema, escreva um pipeline que retorne X". Treine $match → $group → $sort → $project de olhos fechados, saiba explicar por que $match vem primeiro (índice) e por que $lookup é caro, e conheça $facet — mencioná-lo em "como você faria a paginação com filtros?" impressiona.

✏️ Exercício 3 — Ticket médio por mês

Coleção pedidos com { criadoEm: Date, valor: Decimal128, status }. Escreva um pipeline que retorne, para 2026, o número de pedidos pagos e o ticket médio por mês, ordenado por mês.

Gabarito:
db.pedidos.aggregate([
 { $match: { status: "pago", criadoEm: { $gte: ISODate("2026-01-01"), $lt: ISODate("2027-01-01") } } },
 { $group: { _id: { $dateTrunc: { date: "$criadoEm", unit: "month" } }, pedidos: { $sum: 1 }, ticket: { $avg: "$valor" } } },
 { $sort: { _id: 1 } },
 { $project: { mes: "$_id", pedidos: 1, ticket: { $round: ["$ticket", 2] }, _id: 0 } }
])

Um índice { status: 1, criadoEm: 1 } faz o $match usar IXSCAN.

MÓDULO 04 · INTERMEDIÁRIO

Índices, ESR e explain()

Objetivo: parar de "adicionar índice por via das dúvidas" e passar a projetá-los a partir das consultas reais, lendo o plano de execução como um profissional.

4.1 Os tipos de índice

db.clientes.createIndex({ email: 1 }, { unique: true })            // único
db.pedidos.createIndex({ clienteId: 1, criadoEm: -1 })                // composto (ordem importa!)
db.produtos.createIndex({ nome: "text", descricao: "text" })          // texto (básico)
db.lojas.createIndex({ local: "2dsphere" })                          // geoespacial
db.eventos.createIndex({ expiraEm: 1 }, { expireAfterSeconds: 0 })    // TTL — apaga sozinho
db.pedidos.createIndex({ status: 1 }, { partialFilterExpression: { status: "aberto" } })  // parcial: só indexa o que importa
db.usuarios.createIndex({ cpf: 1 }, { unique: true, sparse: true })     // esparso: ignora docs sem o campo

4.2 A regra ESR para índices compostos

💡 Equality, Sort, Range — nessa ordem

Num índice composto, coloque primeiro os campos de igualdade (status: "pago"), depois o campo de ordenação (sort), por último os de faixa ($gt, $lt). Consulta: find({ status: "pago", valor: { $gt: 100 } }).sort({ criadoEm: -1 }) → índice { status: 1, criadoEm: -1, valor: 1 }.

Por quê: igualdade fixa um ponto de entrada estreito no índice; a ordenação sai "de graça" se vier logo depois (sem etapa de sort em memória); a faixa varre um trecho e é a última a "abrir o leque".

4.3 Lendo o explain("executionStats")

db.pedidos.find({ clienteId: "u1" }).sort({ criadoEm: -1 }).explain("executionStats")
O que procurarBomRuim
stageIXSCANFETCHCOLLSCAN (varreu a coleção toda)
Ordenaçãoausente, ou SORT sobre poucos docsSORT com SORT_KEY_GENERATOR sobre milhares (ou erro de 32 MB de sort sem índice)
totalDocsExamined vs nReturnedpróximos (ex.: 105 examinados, 100 retornados)examinou 1.000.000 para retornar 20
totalKeysExaminednReturned>> nReturned (índice pouco seletivo)

Covered query: se todos os campos da projeção (e do filtro) estão no índice, o plano mostra PROJECTION_COVERED e o FETCH some — o MongoDB nem toca nos documentos. É a consulta mais rápida possível.

4.4 O custo do índice e o working set

💼 Mercado de trabalho

O teste prático recorrente: "esta consulta está lenta, o que você faz?". Resposta que aprova: rodar explain("executionStats"), identificar COLLSCAN ou SORT em memória, propor um índice pela regra ESR, e validar comparando totalDocsExamined/nReturned antes e depois. Levar um print desse antes/depois no README de um projeto vale muito.

✏️ Exercício 4 — Projete o índice

Consulta mais frequente do sistema: db.eventos.find({ tenantId: X, tipo: "erro", ts: { $gte: A, $lt: B } }).sort({ ts: -1 }).limit(50). Qual índice? E se a maioria dos tipo for "info" e você quase nunca consultar esses?

Gabarito: Pela regra ESR: igualdades tenantId e tipo, depois o sort ts (que também serve de range) → { tenantId: 1, tipo: 1, ts: -1 }. Como a consulta ordena e filtra por ts na mesma direção, um único campo ts cobre sort + range. Se "info" domina e não é consultado, um índice parcial { tenantId: 1, tipo: 1, ts: -1 } com partialFilterExpression: { tipo: { $in: ["erro","warn"] } } corta o tamanho do índice e o custo de escrita drasticamente.

MÓDULO 05 · INTERMEDIÁRIO → AVANÇADO

Padrões de schema e migrações

Objetivo: conhecer o catálogo de padrões de modelagem que o mercado usa por nome, e evoluir o schema de uma coleção com bilhões de documentos sem downtime.

5.1 O catálogo de padrões (os que caem em entrevista)

PadrãoProblema que resolveComo
SubsetArray 1:muitos grande demais para embutir inteiroEmbute os N mais relevantes (ex.: 5 avaliações recentes) + coleção separada com o resto
BucketSéries temporais / muitos itens pequenos por chave1 documento agrupa uma janela fixa (ex.: 1 h de leituras de um sensor num array)
ComputedRecalcular agregados a cada leitura é caroPré-calcula na escrita ($inc em totalAvaliacoes, notaMedia)
Extended ReferenceSegundo fetch só para exibir um nomeDuplica 2–3 campos estáveis do referenciado
Schema VersioningMigrar schema de coleção enorme sem pararCampo schemaVersion + migração preguiçosa ou backfill
Outlier99% dos docs têm array pequeno, 1% tem milhões (o "usuário Taylor Swift")Trata o outlier à parte (flag hasOverflow + coleção de overflow)
Polymorphic"Tipos" diferentes consultados juntosMesma coleção, campo tipo discrimina; campos variam por tipo
ApproximationContador exato custa 1 escrita por eventoIncrementa em lotes (ex.: +1 a cada 10 views, probabilístico)

5.2 Bucket pattern na prática (séries temporais "na mão")

// Em vez de 1 documento por leitura (milhões/dia), 1 documento por sensor por hora
{
  sensorId: "s-42",
  hora: ISODate("2026-06-04T13:00:00Z"),
  n: 60,
  leituras: [ { t: 0, v: 21.4 }, { t: 60, v: 21.5 }, /* ... */ ],
  min: 21.1, max: 22.0, soma: 1284.6           // agregados do bucket (padrão Computed embutido)
}

Escrita: updateOne({ sensorId, hora }, { $push: { leituras: {...} }, $inc: { n: 1, soma: v }, $min: { min: v }, $max: { max: v } }, { upsert: true }). Leituras de dashboard varrem dezenas de buckets em vez de milhões de documentos. (Desde a versão 5.0 há Time Series Collections nativas — módulo 9 — que fazem isso por baixo dos panos; o bucket manual ainda importa para entender e para casos fora do modelo nativo.)

5.3 Migrações sem downtime

  1. Código tolerante primeiro: faça a aplicação ler tanto o formato antigo quanto o novo (ex.: const nome = doc.nomeCompleto ?? \`${doc.nome} ${doc.sobrenome}\`). Só depois toque nos dados.
  2. Migração preguiçosa: ao gravar um documento antigo, converta-o e marque schemaVersion: 2. Barato, gradual, mas nunca termina sozinho para dados frios.
  3. Backfill em lotes: job que percorre { schemaVersion: { $lt: 2 } } em páginas de alguns milhares, com updateMany ou pipeline de update, pausas entre lotes para não saturar I/O, e retomável (guarda o último _id).
  4. Validação JSON Schema: depois do backfill, ligue db.runCommand({ collMod: "col", validator: {...}, validationLevel: "moderate" }) para impedir regressão ao formato antigo.
⚠️ updateMany num COLLSCAN de coleção gigante

Um updateMany({}, ...) sem filtro indexado numa coleção de bilhões de documentos segura locks e satura o disco por horas. Sempre pagine por _id ou por índice, com limit por lote e respiro entre eles.

💼 Mercado de trabalho

Saber os padrões pelo nome (subset, bucket, computed, extended reference, schema versioning, outlier) é sinal direto de que você estudou MongoDB a sério — a MongoDB University e o blog oficial os ensinam assim, e entrevistadores usam o mesmo vocabulário. "Como você migraria o schema de uma coleção de 2 bilhões de documentos?" é pergunta de sênior: a resposta é código tolerante + schemaVersion + backfill em lotes retomável.

✏️ Exercício 5 — Escolha o padrão

Para cada caso, nomeie o padrão: (a) post de blog precisa mostrar contagem de curtidas sem contar a coleção de curtidas toda vez; (b) 99,9% dos perfis seguem < 5.000 pessoas, mas alguns influenciadores seguem milhões; (c) você adicionou o campo telefone e quer migrar 500 milhões de usuários aos poucos; (d) app de IoT gravando 10 mil leituras/segundo.

Gabarito: (a) Computed — $inc em curtidas na escrita. (b) Outlier — trata influenciadores com coleção/estrutura à parte. (c) Schema Versioning — schemaVersion + migração preguiçosa + backfill. (d) Bucket (ou Time Series Collection nativa).

MÓDULO 06 · AVANÇADO

Replica Set, transações e concerns

Objetivo: entender como o MongoDB fica disponível e durável — eleições, oplog, writeConcern/readConcern/readPreference — e usar transações multi-documento sem sabotar a performance.

6.1 O Replica Set

Um replica set é um grupo de nós com uma cópia dos mesmos dados: 1 primário (aceita escritas) + N secundários que replicam o oplog (um log com cada operação, em coleção limitada). Se o primário some, os nós restantes fazem uma eleição (protocolo tipo Raft) e promovem um secundário em segundos. É o mínimo para produção — nunca rode um nó só.

6.2 Write Concern — quão durável é "gravado"

writeConcernSignificaUso
w: 1o primário confirmou (pode se perder se ele cair antes de replicar)Dados descartáveis (telemetria, logs)
w: "majority"a maioria dos nós que votam confirmou → sobrevive à queda do primárioPadrão recomendado para dados que importam
j: trueescrito no journal em disco, não só na RAMCombine com majority para durabilidade real
wtimeouttempo máximo esperando as confirmaçõesEvita travar a aplicação se um nó está lento

6.3 Read Concern e Read Preference

⚠️ "Vou ler dos secundários para escalar" quase nunca é a resposta

Leitura em secundário devolve dados possivelmente atrasados (lag) e não aumenta a capacidade de escrita (o gargalo comum). Para a maioria dos sistemas, a saída é índice bom + working set na RAM + cache (Redis). Secundário para leitura faz sentido em cargas analíticas isoladas ou geo-distribuição com nearest.

6.4 Transações multi-documento

Operações num único documento são atômicas por natureza — e boa modelagem (embedding do que muda junto) resolve a maioria dos casos sem transação. Desde a 4.0 (replica set) / 4.2 (sharded) existem transações ACID multi-documento:

const session = client.startSession();
try {
  await session.withTransaction(async () => {
    await contas.updateOne({ _id: "A" }, { $inc: { saldo: -100 } }, { session });
    await contas.updateOne({ _id: "B" }, { $inc: { saldo:  100 } }, { session });
  }, { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } });
} finally { await session.endSession(); }
⚠️ Transação não é grátis

Transações seguram locks, têm limite de tempo (60 s por padrão) e degradam throughput sob contenção. Se você precisa de transações multi-documento no caminho quente e o tempo todo, provavelmente modelou de forma relacional. Elas são a ferramenta certa para o caso raro (transferência, baixa de estoque + criação de pedido), não para o CRUD do dia a dia.

💼 Mercado de trabalho

Perguntas reais: "o que acontece quando o primário cai?" (eleição, promoção de secundário, escritas pausam brevemente no lado minoritário), "diferença entre w:1 e w:majority", "MongoDB tem transação?" (sim, multi-documento desde 4.0, mas com custo — e single-doc já é atômico). Conectar com o CAP: MongoDB é CP por padrão.

✏️ Exercício 6 — Concerns para cada dado

Defina writeConcern e readPreference para: (a) evento de clique para analytics; (b) criação de pedido pago; (c) leitura do catálogo de produtos na home (tolera segundos de atraso, altíssimo volume).

Gabarito: (a) w: 1 (perda tolerável), leitura irrelevante. (b) w: "majority", j: true — não pode sumir; readPreference: primary para o cliente ver o próprio pedido logo após criar (read-your-writes). (c) Escrita rara com majority; leitura pode usar secondaryPreferred ou, melhor, cache na frente — o atraso de replicação é aceitável para catálogo.

MÓDULO 07 · AVANÇADO

Sharding e a shard key

Objetivo: entender quando (e quando não) shardar, e escolher a shard key — a decisão praticamente irreversível que define se o cluster escala ou vira um gargalo com passos extras.

7.1 Anatomia de um cluster shardado

💡 Primeiro esgote as alternativas

Sharding multiplica a complexidade operacional. Antes: vertical scaling, índices, working set na RAM, arquivamento de dados frios, cache. Sharde quando o dataset ou o throughput de escrita realmente não cabem num replica set — não por antecipação.

7.2 Escolhendo a shard key

Uma boa shard key tem três propriedades ao mesmo tempo:

  1. Alta cardinalidade — muitos valores distintos (senão os chunks não se dividem).
  2. Baixa frequência / distribuição uniforme — nenhum valor concentra a maioria dos documentos (senão jumbo chunk).
  3. Presente nas consultas mais comuns — senão toda consulta vira scatter-gather (pergunta a todos os shards e junta).
Shard keyProblema
{ criadoEm: 1 } ou { _id: 1 } (ObjectId)Monotônica: todo insert novo cai no mesmo chunk/shard → hot shard na escrita
{ pais: 1 }Baixa cardinalidade + distribuição torta (Brasil concentra tudo)
{ userId: "hashed" }Distribui escritas lindamente, mas consultas por faixa e a maioria dos sort viram scatter-gather
{ tenantId: 1, _id: 1 } (composta)Boa para SaaS multi-tenant: consultas sempre filtram por tenantId; o _id dá cardinalidade dentro do tenant

7.3 Padrões de sharding

⚠️ A shard key é (quase) para sempre

Até versões recentes, mudar a shard key exigia recriar a coleção. Hoje há reshardCollection, mas é uma operação pesada, cara e arriscada em produção. Trate a escolha como definitiva: modele as consultas, simule a distribuição, e só então sharde.

💼 Mercado de trabalho

Sharding é assunto de entrevista sênior/staff e de system design. O clássico: "você shardou por timestamp e as escritas estão todas num shard — por quê e como resolve?" (monotonicidade → hot shard; solução: hashed, ou compound com prefixo de alta cardinalidade). Saber os três critérios da shard key de cor é esperado.

✏️ Exercício 7 — Shard key para um SaaS de notas fiscais

Multi-tenant. Cada empresa (tenant) emite de dezenas a milhões de notas. Consultas: sempre por tenantId, geralmente com faixa de data; ocasionalmente por numeroNota. Escreve muito (picos no fim do mês). Proponha a shard key e justifique.

Gabarito: { tenantId: 1, emitidaEm: 1 } composta. tenantId está em toda consulta (evita scatter-gather) e dá isolamento por cliente; emitidaEm acrescenta cardinalidade dentro do tenant e dá localidade para as consultas por faixa de data. Risco: um tenant gigante pode criar chunk grande — mitigar com { tenantId: 1, _id: 1 } se a distribuição por data for muito torta, ou zona dedicada para os maiores. tenantId sozinho falha (baixa cardinalidade); emitidaEm como prefixo falha (monotônica, hot shard no fim do mês).

MÓDULO 08 · AVANÇADO

Atlas: Search, Vector Search e o resto do ecossistema

Objetivo: conhecer o que a plataforma gerenciada (Atlas) adiciona ao MongoDB — busca textual de verdade, busca vetorial para IA, triggers, federação e arquivamento — e quando cada um substitui um serviço externo.

8.1 Atlas Search — Lucene dentro do MongoDB

O índice text nativo é fraco (sem relevância boa, sem análise linguística, sem faceta rica). Atlas Search embute o Apache Lucene (o motor do Elasticsearch) no cluster: um índice de busca sincronizado automaticamente com as coleções, consultado pelo estágio $search no aggregation pipeline.

db.produtos.aggregate([
  { $search: {
      index: "produtos",
      compound: {
        must:   [ { text: { query: "tênis corrida", path: ["nome", "descricao"], fuzzy: { maxEdits: 1 } } } ],
        filter: [ { range: { path: "preco", lte: 500 } } ]
      }
  } },
  { $project: { nome: 1, preco: 1, score: { $meta: "searchScore" } } }
])

Para muitos casos, isso elimina a necessidade de manter um Elasticsearch separado e todo o pipeline de CDC para sincronizá-lo (o problema do módulo 9 da apostila de NoSQL). Facetas, autocomplete, sinônimos, highlighting — tudo dentro do mesmo banco.

8.2 Atlas Vector Search — a base de RAG tema em alta

Guarde o embedding (vetor de floats) de cada documento num campo e crie um índice vetorial. O estágio $vectorSearch faz busca por similaridade (kNN aproximado, HNSW):

{ $vectorSearch: {
    index: "docs_vec",
    path: "embedding",
    queryVector: [ 0.012, -0.043, /* ...1536 dims... */ ],
    numCandidates: 200,
    limit: 5,
    filter: { tenantId: "t1" }        // pré-filtro combinado com a busca vetorial
} }

Combinado com $search textual na mesma query, você tem busca híbrida (semântica + palavra-chave) sem um banco vetorial dedicado. Conecta diretamente com a apostila de IA Generativa & RAG.

8.3 O resto do ecossistema Atlas

RecursoPara quê
TriggersRodar função (JS) em resposta a mudança na coleção (via change streams), em agenda (cron) ou em auth. Base para rollups, notificações, sincronização.
Change StreamsAPI para "assinar" mudanças de uma coleção em tempo real (db.col.watch()) — resumível por resumeToken. É o oplog exposto de forma limpa; base de CDC, cache invalidation, event-driven.
Data FederationConsultar com sintaxe MongoDB dados que estão no S3 (Parquet, JSON) junto com coleções do cluster — sem ETL.
Online ArchiveMove automaticamente dados frios (por regra de data) para storage barato, ainda consultável. Mantém o working set enxuto.
ChartsDashboards direto sobre coleções e pipelines, sem BI externo para casos simples.
Search NodesNós dedicados só para Atlas Search/Vector, isolando a carga de busca da carga transacional.
💼 Mercado de trabalho

Atlas é o MongoDB que a maioria das empresas roda (gerenciado, multi-cloud). Saber que Atlas Search substitui Elasticsearch em muitos casos e que Atlas Vector Search é uma opção de RAG sem banco vetorial dedicado são pontos que aparecem em decisões de arquitetura — e "já implementei busca híbrida com $search + $vectorSearch" é uma frase forte de currículo em 2026.

✏️ Exercício 8 — Buscar sem um segundo banco

Um e-commerce tem catálogo no MongoDB e hoje mantém um Elasticsearch separado, sincronizado por um serviço próprio que vive quebrando. Também querem lançar "busca por descrição em linguagem natural". O que você propõe?

Gabarito: Migrar a busca textual para Atlas Search ($search) — índice sincronizado automaticamente, elimina o serviço de sincronização e o Elasticsearch. Para a busca em linguagem natural, gerar embeddings dos produtos e usar Atlas Vector Search ($vectorSearch), possivelmente híbrida com o $search. Isolar a carga com Search Nodes se o volume justificar. Resultado: um sistema a menos para operar e um problema de consistência (CDC) eliminado.

MÓDULO 09 · MUITO AVANÇADO

Produção: tuning, segurança e operação

Objetivo: o que se cobra de quem opera MongoDB em produção — performance sob carga, Time Series Collections, criptografia de campo, backup com PITR e as métricas que você monitora.

9.1 Performance tuning sob carga

9.2 Time Series Collections 5.0+

db.createCollection("leituras", {
  timeseries: { timeField: "ts", metaField: "sensor", granularity: "minutes" },
  expireAfterSeconds: 7776000    // TTL de 90 dias, direto na coleção
})

O MongoDB faz o bucketing por baixo dos panos (compressão colunar, índices otimizados), mas você insere e consulta documentos normais. Use para métricas, IoT, dados financeiros. Limitações: updates/deletes restritos, alguns operadores não suportados — leia a doc antes de adotar como coleção transacional.

9.3 Segurança

9.4 Backup e recuperação

9.5 As métricas que você monitora

MétricaAlarme quando…
Replication lag> alguns segundos e crescendo (secundário não acompanha; risco a w:majority)
WiredTiger cache hit ratioCai de forma sustentada (working set não cabe na RAM)
Operações com COLLSCAN / scanAndOrderAparecem no profiler para consultas frequentes (índice faltando)
ConnectionsPerto do limite do tier / maxIncomingConnections (connection storm)
p95/p99 de latência por operaçãoSobe — sempre percentis, nunca média
Page faults / disco IOPSSaturação de I/O (paginação, compaction, backup concorrente)
Oplog windowEncolhe abaixo do tempo que um secundário pode ficar fora
💼 Mercado de trabalho

Este módulo é o dia a dia de vagas de backend sênior, SRE e DBA. Perguntas: "o que você monitora num cluster MongoDB?", "como faria PITR?", "como protege um campo de CPF?" (CSFLE/Queryable Encryption — o servidor nunca vê o claro), "por que a réplica não substitui o backup?". Citar cache hit ratio, replication lag, oplog window e percentis já coloca você na conversa sênior.

✏️ Exercício 9 — Post-mortem

Sintoma: depois de uma campanha que triplicou o tráfego, a latência p99 de leitura saltou de 8 ms para 400 ms, o cache hit ratio do WiredTiger caiu de 98% para 71%, e o profiler mostra várias consultas de listagem com SORT em memória. O que aconteceu e qual o plano?

Gabarito: O crescimento do dataset quente (mais usuários ativos) fez o working set ultrapassar a RAM → mais paginação de disco → latência. As consultas com SORT em memória indicam índice sem o campo de ordenação (violação da regra ESR), o que agrava sob volume. Plano curto: criar/ajustar os índices compostos das listagens pela regra ESR (elimina o sort em memória e reduz docs examinados); escalar a RAM ou o tier para o working set caber; ligar Online Archive para dados frios saírem do caminho. Plano médio: revisar todos os COLLSCAN/scanAndOrder do profiler, adicionar alarme sobre cache hit ratio e p99.

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, portfólio e entrevistas

Objetivo: transformar os módulos anteriores em aprovação — o que as vagas pedem, projetos que geram entrevista, banco de perguntas com a resposta que passa, e certificação.

10.1 O que as vagas realmente testam

TemaComo caiMódulo
Modelagem embedding vs referencing"Modele X" — avaliam o raciocínio (consultas primeiro)2, 5
Aggregation pipeline"Escreva um pipeline que retorne Y"3
Diagnóstico de performance"Esta query está lenta" → explain + índice ESR4
Concerns e replica set"w:1 vs majority", "o que acontece se o primário cai"6
Shard keySystem design: "shardei por data e deu hot shard"7
Segurança"Como protege CPF", "NoSQL injection"9

10.2 Roadmap por trilha

Plano de 6 semanas: sem. 1–2 — módulos 1–2 com um MongoDB local em Docker e uma API CRUD; sem. 3 — módulo 3, reescrever relatórios da API como pipelines; sem. 4 — módulo 4, indexar tudo com explain() antes/depois no README; sem. 5 — módulos 5–6 e migrar o schema uma vez de propósito; sem. 6 — módulos 7–9 em nível conceitual (profundo no da sua trilha) + simulados do 10.3.

10.3 Banco de perguntas (com a resposta que aprova)

Júnior — "Coleção, documento, campo: o que é o quê?"

Database contém coleções; coleção contém documentos (BSON, ≤ 16 MB); documento tem campos, que podem ser escalares, arrays ou objetos aninhados. Analogia SQL: coleção ≈ tabela, documento ≈ linha, campo ≈ coluna — mas cada "linha" pode ter formato próprio. O _id é a chave primária (ObjectId por padrão, único e indexado automaticamente).

Pleno — "Embedding ou referencing para comentários de um post?"

Depende da cardinalidade e do acesso. Comentários são 1:muitos que cresce sem limite → não pode embutir todos (16 MB, array ilimitado). Solução de mercado: padrão subset — embutir os ~5–10 mais recentes no post (a home do post mostra isso numa leitura) + coleção comentarios referenciando postId, com índice { postId: 1, criadoEm: -1 } para paginar o resto. Contadores (totalComentarios) via padrão Computed com $inc.

Pleno — "Esta consulta faz COLLSCAN. O que você faz?"

Rodar explain("executionStats"), confirmar o COLLSCAN e ver totalDocsExamined. Montar um índice pela regra ESR (igualdade → sort → range) cobrindo o filtro e a ordenação da consulta. Reexecutar o explain e comparar: espero IXSCAN, totalDocsExamined ≈ nReturned e o SORT em memória sumir. Se a projeção couber no índice, buscar covered query.

Sênior — "MongoDB tem transação? E é CP ou AP?"

Operações single-document sempre foram atômicas. Transações ACID multi-documento existem desde a 4.0 (replica set) e 4.2 (sharded), com session.withTransaction — mas seguram locks, têm limite de tempo e degradam throughput sob contenção, então são para o caso raro, não o CRUD. Quanto ao CAP: MongoDB é CP por padrão — numa partição de rede, o lado que não tem a maioria não elege primário e para de aceitar escritas, preservando consistência à custa de disponibilidade naquele lado.

Sênior — "Você shardou uma coleção por createdAt e todas as escritas caem num shard. Por quê e como corrige?"

createdAt é monotônico: cada novo documento tem a maior shard key, então cai sempre no último chunk, num único shard — hot shard de escrita, e o balancer não consegue equilibrar carga futura. Correções: shard key hashed ({ _id: "hashed" }) se as consultas forem por igualdade; ou compound com um prefixo de alta cardinalidade e presente nas consultas (ex.: { tenantId: 1, createdAt: 1 }), que dá distribuição e mantém localidade de faixa. Migrar exige reshardCollection (pesado) — daí a shard key ser decisão de design, não de tentativa e erro.

Armadilha — "MongoDB é mais rápido que SQL?"

"Depende do padrão de acesso." Por chave/filtro que a modelagem pré-computou, em documento único, sim — a ausência de JOIN e o dado já agrupado ajudam. Em consultas ad-hoc com muitos relacionamentos, um relacional bem indexado costuma vencer. MongoDB fica rápido porque você modela a partir das consultas e aceita duplicação; jogar dados sem modelar e esperar performance é o erro clássico.

10.4 Projetos de portfólio que geram entrevista

  1. API de e-commerce (Node/Nest ou FastAPI + MongoDB): catálogo com $facet para listagem+filtros, pedidos com extended reference, índices justificados no README com prints de explain() antes/depois, teste de carga (k6) documentado.
  2. Analytics com pipeline + $merge: ingestão de eventos, rollups horários materializados, dashboard que lê da coleção materializada. Diferencial: explicar o custo evitado.
  3. Busca híbrida com Atlas Search + Vector Search: um corpus de documentos seus, $search + $vectorSearch, comparando resultados de busca só textual, só vetorial e híbrida com números.
  4. Serviço event-driven com Change Streams: um watch() resumível que projeta mudanças de uma coleção para um cache/índice, com tratamento de resumeToken e reconexão.

Em todos: o README explica por que cada decisão de modelagem e índice — recrutador técnico lê README, e a justificativa vale mais que a linha de código.

10.5 Certificação e fontes

🏁 Síntese final

Três ideias sustentam tudo: (1) modele a partir das consultas, aceitando duplicação de dado histórico e de exibição; (2) todo problema de performance é resolvido lendo o explain() e aplicando a regra ESR — índice não é chute; (3) em produção, o trabalho é escolher conscientemente os concerns e a shard key, e monitorar working set, lag e percentis. Comandos e sintaxe se consultam na doc; essas três decisões são o que diferencia quem "usou MongoDB" de quem o domina.