Cerramos el módulo 2 con biblioredb en muy buena forma: siete tablas, claves ajenas que impiden los huérfanos, informes de gestión que cuadran. Ese es el núcleo del sistema y va a seguir siéndolo. Pero en cuanto la dirección de BiblioRed aprobó el nuevo portal para los socios, aparecieron cuatro necesidades que el esquema relacional no encaja con comodidad: los lectores quieren publicar reseñas, el catálogo quiere mostrar portadas, sinopsis y etiquetas de materiales que ya no son solo libros, el equipo quiere registrar cada consulta que se hace en el buscador, y marketing quiere recomendar títulos a partir de lo que han leído lectores parecidos.

Ya en la lección 01-02 anticipamos la decisión: esa parte del sistema iría a MongoDB. En esta lección justificamos el porqué de verdad. Vamos a ver qué significa realmente "NoSQL" —y qué no significa—, qué cuatro características comparten todas las bases de esta familia, cómo se escala en horizontal mediante particionado y replicación, qué precio se paga por ello, y daremos los primeros pasos prácticos con MongoDB creando la colección resenas de BiblioRed.

Al terminar sabrás por qué existe este movimiento, no solo cómo se escribe una consulta en él. Y —igual de importante— sabrás decir cuándo no conviene usarlo.

Contenido

  1. Dónde se atasca el esquema relacional de BiblioRed
  2. Qué significa "NoSQL" y qué no significa
  3. Característica 1: esquema flexible (schema-on-read)
  4. Característica 2: orientación al agregado
  5. Característica 3: escalado horizontal frente a vertical
  6. Característica 4: distribución
  7. Cómo se escala de verdad (I): particionado o sharding
  8. Cómo se escala de verdad (II): replicación
  9. Qué se paga a cambio
  10. Primeros pasos con MongoDB: instalación y mongosh
  11. La jerarquía de MongoDB comparada con la relacional
  12. BSON, JSON y el campo _id
  13. Primeras operaciones: insertOne, insertMany, find
  14. Cuándo NO usar NoSQL
  15. Errores comunes y consejos
  16. Ejercicios
  17. Conclusión

  1. Dónde se atasca el esquema relacional de BiblioRed

Antes de hablar de tecnología, veamos el problema real. Son tres situaciones concretas.

Situación A: el catálogo enriquecido

BiblioRed ya no presta solo libros. En el catálogo hay libros, DVD, revistas y audiolibros, y cada tipo de material tiene metadatos propios:

Tipo Metadatos específicos
Libro ISBN, editorial, número de páginas, encuadernación
DVD director, duración, formato de imagen, subtítulos, calificación por edad
Revista ISSN, número, volumen, periodicidad
Audiolibro narrador, duración, códec, tamaño del fichero

Con el modelo relacional hay tres salidas clásicas, y ninguna es agradable:

  1. Una tabla ancha con todas las columnas. materiales tendría isbn, issn, director, narrador, duracion, paginas, codec… y en cada fila la mayoría estarían a NULL. En una fila de revista, el 70 % de las columnas sobran. El esquema deja de describir la realidad y pasa a ser una unión de realidades incompatibles.
  2. Una tabla por tipo. libros, dvds, revistas, audiolibros. Limpio en el papel, pero cualquier consulta del buscador ("todo lo que sea de Julio Verne") necesita un UNION de cuatro tablas, y añadir un quinto tipo de material —cómics, previsto para 2027— significa una tabla nueva y tocar todas las consultas.
  3. Entidad-atributo-valor (EAV). Una tabla atributos (material_id, clave, valor) con valor en texto. Flexible, sí, pero se pierde el tipado, se pierden las restricciones y una ficha completa requiere pivotar veinte filas. Es la solución que más veces se ha lamentado en la historia del diseño de bases de datos.

Situación B: el servicio de reseñas cambia cada mes

La primera versión de las reseñas era: texto y una puntuación de 1 a 5. En el primer trimestre el producto pidió, en este orden:

  • añadir etiquetas libres que pone el lector ("novela histórica", "para regalar");
  • permitir votar si una reseña ha sido útil, guardando quién votó;
  • permitir respuestas de un bibliotecario a una reseña;
  • añadir spoiler: sí/no y ocultar el texto por defecto si lo es.

En el mundo relacional, cada uno de esos cambios es un ALTER TABLE o una tabla nueva, con su migración, su ventana de despliegue y su coordinación con el equipo de aplicación. Cuatro cambios de esquema en tres meses sobre una tabla que aún no tiene un formato estable.

Situación C: el registro de actividad

Cada vez que un socio busca en el catálogo, abre una ficha o filtra por autor, el portal quiere registrar el evento. Las estimaciones del equipo:

12.000 socios × ~1,5 sesiones/semana × ~18 eventos/sesión ≈ 324.000 eventos/semana
                                                          ≈ 17 millones/año

Diecisiete millones de filas al año, escritas de forma continua, que casi nunca se leen fila a fila (se leen agregadas: "búsquedas más frecuentes del mes"), que no necesitan claves ajenas y que a los dos años pierden casi todo su valor. Meterlas en la misma base transaccional que los préstamos significa hacer crecer los índices, alargar las copias de seguridad y competir por la caché de una base que debe responder rápido en el mostrador.

Ninguna de las tres situaciones es un fallo del modelo relacional: es una falta de encaje. El modelo relacional brilla con datos homogéneos, muy relacionados entre sí y con reglas de integridad estrictas —exactamente lo que son los préstamos—. Estas tres situaciones son otra cosa.

  1. Qué significa "NoSQL" y qué no significa

El nombre es, sinceramente, malo. Nació en 2009 como el hashtag de un encuentro técnico en San Francisco y se quedó. La lectura que ha acabado imponiéndose es "not only SQL": no en lugar de SQL, sino además de SQL.

Qué significa en la práctica:

  • Un conjunto de bases de datos que no usan el modelo relacional de tablas, filas y columnas como estructura principal.
  • Sistemas diseñados desde el primer día para distribuirse en varias máquinas.
  • Modelos de datos alternativos: documentos, pares clave-valor, familias de columnas, grafos.

Qué no significa, y conviene desmontarlo ya:

