Skip to content

🎯 Meta: todo el libro usa PostgreSQL, y con razón — es la elección correcta el 90% de las veces. Este apéndice te da el otro 10%: cuándo un documento-store como MongoDB encaja mejor, cómo modelar datos sin esquema fijo, y las trampas que hacen que la gente "haga NoSQL mal" (spoiler: casi siempre es intentar hacer joins que no deberías necesitar).

Versión: MongoDB 8.0 · Driver oficial mongodb (Node) / pymongo (Python) · Mongoose 9 (ODM opcional para Node).

⚠️ Advertencia de posicionamiento: este apéndice NO dice "usa NoSQL". Dice "entiende cuándo el modelo relacional del cap. 01 dejaría de encajar, y qué hacer si eso te pasa a ti". Para la inmensa mayoría de proyectos de este libro, PostgreSQL + JSONB (ap. H) ya te da lo mejor de los dos mundos sin añadir una segunda base de datos que mantener.


Q.1 · SQL vs NoSQL: la pregunta correcta no es "cuál es mejor"

Es: ¿tus datos tienen relaciones que necesitas consultar de forma consistente, o son documentos que se leen/escriben como unidades independientes?

SQL (PostgreSQL, cap. 01-02)          NoSQL documental (MongoDB)
┌────────────────────────┐           ┌────────────────────────┐
│ Esquema fijo, tipado    │           │ Esquema flexible        │
│ Relaciones con JOINs    │           │ Documentos autocontenidos│
│ Transacciones ACID      │           │ Consistencia por documento│
│ Normalizado (sin duplicar)│         │ Denormalizado (a propósito)│
│ Escala verticalmente     │           │ Escala horizontal nativa │
│ Integridad referencial   │           │ Integridad = tu código  │
└────────────────────────┘           └────────────────────────┘

Casos donde MongoDB (o similar) encaja de verdad:

EscenarioPor qué
Catálogos con esquema muy variableUn producto de "electrónica" tiene specs distintas a uno de "ropa" — forzarlo a columnas fijas genera tablas con 40 columnas nulas
Logs/eventos de alto volumen de escrituraEscrituras masivas sin necesidad de transacciones cruzadas
Contenido tipo CMS (artículos, configuraciones anidadas)El documento completo se lee/escribe como unidad; nunca haces "dame solo el título de todos los artículos de todos los autores con JOIN"
Prototipado rápido con el modelo aún moviéndoseCambiar de esquema no exige migraciones (aunque esto se paga después)

Casos donde SQL sigue ganando (la mayoría del libro):

  • Dinero, inventario, pedidos → necesitas transacciones ACID reales (cap. 01, ap. P).
  • Datos muy relacionales (usuarios↔pedidos↔productos↔pagos) → los JOINs son tu amigo, no tu enemigo.
  • Necesitas reportes/analítica con agregaciones complejas → SQL + window functions (cap. 02) es más maduro y expresivo.

🧠 La trampa nº1: migrar a MongoDB "porque escala mejor" cuando el problema real es no tener índices en PostgreSQL. Un Postgres bien indexado aguanta órdenes de magnitud más carga de la que la mayoría de proyectos verá jamás (cap. 01, ap. H). No cambies de paradigma para resolver un problema de configuración.


Q.2 · El modelo mental: documentos, no filas

Un documento MongoDB es JSON (técnicamente BSON — JSON binario con más tipos: fechas, ObjectId, etc.). Se agrupan en colecciones (el equivalente a una tabla, pero sin esquema obligatorio):

javascript
// Un documento de "producto" — anida lo que en SQL serían tablas relacionadas
{
  _id: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
  nombre: "Teclado mecánico RGB",
  precio: 89.90,
  categoria: "perifericos",
  specs: {                              // anidado — en SQL sería otra tabla o columnas JSONB
    switches: "Cherry MX Red",
    retroiluminacion: "RGB por tecla",
    conexion: ["USB-C", "Bluetooth"]
  },
  etiquetas: ["gaming", "rgb", "mecanico"],
  creadoEn: ISODate("2026-01-15T10:00:00Z")
}

