Módulo 2 · Bases de datos

Lección 08 — Modelado de datos

Normalización, relaciones y cuándo desnormalizar: el diseño como consecuencia de las invariantes.

Publicada
En esta lección
  1. Ejercicio 1 — Auditoría
  2. Ejercicio 2 — Snapshot vs derivado
  3. Ejercicio 3 — Tarjeta regalo
  4. Ejercicio 4 — Polimórfico
  5. Ejercicio 5 — Auditoría real
  6. Resumen del profesor

Ejercicio 1 — Auditoría

  1. No hay dependencias transitivas: event.title depende del id; reservation_item.price del ítem. La sospecha clásica sería un event.organizer_name, que no existe — bien.
  2. gateway_reference es un snapshot del identificador externo en el momento del pago. Si la pasarela actualiza referencias, la corrección es un payment_event (historial de callbacks) o nueva fila de pago — nunca un UPDATE silencioso que destruya el histórico (auditoría).
  3. event.slug con UNIQUE — identidad de negocio aparte de la PK. Generación: título + id corto, y NUNCA reutilizable (slug viejo → 410 en la 13).

Ejercicio 2 — Snapshot vs derivado

  1. total de reserva: derivado materializable. Patrón: se escribe una vez al confirmar (o al añadir ítem) y se lee muchas. Yo lo materializaría al confirmar la reserva con SUM de ítems dentro de la misma transacción (un lugar: el método confirm()), no en cada lectura.
  2. Si no se materializa: SUM al vuelo con la técnica de CTEs de la 07 (correcto y simple). Si se materializa: recompute en la misma transacción y CHECK opcional total >= 0. Lo inaceptable: tres lugares distintos calculándolo distinto.

Ejercicio 3 — Tarjeta regalo

sql
CREATE TABLE gift_card (
  id BIGSERIAL PRIMARY KEY,
  code TEXT UNIQUE NOT NULL,          -- identidad de negocio
  initial_amount DECIMAL(10,2) NOT NULL CHECK (initial_amount > 0),
  state TEXT NOT NULL DEFAULT 'ACTIVE',  -- ACTIVE | REDEEMED | DISABLED
  created_by BIGINT NOT NULL REFERENCES "user"(id)
);

CREATE TABLE gift_card_ledger (
  id BIGSERIAL PRIMARY KEY,
  gift_card_id BIGINT NOT NULL REFERENCES gift_card(id),
  reservation_id BIGINT REFERENCES reservation(id),  -- NULL si es carga inicial
  amount DECIMAL(10,2) NOT NULL,      -- +carga, -gasto
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
  1. Estados: ACTIVE (saldo), REDEEMED (agotada), DISABLED (baja/robo).
  2. Sí puede pagarse parcialmente en varias reservas: el ledger (libro mayor) lo permite; un campo balance mutable no da historial ni concurrencia segura.
  3. El saldo es SUM(amount) FROM ledger (o materializado con regla de recompute en la transacción). Un balance solo pierde el "quién gastó qué cuándo" — insustituible para soporte y auditoría. El gasto se inserta en el ledger dentro de la transacción de la reserva (Lección 10: con bloqueo de la tarjeta para evitar doble gasto concurrente).

Ejercicio 4 — Polimórfico

  1. Problemas: (a) sin FK real: puedes borrar el evento y el descuento queda huérfano; (b) consultas con ORs por tipo que matan los índices; (c) reglas distintas por tipo escondidas en IFs; (d) el "global" no tiene tabla destino — el diseño ya no sabe qué es.
  2. Rediseño: discount(code, pct, scope) con scope = 'GLOBAL' | referencia directa opcional event_id NULLable (FK real). La aplicación a reserva: reservation.discount_id NULLable + discount_amount snapshot en el ítem o en la reserva. Si los descuentos aplican por usuario: discount_user (N:M) — tabla puente honesta.
  3. Cupón de uso único por reserva: reservation.discount_id UNIQUE parcial (WHERE discount_id IS NOT NULL) — un cupón, una reserva — más el conteo de usos del cupón en su tabla si admite N usos.

Ejercicio 5 — Auditoría real

Respuesta esperada (varía por proyecto): TIMESTAMPTZ en todo (no naive), DECIMAL en dinero, status con choices acotadas, índices con los de la 00b presentes. Los is_* sospechosos: is_featured (ok, atributo) vs is_cancelled (mal: ya existe state). Mejoras típicas para la 11: cambiar algún campo a NOT NULL con default, añadir CHECK de coherencia (expires_at > created_at), añadir slug único al evento.


Resumen del profesor

  • Normalizar = una verdad, un lugar; desnormalizar = guardar otra verdad (snapshot) o un cálculo caro. Nunca guardar dos veces LA misma verdad.
  • PK surrogate + identidad de negocio única: las dos cosas, con nombres distintos.
  • El dinero: DECIMAL siempre; el tiempo: TIMESTAMPTZ; los estados: choices acotadas.
  • Polimórfico y EAV: flexibilidad que se factura en integridad y rendimiento. Tablas honestas > trucos.

Después de la corrección: Lección 09 — Índices y EXPLAIN.