Stack: PostgreSQL · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 07 — SQL avanzado
Objetivos
- Formalizar el proceso de modelado (la 00b fue la práctica; esta es la teoría).
- Aplicar normalización con criterio: 1FN-3FN y cuándo romperla a propósito.
- Elegir claves, tipos y relaciones conscientemente (no "porque Django lo pone").
- 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)
- Casos de uso → qué consultas y transacciones debe servir el esquema.
- Invariantes → qué no puede pasar nunca (I1-I4): el esquema debe impedirlo.
- Entidades y relaciones → cardinalidades explícitas (1:1, 1:N, N:M con atributos propios).
- Claves y tipos → identidad, unicidad, tipos con dominio real.
- 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
| Forma | Regla | En TicketFlow |
|---|---|---|
| 1FN | valores atómicos, sin grupos repetidos | no guardar "seats: 12,13,14" en una columna |
| 2FN | sin dependencias parciales de una clave compuesta | ítem depende de su id, no de (reserva, asiento) a medias |
| 3FN | sin dependencias transitivas | el 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_purchaseen 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_salesactualizado 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 (
identero o UUID) para todo; la identidad de negocio va en constraintsUNIQUEaparte (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:
DECIMALpara dinero (¡nunca float!),TIMESTAMPTZpara todo instante (UTC dentro),TEXTcon CHECK o enums de Django para estados acotados,BOOLEANsin ternarios escondidos (NULL ≠ false). - Nulos: cada columna NULL debe justificarse ("aún no conocido" vs "no aplica").
gateway_referenceNULL 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_idapuntando 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_v2apilados 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)
| Invariante | Dónde vive en el esquema |
|---|---|
| I1: asiento no vendible dos veces | UNIQUE condicional en reservation_item (00b) |
| I2: reserva caduca | expires_at NOT NULL + índice (status, expires_at) |
| I3: pago único | payment.idempotency_key UNIQUE |
| I4: no reservar evento pasado/cancelado | CHECK + regla de servicio (la BD cubre lo estructural) |
Autoevaluación
- ¿Por qué "snapshot" (
price_at_purchase) no es una violación dañina de 3FN? ¿Qué la diferencia de una redundancia accidentale? - Dame dos razones para separar PK surrogate de identidad de negocio.
- ¿Por qué
DECIMALy noFLOATpara dinero? ¿Qué bug concreto produce float? - Un compañero propone
comment(related_type, related_id)para comentarios sobre eventos o reservas. ¿Qué propones en su lugar y por qué? - ¿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.