Mito Realidad
"NoSQL significa que no hay lenguaje de consulta" MongoDB tiene su lenguaje de consulta y su aggregation framework; Cassandra usa CQL, que se parece muchísimo a SQL; Neo4j usa Cypher. Algunas incluso aceptan SQL directamente.
"NoSQL significa que no hay esquema" Significa que el esquema no lo impone el servidor, no que no exista. El esquema existe siempre: está en el código de la aplicación. Lo veremos en detalle en el apartado 3.
"NoSQL sustituye a las relacionales" En la inmensa mayoría de arquitecturas reales conviven. Es exactamente lo que va a pasar en BiblioRed.
"NoSQL es más moderno, luego mejor" Son herramientas con encajes distintos. Una base documental para gestionar préstamos con recargos sería una mala decisión, y lo será en 2030 igual que hoy.
"NoSQL no tiene transacciones" Fue cierto en muchos productos durante años. MongoDB tiene transacciones multidocumento desde 2018. Volveremos sobre esto en la lección 03-04.

Una forma útil de verlo: las bases NoSQL renuncian deliberadamente a algunas garantías del modelo relacional a cambio de algo concreto —escalado, flexibilidad o eficiencia en un tipo de consulta—. La pregunta que debes hacerte siempre no es "¿es moderna?", sino "¿qué renuncia hace este producto y me sale a cuenta?".

  1. Característica 1: esquema flexible (schema-on-read)

Es la diferencia que más se nota el primer día.

  • Schema-on-write (relacional): el esquema se define antes de escribir. El servidor rechaza cualquier dato que no encaje. Es el CREATE TABLE de la lección 02-02 y las restricciones de la 02-06.
  • Schema-on-read (documental): se escribe lo que sea y la interpretación ocurre al leer. La aplicación es la que sabe qué campos espera.

Míralo con el catálogo de BiblioRed. Dos documentos de la misma colección:

{
  "_id": "MAT-0331",
  "tipo": "libro",
  "titulo": "El mapa del tiempo",
  "isbn": "9788401339097",
  "editorial": "Ediciones Vallmar",
  "paginas": 612
}
{
  "_id": "MAT-0742",
  "tipo": "dvd",
  "titulo": "Cartografías del Mediterráneo",
  "director": "Aina Ferriol",
  "duracion_min": 94,
  "subtitulos": ["es", "ca", "en"]
}

Conviven en la colección catalogo sin ningún NULL, sin UNION, sin tabla nueva. Cuando en 2027 lleguen los cómics, se insertan documentos con "tipo": "comic" y campos dibujante y numero_tomo. Sin ALTER TABLE, sin migración, sin ventana de despliegue.

Ahora la letra pequeña, que importa mucho:

El esquema no ha desaparecido. Se ha movido de sitio: del CREATE TABLE al código de la aplicación. Y ahí no lo comprueba nadie automáticamente.

Si un desarrollador escribe "duracion" en unos documentos y "duracion_min" en otros, MongoDB acepta los dos encantado y el error aparece meses después, en un informe que devuelve la mitad de los datos. Por eso en la lección 03-03 veremos la validación de esquema ($jsonSchema), que permite recuperar parte de esa red de seguridad de forma voluntaria.

Schema-on-write Schema-on-read
Quién valida El servidor de base de datos La aplicación
Cuándo falla En la escritura, inmediatamente En la lectura, quizá meses después
Coste de cambiar ALTER TABLE + migración Escribir el campo nuevo
Documentos heterogéneos Difícil (nulos o EAV) Natural
Garantía de coherencia Alta y automática La que ponga el equipo

  1. Característica 2: orientación al agregado

Este concepto es el que de verdad explica NoSQL, y se subestima porque suena abstracto. Vamos con él despacio.

Un agregado es un conjunto de datos que la aplicación trata como una unidad: se lee junto, se escribe junto y, normalmente, se borra junto.

En BiblioRed, la ficha de una reseña es un agregado: cuando el portal muestra una reseña, muestra a la vez su texto, su puntuación, sus etiquetas, quién la escribió y cuántos votos útiles tiene. Nunca muestra "las etiquetas" solas.

En el modelo relacional, ese agregado está desparramado en varias tablas porque la normalización lo exige (lo estudiaremos formalmente en el módulo 5). Reconstruirlo requiere JOIN:

SELECT r.texto, r.puntuacion, s.nombre, e.etiqueta
FROM resenas r
INNER JOIN socios s ON s.socio_id = r.socio_id
LEFT  JOIN resenas_etiquetas e ON e.resena_id = r.resena_id
WHERE r.libro_id = 331;

Tres tablas, un JOIN y una explosión de filas (una fila por etiqueta) que la aplicación tiene que volver a plegar en memoria. En el modelo documental, el agregado es el documento:

{
  "_id": "RES-1001",
  "libro_id": 331,
  "titulo_libro": "El mapa del tiempo",
  "socio": { "socio_id": 14, "nombre": "Marta Alsina" },
  "puntuacion": 5,
  "texto": "Una novela que juega con el tiempo sin marear al lector.",
  "etiquetas": ["novela histórica", "ciencia ficción", "recomendada"],
  "votos_utiles": 7,
  "fecha": "2026-03-14"
}

Una sola lectura, un solo objeto, cero JOIN. Ese es el núcleo de la propuesta.

Y aquí viene la consecuencia que casi nadie explica al principio: si el dato se lee junto, también se puede guardar junto en disco. Y si está junto en disco, se puede mover entero a otra máquina. El agregado es la unidad natural de distribución, y por eso la orientación al agregado y el escalado horizontal son la misma idea vista desde dos ángulos.

También es la frontera natural de la atomicidad: en MongoDB, la escritura de un documento completo es atómica sin necesidad de transacción. Todo lo que quepa dentro del agregado se actualiza de una pieza. Todo lo que quede fuera, no.

  1. Característica 3: escalado horizontal frente a vertical

Cuando un sistema se queda corto hay dos caminos.

  • Escalado vertical (scale up): máquina más grande. Más CPU, más RAM, discos más rápidos. Es lo primero que se hace y funciona sorprendentemente bien durante mucho tiempo.
  • Escalado horizontal (scale out): más máquinas, cada una con una parte del trabajo.
