Document Database

MongoDB

MongoDB es una base de datos de documentos: documentos BSON flexibles que residen en colecciones, se consultan con un lenguaje similar a JavaScript y escalan horizontalmente mediante replica sets y sharding.

intermediate15 min readUpdated 16 sept 2026
mongosh
js
// mongosh
use shop

db.orders.insertOne({
  customerId: ObjectId("64f1c2a9e13b4a7d8c9e0011"),
  status: "paid",
  items: [
    { sku: "KB-01", name: "Keyboard", qty: 1, price: 8900 },
    { sku: "MS-02", name: "Mouse", qty: 2, price: 2900 },
  ],
  total: 14700,
  createdAt: new Date(),
})

db.orders
  .find({ status: "paid", total: { $gte: 10000 } })
  .sort({ createdAt: -1 })
  .limit(10)
Lanzamiento
2009
Modelo de datos
Document / BSON
Lenguaje de consulta
MQL
Escrito en
C++
Motor de almacenamiento
WiredTiger
Licencia
SSPL
Versión
8.x

Por que importa

Por qué los equipos eligen MongoDB

Los documentos mapean a objetos

Un registro es un documento BSON que refleja el objeto que tu aplicación ya utiliza, por lo que las lecturas rara vez necesitan joins o mapeos de fila a objeto.

Un lenguaje de consulta legible

Los filtros son documentos similares a JSON con operadores como $gt y $in, lo que hace que las consultas sean componibles y fáciles de construir desde el código.

Escalabilidad horizontal cuando sea necesario

Los replica sets brindan alta disponibilidad desde el primer día, y el sharding distribuye una colección entre varios nodos cuando un único primario ya no es suficiente.

La imagen completa

Las tres ideas detrás de MongoDB

Un documento almacena un objeto completo, una colección agrupa documentos y un índice o pipeline los convierte en respuestas.

Documentos

Almacenar

Los datos viven en documentos BSON con campos tipados, objetos anidados y arrays. El esquema es impuesto por tu código, no por el servidor.

Colecciones e índices

Organizar

Los documentos relacionados residen en una colección. Los índices en los campos por los que filtras y ordenas convierten los escaneos completos en búsquedas dirigidas.

Driver & Mongoose

Acceder

El driver oficial de Node.js expone los mismos comandos que ejecutas en mongosh, mientras que Mongoose añade esquemas, validación y modelos.

HTML5 de un vistazo

La caja de herramientas de MongoDB

Colecciones

db.orders es una colección de documentos, creada en la primera inserción.

find()

Pasa un documento de filtro y obtén un cursor de los documentos que coincidan.

Agregación

Un pipeline de etapas $match, $group y $lookup da forma a los resultados.

Índices

Los índices simples, compuestos, únicos, TTL y de texto comparten una misma API.

Transacciones

Transacciones ACID multi-documento para cuando una sola escritura no es suficiente.

Replica sets

Un primario y varios secundarios replican las escrituras y eligen un nuevo primario.

Modelo de datos

Cómo se estructura un documento

Un pedido, sus artículos y su estado, todo en un único documento.

Un documento de pedidoMongoDB document
  • _idObjectIdClave primaria, generada por el driver
  • customerIdObjectIdReferencia a un documento en users
  • statusstringpending, paid o shipped
  • itemsarray<object>Artículos embebidos: sku, name, qty, price
  • totalnumberTotal derivado en centavos
  • createdAtDateIndexado para consultas de pedidos recientes

Un documento en la colección orders, con sus artículos embebidos y su estado indexado.

La guia completa

MongoDB: Todo lo que necesitas saber

¿Qué es MongoDB?

MongoDB es una base de datos de documentos. En lugar de filas en tablas, almacena documentos —objetos similares a JSON codificados como BSON— dentro de colecciones. Un documento puede contener objetos y arrays anidados, por lo que un solo registro puede describir un agregado completo: un pedido con sus artículos, o un usuario con sus direcciones.

Surgió en 2009, cuando el modelo relacional se sentía pesado para aplicaciones que crecían rápido y cambiaban de forma. Su promesa era sencilla: almacenar el objeto que tu código ya posee y escalar horizontalmente sin migraciones dolorosas. Esa promesa lo convirtió en la base de datos NoSQL predeterminada para una generación de equipos de Node.js y JavaScript.