💡 Si esto te suena a JSONB de PostgreSQL (ap. H.3), no es casualidad. MongoDB es "JSONB como base de datos completa", con índices, agregaciones y replicación nativos para ese modelo. La pregunta real muchas veces es: JSONB en PostgreSQL, o Mongo aparte? — y para la mayoría de proyectos, JSONB gana porque no añade una segunda base de datos.


Q.3 · Setup y CRUD básico

bash
npm install mongodb
docker run -d -p 27017:27017 --name mongo mongo:8.0    # local, como en el cap. 12
typescript
// db.ts
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URL!);
await client.connect();
export const db = client.db('tienda');
export const productos = db.collection('productos');
typescript
// Crear
const { insertedId } = await productos.insertOne({
  nombre: 'Teclado mecánico',
  precio: 89.9,
  categoria: 'perifericos',
  creadoEn: new Date(),
});

// Leer — filtros como objetos, no strings SQL
const producto = await productos.findOne({ _id: new ObjectId(id) });
const baratos = await productos.find({ precio: { $lt: 50 } }).toArray();
const porCategoria = await productos
  .find({ categoria: 'perifericos' })
  .sort({ precio: -1 })
  .limit(10)
  .toArray();

// Actualizar — operadores explícitos, nunca "sobreescribir sin querer"
await productos.updateOne(
  { _id: new ObjectId(id) },
  { $set: { precio: 79.9 }, $push: { etiquetas: 'oferta' } },
);

// Borrar
await productos.deleteOne({ _id: new ObjectId(id) });
$eq $ne $gt $gte $lt $lte $in $nin     ← comparación
$and $or $not                          ← lógicos
$set $unset $inc $push $pull           ← actualización

⚠️ _id no es un número autoincremental — es un ObjectId (12 bytes, generado distribuidamente: timestamp + máquina + contador). Sirve para escalar sin coordinar generadores de id entre nodos, a diferencia del SERIAL de PostgreSQL.


Q.4 · Modelado: la decisión que define todo (embed vs referencia)

Esta es LA decisión de diseño en MongoDB — no hay JOIN, así que decides por adelantado si los datos relacionados viven dentro del documento o aparte, referenciados:

javascript
// EMBED: el comentario vive DENTRO del artículo — una sola lectura trae todo
{
  _id: ObjectId("..."),
  titulo: "Cómo funciona MongoDB",
  comentarios: [
    { autor: "Ana", texto: "Genial", fecha: ISODate("...") },
    { autor: "Luis", texto: "Muy claro", fecha: ISODate("...") },
  ]
}

// REFERENCIA: el pedido solo GUARDA el id del usuario — como una FK, pero sin integridad forzada
{
  _id: ObjectId("..."),
  usuarioId: ObjectId("65f1..."),        // referencia, no embed
  items: [{ productoId: ObjectId("..."), cantidad: 2 }],
  total: 179.8
}
Usa embed cuando…Usa referencia cuando…
Los datos se leen SIEMPRE juntosLos datos se consultan por separado a menudo
La relación es "contiene" (artículo→comentarios)La relación es "muchos a muchos" o crece sin límite
El sub-documento no supera ~16 MB (límite duro)El "lado uno" se actualiza mucho y no quieres reescribir todo lo que lo referencia
No necesitas consultar el sub-documento de forma aisladaNecesitas paginar/filtrar la colección referenciada independientemente
javascript
// Traer datos referenciados: $lookup (el "JOIN" de Mongo — úsalo con moderación)
db.pedidos.aggregate([
  { $match: { _id: pedidoId } },
  { $lookup: {
      from: 'usuarios',
      localField: 'usuarioId',
      foreignField: '_id',
      as: 'usuario',
  }},
  { $unwind: '$usuario' },
]);

