Ejercicio 1 — Auditoría
- No hay dependencias transitivas:
event.titledepende del id;reservation_item.pricedel ítem. La sospecha clásica sería unevent.organizer_name, que no existe — bien. gateway_referencees un snapshot del identificador externo en el momento del pago. Si la pasarela actualiza referencias, la corrección es unpayment_event(historial de callbacks) o nueva fila de pago — nunca un UPDATE silencioso que destruya el histórico (auditoría).event.slugconUNIQUE— 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
totalde 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 conSUMde ítems dentro de la misma transacción (un lugar: el métodoconfirm()), no en cada lectura.- Si no se materializa:
SUMal vuelo con la técnica de CTEs de la 07 (correcto y simple). Si se materializa: recompute en la misma transacción y CHECK opcionaltotal >= 0. Lo inaceptable: tres lugares distintos calculándolo distinto.
Ejercicio 3 — Tarjeta regalo
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()
);- Estados: ACTIVE (saldo), REDEEMED (agotada), DISABLED (baja/robo).
- Sí puede pagarse parcialmente en varias reservas: el ledger (libro mayor) lo permite; un campo
balancemutable no da historial ni concurrencia segura. - El saldo es
SUM(amount) FROM ledger(o materializado con regla de recompute en la transacción). Unbalancesolo 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
- 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.
- Rediseño:
discount(code, pct, scope)con scope = 'GLOBAL' | referencia directa opcionalevent_idNULLable (FK real). La aplicación a reserva:reservation.discount_idNULLable +discount_amountsnapshot en el ítem o en la reserva. Si los descuentos aplican por usuario:discount_user(N:M) — tabla puente honesta. - Cupón de uso único por reserva:
reservation.discount_idUNIQUE 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.