Bases de datos NoSQL
"NoSQL no significa 'sin SQL'. Significa 'Not Only SQL' — hay más herramientas además de SQL para distintos problemas."
Qué vas a aprender en este capítulo
El sistema de La Esquina usa PostgreSQL para pedidos y facturas. Pero el dueño ahora quiere: (1) guardar el historial completo de cada cliente con sus preferencias — estructura variable por cliente; (2) cachear el menú para que cargue en milisegundos; (3) analizar qué platillos se piden juntos frecuentemente. Cada uno de estos problemas tiene una herramienta diferente.
4.1 Taxonomía de bases de datos NoSQL
💡 Intuición
SQL (relacional) es como una hoja de cálculo: datos en tablas con columnas fijas. Funciona perfecto cuando los datos son estructurados y relacionados.
NoSQL es un término paraguas para todo lo que no es relacional. No hay un solo "NoSQL" — hay cuatro familias muy distintas, cada una óptima para un tipo de problema.
📐 Fundamento
Las cuatro familias de NoSQL:
| Tipo | Modelo | Ejemplo | Caso de uso ideal |
|---|---|---|---|
| Documentos | Objetos JSON/BSON anidados | MongoDB, CouchDB | Catálogos, perfiles de usuario, CMSs |
| Clave-Valor | Diccionario: clave → valor | Redis, DynamoDB | Caché, sesiones, contadores |
| Columnar | Filas con columnas dinámicas | Cassandra, HBase | Series temporales, logs, analytics |
| Grafos | Nodos y aristas con propiedades | Neo4j, Amazon Neptune | Redes sociales, recomendaciones, rutas |
¿Cuándo elegir SQL vs NoSQL?
| Criterio | Elegir SQL | Elegir NoSQL |
|---|---|---|
| Esquema | Fijo, bien definido | Variable, evoluciona rápido |
| Relaciones | Muchas relaciones complejas | Pocas relaciones o datos anidados |
| Transacciones | ACID crítico (pagos, inventario) | Consistencia eventual aceptable |
| Escala | Vertical (hasta cierto punto) | Horizontal (millones de usuarios) |
| Consultas | Complejas (JOIN, agregaciones) | Simples y predecibles |
| Madurez | Décadas de herramientas | Ecosistema más joven |
4.2 Bases de datos de documentos — MongoDB
💡 Intuición
En una base de datos relacional, el perfil de un cliente con sus preferencias requiere varias tablas (clientes, preferencias, historial_pedidos) con JOINs. En MongoDB, todo está en un solo documento JSON — no hay JOINs porque los datos relacionados van anidados.
Esto simplifica el código de la aplicación cuando los datos son naturalmente jerárquicos.
📐 Fundamento
Conceptos de MongoDB:
| SQL | MongoDB |
|---|---|
| Base de datos | Base de datos |
| Tabla | Colección |
| Fila | Documento |
| Columna | Campo |
| Primary key | _id (ObjectId) |
| JOIN | $lookup o embedding |
Documento de ejemplo:
{
"_id": "ObjectId('...')",
"nombre": "Ana García",
"email": "ana@example.com",
"historial_pedidos": [
{
"fecha": "2026-04-15",
"total": 8.50,
"platillos": ["pupusa de queso", "refresco"]
},
{
"fecha": "2026-04-20",
"total": 12.00,
"platillos": ["pupusa revuelta", "sopa de pata"]
}
],
"preferencias": {
"sin_picante": true,
"mesa_favorita": 3
}
}
Operaciones básicas:
// Insertar
db.clientes.insertOne({
nombre: "Ana García",
email: "ana@example.com",
preferencias: { sin_picante: true }
})
// Buscar
db.clientes.find({ "preferencias.sin_picante": true })
// Actualizar (agregar al array de historial)
db.clientes.updateOne(
{ email: "ana@example.com" },
{ $push: { historial_pedidos: { fecha: new Date(), total: 8.50 } } }
)
// Aggregation pipeline (equivalente a GROUP BY + JOIN)
db.clientes.aggregate([
{ <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>u</mi><mi>n</mi><mi>w</mi><mi>i</mi><mi>n</mi><mi>d</mi><mo>:</mo><mi mathvariant="normal">"</mi></mrow><annotation encoding="application/x-tex">unwind: "</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mord mathnormal">in</span><span class="mord mathnormal">d</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">"</span></span></span></span>historial_pedidos" },
{ <span class="katex-error" title="ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: group: { _id: "" style="color:#cc0000">group: { _id: "</span>email", total_gastado: { <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>s</mi><mi>u</mi><mi>m</mi><mo>:</mo><mi mathvariant="normal">"</mi></mrow><annotation encoding="application/x-tex">sum: "</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">s</span><span class="mord mathnormal">u</span><span class="mord mathnormal">m</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">"</span></span></span></span>historial_pedidos.total" } } },
{ $sort: { total_gastado: -1 } }
])
Embedding vs referencing:
// Embedding: datos anidados (sin JOIN)
// BUENO cuando: los datos siempre se leen juntos, cardinalidad baja
{
pedido_id: 1,
items: [
{ platillo: "pupusa", precio: 1.50, cantidad: 2 }
]
}
// Referencing: como FK en SQL
// BUENO cuando: los datos se actualizan independientemente o la cardinalidad es alta
{
pedido_id: 1,
cliente_id: "ObjectId('...')" // referencia al documento del cliente
}
Cuándo NO usar MongoDB:
- Transacciones complejas multi-documento (MongoDB 4.0+ soporta multi-doc transactions, pero con overhead).
- Datos fuertemente relacionales con muchos JOINs.
- Cuando el esquema es estable y bien definido — SQL es más eficiente.
4.3 Clave-Valor — Redis
💡 Intuición
Redis es como un diccionario en RAM: extremadamente rápido (microsegundos) pero los datos están en memoria, no en disco (aunque tiene persistencia opcional).
El uso más común: caché. En lugar de consultar PostgreSQL cada vez que alguien pide el menú (100ms), guardás el menú en Redis (0.1ms) y lo servís desde ahí.
📐 Fundamento
Tipos de datos en Redis:
# String: el tipo más básico
SET menu:updated_at "2026-05-05T12:00:00"
GET menu:updated_at
# Con TTL (tiempo de vida) — se borra solo después de N segundos
SET sesion:token_abc123 "user_id:42" EX 3600 # expira en 1 hora
# List: cola de mensajes o tareas
LPUSH notificaciones:cocina "Pedido #123 listo"
RPOP notificaciones:cocina # tomar la notificación más antigua
# Hash: objeto con campos
HSET platillo:1 nombre "Pupusa de queso" precio "1.50" disponible "1"
HGET platillo:1 precio # "1.50"
HGETALL platillo:1
# Set: conjunto sin duplicados (para membresías)
SADD mesas:activas 3 5 7
SMEMBERS mesas:activas # {3, 5, 7}
# Sorted Set: con puntuación para ranking
ZADD ventas:semana 150 "pupusa de queso"
ZADD ventas:semana 80 "refresco"
ZREVRANGE ventas:semana 0 9 # top 10 más vendidos
Patrón de caché (Cache-Aside):
def get_menu():
# 1. Intentar leer del caché
menu_cached = redis.get("menu:completo")
if menu_cached:
return json.loads(menu_cached)
# 2. Cache miss: leer de la BD
menu = db.query("SELECT * FROM platillos WHERE disponible = true")
# 3. Guardar en caché con TTL de 5 minutos
redis.setex("menu:completo", 300, json.dumps(menu))
return menu
Redis para rate limiting (limitar solicitudes):
def puede_hacer_pedido(cliente_id: str) -> bool:
key = f"rate_limit:{cliente_id}"
count = redis.incr(key)
if count == 1:
redis.expire(key, 60) # reinicia después de 1 minuto
return count <= 10 # máximo 10 pedidos por minuto
Persistencia en Redis:
- RDB (Redis Database Backup): Snapshot del estado completo cada N segundos. Rápido, pero puede perder datos del último snapshot.
- AOF (Append-Only File): Escribe cada comando en un log. Más seguro, más lento.
- Sin persistencia: Máximo rendimiento, pero los datos se pierden si Redis se reinicia (válido para caché puro).
4.4 Columnar — Cassandra
💡 Intuición
Cassandra es perfecta cuando tenés millones de eventos por segundo (logs, métricas, IoT) y necesitás escribir rápido y leer por rango de tiempo. Es distribuida por diseño — no tiene un Primary, todos los nodos son iguales.
📐 Fundamento
Modelo de datos de Cassandra:
-- Crear tabla (CQL — Cassandra Query Language)
CREATE TABLE eventos_pedido (
local_id UUID,
fecha TIMESTAMP,
pedido_id UUID,
evento TEXT,
PRIMARY KEY ((local_id), fecha, pedido_id)
-- ↑ partition key ↑ clustering columns (orden dentro del partition)
) WITH CLUSTERING ORDER BY (fecha DESC);
-- Insertar
INSERT INTO eventos_pedido (local_id, fecha, pedido_id, evento)
VALUES (uuid(), toTimestamp(now()), uuid(), 'PEDIDO_CREADO');
-- Consultar: todos los eventos del local X en la última hora
SELECT * FROM eventos_pedido
WHERE local_id = :local_id
AND fecha >= toTimestamp(now()) - 3600s;
Características clave:
- Partition key: Determina en qué nodo se almacena la fila. La query DEBE incluir la partition key.
- Clustering columns: Ordenan los datos dentro de la partición.
- Consistencia eventual: Las escrituras se propagan a las réplicas de forma asíncrona. Se puede configurar el nivel de consistencia por query.
- Sin JOINs: Los datos deben modelarse según las queries (no según la normalización).
Cuándo usar Cassandra:
- Series de tiempo: logs, métricas, IoT.
- Escrituras masivas distribuidas globalmente.
- Cuando la consistencia eventual es aceptable.
- Alta disponibilidad es obligatoria (no tiene single point of failure).
Cuándo NO usar:
- Transacciones ACID complejas.
- Consultas ad-hoc con muchos filtros variables.
- Relaciones complejas entre entidades.
4.5 Grafos — Neo4j (conceptual)
💡 Intuición
Si tuvieras que encontrar "todos los clientes que compraron el mismo platillo que Ana en los últimos 3 meses y que también viven en San Miguel" en SQL, necesitarías varios JOINs anidados que se vuelven lentos exponencialmente.
En una base de datos de grafos, esa es exactamente el tipo de query que hace rápido — porque los datos ya están modelados como una red de relaciones.
📐 Fundamento
Modelo de grafos:
- Nodos: Entidades (Cliente, Platillo, Pedido).
- Aristas: Relaciones con dirección y tipo (
:COMPRÓ,:CONTIENE,:RECOMIENDA). - Propiedades: Atributos en nodos y aristas.
Cypher (lenguaje de consulta de Neo4j):
// Crear nodos y relaciones
CREATE (ana:Cliente {nombre: "Ana García", ciudad: "San Miguel"})
CREATE (pupusa:Platillo {nombre: "Pupusa de queso", precio: 1.50})
CREATE (ana)-[:COMPRÓ {fecha: "2026-05-05", cantidad: 2}]->(pupusa)
// "Clientes que compraron lo mismo que Ana"
MATCH (ana:Cliente {nombre: "Ana García"})-[:COMPRÓ]->(platillo)
MATCH (otro:Cliente)-[:COMPRÓ]->(platillo)
WHERE otro <> ana
RETURN DISTINCT otro.nombre, platillo.nombre
// Recomendación: "platillos que los clientes similares a ti compraron pero tú no"
MATCH (tu:Cliente {id: $id})-[:COMPRÓ]->(platillo)<-[:COMPRÓ]-(similar:Cliente)
MATCH (similar)-[:COMPRÓ]->(recomendado)
WHERE NOT (tu)-[:COMPRÓ]->(recomendado)
RETURN recomendado.nombre, COUNT(*) as popularidad
ORDER BY popularidad DESC
LIMIT 5
Casos de uso de Neo4j:
- Detección de fraude (¿esta tarjeta fue usada desde dos países distintos en 5 minutos?).
- Redes sociales (amigos de amigos).
- Motores de recomendación.
- Grafos de conocimiento.
- Rutas y logística.
🛠️ En la práctica
La Esquina: arquitectura políglota
El sistema completo de La Esquina con tres locales usa diferentes bases de datos para diferentes necesidades:
┌─────────────────────────────────────────────────────────┐
│ App de La Esquina │
└──────┬──────────┬──────────┬──────────────┬─────────────┘
↓ ↓ ↓ ↓
┌──────────┐ ┌──────┐ ┌──────────┐ ┌──────────────┐
│PostgreSQL│ │Redis │ │ Cassandra│ │ Neo4j │
│ │ │ │ │ │ │ (opcional) │
│Pedidos │ │Caché │ │Logs de │ │Recomendacion │
│Facturas │ │Menú │ │eventos │ │de platillos │
│Inventario│ │Sesion│ │Métricas │ │ │
│Clientes │ │Rates │ │ │ │ │
└──────────┘ └──────┘ └──────────┘ └──────────────┘
Regla de decisión:
- Datos transaccionales con ACID → PostgreSQL.
- Caché y datos temporales → Redis.
- Logs y métricas de alta velocidad → Cassandra.
- Análisis de relaciones entre datos → Neo4j.
Esta arquitectura se llama persistencia políglota (polyglot persistence).
4.6 Ejercicios
✏️ Ejercicio 4.1 — Elegir la base de datos correcta
Para cada caso de uso, elegí la base de datos más apropiada (PostgreSQL, MongoDB, Redis, Cassandra, Neo4j) y justificá:
a. Guardar los resultados de búsquedas frecuentes para no repetir el cálculo. b. Registrar 1 millón de eventos de clics por hora en un sitio web. c. Guardar el perfil de usuario con estructura variable (algunos tienen foto, otros no; algunos tienen múltiples direcciones, etc.). d. Encontrar la ruta más corta entre dos sucursales de una red de distribución. e. Registrar ventas, pagos y facturas con integridad referencial.
Solución
a. Redis — caché con TTL. Operaciones en microsegundos, datos temporales.
b. Cassandra — escrituras masivas de series de tiempo. Alta velocidad de ingestión distribuida.
c. MongoDB — esquema flexible, documentos JSON con estructura variable por documento.
d. Neo4j — algoritmos de grafos (Dijkstra, A*) para encontrar rutas óptimas en una red de sucursales.
e. PostgreSQL — ACID, integridad referencial con foreign keys, transacciones complejas.
✏️ Ejercicio 4.2 — Modelar en MongoDB
Modelá en MongoDB el siguiente escenario:
Un sistema de reseñas de restaurantes donde cada restaurante tiene: nombre, dirección, categorías (array), horario (objeto con días y horas), y múltiples reseñas de clientes (con texto, calificación 1-5, y fecha).
a. Diseñá el documento JSON de ejemplo. b. Escribí la query para encontrar todos los restaurantes con calificación promedio > 4.0. c. ¿Embedirías las reseñas en el documento del restaurante o las pondrías en una colección separada? Justificá.
Solución
a. Documento:
{
"_id": "ObjectId('...')",
"nombre": "La Esquina",
"direccion": { "calle": "5a Calle Oriente #123", "ciudad": "San Miguel" },
"categorias": ["pupuseria", "comida_tipica", "familiar"],
"horario": {
"lunes": { "abre": "07:00", "cierra": "21:00" },
"martes": { "abre": "07:00", "cierra": "21:00" },
"domingo": "cerrado"
},
"calificacion_promedio": 4.5,
"total_resenas": 128,
"resenas": [
{ "usuario": "Ana", "texto": "Excelentes pupusas", "nota": 5, "fecha": "2026-05-01" }
]
}
b. Query con aggregation:
db.restaurantes.aggregate([
{ $addFields: {
promedio: { <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mi>v</mi><mi>g</mi><mo>:</mo><mi mathvariant="normal">"</mi></mrow><annotation encoding="application/x-tex">avg: "</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord">"</span></span></span></span>resenas.nota" }
}},
{ <span class="katex-error" title="ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … { promedio: { " style="color:#cc0000">match: { promedio: { </span>gt: 4.0 } } },
{ $sort: { promedio: -1 } }
])
// O más eficiente: mantener calificacion_promedio desnormalizado
db.restaurantes.find({ calificacion_promedio: { $gt: 4.0 } })
c. Decisión de embedding vs referencing:
Para restaurantes con pocas reseñas (< 100): embedding — se leen siempre juntos y es más simple.
Para restaurantes populares con miles de reseñas: referencing — un documento de 10MB es lento de transferir y MongoDB tiene límite de 16MB por documento. Crear colección resenas separada con restaurante_id como referencia.
Opción híbrida: embeddir las últimas 10 reseñas en el documento, y guardar todas en colección separada.
4.7 Para profundizar
- Sadalage & Fowler, NoSQL Distilled — introducción a todos los modelos NoSQL.
- MongoDB University — cursos gratuitos oficiales.
- Redis docs — documentación con ejemplos por tipo de dato.
- Siguiente: Diseño avanzado de bases de datos.
Definiciones nuevas: NoSQL, base de datos de documentos, base de datos de clave-valor, base de datos columnar, base de datos de grafos, MongoDB, Redis, Cassandra, Neo4j, embedding, referencing, caché, TTL, partition key, clustering column, consistencia eventual, Cypher, polyglot persistence.