🧠 Si te encuentras haciendo $lookup de 3 colecciones en cada query, esa es la señal de que tus datos son relacionales de verdad y PostgreSQL era la herramienta correcta desde el principio. No es un fracaso de modelado — es información: MongoDB no "hace joins mal", es que estás forzando un problema relacional en una herramienta documental.

Denormalización deliberada (el patrón MongoDB por excelencia): duplicas datos que casi nunca cambian para evitar el $lookup en el camino caliente:

javascript
// En vez de referenciar SOLO el id del autor, guardas también lo que casi nunca cambia
{
  titulo: "Cómo funciona MongoDB",
  autor: { id: ObjectId("..."), nombre: "Ana García" },   // duplicado a propósito
  // Si Ana cambia su nombre, actualizas TODOS los artículos (trade-off consciente)
}

Q.5 · Índices (exactamente tan importantes que en SQL)

javascript
// Índice simple — igual de crítico que en PostgreSQL (cap. 01)
db.productos.createIndex({ categoria: 1 });

// Índice compuesto — el orden importa (igual que en SQL)
db.pedidos.createIndex({ usuarioId: 1, creadoEn: -1 });

// Índice de texto — búsqueda full-text nativa
db.productos.createIndex({ nombre: 'text', descripcion: 'text' });
db.productos.find({ $text: { $search: 'teclado mecanico' } });

// Único — como UNIQUE en SQL
db.usuarios.createIndex({ email: 1 }, { unique: true });
javascript
db.productos.find({ categoria: 'perifericos' }).explain('executionStats');
// mira "COLLSCAN" (escaneo completo, malo) vs "IXSCAN" (usa índice, bien)
// — el mismo diagnóstico que EXPLAIN ANALYZE en PostgreSQL (cap. 01.9)

Q.6 · Aggregation pipeline: el "SQL" de Mongo

Para consultas más allá de find() (agrupar, sumar, unir, paginar en un solo viaje), MongoDB usa el aggregation pipeline: una secuencia de etapas, cada una transformando la salida de la anterior — muy parecido a encadenar WHEREGROUP BYHAVING en SQL (cap. 02), pero como un array de pasos explícitos:

javascript
// Equivalente a: SELECT categoria, COUNT(*), AVG(precio) FROM productos
//                WHERE precio > 20 GROUP BY categoria HAVING COUNT(*) > 1
db.productos.aggregate([
  { $match: { precio: { $gt: 20 } } },              // ~ WHERE (filtra primero, barato)
  { $group: {
      _id: '$categoria',                            // ~ GROUP BY
      total: { $sum: 1 },
      precioMedio: { $avg: '$precio' },
  }},
  { $match: { total: { $gt: 1 } } },                 // ~ HAVING (filtra sobre lo agrupado)
  { $sort: { precioMedio: -1 } },
]);
javascript
// $facet: varias agregaciones EN PARALELO sobre el mismo conjunto — el patrón
// para pintar una página de resultados + sus contadores de filtro en UNA sola consulta
db.productos.aggregate([
  { $match: { categoria: 'perifericos' } },
  { $facet: {
      resultados: [{ $skip: 0 }, { $limit: 20 }],           // la página actual
      totalPorMarca: [{ $group: { _id: '$marca', n: { $sum: 1 } } }],  // para las facetas del sidebar
      totalGeneral: [{ $count: 'total' }],
  }},
]);

🧠 Piensa en el pipeline como una tubería de transformaciones, no como una query declarativa: cada etapa recibe los documentos que salieron de la anterior. $match temprano (antes de $group/$sort) es la optimización nº1 — reduce cuántos documentos procesan las etapas caras, igual que un buen índice reduce lo que escanea un WHERE en SQL (cap. 01.9).

