En la lección 01-02 vimos las cuatro familias NoSQL desde muy arriba: qué son y para qué sirven, en un par de párrafos cada una. En la lección anterior entendimos el porqué del movimiento y dimos los primeros pasos con MongoDB. Ahora toca bajar al suelo.
"NoSQL" no es una tecnología: es una etiqueta que agrupa cuatro modelos de datos radicalmente distintos entre sí. Una base documental y una base de grafos se parecen tanto como una hoja de cálculo y un mapa de metro. Decir "vamos a usar NoSQL" es tan poco informativo como decir "vamos a usar un vehículo": la pregunta útil es cuál, y para qué.
En esta lección recorremos las cuatro familias con operación real y ejecutable: documental con MongoDB, clave-valor con Redis, columnar con Cassandra y grafos con Neo4j. De cada una veremos su modelo de datos, cómo se consulta, qué productos la representan, sus fortalezas, sus límites y —lo que hilará toda la lección— qué pieza de BiblioRed le encaja. Al final, una tabla comparativa y un árbol de decisión para elegir.
Contenido
- Las cuatro familias de un vistazo
- Bases documentales: MongoDB
- Bases clave-valor: Redis
- Bases columnares: Cassandra
- Bases de grafos: Neo4j
- Familias especializadas que este curso no vuelve a tratar
- Tabla comparativa de las cuatro familias
- Árbol de decisión
- El reparto final de BiblioRed
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Las cuatro familias de un vistazo
Las cuatro se pueden ordenar por la riqueza de la estructura que entienden: desde la que no entiende nada del valor que guarda hasta la que entiende las relaciones entre entidades.
| Familia | Unidad de datos | Qué "entiende" el motor | Consulta típica |
|---|---|---|---|
| Clave-valor | Par clave → valor |
Solo la clave. El valor es opaco | "Dame el valor de esta clave" |
| Documental | Documento tipo JSON | Toda la estructura interna del documento | "Dame los documentos donde puntuacion > 4" |
| Columnar | Fila ancha en una familia de columnas | La clave de partición y el orden dentro de ella | "Dame las filas de esta partición entre estas dos fechas" |
| Grafos | Nodo + relación + propiedades | Las conexiones entre entidades | "Dame lo que leyeron quienes leyeron esto" |
Y las cuatro piezas de BiblioRed que buscan acomodo:
- Reseñas y catálogo enriquecido → estructura rica, heterogénea, consultada por contenido → documental.
- Portadas y sesiones del portal → acceso por clave, muy frecuente, con caducidad → clave-valor.
- Registro de actividad → altísimo volumen de escritura, consulta por socio y rango de fechas → columnar.
- Motor de recomendaciones → recorrer relaciones entre lectores y títulos → grafos.
Una advertencia antes de empezar, para que esta lección no se lea mal: BiblioRed no va a desplegar cuatro bases de datos. Va a desplegar PostgreSQL y MongoDB. Redis, Cassandra y Neo4j aparecen aquí porque son la mejor forma de entender qué resuelve cada familia y porque el día que BiblioRed crezca, sabrá reconocer el momento. Confundir "entender una familia" con "instalarla" es el camino más rápido a una arquitectura ingobernable.
- Bases documentales: MongoDB
2.1 Modelo de datos
La unidad es el documento: una estructura tipo JSON —guardada en BSON, como vimos en 03-01— que admite anidamiento y arrays. Los documentos viven en colecciones, que no imponen esquema.
Tres propiedades que definen la familia:
- El documento es autocontenido. Todo lo que la aplicación necesita para mostrar una entidad cabe dentro.
- El motor entiende el interior. Puede filtrar, ordenar, indexar y agregar por cualquier campo, incluso anidado o dentro de un array. Esto es lo que separa a una base documental de una clave-valor que guardase JSON: para la segunda, el valor es una bolsa de bytes opaca.
- La escritura de un documento es atómica, sin necesidad de transacción.
Productos representativos: MongoDB (el dominante y nuestra referencia), Couchbase, Amazon DocumentDB, RavenDB, Firestore. Y una nota importante: PostgreSQL con el tipo jsonb hace muchas de estas cosas sin dejar de ser relacional; lo compararemos con detalle en la lección 03-04.
2.2 Operadores de consulta
Seguimos sobre la colección resenas que creamos en la lección anterior. Un filtro es un documento; cada campo es una condición y varios campos se combinan con Y lógico. Cuando la condición no es una igualdad, el valor pasa a ser un documento con un operador que empieza por $.
| Operador | Significado | Equivalente SQL |
|---|---|---|
$eq |
Igual (implícito si escribes el valor directo) | = |
$ne |
Distinto | <> |
$gt / $gte |
Mayor / mayor o igual | > / >= |
$lt / $lte |
Menor / menor o igual | < / <= |
$in / $nin |
Está / no está en la lista | IN / NOT IN |
$exists |
El campo existe (o no) en el documento | sin equivalente directo |
$regex |
Coincide con una expresión regular | LIKE / ~ |
$and / $or / $not |
Combinaciones lógicas | AND / OR / NOT |
$size |
El array tiene N elementos | sin equivalente directo |
$all |
El array contiene todos estos elementos | requiere varios EXISTS |
[
{ _id: ObjectId('...c82'), titulo_libro: 'El mapa del tiempo', puntuacion: 5, socio: { socio_id: 14, nombre: 'Marta Alsina' }, ... },
{ _id: ObjectId('...c84'), titulo_libro: 'Los pilares de la Tierra', puntuacion: 5, socio: { socio_id: 16, nombre: 'Nuria Bastos' }, ... },
{ _id: ObjectId('...c85'), titulo_libro: 'Los pilares de la Tierra', puntuacion: 4, socio: { socio_id: 14, nombre: 'Marta Alsina' }, ... }
][
{ _id: ObjectId('...c83'), socio: { socio_id: 15, nombre: 'Iván Pereda' }, puntuacion: 3, ... },
{ _id: ObjectId('...c84'), socio: { socio_id: 16, nombre: 'Nuria Bastos' }, puntuacion: 5, ... }
]// Búsqueda por texto en el cuerpo de la reseña, sin distinguir mayúsculas
db.resenas.find({ texto: { $regex: "catedral", $options: "i" } })[
{ _id: ObjectId('...c84'), titulo_libro: 'Los pilares de la Tierra',
texto: 'Mil páginas que se pasan volando. La construcción de la catedral engancha.', ... }
]// Reseñas que tienen respuesta del bibliotecario
db.resenas.find({ respuesta_bibliotecario: { $exists: true } })[
{ _id: ObjectId('...c84'), respuesta_bibliotecario: { nombre: 'Equipo Sucursal Sur', ... }, ... }
]$exists no tiene equivalente en SQL, y por una razón de fondo: en una tabla, la columna existe siempre; como mucho vale NULL. En una colección, un campo puede sencillamente no estar. Es la diferencia entre "no tiene valor" y "esta entidad no tiene ese concepto", y en el mundo documental son dos cosas distintas.
// Reseñas con AMBAS etiquetas a la vez
db.resenas.find({ etiquetas: { $all: ["novela histórica", "para regalar"] } })[
{ _id: ObjectId('...c84'), etiquetas: [ 'novela histórica', 'para regalar', 'clásico moderno' ], ... }
]// Combinación con O lógico: muy buenas o muy votadas
db.resenas.find({ $or: [ { puntuacion: 5 }, { votos_utiles: { $gte: 10 } } ] })[
{ _id: ObjectId('...c82'), puntuacion: 5, votos_utiles: 7, ... },
{ _id: ObjectId('...c84'), puntuacion: 5, votos_utiles: 12, ... }
]2.3 Proyección, orden y límite
El segundo argumento de find es la proyección: qué campos devolver. 1 incluye, 0 excluye.
db.resenas.find(
{ libro_id: 412 },
{ _id: 0, "socio.nombre": 1, puntuacion: 1, votos_utiles: 1 }
).sort({ votos_utiles: -1 }).limit(2)[
{ socio: { nombre: 'Nuria Bastos' }, puntuacion: 5, votos_utiles: 12 },
{ socio: { nombre: 'Marta Alsina' }, puntuacion: 4, votos_utiles: 4 }
]-- Equivalente relacional
SELECT s.nombre, r.puntuacion, r.votos_utiles
FROM resenas r INNER JOIN socios s ON s.socio_id = r.socio_id
WHERE r.libro_id = 412
ORDER BY r.votos_utiles DESC
LIMIT 2;Reglas de la proyección que conviene memorizar:
- No se pueden mezclar inclusiones y exclusiones en la misma proyección, con una única excepción:
_id: 0se puede combinar con inclusiones. - El
_idse devuelve siempre salvo que lo excluyas explícitamente. .sort(),.limit()y.skip()se encadenan sobre el cursor y equivalen aORDER BY,LIMITyOFFSET. Ensort,1es ascendente y-1descendente.
2.4 Actualizaciones: $set, $push, $inc
Aquí aparece una diferencia grande respecto a SQL: en MongoDB no se reescribe el documento entero, se aplican operadores de actualización sobre partes de él.
| Operador | Qué hace |
|---|---|
$set |
Asigna un valor a un campo (lo crea si no existe) |
$unset |
Elimina un campo del documento |
$inc |
Incrementa (o decrementa, con negativo) un número |
$push |
Añade un elemento a un array |
$addToSet |
Añade a un array solo si no está ya |
$pull |
Elimina de un array los elementos que cumplan una condición |
$currentDate |
Pone la fecha actual del servidor |
// Un lector marca como útil la reseña de Nuria: contador +1
db.resenas.updateOne(
{ _id: ObjectId('66ab15c15c9e1b2f3d4a6c84') },
{ $inc: { votos_utiles: 1 } }
)// Añadir una etiqueta sin duplicar y registrar la edición
db.resenas.updateOne(
{ _id: ObjectId('66ab15c15c9e1b2f3d4a6c84') },
{
$addToSet: { etiquetas: "edad media" },
$currentDate: { editada: true }
}
)
db.resenas.findOne(
{ _id: ObjectId('66ab15c15c9e1b2f3d4a6c84') },
{ _id: 0, etiquetas: 1, votos_utiles: 1, editada: 1 }
){
etiquetas: [ 'novela histórica', 'para regalar', 'clásico moderno', 'edad media' ],
votos_utiles: 13,
editada: ISODate('2026-08-02T09:41:07.512Z')
}Detente un segundo en lo que acaba de pasar. El campo editada no existía en ningún documento de la colección y ahora existe en uno. No ha hecho falta un ALTER TABLE, no ha habido bloqueo y los otros tres documentos no se han enterado. Eso es el esquema flexible en su forma más práctica.
// Añadir un comentario al array de respuestas de una reseña
db.resenas.updateOne(
{ _id: ObjectId('66ab14b25c9e1b2f3d4a6c82') },
{
$push: {
comentarios: {
socio_id: 15,
nombre: "Iván Pereda",
texto: "Coincido, el final es lo mejor.",
fecha: ISODate("2026-04-05T17:30:00Z")
}
}
}
)// updateMany: normalizar una etiqueta en toda la colección
db.resenas.updateMany(
{ etiquetas: "novela histórica" },
{ $set: { "etiquetas.$": "novela-historica" } }
)El operador posicional $ se refiere al primer elemento del array que ha coincidido con el filtro. Es la forma de modificar un elemento concreto sin reescribir el array entero.
Cuidado con un detalle que ha causado incidentes reales: deleteMany({}) con filtro vacío borra la colección entera y no pide confirmación. En SQL, DELETE FROM tabla hace lo mismo, pero al menos hay una transacción que se puede deshacer con ROLLBACK. Aquí, salvo que estés dentro de una transacción explícita, no la hay.
2.5 La primera aggregation pipeline
Las consultas de resumen —lo que en la lección 02-05 hicimos con GROUP BY— se hacen en MongoDB con el marco de agregación: una tubería de etapas, donde cada etapa recibe documentos, los transforma y se los pasa a la siguiente.
Objetivo: la puntuación media y el número de reseñas de cada libro, ordenado de mejor a peor.
-- Lo que haríamos en PostgreSQL (lección 02-05)
SELECT libro_id, COUNT(*) AS total, ROUND(AVG(puntuacion), 2) AS media
FROM resenas
GROUP BY libro_id
HAVING COUNT(*) >= 1
ORDER BY media DESC;db.resenas.aggregate([
{ $match: { spoiler: false } },
{ $group: {
_id: "$libro_id",
titulo: { $first: "$titulo_libro" },
total: { $sum: 1 },
media: { $avg: "$puntuacion" },
votos: { $sum: "$votos_utiles" }
}},
{ $sort: { media: -1 } }
])[
{ _id: 412, titulo: 'Los pilares de la Tierra', total: 1, media: 5, votos: 13 },
{ _id: 331, titulo: 'El mapa del tiempo', total: 2, media: 4, votos: 9 }
]Traducción etapa por etapa, que es la mejor forma de entenderlo:
| Etapa de la tubería | Equivalente SQL | Qué hace aquí |
|---|---|---|
$match |
WHERE |
Se queda solo con las reseñas sin spoiler |
$group |
GROUP BY |
Agrupa por libro_id y calcula los acumulados |
$sort |
ORDER BY |
Ordena por media descendente |
$project |
Lista del SELECT |
Elige y calcula los campos de salida |
$limit |
LIMIT |
Corta el resultado |
$unwind |
(no tiene equivalente) | Convierte cada elemento de un array en un documento propio |
$lookup |
LEFT JOIN |
Trae documentos de otra colección |
Dos convenciones de sintaxis que hay que fijar:
"$campo"con dólar delante significa "el valor de ese campo", no la cadena literal."$puntuacion"es el número;"puntuacion"sería el texto.- En
$group,_ides la clave de agrupación, no un identificador._id: "$libro_id"es literalmente elGROUP BY libro_id. Si pones_id: null, agrupas todo en una sola fila, que es elSELECT AVG(...) FROM tablasinGROUP BY.
Un ejemplo más, con $unwind, que no tiene contrapartida limpia en SQL: las etiquetas más usadas.
db.resenas.aggregate([
{ $unwind: "$etiquetas" },
{ $group: { _id: "$etiquetas", usos: { $sum: 1 } } },
{ $sort: { usos: -1, _id: 1 } },
{ $limit: 5 }
])[
{ _id: 'novela-historica', usos: 3 },
{ _id: 'clásico moderno', usos: 1 },
{ _id: 'edad media', usos: 1 },
{ _id: 'para regalar', usos: 1 }
]$unwind ha "desplegado" la reseña de Nuria, que tenía cuatro etiquetas, en cuatro documentos idénticos salvo por la etiqueta. Después, $group cuenta. Es exactamente el trabajo que en el modelo relacional haría la tabla intermedia resenas_etiquetas que evitamos al embeber el array.
2.6 Fortalezas, límites y encaje en BiblioRed
Fortalezas
- Estructuras heterogéneas y anidadas sin tablas auxiliares ni columnas nulas.
- El esquema evoluciona sin migración.
- Lectura de una entidad completa en un solo acceso.
- Lenguaje de consulta rico: filtros, agregación, índices sobre cualquier campo.
Límites
- Cruzar colecciones (
$lookup) es caro y no debe ser la norma. - Sin integridad referencial declarativa.
- El límite de 16 MB por documento acota lo que puedes embeber.
- Las transacciones multidocumento existen, pero cuestan rendimiento; el diseño debe minimizar su necesidad.
Encaje en BiblioRed: las reseñas y el catálogo enriquecido. Son entidades autocontenidas, heterogéneas, que se leen enteras y cambian de forma con frecuencia. Es el encaje perfecto, y por eso MongoDB es la base NoSQL que BiblioRed va a desplegar de verdad.
- Bases clave-valor: Redis
3.1 Modelo de datos
El modelo más simple que existe: un diccionario gigante y distribuido. Una clave, un valor. El motor no sabe nada del contenido del valor —para él es una cadena o una estructura, pero no algo que se pueda filtrar por su interior—.
Esa renuncia radical compra una cosa: velocidad. Redis mantiene todos los datos en memoria y responde en microsegundos, con cifras habituales de más de 100.000 operaciones por segundo en una máquina modesta.
Productos representativos: Redis (nuestra referencia), Valkey (su bifurcación de código abierto), Memcached (más simple aún), Amazon DynamoDB (clave-valor con capacidades documentales), etcd (configuración de clústeres).
3.2 Operaciones básicas y caducidad
# Arrancar un Redis en Docker y entrar a su cliente
docker run -d --name redis-biblioRed -p 6379:6379 redis:7
docker exec -it redis-biblioRed redis-cli// Guardar y recuperar la ficha lista para pintar de un material del catálogo
SET catalogo:MAT-0331:ficha "{\"titulo\":\"El mapa del tiempo\",\"portada\":\"/img/0331.webp\"}"
GET catalogo:MAT-0331:fichaLa operación estrella de esta familia es la caducidad, y merece su propio apartado porque es lo que la diferencia de todo lo demás:
// Guardar con caducidad de 1 hora (3600 segundos)
SET catalogo:MAT-0331:ficha "{...}" EX 3600
// Consultar cuánto le queda de vida
TTL catalogo:MAT-0331:ficha// Sesión del portal: caduca sola a los 30 minutos de inactividad
SET sesion:8f3a2b1c "{\"socio_id\":14,\"nombre\":\"Marta Alsina\",\"sucursal\":1}" EX 1800
// Cada petición del socio renueva la ventana
EXPIRE sesion:8f3a2b1c 1800
TTL sesion:8f3a2b1cPor qué la caducidad es una función de primera clase. En una base relacional, "borrar lo que haya caducado" es una tarea programada que alguien tiene que escribir, vigilar y ejecutar; y mientras no se ejecuta, los datos caducados siguen ocupando espacio y ensuciando consultas. En Redis, la caducidad es una propiedad de la clave: el sistema la elimina solo, sin intervención. Para una caché —donde el dato antiguo no es un error, solo es inútil— y para sesiones —donde el dato debe desaparecer por seguridad—, esa diferencia es la razón misma de elegir la herramienta.
3.3 Estructuras de datos
Redis no es solo cadenas. Su verdadero valor son las estructuras nativas, cada una con operaciones atómicas propias.
| Estructura | Qué es | Uso en BiblioRed |
|---|---|---|
| String | Cadena o número | Ficha del catálogo en caché, contadores |
| List | Lista ordenada, con acceso por los extremos | Cola de trabajos: portadas pendientes de redimensionar |
| Set | Conjunto sin orden ni duplicados | Materiales disponibles ahora mismo en la sucursal Norte |
| Sorted Set | Conjunto ordenado por puntuación | Ranking de los más consultados de la semana |
| Hash | Diccionario de campos dentro de una clave | Datos de la sesión, campo a campo |
| Counter | Un String con operaciones atómicas |
Consultas de hoy sobre un material |
// HASH: la sesión, campo a campo, sin reescribir todo el objeto
HSET sesion:8f3a2b1c socio_id 14 nombre "Marta Alsina" sucursal 1 idioma es
HGET sesion:8f3a2b1c nombre
HGETALL sesion:8f3a2b1c(integer) 4
"Marta Alsina"
1) "socio_id"
2) "14"
3) "nombre"
4) "Marta Alsina"
5) "sucursal"
6) "1"
7) "idioma"
8) "es"// CONTADOR: incremento atómico, sin condiciones de carrera
INCR consultas:MAT-0331:2026-08-02
INCR consultas:MAT-0331:2026-08-02
INCR consultas:MAT-0331:2026-08-02
GET consultas:MAT-0331:2026-08-02INCR es atómico: si mil socios abren la ficha a la vez, el contador acaba en mil exactos. Hacer esto en SQL requiere UPDATE ... SET n = n + 1 con su bloqueo de fila y su coste transaccional; aquí es una operación de microsegundos.
// SORTED SET: ranking de materiales más consultados de la semana
ZINCRBY ranking:semana:31 3 "MAT-0331"
ZINCRBY ranking:semana:31 8 "MAT-0412"
ZINCRBY ranking:semana:31 5 "MAT-0801"
ZREVRANGE ranking:semana:31 0 2 WITHSCORESUn ranking siempre ordenado, actualizado en el momento y consultable en tiempo constante. La alternativa relacional es un ORDER BY COUNT(*) DESC sobre millones de filas cada vez que alguien mira la página de inicio.
// SET: qué materiales están disponibles ahora en la sucursal Norte
SADD disponibles:sucursal:2 "EJ-3081" "EJ-3084" "EJ-3090"
SISMEMBER disponibles:sucursal:2 "EJ-3084"
SCARD disponibles:sucursal:2
SREM disponibles:sucursal:2 "EJ-3084"
SCARD disponibles:sucursal:23.4 Fortalezas, límites y encaje en BiblioRed
Fortalezas: latencia de microsegundos, operaciones atómicas sobre estructuras, caducidad nativa, modelo trivial de entender y operar.
Límites: solo se consulta por clave (no hay "dame todo lo que tenga puntuación 5"); los datos viven en memoria, así que la RAM es el techo y el coste; la persistencia existe (instantáneas y registro de operaciones) pero está pensada para recuperación, no como almacén principal.
Encaje en BiblioRed: caché de las fichas y portadas del catálogo —evita ir a MongoDB en cada visita a una página muy visitada— y sesiones del portal —que deben caducar solas—. Nunca como fuente de verdad: si Redis se vacía, BiblioRed debe seguir funcionando, solo que más despacio. Esa es la prueba del algodón de una caché bien planteada.
- Bases columnares: Cassandra
4.1 Modelo de datos
El nombre "columnar" despista. Aquí no significa "orientada a columnas para análisis" (eso son ClickHouse o Amazon Redshift), sino familias de columnas: filas identificadas por una clave, agrupadas físicamente, donde cada fila puede tener columnas distintas y muy numerosas.
Las dos piezas que hay que entender son:
- Clave de partición: decide en qué nodo vive la fila. Todas las filas con la misma clave de partición están juntas en el mismo nodo y contiguas en disco.
- Clave de agrupamiento (clustering key): decide el orden de las filas dentro de la partición.
flowchart TD
subgraph N1["Nodo 1"]
P1["Partición socio_id=14<br/>ordenada por fecha DESC<br/>→ 8.400 eventos contiguos"]
end
subgraph N2["Nodo 2"]
P2["Partición socio_id=15<br/>ordenada por fecha DESC<br/>→ 6.100 eventos contiguos"]
end
subgraph N3["Nodo 3"]
P3["Partición socio_id=16<br/>ordenada por fecha DESC<br/>→ 9.700 eventos contiguos"]
end
Q["Consulta:<br/>actividad del socio 15<br/>del último mes"] --> N2
Esa contigüidad física es todo el secreto: leer "los 50 últimos eventos del socio 15" es una lectura secuencial de disco dentro de una sola partición en un solo nodo. No hay búsqueda dispersa, no hay JOIN, no hay coordinación entre máquinas.
Productos representativos: Apache Cassandra (nuestra referencia), ScyllaDB (compatible y más rápida), Apache HBase, Google Bigtable (el artículo de 2006 que originó la familia), Amazon Keyspaces.
4.2 CQL: parecido a SQL, reglas distintas
Cassandra se consulta con CQL, que se escribe casi como SQL. Esa familiaridad es una trampa amable: la sintaxis es parecida, las reglas no.
docker run -d --name cassandra-biblioRed -p 9042:9042 cassandra:5
docker exec -it cassandra-biblioRed cqlsh-- Un "keyspace" es el equivalente aproximado de una base de datos
CREATE KEYSPACE bibliored
WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 3 };
USE bibliored;
-- La tabla de actividad del catálogo
CREATE TABLE actividad_por_socio (
socio_id int,
fecha_evento timestamp,
evento_id uuid,
tipo text,
termino text,
material_id text,
sucursal_id int,
PRIMARY KEY ((socio_id), fecha_evento, evento_id)
) WITH CLUSTERING ORDER BY (fecha_evento DESC, evento_id ASC);Lee con cuidado esa clave primaria, porque contiene toda la lección:
(socio_id), entre paréntesis propios, es la clave de partición. Todos los eventos de un socio viven juntos.fecha_eventoyevento_idson las claves de agrupamiento: ordenan dentro de la partición, de más reciente a más antiguo.evento_idestá ahí para desempatar: sin él, dos eventos en el mismo milisegundo se sobrescribirían.
INSERT INTO actividad_por_socio (socio_id, fecha_evento, evento_id, tipo, termino, sucursal_id)
VALUES (15, '2026-08-02 09:14:02', uuid(), 'busqueda', 'julio verne', 2);
INSERT INTO actividad_por_socio (socio_id, fecha_evento, evento_id, tipo, material_id, sucursal_id)
VALUES (15, '2026-08-02 09:14:31', uuid(), 'ficha', 'MAT-0331', 2);
SELECT fecha_evento, tipo, termino, material_id
FROM actividad_por_socio
WHERE socio_id = 15
LIMIT 5; fecha_evento | tipo | termino | material_id
---------------------------------+----------+-------------+-------------
2026-08-02 09:14:31.000000+0000 | ficha | null | MAT-0331
2026-08-02 09:14:02.000000+0000 | busqueda | julio verne | null
(2 rows)Ahora, las reglas que rompen las expectativas de quien viene de SQL:
InvalidRequest: Error from server: code=2200 [Invalid query]
message="Cannot execute this query as it might involve data filtering and thus
may have unpredictable performance. If you want to execute this query despite the
performance unpredictability, use ALLOW FILTERING"Cassandra se niega a ejecutar una consulta que no pueda resolver de forma eficiente. No hay WHERE libre sobre cualquier columna, no hay JOIN, no hay subconsultas y el ORDER BY solo puede seguir el orden de agrupamiento ya definido. Y ese error, lejos de ser una limitación molesta, es una de las mejores decisiones de diseño del producto: te avisa en desarrollo de que tu consulta no escala, en vez de dejar que lo descubras en producción con diez millones de filas.
4.3 Diseño dirigido por la consulta
De la restricción anterior sale el principio central de la familia:
En Cassandra no se diseña un modelo de datos y luego se consulta. Se parte de la lista de consultas y se crea una tabla por consulta, duplicando los datos tantas veces como haga falta.
Si BiblioRed también necesita "los términos más buscados de un día", no se añade un índice: se crea otra tabla, con otra clave de partición, alimentada con la misma escritura.
CREATE TABLE actividad_por_dia (
dia date,
fecha_evento timestamp,
evento_id uuid,
socio_id int,
tipo text,
termino text,
PRIMARY KEY ((dia), fecha_evento, evento_id)
) WITH CLUSTERING ORDER BY (fecha_evento DESC, evento_id ASC);
SELECT termino, COUNT(*) FROM actividad_por_dia
WHERE dia = '2026-08-02' AND tipo = 'busqueda'
GROUP BY dia
ALLOW FILTERING;A alguien que viene del módulo 5 —que aún no hemos visto— esto le parecerá una herejía: es duplicación pura y dura. Y lo es, deliberadamente. En Cassandra el disco es barato, las escrituras son baratísimas y lo caro es la lectura ineficiente. Duplicar es la estrategia, no el error. Es la manifestación más extrema del principio de modelar a partir de las consultas que estudiaremos a fondo en la lección 03-03.
4.4 Fortalezas, límites y encaje en BiblioRed
Fortalezas: escritura extraordinariamente rápida y sostenida; escalado lineal (el doble de nodos, aproximadamente el doble de capacidad); sin nodo primario —todos los nodos aceptan escrituras, así que no hay punto único de fallo—; replicación entre centros de datos integrada; caducidad por fila (TTL) nativa.
Límites: consultas rígidas, atadas al diseño de la clave; sin JOIN ni agregaciones libres; duplicación masiva que la aplicación debe mantener coherente; operación de un clúster no trivial; una consulta nueva puede exigir una tabla nueva y un reproceso de todo el histórico.
Encaje en BiblioRed: el registro de actividad. Diecisiete millones de eventos anuales, escritura continua, lectura por socio y rango de fechas, sin necesidad de integridad referencial y con valor que decae con el tiempo. Es el caso de uso canónico de la familia.
Ahora bien, seamos consecuentes con lo que dijimos en 03-01: 17 millones de documentos al año no justifican desplegar y operar un clúster Cassandra. MongoDB los absorbe sin dificultad con el patrón de agrupación que veremos en 03-03. Cassandra entraría en escena si BiblioRed pasara de una red municipal a una red autonómica con cientos de millones de eventos. Conocer el criterio es lo que te permite tomar esa decisión el día que llegue.
- Bases de grafos: Neo4j
5.1 Modelo de datos
Tres elementos y ya está:
- Nodos: las entidades (un socio, un libro, un autor). Llevan una o más etiquetas (
:Socio,:Libro) y propiedades. - Relaciones: las conexiones entre nodos. Tienen tipo (
LEYO,ESCRIBIO), dirección y también propiedades propias. - Propiedades: pares clave-valor en nodos y relaciones.
La diferencia decisiva con el modelo relacional: en una base relacional, una relación se calcula en el momento de la consulta comparando valores de clave ajena; en una base de grafos, la relación está materializada como un puntero físico. Recorrerla no cuesta una búsqueda, cuesta seguir una referencia. A eso se le llama adyacencia sin índice, y es la razón de que los recorridos profundos sean tan rápidos.
Productos representativos: Neo4j (nuestra referencia), Amazon Neptune, ArangoDB, JanusGraph, Memgraph.
5.2 Cypher: MATCH, WHERE, RETURN
Cypher se lee dibujando. () es un nodo, -[]-> es una relación dirigida.
docker run -d --name neo4j-biblioRed -p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/biblioRed2026 neo4j:5
docker exec -it neo4j-biblioRed cypher-shell -u neo4j -p biblioRed2026// Crear los nodos de socios y libros
CREATE (m:Socio {socio_id: 14, nombre: 'Marta Alsina', sucursal: 1}),
(i:Socio {socio_id: 15, nombre: 'Iván Pereda', sucursal: 2}),
(n:Socio {socio_id: 16, nombre: 'Nuria Bastos', sucursal: 3}),
(l1:Libro {libro_id: 331, titulo: 'El mapa del tiempo', isbn: '9788401339097'}),
(l2:Libro {libro_id: 412, titulo: 'Los pilares de la Tierra', isbn: '9788401337208'}),
(l3:Libro {libro_id: 508, titulo: 'La sombra del faro', isbn: '9788401338441'}),
(l4:Libro {libro_id: 613, titulo: 'Cuadernos de Vallmar', isbn: '9788401339554'})// Crear las relaciones de lectura, con la valoración como propiedad de la relación
MATCH (m:Socio {socio_id: 14}), (i:Socio {socio_id: 15}), (n:Socio {socio_id: 16}),
(l1:Libro {libro_id: 331}), (l2:Libro {libro_id: 412}),
(l3:Libro {libro_id: 508}), (l4:Libro {libro_id: 613})
CREATE (m)-[:LEYO {puntuacion: 5, fecha: date('2026-03-14')}]->(l1),
(m)-[:LEYO {puntuacion: 4, fecha: date('2026-04-02')}]->(l2),
(i)-[:LEYO {puntuacion: 3, fecha: date('2026-03-22')}]->(l1),
(i)-[:LEYO {puntuacion: 5, fecha: date('2026-05-08')}]->(l3),
(n)-[:LEYO {puntuacion: 5, fecha: date('2026-03-24')}]->(l2),
(n)-[:LEYO {puntuacion: 4, fecha: date('2026-06-01')}]->(l4)// Consulta simple: qué ha leído Marta
MATCH (s:Socio {nombre: 'Marta Alsina'})-[r:LEYO]->(l:Libro)
RETURN l.titulo AS titulo, r.puntuacion AS puntuacion
ORDER BY r.puntuacion DESC+-------------------------------------------+
| titulo | puntuacion |
+-------------------------------------------+
| "El mapa del tiempo" | 5 |
| "Los pilares de la Tierra" | 4 |
+-------------------------------------------+5.3 El recorrido de varios saltos: la recomendación
Aquí es donde la familia se gana el sueldo. La pregunta de negocio de BiblioRed es: "lectores como tú también leyeron". Formalmente: partiendo de un socio, ir a los libros que ha leído, de ahí a los otros socios que también los leyeron, y de ahí a los libros que esos socios leyeron y el nuestro no.
Son tres saltos en el grafo.
flowchart LR
M["Socio<br/>Marta Alsina"] -->|LEYO| L1["Libro<br/>El mapa del tiempo"]
M -->|LEYO| L2["Libro<br/>Los pilares de la Tierra"]
I["Socio<br/>Iván Pereda"] -->|LEYO| L1
I -->|LEYO| L3["Libro<br/>La sombra del faro<br/>★ RECOMENDADO"]
N["Socio<br/>Nuria Bastos"] -->|LEYO| L2
N -->|LEYO| L4["Libro<br/>Cuadernos de Vallmar<br/>★ RECOMENDADO"]
MATCH (yo:Socio {socio_id: 14})-[:LEYO]->(:Libro)<-[:LEYO]-(otro:Socio)-[:LEYO]->(sugerencia:Libro)
WHERE NOT (yo)-[:LEYO]->(sugerencia)
AND yo <> otro
RETURN sugerencia.titulo AS recomendacion,
COUNT(DISTINCT otro) AS lectores_afines,
COLLECT(DISTINCT otro.nombre) AS quienes
ORDER BY lectores_afines DESC+----------------------------------------------------------------------+
| recomendacion | lectores_afines | quienes |
+----------------------------------------------------------------------+
| "La sombra del faro" | 1 | ["Iván Pereda"] |
| "Cuadernos de Vallmar" | 1 | ["Nuria Bastos"] |
+----------------------------------------------------------------------+Fíjate en la primera línea del MATCH: es literalmente el dibujo del camino. Lees "yo leí un libro que fue leído por otro que leyó una sugerencia" siguiendo las flechas. Y WHERE NOT (yo)-[:LEYO]->(sugerencia) es un anti-patrón de camino: descarta lo que Marta ya ha leído, igual que el anti-join de la lección 02-04 descartaba filas con NOT EXISTS.
5.4 Por qué esto no es un problema de JOIN
El equivalente SQL de esa consulta, sobre el esquema del módulo 2, sería aproximadamente:
SELECT l2.titulo, COUNT(DISTINCT p2.socio_id) AS lectores_afines
FROM prestamos p1
INNER JOIN ejemplares e1 ON e1.ejemplar_id = p1.ejemplar_id
INNER JOIN ejemplares e2 ON e2.libro_id = e1.libro_id
INNER JOIN prestamos p2 ON p2.ejemplar_id = e2.ejemplar_id AND p2.socio_id <> 14
INNER JOIN prestamos p3 ON p3.socio_id = p2.socio_id
INNER JOIN ejemplares e3 ON e3.ejemplar_id = p3.ejemplar_id
INNER JOIN libros l2 ON l2.libro_id = e3.libro_id
WHERE p1.socio_id = 14
AND NOT EXISTS (
SELECT 1 FROM prestamos px
INNER JOIN ejemplares ex ON ex.ejemplar_id = px.ejemplar_id
WHERE px.socio_id = 14 AND ex.libro_id = l2.libro_id
)
GROUP BY l2.titulo
ORDER BY lectores_afines DESC;Seis JOIN y una subconsulta correlacionada para tres saltos. Funciona, es correcto y con los datos de BiblioRed responderá rápido. El problema es la curva:
| Saltos | En SQL | En un grafo |
|---|---|---|
| 1 (qué he leído) | 1 JOIN, inmediato |
Inmediato |
| 2 (quién más lo leyó) | 3 JOIN, rápido |
Inmediato |
| 3 (qué leyeron ellos) | 6 JOIN, aceptable |
Rápido |
| 4 (y qué leyó su círculo) | 9 JOIN, empieza a doler |
Rápido |
| 5+ | Inviable en la práctica | Sigue siendo viable |
Cada salto en SQL añade uno o más JOIN, y el coste de cada JOIN depende del tamaño total de las tablas. En un grafo, cada salto sigue punteros desde los nodos que ya tienes: el coste depende del número de vecinos, no del tamaño de la base. Por eso la profundidad es gratis en un grafo y carísima en SQL.
Regla práctica: hasta dos saltos, SQL. A partir de tres, y sobre todo si la profundidad es variable, grafo.
5.5 Fortalezas, límites y encaje en BiblioRed
Fortalezas: recorridos profundos a coste casi constante; el modelo se dibuja igual que se piensa; relaciones con propiedades propias; excelente para recomendaciones, detección de fraude, redes sociales, análisis de dependencias y árboles genealógicos u organizativos.
Límites: mala elección para agregaciones masivas sobre todos los nodos; escalado horizontal más difícil que en las otras familias (repartir un grafo entre máquinas sin cortar relaciones es un problema duro); ecosistema y talento más pequeños; suele ser una base secundaria alimentada desde la principal.
Encaje en BiblioRed: el motor de recomendaciones. Y con la misma honestidad de antes: BiblioRed no lo va a desplegar de momento; con 12.000 socios, una consulta SQL nocturna que precalcule recomendaciones y las deje en una colección de MongoDB resuelve el problema con una pieza menos que operar. Neo4j entra cuando las recomendaciones deban ser interactivas, personalizadas y de profundidad variable.
- Familias especializadas que este curso no vuelve a tratar
Fuera de las cuatro grandes hay tres familias que aparecen constantemente en arquitecturas reales. Las nombramos para que las reconozcas, sin desarrollarlas.
Motores de búsqueda (Elasticsearch, OpenSearch, Solr). Técnicamente son documentales, pero su motor está construido sobre un índice invertido —una estructura que va de cada palabra a los documentos que la contienen—. Eso les da relevancia ordenada, tolerancia a erratas, resaltado de coincidencias, sinónimos y facetas. Si BiblioRed quisiera un buscador del catálogo de calidad profesional, con sugerencias mientras se escribe y corrección de "Julo Verne" a "Julio Verne", esta sería la herramienta. MongoDB tiene índices de texto que cubren lo básico; un motor de búsqueda cubre lo exigente.
Series temporales (InfluxDB, TimescaleDB, Prometheus). Optimizadas para datos de la forma (instante, medida, valor) con escritura continua y consulta por ventanas. Comprimen extraordinariamente bien porque los valores contiguos se parecen, y traen agregación por intervalos y caducidad automática de los datos antiguos. TimescaleDB es una extensión de PostgreSQL, lo que la hace especialmente cómoda si ya tienes la relacional. El registro de actividad de BiblioRed también podría vivir aquí.
Vectoriales (Pinecone, Weaviate, Qdrant, pgvector). Guardan vectores numéricos —representaciones de significado generadas por modelos de lenguaje— y responden a "¿qué es lo más parecido a esto?" mediante búsqueda de vecinos próximos. Son la pieza que permitiría a BiblioRed responder a "quiero algo parecido a El mapa del tiempo pero más corto" sin que ninguna palabra coincida. Es la familia más joven y la de crecimiento más rápido.
- Tabla comparativa de las cuatro familias
| Dimensión | Documental | Clave-valor | Columnar | Grafos |
|---|---|---|---|---|
| Producto de referencia | MongoDB | Redis | Cassandra | Neo4j |
| Unidad de datos | Documento BSON | Par clave → valor | Fila ancha en partición | Nodo y relación |
| ¿El motor ve el interior? | Sí, completo | No, opaco | Sí, por columnas | Sí, nodos y aristas |
| Lenguaje | Consultas + agregación | Órdenes (GET, SET…) |
CQL | Cypher |
| Consulta por campo cualquiera | Sí | No | Solo con la clave definida | Sí |
| Agregaciones | Sí (pipeline) | Limitadas | Muy limitadas | Sí, pero no es su fuerte |
| Escritura | Rápida | Muy rápida | Extremadamente rápida | Moderada |
| Escalado horizontal | Bueno | Bueno | Excelente y lineal | Difícil |
| Esquema | Flexible | Inexistente | Fijo por tabla, flexible por fila | Flexible |
| Relaciones entre entidades | Referencias manuales | No | No | Su razón de ser |
| Persistencia | Disco | Memoria (con volcado) | Disco | Disco |
| Caducidad nativa | Sí (índice TTL) | Sí, central | Sí (por fila) | No |
| Encaje en BiblioRed | Reseñas y catálogo | Caché y sesiones | Registro de actividad | Recomendaciones |
| Se despliega en BiblioRed | Sí | No, de momento | No, de momento | No, de momento |
- Árbol de decisión
flowchart TD
A["¿Qué necesito guardar?"] --> B{"¿Las relaciones entre<br/>entidades son el objeto<br/>principal de la consulta?"}
B -->|Sí, 3+ saltos| G["GRAFOS — Neo4j<br/>recomendaciones, fraude, redes"]
B -->|No| C{"¿Accedo siempre<br/>por una clave conocida?"}
C -->|Sí, y es efímero| KV["CLAVE-VALOR — Redis<br/>caché, sesiones, contadores"]
C -->|No| D{"¿Volumen enorme de escritura<br/>con consultas conocidas<br/>de antemano?"}
D -->|Sí| CO["COLUMNAR — Cassandra<br/>eventos, telemetría, registros"]
D -->|No| E{"¿Estructura rica,<br/>heterogénea y<br/>consultada por contenido?"}
E -->|Sí| DOC["DOCUMENTAL — MongoDB<br/>catálogos, perfiles, contenidos"]
E -->|No| REL["RELACIONAL — PostgreSQL<br/>la respuesta por defecto"]
Presta atención a la última rama: cuando ninguna de las cuatro familias encaja con claridad, la respuesta correcta es la relacional. No es un consuelo, es el criterio profesional. Lo desarrollaremos con más argumentos en la lección 03-04.
- El reparto final de BiblioRed
Uniendo lo decidido en 01-02 con lo que hemos visto aquí:
| Pieza del sistema | Familia idónea | Decisión real de BiblioRed | Por qué |
|---|---|---|---|
| Socios, préstamos, reservas, ejemplares | Relacional | PostgreSQL | Integridad, transacciones, consultas impredecibles |
| Reseñas de lectores | Documental | MongoDB | Estructura cambiante, agregado autocontenido |
| Catálogo enriquecido | Documental | MongoDB | Metadatos heterogéneos por tipo de material |
| Registro de actividad | Columnar | MongoDB con patrón de agrupación | El volumen aún no justifica un clúster Cassandra |
| Caché de fichas y sesiones | Clave-valor | Aplazado | Aún no hay problema de latencia que resolver |
| Recomendaciones | Grafos | Precálculo nocturno en SQL → MongoDB | 12.000 socios no justifican otra base más |
Dos bases de datos, cuatro necesidades cubiertas y un criterio escrito para cuando cada aplazamiento deje de ser razonable. Esa mezcla deliberada de motores se llama persistencia políglota; la nombraremos formalmente en la lección 03-04 y será el caso de estudio completo de la 08-03.
Errores Comunes y Consejos
Error 1: usar una base de grafos porque el dominio "tiene relaciones".
Todos los dominios tienen relaciones; para eso existe el modelo relacional. El grafo gana cuando el recorrido de profundidad variable es la consulta principal. Un par de JOIN no justifican otra base de datos.
Error 2: usar Redis como almacén principal. Es memoria: si el proceso cae y la persistencia no estaba bien configurada, se pierden datos. Regla: si vaciar Redis pierde información que no puedes reconstruir, lo estás usando mal.
Error 3: escribir CQL como si fuera SQL.
La sintaxis engaña. En Cassandra no hay JOIN, el WHERE solo funciona sobre las columnas de la clave y ORDER BY está atado al orden de agrupamiento. Si te ves escribiendo ALLOW FILTERING para que una consulta funcione, el problema no es la consulta: es el modelo de datos.
Error 4: confundir "columnar" con "analítica". Cassandra (familias de columnas) está pensada para operación transaccional a gran escala. ClickHouse o Redshift (almacenamiento por columnas) están pensadas para análisis OLAP, que vimos en 01-02. Comparten adjetivo y no propósito.
Error 5: usar $lookup de MongoDB como si fuera un JOIN normal.
Funciona, pero delata que el diseño de documentos no es el adecuado. En la lección siguiente lo veremos catalogado como anti-patrón.
Consejo 1: diseña las claves de Redis con una convención jerárquica.
entidad:identificador:aspecto, por ejemplo catalogo:MAT-0331:ficha o sesion:8f3a2b1c. Sin esa disciplina, en seis meses nadie sabrá qué hay dentro de una clave llamada m331.
Consejo 2: en Cassandra, escribe la lista de consultas antes que el CREATE TABLE.
Es el orden inverso al relacional y es obligatorio. Una consulta no prevista puede costar una tabla nueva y reprocesar todo el histórico.
Consejo 3: prueba las cuatro familias en Docker.
Una tarde levantando Redis, Cassandra y Neo4j en contenedores y trasteando con veinte documentos enseña más que cualquier comparativa escrita. Y docker rm -f lo deja todo como estaba.
Consejo 4: cada base de datos añadida es una base de datos que operar. Copias de seguridad, actualizaciones, supervisión, permisos, un experto en el equipo. Antes de añadir la tercera, pregúntate si el problema no se resuelve con un índice en la primera.
Ejercicios
Ejercicio 1
BiblioRed quiere una página "Lo más valorado del mes" con, para cada libro, su puntuación media y su número de reseñas, considerando solo reseñas de abril de 2026 y mostrando únicamente los libros con media igual o superior a 4. Escribe la aggregation pipeline de MongoDB y su equivalente SQL, y explica a qué cláusula de SQL corresponde cada etapa.
Ejercicio 2
Para cada una de estas cuatro necesidades nuevas de BiblioRed, elige la familia NoSQL más adecuada y justifícalo en dos o tres frases:
- (a) Un contador de "ejemplares disponibles ahora mismo" por sucursal, consultado en cada carga de la página de inicio y que debe ser rapidísimo.
- (b) Un histórico de todos los cambios de estado de cada ejemplar (
disponible,prestado,reparación,baja), con unos 300.000 cambios al año, consultado como "historia de este ejemplar". - (c) Detectar clubes de lectura informales: grupos de socios que coinciden repetidamente leyendo los mismos títulos en las mismas semanas.
- (d) Fichas de autor con biografía, foto, premios, enlaces externos y bibliografía, donde cada autor tiene un conjunto distinto de datos disponibles.
Ejercicio 3
Diseña la tabla de Cassandra que resolvería esta consulta de BiblioRed: "muéstrame los últimos 20 eventos de actividad de una sucursal concreta, del más reciente al más antiguo". Escribe el CREATE TABLE, indica cuál es la clave de partición y cuál la de agrupamiento, y responde: ¿por qué esta tabla no puede responder también a "los últimos 20 eventos de un socio concreto"?
Soluciones
Solución 1
db.resenas.aggregate([
{ $match: {
fecha: { $gte: ISODate("2026-04-01T00:00:00Z"),
$lt: ISODate("2026-05-01T00:00:00Z") }
}},
{ $group: {
_id: "$libro_id",
titulo: { $first: "$titulo_libro" },
media: { $avg: "$puntuacion" },
total: { $sum: 1 }
}},
{ $match: { media: { $gte: 4 } } },
{ $project: {
_id: 0,
libro_id: "$_id",
titulo: 1,
media: { $round: ["$media", 2] },
total: 1
}},
{ $sort: { media: -1, total: -1 } }
])SELECT r.libro_id, l.titulo, ROUND(AVG(r.puntuacion), 2) AS media, COUNT(*) AS total
FROM resenas r
INNER JOIN libros l ON l.libro_id = r.libro_id
WHERE r.fecha >= '2026-04-01' AND r.fecha < '2026-05-01'
GROUP BY r.libro_id, l.titulo
HAVING AVG(r.puntuacion) >= 4
ORDER BY media DESC, total DESC;| Etapa | Cláusula SQL | Comentario |
|---|---|---|
$match (1.ª) |
WHERE |
Filtra antes de agrupar. Ponerla primera es esencial: reduce el volumen que procesan las etapas siguientes y permite usar índices. |
$group |
GROUP BY |
_id es la clave de agrupación. $first recupera el título, que está duplicado en cada reseña —y por eso no hace falta JOIN con libros, a diferencia del SQL—. |
$match (2.ª) |
HAVING |
Filtra después de agrupar, sobre el resultado calculado. La misma etapa cumple dos papeles distintos según dónde se coloque: ahí está el orden lógico de ejecución de la lección 02-05. |
$project |
Lista del SELECT |
Elige y da forma a la salida; $round es el ROUND de SQL. |
$sort |
ORDER BY |
Ordenación final. |
Solución 2
(a) Contador de disponibles → clave-valor (Redis). Acceso siempre por una clave conocida (disponibles:sucursal:2), lectura muy frecuente, valor diminuto y tolerancia a unos segundos de desfase. INCR/DECR son atómicos y responden en microsegundos, mientras que un COUNT(*) sobre ejemplares en cada carga de la página de inicio es un desperdicio evitable.
(b) Histórico de estados de ejemplares → columnar (Cassandra). Es una serie de eventos inmutables, con escritura continua y una consulta perfectamente conocida de antemano: por ejemplar y ordenada por fecha. Clave de partición ejemplar_id, clave de agrupamiento fecha descendente. Dicho esto, y siendo coherentes con el criterio del apartado 9: 300.000 cambios al año son pocos, y una tabla en PostgreSQL con un índice sobre (ejemplar_id, fecha) resolvería esto perfectamente durante muchos años.
(c) Detectar clubes de lectura informales → grafos (Neo4j). Buscar grupos de socios densamente conectados entre sí a través de los títulos que comparten es detección de comunidades, un problema clásico de grafos que requiere recorridos de profundidad variable. En SQL sería una autounión repetida de coste creciente; en Cypher es un patrón de camino, y Neo4j incluso trae algoritmos de comunidad ya implementados.
(d) Fichas de autor → documental (MongoDB). Estructura rica y heterogénea (un autor tiene tres premios y ninguna foto, otro cinco enlaces y una biografía larga), leída entera de una vez para pintar la ficha y consultada por su contenido ("autores nacidos en Vallmar"). Es exactamente el mismo argumento que llevó el catálogo a MongoDB, así que además reutiliza una base que BiblioRed ya opera.
Solución 3
CREATE TABLE actividad_por_sucursal (
sucursal_id int,
fecha_evento timestamp,
evento_id uuid,
socio_id int,
tipo text,
termino text,
material_id text,
PRIMARY KEY ((sucursal_id), fecha_evento, evento_id)
) WITH CLUSTERING ORDER BY (fecha_evento DESC, evento_id ASC);
SELECT fecha_evento, socio_id, tipo, termino, material_id
FROM actividad_por_sucursal
WHERE sucursal_id = 2
LIMIT 20;- Clave de partición:
(sucursal_id). Toda la actividad de una sucursal vive junta en el mismo nodo. - Claves de agrupamiento:
fecha_evento(descendente, para que los 20 últimos sean los 20 primeros de la partición) yevento_id(para desempatar eventos simultáneos).
Por qué no sirve para consultar por socio. Porque socio_id no forma parte de la clave: es una columna normal. Cassandra necesita conocer la clave de partición para saber a qué nodo ir; sin ella tendría que preguntar a todos los nodos y filtrar cada partición entera, que es precisamente lo que el motor se niega a hacer sin ALLOW FILTERING.
Y esto ilustra la regla central de la familia: una tabla por consulta. Si BiblioRed necesita las dos vistas, mantiene las dos tablas —actividad_por_socio y actividad_por_sucursal— y escribe cada evento en ambas. La duplicación no es un defecto del diseño: es el diseño.
Un aviso adicional que un buen diseñador vería: si una sucursal genera millones de eventos, su partición crecerá sin límite, y las particiones ilimitadas son un problema conocido en Cassandra. La solución habitual es una clave de partición compuesta que incluya el periodo, ((sucursal_id, mes), fecha_evento, evento_id), acotando cada partición a un mes. Es la misma idea que el patrón de agrupación (bucket) que estudiaremos en la lección siguiente.
Conclusión
Hemos recorrido las cuatro familias NoSQL con operación real:
- Documentales (MongoDB): documentos BSON anidados en colecciones sin esquema. Filtros con
$gt,$in,$regex,$exists,$all; proyección con1/0;sort,limit,skip; actualizaciones por partes con$set,$inc,$push,$addToSety el operador posicional$; y la aggregation pipeline como equivalente delGROUP BY, donde$matchesWHEREantes de agrupar yHAVINGdespués,$groupagrupa con_idcomo clave y$unwinddespliega arrays. Encaje: reseñas y catálogo. - Clave-valor (Redis): el motor no ve el interior del valor y a cambio responde en microsegundos.
SET/GET/EXPIRE/TTL, más estructuras nativas —listas, conjuntos, conjuntos ordenados, hashes y contadores atómicos conINCR—. La caducidad es una función de primera clase, y por eso es la herramienta natural para caché y sesiones. Nunca como fuente de verdad. - Columnares (Cassandra): filas anchas agrupadas por clave de partición y ordenadas por clave de agrupamiento; CQL se escribe como SQL pero prohíbe
JOIN, subconsultas yWHERElibre —el error deALLOW FILTERINGes un aviso de diseño, no una molestia—. El principio es una tabla por consulta, con duplicación deliberada. Encaje: registros de actividad de altísimo volumen. - Grafos (Neo4j): nodos, relaciones con propiedades y dirección, y Cypher, que se escribe dibujando el camino. La adyacencia sin índice hace que el coste de un salto dependa del número de vecinos y no del tamaño de la base: hasta dos saltos, SQL; a partir de tres, grafo. Encaje: el "lectores como tú también leyeron".
- Especializadas: motores de búsqueda con índice invertido (Elasticsearch), series temporales (InfluxDB, TimescaleDB) y vectoriales (pgvector, Qdrant) para búsqueda por similitud semántica.
- La decisión real de BiblioRed: PostgreSQL y MongoDB. Redis, Cassandra y Neo4j quedan aplazados con un criterio escrito de cuándo dejarían de estarlo. Cada base añadida es una base que operar.
Sabemos ya qué familias existen y cómo se opera cada una. Falta lo más difícil y lo que más se equivoca en la práctica: diseñar bien. En la lección 03-03, Modelado de Datos en NoSQL, invertiremos el orden que aprendimos en el mundo relacional —dejaremos de modelar el dominio para modelar a partir de las consultas—, definiremos el concepto de agregado, resolveremos la decisión central de embeber frente a referenciar con criterios explícitos, estudiaremos los patrones documentales (referencia extendida, subconjunto, agrupación, valor atípico, campo calculado) y sus anti-patrones, y entregaremos el diseño final y justificado de las colecciones de BiblioRed: resenas, catalogo y actividad.
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
