trilha

Tecnologias/02 - Bancos de Dados/02 - Não Relacionais/01 - MongoDB6 min

MongoDB

Conceito

MongoDB guarda documentos BSON (JSON binário, com tipos: ObjectId, Date, Decimal128, binário) em coleções, sem schema obrigatório.

A tese não é "sem schema" — é localidade:

Um pedido, com seus itens, endereço de entrega e status, é um documento. Uma leitura de disco traz a coisa inteira. Em modelo relacional normalizado, o mesmo pedido são 4 tabelas e 3 joins.

O schema não desapareceu; ele saiu do banco e foi para a aplicação (e, se você quiser de volta, para o $jsonSchema de validação). Essa é a troca central, e vale tanto de elogio quanto de crítica.

Arquitetura em uma tela

  • mongod é o processo do servidor. WiredTiger é o motor de armazenamento: concorrência em nível de documento, snapshots MVCC, compressão (Snappy/Zstd) por padrão.
  • Replica set: 1 primário + N secundários. Só o primário aceita escrita; os secundários replicam pelo oplog (uma capped collection). Eleição automática em ~10s por um protocolo tipo Raft. É a unidade mínima de produção — nunca rode um mongod solitário. Ver 3. Transações, Replica Set e Durabilidade.
  • Sharding: mongos (roteador) + config servers + N shards, cada um um replica set. A shard key decide a distribuição — e é a decisão mais difícil de reverter no MongoDB.
  • Aggregation pipeline: estágios encadeados ($match, $group, $sort, $lookup, $unwind, $facet) — é o "SQL" do Mongo, e é bem mais capaz do que a fama sugere.
  • Change streams: fluxo de mudanças em tempo real sobre o oplog. É o CDC nativo, e o caminho natural para alimentar um tópico Kafka.

Índices

Não é um detalhe: no MongoDB, consulta sem índice não é lenta, é uma varredura da coleção inteira — e o padrão silencioso é permitir isso.

Tipo Para quê
single field / compound o essencial
multikey criado automaticamente ao indexar um array
partial só documentos que satisfazem um filtro
TTL expiração automática de documento (sessão, cache, log)
text / wildcard busca textual; campos de nome desconhecido
geo (2dsphere) consulta geoespacial
vector (Atlas) busca semântica / RAG

A regra prática para índice composto é ESR: campos de Equality primeiro, depois Sort, depois Range. Um índice {status: 1, criado_em: -1} serve find({status}).sort({criado_em: -1}) sem ordenação em memória; na ordem inversa, não. Ver 5. Índices.

Versões

MongoDB 8.0 é a major atual; 8.1, 8.2 e 8.3 são rapid releases. A 8.3 (GA em mai/2026) reporta até 45% mais leitura, 35% mais escrita e 15% melhor desempenho de transação ACID em relação à 8.0. A empresa também empurrou o produto para IA — depois da compra da Voyage AI (fev/2025), embeddings e reranking passaram a fazer parte da plataforma, com $vectorSearch e $rerank no pipeline.

Trade-offs

Não há integridade referencial. Não existe foreign key, ON DELETE CASCADE nem constraint entre coleções. Se o pedido referencia um cliente apagado, o banco não reclama. A consistência do grafo de dados é responsabilidade da aplicação — sempre.

$lookup não é JOIN. Ele existe desde a 3.2 e melhorou muito, mas não tem o otimizador de join de um relacional: sem índice no campo estrangeiro, é uma varredura por documento da coleção externa. Um pipeline com dois $lookup sobre coleções grandes é o antipadrão de performance nº 1 — e o sinal de que a modelagem está relacional demais (2. Modelagem de Documentos).

Limite de 16 MB por documento. Parece generoso e é o teto que mata: qualquer array que cresce sem limite (comentários de um post, eventos de um dispositivo, histórico de uma conta) eventualmente estoura, e a correção é uma remodelagem com o sistema em produção.

Consulta ad-hoc é fraca. "Quantos clientes de SP compraram produto da categoria X no Q2, agrupado por canal?" é trivial em SQL e trabalhosa aqui — porque o schema foi desenhado para outras perguntas. Para análise, o caminho é exportar para um armazém ou usar o Atlas Data Federation, não brigar com o pipeline.

