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. Objetivos
  2. 1. El proceso (recap formal de la 00b)
  3. 2. Normalización con criterio
  4. 3. Claves y tipos con dominio real
  5. 4. Jerarquías, polimorfismo y anti-patrones
  6. 5. De la invariante al esquema (cerrando I1-I4)
  7. Autoevaluación

Stack: PostgreSQL · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 07 — SQL avanzado


Objetivos

  1. Formalizar el proceso de modelado (la 00b fue la práctica; esta es la teoría).
  2. Aplicar normalización con criterio: 1FN-3FN y cuándo romperla a propósito.
  3. Elegir claves, tipos y relaciones conscientemente (no "porque Django lo pone").
  4. Modelar jerarquías y estados sin trucos que luego dueñen (polimórficos, EAV, flags de tipo).

1. El proceso (recap formal de la 00b)

  1. Casos de uso → qué consultas y transacciones debe servir el esquema.
  2. Invariantes → qué no puede pasar nunca (I1-I4): el esquema debe impedirlo.
  3. Entidades y relaciones → cardinalidades explícitas (1:1, 1:N, N:M con atributos propios).
  4. Claves y tipos → identidad, unicidad, tipos con dominio real.
  5. Normalización → quitar redundancia donde daña; romperla donde compensa (snapshot).

La secuencia importa: el esquema es la consecuencia de los casos de uso y las invariantes, no un diagrama estético.

2. Normalización con criterio

FormaReglaEn TicketFlow
1FNvalores atómicos, sin grupos repetidosno guardar "seats: 12,13,14" en una columna
2FNsin dependencias parciales de una clave compuestaítem depende de su id, no de (reserva, asiento) a medias
3FNsin dependencias transitivasel nombre del organizador NO vive en event

El sentido de normalizar: una verdad, un lugar. Si el nombre del organizador vive en cada evento, cambiarlo toca N filas (y alguien olvidará una).

Cuándo romperla a propósito (desnormalización defendible):

  • Snapshot histórico: price_at_purchase en el ítem (el precio "actual" cambia; lo facturado no). No es redundancia: es otra verdad (la del pasado).
  • Derivados caros materializados con regeneración barata: event.total_sales actualizado por trigger/job — aceptable cuando se lee mil veces más de lo que se escribe.
  • Reportes analíticos: tablas agregadas aparte (el OLTP queda normalizado; el OLAP, desnormalizado).

3. Claves y tipos con dominio real

  • PK: surrogate (id entero o UUID) para todo; la identidad de negocio va en constraints UNIQUE aparte (asiento: (event, sector, row, number)).
  • UUID vs entero: entero = más compacto y rápido; UUID v4 = no secuencial (no filtra URLs ni carga caliente en un índice) — patrón pro: PK entera interna + UUID público en la API (lo verás en la 13).
  • Tipos: DECIMAL para dinero (¡nunca float!), TIMESTAMPTZ para todo instante (UTC dentro), TEXT con CHECK o enums de Django para estados acotados, BOOLEAN sin ternarios escondidos (NULL ≠ false).
  • Nulos: cada columna NULL debe justificarse ("aún no conocido" vs "no aplica"). gateway_reference NULL hasta que la pasarela responde: correcto.

4. Jerarquías, polimorfismo y anti-patrones

  • N:M con atributos propios: reservation_item (reserva × asiento + precio). La tabla puente no es un mal necesario: es donde vive la verdad de la relación.
  • Self-referencia: user.referred_by → user.id ( referrals de TicketFlow): una FK a la misma tabla y ya.
  • Polimórfico (evítalo): columna related_type + related_id apuntando a cualquier tabla — sin FK real, sin integridad. Alternativa: tabla por tipo, o una tabla base compartida.
  • EAV (evítalo): filas clave-valor para atributos flexibles. Flexible e injugable: sin tipos, sin constraints, sin planes decentes. Si el dominio es "cada evento tiene campos distintos", mejor JSONB con GIN + validación en la app, no EAV.
  • Flags de tipo: is_vip, is_cancelled, type_v2 apilados en columnas — huele a jerarquía escondida. Si hay N tipos con comportamiento distinto, tabla hija o enum + campos anulables justificados.

5. De la invariante al esquema (cerrando I1-I4)

InvarianteDónde vive en el esquema
I1: asiento no vendible dos vecesUNIQUE condicional en reservation_item (00b)
I2: reserva caducaexpires_at NOT NULL + índice (status, expires_at)
I3: pago únicopayment.idempotency_key UNIQUE
I4: no reservar evento pasado/canceladoCHECK + regla de servicio (la BD cubre lo estructural)

Autoevaluación

  1. ¿Por qué "snapshot" (price_at_purchase) no es una violación dañina de 3FN? ¿Qué la diferencia de una redundancia accidentale?
  2. Dame dos razones para separar PK surrogate de identidad de negocio.
  3. ¿Por qué DECIMAL y no FLOAT para dinero? ¿Qué bug concreto produce float?
  4. Un compañero propone comment(related_type, related_id) para comentarios sobre eventos o reservas. ¿Qué propones en su lugar y por qué?
  5. ¿Cuándo usarías JSONB en lugar de columnas, y qué pierdes si lo haces?

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