Hemos construido las dos mitades del sistema de BiblioRed: biblioredb en PostgreSQL, con siete tablas, claves ajenas y consultas multitabla; y bibliored en MongoDB, con tres colecciones diseñadas a partir de las consultas que la aplicación necesita servir. Ahora toca poner las dos mitades frente a frente y responder a la pregunta que todo profesional acaba teniendo que responder delante de alguien que decide un presupuesto: ¿cuándo cada una, y por qué?
En el camino hemos ido aplazando tres piezas teóricas, y esta es su lección. El teorema CAP —probablemente el concepto peor explicado de toda la disciplina—, su refinamiento PACELC, y el contraste entre ACID y BASE con lo que la consistencia eventual significa de verdad para el lector que acaba de publicar una reseña y no la ve.
Y hay un matiz que cambia la conversación entera y que casi ninguna comparativa antigua recoge: la frontera se ha difuminado. PostgreSQL guarda documentos JSON y los indexa; MongoDB tiene transacciones multidocumento desde 2018. La elección de 2026 no se parece a la de 2012, y decidir con los argumentos de 2012 es la forma más común de equivocarse.
Terminaremos con una guía de decisión honesta, los mitos más repetidos desmontados, y el nombre de la arquitectura a la que ha llegado BiblioRed: la persistencia políglota.
Contenido
- Comparación dimensión por dimensión
- El teorema CAP, bien explicado
- PACELC: el refinamiento necesario
- ACID frente a BASE
- Qué significa "eventualmente consistente" para un lector de BiblioRed
- Consistencia ajustable: quórum,
writeConcernyreadConcern - La frontera se ha difuminado (I): PostgreSQL con
jsonb - La frontera se ha difuminado (II): transacciones en MongoDB
- La misma consulta en los dos mundos
- Guía de decisión
- Mitos desmontados
- Persistencia políglota: la arquitectura de BiblioRed
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Comparación dimensión por dimensión
| Dimensión | Relacional (PostgreSQL) | Documental (MongoDB) |
|---|---|---|
| Modelo de datos | Tablas, filas y columnas; valores atómicos en cada celda | Documentos anidados con arrays y subdocumentos |
| Esquema | Explícito, obligatorio, validado por el servidor antes de escribir | Flexible; validación opcional con $jsonSchema |
| Cambiar el esquema | ALTER TABLE + migración + ventana de despliegue |
Escribir el campo nuevo; versionado con esquema_v |
| Lenguaje de consulta | SQL, estándar ISO desde 1987, portable entre productos | API propia + tubería de agregación; no portable |
| Relaciones | JOIN en el servidor, entre cualquier par de tablas |
Referencias resueltas por la aplicación; $lookup como excepción |
| Integridad referencial | Declarativa: FOREIGN KEY con cinco acciones ON DELETE |
Inexistente; responsabilidad de la aplicación |
| Otras restricciones | NOT NULL, UNIQUE, CHECK, dominios |
Índices únicos y $jsonSchema |
| Transacciones | ACID completas, multitabla, desde siempre; niveles de aislamiento | Atómicas por documento; multidocumento desde 2018, con coste |
| Escalado de lectura | Réplicas de lectura | Secundarios del conjunto de réplicas |
| Escalado de escritura | Vertical; particionado posible pero complejo | Horizontal por particionado, integrado |
| Consistencia por defecto | Fuerte | Fuerte al leer del primario; eventual al leer de secundarios |
| Datos heterogéneos | Columnas nulas, tablas por tipo o EAV | Natural |
| Consultas imprevistas | Excelente: el esquema normalizado responde a lo que no se previó | Limitada al diseño; puede requerir rediseño |
| Análisis e informes | Nativo; todas las herramientas de BI hablan SQL | Tubería de agregación; conectores para BI |
| Madurez | Más de 45 años de teoría y producto | Cerca de 20 años, con evolución rápida |
| Herramientas | Ecosistema enorme: ORM, migraciones, BI, auditoría, réplica lógica | Amplio y en crecimiento; menor en nichos |
| Talento disponible | Muy amplio: SQL es conocimiento básico | Más escaso, sobre todo en modelado avanzado |
| Coste operativo | Bajo en un nodo; alto si se distribuye | Bajo en un nodo; medio distribuido, pero previsto por diseño |
| Casos de uso ideales | Transacciones, finanzas, inventario, informes, integridad crítica | Catálogos, contenidos, perfiles, eventos, estructuras cambiantes |
| Casos de uso desaconsejados | Datos muy heterogéneos y esquemas que cambian cada semana | Datos muy relacionados con consultas impredecibles y transacciones cruzadas |
Dos filas merecen un comentario, porque suelen decidir proyectos y rara vez se mencionan en las presentaciones comerciales.
Talento disponible. SQL lo conoce casi cualquier persona con formación técnica. El modelado documental avanzado —lo de la lección anterior— lo domina mucha menos gente. Una decisión tecnológica que el equipo no sabe operar es una mala decisión, por muy correcta que sea sobre el papel.
Consultas imprevistas. Es la ventaja más infravalorada del modelo relacional. Un esquema normalizado responde a preguntas que nadie había pensado el día que se diseñó. Un diseño documental responde muy bien a las preguntas para las que se hizo, y puede necesitar un rediseño para las nuevas. Si no sabes qué te van a preguntar dentro de dos años, eso es un argumento a favor de SQL.
- El teorema CAP, bien explicado
Formulado por Eric Brewer en 2000 y demostrado formalmente en 2002, el teorema CAP es el resultado teórico más citado y peor entendido del mundo de las bases de datos distribuidas.
Las tres letras se refieren a propiedades de un sistema distribuido —varios nodos que se comunican por red—:
- C, consistencia (consistency): toda lectura devuelve la escritura más reciente. Todos los nodos ven lo mismo en el mismo instante. Ojo: no es la C de ACID; son conceptos distintos con la misma letra, y esa coincidencia ha causado una confusión considerable.
- A, disponibilidad (availability): toda petición a un nodo que funciona recibe una respuesta, sin error y sin espera indefinida.
- P, tolerancia a particiones (partition tolerance): el sistema sigue operando aunque se pierdan mensajes entre nodos.
El malentendido
La formulación popular —"elige dos de tres"— es incorrecta y engañosa. Sugiere que existe un menú con tres opciones: CA, CP y AP. Y no es así.
La partición no es una opción de diseño: es un hecho de la naturaleza. Los cables se rompen, los conmutadores se reinician, los centros de datos se aíslan. Si tu sistema está distribuido, habrá particiones, no puedes renunciar a P, y por tanto "CA" no existe como categoría real.
El enunciado correcto es este:
Cuando se produce una partición de red, un sistema distribuido debe elegir entre seguir respondiendo con datos posiblemente desactualizados (A) o dejar de responder para no dar datos incorrectos (C). Mientras no hay partición, se puede tener las dos cosas.
flowchart TD
P{"¿Hay partición de red<br/>ahora mismo?"}
P -->|NO — el 99,9% del tiempo| OK["Se tienen C y A a la vez.<br/>El teorema no dice nada."]
P -->|SÍ| E{"Hay que elegir"}
E -->|Prioriza C| CP["CP: el nodo aislado RECHAZA<br/>peticiones antes que arriesgar<br/>datos incorrectos.<br/>MongoDB, HBase, etcd"]
E -->|Prioriza A| AP["AP: el nodo aislado RESPONDE<br/>con lo que tiene, aunque<br/>esté desactualizado.<br/>Cassandra, DynamoDB, Riak"]
El ejemplo concreto de BiblioRed
Imagina que la red de la sucursal Sur se corta y su nodo queda aislado del resto. Marta Alsina, desde el portal, publica una reseña que llega al nodo principal. Iván Pereda, conectado por la red de la sucursal Sur, pide ver las reseñas de ese libro.
- Sistema CP (así se comporta MongoDB): el nodo aislado sabe que no puede confirmar que sus datos estén al día, así que devuelve un error o rechaza la operación. Iván ve un mensaje de "servicio no disponible". Molesto, pero nunca ve información incorrecta.
- Sistema AP (así se comporta Cassandra por defecto): el nodo aislado responde con lo que tiene. Iván ve las reseñas sin la de Marta. Cuando la red se restablece, los nodos se sincronizan y la reseña aparece. Servicio siempre disponible, a costa de haber mostrado un estado antiguo.
Ninguna de las dos es "mejor". Depende de qué duela más en tu dominio: para un catálogo de biblioteca, AP es perfectamente razonable —a nadie le arruina el día no ver una reseña durante treinta segundos—. Para un saldo bancario o para el registro de un préstamo con recargo, la respuesta es CP sin discusión.
Dónde cae cada producto
| Producto | Comportamiento ante partición | Comentario |
|---|---|---|
| PostgreSQL en un nodo | El teorema no aplica | Sin distribución no hay partición posible |
| PostgreSQL con réplicas síncronas | CP | Si la réplica no confirma, la escritura no se completa |
| MongoDB (conjunto de réplicas) | CP por defecto | Sin mayoría no hay primario, y sin primario no hay escrituras |
| Cassandra | AP, ajustable a CP | El nivel de consistencia se elige por operación |
| Redis | Depende de la configuración | En clúster, tiende a AP |
| Neo4j (clúster) | CP | Prioriza la corrección del grafo |
La fila de Cassandra es importante: los productos modernos no eligen una casilla fija. Ofrecen un mando que la aplicación ajusta operación por operación, y lo veremos en el apartado 6.
- PACELC: el refinamiento necesario
CAP tiene un problema práctico: solo dice algo sobre el momento de la partición, que es el 0,1 % del tiempo. No dice nada de lo que ocurre el otro 99,9 %. Y ahí también hay decisiones importantes.
Daniel Abadi propuso en 2012 la extensión PACELC, que se lee así:
Si hay Partición (P), elegir entre Disponibilidad (A) y Consistencia (C); En caso contrario (E, else), elegir entre Latencia (L) y Consistencia (C).
La segunda mitad es la aportación valiosa. Incluso con la red perfecta, mantener a todos los nodos perfectamente sincronizados cuesta tiempo: cada escritura debe esperar la confirmación de las réplicas antes de darse por buena. Es un intercambio permanente entre rapidez y certeza.
| Sistema | Clasificación PACELC | Traducción |
|---|---|---|
| PostgreSQL con réplica síncrona | PC/EC | Ante partición prioriza consistencia; y sin partición también, aunque cueste latencia |
| MongoDB (configuración por defecto) | PC/EC | Consistencia primero en ambos escenarios |
| MongoDB leyendo de secundarios | PC/EL | Consistente ante partición, pero acepta datos algo atrasados a cambio de latencia baja |
| Cassandra | PA/EL | Disponibilidad ante partición y latencia baja el resto del tiempo |
| DynamoDB | PA/EL (configurable) | Se puede pedir lectura fuertemente consistente pagando más y esperando más |
Lo que hay que llevarse de PACELC: la consistencia no es gratis ni siquiera cuando todo funciona bien. Cada vez que un sistema garantiza que ves el último dato, alguien ha esperado. La pregunta de diseño no es "¿quiero consistencia?" —todo el mundo la quiere—, sino "¿cuánta latencia estoy dispuesto a pagar por ella, en esta operación concreta?".
Y la respuesta suele ser distinta según la operación. En BiblioRed:
- Registrar la devolución de un ejemplar con recargo: máxima consistencia, la latencia da igual.
- Mostrar la lista de reseñas de un libro: mínima latencia, un desfase de un segundo no molesta a nadie.
Que sean respuestas distintas dentro del mismo sistema es exactamente el punto.
- ACID frente a BASE
Son los dos modelos de garantías, y sus siglas están elegidas con humor de químico: un ácido y una base.
ACID
Lo veremos en profundidad en la lección 06-01. Aquí, el contraste conceptual:
| Letra | Nombre | Qué garantiza |
|---|---|---|
| A | Atomicidad | La transacción se aplica entera o no se aplica nada |
| C | Consistencia | Al terminar, todas las reglas del esquema se cumplen |
| I | Aislamiento | Las transacciones simultáneas no se interfieren |
| D | Durabilidad | Lo confirmado sobrevive a un corte de corriente |
Ejemplo en BiblioRed: registrar un préstamo inserta una fila en prestamos y cambia ejemplares.estado a prestado. Con ACID, o pasan las dos cosas o ninguna. Nunca queda un préstamo de un ejemplar que sigue figurando como disponible.
BASE
El acrónimo alternativo que describe cómo se comportan muchos sistemas distribuidos:
| Letra | Nombre | Qué significa |
|---|---|---|
| BA | Basically Available (básicamente disponible) | El sistema responde siempre, aunque a veces con datos antiguos o parciales |
| S | Soft state (estado blando) | El estado del sistema puede cambiar sin que llegue una escritura nueva, porque las réplicas se están poniendo al día |
| E | Eventually consistent (consistencia eventual) | Si dejan de llegar escrituras, todas las réplicas convergen al mismo valor en algún momento |
La palabra clave de BASE es "eventualmente", y esconde la pregunta que hay que hacer siempre: ¿eventualmente cuándo? La teoría no lo dice; la práctica sí, y en sistemas bien dimensionados suele ser de milisegundos a unos pocos segundos.
| ACID | BASE | |
|---|---|---|
| Prioridad | Corrección | Disponibilidad |
| Consistencia | Inmediata y garantizada | Eventual |
| Disponibilidad ante fallo | Puede degradarse | Se mantiene |
| Complejidad para el desarrollador | Baja: el gestor se encarga | Alta: el código gestiona el desfase |
| Escalado | Más difícil de distribuir | Diseñado para distribuirse |
| Dominios típicos | Banca, inventario, facturación, préstamos | Catálogos, contenidos sociales, telemetría, cachés |
Un aviso frecuente y necesario: ACID y BASE no son una elección de producto, son una elección de operación. MongoDB puede darte ACID en una transacción multidocumento y BASE en una lectura desde un secundario. PostgreSQL con réplicas asíncronas también entrega lecturas eventualmente consistentes. La etiqueta correcta se pone a la operación, no al logotipo.
- Qué significa "eventualmente consistente" para un lector de BiblioRed
La teoría se entiende mejor con la escena concreta. Marta Alsina publica una reseña de "El mapa del tiempo" a las 10:25:03.
sequenceDiagram
participant M as Marta (navegador)
participant P as Primario
participant S as Secundario
participant I as Iván (navegador)
M->>P: 10:25:03.000 escribe la reseña
P-->>M: 10:25:03.012 confirmado
Note over P,S: la réplica tarda ~40 ms
I->>S: 10:25:03.030 pide las reseñas del libro
S-->>I: devuelve 811 reseñas — SIN la de Marta
P->>S: 10:25:03.052 el secundario aplica el oplog
I->>S: 10:25:04.500 Iván recarga la página
S-->>I: devuelve 812 reseñas — con la de Marta
Durante 40 milisegundos, dos usuarios del mismo sistema ven realidades distintas. Eso es la consistencia eventual, y en este caso es completamente inofensiva.
Ahora la variante que sí duele: Marta recarga su propia página a los 20 ms y la petición cae en el secundario. Marta no ve su propia reseña. Y ese es el escenario que genera incidencias, porque el usuario está seguro de que ha pulsado "publicar" y el sistema parece haberlo perdido. Puede incluso volver a publicarla, y entonces habrá dos.
Este caso tiene nombre y solución. Se llama lee tus propias escrituras (read-your-own-writes), y es una de las garantías de sesión que los sistemas distribuidos ofrecen:
| Garantía de sesión | Qué asegura |
|---|---|
| Lee tus propias escrituras | Un cliente ve siempre sus propios cambios |
| Lecturas monótonas | Un cliente nunca ve una versión anterior a otra que ya vio |
| Escrituras monótonas | Las escrituras de un mismo cliente se aplican en su orden |
| Lecturas coherentes con la escritura | Si una escritura dependía de una lectura, el orden se respeta |
MongoDB implementa estas garantías con las sesiones causalmente consistentes, activadas por defecto en los controladores modernos:
const sesion = db.getMongo().startSession({ causalConsistency: true })
const resenas = sesion.getDatabase("bibliored").getCollection("resenas")
// Marta publica su reseña
resenas.insertOne({
_id: "RES-1813",
material: { material_id: "MAT-0331", titulo: "El mapa del tiempo" },
socio: { socio_id: 14, nombre_mostrado: "Marta Alsina" },
puntuacion: 5,
texto: "Una novela que juega con el tiempo sin marear al lector.",
estado: "publicada",
fecha: new Date(),
esquema_v: 2
})
// En la MISMA sesión: garantizado que la ve, aunque lea de un secundario
resenas.countDocuments({ "material.material_id": "MAT-0331" })
sesion.endSession()La regla práctica de diseño, que vale para cualquier sistema distribuido:
Después de una escritura, lee del primario o dentro de una sesión causal. El resto de lecturas —listados, búsquedas, navegación— pueden ir a secundarios sin problema. Distinguir esas dos rutas en el código es una de las mejores inversiones de un sistema distribuido.
- Consistencia ajustable: quórum,
writeConcern y readConcern
writeConcern y readConcernLos sistemas modernos no te obligan a elegir una vez para siempre. Ofrecen un mando por operación.
La idea del quórum
Con N réplicas, se define cuántas deben confirmar una escritura (W) y de cuántas se lee (R). La consistencia fuerte está garantizada cuando:
Con N = 3 réplicas:
| W | R | W + R > N | Comportamiento |
|---|---|---|---|
| 1 | 1 | 2 > 3 ✗ | Rapidísimo, pero puedes leer datos antiguos |
| 2 | 2 | 4 > 3 ✓ | Equilibrio habitual: mayoría en ambos lados |
| 3 | 1 | 4 > 3 ✓ | Escritura lenta y frágil, lectura rapidísima |
| 1 | 3 | 4 > 3 ✓ | Escritura rapidísima, lectura lenta y frágil |
La intuición detrás de la fórmula: si escribes en 2 de 3 y lees de 2 de 3, al menos un nodo está en ambos conjuntos y por tanto tiene el dato nuevo. La aritmética es todo el truco.
writeConcern: cuánta confirmación exijo al escribir
// Máxima seguridad: la mayoría de nodos confirma Y el dato está en disco
db.resenas.insertOne(
{ _id: "RES-1814", material: { material_id: "MAT-0412" }, puntuacion: 5,
estado: "publicada", fecha: new Date(), esquema_v: 2 },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)// Máxima velocidad: no espero confirmación de nadie
db.actividad.updateOne(
{ _id: "ACT-14-2026-08-02" },
{ $push: { eventos: { t: new Date(), tipo: "ficha", material_id: "MAT-0331" } },
$inc: { num_eventos: 1 } },
{ upsert: true, writeConcern: { w: 0 } }
)Fíjate en ese acknowledged: false: el servidor no ha confirmado nada. Es aceptable para un evento de actividad —si se pierde uno entre 17 millones, no pasa nada— e inaceptable para una reseña que el usuario cree haber publicado.
| Opción | Significado | Uso en BiblioRed |
|---|---|---|
w: 0 |
Sin confirmación | Eventos de actividad |
w: 1 |
El primario ha escrito (por defecto) | Operaciones normales |
w: "majority" |
La mayoría de nodos lo tiene | Reseñas, moderación |
j: true |
Escrito en el registro de diario, sobrevive a un corte | Datos que no se pueden perder |
wtimeout |
Milisegundos máximos de espera | Siempre, con majority |
Advertencia sobre wtimeout: si expira, la operación devuelve error pero puede haberse aplicado igualmente. El tiempo de espera limita cuánto esperas, no lo que ocurre en el servidor. La aplicación debe estar preparada para reintentar de forma idempotente.
readConcern: cuánta certeza exijo al leer
// Lectura garantizada: solo datos confirmados por la mayoría, nunca revertibles
db.resenas.find({ "material.material_id": "MAT-0331" })
.readConcern("majority").limit(3)
// Lectura rápida: lo que tenga el nodo, aunque pueda revertirse
db.catalogo.find({ etiquetas: "novela histórica" })
.readConcern("local").limit(10)| Nivel | Qué devuelve | Uso |
|---|---|---|
local |
Lo que tiene el nodo, sin garantías (por defecto) | Listados, búsquedas |
majority |
Solo lo confirmado por la mayoría; nunca se revierte | Datos que el usuario ha modificado |
linearizable |
La lectura más reciente posible, con máxima latencia | Muy raro; solo para lecturas críticas |
snapshot |
Una foto coherente en el tiempo | Dentro de transacciones |
El concepto de reversión (rollback) explica por qué majority importa. Si el primario acepta una escritura, se cae antes de replicarla y otro nodo es elegido primario, esa escritura desaparece: nunca formó parte de la mayoría. Un cliente que la hubiera leído con local habría visto un dato que dejó de existir. Con majority, eso no puede pasar.
- La frontera se ha difuminado (I): PostgreSQL con
jsonb
jsonbAquí está el matiz que hace obsoletas casi todas las comparativas escritas antes de 2016.
PostgreSQL tiene el tipo jsonb: JSON binario, indexable y consultable con operadores propios. Es decir: una base relacional que guarda documentos.
CREATE TABLE catalogo (
material_id VARCHAR(12) PRIMARY KEY,
tipo VARCHAR(20) NOT NULL,
titulo VARCHAR(200) NOT NULL,
idioma CHAR(2) NOT NULL,
activo BOOLEAN NOT NULL DEFAULT TRUE,
metadatos JSONB NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT ck_tipo CHECK (tipo IN ('libro','dvd','revista','audiolibro'))
);
CREATE INDEX idx_catalogo_metadatos ON catalogo USING GIN (metadatos);
INSERT INTO catalogo (material_id, tipo, titulo, idioma, metadatos) VALUES
('MAT-0331','libro','El mapa del tiempo','es',
'{"isbn":"9788401339097","editorial":"Ediciones Vallmar","paginas":612,
"etiquetas":["novela histórica","ciencia ficción"]}'),
('MAT-0802','revista','Vallmar Cultural','ca',
'{"issn":"2604-1188","numero":42,"volumen":7,"periodicidad":"mensual",
"etiquetas":["cultura local"]}'),
('MAT-0801','audiolibro','El mapa del tiempo','es',
'{"narrador":"Àlex Roure","duracion_min":860,"formato":"MP3",
"etiquetas":["novela histórica","audio"]}');Observa lo que se acaba de conseguir: columnas fijas para lo que es común y validable —con su PRIMARY KEY, su NOT NULL y su CHECK— y un documento libre para lo que es específico de cada tipo. Es exactamente el problema del apartado 1 de la lección 03-01, resuelto sin salir de PostgreSQL.
-- Buscar por una clave dentro del JSON: el operador @> pregunta "¿contiene esto?"
SELECT material_id, titulo
FROM catalogo
WHERE metadatos @> '{"etiquetas":["novela histórica"]}'; material_id | titulo
-------------+--------------------
MAT-0331 | El mapa del tiempo
MAT-0801 | El mapa del tiempo
(2 filas)-- Extraer un campo del JSON y usarlo como columna
SELECT titulo,
metadatos ->> 'editorial' AS editorial,
(metadatos ->> 'paginas')::int AS paginas
FROM catalogo
WHERE tipo = 'libro'; titulo | editorial | paginas
--------------------+--------------------+---------
El mapa del tiempo | Ediciones Vallmar | 612
(1 fila)-- Actualizar un campo dentro del JSON sin reescribir el resto
UPDATE catalogo
SET metadatos = jsonb_set(metadatos, '{paginas}', '618')
WHERE material_id = 'MAT-0331';Los operadores esenciales de jsonb:
| Operador | Qué hace | Ejemplo |
|---|---|---|
-> |
Extrae un campo como jsonb |
metadatos -> 'etiquetas' |
->> |
Extrae un campo como texto | metadatos ->> 'editorial' |
#> / #>> |
Extrae por ruta anidada | metadatos #>> '{portada,grande}' |
@> |
¿Contiene este fragmento? | metadatos @> '{"idioma":"ca"}' |
? |
¿Existe esta clave? | metadatos ? 'issn' |
jsonb_set |
Modifica un valor | jsonb_set(m, '{paginas}', '618') |
jsonb_array_elements |
Despliega un array (como $unwind) |
En un FROM lateral |
Y el índice GIN hace que las búsquedas por contenido del JSON usen índice en lugar de recorrer la tabla, igual que un índice multiclave de MongoDB.
Qué gana PostgreSQL con jsonb: flexibilidad de esquema para la parte heterogénea, sin perder JOIN, transacciones ACID multitabla, integridad referencial ni SQL.
Qué sigue haciendo mejor MongoDB: actualizaciones parciales sobre documentos grandes, arrays profundamente anidados con operaciones ricas ($push, $pull, operador posicional), particionado horizontal integrado, y una ergonomía de trabajo con documentos que en SQL siempre resulta algo más ceremoniosa.
Consecuencia práctica de primer orden: si tu único motivo para adoptar MongoDB es "necesito guardar unos datos con forma variable", es muy probable que jsonb te resuelva el problema sin añadir una base de datos a tu arquitectura. Y una base de datos menos son copias de seguridad menos, supervisión menos, actualizaciones menos y un experto menos que necesitas contratar.
- La frontera se ha difuminado (II): transacciones en MongoDB
El movimiento inverso también ocurrió. Durante años, "MongoDB no tiene transacciones" fue el argumento definitivo contra ella. Dejó de ser cierto:
- MongoDB 4.0 (2018): transacciones multidocumento en conjuntos de réplicas.
- MongoDB 4.2 (2019): transacciones distribuidas, también entre fragmentos.
const sesion = db.getMongo().startSession()
const bd = sesion.getDatabase("bibliored")
try {
sesion.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
})
// 1. Publicar la reseña
bd.resenas.insertOne({
_id: "RES-1815",
material: { material_id: "MAT-0331", titulo: "El mapa del tiempo", tipo: "libro" },
socio: { socio_id: 16, nombre_mostrado: "Nuria Bastos", sucursal_id: 3 },
puntuacion: 4,
texto: "Muy buena ambientación, aunque el ritmo baja en la parte central.",
estado: "publicada", votos_utiles: 0, fecha: new Date(), esquema_v: 2
}, { session: sesion })
// 2. Actualizar el campo calculado del catálogo (lección 03-03)
bd.catalogo.updateOne(
{ _id: "MAT-0331" },
{ $inc: { "valoracion.total_resenas": 1,
"valoracion.suma_puntuaciones": 4,
"valoracion.distribucion.4": 1 } },
{ session: sesion }
)
sesion.commitTransaction()
print("Transacción confirmada: reseña y contadores coherentes")
} catch (e) {
sesion.abortTransaction()
print("Transacción abortada: " + e.message)
} finally {
sesion.endSession()
}Esto resuelve el problema de coherencia que dejamos abierto en la lección 03-03: la reseña y el contador del catálogo cambian de una pieza. O las dos o ninguna.
Pero hay letra pequeña, y es importante:
| Aspecto | Realidad |
|---|---|
| Coste | Notablemente más caro que una escritura simple |
| Duración máxima | 60 segundos por defecto; más allá, se aborta |
| Requisito | Conjunto de réplicas o clúster particionado; no funciona en un nodo suelto |
| Filosofía | Son una red de seguridad, no la herramienta habitual |
La postura del propio fabricante lo dice bien: si tu aplicación necesita transacciones constantemente, el diseño de documentos no es el adecuado —o el dominio quería una relacional—. Un buen diseño documental hace que la mayoría de las operaciones sean atómicas por naturaleza, porque todo lo que debe cambiar junto está en el mismo documento. Justo lo que buscábamos con el concepto de agregado.
- La misma consulta en los dos mundos
Comparemos el mismo requisito real: "materiales en español con la etiqueta novela histórica, con su puntuación media, ordenados de mejor a peor, los cinco primeros".
-- PostgreSQL con jsonb
SELECT c.material_id,
c.titulo,
(c.metadatos ->> 'editorial') AS editorial,
ROUND(AVG(r.puntuacion), 2) AS media,
COUNT(r.resena_id) AS total_resenas
FROM catalogo c
LEFT JOIN resenas r ON r.material_id = c.material_id AND r.estado = 'publicada'
WHERE c.idioma = 'es'
AND c.activo
AND c.metadatos @> '{"etiquetas":["novela histórica"]}'
GROUP BY c.material_id, c.titulo, c.metadatos
HAVING COUNT(r.resena_id) >= 3
ORDER BY media DESC NULLS LAST
LIMIT 5; material_id | titulo | editorial | media | total_resenas
-------------+--------------------------+--------------------+-------+---------------
MAT-0412 | Los pilares de la Tierra | Ediciones Vallmar | 4.61 | 1240
MAT-0331 | El mapa del tiempo | Ediciones Vallmar | 4.30 | 812
MAT-0508 | La sombra del faro | Faro Editorial | 4.12 | 97
(3 filas)// MongoDB, con el diseño de la lección 03-03
db.catalogo.find(
{
idioma: "es",
activo: true,
etiquetas: "novela histórica",
"valoracion.total_resenas": { $gte: 3 }
},
{ titulo: 1, "metadatos.editorial": 1, "valoracion.media": 1, "valoracion.total_resenas": 1 }
).sort({ "valoracion.media": -1 }).limit(5)[
{ _id: 'MAT-0412', titulo: 'Los pilares de la Tierra',
metadatos: { editorial: 'Ediciones Vallmar' },
valoracion: { media: 4.61, total_resenas: 1240 } },
{ _id: 'MAT-0331', titulo: 'El mapa del tiempo',
metadatos: { editorial: 'Ediciones Vallmar' },
valoracion: { media: 4.3, total_resenas: 812 } },
{ _id: 'MAT-0508', titulo: 'La sombra del faro',
metadatos: { editorial: 'Faro Editorial' },
valoracion: { media: 4.12, total_resenas: 97 } }
]La comparación honesta, punto por punto:
| Aspecto | PostgreSQL con jsonb |
MongoDB |
|---|---|---|
| Datos leídos | Cruza dos tablas y agrega en el momento | Un solo documento por material |
| Coste con 2.150 reseñas por material | Agrega en cada ejecución | Lee un número ya calculado |
| Precisión de la media | Siempre exacta | Depende de que el campo calculado esté al día |
| Coste de escritura | Ninguno adicional | Cada reseña actualiza el catálogo |
| Consulta imprevista nueva | Se escribe y funciona | Puede requerir rediseño o $lookup |
| Complejidad del código | Una consulta declarativa | Una consulta simple + mantenimiento del campo calculado |
Lo importante no es cuál gana. Es que las dos son buenas soluciones con equilibrios distintos: PostgreSQL calcula al leer y garantiza exactitud; MongoDB calcula al escribir y garantiza velocidad de lectura. La elección depende de la proporción entre lecturas y escrituras y de cuánta exactitud instantánea exija el negocio.
Y fíjate en algo revelador: el campo calculado también se puede hacer en PostgreSQL —con una vista materializada o una columna mantenida por disparador—, y el cálculo al vuelo también se puede hacer en MongoDB con una tubería de agregación. Las técnicas de modelado son transversales; lo que cambia es qué te da el motor de serie.
- Guía de decisión
Las ocho preguntas, en orden
- ¿Cómo son mis datos? Homogéneos y muy relacionados → relacional. Heterogéneos, anidados, con forma variable → documental (o
jsonb). - ¿Conozco mis consultas? Si no puedo enumerarlas, o si van a cambiar de forma impredecible → relacional. El modelado documental exige conocerlas.
- ¿Necesito transacciones entre entidades como norma? Sí → relacional. Ocasionalmente → cualquiera de las dos.
- ¿Cuánta integridad exige el negocio? Si un dato incoherente tiene consecuencias legales o económicas → relacional, con las garantías en el servidor.
- ¿Qué volumen y qué crecimiento tengo? Menos de unos cientos de gigabytes y crecimiento previsible → cabe en un nodo, y eso descarta el argumento del escalado.
- ¿Con qué frecuencia cambia el esquema? Cada semana → documental o
jsonb. Cada seis meses →ALTER TABLEno es un problema. - ¿Qué sabe operar mi equipo? Es una pregunta técnica de primer orden, no un detalle de recursos humanos.
- ¿Necesito informes y análisis? Si el uso principal son cuadros de mando y analistas → relacional, porque las herramientas hablan SQL.
El consejo por defecto
Empieza por la base relacional salvo que tengas un motivo claro para no hacerlo, y sé capaz de escribir ese motivo en una frase.
No es conservadurismo. Es que la relacional es el modelo más general: sirve razonablemente bien para casi todo, tiene el ecosistema más grande, el talento más disponible, y —con jsonb— ha absorbido buena parte de la flexibilidad que motivó el movimiento NoSQL. Empezar ahí y migrar lo que no encaje es mucho más barato que el camino inverso.
El motivo de BiblioRed, escrito en una frase, cumple ese listón: "el catálogo tiene metadatos distintos por tipo de material, el servicio de reseñas cambia de forma cada mes y la actividad es un volumen alto de escrituras que no necesita integridad referencial".
flowchart TD
A["Nuevo sistema o subsistema"] --> B{"¿Transacciones entre entidades<br/>o integridad crítica?"}
B -->|Sí| REL["RELACIONAL<br/>PostgreSQL"]
B -->|No| C{"¿Conozco bien<br/>mis consultas?"}
C -->|No| REL
C -->|Sí| D{"¿Datos heterogéneos<br/>o esquema muy cambiante?"}
D -->|No| REL
D -->|Sí| E{"¿Cabe en un nodo<br/>y el resto del sistema<br/>ya es relacional?"}
E -->|Sí| JB["RELACIONAL + jsonb<br/>una base menos que operar"]
E -->|No| DOC["DOCUMENTAL<br/>MongoDB"]
- Mitos desmontados
Mito 1: "NoSQL es más rápido".
Más rápido ¿en qué? Una lectura de un agregado completo por su clave es más rápida en MongoDB que reconstruirlo con cinco JOIN. Una agregación compleja sobre datos normalizados suele ser más rápida en PostgreSQL. Y la mayoría de los problemas de rendimiento reales se resuelven con un índice, no con un cambio de motor. La velocidad depende del encaje entre el diseño y la consulta, no del logotipo.
Mito 2: "NoSQL no tiene esquema".
Tiene esquema. Está en el código de la aplicación en lugar de en el servidor. La diferencia es quién lo comprueba y cuándo falla: en el INSERT y de forma automática, o en la lectura y seis meses después. Un esquema no vigilado no es ausencia de esquema: es esquema sin garantías.
Mito 3: "Las relacionales no escalan". Escalan extraordinariamente bien en vertical, y una sola instancia de PostgreSQL bien afinada gestiona cientos de gigabytes y miles de transacciones por segundo. Lo que no escala con facilidad es la escritura distribuida en varios nodos, que es una necesidad mucho menos frecuente de lo que se cree. La mayoría de los sistemas que "necesitan escalar" necesitan en realidad un índice y una consulta mejor escrita.
Mito 4: "MongoDB pierde datos".
Fue una crítica legítima hacia 2011, cuando la configuración por defecto no esperaba confirmación de escritura. Desde hace años el valor por defecto es w: 1, y con w: "majority" y j: true las garantías son comparables a las de una relacional con réplicas síncronas. Los datos se pierden por configuraciones inadecuadas, no por el producto.
Mito 5: "Con NoSQL no hace falta diseñar". Es el mito más caro. La lección 03-03 entera existe porque hace falta diseñar más, no menos: sin normalización ni claves ajenas que corrijan los errores, un mal modelo documental no tiene red de seguridad.
Mito 6: "Hay que elegir una y usarla para todo". Falso desde hace tiempo, y es precisamente el tema del apartado siguiente.
- Persistencia políglota: la arquitectura de BiblioRed
El término lo acuñó Martin Fowler en 2011 por analogía con la programación políglota, y describe una idea sencilla:
Persistencia políglota: usar deliberadamente varios motores de almacenamiento dentro de un mismo sistema, eligiendo para cada tipo de dato el que mejor lo gestiona, en lugar de forzar todo en un único motor.
Es exactamente donde ha acabado BiblioRed:
flowchart TD
APP["Portal y aplicación de gestión<br/>de BiblioRed"]
APP --> PG["PostgreSQL — biblioredb<br/>sucursales · socios · autores · libros<br/>ejemplares · prestamos · reservas<br/>ACID, integridad, informes"]
APP --> MG["MongoDB — bibliored<br/>catalogo · resenas · actividad<br/>esquema flexible, agregados"]
PG -->|sincronización horaria<br/>de disponibilidad| MG
MG -.->|proceso nocturno:<br/>recomendaciones precalculadas| MG
| Necesidad | Motor | Por qué |
|---|---|---|
| Préstamos, socios, ejemplares, reservas | PostgreSQL | Transacciones, integridad referencial, consultas impredecibles, informes de gestión |
| Catálogo enriquecido | MongoDB | Metadatos heterogéneos por tipo de material |
| Reseñas | MongoDB | Agregado autocontenido, esquema que cambia cada mes |
| Registro de actividad | MongoDB | Volumen alto de escrituras sin necesidad de integridad |
| Recomendaciones | Precálculo → MongoDB | 12.000 socios no justifican desplegar Neo4j |
| Caché y sesiones | Aplazado | Aún no hay un problema de latencia que resolver |
El precio de la persistencia políglota
Conviene decirlo con claridad, porque las presentaciones de arquitectura casi nunca lo dicen:
- Coherencia entre motores: la disponibilidad vive en PostgreSQL y se copia a MongoDB. Esa copia puede desfasarse, y alguien tiene que vigilarlo.
- Sin transacciones entre motores: no existe forma de que una escritura en PostgreSQL y otra en MongoDB sean atómicas entre sí. Hay patrones para mitigarlo, pero ninguno es gratis.
- Doble operación: dos productos que actualizar, dos esquemas de copias de seguridad y restauración, dos sistemas de permisos, dos conjuntos de métricas.
- Doble competencia en el equipo: hay que saber diseñar y afinar los dos.
- Informes que cruzan motores: cualquier análisis que combine préstamos y reseñas requiere una capa de integración, un almacén analítico o un proceso de extracción.
La regla que resume el criterio profesional:
Cada motor que añades debe justificar su coste de operación con una ventaja que no puedas conseguir de otro modo. Dos motores bien elegidos y bien operados son una buena arquitectura. Cinco motores elegidos por curiosidad son una deuda técnica con nómina propia.
Y por eso BiblioRed se ha quedado en dos: Redis, Cassandra y Neo4j quedaron aplazados no porque no encajaran —encajan muy bien, como vimos en 03-02— sino porque su ventaja no compensa todavía el coste de operarlos.
Esta arquitectura se desarrollará por completo en la lección 08-03, Caso de Estudio: Persistencia Políglota, con el flujo de datos entre motores, los procesos de sincronización, la gestión de fallos y la evolución prevista del sistema. Antes, las lecciones 08-01 y 08-02 analizarán por separado un caso relacional y un caso documental.
Errores Comunes y Consejos
Error 1: repetir "elige dos de tres" al hablar de CAP. Es la formulación incorrecta y quien la escucha se lleva una idea equivocada. La partición no se elige: ocurre. La elección es entre C y A cuando hay partición, y el resto del tiempo se tienen las dos.
Error 2: confundir la C de CAP con la C de ACID. La C de CAP es "todos los nodos ven lo mismo". La C de ACID es "las reglas del esquema se cumplen al terminar la transacción". Misma letra, conceptos distintos.
Error 3: leer de un secundario justo después de escribir. Es la causa número uno de incidencias de tipo "el sistema ha perdido mi cambio". Solución: leer del primario tras escribir, o usar sesiones causalmente consistentes.
Error 4: usar w: 0 para datos que importan.
acknowledged: false significa literalmente que nadie ha confirmado nada. Solo es aceptable para datos que puedes permitirte perder.
Error 5: decidir con comparativas de 2013.
jsonb y las transacciones multidocumento cambiaron el terreno. Un argumento del tipo "MongoDB no tiene transacciones" o "PostgreSQL no puede con datos flexibles" está diez años desactualizado.
Error 6: adoptar persistencia políglota por elegancia. Cada motor adicional multiplica el coste de operación. Si dudas, no lo añadas: siempre puedes añadirlo después con datos que lo justifiquen.
Consejo 1: escribe la decisión, no solo la tomes. Un documento de media página —contexto, opciones consideradas, decisión, motivo, consecuencias aceptadas— por cada elección de motor. Dentro de dos años, cuando alguien pregunte "¿por qué MongoDB?", esa media página vale su peso en oro.
Consejo 2: ajusta la consistencia por operación, no por sistema.
Es la lección de PACELC llevada al código: w: "majority" para publicar una reseña, w: 0 para registrar un evento de navegación. La misma base de datos, dos garantías distintas, cada una justificada.
Consejo 3: prueba jsonb antes de añadir MongoDB.
Si ya tienes PostgreSQL y tu problema es solo la flexibilidad de esquema, una tarde de pruebas con jsonb y un índice GIN puede ahorrarte un motor entero.
Consejo 4: mide la latencia de replicación en producción. "Eventualmente consistente" no dice nada útil hasta que sabes que en tu sistema son 40 ms. Con ese número puedes decidir qué lecturas van al primario; sin él, solo puedes suponer.
Ejercicios
Ejercicio 1
Para cada una de estas cuatro operaciones de BiblioRed, indica: (a) qué writeConcern o readConcern usarías, (b) si la lectura debería ir al primario o puede ir a un secundario, y (c) si es un escenario donde priorizarías consistencia o disponibilidad ante una partición de red. Justifica cada respuesta.
- Registrar la devolución de un ejemplar con un recargo de 3,50 €.
- Mostrar la lista de reseñas de "Los pilares de la Tierra" a un visitante anónimo.
- Registrar que un socio ha abierto la ficha de un material.
- Publicar una reseña y, acto seguido, redirigir al lector a la página del material donde debe verla.
Ejercicio 2
Un compañero afirma: "Vamos a migrar todo BiblioRed a MongoDB porque NoSQL escala mejor y es más rápido, y así tenemos una sola base de datos." Redacta una respuesta técnica y respetuosa que aborde los tres argumentos —escalado, velocidad y unificación— con los conceptos de esta lección, y propón una alternativa concreta.
Ejercicio 3
BiblioRed quiere añadir listas de lectura personales: cada socio puede crear listas con nombre, descripción, visibilidad (privada/pública) y entre 5 y 200 materiales, y puede seguir listas públicas de otros socios. Consultas previstas: ver mis listas; ver una lista con los datos de sus materiales; buscar listas públicas por etiqueta; contar seguidores de una lista.
Decide dónde vive esta funcionalidad —PostgreSQL, PostgreSQL con jsonb, o MongoDB— recorriendo las ocho preguntas del apartado 10, y escribe el esquema o el documento resultante.
Soluciones
Solución 1
1. Devolución con recargo de 3,50 €.
(a) writeConcern: { w: "majority", j: true }. Hay dinero de por medio: la escritura debe estar confirmada por la mayoría y en disco, para que sobreviva a una caída del primario y no pueda revertirse. (b) Lectura del primario, con readConcern: "majority". (c) Consistencia. Ante partición, es preferible que el mostrador muestre "servicio no disponible, inténtelo en un minuto" a que registre un recargo que luego desaparezca o se duplique. Además, esta operación en realidad vive en PostgreSQL: toca prestamos y ejemplares de forma atómica y es un caso ACID de manual.
2. Lista de reseñas para un visitante anónimo.
(a) readConcern: "local". (b) Secundario, sin dudarlo: es la ruta de lectura más frecuente del portal y descargar el primario es exactamente para lo que existen los secundarios. (c) Disponibilidad. Que un visitante vea 811 reseñas en lugar de 812 durante unos milisegundos no tiene ninguna consecuencia; que la página del catálogo devuelva un error, sí.
3. Registrar la apertura de una ficha.
(a) writeConcern: { w: 0 }. Es un evento de telemetría entre 17 millones anuales; perder alguno no altera ningún informe y esperar confirmación penalizaría la latencia de la navegación (requisito C6: menos de 10 ms). (b) No aplica: es escritura pura. (c) Disponibilidad, absolutamente. Ante partición, lo correcto es descartar el evento en silencio antes que degradar la experiencia de navegación o encolar datos indefinidamente.
4. Publicar una reseña y redirigir a la página del material.
(a) Escritura con writeConcern: { w: "majority" } —el usuario cree que la ha publicado y no puede desaparecer— y lectura posterior con readConcern: "majority". (b) Primario, o dentro de una sesión causalmente consistente. Es el caso canónico de lee tus propias escrituras: si la lectura posterior cae en un secundario retrasado, Marta no ve su propia reseña, cree que se ha perdido y probablemente la publica otra vez. (c) Consistencia, pero por una razón de percepción más que de corrección: el coste de un usuario que duplica su contenido es mayor que el de un mensaje de error momentáneo.
Lo que ilustra el conjunto: cuatro operaciones del mismo sistema con cuatro configuraciones distintas. Es la aplicación práctica de PACELC.
Solución 2
Gracias por plantearlo; vale la pena analizar los tres argumentos por separado, porque creo que uno de ellos apunta a algo real y los otros dos no.
Sobre el escalado. Es cierto que MongoDB escala mejor en horizontal. La pregunta es si lo necesitamos: BiblioRed tiene 12.000 socios, 40.000 ejemplares y unos 17 millones de eventos de actividad al año. Eso cabe holgadamente en una sola instancia de PostgreSQL con hardware corriente, y de hecho nuestro cuello de botella actual no es el volumen. Migrar por un escalado que no vamos a usar nos deja el coste de la migración sin la ventaja.
Sobre la velocidad. "Más rápido" depende de la operación. MongoDB es más rápido leyendo un agregado completo por su clave; PostgreSQL es más rápido agregando datos normalizados, que es exactamente lo que hacen nuestros informes mensuales de préstamos por sucursal. Además, la mayoría de los problemas de rendimiento que hemos tenido se resolvieron con un índice. Antes de cambiar de motor, propongo medir con
EXPLAINlas tres consultas más lentas: si el problema es de índices, la migración no lo arregla.Sobre unificar en una sola base. Este es el argumento con más fuerza: dos motores cuestan el doble de copias de seguridad, supervisión y competencia en el equipo. Pero unificar en MongoDB nos costaría algo más caro: perderíamos la integridad referencial de los préstamos y las transacciones ACID que garantizan que un préstamo y el estado de su ejemplar cambien de una pieza. Eso hoy nos lo da el servidor gratis; en MongoDB tendríamos que escribirlo y mantenerlo nosotros, y cada error que se nos escape acaba en el mostrador.
Propuesta alternativa. Si el objetivo real es simplificar, exploremos el camino inverso: mover el catálogo y las reseñas a PostgreSQL usando columnas
jsonbcon índices GIN. Nos daría la flexibilidad de esquema que buscábamos —metadatos distintos por tipo de material, campos nuevos sinALTER TABLE— sin renunciar aJOIN, transacciones ni integridad, y con una sola base de datos, que es lo que querías. Propongo una prueba de concepto de una semana con los datos reales del catálogo y una comparación medida de las cinco consultas más frecuentes. Sijsonbno da la talla, tendremos un argumento sólido para mantener MongoDB, escrito y con números.
Solución 3
Recorrido de las ocho preguntas
| # | Pregunta | Respuesta para las listas de lectura |
|---|---|---|
| 1 | ¿Cómo son los datos? | Homogéneos: todas las listas tienen la misma estructura. No es un caso de heterogeneidad |
| 2 | ¿Conozco mis consultas? | Sí, las cuatro están enumeradas |
| 3 | ¿Transacciones entre entidades? | Muy poco: añadir un material a una lista afecta a la lista y a un contador |
| 4 | ¿Integridad? | Media-alta: una lista que apunta a materiales inexistentes se ve fatal en el portal |
| 5 | ¿Volumen? | 12.000 socios × pocas listas × hasta 200 materiales. Pequeño |
| 6 | ¿Cambia el esquema? | Poco: es una funcionalidad de forma estable |
| 7 | ¿Qué opera el equipo? | Los dos motores; no desempata |
| 8 | ¿Informes? | Alguno: listas más seguidas, materiales más listados |
Decisión: MongoDB. Y no por las preguntas 1 y 6, que apuntaban a relacional, sino por el agregado. Una lista de lectura cumple las tres condiciones de la lección 03-03: se lee entera (la consulta principal es "ver una lista con sus materiales"), se escribe junta, y tiene una raíz clara. Con un tope de 200 materiales es un caso de uno a pocos de manual, y con referencia extendida (título, tipo y portada) la consulta principal se resuelve en un solo acceso, sin JOIN con catalogo.
El argumento decisivo es la pregunta 4 combinada con el patrón: la integridad importa, pero es integridad de visualización, no de negocio. Si un material se retira del catálogo y queda en una lista, la ficha muestra "material no disponible" y no pasa nada grave. Eso no exige una FOREIGN KEY.
Los seguidores van fuera: crecen sin tope conocido (una lista popular puede tener miles) y se consultan por separado ("listas que sigo"). Es uno a muchos, así que colección propia con un contador calculado en la lista.
{
"_id": "LST-00471",
"esquema_v": 1,
"propietario": {
"socio_id": 14,
"nombre_mostrado": "Marta Alsina",
"sucursal_id": 1
},
"nombre": "Novela histórica para el verano",
"descripcion": "Lo que me llevo a la playa este año. Nada por debajo de las 400 páginas.",
"visibilidad": "publica",
"etiquetas": ["novela histórica", "verano", "recomendaciones"],
"materiales": [
{ "material_id": "MAT-0331", "titulo": "El mapa del tiempo",
"tipo": "libro", "portada": "/img/catalogo/0331-s.webp",
"anadido": "2026-06-02T18:20:00Z", "nota": "Empezar por este." },
{ "material_id": "MAT-0412", "titulo": "Los pilares de la Tierra",
"tipo": "libro", "portada": "/img/catalogo/0412-s.webp",
"anadido": "2026-06-02T18:22:00Z", "nota": null },
{ "material_id": "MAT-0801", "titulo": "El mapa del tiempo",
"tipo": "audiolibro", "portada": "/img/catalogo/0801-s.webp",
"anadido": "2026-06-14T09:05:00Z", "nota": "Versión narrada, para el coche." }
],
"num_materiales": 3,
"num_seguidores": 47,
"creada": "2026-06-02T18:18:00Z",
"actualizada": "2026-06-14T09:05:00Z"
}{
"_id": "SEG-LST-00471-16",
"lista_id": "LST-00471",
"socio_id": 16,
"nombre_mostrado": "Nuria Bastos",
"desde": "2026-06-20T11:40:00Z",
"avisos": true
}Índices: { "propietario.socio_id": 1, actualizada: -1 } para "mis listas"; { visibilidad: 1, etiquetas: 1, num_seguidores: -1 } para la búsqueda de listas públicas; y en seguidores_lista, { socio_id: 1 } y { lista_id: 1 }.
Validación ($jsonSchema): maxItems: 200 en materiales —convierte el tope de diseño en regla comprobada por el servidor— y enum: ["privada","publica"] en visibilidad.
Nota final de honestidad. Esta funcionalidad también se resolvería perfectamente en PostgreSQL con dos tablas y una clave ajena, con la ventaja añadida de que la integridad la garantizaría el servidor. La decisión se inclina hacia MongoDB porque el catálogo ya está ahí —la referencia extendida se resuelve dentro del mismo motor— y porque el patrón de agregado encaja de forma natural. Si el catálogo viviera en PostgreSQL, la respuesta correcta sería la contraria. La decisión de un subsistema depende de dónde vive el resto del sistema, y ese es un criterio que ninguna tabla comparativa recoge.
Conclusión
Esta lección ha puesto las dos mitades del curso frente a frente y ha cerrado la teoría pendiente:
- La comparación dimensión por dimensión muestra que no hay ganador: hay equilibrios distintos. Las dos filas más infravaloradas son el talento disponible y la capacidad de responder a consultas imprevistas, ambas a favor del modelo relacional.
- El teorema CAP no es "elige dos de tres". La partición no se elige, ocurre; la elección real es entre consistencia y disponibilidad cuando hay partición, y mientras no la hay se tienen las dos. MongoDB es CP, Cassandra es AP por defecto y ajustable.
- PACELC añade lo que falta: incluso sin partición hay que elegir entre latencia y consistencia, porque sincronizar réplicas cuesta tiempo. La consistencia no es gratis ni cuando todo funciona.
- ACID garantiza corrección inmediata; BASE —básicamente disponible, estado blando, eventualmente consistente— garantiza disponibilidad y convergencia. No son etiquetas de producto sino de operación: la misma base puede ofrecer las dos.
- La consistencia eventual significa, en concreto, que durante unas decenas de milisegundos dos usuarios pueden ver realidades distintas. Inofensivo casi siempre, salvo en lee tus propias escrituras, que se resuelve leyendo del primario o con sesiones causalmente consistentes.
- La consistencia es ajustable por operación: la regla del quórum
W + R > N,writeConcern(w: 0,w: 1,w: "majority",j: true,wtimeout) yreadConcern(local,majority,linearizable,snapshot). - La frontera se ha difuminado. PostgreSQL guarda documentos con
jsonb, los indexa con GIN y los consulta con@>,->>yjsonb_set, sin renunciar aJOIN, transacciones ni integridad. MongoDB tiene transacciones multidocumento desde 2018, con la advertencia de que son una red de seguridad y no la herramienta habitual. Decidir con argumentos de 2013 es la forma más común de equivocarse. - La guía de decisión son ocho preguntas —forma de los datos, previsibilidad de las consultas, transacciones, integridad, volumen, velocidad de cambio del esquema, competencia del equipo y necesidades analíticas— y un consejo por defecto: empieza por la relacional y sé capaz de escribir en una frase el motivo de no hacerlo.
- Seis mitos desmontados: NoSQL no es "más rápido" en abstracto, sí tiene esquema (sin vigilancia), las relacionales sí escalan (en vertical), MongoDB no pierde datos con la configuración adecuada, con NoSQL hay que diseñar más, y no hay que elegir un solo motor.
- La persistencia políglota es usar deliberadamente varios motores, cada uno para lo que gestiona mejor. Tiene un precio real —coherencia entre motores, ausencia de transacciones cruzadas, doble operación, doble competencia, informes que cruzan sistemas— y por eso cada motor añadido debe justificar su coste con una ventaja que no se pueda conseguir de otro modo.
Con esto termina el módulo 3. Hemos entendido por qué nació NoSQL y qué renuncia a cambio de qué, hemos recorrido sus cuatro familias con operación real, hemos aprendido a diseñar documentos a partir de las consultas —con sus patrones, sus anti-patrones y sus garantías recuperables— y hemos comparado los dos mundos con los conceptos que gobiernan cualquier sistema distribuido. BiblioRed ha salido de aquí con una arquitectura completa y, sobre todo, justificada: PostgreSQL para lo que exige integridad y transacciones, MongoDB para lo que exige flexibilidad y volumen, y tres motores más conscientemente aplazados con un criterio escrito de cuándo dejarían de estarlo. En el módulo 4, Diseño de Esquemas, volvemos al principio de todo con una mirada más exigente: antes de escribir un CREATE TABLE o un documento, hay que diseñar. Veremos los principios del buen diseño de esquemas, los diagramas entidad-relación como lenguaje para pensar un dominio antes de tocar el teclado, cómo se transforman esos diagramas en esquemas relacionales, y cómo se eligen los tipos de datos y las restricciones que harán que el esquema resista el paso del tiempo.
Fundamentos de Bases de Datos
Módulo 1: Introducción a las Bases de Datos
- Conceptos Básicos de Bases de Datos
- Tipos de Bases de Datos
- Historia y Evolución de las Bases de Datos
- Sistemas Gestores de Bases de Datos y Arquitectura
Módulo 2: Bases de Datos Relacionales
- Modelo Relacional
- Lenguaje SQL
- Operaciones Básicas en SQL
- Consultas Multitabla: JOIN y Subconsultas
- Agregación y Agrupación de Datos
- Integridad Referencial
Módulo 3: Bases de Datos No Relacionales
- Introducción a NoSQL
- Tipos de Bases de Datos NoSQL
- Modelado de Datos en NoSQL
- Comparación entre Bases de Datos Relacionales y No Relacionales
Módulo 4: Diseño de Esquemas
- Principios de Diseño de Esquemas
- Diagramas Entidad-Relación (ER)
- Transformación de Diagramas ER a Esquemas Relacionales
- Tipos de Datos y Restricciones
Módulo 5: Normalización
Módulo 6: Transacciones, Rendimiento y Seguridad
- Transacciones y Propiedades ACID
- Concurrencia y Niveles de Aislamiento
- Índices y Optimización de Consultas
- Seguridad, Permisos y Copias de Seguridad
Módulo 7: Ejercicios Prácticos
- Ejercicios de SQL
- Ejercicios de Diseño de Esquemas
- Ejercicios de Normalización
- Ejercicios de Consultas Avanzadas y Transacciones
Módulo 8: Casos de Estudio
- Caso de Estudio: Base de Datos Relacional
- Caso de Estudio: Base de Datos No Relacional
- Caso de Estudio: Persistencia Políglota