Vertical Horizontal
Cómo se crece Se sustituye el servidor Se añaden servidores
Techo Existe y es duro: la máquina más grande del catálogo Prácticamente ilimitado
Coste Crece más que linealmente (el doble de CPU cuesta bastante más del doble) Aproximadamente lineal
Complejidad Baja: la aplicación no se entera Alta: hay que repartir y coordinar datos
Punto único de fallo No, si hay replicación
Parada para crecer Normalmente sí No

El modelo relacional clásico se escala en vertical con naturalidad, y en horizontal con dificultad: un JOIN entre dos tablas que viven en máquinas distintas requiere mover datos por la red, y una transacción que toca varias máquinas requiere protocolos de compromiso en dos fases, que son lentos y frágiles. Las bases NoSQL eliminan por diseño las dos operaciones problemáticas —el JOIN del lado del servidor y la transacción distribuida generalizada— y a cambio se reparten sin fricción.

Una advertencia contra la exageración de las presentaciones comerciales: BiblioRed tiene 12.000 socios y 17 millones de eventos al año. Eso cabe de sobra en una sola máquina PostgreSQL bien configurada. La razón por la que BiblioRed va a usar MongoDB no es el volumen, es la heterogeneidad del catálogo y la velocidad de cambio del servicio de reseñas. Ser honesto con el motivo real es parte del oficio.

  1. Característica 4: distribución

La cuarta característica es consecuencia de la tercera: estos sistemas se diseñaron partiendo de la base de que van a vivir en varias máquinas, no como una extensión añadida después.

Eso implica tres cosas que se dan por sentadas en NoSQL y que en una relacional clásica son proyectos:

  1. Añadir un nodo es una operación rutinaria, no una migración.
  2. La caída de un nodo no es una caída del servicio, si hay réplicas.
  3. Los datos se colocan automáticamente: el sistema decide en qué nodo va cada dato y lo reequilibra solo.

Los dos mecanismos que lo hacen posible son el particionado y la replicación. Son distintos, resuelven problemas distintos y se usan a la vez. Vamos con ellos.

  1. Cómo se escala de verdad (I): particionado o sharding

Particionar (o shardear) es repartir el conjunto de datos entre varios nodos, de modo que cada nodo guarde solo una parte. Objetivo: repartir volumen y repartir carga de escritura.

La pieza clave es la clave de partición (shard key): el campo cuyo valor decide en qué fragmento vive cada documento.

flowchart TD
    APP["Aplicación<br/>del portal BiblioRed"] --> R["Router / mongos<br/>consulta el mapa de fragmentos"]
    R --> S1["Fragmento A<br/>socio_id 1 – 4000<br/>~5,7 M eventos"]
    R --> S2["Fragmento B<br/>socio_id 4001 – 8000<br/>~5,6 M eventos"]
    R --> S3["Fragmento C<br/>socio_id 8001 – 12000<br/>~5,9 M eventos"]
    CFG["Servidores de configuración<br/>rangos → fragmento"] -.-> R

Cómo funciona una operación:

  • Escritura de un evento del socio 15 → el router calcula que 15 cae en el fragmento A → escribe solo ahí. Los otros dos fragmentos ni se enteran, y por eso las escrituras se reparten.
  • Lectura filtrada por socio_id: 15 → el router va directo al fragmento A. Es una consulta dirigida y es rápida.
  • Lectura sin filtrar por la clave (por ejemplo, "eventos del último día") → el router debe preguntar a los tres fragmentos y unir los resultados. Es una consulta dispersa, y es cara.

De ahí la regla práctica más importante del particionado:

Elegir la clave de partición es elegir qué consultas serán rápidas. Una mala clave convierte todas las consultas en dispersas y hace el sistema más lento que una sola máquina.

Qué caracteriza a una buena clave de partición:

Criterio Por qué importa Ejemplo malo en BiblioRed
Cardinalidad alta Muchos valores distintos = muchos fragmentos posibles tipo_material (solo 4 valores: máximo 4 fragmentos)
Distribución uniforme Evita que un fragmento reciba casi todo sucursal_id si la sucursal Centro concentra el 60 % de la actividad
Sin monotonía creciente Una clave siempre creciente manda todas las escrituras nuevas al último fragmento (hotspot) fecha_evento en bruto
Presente en las consultas frecuentes Si no, todas las lecturas son dispersas _id aleatorio cuando siempre se filtra por socio

Dos estrategias de reparto:

  • Por rango: el fragmento A guarda socio_id 1–4000, el B 4001–8000. Ventaja: las consultas por rango van a pocos fragmentos. Riesgo: desequilibrio si los valores no se reparten bien.
  • Por hash: se aplica una función hash a la clave y el resultado decide el fragmento. Ventaja: reparto muy uniforme. Inconveniente: las consultas por rango se vuelven dispersas, porque valores contiguos acaban en fragmentos distintos.

Y una nota de realismo: BiblioRed no va a particionar nada. Sus volúmenes caben holgadamente en un solo nodo. El particionado se estudia para entender la propuesta arquitectónica de NoSQL y para reconocer cuándo hará falta, no porque haya que activarlo el primer día. Activarlo sin necesidad añade complejidad operativa a cambio de nada.

  1. Cómo se escala de verdad (II): replicación

Replicar es mantener copias completas del mismo conjunto de datos en varios nodos. Objetivo: sobrevivir a fallos y, secundariamente, repartir carga de lectura.

El modelo dominante es primario-secundarios. En MongoDB se llama conjunto de réplicas (replica set):

flowchart TD
    APP["Aplicación"] -->|escrituras| P["PRIMARIO<br/>acepta lecturas y escrituras"]
    APP -.->|lecturas opcionales| S1
    APP -.->|lecturas opcionales| S2
    P -->|replica el oplog| S1["SECUNDARIO 1<br/>copia completa"]
    P -->|replica el oplog| S2["SECUNDARIO 2<br/>copia completa"]
    S1 <-->|latido cada 2 s| S2