⚠️ $match se beneficia de índices (Q.5) solo si es la PRIMERA etapa (o casi). Un $match después de un $group ya no puede usar ningún índice — opera sobre datos calculados en memoria. Ordena tus etapas pensando en esto, igual que ordenas condiciones en una query SQL.

Validación de esquema con $jsonSchema — la excepción a "Mongo no valida en la BD" (Q.8):

javascript
db.createCollection('productos', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['nombre', 'precio'],
      properties: {
        nombre: { bsonType: 'string' },
        precio: { bsonType: 'number', minimum: 0 },       // ⚠️ bsonType, no "type" de JSON Schema puro
        categoria: { enum: ['perifericos', 'ropa', 'libros'] },
      },
    },
  },
  validationLevel: 'strict',      // rechaza inserts/updates que no cumplan
});

💡 $jsonSchema existe pero se usa poco en la práctica por una razón concreta: si tu equipo ya tiene disciplina para validar en la capa de aplicación (Zod/Pydantic, Q.8), añadir validación en la BD es una segunda fuente de verdad que mantener sincronizada. Tiene sentido cuando varios servicios distintos escriben en la misma colección y no puedes garantizar que todos validen igual — la BD se convierte en la última línea de defensa compartida.


Q.7 · Transacciones (existen, con matices)

Desde MongoDB 4.0, hay transacciones multi-documento — pero úsalas como excepción, no como costumbre:

typescript
const session = client.startSession();
try {
  await session.withTransaction(async () => {
    await cuentas.updateOne({ _id: origenId }, { $inc: { saldo: -100 } }, { session });
    await cuentas.updateOne({ _id: destinoId }, { $inc: { saldo: 100 } }, { session });
  });
} finally {
  await session.endSession();
}

⚠️ Si tu dominio necesita transacciones multi-documento constantemente, esa es otra señal de que el modelo relacional (con transacciones ACID nativas y gratis, cap. 01) es la herramienta correcta. MongoDB las soporta para el caso ocasional; diseñarlo todo alrededor de transacciones frecuentes es nadar contracorriente del modelo documental.


Q.8 · Con tu framework (repaso rápido de integración)

typescript
// NestJS + Mongoose (ODM: valida esquema en la capa de app, ya que Mongo no lo hace en la BD)
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';

@Schema({ timestamps: true })
export class Producto {
  @Prop({ required: true }) nombre: string;
  @Prop({ required: true, min: 0 }) precio: number;
  @Prop({ type: [String] }) etiquetas: string[];
}
export const ProductoSchema = SchemaFactory.createForClass(Producto);
python
# FastAPI + Motor (driver async) — sin ODM, con Pydantic como capa de validación
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel

client = AsyncIOMotorClient(MONGODB_URL)
db = client.tienda

class Producto(BaseModel):
    nombre: str
    precio: float

@app.post("/productos")
async def crear(p: Producto):
    resultado = await db.productos.insert_one(p.model_dump())
    return {"id": str(resultado.inserted_id)}

🧠 Mongo no valida esquema en la base de datos (salvo $jsonSchema, poco usado en la práctica). Toda la validación que en PostgreSQL te da gratis (NOT NULL, CHECK, tipos) aquí es responsabilidad de tu capa de aplicación — Zod, Pydantic, class-validator (caps. 03-08). Es el mismo patrón "valida en el borde", pero aquí NO hay red de seguridad extra en la BD.


Q.9 · Patrón híbrido: la respuesta más común en 2026

En proyectos reales, la elección casi nunca es "todo SQL" o "todo NoSQL": es PostgreSQL como base + Mongo (o JSONB) solo donde el modelo documental encaja:

┌─────────────────────────────────────────────────────────┐
│ PostgreSQL: usuarios, pedidos, pagos, inventario          │
│   → todo lo que necesita transacciones e integridad       │
├─────────────────────────────────────────────────────────┤
│ MongoDB (opcional): catálogo con specs muy variables,     │
│   logs de eventos de analítica, contenido tipo CMS        │
└─────────────────────────────────────────────────────────┘

