Cuadernillo de problemas resueltos: nueve consultas SQL resueltas
Escribir y depurar las consultas SQL analiticas esenciales: JOINs, anti-JOINs, agregaciones con HAVING, funciones de ventana y diagnostico de rendimiento con indices.
Cuadernillo de problemas resueltos: nueve consultas sobre una tiendita
SQL no se aprende leyendo: se aprende escribiendo consultas y peleando con resultados equivocados. Este cuadernillo resuelve sobre UNA base de datos fija —una tiendita de tres tablas— las nueve consultas que cubren el 90% del trabajo diario del analista: unir, buscar lo que falta, agrupar, filtrar grupos, comparar contra promedios, rankear, acumular, medir variacion mensual y acelerar con indices. Cada problema muestra la consulta, por que esta escrita asi y que devuelve.
Todo el cuadernillo gira sobre este esquema de tres tablas: aprendetelo de memoria para poder razonar las respuestas.
El esquema del ejercicio CLIENTES(id, nombre, ciudad). PRODUCTOS(id, nombre, categoria, precio). VENTAS(id, cliente_id, producto_id, fecha, cantidad). VENTAS es la tabla de hechos: cada fila es una linea de ticket. Las claves ajenas cliente_id y producto_id apuntan a las otras dos. Todas las consultas corren sobre Postgres; en MySQL solo cambia algo de sintaxis de fechas.
Bloque 1 · Unir y echar de menos
Problema 1. Lista que compro cada cliente: nombre del cliente, producto y fecha.
Solucion paso a paso
Inventario: el nombre vive en clientes, el producto en productos, la fecha en ventas. Dos saltos = dos JOIN.
Escribe la consulta desde la tabla de hechos hacia las dimensiones.
JOIN interno: si una venta tuviera cliente_id roto (huerfano), la fila DESAPARECE del resultado. ¿Sospecha de datos huérfanos? Usa el problema 2 al reves sobre las claves.
Alias v, c, p: acortan y obligan a calificar cada columna. Sin calificar, "nombre" seria ambiguo (existe en clientes Y productos) y la consulta moriria.
Problema 2. Encuentra los clientes que NUNCA compraron (inversion del JOIN).
Solucion paso a paso
LEFT JOIN desde clientes: conserva TODOS los clientes y rellena NULL donde no hay venta.
Luego filtra los NULL: esos son los que nunca compraron.
Trampa clasica: WHERE v.id = NULL NO funciona; NULL no es igual a nada, se pregunta con IS NULL. Con = no devuelve ni una fila y parece que "no hay inactivos".
Alternativa con subconsulta: WHERE c.id NOT IN (SELECT cliente_id FROM ventas) — con NOT IN, cuidado: si la subconsulta devuelve un solo NULL, el resultado queda vacio. NOT EXISTS es la version a prueba de balas.
Problema 3. ¿Cuantos clientes unicos tiene cada ciudad y cuanto gastan en total?
Solucion paso a paso
Agrupar por ciudad y contar: dos tablas implicadas (clientes y ventas) mas productos para el importe: tres JOINs y un GROUP BY.
SUM con multiplicacion: cada linea de ticket vale precio por cantidad.
COUNT(DISTINCT c.id): sin DISTINCT contarias FILAS de ventas (tickets), no clientes: el JOIN duplica al cliente por cada compra. Es el error de doble conteo que mas dashboard mata.
Bloque 2 · Filtrar grupos y comparar contra agregados
Problema 4. Categorias de producto que VENDAN MAS de 100 unidades acumuladas.
Solucion paso a paso
El filtro actua SOBRE EL GRUPO: eso es HAVING, no WHERE. WHERE filtra filas antes de agrupar; HAVING filtra grupos despues.
Escritura correcta.
Orden mental de ejecucion SQL: FROM y JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Por eso en WHERE no puedes usar el alias del SELECT: todavia no existe.
Problema 5. Productos caros: los que cuestan MAS que el precio promedio de su propia categoria.
Solucion paso a paso
Necesitas el promedio POR CATEGORIA y compararlo con cada fila: o ventana, o subconsulta correlacionada. Version ventana, mas limpia.
Primero calcula el promedio de categoria en una capa interior; luego filtra afuera, porque WHERE no puede ver a AVG OVER.
¿Por que el WHERE exterior? Las funciones de ventana se evaluan DESPUES de WHERE y del GROUP BY: si filtraras dentro, aun no existiria media_cat. La subconsulta (o CTE con WITH) es el puente.
Problema 6. Top 3 productos mas vendidos de cada categoria.
Solucion paso a paso
Primero agregacion: unidades por producto con su categoria.
Luego rankeo dentro de cada grupo con RANK() OVER (PARTITION BY categoria ORDER BY unidades DESC).
RANK vs DENSE_RANK vs ROW_NUMBER: empates al frente → RANK salta numeros (1,1,3), DENSE_RANK no salta (1,1,2), ROW_NUMBER corta el empate arbitrariamente (1,2,3). Para "top 3 con empates justos" decide primero que variante quieres.
Bloque 3 · Ventanas temporales y rendimiento
Problema 7. Ventas acumuladas mes a mes (curva de crecimiento).
Solucion paso a paso
Capa 1: total por mes con date_trunc.
Capa 2: la acumulada es un SUM sobre todas las filas anteriores ordenadas por mes.
SUM OVER (ORDER BY mes) sin FRAME toma el valor por defecto: desde el inicio hasta la fila actual (peers incluidos). Para comportamiento estricto escribe ROWS UNBOUNDED PRECEDING AND CURRENT ROW.
Problema 8. Variacion porcentual de cada mes contra el anterior.
Solucion paso a paso
LAG(ventas) OVER (ORDER BY mes) trae el valor de la fila anterior a la columna actual: comparar entre filas sin comparar contra si misma.
Calcula el porcentaje con cuidado del primer mes (no tiene anterior: NULL).
NULLIF(x, 0): si el mes anterior vendio 0, la division daria error; NULLIF lo convierte en NULL y la fila sobrevive. En reportes de crecimiento, el 0 previo es frecuentisimo.
Problema 9. La consulta del problema 3 vuela en tu laptop pero en produccion tarda 40 segundos. Diagnostica y acelera.
Solucion paso a paso
Primero medir, luego tocar: EXPLAIN ANALYZE muestra el plan real y donde arde.
Sospechoso usual: Seq Scan sobre ventas de millones de filas porque no existe indice para la clave ajena. Los JOIN por cliente_id usan el indice del PRIMARY de clientes pero necesitan uno en ventas.cliente_id.
Solucion tipica: indice en la columna del JOIN (y compuesta si el filtro incluye fecha).
Costo: cada indice acelera lecturas y ENLENTECE escrituras ademas de ocupar disco. No se indexa "por si acaso": se indexa lo que EXPLAIN pide. Despues del cambio, vuelve a pasar EXPLAIN y compara tiempos: sin antes y despues no hay ingenieria.
Arbol de decisiones SQL
Que construccion SQL uso
Autoexamen cronometrado (20 minutos)
Sobre el mismo esquema de la tiendita. Responde sin volver a leer los problemas.
¿Cual clausula filtra GRUPOS ya formados?
Para hallar clientes sin ventas sirve WHERE v.id = NULL.
Tienes ventas por cliente duplicadas tras un JOIN y el total sale inflado. ¿Cual es la causa mas probable?
La funcion trae el valor de la fila anterior dentro de la ventana ordenada.
Empareja necesidad y construccion.
VENTAS tiene 5000 filas (cada venta apunta a EXACTAMENTE 1 producto valido) y PRODUCTOS 200 filas. ¿Cuantas filas devuelve ventas JOIN productos?
Video de repaso
Para profundizar
PG Exercises Problemas SQL interactivos con solucion, gratis, sobre postgres.
El problema 3 con COUNT sin DISTINCT es el bug de dashboard mas frecuente del mundo laboral. Inventa un JOIN 1:N tuyo (pedidos-lineas, sesiones-eventos) y escribe la version inflada y la correcta: nota que ambas "funcionan" y solo una miente.