El compromiso es real. MongoDB renuncia a los joins por defecto y traslada la validación del esquema a tu aplicación. Cuando tus datos tienen forma de documento, esto es una ventaja. Cuando son profundamente relacionales, una base de datos relacional suele ser la mejor herramienta.

Documentos, colecciones y BSON

Un documento es un conjunto ordenado de pares campo-valor. Una colección es un grupo de documentos que generalmente comparten una estructura, aunque nada los obliga a ello. Las colecciones se crean en el momento en que realizas la primera inserción en ellas.

db.users.insertOne({
  email: "[email protected]",
  name: "Ada",
  address: { city: "London", country: "GB" },
  tags: ["admin", "beta"],
});

Los valores tienen tipos BSON, no solo tipos JSON. BSON es una codificación binaria que añade ObjectId, Date, Decimal128, BinData y más, permitiéndote almacenar fechas reales y decimales precisos en lugar de strings. El orden de los campos se preserva y los nombres de los campos distinguen entre mayúsculas y minúsculas.

Un hábito útil es darle a cada documento de una colección una estructura consistente, a pesar de que el servidor permita variaciones. De este modo, obtendrás consultas e índices predecibles, manteniendo la flexibilidad para cuando necesites añadir un campo a un solo documento primero.

_id y ObjectId

Cada documento necesita un _id único. Si no proporcionas uno, el driver genera un ObjectId: un valor de 12 bytes compuesto por una marca de tiempo (timestamp), un valor aleatorio por proceso y un contador incremental. Debido a que la marca de tiempo va primero, los valores de ObjectId se ordenan aproximadamente por tiempo de creación, lo cual es conveniente para la paginación.

const { ObjectId } = require("mongodb");

const id = new ObjectId("64f1c2a9e13b4a7d8c9e0011");
id.getTimestamp(); // 2023-09-01T...

El campo _id se indexa automáticamente y siempre es único. Puedes proporcionar tu propio valor —una clave natural, una cadena UUID o una clave compuesta— cuando esto haga que las búsquedas sean más eficientes o cuando necesites inserciones idempotentes.

CRUD en la práctica

Create, read, update y delete se mapean a un conjunto reducido de métodos que se comportan de la misma manera en mongosh y en el driver de 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 y updateMany reciben un filtro y un documento de actualización. El documento de actualización utiliza operadores: $set reemplaza o añade campos, $unset los elimina, $inc suma a un número, $push añade elementos al final de un array, $addToSet añade solo si el campo no existe, y $pull elimina elementos coincidentes de un array.

db.users.updateOne(
  { email: "[email protected]" },
  {
    $set: { lastSeenAt: new Date() },
    $inc: { logins: 1 },
    $addToSet: { tags: "beta" },
  },
);

También existe replaceOne, que reemplaza el documento completo excepto _id. Es preferible usar operadores de campo para evitar que escrituras concurrentes sobrescriban los cambios de otros usuarios.

Consultas con operadores

Un filtro es, en sí mismo, un documento. La igualdad es el comportamiento por defecto y los operadores comienzan con $.

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 es importante cuando varias condiciones deben aplicarse al mismo elemento de un array. Sin ello, { "items.qty": { $gte: 2 }, "items.price": { $lt: 3000 } } podría coincidir con elementos diferentes.

Combina filtros con $and, $or y $not:

db.orders.find({
  $or: [
    { status: "paid" },
    { status: "pending", total: { $lt: 1000 } },
  ],
});

El lenguaje de consultas es componible porque es data, no una cadena de texto. Es por eso que construir filtros en el código de la aplicación se siente natural: ensamblas un objeto y lo pasas a find.

Proyección, ordenación y paginación

Una proyección selecciona qué campos devolver. Incluye campos con 1 o exclúyelos con 0, pero nunca mezcles ambos estilos excepto para _id.

db.users.find(
  { plan: "pro" },
  { email: 1, name: 1, _id: 0 },
);

Ordena y pagina con .sort(), .skip() y .limit(). Para offsets grandes, skip se vuelve más lento porque el servidor sigue recorriendo los documentos omitidos. Es preferible usar la paginación por keyset, donde filtras basándote en el último valor obtenido.

db.orders
  .find({ customerId, createdAt: { $lt: lastSeen } })
  .sort({ createdAt: -1 })
  .limit(20);