Reglas del juego:

  1. Todas las escrituras van al primario. Hay uno solo, y por eso no hay conflictos de escritura.
  2. El primario registra cada cambio en un registro de operaciones (el oplog), y los secundarios lo aplican en el mismo orden. Van unos milisegundos por detrás.
  3. Los nodos se envían latidos cada pocos segundos. Si el primario deja de responder, los secundarios eligen un nuevo primario por votación y el servicio continúa. Este proceso se llama failover y suele tardar entre 5 y 15 segundos.
  4. Para que haya mayoría en la votación conviene un número impar de nodos. Tres es la configuración mínima sensata.

Qué gana BiblioRed con esto: si el servidor que aloja las reseñas se apaga a las tres de la madrugada, el portal deja de funcionar unos segundos y sigue. Con una sola máquina, deja de funcionar hasta que alguien llegue.

Y aquí aparece un matiz que conviene sembrar ya, aunque lo desarrollaremos en la lección 03-04: si la aplicación lee de un secundario, puede leer un dato ligeramente atrasado. Una reseña que Marta Alsina acaba de publicar podría no aparecerle a Iván Pereda durante unos milisegundos. Ese fenómeno se llama consistencia eventual, y es la contrapartida del reparto. En 03-04 lo estudiaremos junto al teorema CAP y al contraste entre ACID y BASE.

Particionado (sharding) Replicación
Qué guarda cada nodo Una parte de los datos Todos los datos
Problema que resuelve Volumen y carga de escritura Disponibilidad y carga de lectura
Si cae un nodo Se pierde acceso a esa parte No pasa nada: hay copias
Decisión clave La clave de partición Cuántos nodos y desde dónde se lee
¿Se usan juntos? Sí: en producción, cada fragmento es a su vez un conjunto de réplicas

  1. Qué se paga a cambio

Ninguna de estas ventajas es gratis. Estas son las cuatro facturas, y hay que verlas antes de firmar.

9.1 No hay JOIN del lado del servidor

En el módulo 2 cruzamos siete tablas con una consulta. En una base documental, si la información que necesitas está en dos colecciones, tienes tres opciones y las tres tienen coste:

  1. Duplicar el dato dentro del documento (lo habitual).
  2. Hacer dos consultas desde la aplicación y combinar en memoria.
  3. Usar $lookup, la operación de MongoDB parecida a un LEFT JOIN, que existe pero es lenta y no está pensada para usarse en todas partes (en la lección 03-03 la veremos como anti-patrón cuando se abusa de ella).

9.2 No hay integridad referencial declarativa

Todo el módulo 2 se cerró explicando que la base de datos es la última línea de defensa. En MongoDB esa línea no existe: puedes guardar una reseña con libro_id: 9999 sin que nadie proteste, aunque ese libro no exista. La responsabilidad pasa íntegra a la aplicación.

9.3 Duplicación deliberada de datos

Fíjate en el documento de reseña del apartado 4: guarda "titulo_libro": "El mapa del tiempo" y "nombre": "Marta Alsina". Eso es información que también está en libros y en socios. Está duplicada a propósito, para poder pintar la reseña sin ir a buscar nada más.

Y esa duplicación tiene consecuencias: si Marta se casa y cambia de apellido, hay 34 reseñas suyas con el apellido antiguo. ¿Hay que actualizarlas todas? A veces sí (nombre para mostrar) y a veces no (el nombre en el momento de la reseña es un dato histórico legítimo). Es una decisión de diseño consciente, y la trataremos a fondo en 03-03.

9.4 La coherencia se traslada a la aplicación

Resumido en una tabla, para que quede claro quién hace qué:

Responsabilidad Relacional Documental
Que los tipos sean correctos Servidor (CREATE TABLE) Aplicación
Que no falten campos obligatorios Servidor (NOT NULL) Aplicación
Que no haya duplicados Servidor (UNIQUE) Servidor (índice único) o aplicación
Que las referencias existan Servidor (FOREIGN KEY) Aplicación
Que el dato duplicado esté al día No aplica (no se duplica) Aplicación
Que varias escrituras sean todo o nada Servidor (transacción) Dentro del documento: servidor. Entre documentos: transacción explícita

La conclusión operativa: NoSQL no elimina trabajo, lo mueve de la base de datos al código. Si el equipo es disciplinado y las revisiones de código son serias, el trato sale bien. Si no, sale muy mal.

  1. Primeros pasos con MongoDB: instalación y mongosh

Vamos a la práctica. Necesitas un servidor MongoDB y su cliente de línea de órdenes, mongosh.

Opción A: contenedor Docker (recomendada para aprender)

Es la vía más limpia: no ensucia el sistema y se borra entera cuando acabes.

# Descarga la imagen y arranca un servidor en el puerto 27017
docker run -d --name mongo-biblioRed -p 27017:27017 mongo:7

# Comprobar que está en marcha
docker ps --filter name=mongo-biblioRed --format "{{.Names}}\t{{.Status}}"
mongo-biblioRed	Up 12 seconds
# Abrir la shell dentro del contenedor
docker exec -it mongo-biblioRed mongosh

Opción B: instalación local

# Debian / Ubuntu (tras añadir el repositorio oficial de MongoDB)
sudo apt install -y mongodb-org
sudo systemctl start mongod
sudo systemctl status mongod --no-pager | head -3
● mongod.service - MongoDB Database Server
     Loaded: loaded (/lib/systemd/system/mongod.service; enabled)
     Active: active (running)
mongosh

Opción C: MongoDB Atlas

Es el servicio gestionado en la nube del propio fabricante y tiene una capa gratuita suficiente para el curso. Te dan una cadena de conexión y te conectas con:

mongosh "mongodb+srv://cluster0.ejemplo.mongodb.net/" --username alumno

Comprobar que la shell funciona

Al entrar verás algo así:

Current Mongosh Log ID:	66ab0f1c2d4e5f6a7b8c9d0e
Connecting to:		mongodb://127.0.0.1:27017/
Using MongoDB:		7.0.11
Using Mongosh:		2.2.6

test>

Ese test> es el indicador: estás en la base de datos test. Y aquí llega el detalle más agradable de mongosh: es un intérprete de JavaScript completo. Puedes declarar variables, usar bucles y llamar a funciones. No es un lenguaje aparte como SQL: son llamadas a métodos de objetos.

