Dados e bancos de dados
MongoDB e o modelo de documentos: design de schema, agregação e sharding
Intermediário9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
MongoDB não é um banco relacional sem as restrições: ele recompensa outro instinto de modelagem, em que a unidade de armazenamento é aquilo que você lê de uma vez só. Este guia cobre o modelo de documentos e o BSON, padrões de design de schema, o pipeline de agregação, a escolha de índices, a durabilidade em replica sets, a escolha da shard key e onde transações multidocumento realmente fazem sentido.
O que você vai aprender
- Explicar o modelo de documentos, os tipos BSON e os limites que moldam um design
- Escolher entre embutir e referenciar, e aplicar os padrões extended reference e bucket
- Escrever pipelines de agregação que filtram cedo e dão forma aos dados no servidor
- Projetar índices com a regra igualdade-ordenação-intervalo e verificá-los com explain
- Raciocinar sobre eleições em replica sets, read e write concerns, sharding e transações
O MongoDB é o banco de dados que mais se adota pelo motivo errado e mais se abandona pelo motivo certo. O motivo errado é "não queremos definir um schema"; o motivo certo para sair é descobrir, dois anos depois, que os dados eram relacionais desde o início. Usado de forma deliberada, porém, o modelo de documentos resolve um problema real: quando aquilo que você armazena e aquilo que você lê são o mesmo objeto autocontido, mantê-lo em um único documento elimina joins, idas e voltas ao banco e código de mapeamento.
Este guia trata do MongoDB 8 e parte do princípio de que você já conhece modelagem relacional. Os hábitos que se transferem são os ligados a padrões de acesso; os ligados a formas normais, em sua maioria, não, e saber distinguir uns dos outros é justamente onde está toda a habilidade.
Documentos, BSON e o que eles implicam
Um documento é um conjunto de pares campo-valor armazenado em BSON, uma codificação binária de JSON com tipos extras: ObjectId (um identificador de 12 bytes que embute um timestamp), Date, Decimal128 para valores monetários exatos, Binary, inteiros de 32 e 64 bits e expressões regulares. Documentos vivem em coleções, e coleções vivem em um banco de dados. Todo documento tem um _id único, indexado automaticamente.
{
"_id": { "$oid": "665f1a2b7c9e4f0012ab34cd" },
"sku": "BK-1042",
"title": "Field Guide to Estuaries",
"price": { "$numberDecimal": "24.90" },
"tags": ["nature", "reference"],
"dimensions": { "widthMm": 130, "heightMm": 198 },
"stock": [
{ "warehouse": "LYON", "quantity": 12 },
{ "warehouse": "PORTO", "quantity": 3 }
]
}
Três propriedades dessa estrutura determinam todo o resto. Os campos podem ser aninhados à vontade, então um agregado inteiro cabe em um único registro. Arrays são cidadãos de primeira classe e podem ser indexados, então um relacionamento um-para-muitos não precisa de uma segunda coleção. E um documento é limitado a 16 MB, o que é uma restrição de design disfarçada de limite técnico: qualquer array que possa crescer sem limite é um erro de modelagem, não um problema de capacidade.
Uma escrita em um único documento é atômica, não importa quão profundo seja o aninhamento. Essa garantia é o que faz a modelagem de documentos funcionar: se os dados que precisam mudar juntos vivem no mesmo documento, você ganha comportamento transacional de graça.
Documentos não são "sem schema" em nenhum sentido útil — seu código depende de os campos existirem e terem o tipo certo. Declare essa dependência:
db.createCollection("product", {
validator: { $jsonSchema: {
bsonType: "object",
required: ["sku", "title", "price"],
properties: {
sku: { bsonType: "string", pattern: "^[A-Z]{2}-[0-9]{4}$" },
price: { bsonType: "decimal", description: "list price, excl. tax" }
}
}},
validationLevel: "strict"
});
Leitura e escrita
O mongosh é o shell, e as APIs dos drivers espelham-no de perto.
db.product.insertOne({ sku: "BK-1042", title: "Field Guide to Estuaries", price: NumberDecimal("24.90") });
db.product.find({ tags: "nature", "price": { $lt: NumberDecimal("30") } })
.sort({ title: 1 }).limit(20);
// Casar um mesmo elemento do array, não duas condições independentes
db.product.find({ stock: { $elemMatch: { warehouse: "LYON", quantity: { $gt: 0 } } } });
db.product.updateOne(
{ sku: "BK-1042", "stock.warehouse": "LYON" },
{ $inc: { "stock.$.quantity": -1 }, $set: { updatedAt: new Date() } }
);
Dois detalhes pegam quem está começando. find({ "stock.warehouse": "LYON", "stock.quantity": { $gt: 0 } }) encontra um documento em que algum elemento tem aquele depósito e algum elemento tem estoque — não necessariamente o mesmo; o que você queria era $elemMatch. E os operadores de atualização ($set, $inc, $push, $pull, $addToSet) modificam os campos no lugar, enquanto passar um documento puro o substitui inteiro, apagando silenciosamente tudo o que você deixou de fora. Quando uma atualização aninhada se comporta de forma estranha, colar o documento em um formatador de JSON para ver sua forma real é mais rápido do que forçar a vista na saída do shell.
Modelagem: embutir ou referenciar
O instinto relacional é normalizar até que cada fato exista em um só lugar. O instinto de documentos é armazenar junto o que é lido junto e aceitar duplicação quando a cópia é imutável ou barata de atualizar. Nenhum dos dois está certo sempre; a pergunta é sempre o que a aplicação pede.
Embuta quando o filho não tem vida própria, existe em quantidade limitada e é lido junto com o pai — itens de pedido dentro do pedido, um endereço dentro do cliente, o estoque por depósito dentro do produto. Uma leitura, uma escrita atômica, nenhum join.
Referencie quando o filho é grande, ilimitado, compartilhado entre pais ou acessado sozinho — um autor referenciado por milhares de livros, ou uma trilha de auditoria que cresce para sempre.
Quatro padrões cobrem a maior parte do meio-termo:
- Extended reference. Armazene a referência mais os dois ou três campos que você sempre exibe:
{ "authorId": ..., "authorName": "..." }. Isso tira a busca do caminho de leitura. É desnormalização, então decida desde o início como uma mudança de nome se propaga — muitas vezes por um job em background, às vezes de jeito nenhum, porque o valor é histórico. - Bucket. Para séries temporais de alto volume, agrupe muitas leituras em um documento por dispositivo por hora, em vez de um documento por leitura. Menos documentos, índice menor, overhead drasticamente menor. As coleções time series nativas do MongoDB implementam isso internamente.
- Computed. Armazene um total acumulado ou uma contagem ao lado dos dados brutos e mantenha-o com
$incna escrita, em vez de agregar a cada leitura. - Subset. Mantenha no documento pai os dez elementos de array mais recentes ou mais relevantes e o histórico completo em uma coleção separada, para que o documento mais acessado continue pequeno.
O antipadrão que merece ser citado explicitamente é o array ilimitado: comentários embutidos em um post, eventos embutidos em um usuário. Ele passa dos 16 MB e, muito antes disso, cada atualização reescreve um documento que não para de crescer. O raciocínio relacional sobre chaves e dependências visto em design de banco de dados e normalização continua útil aqui: você está escolhendo quais dependências colocar fisicamente juntas, não fingindo que elas não existem.
O pipeline de agregação
A agregação é uma sequência de estágios (stages), cada um transformando o fluxo de documentos que recebe. É onde o MongoDB faz o trabalho que o SQL faz com GROUP BY, joins e funções de janela.
db.order.aggregate([
{ $match: { placedAt: { $gte: ISODate("2026-01-01") }, status: "SHIPPED" } },
{ $unwind: "$lines" },
{ $group: {
_id: "$lines.sku",
units: { $sum: "$lines.quantity" },
revenue: { $sum: { $multiply: ["$lines.quantity", "$lines.unitPrice"] } }
}},
{ $sort: { revenue: -1 } },
{ $limit: 10 },
{ $lookup: { from: "product", localField: "_id", foreignField: "sku", as: "product" } },
{ $project: { _id: 0, sku: "$_id", units: 1, revenue: 1, title: { $first: "$product.title" } } }
]);
A regra mais importante de todas é colocar $match primeiro, e $sort logo depois se um índice puder atender aos dois — são os únicos estágios que conseguem usar um índice, e só enquanto continuam no início do pipeline. Tudo o que vem depois de $unwind ou $group opera sobre documentos intermediários materializados.
Outros estágios que vale conhecer: $facet executa vários subpipelines sobre a mesma entrada, que é como você calcula resultados e suas contagens agregadas em uma única ida e volta; $bucket e $bucketAuto montam histogramas; $merge e $out gravam os resultados em uma coleção, transformando um pipeline em uma view materializada agendada. $lookup faz um left outer join, e funciona, mas um pipeline que precisa de três deles está dizendo que o modelo é relacional.
Índices
As regras de índice lembram as relacionais do guia de planos de execução, com particularidades do MongoDB:
- Índices compostos seguem a regra ESR: primeiro os campos de igualdade (Equality), depois os de ordenação (Sort) e por fim os de intervalo (Range). Colocar a ordenação antes do intervalo é o que evita uma ordenação em memória, e o MongoDB aborta ordenações acima de 100 MB, a menos que você permita o uso de disco.
- Índices multikey são o que você obtém automaticamente ao indexar um campo de array; cada elemento vira uma entrada. Não é possível combinar dois campos de array no mesmo índice composto.
- Índices parciais (
partialFilterExpression) indexam apenas os documentos que atendem ao filtro — ideal para um pequeno subconjunto "pendente" de uma coleção grande. Sparse é a versão mais antiga e menos precisa. - Índices TTL apagam documentos um tempo definido depois de um campo de data, que é como você expira sessões ou logs sem um job de limpeza.
- Índices wildcard cobrem nomes de campo desconhecidos em atributos definidos pelo usuário. Práticos, e bem maiores que um índice direcionado.
- Índices text e vector dão suporte a busca por palavra-chave e por similaridade; para relevância séria em busca full-text, um mecanismo dedicado ainda é melhor, como explica o guia de Solr.
Verifique com explain, lendo-o do mesmo jeito que você lê um plano relacional:
db.order.find({ status: "SHIPPED", placedAt: { $gte: ISODate("2026-01-01") } })
.sort({ placedAt: -1 })
.explain("executionStats");
Compare totalKeysExamined, totalDocsExamined e nReturned. Um número de documentos examinados muito acima do de documentos retornados significa que o índice filtra mal; um COLLSCAN como estágio vencedor significa que o índice nem está sendo usado.
Replica sets, sharding e consistência
Um replica set é um grupo de processos mongod que mantêm os mesmos dados: um primário que aceita escritas e secundários que replicam o oplog dele. Se o primário ficar inacessível, os membros restantes fazem uma eleição e uma maioria escolhe um novo primário, normalmente em poucos segundos. Rode sempre um número ímpar de membros votantes, distribuídos em domínios de falha separados, e use um árbitro só como último recurso.
Durabilidade e atualidade dos dados são escolhas por operação, não configurações globais do servidor:
- Write concern
{ w: "majority" }só confirma depois que a maioria tem a escrita, o que a torna durável em um failover.w: 1confirma no primário, e a escrita pode sofrer rollback se esse primário falhar. Acrescentej: truepara exigir o journal gravado em disco. - Read concern
"local"retorna o que o nó tiver;"majority"retorna apenas dados que não podem mais sofrer rollback;"snapshot"oferece uma visão consistente de um ponto no tempo dentro de uma transação. - Read preference decide qual membro atende a leitura.
secondaryPreferreddistribui a carga ao custo de dados possivelmente defasados — uma boa troca para relatórios, uma péssima para fluxos em que você precisa ler a própria escrita.
O sharding particiona uma coleção entre shards por uma shard key. Cada shard é, ele próprio, um replica set; os roteadores mongos direcionam as consultas, e os config servers guardam os metadados. Uma consulta que inclui a shard key é roteada para um único shard; uma que não inclui é enviada a todos, e essa é a diferença entre um milissegundo e um fan-out pelo cluster inteiro.
Escolha a shard key por três critérios: alta cardinalidade, distribuição uniforme das escritas e presença nos filtros de consulta mais comuns. Um timestamp reprova no segundo — todas as inserções caem no shard dono do intervalo mais alto. Aplicar hash à chave espalha as escritas de forma uniforme, mas destrói as consultas por intervalo. Na prática, uma chave composta como { tenantId: 1, orderId: "hashed" } costuma atender aos três. Versões modernas permitem refinar uma shard key e até fazer resharding de uma coleção em produção, mas é uma operação pesada; acerte mais ou menos de primeira. E não faça sharding antes da hora — um replica set bem indexado em um hardware decente aguenta muito mais do que a maioria dos times imagina, e o sharding traz um peso operacional real, tema que o guia de arquitetura de big data retoma.
Transações abrangem vários documentos e coleções com isolamento snapshot, tanto em replica sets quanto em clusters shardados. São a ferramenta certa para uma transferência entre duas contas guardadas em dois documentos. São a ferramenta errada para algo que um modelo melhor teria transformado em uma atualização de um único documento, e têm um custo real em latência e contenção de locks.
Principais pontos
- Um documento é atômico na escrita, então armazene junto o que muda junto; o limite de 16 MB é uma restrição de modelagem que proíbe arrays ilimitados.
- Embuta dados limitados, dependentes e lidos juntos; referencie dados compartilhados ou ilimitados; use os padrões extended reference, bucket, computed e subset para os casos intermediários.
- Pipelines de agregação precisam começar com
$match(e um$sortbem posicionado) para usar um índice;$facet,$mergee$bucketatendem às necessidades de relatórios sem idas e voltas extras. - Monte índices compostos na ordem igualdade-ordenação-intervalo e confirme com
explain("executionStats")que os documentos examinados ficam próximos dos documentos retornados. - Write concern, read concern e read preference são decisões por operação;
w: "majority"é o que faz uma escrita sobreviver a um failover. - Faça sharding só quando for preciso, com uma chave de alta cardinalidade, escritas bem distribuídas e presença nos filtros mais comuns — e valide com
$jsonSchemao schema do qual você já depende.
Fontes e leituras complementares
Perguntas frequentes
- O MongoDB realmente não tem schema?
- Por padrão, ele não impõe um schema na escrita, o que é bem diferente de não ter nenhum. Seu código de aplicação, seus índices e cada consulta que você escreve codificam um schema, declarado ou não. As coleções aceitam regras de validação com JSON Schema, e aplicá-las aos campos de que você depende transforma uma corrupção silenciosa em um erro imediato, que em geral é o que você quer em produção.
- É seguro usar transações do MongoDB em código comum?
- Sim, desde a versão 4.0 em replica sets e a 4.2 em clusters shardados, e elas oferecem isolamento snapshot com commit tudo-ou-nada. Também são mais caras que uma escrita em um único documento, que já é atômica. Trate-as como exceção para operações que realmente envolvem vários documentos, e não como o invólucro padrão, e modele os dados para que a maioria das atualizações toque um único documento.
- Como escolher uma shard key?
- Você precisa de alta cardinalidade, distribuição uniforme das escritas e, de preferência, alinhamento com o filtro das consultas mais comuns, para que as leituras atinjam um único shard. Uma chave monotonicamente crescente, como um timestamp, manda todas as inserções para o mesmo shard; aplicar hash a ela resolve a distribuição, mas quebra as consultas por intervalo. Uma chave composta que combina um tenant ou uma categoria com um componente hasheado costuma ser o melhor meio-termo.
- Quando devo usar MongoDB em vez de PostgreSQL?
- Quando seus dados realmente chegam como documentos autocontidos, de formato variável, que você lê por inteiro, e quando escalar escritas horizontalmente importa mais do que fazer joins entre muitas entidades. Se seus padrões de acesso são relacionais — relacionamentos muitos-para-muitos, joins ad hoc, necessidades fortes de relatórios —, um banco relacional com uma coluna jsonb geralmente atende melhor.