Los índices en los campos de ordenación hacen que tanto el filtro como el orden sean eficientes, que es precisamente donde entra la siguiente sección.

Índices: la diferencia entre lo rápido y lo inutilizable

Sin un índice, MongoDB lee cada documento de la colección: un collection scan. Con uno, busca directamente el rango coincidente. Casi cualquier problema de rendimiento en MongoDB se debe a un índice faltante o 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" });
  • Los índices de campo único (single-field) aceleran las consultas de igualdad y de rango en un solo campo.
  • Los índices compuestos (compound) siguen la regla ESR: primero los campos de igualdad, luego el ordenamiento (sort) y finalmente el rango. { customerId: 1, createdAt: -1 } sirve tanto para el filtro como para el ordenamiento.
  • Los índices únicos (unique) rechazan duplicados y hacen que los upserts sean seguros.
  • Los índices TTL eliminan documentos después de una fecha específica, lo cual es perfecto para sesiones y tokens de un solo uso.
  • Los índices de texto (text) permiten la búsqueda $text con stemming y scoring.

Usa explain() para ver qué hizo el planificador. Busca IXSCAN en lugar de COLLSCAN, y verifica que el número de documentos examinados sea cercano al número de documentos devueltos.

db.orders.find({ customerId, status: "paid" }).explain("executionStats");

El pipeline de agregación

Cuando una consulta no es suficiente, el framework de agregación ejecuta un pipeline de etapas. Cada etapa transforma un flujo de documentos y lo pasa a la siguiente. Las etapas más comunes son $match, $group, $sort, $project, $lookup, $unwind y $limit.

Aquí tienes un ejemplo práctico: los cinco productos con mayores ingresos en pedidos pagados.

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,
    },
  },
]);

Léelo de arriba abajo. $match filtra los datos al principio para que las etapas posteriores tengan menos trabajo. $unwind convierte cada elemento del array en su propio documento. $group acumula las unidades y los ingresos por SKU. $sort y $limit mantienen los cinco primeros. $project renombra _id a sku y elimina el resto.

$lookup realiza un left outer join con otra colección, que es la forma de combinar datos referenciados:

db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $lookup: {
      from: "users",
      localField: "customerId",
      foreignField: "_id",
      as: "customer",
    },
  },
  { $unwind: "$customer" },
  { $project: { total: 1, "customer.email": 1 } },
]);

Coloca siempre $match primero para que pueda utilizar un índice, y sitúa $limit lo más pronto posible según lo permita la lógica del proceso. La agregación es potente, pero un pipeline que escanea cada documento en cada etapa es una receta para tener consultas lentas.

Embeber vs referenciar

Esta es la decisión central de modelado. Pregúntate cómo se leen los datos.

Embebe (Embed) cuando los datos hijos se lean junto con el padre, se escriban con él y tengan un tamaño limitado. Los artículos de un pedido son el caso clásico: una sola lectura devuelve todo y no hay join.

{
  _id: ObjectId("..."),
  customerId: ObjectId("..."),
  items: [
    { sku: "KB-01", qty: 1, price: 8900 },
    { sku: "MS-02", qty: 2, price: 2900 },
  ],
  total: 14700,
}

Referencia cuando los datos son voluminosos, compartidos o se actualizan de forma independiente. Los usuarios, productos y categorías son referenciados por _id, y $lookup o una segunda consulta realiza el join.

{
  _id: ObjectId("..."),
  customerId: ObjectId("64f1c2a9e13b4a7d8c9e0011"),
  items: [{ productId: ObjectId("..."), qty: 1, price: 8900 }],
}

La regla de oro es que los datos que se acceden juntos deben almacenarse juntos. Duplica un poco de información cuando eso convierta la lectura más común en una sola búsqueda, pero recuerda que las copias deben actualizarse en cada lugar donde residan. Y nunca embebas un array que crezca sin límite: los documentos tienen un tope de 16 MB.

Transacciones

Las escrituras de un solo documento son atómicas. Si necesitas modificar varios documentos o colecciones como una sola unidad, utiliza una transacción multi-documento. Las sesiones son el mecanismo para lograrlo.

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();
}