db.version()
7.0.11

  1. La jerarquía de MongoDB comparada con la relacional

La correspondencia mental que necesitas es esta:

PostgreSQL MongoDB Comentario
Servidor / clúster Servidor / deployment Un proceso escuchando en un puerto
Base de datos Base de datos Mismo concepto
Tabla Colección Conjunto de documentos, sin esquema impuesto
Fila Documento Estructura tipo JSON, puede anidar
Columna Campo Puede faltar en unos documentos y estar en otros
Clave primaria Campo _id Obligatorio y único, generado si no lo pones
Índice Índice Mismo concepto y misma finalidad (módulo 6)
JOIN $lookup Existe, pero no es el camino habitual
Esquema (DDL) (nada equivalente obligatorio) Validación opcional con $jsonSchema (03-03)
flowchart LR
    subgraph REL["PostgreSQL — biblioredb"]
        T1["tabla socios"] --> F1["fila: socio_id 14, Marta Alsina"]
        T2["tabla prestamos"] --> F2["fila: prestamo_id 902"]
    end
    subgraph DOC["MongoDB — bibliored"]
        C1["colección resenas"] --> D1["documento:<br/>{_id, socio:{...}, etiquetas:[...]}"]
        C2["colección catalogo"] --> D2["documento:<br/>{_id, tipo, metadatos:{...}}"]
    end

Un detalle práctico que sorprende a quien viene de SQL: las bases y las colecciones se crean solas. No hay CREATE DATABASE ni CREATE TABLE. Basta con seleccionar una base e insertar; MongoDB la materializa en el primer documento escrito.

use bibliored
switched to db bibliored
// Todavía no existe físicamente: no hay datos
show dbs
admin   40.00 KiB
config  12.00 KiB
local   72.00 KiB

bibliored no aparece. Es normal: aparecerá tras la primera inserción.

  1. BSON, JSON y el campo _id

BSON

Los documentos se escriben con aspecto de JSON, pero MongoDB los guarda internamente en BSON (Binary JSON). Las diferencias importan:

JSON BSON
Formato Texto Binario
Tipos numéricos Un solo tipo number int32, int64, double, decimal128
Fechas No existen (se usan cadenas) Tipo Date nativo
Datos binarios No (hay que codificar en base64) Tipo BinData
Recorrido Hay que analizar todo el texto Lleva longitudes: puede saltar campos
Tamaño Más compacto en texto plano Algo mayor, pero mucho más rápido de recorrer

Consecuencia práctica: usa los tipos nativos. Una fecha guardada como cadena "2026-03-14" no se puede comparar por rango ni agrupar por mes de forma fiable; guardada como ISODate sí.

// Mal: la fecha es una cadena
{ fecha: "2026-03-14" }

// Bien: la fecha es un tipo Date de BSON
{ fecha: ISODate("2026-03-14T10:25:00Z") }

Ojo también con el límite de 16 MB por documento. Parece enorme —son unas 8.000 páginas de texto— pero es el límite que hace inviable, por ejemplo, meter dentro del documento de un libro popular sus 40.000 eventos de consulta. Volveremos a este límite en 03-03, porque es el criterio que gobierna la decisión de embeber o referenciar.

El campo _id

Todo documento tiene un campo _id que hace de clave primaria:

  • Es obligatorio: si no lo pones, MongoDB lo genera.
  • Es único dentro de la colección, con un índice creado automáticamente que no se puede eliminar.
  • Es inmutable: no se puede modificar después.
  • Puede ser de cualquier tipo: ObjectId, cadena, número, incluso un documento.

Por defecto es un ObjectId, un identificador de 12 bytes que se genera en el cliente (no en el servidor) y que contiene la marca de tiempo de creación, un identificador de proceso y un contador. Eso lo hace único sin coordinación entre máquinas, que es justo lo que necesita un sistema distribuido —a diferencia del SERIAL de PostgreSQL, que exige un contador central—.

const id = new ObjectId()
id
id.getTimestamp()
ObjectId('66ab13a45c9e1b2f3d4a6c81')
ISODate('2026-08-02T09:14:12.000Z')

Cuando tengas un identificador natural con significado propio, úsalo como _id: te ahorras un índice. En el catálogo de BiblioRed usaremos códigos como "MAT-0331".

  1. Primeras operaciones: insertOne, insertMany, find

Creamos por fin la colección resenas de BiblioRed. Todos los datos son ficticios.

Insertar un documento

use bibliored

db.resenas.insertOne({
  libro_id: 331,
  isbn: "9788401339097",
  titulo_libro: "El mapa del tiempo",
  socio: { socio_id: 14, nombre: "Marta Alsina" },
  sucursal_id: 1,
  puntuacion: 5,
  texto: "Una novela que juega con el tiempo sin marear al lector. Muy recomendable.",
  etiquetas: ["novela histórica", "ciencia ficción"],
  votos_utiles: 7,
  spoiler: false,
  fecha: ISODate("2026-03-14T10:25:00Z")
})
{
  acknowledged: true,
  insertedId: ObjectId('66ab14b25c9e1b2f3d4a6c82')
}

Léelo con calma, porque hay tres cosas nuevas respecto a un INSERT de SQL:

  1. No hemos creado nada antes. Ni la base bibliored, ni la colección resenas. Existen desde esta línea.
  2. socio es un documento anidado. En SQL eso serían dos columnas o una tabla aparte; aquí es un objeto dentro del objeto.
  3. etiquetas es un array. El modelo relacional no admite valores múltiples en una celda —la primera forma normal lo prohíbe, y lo veremos en 05-02—. El modelo documental sí, y esa es una de sus diferencias más profundas.

Insertar varios documentos

