Ejercicio 1 — CTE de compradores repetidores
WITH confirmadas AS (
SELECT user_id, COUNT(*) AS n
FROM reservation WHERE status = 'CONFIRMED' GROUP BY user_id
),
gasto AS (
SELECT r.user_id, SUM(p.amount) AS total
FROM reservation r JOIN payment p ON p.reservation_id = r.id
WHERE r.status = 'CONFIRMED' AND p.status = 'SUCCEEDED'
GROUP BY r.user_id
)
SELECT u.email, c.n, g.total
FROM confirmadas c
JOIN "user" u ON u.id = c.user_id
JOIN gasto g ON g.user_id = c.user_id
WHERE c.n >= 3
ORDER BY c.n DESC;- Sin
WITHes la misma consulta con subconsultas anidadas: funciona, pero el ojo debe saltar dentro-fuera para entender. CTE gana en review. (La versión conHAVINGtambién es válida para el filtro n>=3.)
Ejercicio 2 — Ranking con ventana
RANK()deja huecos con empates (1,1,3);DENSE_RANK()no (1,1,2). Para "top ventas" da igual; para "posición en una lista paginada" usaROW_NUMBER().LAG(SUM(...)) OVER (ORDER BY e.starts_at)y la diferencia con el valor actual; en el primer eventoLAGda NULL → envuélvelo conCOALESCE(..., 0).- Patrón top-N por grupo:
SELECT * FROM (
SELECT r.*, ROW_NUMBER() OVER (PARTITION BY event_id ORDER BY created_at) AS rn
FROM reservation r
) t WHERE rn <= 2;Ejercicio 3 — El duplicado silencioso
- Con 1 reserva, 2 ítems y 1 pago: el JOIN produce 2 filas (cada ítem cruza con el pago), así que
SUM(p.amount)= 2×el pago. Es el error de grano: agregas a nivel reserva pero el JOIN explotó a nivel ítem. - La versión con CTEs
pagos/itemsagrega cada relación a granoreservation_idantes de unir: importes correctos. Moraleja: agrega primero, cruza después.
Ejercicio 4 — Disponibilidad
- Deben coincidir (misma semántica que la subconsulta de la 00b). Si no, revisa los estados filtrados.
LEFT JOIN reservation_item ri... LEFT JOIN reservation r... WHERE r.id IS NULL: válida y a veces más rápida;NOT EXISTSexpresa la intención ("no existe reserva activa") y es la que recomendaría en review.ORDER BY s.sector, s.row, s.number LIMIT 50— paginación en la BD, no en Python.
Ejercicio 5 — Serie y conversión
- Con el pago de ayer, el día aparece con importe; sin él, 0.00. El truco es que la serie es la tabla base y el LEFT JOIN solo decora: así no desaparecen días vacíos.
- Expiración:
COUNT() FILTER (WHERE r.status = 'CANCELLED' AND r.expires_at < now())::float / NULLIF(COUNT(), 0)sobre reservas; si quieres precisión semántica distingue canceladas-por-usuario de expiradas por timeout (el job de la 29 marcará la causa).
Ejercicio 6 — El árbol
- La CTE con guardia:
WITH RECURSIVE org_tree AS (
SELECT id, parent_id, name, 0 AS depth FROM org WHERE id = :root_id
UNION ALL
SELECT o.id, o.parent_id, o.name, t.depth + 1
FROM org o JOIN org_tree t ON o.parent_id = t.id
WHERE t.depth < 20 -- la guardia anti-ciclo
)
...- El ciclo A→B→A sin guardia: el paso recursivo re-encuentra A indefinidamente hasta que Postgres aborta ("statement recursion depth exceeded" en versiones nuevas, o memoria/timeout en las viejas: el desastre es real, no teórico). Con guardia: termina a depth 20 con filas repetidas del ciclo — el dato corrupto necesita fix, la query solo contuvo el daño.
- Sin índice en parent_id: cada hop = seq scan de org (el árbol de 10k orgs: 10k filas leídas × 3 hops). Con índice: index scan por hop (~3-5 filas). El costo total baja de decenas de ms a µs: el 09 aplicado al recursivo.
Ejercicio 7 — Los dos granos
- Los GROUPING SETS:
SELECT e.name,
DATE(r.created_at) AS dia,
SUM(r.total_amount) AS total,
GROUPING(e.id, DATE(r.created_at)) AS nivel -- 0=fila evento×día, 1=fila evento
FROM events e JOIN reservations r ON r.event_id = e.id AND r.status='CONFIRMED'
GROUP BY GROUPING SETS ((e.id), (e.id, DATE(r.created_at)));El cuadre: la fila nivel evento = la SUMA de sus filas nivel evento×día (el assert del test de la 08). Si no cuadra: hay datos con fecha NULL (GROUPING los agrupa aparte) — el hallazgo típico.
- El plan: GROUPING SETS = UNA pasada de agregación (más rápido en datasets grandes: una lectura); el subquery = 2 pasadas pero el plan puede ser más simple con índices selectivos por evento. Con 10k eventos × 90 días: GROUPING SETS gana ~2×; con consultas dirigidas a UN evento, el subquery basta. La regla: informe completo → sets; detalle dirigido → subquery.
- El "grano" de cada informe: ventas de la portada = fila por EVENTO; el dashboard del 31 = fila por evento×día; el ledger = fila por TRANSACCIÓN (el más fino, la fuente de todos). Tres granos, una misma verdad contable: el chequeo es que los más finos sumen los más gruesos.
Resumen del profesor
- CTE para legibilidad; agrega a grano correcto antes de cruzar; ventanas para rankings/series sin colapsar filas.
NOT EXISTSexpresa intención;generate_series+ LEFT JOIN para series completas;FILTERpara métricas condicionales.- SQL es una habilidad de lectura tanto como de escritura: la query de review se agradece como el código.
Después de la corrección: Lección 08 — Modelado de datos.