Las transacciones requieren un replica set o un sharded cluster, y conllevan una carga adicional: bloqueos, una ventana de tiempo más larga y reintentos en caso de conflicto. El mejor uso de las transacciones es aquel que es poco frecuente. Si te encuentras envolviendo cada escritura en una transacción, es probable que tu modelo de datos necesite más embedding.

Replica sets y sharding

Un replica set es un grupo de nodos que contienen los mismos datos. Uno de ellos es el primario y gestiona todas las escrituras; los demás replican el oplog del primario y pueden atender lecturas. Si el primario falla, el set elige uno nuevo automáticamente. Este es el despliegue predeterminado para producción y también permite el uso de change streams y transacciones.

El sharding particiona una colección a través de múltiples replica sets mediante una shard key. Cada shard es dueño de un rango de valores de la clave. Una buena shard key tiene una alta cardinalidad, distribuye las escrituras de manera uniforme y aparece en la mayoría de las consultas para que el router pueda dirigirse a un único shard. Una clave deficiente crea un hotspot o fuerza a que cada consulta se distribuya (fan out) a todos los shards.

Elige la shard key antes de tener datos, ya que cambiarla más tarde implica migrar la colección. Para la mayoría de las aplicaciones, comienza con un replica set y recurre al sharding solo cuando un único primario ya no sea capaz de soportar la carga.

Mongoose en Node

El driver oficial de mongodb es todo lo que necesitas para realizar consultas, pero la mayoría de los equipos de Node utilizan Mongoose por sus esquemas, validación y modelos. Un esquema describe la estructura de un documento, y un modelo es la clase consultable construida a partir de él.

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();

Mongoose valida al guardar, realiza la conversión de tipos (casting) y te ofrece populate() para las referencias. Dos hábitos ayudan a mantenerlo rápido: declarar los índices que requieren las consultas y llamar a .lean() cuando solo necesites leer datos, lo que evita la creación de documentos completos de Mongoose.

Escrituras masivas y upserts

Ejecutar un comando por documento desperdicia viajes de ida y vuelta (round trips). Cuando necesitas aplicar muchos cambios, bulkWrite envía un lote en una sola llamada e informa exactamente qué sucedió.

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]" } },
  },
]);

La opción upsert es otra herramienta fundamental. Actualiza un documento que coincida o inserta uno si no existe, lo que facilita la creación de contadores e importaciones idempotentes.

db.stats.updateOne(
  { day: "2026-09-16" },
  { $inc: { visits: 1 } },
  { upsert: true },
);

Las escrituras masivas no son transacciones por defecto. Pasa { ordered: false } para continuar después de un error y recopilar cada fallo, o mantén el modo ordenado predeterminado cuando cada escritura dependa de la anterior. En cualquier caso, revisa el objeto de resultado: este contabiliza los documentos coincidentes, modificados, insertados y eliminados, y un upsert que no encontró ninguna coincidencia no se considera un error.

Change streams

Un replica set registra cada escritura en un oplog. Los change streams exponen ese log como un feed reanudable, permitiendo que una aplicación reaccione a inserciones, actualizaciones y eliminaciones sin necesidad de hacer polling.

const changeStream = db.orders.watch([
  { $match: { "fullDocument.status": "paid" } },
]);

for await (const change of changeStream) {
  console.log(change.operationType, change.fullDocument._id);
}

El feed es reanudable: guarda el _id del último evento procesado como un resume token y envíalo de vuelta con resumeAfter después de un reinicio para que no se pierda ningún evento. Debido a que los change streams se ejecutan sobre el oplog, requieren un replica set y solo informan sobre los cambios que aún no han sido eliminados del log.

Dos reglas garantizan su fiabilidad. Haz que el consumidor sea idempotente, ya que un evento puede entregarse nuevamente después de una reanudación. Y mantén el procesamiento rápido o delega el trabajo a una cola, ya que un consumidor lento permite que el oplog avance más allá de su posición.

Mejores prácticas

  • Modela para la lectura: embebe lo que se lee en conjunto, referencia lo que es compartido o no tiene límites.
  • Indexa los campos por los que filtres y ordenes, y sigue la regla ESR para los índices compuestos.
  • Proyecta siempre solo los campos que el cliente necesita; nunca devuelvas secretos.
  • Coloca $match primero en un pipeline de agregación para que pueda utilizar un índice.
  • Limita el crecimiento de los arrays y vigila el límite de 16 MB por documento.
  • Valida las escrituras en la aplicación o mediante un validador JSON Schema de la colección.
  • Usa explain() antes de asumir que una consulta es correcta, y realiza pruebas con datos de tamaño real de producción.
  • Prefiere la paginación por keyset sobre offsets skip grandes.