db.resenas.insertMany([
  {
    libro_id: 331,
    isbn: "9788401339097",
    titulo_libro: "El mapa del tiempo",
    socio: { socio_id: 15, nombre: "Iván Pereda" },
    sucursal_id: 2,
    puntuacion: 3,
    texto: "Empieza muy bien, pero la parte final se me hizo larga.",
    etiquetas: ["novela histórica"],
    votos_utiles: 2,
    spoiler: false,
    fecha: ISODate("2026-03-22T18:40:00Z")
  },
  {
    libro_id: 412,
    isbn: "9788401337208",
    titulo_libro: "Los pilares de la Tierra",
    socio: { socio_id: 16, nombre: "Nuria Bastos" },
    sucursal_id: 3,
    puntuacion: 5,
    texto: "Mil páginas que se pasan volando. La construcción de la catedral engancha.",
    etiquetas: ["novela histórica", "para regalar", "clásico moderno"],
    votos_utiles: 12,
    spoiler: false,
    respuesta_bibliotecario: {
      nombre: "Equipo Sucursal Sur",
      texto: "Si te gustó, tenemos disponible la continuación en la sucursal Sur.",
      fecha: ISODate("2026-03-25T09:10:00Z")
    },
    fecha: ISODate("2026-03-24T12:05:00Z")
  },
  {
    libro_id: 412,
    isbn: "9788401337208",
    titulo_libro: "Los pilares de la Tierra",
    socio: { socio_id: 14, nombre: "Marta Alsina" },
    sucursal_id: 1,
    puntuacion: 4,
    texto: "Muy entretenida, aunque algunos personajes son demasiado planos.",
    etiquetas: ["novela histórica"],
    votos_utiles: 4,
    spoiler: true,
    fecha: ISODate("2026-04-02T20:15:00Z")
  }
])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('66ab15c15c9e1b2f3d4a6c83'),
    '1': ObjectId('66ab15c15c9e1b2f3d4a6c84'),
    '2': ObjectId('66ab15c15c9e1b2f3d4a6c85')
  }
}

Observa que el tercer documento tiene un campo, respuesta_bibliotecario, que los demás no tienen. Nadie ha protestado. Eso es schema-on-read en acción: cuando el producto pidió las respuestas del bibliotecario, no hubo ALTER TABLE; simplemente empezaron a escribirse documentos con ese campo.

Leer documentos

find es el equivalente de SELECT. Recibe un documento de filtro: cada campo es una condición y se combinan con Y lógico.

// Todas las reseñas — equivale a SELECT * FROM resenas
db.resenas.find()
[
  { _id: ObjectId('...c82'), libro_id: 331, titulo_libro: 'El mapa del tiempo', puntuacion: 5, ... },
  { _id: ObjectId('...c83'), libro_id: 331, titulo_libro: 'El mapa del tiempo', puntuacion: 3, ... },
  { _id: ObjectId('...c84'), libro_id: 412, titulo_libro: 'Los pilares de la Tierra', puntuacion: 5, ... },
  { _id: ObjectId('...c85'), libro_id: 412, titulo_libro: 'Los pilares de la Tierra', puntuacion: 4, ... }
]
// Filtro por igualdad — WHERE libro_id = 331
db.resenas.find({ libro_id: 331 })
[
  { _id: ObjectId('...c82'), socio: { socio_id: 14, nombre: 'Marta Alsina' }, puntuacion: 5, ... },
  { _id: ObjectId('...c83'), socio: { socio_id: 15, nombre: 'Iván Pereda' }, puntuacion: 3, ... }
]
// Filtro sobre un campo anidado: se usa la notación de punto entre comillas
db.resenas.find({ "socio.socio_id": 14 })
[
  { _id: ObjectId('...c82'), titulo_libro: 'El mapa del tiempo', puntuacion: 5, ... },
  { _id: ObjectId('...c85'), titulo_libro: 'Los pilares de la Tierra', puntuacion: 4, ... }
]
// Filtro sobre un array: coincide si CUALQUIER elemento vale
db.resenas.find({ etiquetas: "para regalar" })
[
  { _id: ObjectId('...c84'), titulo_libro: 'Los pilares de la Tierra', socio: { socio_id: 16, nombre: 'Nuria Bastos' }, ... }
]

Esa última consulta merece un momento de atención. En SQL, para consultar etiquetas necesitarías una tabla resenas_etiquetas, un JOIN y un DISTINCT. Aquí es un filtro de igualdad sobre un array, y MongoDB entiende automáticamente que hay que mirar dentro. Es un ejemplo perfecto de qué gana la orientación al agregado.

// Comprobación final
db.resenas.countDocuments()
show collections
4
resenas

Los operadores de consulta completos ($gt, $in, $regex, proyección, ordenación, actualizaciones con $set y $push, y la aggregation pipeline) los veremos en la lección siguiente, 03-02, junto con las otras tres familias NoSQL.

  1. Cuándo NO usar NoSQL

Esta sección es la más honesta de la lección y probablemente la más útil en tu carrera. Estas son las señales de que la respuesta correcta es una base relacional:

  1. Los datos son muy relacionados y las consultas son impredecibles. Si mañana alguien puede pedir "préstamos de socios de la sucursal Norte, de libros en catalán publicados después de 2015, cuyo autor tenga otro libro reservado", quieres SQL. Ese es exactamente el terreno donde el modelo relacional no tiene rival.
  2. Necesitas transacciones sobre varias entidades como norma, no como excepción. Registrar un préstamo toca prestamos y ejemplares a la vez y tiene que ser todo o nada. Es un caso relacional de libro.
  3. La integridad es un requisito, no una preferencia. Dinero, recargos, historiales legales, datos regulados. Si un huérfano es inaceptable, quieres que lo impida el servidor, no un if en el código.
  4. El volumen cabe en una máquina. Que es casi siempre. Una sola instancia de PostgreSQL con hardware corriente gestiona sin despeinarse cientos de gigabytes y miles de transacciones por segundo. Si esa es tu escala, el escalado horizontal solo te aporta complejidad.
  5. El equipo no tiene experiencia operando sistemas distribuidos. Un clúster mal operado es menos fiable que una máquina bien operada. La tecnología no compensa la falta de rodaje.
  6. Los informes y el análisis son el uso principal. Las herramientas de BI, los cuadros de mando y los analistas hablan SQL. Llevar los datos a un modelo documental para luego tener que sacarlos es trabajo en contra.
  7. "Porque es lo que se lleva". Es el peor motivo posible y, estadísticamente, uno de los más frecuentes.