Pero antes de añadir una segunda base de datos (con su propio backup, monitorización, cliente en tu código — cap. 12, ap. F), pregúntate: ¿JSONB en PostgreSQL (ap. H.3) no me da ya el 90% de lo que necesito, sin el coste operativo de un segundo sistema? Casi siempre, sí.


Q.10 · Buenas prácticas

  1. PostgreSQL por defecto. Sal de SQL solo cuando el problema (esquema muy variable, volumen de escritura extremo) lo justifique de verdad, no por moda.
  2. Prueba JSONB antes que una segunda base de datos. Apéndice H.3: menos infraestructura, mismo modelo flexible, transacciones ACID gratis.
  3. Decide embed vs referencia por patrón de acceso, no por "parece más ordenado".
  4. Índices desde el día 1 — Mongo sin índices es tan lento como Postgres sin índices.
  5. Valida en tu capa de aplicación (Zod/Pydantic) — la BD no te va a salvar aquí.
  6. $lookup frecuente = señal de alarma, no una feature a usar con normalidad.
  7. Si necesitas transacciones multi-documento a menudo, replantea el modelo relacional.

✅ Ejercicio del apéndice

1. Levanta MongoDB con Docker (cap. 12) y modela un catálogo de productos con
   specs muy variables por categoría (electrónica vs ropa vs libros).
2. Decide embed vs referencia para: comentarios de producto (embed) y pedidos→usuario
   (referencia). Justifica cada decisión por patrón de acceso.
3. Índices: uno simple (categoria), uno compuesto (usuarioId + fecha) y uno de texto
   (nombre + descripción). Verifica con explain() que se usan (IXSCAN, no COLLSCAN).
4. Aggregation pipeline: total y precio medio por categoría con $match + $group,
   y una consulta con $facet que devuelva la página de resultados y los contadores
   por marca en una sola llamada (el patrón de facetas del ap. T.2).
5. Añade validación $jsonSchema a tu colección de productos y demuestra que
   rechaza un insert con precio negativo.
6. Implementa el mismo catálogo con JSONB en PostgreSQL (ap. H.3) y compara:
   líneas de código, complejidad de las queries, y qué se siente más natural.
7. Escribe 200 líneas justificando POR ESCRITO cuál usarías para la Cantina (cap. 22)
   y por qué — el ejercicio real es razonar la decisión, no solo escribir código.

🧠 Autoevaluación

No "cuál escala mejor" sino "¿mis datos son relacionales con necesidad de consistencia transversal, o documentos que se leen/escriben como unidades independientes?". La mayoría de dominios de negocio (pedidos, pagos, inventario) son relacionales de verdad.

Embed cuando los datos se leen siempre juntos y no crecen sin límite (comentarios de un artículo). Referencia cuando se consultan por separado, la relación es muchos-a-muchos, o el "lado uno" cambia con frecuencia y no quieres reescribir todo lo que lo embebe.

Que el dominio es relacional de verdad y probablemente PostgreSQL era la herramienta correcta. $lookup ocasional está bien; como patrón constante es forzar un problema relacional en un modelo documental.

PostgreSQL rechaza datos inválidos en la propia base de datos (NOT NULL, CHECK, tipos). MongoDB no impone esquema por defecto: sin validación en la capa de aplicación (Zod/Pydantic), cualquier documento malformado se guarda tal cual.

Un $match como primera etapa puede aprovechar los índices existentes (Q.5) y descarta documentos irrelevantes antes de que las etapas más costosas ($group, $sort) tengan que procesarlos. Un $match colocado después de un $group ya opera sobre datos agregados en memoria, sin poder usar ningún índice — el mismo principio que filtrar temprano en una query SQL.


Volver al: README.md · Relacionado: 01-bases-de-datos-sql.md, H-patrones-datos.md, 11-arquitectura.md