Tecnologias/02 - Bancos de Dados/02 - Não Relacionais/01 - MongoDB6 min
Modelagem de Documentos
Conceito
Modelar em MongoDB é responder uma pergunta, repetidamente:
Este dado é lido junto com aquele?
Se sim, embuta. Se não, referencie. Todo o resto é refinamento.
Isso inverte o método relacional. Em SQL você modela as entidades, normaliza até a 3FN e deixa o otimizador montar as consultas (4. Modelagem e Normalização). Aqui você lista as consultas da aplicação e desenha o documento para servi-las — aceitando duplicação como parte do projeto, não como falha.
Embutir ou referenciar
| Relação | Padrão | Por quê |
|---|---|---|
| 1 : poucos (endereços de um cliente, itens de um pedido) | embutir | sempre lidos juntos, e o array não cresce |
| 1 : muitos (produtos de uma categoria) | referenciar do lado "muitos" | array cresceria demais |
| 1 : milhões (eventos de um sensor, logs) | referenciar + bucket | nem array nem documento por evento aguentam |
| muitos : muitos (alunos ↔ turmas) | referenciar dos dois lados, ou coleção de ligação | duplicar seria insustentável |
As três perguntas que decidem, na ordem:
- O array tem limite conhecido? Se pode crescer sem teto → não embuta. Os 16 MB chegam.
- Eu leio o filho sem o pai? Se sim, o filho quer coleção própria.
- O filho muda com frequência? Dado embutido e duplicado precisa de atualização em N lugares.
Os padrões que resolvem os casos difíceis
Extended reference — duplique só os campos que a tela mostra:
// pedido: referência + os 2 campos que a listagem exibe
cliente: { _id: ObjectId("..."), nome: "Ana Souza" }
Evita o $lookup na consulta mais frequente. Se o nome mudar, você atualiza os pedidos
em lote (ou não atualiza — num pedido, o nome de então é o correto).
Subset — embuta os N primeiros, referencie o resto:
// produto: as 5 avaliações que aparecem na página
avaliacoes_recentes: [ {...}, {...}, {...}, {...}, {...} ],
total_avaliacoes: 3742
// as outras 3737 ficam na coleção `avaliacoes`
Computed — grave o resultado, não recalcule:
total: Decimal128("429.70"), // em vez de somar itens a cada leitura
media_nota: 4.6, qtd_notas: 3742
Bucket — agrupe muitos eventos pequenos em documentos por janela de tempo. É o padrão para série temporal (e a base das time series collections nativas, desde a 5.0):
{
sensor_id: "s-42",
dia: ISODate("2026-09-08"),
leituras: [ { t: ISODate("...T00:00:00Z"), v: 21.4 }, /* ~1440 no dia */ ],
min: 19.8, max: 26.1, media: 22.3 // computed junto
}
1440 documentos por dia por sensor viram 1. Menos overhead de índice, menos I/O, e o array tem teto conhecido — o que resolve o problema dos 16 MB por construção.
Schema versioning — carregue a versão no documento:
{ _schema: 3, nome: "Ana", contatos: [{ tipo: "email", valor: "..." }] }
Sem migração global: o app lê versões antigas e escreve na nova. É o superpoder real do schema flexível, e quase ninguém usa.
Trade-offs
Duplicar acelera leitura e cobra na escrita. O nome do cliente em 40 mil pedidos é
uma leitura instantânea e uma atualização de 40 mil documentos. A pergunta certa não é
"posso duplicar?", é "esse campo muda, e se mudar eu preciso propagar?". Para dado
histórico (pedido, nota fiscal, log), duplicar é mais correto — o valor de então é o
valor certo.
Embutir tem teto. 16 MB por documento, e o custo aparece antes disso: o MongoDB lê e grava o documento inteiro. Um documento de 8 MB do qual você só precisa de 3 campos é 8 MB de I/O por leitura.
Modelar por consulta assume que você conhece as consultas. Se em seis meses o produto
pedir uma pergunta nova, o schema não vai ajudar — e a resposta será um $lookup lento ou
uma migração. Em SQL normalizado, a pergunta nova é só um JOIN novo. É aqui que a
troca aparece de verdade.
O trade-off central: o schema flexível não elimina o schema — ele move a decisão para o momento da escrita e a responsabilidade para o código. O ganho é evoluir sem
ALTER TABLE; o custo é que nada garante o formato além do seu próprio cuidado.
Exemplo prático
O mesmo domínio, modelado errado e certo.
// ❌ Relacional disfarçado: 3 coleções, 2 $lookup na consulta da tela
// pedidos: { _id, cliente_id, criado_em }
// itens: { _id, pedido_id, sku, qtd, preco }
// clientes: { _id, nome, email }
db.pedidos.aggregate([
{ $match: { _id: id } },
{ $lookup: { from: "itens", localField: "_id", foreignField: "pedido_id", as: "itens" } },
{ $lookup: { from: "clientes", localField: "cliente_id", foreignField: "_id", as: "cliente" } }
])
// Sem FK, sem transação por padrão, e agora sem performance. Você pagou por nada.
// ✅ Um agregado, uma leitura
{
_id: ObjectId("..."),
cliente: { _id: ObjectId("..."), nome: "Ana Souza" }, // extended reference
itens: [ { sku: "A1", qtd: 1, preco: Decimal128("249.90") } ], // 1:poucos
total: Decimal128("249.90"), // computed
status: "pago",
criado_em: ISODate("2026-09-08T14:12:00Z"),
_schema: 2
}
db.pedidos.findOne({ _id: id }) // acabou
E o schema de volta ao banco, quando você quer garantias:
db.createCollection("pedidos", {
validator: { $jsonSchema: {
bsonType: "object",
required: ["cliente", "itens", "total", "status", "criado_em"],
properties: {
status: { enum: ["novo", "pago", "enviado", "cancelado"] },
total: { bsonType: "decimal", minimum: 0 },
itens: { bsonType: "array", minItems: 1, maxItems: 200 } // teto explícito!
}
}},
validationLevel: "strict", validationAction: "error"
})
O documento deve corresponder ao aggregate root do seu domínio — a unidade que muda junto, é consultada junto e precisa ser consistente junto. Um pedido com seus itens é um agregado. Um cliente com todos os seus pedidos não é: você lê os pedidos sem o cliente, e a lista cresce para sempre. Ver 2. Abstração.
Como isso falha em produção
- Array sem teto.
comentarios: []num post viral →BSONObjectTooLarge, e a correção é remodelar com o sistema no ar. $pushem documento grande. Cada inserção reescreve o documento inteiro; a escrita fica mais lenta conforme cresce.- Duplicação sem plano de propagação. Nome do cliente em 5 coleções, e a de faturamento nunca foi atualizada.
$lookupem cascata. Pipeline com 3 estágios de lookup: o tempo multiplica, não soma.- Nenhuma validação. Três formatos de
telefoneconvivendo, descobertos no relatório. - Índice para cada consulta nova. Índice custa escrita e RAM; 15 índices numa coleção de escrita intensa é o gargalo.
Aprofundamento aplicado
A fronteira do agregado precisa de limite de crescimento
Embutir itens de um pedido combina dados lidos juntos e com ciclo de vida comum. Embutir todo o histórico de acesso de um usuário cria crescimento contínuo, mesmo quando a tela só precisa das últimas dez ocorrências.
| Pergunta | Efeito no desenho |
|---|---|
| Os dados são alterados juntos? | Pode favorecer documento único |
| A coleção aninhada cresce sem limite? | Pode exigir referência ou divisão em blocos |
| O mesmo dado aparece em milhares de documentos? | A atualização passa a exigir propagação |
| O valor é um retrato histórico? | Duplicar pode representar corretamente o domínio |
O endereço de entrega de um pedido concluído pode ser uma fotografia da compra. Atualizar esse endereço toda vez que o cliente muda de casa destruiria informação histórica. Já uma cópia do nome atual para busca precisa de política de atualização.
Exercício: modele perfil, últimos acessos e histórico completo. Resultado esperado: o perfil permanece limitado e a leitura do histórico é paginada; explique quais cópias são históricas e quais devem convergir ao valor atual.
Referência: modelagem de documentos.
Relacionado
- 1. MongoDB
- 4. MongoDB vs Relacional
- 4. Modelagem e Normalização
- Denormalization
- Document Store
- 5. Índices
Parte de MOC MongoDB · Data Modeling (MongoDB)