El consejo por defecto, que repetiremos con más argumentos en la lección 03-04: empieza por la relacional y añade NoSQL cuando tengas un motivo concreto que puedas escribir en una frase. BiblioRed puede escribirla: "el catálogo es heterogéneo por tipo de material, las reseñas cambian de forma cada mes y la actividad es un volumen alto de escrituras que no necesita integridad referencial". Eso es un motivo. "Queremos modernizarnos" no lo es.

Errores Comunes y Consejos

Error 1: creer que NoSQL significa "sin esquema" y no diseñar nada. El esquema existe siempre; solo cambia quién lo vigila. Escribe el esquema esperado de cada colección en la documentación del proyecto desde el primer día, aunque el servidor no lo exija. En 03-03 verás cómo hacer que el servidor también lo vigile con $jsonSchema.

Error 2: migrar toda la base relacional a MongoDB "para unificar". Es la decisión que más arrepentimientos ha producido en la última década. BiblioRed no mueve prestamos ni socios: ahí el modelo relacional gana. Se mueve solo lo que encaja mal.

Error 3: adoptar NoSQL "por rendimiento" sin haber medido. Muchos problemas atribuidos a "que la relacional es lenta" son en realidad la falta de un índice o una consulta mal escrita. Antes de cambiar de tecnología, mide y optimiza lo que tienes (módulo 6, lección 03).

Error 4: guardar fechas y números como cadenas de texto. { fecha: "14/03/2026" } y { puntuacion: "5" } funcionan al insertar y arruinan cualquier comparación, ordenación o agregación posterior. Usa ISODate(...) y números de verdad. Es el error más común y el más caro de arreglar a posteriori.

Error 5: elegir la clave de partición con la primera idea que se te ocurra. Es la decisión más difícil de rectificar de todo el sistema. Antes de elegirla, escribe las cinco consultas más frecuentes de la aplicación y comprueba cuáles serían dirigidas y cuáles dispersas.

Consejo 1: usa un identificador natural como _id cuando exista. "MAT-0331" es más legible que un ObjectId en los registros de errores y te ahorra un índice adicional.

Consejo 2: nombra los campos con una convención y respétala. Elige snake_case o camelCase, escríbelo en el manual del equipo y no lo mezcles. Sin CREATE TABLE que ponga orden, la convención es tu única defensa contra duracion / duracion_min / durationMin en la misma colección.

Consejo 3: en mongosh tienes JavaScript completo. Para generar datos de prueba, un bucle basta:

const docs = []
for (let i = 1; i <= 5; i++) {
  docs.push({ evento: "busqueda", termino: "verne", socio_id: 14, orden: i })
}
db.actividad_pruebas.insertMany(docs)
db.actividad_pruebas.countDocuments()
5

Consejo 4: aprende a leer el resultado acknowledged: true. Significa que el servidor ha confirmado la escritura. Cuando en 03-04 veamos writeConcern, entenderás qué nivel de confirmación hay detrás de ese true y por qué se puede ajustar.

Ejercicios

Ejercicio 1

BiblioRed quiere guardar en el catálogo un audiolibro y una revista. Justifica en tres o cuatro frases por qué esto encaja mejor en una colección documental que en la tabla libros del esquema relacional del módulo 2, y después escribe las dos inserciones en mongosh sobre una colección catalogo, usando un _id natural del estilo "MAT-0801". El audiolibro es una versión de "El mapa del tiempo" narrada por Àlex Roure, de 14 h 20 min, en formato MP3. La revista es "Vallmar Cultural", ISSN 2604-1188, número 42, volumen 7, periodicidad mensual.

Ejercicio 2

Para el registro de actividad de BiblioRed (17 millones de eventos al año, consultados casi siempre como "actividad de un socio concreto" y ocasionalmente como "búsquedas más frecuentes del mes"), evalúa estas tres candidatas a clave de partición y elige una, justificando la decisión con los cuatro criterios del apartado 7:

  • (a) fecha_evento
  • (b) tipo_evento (valores posibles: busqueda, ficha, filtro, descarga)
  • (c) socio_id

Ejercicio 3

Sobre la colección resenas que has creado en el apartado 13, escribe las consultas find que respondan a estas tres preguntas, y di además cuál sería el equivalente SQL aproximado de cada una:

  1. Todas las reseñas escritas por el socio 14.
  2. Todas las reseñas marcadas como spoiler.
  3. Todas las reseñas de la sucursal 3 sobre el libro 412.

Soluciones

Solución 1

Justificación. Un audiolibro y una revista comparten con el libro solo un puñado de campos (titulo, idioma, anio) y difieren en todo lo demás: narrador, duracion y formato_audio frente a issn, numero, volumen y periodicidad. En la tabla libros habría que añadir siete columnas que estarían a NULL en la inmensa mayoría de filas, o crear dos tablas nuevas que obligarían a un UNION en cada consulta del buscador. En una colección documental, cada material lleva solo los campos que tiene sentido que lleve, y añadir el tipo "cómic" en 2027 no requerirá ningún cambio de esquema ni migración.

use bibliored

db.catalogo.insertMany([
  {
    _id: "MAT-0801",
    tipo: "audiolibro",
    titulo: "El mapa del tiempo",
    idioma: "es",
    obra_relacionada: { libro_id: 331, isbn: "9788401339097" },
    metadatos: {
      narrador: "Àlex Roure",
      duracion_min: 860,
      formato_audio: "MP3",
      tamano_mb: 742
    },
    etiquetas: ["novela histórica", "audio"],
    alta: ISODate("2026-05-11T09:00:00Z")
  },
  {
    _id: "MAT-0802",
    tipo: "revista",
    titulo: "Vallmar Cultural",
    idioma: "ca",
    metadatos: {
      issn: "2604-1188",
      numero: 42,
      volumen: 7,
      periodicidad: "mensual"
    },
    etiquetas: ["cultura local", "hemeroteca"],
    alta: ISODate("2026-05-11T09:04:00Z")
  }
])
{
  acknowledged: true,
  insertedIds: { '0': 'MAT-0801', '1': 'MAT-0802' }
}

Fíjate en que insertedIds devuelve las cadenas que hemos puesto nosotros, no ObjectId: al proporcionar un _id, MongoDB lo respeta.

Solución 2

