Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-12
BloqueadoMongo virou meme, "use Postgres" é o conselho default e correto pra maioria dos casos. Mas Mongo continua relevante: workloads com schema flexível (eventos heterogêneos, ingestão de dados de múltiplas fontes), agregações complexas em coleções grandes, modelos hierárquicos onde JOIN seria custoso. Saber Mongo bem te dá clareza sobre quando relacional não é o ajuste certo e como pensar em modelagem orientada a documento sem cair nos antipadrões.
Este módulo é Mongo de fato: storage engine (WiredTiger), modelo BSON, schema de fato implícito, índices (single, compound, text, geo, wildcard), aggregation pipeline, transações multi-documento, replica sets, sharding, e os trade-offs reais com Postgres + jsonb. Não é "Mongo é melhor que SQL", é entender onde encaixa.
$jsonSchema validators).Document IDs:
_id é PK obrigatório.ObjectId (12 bytes: timestamp 4B + machine/pid 5B + counter 3B). Quase sortable por tempo.Mongo permite docs heterogêneos. Em prática, toda coleção tem schema implícito que sua app espera. Sem disciplina, vira bagunça.
Validação:
db.createCollection('orders', { validator: { $jsonSchema: { ... } } }).error (default) ou warn.Em projeto sério: sempre defina validator. Combinado com Mongoose/Zod no app, garante shape.
Default desde 3.2. B+Tree, compressão por block, MVCC.
Tipos:
{ status: 1 }.{ tenantId: 1, status: 1, createdAt: -1 }. Order matters (mesma regra de B-Tree).{ "$**": 1 } indexa todos os fields (cuidado, custoso).{ partialFilterExpression: { status: 'active' } }.{ expireAfterSeconds: N } em campo date, Mongo deleta docs após.Sempre crie index em background (default em versões modernas, não bloqueia).
db.orders.find(
{ tenantId: t, status: { $in: ['pending', 'paid'] } },
{ _id: 1, total: 1, customerName: 1 }
).sort({ createdAt: -1 }).limit(20);
Operators:
$eq, $gt, $gte, $lt, $lte, $ne, $in, $nin.$and, $or, $not, $nor.$exists, $type.$all, $elemMatch, $size.$regex, $expr, $jsonSchema.$expr permite usar aggregation expressions em queries.
$set, $unset, $inc, $mul.$push, $pull, $addToSet em arrays.$rename.$ (matched element), $[] (todos), $[<filter>] (filtered).arrayFilters define filtros por nome.db.orders.updateOne(
{ _id, "items.sku": "X" },
{ $set: { "items.$.quantity": 5 } }
);
A feature flagship do Mongo. Sequência de stages que transformam documentos:
db.orders.aggregate([
{ $match: { tenantId: t, createdAt: { $gte: yesterday } } },
{ $group: { _id: "$status", count: { $sum: 1 }, total: { $sum: "$total" } } },
{ $sort: { count: -1 } }
]);
Stages comuns:
$match: filter (use cedo, antes de transformações pesadas).$project / $set / $unset: shape.$group: agrupamento + accumulators.$sort, $limit, $skip.$lookup: JOIN-like (com outra collection). Custoso.$unwind: desempacota array em N docs.$facet: pipelines paralelos no mesmo input.$bucket / $bucketAuto: histogramas.$graphLookup: traversal recursivo.$merge / $out: persistir resultado.$densify, $fill: time-series helpers.$lookup é JOIN, mas executar em coleções grandes sem índice é desastroso. Em queries críticas, denormalize ou faça JOIN no app.
Decisão central. Não há "regras" universais; há trade-offs:
Embed (nested doc):
Reference:
customerId, faz query separada (ou $lookup).Heurísticas:
Pre-4.0: atomicidade só no nível de documento. 4.0+: multi-document transactions em replica set. 4.2+: transactions em sharded cluster.
const session = client.startSession();
try {
await session.withTransaction(async () => {
await orders.updateOne({ _id }, { $set: { status: 'paid' } }, { session });
await events.insertOne({ orderId: _id, type: 'paid' }, { session });
});
} finally {
await session.endSession();
}
Custo: txns multi-doc são mais lentas (locks, MVCC overhead). Mongo recomenda design que minimize txns multi-doc, embed quando possível.
Cluster de N nós: 1 primary + replicas + (opcional) arbiter.
oplog é capped collection no primary; replicas tail.Read preference (cliente escolhe):
primary (default).primaryPreferred.secondary / secondaryPreferred.nearest.Write concern:
{ w: 1 }: ack do primary (default em alguns drivers).{ w: 'majority' }: ack de majority. Mais durável, mais latente.{ j: true }: journal. Garante durabilidade local antes de ack.{ wtimeout: ms }: timeout para w concern.Read concern:
local: padrão.majority: dados commitados em majority.linearizable: stronger, mais lento.Em prod: writes com majority, reads default local (ou majority em workloads onde stale leitura é problema).
Mongo faz sharding nativo, ao contrário de Postgres (Postgres precisa de Citus/Foreign Tables).
Componentes:
mongos: router. Cliente conecta nele.Shard key: campo (ou compound) que define como docs se distribuem. Decisões:
Escolha errada: re-shard é trabalhoso. Em projetos < 1 TB, não shardar, replica set comum sustenta.
Atlas Search e Vector Search agregam features que historicamente exigiam Elasticsearch / pgvector adicionais.
Node:
mongodb (low-level).Em projetos onde tipagem importa: driver direto + Zod, ou Mongoose com Schema cuidadosamente tipados, ou wrappers como mongo-models ou Drizzle (que adicionou suporte a Mongo). Não ache que ODM substitui domain layer.
Para dúvida típica em backend de aplicação CRUD-ish: comece com Postgres. Adicione Mongo só se você bate em uma necessidade clara.
$lookup, normalização excessiva, transações multi-doc onipresentes. Você está usando Mongo errado.mongodump / mongorestore: dump lógico. Para datasets pequenos.mongosh é o shell oficial novo.compact (libera espaço pós-DELETEs); em replica sets, fazer um nó por vez.MongoDB schema = trade-off contínuo. "Embed everything" vence em read-heavy mas explode em update + atinge 16MB doc limit. "Reference everything" vira N+1 disfarçado em NoSQL. Aggregation pipeline tem 30+ stages, mas mal-orquestrado consome RAM brutal. Transactions custam 2-5x latência vs single-doc atomic ops. Esta seção dá decision tree concreto + código produção.
| Cenário | Embed | Reference |
|---|---|---|
| One-to-few (< 100 children) | ✓ | overkill |
| One-to-many com bound conhecido (< 1000) | ✓ se < 16MB | ✓ se queries variam |
| One-to-many unbounded (logs, comments) | ✗ (16MB limit) | ✓ |
| Many-to-many | ✗ | ✓ (refs ou junction collection) |
| Acessado SEMPRE junto | ✓ | ✗ |
| Acessado independente frequente | ✗ | ✓ |
| Update do child sem ler parent | ✗ | ✓ |
| Atomicity entre child e parent obrigatória | ✓ (single-doc atomic) | requires transaction |
// orders collection
{
_id: ObjectId("..."),
tenantId: UUID("..."),
status: "in_transit",
items: [
{ productId: ObjectId("..."), name: "Smartphone X", qty: 2, unitPrice: 1500_00, subtotal: 3000_00 },
{ productId: ObjectId("..."), name: "Capa", qty: 1, unitPrice: 50_00, subtotal: 50_00 }
],
courier: {
_id: ObjectId("..."),
name: "João Silva",
vehicle: "moto",
phoneSnapshot: "+5511999..."
},
total: 3050_00,
createdAt: ISODate("2026-04-15T..."),
statusHistory: [
{ status: "pending", at: ISODate("...") },
{ status: "in_transit", at: ISODate("...") }
]
}
name, vehicle) congela o nome NO MOMENTO da assignação — historical accuracy preservada se courier muda nome depois.// tracking_pings collection (high-write, unbounded)
{
_id: ObjectId("..."),
orderId: ObjectId("..."),
courierId: ObjectId("..."),
coords: { type: "Point", coordinates: [-46.633, -23.55] },
speedKmh: 45.2,
accuracyM: 8.5,
ts: ISODate("...")
}
// Index pra lookup por order
db.tracking_pings.createIndex({ orderId: 1, ts: -1 });
// Time-series collection (Mongo 6+, melhor performance pra time data)
db.createCollection('tracking_pings', {
timeseries: {
timeField: 'ts',
metaField: 'meta', // { courierId, orderId }
granularity: 'seconds'
},
expireAfterSeconds: 90 * 24 * 3600
});
meta campos.// Pattern: schemaVersion field + lazy migration
{
_id: ObjectId("..."),
schemaVersion: 2,
name: "Acme Corp",
// v2: added 'tier' field
tier: "premium"
}
// Migration in app code
function migrateLojista(doc) {
if (doc.schemaVersion === 1) {
doc.tier = doc.legacyPlan === 'pro' ? 'premium' : 'standard';
doc.schemaVersion = 2;
db.lojistas.updateOne({ _id: doc._id }, { $set: { tier: doc.tier, schemaVersion: 2 } });
}
return doc;
}
schemaVersion: 5 anos de prod = 7 estruturas diferentes na mesma collection. Code precisa handle todas.// Top 10 lojistas por revenue last 30 days, com nome populated
db.orders.aggregate([
{ $match: {
tenantId: UUID("..."),
status: "completed",
createdAt: { $gte: ISODate("...") }
}},
{ $group: {
_id: "$lojistaId",
orderCount: { $sum: 1 },
totalRevenue: { $sum: "$total" }
}},
{ $sort: { totalRevenue: -1 }},
{ $limit: 10 },
{ $lookup: {
from: "lojistas",
localField: "_id",
foreignField: "_id",
as: "lojista",
pipeline: [
{ $project: { name: 1, tier: 1 }} // só campos necessários
]
}},
{ $unwind: "$lojista" },
{ $project: {
lojistaName: "$lojista.name",
tier: "$lojista.tier",
orderCount: 1,
totalRevenue: 1
}}
]);
$match PRIMEIRO: aproveita index. Sem index em tenantId + createdAt, full collection scan.$lookup com pipeline: filtra/projeta no lookup; sem isso, traz docs inteiros.$project no fim: reduz network bytes ao client.db.orders.aggregate([...], {
allowDiskUse: true, // pra stages que excedem 100MB RAM (sort, group)
maxTimeMS: 30000, // kill após 30s
hint: { tenantId: 1, createdAt: -1 } // force index
});
$sort/$group sem index podem alocar 100MB+ RAM. allowDiskUse: true spilla pra disk (lento mas não OOM).$out pra pre-computar.$merge / $out — materialized views// Materialized daily revenue per tenant
db.orders.aggregate([
{ $match: { status: "completed" }},
{ $group: {
_id: { tenant: "$tenantId", day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }}},
revenue: { $sum: "$total" },
orderCount: { $sum: 1 }
}},
{ $merge: {
into: "daily_revenue",
on: ["_id"],
whenMatched: "replace",
whenNotMatched: "insert"
}}
]);
daily_revenue direto, < 50ms.// Multi-doc atomic — Mongo 4+
const session = client.startSession();
try {
await session.withTransaction(async () => {
await db.orders.insertOne({ ... }, { session });
await db.inventory.updateOne(
{ productId: pid },
{ $inc: { stock: -1 }},
{ session }
);
}, {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' },
maxCommitTimeMS: 5000
});
} finally {
await session.endSession();
}
$set/$inc/$push em embedded array. Embed-design correto evita transaction need.{ orderId, productId } em items array; cada read faz $lookup de products; vira aggregation N+1.$lookup sem índice no foreignField: full scan da collection lookup. Index obrigatório.$match first: full collection scan; performance cai 100x em scale.allowDiskUse: true como default: mascara queries mal-projetadas; lentidão silenciosa. Remova; deixe queries quebrar; force optimization.schemaVersion: app code com 7 if/else por versão.db.collection.find().sort() sem index compound: scan + in-memory sort. Compound index { filter: 1, sort: -1 }.$lookup em hot path como substituto de JOIN relacional: cada request paga pipeline + collection scan no foreign side; latência cresce N×M e nunca aproveita planner como Postgres. Mongo não foi feito pra joins — se virou padrão no fluxo crítico, a modelagem está errada (denormalize via embed/snapshot) ou o storage está errado (mude pra Postgres).Cruza com 02-12 §2.10 (replica sets — w='majority' pra durability), 02-12 §2.11 (sharding afeta transaction scope), 02-12 §2.16 (anti-patterns gerais), 02-09 (comparação Postgres pra mesmas decisions), 04-13 §2.12 (CDC de Mongo via change streams).
Aggregation pipeline é o motor analytics do Mongo (7+, Atlas 2026). Domine stages avançados: $lookup (joins), $graphLookup (recursão), $facet (multi-pipeline), time-series collections, $merge (materialized views). Sem isso, fallback pra app-side joins ou sync pra warehouse — caro e lento.
Mental model: pipeline = sequência de stages; documents fluem, transformam-se a cada stage. Stage order é load-bearing: $match first reduz input ANTES de operations caras; $project reduz fields para minimizar memory; $sort aproveita index APENAS se vier antes de stages que transformam ($group, $lookup). Memory limit: 100MB per stage default; allowDiskUse: true permite spill ao disk (10x mais lento). Index usage: APENAS $match + $sort em stages iniciais.
$lookup deep — equivalente SQL JOIN:
db.orders.aggregate([
{ $match: { tenantId: 'tenant-123', status: 'delivered' } }, // first: usa index
{ $lookup: {
from: 'couriers',
localField: 'courierId',
foreignField: '_id',
as: 'courier'
}},
{ $unwind: '$courier' }, // flatten array (1-to-1 join)
{ $project: { _id: 1, courierName: '$courier.name', deliveredAt: 1 } }
]);
Pipeline-form $lookup (mais poderoso; filtra dentro do JOIN):
{
$lookup: {
from: 'couriers',
let: { cid: '$courierId', tid: '$tenantId' },
pipeline: [
{ $match: { $expr: { $and: [
{ $eq: ['$_id', '$$cid'] },
{ $eq: ['$tenantId', '$$tid'] },
{ $eq: ['$active', true] }
]}}},
{ $project: { name: 1, vehicleType: 1 } }
],
as: 'courier'
}
}
Pegadinha performance: $lookup SEM index em foreignField = O(N×M) brutal. Index obrigatório.
$graphLookup — recursive traversal: hierarquias (org chart, comment threads, courier referral chains). Logística — referral chain até 3 níveis:
db.couriers.aggregate([
{ $match: { _id: ObjectId('...') } },
{ $graphLookup: {
from: 'couriers',
startWith: '$referredBy',
connectFromField: 'referredBy',
connectToField: '_id',
as: 'referralChain',
maxDepth: 3,
depthField: 'depth'
}}
]);
Index em connectToField mandatory; sem isso, recursive scan brutal. Max depth 100; recommend < 5 em prod por perf.
$facet — multi-pipeline single query: roda múltiplas aggregations em parallel sem repeat input scan. Logística — dashboard cards (counts por status + top 5 couriers + revenue):
db.orders.aggregate([
{ $match: { tenantId: 'tenant-123', createdAt: { $gte: ISODate('2026-05-01') } } },
{ $facet: {
byStatus: [
{ $group: { _id: '$status', count: { $sum: 1 } } }
],
topCouriers: [
{ $match: { status: 'delivered' } },
{ $group: { _id: '$courierId', deliveries: { $sum: 1 } } },
{ $sort: { deliveries: -1 } },
{ $limit: 5 }
],
revenueTotal: [
{ $match: { status: 'delivered' } },
{ $group: { _id: null, total: { $sum: '$priceCents' } } }
]
}}
]);
Cost: input scanned uma vez + N output paths; vs N separate queries 10x faster.
Time-series collections (Mongo 5.0+, stable 7+): native time-series storage com bucket optimization (similar TimescaleDB hypertables). Pattern courier location pings:
db.createCollection('tracking_pings', {
timeseries: {
timeField: 'ts',
metaField: 'metadata', // grouping key (courierId, tenantId)
granularity: 'minutes' // 'seconds' | 'minutes' | 'hours'
},
expireAfterSeconds: 86400 * 90 // 90 days retention auto-purge
});
db.tracking_pings.insertMany([
{ ts: new Date(), metadata: { courierId: 'c1', tenantId: 't1' }, lat: 12.34, lng: 56.78 }
]);
Storage savings: 10-50x vs regular collection (column-oriented bucketization). Limitations: no UPDATE/DELETE per-document (apenas regen via $out); shard key deve incluir metaField.
$merge — output to collection (materialized views): precompute analytics; refresh nightly via cron.
db.orders.aggregate([
{ $match: { createdAt: { $gte: ISODate('2026-05-01'), $lt: ISODate('2026-06-01') } } },
{ $group: {
_id: { tenantId: '$tenantId', status: '$status' },
count: { $sum: 1 },
revenue: { $sum: '$priceCents' }
}},
{ $merge: {
into: 'monthly_reports',
on: '_id',
whenMatched: 'replace',
whenNotMatched: 'insert'
}}
]);
Diferença $out vs $merge: $merge upserts (incremental refresh, runs em background); $out substitui collection inteira e bloqueia reads no target.
$expr + complex conditional logic — inline expressions em $match/$project:
{ $match: {
$expr: {
$and: [
{ $gt: [{ $size: '$items' }, 0] },
{ $lt: [{ $subtract: ['$deliveredAt', '$createdAt'] }, 1000 * 60 * 60 * 2] } // delivered < 2h
]
}
}}
Pegadinha: $expr não usa indexes pre-4.4 (improved 5+); ainda mais lento que pure index match. Use sparingly.
Diagnostics + hints:
.explain('executionStats'): pipeline plan + per-stage timing + index usage..hint({ field: 1 }): força specific index quando planner escolhe errado.maxTimeMS: 5000: kill aggregations slow.allowDiskUse: true: spill ao disk além do 100MB stage limit.Logística applied — analytics dashboard:
$facet aggrega orders + couriers + revenue em < 200ms via index (tenantId, createdAt, status).monthly_reports): nightly cron refresh via $merge; faster reads em year-over-year analysis.tracking_pings): native time-series collection com 90d retention; 30x storage savings vs regular.$graphLookup para spam detection (anel circular = fraud signal).Anti-patterns observados:
$match AFTER $lookup/$project: index não usado; full scan.$lookup em foreignField sem index: O(N×M) brutal.$graphLookup sem maxDepth: infinite recursion stack overflow.$facet em pipeline com 100GB input: cada branch scaneia full; redirect via early $match.$merge em hot path: write contention; schedule offline cron.allowDiskUse: false em large pipeline: errors em prod; default true em recent drivers.$expr em vez de native $match operators: less indexable.$out para break pipeline.maxTimeMS em hot endpoint aggregation: slow query DoS via crafted input.Cruza com 02-09 (Postgres, equivalente CTE/window functions), 04-13 (streaming/batch, lakehouse alternativo), 02-15 (search, $search Atlas), 03-13 (analytics DBs alternative), 04-09 (scaling, sharding aggregation considerations).
CRUD + aggregation + replica set + sharding cobrem o core relacional do Mongo. Production stack 2026 sobrepõe quatro pilares Atlas-native que transformam Mongo em plataforma multi-modal: Atlas Search (Lucene-backed full-text via $search aggregation stage, GA desde 2020 — substitui Elastic standalone para workloads <10k QPS com ~30-50% menor TCO), Atlas Vector Search (HNSW índice via $vectorSearch, GA Q2 2024 — semantic retrieval co-locado com operational data, dispensa pgvector/Qdrant separados), Queryable Encryption (CSFLE evolved — equality queries GA 2023, range queries GA 2024 com MongoDB 8.0 Q4 2024 — encrypted-at-rest com queryability sem decrypt server-side), Change Streams advanced (resume token + fullDocument lookup + filter + sharded cluster awareness — base do Outbox pattern resiliente). MongoDB 8.1 (Q3 2025) adiciona $rankFusion para hybrid search nativo. Time-series collections (GA 5.0, otimização contínua até 8.0) competem com TimescaleDB para workloads <100k writes/s.
Atlas Search deep — Lucene index + $search stage:
// Definição de índice via Atlas UI/CLI/API — JSON declarativo
// orders.search_idx (static mapping — recomendado >10M docs)
{
"mappings": {
"dynamic": false,
"fields": {
"customer_name": [
{ "type": "string", "analyzer": "lucene.portuguese" },
{ "type": "autocomplete", "tokenization": "edgeGram", "minGrams": 2, "maxGrams": 10 }
],
"product_name": { "type": "string", "analyzer": "lucene.standard" },
"status": { "type": "token" }, // exact match facet
"tenant_id": { "type": "token" }, // pre-filter multi-tenant
"created_at": { "type": "date" },
"total_amount": { "type": "number" }
}
}
}
// Query: full-text + autocomplete + fuzzy + facets + score boost
db.orders.aggregate([
{
$search: {
index: "search_idx",
compound: {
must: [
{ equals: { path: "tenant_id", value: "tenant_42" } } // pre-filter sempre
],
should: [
{
text: {
query: "notebook dell",
path: ["customer_name", "product_name"],
fuzzy: { maxEdits: 1, prefixLength: 2 }, // typo tolerance
score: { boost: { value: 3 } }
}
},
{
autocomplete: {
query: "joa",
path: "customer_name",
score: { boost: { value: 2 } }
}
}
],
filter: [
{ range: { path: "created_at", gte: ISODate("2026-01-01") } }
]
}
}
},
{
$facet: {
results: [{ $limit: 20 }, { $project: { customer_name: 1, score: { $meta: "searchScore" } } }],
status_facet: [{ $sortByCount: "$status" }]
}
}
]);
dynamic: true indexa todos os campos — index size explode em collection 100M docs (overhead 30-60% storage). Static mapping com selected fields: 5-10% overhead. analyzer: lucene.portuguese aplica stemming PT (pedidos → pedid). lucene.standard para identifiers/SKUs.
Atlas Vector Search — HNSW + $vectorSearch:
// Índice vector — HNSW com filter fields declarados
{
"fields": [
{
"type": "vector",
"path": "embedding",
"numDimensions": 1536, // OpenAI text-embedding-3-small
"similarity": "cosine" // cosine | euclidean | dotProduct
},
{ "type": "filter", "path": "tenant_id" }, // pre-filter multi-tenant
{ "type": "filter", "path": "status" }
]
}
// Query: top-K semantic + pre-filter
db.orders.aggregate([
{
$vectorSearch: {
index: "vector_idx",
path: "embedding",
queryVector: queryEmbedding, // [1536 floats] gerado pelo embedder
numCandidates: 200, // 10-20x limit — recall vs latency
limit: 20,
filter: {
tenant_id: "tenant_42", // CRITICAL: filter declarado no índice
status: { $in: ["completed", "shipped"] }
}
}
},
{ $project: { customer_name: 1, score: { $meta: "vectorSearchScore" } } }
]);
numCandidates próximo de limit → recall ruim (HNSW retorna approximate, precisa explorar mais nós). Rule: 10-20x. p99 típico: 50-200ms para 1M docs 1536-dim em M30 cluster. Pre-filter via filter field declarado no índice usa HNSW pre-filtering nativo — sem isso, post-filter scan.
Hybrid search MongoDB 8.1 — $rankFusion:
// $rankFusion: combina múltiplos pipelines com Reciprocal Rank Fusion nativo
db.orders.aggregate([
{
$rankFusion: {
input: {
pipelines: {
textSearch: [
{ $search: { index: "search_idx", text: { query: "notebook dell", path: "product_name" } } },
{ $limit: 100 }
],
vectorSearch: [
{ $vectorSearch: { index: "vector_idx", path: "embedding", queryVector: qVec, numCandidates: 200, limit: 100 } }
]
}
},
combination: { weights: { textSearch: 0.4, vectorSearch: 0.6 } } // bias semantic
}
},
{ $limit: 20 }
]);
Pre-8.1: implementar RRF manual via $unionWith + $group + score normalization. $rankFusion faz isso nativo, normaliza ranks (não scores brutos — vector dominaria).
Queryable Encryption — range queries on encrypted fields (GA 8.0):
import { MongoClient, ClientEncryption } from 'mongodb';
const encryptedFieldsMap = {
'logistics.customers': {
fields: [
{ keyId: dekId1, path: 'cpf', bsonType: 'string', queries: { queryType: 'equality' } },
{
keyId: dekId2,
path: 'birth_year',
bsonType: 'int',
queries: { queryType: 'range', min: 1900, max: 2030, sparsity: 1, trimFactor: 4 }
},
{ keyId: dekId3, path: 'email', bsonType: 'string', queries: { queryType: 'equality' } }
]
}
};
const client = new MongoClient(uri, {
autoEncryption: {
keyVaultNamespace: 'encryption.__keyVault',
kmsProviders: { aws: { accessKeyId, secretAccessKey } },
encryptedFieldsMap
}
});
// Query — driver encrypta/decrypta transparente, server nunca vê plaintext
await db.collection('customers').find({
email: 'joao@example.com', // equality on encrypted
birth_year: { $gte: 1980, $lte: 1995 } // range on encrypted (8.0+)
}).toArray();
QE usa structured encryption com encrypted indexes — server faz match em ciphertext. Overhead: storage 2-4x, query latency +20-50% para encrypted fields. vs application-level encryption: QE permite query, app-level força decrypt-all-then-filter. Aplicar só em PII queryable (CPF, email) — não em campos raramente filtrados.
Change Streams advanced — resume token + fullDocument + filter:
const pipeline = [
{ $match: { 'fullDocument.tenant_id': 'tenant_42', operationType: { $in: ['insert', 'update'] } } }
];
let resumeToken = await loadResumeTokenFromRedis(); // persistir SEMPRE
const changeStream = db.collection('orders').watch(pipeline, {
fullDocument: 'updateLookup', // SELECT full doc post-update (custa +read)
fullDocumentBeforeChange: 'whenAvailable', // 6.0+ — diff pre/post
resumeAfter: resumeToken,
maxAwaitTimeMS: 1000
});
for await (const change of changeStream) {
await publishToKafka('orders.cdc', change.fullDocument);
await persistResumeToken(change._id); // após ack do downstream
}
Sem resumeAfter persistido: restart perde events ocorridos durante downtime se janela > oplog retention (default 1h em replica sets pequenos). Configurar replSetResizeOplog para 24-48h em cluster com Change Stream consumers críticos. Sharded change streams: routing via mongos, latência +50-100ms vs replica set unsharded. fullDocument: 'updateLookup' faz read adicional pós-update — pode retornar versão > diff (eventual consistency entre oplog e majority read).
Time-series collections:
db.createCollection('courier_locations', {
timeseries: {
timeField: 'ts',
metaField: 'courier_id',
granularity: 'minutes' // seconds | minutes | hours
},
expireAfterSeconds: 60 * 60 * 24 * 30 // TTL 30d automático
});
db.courier_locations.insertMany([
{ ts: new Date(), courier_id: 'C-1', lat: -23.5, lng: -46.6, speed_kmh: 45 }
]);
// $bucketAuto para downsample em query
db.courier_locations.aggregate([
{ $match: { courier_id: 'C-1', ts: { $gte: ISODate('2026-05-01') } } },
{ $bucketAuto: { groupBy: '$ts', buckets: 24, output: { avg_speed: { $avg: '$speed_kmh' } } } }
]);
granularity: 'seconds' para hourly data → buckets de 1h cada com 1 doc → waste storage (compression bucket-level perde efeito). Match granularity ao write rate. vs TimescaleDB: Mongo time-series não tem continuous aggregates nativo (precisa scheduled $merge jobs); TSDB Postgres mais maduro para analytical workloads.
Stable API — versioned API:
const client = new MongoClient(uri, {
serverApi: { version: '1', strict: true, deprecationErrors: true }
});
Opt-in declara compatibilidade — server rejeita comandos fora da v1 ou deprecated. Forward-compat com upgrades server major sem breakage de driver.
Stack Logística aplicada:
orders (customer_name + product_name + tracking_code) com autocomplete em customer_name para CS dashboard — substitui Elasticsearch que tinha 18k QPS pico, custo M30 Atlas Search ~40% menor que cluster ES dedicado.order_embeddings (descrição produto + endereço destino → embedding 1536-dim) para "pedidos similares" detection — fraud team usa para clusterizar padrões anômalos. Pre-filter tenant_id via filter field.customers.cpf + customers.email + couriers.cpf — equality queries para login/lookup, sem expor plaintext em backups/logs.orders driving Outbox → Kafka topic orders.events (substitui polling outbox table — latência 2s → ~150ms p95). Resume token persistido em Redis com TTL 7d.courier_locations granularity minutes, retention 30d, downsample hourly via $merge scheduled job para courier_locations_hourly.Anti-patterns:
$search index com dynamic: true em collection 100M docs — index storage 30-60% overhead, build time horas. Static mapping com fields selected.$vectorSearch com numCandidates ≈ limit — recall <70% (HNSW approximate precisa explorar). Rule: 10-20x.granularity: 'seconds' para hourly write rate — buckets sub-utilizados, perde compression vantagem.$rankFusion sem ajustar weights — text e vector contribuem 50/50 default, semantic-heavy use case quer 0.3/0.7.$vectorSearch filter em campo NÃO declarado como filter no índice — Mongo ignora pre-filter, faz full HNSW scan + post-filter (latência 10x).fullDocument: 'updateLookup' em high-throughput stream sem necessidade — duplica read load no primary; usar diff via updateDescription quando suficiente.tenant_id no compound.must — multi-tenant search vaza dados via score ranking.sparsity: 1 em range field high-cardinality (timestamp ms) — query latency degrada (sparsity controla false-positives no encrypted index); ajustar sparsity/trimFactor por field.Cruza com 02-12 §2.4 (indexes foundation — search/vector indexes não substituem B-tree para OLTP), §2.7 (aggregation pipeline — $search/$vectorSearch são primeiros stages), §2.10 (replica sets — Change Streams dependem do oplog), §2.11 (sharding — change stream routing via mongos), §2.18 (schema design — embedded vs referenced afeta fullDocumentBeforeChange), §2.19 (aggregation advanced — $facet/$lookup complementam $search), 02-15 §2.20 (vector search comparison: pgvector vs Atlas Vector Search vs Qdrant — co-location vs especialização), 03-08 §2.13 (encryption foundation — KMS + DEK hierarchy), 04-03 §2.19 (Outbox via Change Streams ao invés de polling table), 04-13 §2.12 (CDC Mongo → Kafka via Debezium connector como alternativa).
Você precisa, sem consultar:
explain('executionStats'), quais campos olhar.Adicionar MongoDB pra eventos de Logística: não substitui Postgres, complementa.
mongodb em Node (Fastify do 02-08).external_events armazena tudo. Cada doc tem source, receivedAt, tenantId, payload (livre), processedAt, result.external_events tem $jsonSchema exigindo source, tenantId, receivedAt, payload.payload pode ser qualquer shape (intencional).{ tenantId: 1, source: 1, receivedAt: -1 }, listagem por tenant + source.{ receivedAt: 1 }, expireAfterSeconds: 7776000).payload.tags (quando existe).POST /webhooks/:source, recebe e armazena evento.GET /events?tenant=X&source=Y&from=&to=, paginated listing.GET /events/stats, aggregation:
processedAt: null, processa (lógica fictícia: parse, atualizar order em Postgres se aplicável), marca processedAt e result.processedAt.$match → $group → $facet → $project que produz dashboard com 3 métricas em 1 query.$lookup na aggregation principal, denormalize se precisar (anote a decisão).explain mostrando uso de index.payload.external_events em real-time (via Mongo CDC).Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Quando faz mais sentido modelar uma relação como embed (sub-documento) em vez de reference no MongoDB?
Q2Por que executar `$match` no início do pipeline de aggregation é uma decisão de performance crítica?
Q3O que é um anti-pattern claro ao usar MongoDB?
Q4Em Atlas Vector Search, por que `numCandidates` deve ser 10-20x o `limit` desejado?
Q5Em Change Streams, por que persistir o resume token é crítico?
Destrava
02-12 é prereq dos seguintes módulos: