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">&quot;</mi></mrow><annotation encoding="application/x-tex">unwind: &quot;</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">&quot;</span></span></span></span>historial_pedidos" },
  { <span class="katex-error" title="ParseError: KaTeX parse error: Expected &#x27;}&#x27;, got &#x27;EOF&#x27; at end of input: group: { _id: &quot;" style="color:#cc0000">group: { _id: &quot;</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">&quot;</mi></mrow><annotation encoding="application/x-tex">sum: &quot;</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">&quot;</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.

✏️ 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á.


4.7 Para profundizar


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.