Candidata Cardinalidad Distribución Monotonía En las consultas Veredicto
(a) fecha_evento Alta Uniforme a la larga Siempre creciente Solo en la consulta mensual Mala. Todas las escrituras de hoy caerían en el mismo fragmento: un punto caliente permanente. Es el error de particionado más clásico.
(b) tipo_evento Muy baja (4 valores) Muy desigual (busqueda sería la mayoría) No Casi nunca se filtra por esto Mala. Máximo cuatro fragmentos y uno de ellos con el 60 % de los datos. La cardinalidad baja es descalificatoria.
(c) socio_id Alta (12.000 valores) Razonablemente uniforme No Sí: es el filtro de la consulta frecuente La elegida.

Decisión: socio_id. Cumple los cuatro criterios y, sobre todo, el cuarto: la consulta habitual ("actividad del socio 14") se convierte en una consulta dirigida a un solo fragmento. La consulta mensual de búsquedas frecuentes sí será dispersa, pero es ocasional, se ejecuta fuera de hora punta y es agregada por naturaleza, así que su coste es asumible.

Una mejora aún mejor sería una clave compuesta { socio_id: 1, fecha_evento: 1 }: reparte por socio y, dentro de cada socio, mantiene los eventos ordenados por fecha, lo que acelera las consultas de tipo "actividad de este socio en el último mes".

Y el recordatorio realista: con 17 millones de documentos al año, BiblioRed no necesita particionar todavía. El ejercicio sirve para saber qué clave elegiría el día que haga falta, no para activarlo mañana.

Solución 3

// 1. Reseñas del socio 14
db.resenas.find({ "socio.socio_id": 14 })
[
  { _id: ObjectId('...c82'), titulo_libro: 'El mapa del tiempo', puntuacion: 5, ... },
  { _id: ObjectId('...c85'), titulo_libro: 'Los pilares de la Tierra', puntuacion: 4, ... }
]
-- Equivalente aproximado
SELECT * FROM resenas WHERE socio_id = 14;
// 2. Reseñas marcadas como spoiler
db.resenas.find({ spoiler: true })
[
  { _id: ObjectId('...c85'), socio: { socio_id: 14, nombre: 'Marta Alsina' }, spoiler: true, ... }
]
SELECT * FROM resenas WHERE spoiler = TRUE;
// 3. Reseñas de la sucursal 3 sobre el libro 412
db.resenas.find({ sucursal_id: 3, libro_id: 412 })
[
  { _id: ObjectId('...c84'), socio: { socio_id: 16, nombre: 'Nuria Bastos' }, puntuacion: 5, ... }
]
SELECT * FROM resenas WHERE sucursal_id = 3 AND libro_id = 412;

Detalle importante de la tercera: poner dos campos en el documento de filtro equivale a AND. No hay un operador $and explícito en el caso normal; la conjunción es el comportamiento por defecto.

Conclusión

En esta lección hemos cruzado la frontera entre los dos mundos del curso:

  • El esquema relacional de BiblioRed no falla, deja de encajar en tres casos concretos: un catálogo con metadatos distintos por tipo de material, un servicio de reseñas cuya forma cambia cada mes y un registro de actividad de 17 millones de eventos anuales que no necesita integridad referencial.
  • "NoSQL" significa not only SQL: bases que no usan el modelo relacional como estructura principal y que nacen distribuidas. No significa "sin lenguaje de consulta", ni "sin esquema", ni "sustituto de lo relacional".
  • Esquema flexible (schema-on-read): el esquema no desaparece, se muda del servidor al código de la aplicación, con lo que se gana agilidad y se pierde una red de seguridad.
  • Orientación al agregado: la unidad de datos que se lee y se escribe junta se guarda junta. De ahí salen a la vez la ausencia de JOIN y la facilidad para distribuir.
  • Escalado horizontal en lugar de vertical: más máquinas en vez de una más grande, con techo prácticamente ilimitado y coste aproximadamente lineal, a cambio de complejidad operativa.
  • Particionado: cada nodo guarda una parte de los datos; la clave de partición debe tener cardinalidad alta, reparto uniforme, ausencia de crecimiento monótono y presencia en las consultas frecuentes. Replicación: cada nodo guarda una copia completa; el primario acepta las escrituras, los secundarios siguen el oplog y eligen un nuevo primario si el actual cae.
  • El precio: sin JOIN del lado del servidor, sin integridad referencial declarativa, con duplicación deliberada de datos y con la coherencia trasladada a la aplicación. NoSQL no elimina trabajo: lo mueve de la base de datos al código.
  • MongoDB en la práctica: servidor → base de datos → colección → documento; bases y colecciones que se crean solas; BSON con tipos nativos (usa ISODate, no cadenas) y límite de 16 MB por documento; el _id obligatorio, único, inmutable y generado en el cliente como ObjectId. Con insertOne, insertMany y find ya hemos creado y consultado la colección resenas, con documentos anidados, arrays y campos que solo tienen algunos documentos.
  • Cuándo no usarlo: datos muy relacionados con consultas impredecibles, transacciones multientidad habituales, integridad como requisito, volúmenes que caben en una máquina, equipo sin rodaje en sistemas distribuidos, cargas analíticas… o la moda como único argumento.

En la lección siguiente, 03-02, Tipos de Bases de Datos NoSQL, bajamos al detalle de las cuatro familias. Volveremos sobre el panorama que en 01-02 solo asomamos, pero esta vez con operación real: consultas y actualizaciones completas en MongoDB —incluida la primera aggregation pipeline como equivalente del GROUP BY de la lección 02-05—, caché y caducidad en Redis, filas anchas y CQL en Cassandra, y recorridos de varios saltos en Cypher sobre Neo4j para resolver el "lectores como tú también leyeron" que necesita el motor de recomendaciones de BiblioRed.

Fundamentos de Bases de Datos

Módulo 1: Introducción a las Bases de Datos

Módulo 2: Bases de Datos Relacionales

Módulo 3: Bases de Datos No Relacionales

Módulo 4: Diseño de Esquemas

Módulo 5: Normalización

Módulo 6: Transacciones, Rendimiento y Seguridad

Módulo 7: Ejercicios Prácticos

Módulo 8: Casos de Estudio

Módulo 9: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados