O que é MongoDB?
O MongoDB é um banco de dados de documentos. Em vez de linhas em tabelas, ele armazena documentos — objetos semelhantes a JSON codificados como BSON — dentro de collections. Um documento pode conter objetos aninhados e arrays, permitindo que um único registro descreva um agregado completo: um pedido com seus itens, ou um usuário com seus endereços.
Ele surgiu em 2009, quando o modelo relacional parecia pesado para aplicações que cresciam rapidamente e mudavam de formato. Sua promessa era simples: armazenar o objeto que seu código já possui e escalar horizontalmente sem a necessidade de migrações dolorosas. Essa promessa o tornou o banco de dados NoSQL padrão para uma geração de equipes de Node.js e JavaScript.
O trade-off é real. O MongoDB abre mão de joins por padrão e transfere a validação do schema para a sua aplicação. Quando seus dados têm formato de documento, isso é uma vantagem. Quando são profundamente relacionais, um banco de dados relacional costuma ser a ferramenta ideal.
Documentos, coleções e BSON
Um documento é um conjunto ordenado de pares campo-valor. Uma coleção é um grupo de documentos que geralmente compartilham a mesma estrutura, embora nada os obrigue a isso. As coleções são criadas no momento em que você insere dados nelas pela primeira vez.
db.users.insertOne({
email: "[email protected]",
name: "Ada",
address: { city: "London", country: "GB" },
tags: ["admin", "beta"],
});
Os valores possuem tipos BSON, e não apenas tipos JSON. BSON é uma codificação binária que adiciona ObjectId, Date, Decimal128, BinData e outros, permitindo que você armazene datas reais e decimais precisos em vez de strings. A ordem dos campos é preservada e os nomes dos campos diferenciam maiúsculas de minúsculas (case-sensitive).
Um hábito útil é dar a cada documento de uma coleção uma estrutura consistente, mesmo que o servidor permita variações. Isso garante consultas e índices previsíveis, mantendo a flexibilidade para quando você precisar adicionar um campo a apenas um documento primeiro.
_id e ObjectId
Todo documento precisa de um _id único. Se você não fornecer um, o driver gera um ObjectId — um valor de 12 bytes composto por um timestamp, um valor aleatório por processo e um contador incremental. Como o timestamp vem primeiro, os valores de ObjectId são ordenados aproximadamente pelo tempo de criação, o que é conveniente para a paginação.
const { ObjectId } = require("mongodb");
const id = new ObjectId("64f1c2a9e13b4a7d8c9e0011");
id.getTimestamp(); // 2023-09-01T...
O campo _id é indexado automaticamente e é sempre único. Você pode fornecer seu próprio valor — uma chave natural, uma string UUID ou uma chave composta — quando isso tornar as buscas mais baratas ou quando você precisar de inserções idempotentes.
CRUD na prática
Create, read, update e delete mapeiam para um pequeno conjunto de métodos que se comportam da mesma forma no mongosh e no driver do Node.js.
// Create
db.users.insertOne({ email: "[email protected]", plan: "pro" });
db.users.insertMany([
{ email: "[email protected]", plan: "free" },
{ email: "[email protected]", plan: "team" },
]);
// Read
db.users.findOne({ email: "[email protected]" });
db.users.find({ plan: "pro" }).toArray();
// Update
db.users.updateOne(
{ email: "[email protected]" },
{ $set: { plan: "team" } },
);
// Delete
db.users.deleteOne({ email: "[email protected]" });
updateOne e updateMany recebem um filter e um update document. O update document utiliza operadores: $set substitui ou adiciona campos, $unset os remove, $inc soma a um número, $push anexa a um array, $addToSet adiciona apenas se estiver ausente e $pull remove elementos correspondentes de um array.
db.users.updateOne(
{ email: "[email protected]" },
{
$set: { lastSeenAt: new Date() },
$inc: { logins: 1 },
$addToSet: { tags: "beta" },
},
);
Existe também o replaceOne, que substitui todo o documento, exceto o _id. Prefira os operadores de campo para que gravações simultâneas não sobrescrevam as alterações umas das outras.
Consultando com operadores
Um filtro é, por si só, um documento. A igualdade é o padrão, e os operadores começam com $.
db.orders.find({ status: "paid" }); // equality
db.orders.find({ total: { $gt: 5000, $lte: 20000 } }); // range
db.orders.find({ status: { $in: ["paid", "shipped"] } });
db.users.find({ email: { $regex: /@example\.com$/i } });
db.orders.find({ "items.sku": "KB-01" }); // nested field
db.orders.find({
items: { $elemMatch: { qty: { $gte: 2 }, price: { $lt: 3000 } } },
});
$elemMatch é importante quando várias condições devem ser aplicadas ao mesmo elemento de um array. Sem isso, { "items.qty": { $gte: 2 }, "items.price": { $lt: 3000 } } pode corresponder a elementos diferentes.
Combine filtros com $and, $or e $not:
db.orders.find({
$or: [
{ status: "paid" },
{ status: "pending", total: { $lt: 1000 } },
],
});
A linguagem de consulta é composível porque ela é composta por dados, não por uma string. É por isso que construir filtros no código da aplicação parece natural — você monta um objeto e o passa para find.
Projeção, ordenação e paginação
Uma projeção seleciona quais campos devem ser retornados. Inclua campos com 1 ou exclua-os com 0, mas nunca misture os dois estilos, exceto para _id.
db.users.find(
{ plan: "pro" },
{ email: 1, name: 1, _id: 0 },
);
Ordene e pagine com .sort(), .skip() e .limit(). Para offsets grandes, skip torna-se mais lento porque o servidor ainda percorre os documentos ignorados. Prefira a paginação por keyset, onde você filtra pelo último valor visualizado.
db.orders
.find({ customerId, createdAt: { $lt: lastSeen } })
.sort({ createdAt: -1 })
.limit(20);
Índices nos campos de ordenação tornam tanto o filtro quanto a ordenação eficientes, e é aí que entra a próxima seção.
Índices: a diferença entre o rápido e o inutilizável
Sem um índice, o MongoDB lê todos os documentos da coleção — um collection scan. Com um índice, ele busca diretamente o intervalo correspondente. Quase todo problema de performance no MongoDB é causado por um índice ausente ou mal ordenado.
db.users.createIndex({ email: 1 }, { unique: true });
db.orders.createIndex({ customerId: 1, createdAt: -1 });
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
db.products.createIndex({ name: "text", description: "text" });
- Índices de campo único (single-field) aceleram consultas de igualdade e de intervalo em um único campo.
- Índices compostos (compound) seguem a regra ESR: campos de igualdade primeiro, depois ordenação (sort) e, por fim, intervalo (range).
{ customerId: 1, createdAt: -1 }atende tanto ao filtro quanto à ordenação. - Índices únicos (unique) rejeitam duplicatas e tornam os upserts seguros.
- Índices TTL deletam documentos após uma data específica, o que é perfeito para sessões e tokens de uso único.
- Índices de texto (text) suportam a busca
$textcom stemming e scoring.
Use explain() para ver o que o planejador fez. Procure por IXSCAN em vez de COLLSCAN, e verifique se o número de documentos examinados está próximo do número de documentos retornados.
db.orders.find({ customerId, status: "paid" }).explain("executionStats");
O pipeline de agregação
Quando uma query não é suficiente, o framework de agregação executa um pipeline de estágios. Cada estágio transforma um fluxo de documentos e o repassa adiante. Os estágios mais comuns são $match, $group, $sort, $project, $lookup, $unwind e $limit.
Aqui está um exemplo prático: os cinco produtos com maior receita para pedidos pagos.
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $unwind: "$items" },
{
$group: {
_id: "$items.sku",
units: { $sum: "$items.qty" },
revenue: {
$sum: { $multiply: ["$items.qty", "$items.price"] },
},
},
},
{ $sort: { revenue: -1 } },
{ $limit: 5 },
{
$project: {
_id: 0,
sku: "$_id",
units: 1,
revenue: 1,
},
},
]);
Leia de cima para baixo. O $match filtra precocemente para que os estágios posteriores tenham menos trabalho. O $unwind transforma cada elemento do array em seu próprio documento. O $group acumula unidades e receita por SKU. O $sort e o $limit mantêm os cinco primeiros. O $project renomeia _id para sku e descarta o restante.
O $lookup realiza um left outer join com outra coleção, que é a forma de combinar dados referenciados:
db.orders.aggregate([
{ $match: { status: "paid" } },
{
$lookup: {
from: "users",
localField: "customerId",
foreignField: "_id",
as: "customer",
},
},
{ $unwind: "$customer" },
{ $project: { total: 1, "customer.email": 1 } },
]);
Sempre coloque o $match primeiro para que ele possa utilizar um índice, e coloque o $limit o mais cedo possível, desde que a lógica permita. A agregação é poderosa, mas um pipeline que escaneia todos os documentos em cada estágio é a receita para uma query lenta.
Embedding vs referencing
Esta é a decisão central de modelagem. Pergunte-se como os dados são lidos.
Embed (incorpore) quando os dados filhos são lidos junto com o pai, gravados com ele e possuem um tamanho limitado. Os itens de um pedido são o caso clássico: uma única leitura retorna tudo e não há join.
{
_id: ObjectId("..."),
customerId: ObjectId("..."),
items: [
{ sku: "KB-01", qty: 1, price: 8900 },
{ sku: "MS-02", qty: 2, price: 2900 },
],
total: 14700,
}
Reference (referencie) quando os dados são volumosos, compartilhados ou atualizados em seu próprio ciclo. Usuários, produtos e categorias são referenciados por _id, e $lookup ou uma segunda query faz o join entre eles.
{
_id: ObjectId("..."),
customerId: ObjectId("64f1c2a9e13b4a7d8c9e0011"),
items: [{ productId: ObjectId("..."), qty: 1, price: 8900 }],
}
A regra geral é: dados que são acessados juntos devem ser armazenados juntos. Duplique um pouco de dados quando isso transformar a leitura comum em uma única busca, mas lembre-se de que as cópias devem ser atualizadas em todos os lugares onde residem. E nunca incorpore um array que cresça sem limite — os documentos têm um limite máximo de 16 MB.
Transações
Escritas em um único documento são atômicas. Se você precisar alterar vários documentos ou coleções como uma única unidade, utilize uma transação multi-documento. As sessões são o mecanismo para isso.
const session = client.startSession();
try {
await session.withTransaction(async () => {
await accounts.updateOne(
{ _id: from },
{ $inc: { balance: -100 } },
{ session },
);
await accounts.updateOne(
{ _id: to },
{ $inc: { balance: 100 } },
{ session },
);
});
} finally {
await session.endSession();
}
Transações exigem um replica set ou um sharded cluster e possuem um custo operacional: locks, uma janela de execução mais longa e tentativas de reexecução (retries) em caso de conflito. O uso ideal de transações é raro. Se você perceber que está envolvendo cada escrita em uma transação, seu modelo de dados provavelmente precisa de mais embedding.
Replica sets e sharding
Um replica set é um grupo de nós que armazenam os mesmos dados. Um deles é o primário e recebe todas as escritas; os outros replicam o oplog do primário e podem processar leituras. Se o primário falhar, o set elege um novo automaticamente. Esta é a implantação padrão para produção e também habilita change streams e transações.
O sharding particiona uma coleção entre vários replica sets por meio de uma shard key. Cada shard detém um intervalo de valores de chave. Uma boa shard key possui alta cardinalidade, distribui as escritas de forma uniforme e aparece na maioria das queries, permitindo que o roteador direcione a requisição a um único shard. Uma chave ruim cria um hotspot ou força cada query a fazer um fan out para todos os shards.
Escolha a shard key antes de ter os dados, pois alterá-la posteriormente significa migrar a coleção. Para a maioria das aplicações, comece com um replica set e utilize sharding apenas quando um único primário não for mais capaz de suportar a carga.
Mongoose no Node
O driver oficial mongodb é tudo o que você precisa para realizar queries, mas a maioria das equipes de Node utiliza o Mongoose por causa de seus schemas, validação e models. Um schema descreve a estrutura de um documento, e um model é a classe consultável construída a partir dele.
import mongoose from "mongoose";
const orderSchema = new mongoose.Schema({
customerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
status: {
type: String,
enum: ["pending", "paid", "shipped"],
default: "pending",
},
items: [
{
sku: String,
qty: { type: Number, min: 1 },
price: { type: Number, min: 0 },
},
],
total: Number,
createdAt: { type: Date, default: Date.now, index: true },
});
export const Order = mongoose.model("Order", orderSchema);
const paid = await Order.find({ status: "paid" })
.sort({ createdAt: -1 })
.limit(20)
.lean();
O Mongoose realiza a validação ao salvar, faz a conversão de tipos (casting) e oferece populate() para referências. Dois hábitos o mantêm rápido: declare os índices que as queries necessitam e utilize .lean() quando for apenas ler dados, o que evita a construção de documentos completos do Mongoose.
Escritas em lote e upserts
Executar um comando por documento desperdiça round trips. Quando você precisa aplicar diversas alterações, o bulkWrite envia um lote em uma única chamada e reporta exatamente o que aconteceu.
await db.users.bulkWrite([
{
updateOne: {
filter: { email: "[email protected]" },
update: { $set: { plan: "team" } },
upsert: true,
},
},
{
insertOne: {
document: { email: "[email protected]", plan: "pro" },
},
},
{
deleteOne: { filter: { email: "[email protected]" } },
},
]);
A opção upsert é outra ferramenta fundamental. Ela atualiza um documento correspondente ou insere um novo caso nenhum exista, o que facilita a criação de contadores e importações idempotentes.
db.stats.updateOne(
{ day: "2026-09-16" },
{ $inc: { visits: 1 } },
{ upsert: true },
);
Escritas em lote não são transações por padrão. Passe { ordered: false } para continuar a execução após um erro e coletar todas as falhas, ou mantenha o modo ordenado (padrão) quando cada escrita depender da anterior. De qualquer forma, verifique o objeto de resultado: ele contabiliza documentos correspondentes, modificados, inseridos e deletados, e um upsert que não encontrou correspondência não é considerado um erro.
Change streams
Um replica set registra cada escrita em um oplog. Change streams expõem esse log como um feed retomável, permitindo que uma aplicação reaja a inserts, updates e deletes sem a necessidade de polling.
const changeStream = db.orders.watch([
{ $match: { "fullDocument.status": "paid" } },
]);
for await (const change of changeStream) {
console.log(change.operationType, change.fullDocument._id);
}
O feed é retomável: armazene o _id do último evento processado como um resume token e envie-o de volta com resumeAfter após um reinício para que nenhum evento seja perdido. Como os change streams rodam sobre o oplog, eles exigem um replica set e reportam apenas as alterações que ainda não foram removidas do log.
Duas regras garantem a confiabilidade: torne o consumidor idempotente, pois um evento pode ser entregue novamente após a retomada. E mantenha o processamento rápido ou delegue o trabalho para uma fila, já que um consumidor lento permite que o oplog avance além de sua posição.
Melhores práticas
- Modele para a leitura: incorpore o que é lido junto, referencie o que é compartilhado ou ilimitado.
- Indexe os campos pelos quais você filtra e ordena, e siga a regra ESR para índices compostos.
- Projete sempre apenas os campos que o cliente precisa; nunca retorne segredos.
- Coloque
$matchprimeiro em um aggregation pipeline para que ele possa utilizar um índice. - Limite o crescimento de arrays e fique atento ao limite de 16 MB por documento.
- Valide as gravações na aplicação ou com um validador de JSON Schema da coleção.
- Use
explain()antes de assumir que uma query está correta, e teste com dados em tamanho de produção. - Prefira a paginação por keyset em vez de grandes offsets de
skip.
Erros comuns
- Tratar o MongoDB como schemaless e permitir que os documentos assumam formatos incompatíveis.
- Executar queries sem index e questionar por que a latência aumenta conforme a collection cresce.
- Criar compound indexes na ordem errada, impedindo que o sort os utilize.
- Incorporar um array que cresce indefinidamente até que os documentos atinjam o limite de tamanho.
- Usar
$lookupem cada requisição em vez de incorporar dados que são lidos juntos. - Recorrer a multi-document transactions quando um single-document update seria suficiente.
- Retornar documentos inteiros e expor hashes de senhas ou tokens.
- Escolher uma shard key de baixa cardinalidade e criar um write hotspot.
Próximos passos
O MongoDB ensina modelagem de documentos, design de índices e agregação — habilidades que são transferíveis para qualquer armazenamento de dados. Se os seus dados forem profundamente relacionais e exigirem joins e constraints no nível do banco de dados, leia o guia de PostgreSQL. Para leituras em microssegundos, expiração via TTL e contadores, combine o MongoDB com Redis. Se quiser conectá-lo a um serviço Node, revise os fundamentos de Node.js e, em seguida, compare o modelo de consulta com SQL.