Errores comunes

  • Tratar MongoDB como si no tuviera esquema y permitir que los documentos evolucionen hacia formas incompatibles.
  • Ejecutar consultas sin índices y preguntarse por qué la latencia aumenta a medida que crece la colección.
  • Crear índices compuestos en el orden incorrecto, impidiendo que el ordenamiento (sort) pueda utilizarlos.
  • Embeber un array que crece indefinidamente hasta que los documentos alcanzan el límite de tamaño.
  • Usar $lookup en cada solicitud en lugar de embeber los datos que se leen juntos.
  • Recurrir a transacciones multi-documento cuando una actualización de un solo documento sería suficiente.
  • Retornar documentos completos y filtrar hashes de contraseñas o tokens.
  • Elegir una shard key de baja cardinalidad y crear un hotspot de escritura.

Próximos pasos

MongoDB enseña modelado de documentos, diseño de índices y agregaciones; habilidades que son transferibles a cualquier almacén de datos. Si tus datos son profundamente relacionales y requieren joins y restricciones a nivel de base de datos, lee la guía de PostgreSQL. Para lecturas de microsegundos, expiración basada en TTL y contadores, combina MongoDB con Redis. Si quieres conectarlo a un servicio de Node, repasa los conceptos básicos de Node.js y luego compara el modelo de consultas con SQL.

En la practica

Cuatro comandos que cubren la mayor parte del trabajo

Las mismas estructuras aparecen en mongosh y en el driver de Node.js: insert, update, aggregate e index.

mongosh
db.users.insertMany([
  { email: "[email protected]", name: "Ada", plan: "pro" },
  { email: "[email protected]", name: "Linus", plan: "free" },
]);

db.users.find(
  { plan: "pro" },
  { email: 1, name: 1, _id: 0 },
);

Proyectar solo lo que necesitas

Una proyección reduce los bytes que viajan por la red y mantiene los campos sensibles fuera de las respuestas.

Preferir
db.users.find(
  { plan: "pro" },
  { email: 1, name: 1, _id: 0 },
);
Evitar
db.users.find({ plan: "pro" });
// every field crosses the wire,
// including passwordHash and tokens

Mantener los arrays acotados

Embebe los datos que se lean juntos, pero nunca permitas que un solo documento crezca sin límite. Los documentos tienen un techo de 16 MB.

Preferir
// Line items are read with the order.
db.orders.insertOne({
  customerId,
  items: [{ sku: "KB-01", qty: 1, price: 8900 }],
});
Evitar
// This array grows forever.
db.users.updateOne(
  { _id },
  { $push: { events: event } },
);

Compromisos

¿Es MongoDB el almacenamiento adecuado para esto?

MongoDB es excelente para lecturas con forma de documento y escalado horizontal. Sus costos aparecen cuando los datos son profundamente relacionales o necesitan garantías fuertes entre documentos.

Strengths

  • El objeto es el registro

    Almacenar un documento que ya coincide con la forma de tu aplicación elimina la capa de mapeo, y muchas lecturas se convierten en una sola búsqueda.

  • Flexibilidad de esquema

    Los documentos en una colección pueden tener campos diferentes, lo que se adapta a productos en evolución y datos heterogéneos.

  • Escalado horizontal integrado

    Los replica sets y el sharding son ciudadanos de primera clase, por lo que el mismo modelo de datos puede crecer desde un solo nodo hasta un clúster.

Trade-offs

  • Las relaciones se vuelven más difíciles

    Sin joins por defecto, modelar datos muchos-a-muchos implica duplicación o $lookup, y mantener las copias consistentes se convierte en tu responsabilidad.

  • El esquema en la escritura sigue siendo un esquema

    El servidor no detendrá un documento mal formado. La validación debe residir en tu código o en un validador de JSON Schema.

  • Las transacciones cuestan más

    Las transacciones multi-documento funcionan, pero son más lentas y complejas que una actualización de un solo documento. Diseña para que rara vez las necesites.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender MongoDB?

Nuestro tutorial interactivo te guia a traves de MongoDB paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.