Módulo 2 · Bases de datos

Lección 12 — NoSQL

Redis, MongoDB y motores de búsqueda: qué sacrificas con cada uno y cuándo compensa.

Publicada
En esta lección
  1. Objetivos
  2. 1. Qué sacrificas con cada almacén
  3. 2. Redis: los cinco usos que sí valen
  4. 3. JSONB: el NoSQL que ya tienes
  5. 4. Anti-patrones que verás (y cómo responder)
  6. Autoevaluación

Stack: PostgreSQL + Redis · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de datos Prerrequisito: Lección 11 — Migraciones


Objetivos

  1. Elegir entre Postgres y un almacén especializado con criterio (qué sacrificas con cada uno).
  2. Usar Redis como caché, contador atómico, lock distribuido y leaderboard — con TTLs razonables.
  3. Usar JSONB de PostgreSQL cuando el "NoSQL" que necesitas cabe en tu BD actual.
  4. Reconocer los anti-patrones: Mongo "porque es moderno", Redis como base de datos primaria.

1. Qué sacrificas con cada almacén

AlmacénGanaSacrificaEn TicketFlow
PostgreSQLACID, constraints, SQL, EXPLAINescritura simple a escala extremala fuente de verdad (siempre)
Redislatencia µs, atomics, TTL, estructurasdurabilidad (por defecto), sin SQL, RAMcaché, contadores, locks, colas (broker)
MongoDBdocumentos flexibles, sharding fáciljoins/ACID multi-doc (mejoró, pero no es su punto)catálogos de solo-lectura variados (aquí: no)
Elasticsearchbúsqueda de texto, agregacionesconsistencia, coste de syncbúsqueda de eventos (más adelante, si hay demanda)

La regla senior: Postgres es la base de datos primaria; lo demás es especialización justificada por una métrica (latencia de disponibilidad, throughput de contador, búsqueda de texto). "Necesito NoSQL" no es una métrica.

2. Redis: los cinco usos que sí valen

python
import redis
r = redis.Redis(host="localhost", port=6379, db=0)

# 1. Caché con TTL (disponibilidad, listado público)
r.setex(f"avail:{event_id}", 10, json.dumps(asientos_libres))   # TTL 10s: dato "fresco-ish"

# 2. Contador atómico (rate limit por IP, tickets por usuario)
r.incr(f"rate:{ip}:{minute}", ex=60)

# 3. Lock distribuido (evitar doble canje de tarjeta regalo en 2 pods)
lock = r.lock(f"gift:{card_code}", timeout=5, blocking_timeout=2)
with lock:                       # SET NX + expiración: el lock muere si el pod muere
    descontar_del_ledger(...)

# 4. Leaderboard (top ventas) — sorted sets
r.zincrby("sales:ranking", importe, event_id)
r.zrevrange("sales:ranking", 0, 9, withscores=True)

# 5. Cola simple (broker de Celery: Lección 29)
r.lpush("jobs:emails", job_id)

Lo que hay que saber: Redis es single-threaded (comandos atómicos: por eso INCR y SET NX resuelven concurrencia sin locks de tu parte), en memoria (la durabilidad depende de configuración RDB/AOF: no es tu fuente de verdad), y sin SQL (patrones de acceso por clave: si necesitas "todos los X con condición", no es Redis).

3. JSONB: el NoSQL que ya tienes

Campos semi-estructurados con índice, dentro de Postgres:

sql
ALTER TABLE events_event ADD COLUMN metadata JSONB DEFAULT '{}';
CREATE INDEX idx_event_metadata ON events_event USING GIN (metadata);

SELECT * FROM events_event
WHERE metadata @> '{"venue_type": "stadium"}';   -- índice GIN lo sirve

Cuándo: atributos que varían por evento (configuración de aforo por zona, preferencias del organizador) sin schema que evolucione por migración. Qué pierdes: constraints por campo, NOT NULLs, tipos duros. Regla: JSONB para decoraciones; columnas para identidad y dinero.

4. Anti-patrones que verás (y cómo responder)

  • "Mongo porque el schema cambia rápido" → el schema cambia por migraciones versionadas justamente para controlar ese cambio (Lección 11). Lo que cambia cada día no es el schema: es la disciplina.
  • "Redis como BD primaria" → la cola pierde mensajes si Redis cae (persistencia parcial), no hay queries ad hoc, todo cabe en RAM. Redis es cofactor, no protagonista.
  • "Polyglot everywhere" → cada almacén nuevo es: otro deploy, otro backup, otro failure mode, otro set de drivers. El coste se paga en operación (Módulo 9), no en el MVP.
  • Cache stampede → 1000 peticiones golpean la BD al expirar la caché caliente. Antídoto: TTL con jitter, o lock de regeneración (solo un worker recalcula).

Autoevaluación

  1. ¿Qué tres preguntas haces antes de añadir un almacén nuevo al stack?
  2. ¿Por qué INCR de Redis resuelve el rate limit sin race condition y un SELECT + UPDATE de Postgres (sin FOR UPDATE) no?
  3. ¿Qué pasaría en producción si el lock de tarjeta regalo no tuviera TTL y el pod muera con el lock tomado?
  4. ¿Cuándo eliges JSONB sobre columnas reales, y qué constraint pierdes en el camino?
  5. El rate de aciertos de tu caché de disponibilidad es del 40% con TTL 10s. ¿Qué miras: TTL, clave, o que la consulta no debía cachearse?

Continúa con los ejercicios. Las soluciones solo tras intentarlo.