Shard key é quase irreversível. Desde a 5.0 é possível refinar e até reshardar, mas em coleção grande é uma operação longa e delicada. Escolher errado (um campo de baixa cardinalidade, ou monotônico como timestamp) concentra a carga num shard — o hot shard.

O trade-off central: MongoDB entrega velocidade de desenvolvimento e escala horizontal de escrita em troca de garantias de schema e liberdade de consulta. Quando o padrão de acesso é conhecido e estável, é uma troca excelente. Quando o dado é muito relacionado e as perguntas mudam, é o pior dos dois mundos.

Exemplo prático

Um documento de pedido — o mesmo dado que seriam 4 tabelas:

{
  _id: ObjectId("..."),
  numero: "PED-2026-0917",
  cliente: {                       // extended reference: só o que a tela mostra
    _id: ObjectId("..."),
    nome: "Ana Souza",
    email: "ana@exemplo.com"
  },
  itens: [                         // 1-para-poucos: EMBUTIDO
    { sku: "A1", nome: "Teclado", qtd: 1, preco: Decimal128("249.90") },
    { sku: "B7", nome: "Mouse",   qtd: 2, preco: Decimal128("89.90")  }
  ],
  total: Decimal128("429.70"),     // pré-calculado: computed pattern
  status: "pago",
  criado_em: ISODate("2026-09-08T14:12:00Z")
}
// índice composto seguindo ESR: Equality (status) → Sort (criado_em)
db.pedidos.createIndex({ status: 1, criado_em: -1 })

// a consulta da tela: uma leitura, zero join
db.pedidos.find({ status: "pago" }).sort({ criado_em: -1 }).limit(20)

// relatório: agregação por dia
db.pedidos.aggregate([
  { $match: { criado_em: { $gte: ISODate("2026-09-01") } } },
  { $group: { _id: { $dateTrunc: { date: "$criado_em", unit: "day" } },
              receita: { $sum: "$total" }, pedidos: { $sum: 1 } } },
  { $sort: { _id: 1 } }
])

Duas decisões de projeto estão visíveis aí, e as duas são deliberadas:

  1. cliente está duplicado dentro do pedido. Se a Ana trocar de e-mail, os pedidos antigos guardam o antigo — o que para um pedido é correto (é um registro histórico), e para um perfil seria errado. Ver Denormalization.
  2. total está pré-calculado. Some os itens toda vez e você paga na leitura; grave o total e você paga em consistência. Como pedido é imutável depois de pago, é barato.

Como isso falha em produção

Sintoma Causa
Query lenta e CPU alta no primário COLLSCAN: falta índice. Confirme com .explain("executionStats")
Memória estourando no aggregate $group/$sort além de 100 MB sem allowDiskUse
BSONObjectTooLarge array crescendo sem limite → 16 MB. Ver bucket pattern
Um shard com 90% da carga shard key monotônica (timestamp, ObjectId) ou de baixa cardinalidade
Leitura devolvendo dado velho readPreference: secondary com réplica atrasada (3. Transações, Replica Set e Durabilidade)
Escrita "perdida" após failover writeConcern: 1 — o primário confirmou e caiu antes de replicar
Réplica precisando de resync total janela do oplog menor que o tempo que ela ficou fora
Dado inconsistente entre documentos ausência de validação: $jsonSchema não foi configurado

Aprofundamento aplicado

Atualização parcial e condição na mesma operação

Ler um documento inteiro, modificar um campo na aplicação e substituir tudo pode sobrescrever alterações independentes. Operadores de atualização reduzem o escopo; um filtro com a versão esperada também detecta concorrência sobre a mesma decisão.

// mongosh; coleção de estudo
 db.estoque_estudo.insertOne({ _id: "p1", saldo: 1 });
 db.estoque_estudo.updateOne(
   { _id: "p1", saldo: { $gte: 1 } },
   { $inc: { saldo: -1 } }
 );

Verifique matchedCount para saber se a condição encontrou um documento. A atomicidade individual não transforma várias atualizações de documentos em uma transação conjunta. Referência: atomicidade de escritas.

Exercício: execute a reserva duas vezes sobre saldo inicial 1. Resultado esperado: a primeira encontra o documento; a segunda não encontra saldo suficiente. Repita concorrentemente e preserve a mesma propriedade.

Relacionado


Parte de MOC MongoDB · mongodb.com/docs · Notas da 8.3

Buscar

Busca por título, seção e texto das notas