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.
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.
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).
- Tipos que só existem em BSON:
ObjectId(12 bytes: timestamp + contador + aleatório — ordenável por criação),Date(inteiro de 64 bits, ms desde epoch),Decimal128(decimal exato — use para dinheiro, nuncadouble),Binary,Int32/Int64,Timestampinterno. - Limite rígido: um documento tem no máximo 16 MB. Não é sugestão — é o teto que molda toda a modelagem (módulo 2).
- Profundidade máxima: 100 níveis de aninhamento.
double é bug garantido0.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
| SQL | MongoDB (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");
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.
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.
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ção | Exemplo | Padrão recomendado |
|---|---|---|
| 1:1 | usuário ↔ perfil | Embedding (ou mesmo documento) |
| 1:poucos (limite conhecido e pequeno) | usuário ↔ endereços (≤ ~10) | Embedding em array |
| 1:muitos (cresce, mas cabe) | post ↔ comentários | Referencing, ou subset (embutir os N recentes + referenciar o resto) |
| 1:milhões / ilimitado | sensor ↔ leituras; canal ↔ mensagens | Referencing sempre; considerar bucket pattern |
| muitos:muitos | produto ↔ tags; aluno ↔ turmas | Array de referências no lado que consulta mais; às vezes nos dois |
2.3 Os três antipadrões que explodem em produção
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.
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.
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.
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.
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ágio | Faz | Cuidado |
|---|---|---|
$match | Filtra (WHERE) | Ponha primeiro — só assim usa índice |
$group | Agrupa 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 / $unset | Molda campos, cria calculados | $set (ex-$addFields) adiciona sem apagar o resto |
$unwind | 1 documento por elemento de array | Multiplica a contagem; use preserveNullAndEmptyArrays se precisar manter vazios |
$lookup | LEFT JOIN com outra coleção | Caro; a coleção "de fora" precisa de índice no campo de junção |
$facet | Vários sub-pipelines na mesma passada | Ótimo para "resultados + contagem total + agregados de filtro" numa request só |
$merge / $out | Grava o resultado numa coleção | $merge atualiza incrementalmente (base de rollups); $out substitui tudo |
$setWindowFields | Window 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.
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.
Í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
- Multikey: indexar um campo que é array cria automaticamente um índice multikey (uma entrada por elemento). Um índice composto pode ter no máximo um campo array.
- Parcial > esparso:
partialFilterExpressioné mais expressivo e é o recomendado hoje.
4.2 A regra ESR para índices compostos
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 procurar | Bom | Ruim |
|---|---|---|
stage | IXSCAN → FETCH | COLLSCAN (varreu a coleção toda) |
| Ordenação | ausente, ou SORT sobre poucos docs | SORT com SORT_KEY_GENERATOR sobre milhares (ou erro de 32 MB de sort sem índice) |
totalDocsExamined vs nReturned | próximos (ex.: 105 examinados, 100 retornados) | examinou 1.000.000 para retornar 20 |
totalKeysExamined | ≈ nReturned | >> 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
- Cada índice ocupa RAM e torna toda escrita mais lenta (o índice também é atualizado). Indexe consultas reais e frequentes.
- Working set = dados + índices efetivamente acessados. Se ele não cabe na RAM do servidor, o MongoDB pagina do disco e a latência despenca. Dimensione a máquina pelo working set, não pelo dataset total.
- Encontre índices inúteis com
$indexStats(db.col.aggregate([{ $indexStats: {} }])) — seaccesses.opsé ~0 há semanas, remova. $regexsem âncora (/ana/) não usa índice;/^ana/(prefixo) usa. Busca textual de verdade → índicetextou Atlas Search (módulo 8).
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.
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ão | Problema que resolve | Como |
|---|---|---|
| Subset | Array 1:muitos grande demais para embutir inteiro | Embute os N mais relevantes (ex.: 5 avaliações recentes) + coleção separada com o resto |
| Bucket | Séries temporais / muitos itens pequenos por chave | 1 documento agrupa uma janela fixa (ex.: 1 h de leituras de um sensor num array) |
| Computed | Recalcular agregados a cada leitura é caro | Pré-calcula na escrita ($inc em totalAvaliacoes, notaMedia) |
| Extended Reference | Segundo fetch só para exibir um nome | Duplica 2–3 campos estáveis do referenciado |
| Schema Versioning | Migrar schema de coleção enorme sem parar | Campo schemaVersion + migração preguiçosa ou backfill |
| Outlier | 99% 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 juntos | Mesma coleção, campo tipo discrimina; campos variam por tipo |
| Approximation | Contador exato custa 1 escrita por evento | Incrementa 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
- 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. - Migração preguiçosa: ao gravar um documento antigo, converta-o e marque
schemaVersion: 2. Barato, gradual, mas nunca termina sozinho para dados frios. - Backfill em lotes: job que percorre
{ schemaVersion: { $lt: 2 } }em páginas de alguns milhares, comupdateManyou pipeline de update, pausas entre lotes para não saturar I/O, e retomável (guarda o último_id). - 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 giganteUm 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.
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).
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ó.
- Oplog: idempotente por design (reaplicar não estraga). Seu tamanho define a janela de recuperação: um secundário que ficou fora tempo demais para "cair do oplog" precisa de ressincronização completa.
- Árbitro: nó sem dados que só vota, para desempatar em setups de 2 nós. Evite se puder — 3 nós com dados é melhor (um árbitro não dá durabilidade a
w:"majority"). - Lag de replicação: quanto os secundários estão atrás do primário. Métrica crítica; se cresce, leituras em secundário servem dados velhos e
w:"majority"fica lento.
6.2 Write Concern — quão durável é "gravado"
| writeConcern | Significa | Uso |
|---|---|---|
w: 1 | o 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ário | Padrão recomendado para dados que importam |
j: true | escrito no journal em disco, não só na RAM | Combine com majority para durabilidade real |
wtimeout | tempo máximo esperando as confirmações | Evita travar a aplicação se um nó está lento |
6.3 Read Concern e Read Preference
readConcern— qual "versão" você lê:"local"(o que este nó tem agora),"majority"(só o que a maioria confirmou — não sofre rollback),"linearizable"(a mais recente confirmada, com custo),"snapshot"(usado em transações).readPreference— de qual nó você lê:primary(consistente, padrão),primaryPreferred,secondary,secondaryPreferred(escala leitura aceitando lag),nearest(menor latência de rede).
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çõ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.
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.
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
mongos— roteador sem estado; a aplicação fala com ele. Decide qual shard tem o dado a partir da shard key.- Config servers (um replica set) — guardam o mapa de quais faixas de shard key vivem em qual shard.
- Shards — cada um é um replica set completo, dono de uma fatia dos dados.
- Chunk — faixa contígua de valores da shard key; o balancer move chunks entre shards para equilibrar.
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:
- Alta cardinalidade — muitos valores distintos (senão os chunks não se dividem).
- Baixa frequência / distribuição uniforme — nenhum valor concentra a maioria dos documentos (senão jumbo chunk).
- Presente nas consultas mais comuns — senão toda consulta vira scatter-gather (pergunta a todos os shards e junta).
| Shard key | Problema |
|---|---|
{ 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
- Hashed sharding —
sh.shardCollection("db.col", { userId: "hashed" }). Espalha escritas; perde localidade de faixa. - Ranged sharding — mantém localidade (consultas por faixa tocam poucos shards); exige shard key não-monotônica.
- Compound shard key — o mais usado em produção: um prefixo de igualdade (tenant, região) + um sufixo de alta cardinalidade.
- Zone sharding — amarrar faixas de shard key a shards físicos (ex.: dados de clientes da UE em shards na Europa) para compliance/latência.
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.
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).
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
| Recurso | Para quê |
|---|---|
| Triggers | Rodar 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 Streams | API 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 Federation | Consultar com sintaxe MongoDB dados que estão no S3 (Parquet, JSON) junto com coleções do cluster — sem ETL. |
| Online Archive | Move automaticamente dados frios (por regra de data) para storage barato, ainda consultável. Mantém o working set enxuto. |
| Charts | Dashboards direto sobre coleções e pipelines, sem BI externo para casos simples. |
| Search Nodes | Nós dedicados só para Atlas Search/Vector, isolando a carga de busca da carga transacional. |
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.
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
- Perfil de consultas lentas: o database profiler (
db.setProfilingLevel(1, { slowms: 100 })) grava emsystem.profiletoda operação acima do limite. Em Atlas, o Performance Advisor sugere índices a partir disso. - WiredTiger cache: por padrão ~50% da RAM. Métrica-chave: cache hit ratio. Se despenca, o working set não cabe — mais RAM, arquivar dados frios, ou revisar índices grandes demais.
COLLSCANem produção é quase sempre um índice faltando; caçá-los no profiler é rotina.- Connection pooling e serverless: revisão do módulo 1 — em Lambda, pool pequeno + client reaproveitado; considere o proxy do Atlas (Data API / drivers com pooling externo).
- Retry storms:
retryWrites/retryReadsligados + backoff exponencial com jitter no lado da aplicação. Escrita idempotente (ex.:updateOnecomupsertpor chave de negócio) é pré-requisito para retry seguro.
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
- A praga histórica: instâncias abertas na internet sem autenticação causaram milhares de vazamentos e ataques de resgate. Regra: bind em rede privada (VPC peering / private endpoint no Atlas), auth sempre ligada, TLS em trânsito, IP allowlist.
- NoSQL injection: em Node, um corpo
{ "senha": { "$gt": "" } }passa por um login que fazfind(req.body). Defesa: validar tipos (rejeitar objeto onde se espera string), ODM com schema (Mongoose), nunca interpolar entrada em$where/$expr. - RBAC de menor privilégio: um usuário de banco por serviço, com apenas as ações e coleções necessárias (a API de leitura não recebe
insert). - Encryption at rest (transparente, no storage) e Client-Side Field Level Encryption (CSFLE) / Queryable Encryption: campos sensíveis (CPF, cartão) são cifrados pelo driver, antes de sair da aplicação — o servidor (e o DBA, e um dump vazado) nunca vê o texto claro. Queryable Encryption ainda permite igualdade/faixa sobre o dado cifrado.
- Auditoria: log de acessos e mudanças de schema/role (enterprise/Atlas) para compliance.
9.4 Backup e recuperação
- Réplica não é backup: um
deleteManyerrado replica na hora para todos os secundários. - Point-in-Time Recovery (PITR): snapshots periódicos + oplog contínuo permitem restaurar para "3 minutos antes do incidente". Atlas Continuous Backup faz isso; self-hosted, é snapshot de volume + arquivamento do oplog.
- Teste a restauração: "backup que nunca foi restaurado não é backup". Game day trimestral restaurando num ambiente isolado.
mongodump/mongorestoreservem para coleções pequenas e migrações pontuais — não como estratégia de backup de um cluster grande.
9.5 As métricas que você monitora
| Métrica | Alarme quando… |
|---|---|
| Replication lag | > alguns segundos e crescendo (secundário não acompanha; risco a w:majority) |
| WiredTiger cache hit ratio | Cai de forma sustentada (working set não cabe na RAM) |
Operações com COLLSCAN / scanAndOrder | Aparecem no profiler para consultas frequentes (índice faltando) |
| Connections | Perto do limite do tier / maxIncomingConnections (connection storm) |
| p95/p99 de latência por operação | Sobe — sempre percentis, nunca média |
| Page faults / disco IOPS | Saturação de I/O (paginação, compaction, backup concorrente) |
| Oplog window | Encolhe abaixo do tempo que um secundário pode ficar fora |
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.
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
| Tema | Como cai | Mó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 ESR | 4 |
| Concerns e replica set | "w:1 vs majority", "o que acontece se o primário cai" | 6 |
| Shard key | System design: "shardei por data e deu hot shard" | 7 |
| Segurança | "Como protege CPF", "NoSQL injection" | 9 |
10.2 Roadmap por trilha
- Backend / Full-stack: módulos 1–4 são 80% do que cai. Domine CRUD, modelagem, pipeline e índices; depois 6 (concerns) e 8 (Atlas Search). Sharding em nível conceitual.
- Engenharia de Dados: foco em 3 (pipeline), 5 (bucket/time series), 7 (sharding), 8 (Change Streams, Data Federation) e a apostila de Modelagem de Dados NoSQL.
- SRE / DBA: 6, 7 e 9 inteiros — replica set, sharding, tuning, segurança, backup, métricas.
- IA / ML: 8.2 (Vector Search) + a apostila de RAG; MongoDB como store de embeddings e de estado de agentes.
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
- API de e-commerce (Node/Nest ou FastAPI + MongoDB): catálogo com
$facetpara listagem+filtros, pedidos com extended reference, índices justificados no README com prints deexplain()antes/depois, teste de carga (k6) documentado. - Analytics com pipeline +
$merge: ingestão de eventos, rollups horários materializados, dashboard que lê da coleção materializada. Diferencial: explicar o custo evitado. - 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. - 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 deresumeTokene 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
- MongoDB Associate Developer e Associate Database Administrator — preparação gratuita e excelente na MongoDB University (trilhas com laboratórios). É a certificação de NoSQL com mais reconhecimento direto.
- Leituras: Designing Data-Intensive Applications (Kleppmann) para a base distribuída dos módulos 6–7; a documentação oficial e o MongoDB Blog (os padrões de schema do módulo 5 vêm de lá, com o mesmo vocabulário).
- Prática permanente:
docker run -d -p 27017:27017 mongopara o básico; uma conta gratuita no Atlas (M0) para Search, Vector Search, Triggers e Charts.
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.