lección 2
El problema que resuelven las window functions
Quiero el detalle Y el resumen al mismo tiempo. Problemas reales que GROUP BY no puede resolver solo.
⏱ 55 min
Hay un antes y un después en la vida de todo profesional de datos: el momento en que descubres las window functions. Antes de ese momento, para resolver ciertas preguntas hacías malabares con subconsultas correlacionadas, self-joins absurdos, o — seamos honestos — exportabas a Excel para "hacer la fórmula ahí". Después de ese momento, escribes una línea de SQL y el resultado aparece como por arte de magia.
En esta lección no vamos a aprender la sintaxis todavía. Vamos a entender el PROBLEMA. Porque si no entiendes por qué existen las window functions, la sintaxis te parecerá arbitraria y la olvidarás en una semana. Pero si entiendes el dolor que resuelven, la sintaxis se vuelve obvia.
### El muro de GROUP BY
Ya dominas GROUP BY. Sabes calcular totales por categoría, medias por región, conteos por mes. Pero GROUP BY tiene una limitación fundamental: COLAPSA filas. Cuando agrupas por ciudad, pierdes el detalle de cada cliente individual. Es como mirar un mapa de España donde solo ves las comunidades autónomas — útil para una vista general, pero no puedes ver las calles.
Ahora imagina que tu jefe te pide: "Para cada vendedor, quiero ver sus ventas mensuales Y al lado la media del equipo ese mes". Con GROUP BY puedes calcular la media del equipo. Pero si agrupas por equipo, pierdes los vendedores individuales. Si no agrupas, no puedes calcular la media. Estás atrapado.
### Cinco problemas que NO puedes resolver con GROUP BY solo
- 01."Para cada pedido, muéstrame qué porcentaje representa del gasto total de su cliente" — necesitas el detalle (cada pedido) Y el agregado (total del cliente) en la misma fila.
- 02."Dame los 3 productos más vendidos de CADA categoría" — el top 3 global es fácil con LIMIT. El top 3 POR GRUPO requiere algo más.
- 03."Para cada venta, muéstrame cuánto creció o bajó respecto a la venta anterior del mismo cliente" — necesitas acceder a la fila anterior, cosa que GROUP BY no hace.
- 04."Calcula el acumulado de ventas día a día para ver cuándo superamos el objetivo mensual" — necesitas una suma que crece fila a fila, no un total final.
- 05."Detecta clientes cuyo último pedido fue hace más de 90 días sin perder el resto de su historial" — necesitas comparar la fecha más reciente con hoy, pero manteniendo todas las filas.
Cada uno de estos problemas tiene una solución sin window functions (subconsultas, self-joins, variables temporales), pero son soluciones feas, lentas y frágiles. Las window functions los resuelven de forma elegante, eficiente y legible.
### La analogía: la maratón
Imagina que estás corriendo una maratón con 500 corredores. Mientras corres, quieres saber tres cosas simultáneamente: tu posición actual ("voy 47 de 500"), tu diferencia con el corredor de delante ("le saco 12 segundos"), y tu tiempo acumulado por kilómetro. Lo crucial: para obtener esa información NO necesitas que nadie pare. La carrera sigue, cada corredor mantiene su posición individual, pero tienes datos calculados SOBRE el grupo al lado de cada individuo.
GROUP BY sería parar la carrera, agrupar por categoría (edad, sexo, club), calcular el tiempo medio de cada grupo, y devolver UNA fila por grupo. Pierdes a los 500 corredores — solo ves promedios. Window functions: siguen los 500 corredores, pero al lado de cada uno aparece su posición en su categoría, su diferencia con el anterior, su acumulado. Detalle + resumen. Los dos a la vez.
### La sintaxis: OVER es la palabra mágica
La sintaxis de una window function tiene tres ingredientes. La función (qué calcular), la palabra OVER (que indica "esto es una ventana, no un GROUP BY"), y la especificación de la ventana (sobre qué grupo y en qué orden):
1-- Estructura:2funcion(argumentos) OVER (3 PARTITION BY columna_grupo -- divide en ventanas (opcional)4 ORDER BY columna_orden -- ordena dentro de cada ventana (opcional)5)67-- Ejemplo real: cada pedido + gasto total de su cliente8SELECT9 id,10 cliente_id,11 fecha,12 importe,13 SUM(importe) OVER (PARTITION BY cliente_id) AS total_cliente14FROM pedidos;
PARTITION BY es como un GROUP BY que NO colapsa. ORDER BY cambia el marco de la ventana.
Lee SUM(importe) OVER (PARTITION BY cliente_id) así: "la suma del importe, calculada para cada grupo de cliente_id, pero sin colapsar las filas". Cada fila mantiene sus datos originales y ADEMÁS obtiene el total de su grupo. Es como si al lado de cada pedido apareciera un post-it con "por cierto, este cliente ha gastado 2.340€ en total".
### Las tres familias de window functions
- RANKING: ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE() — "¿en qué posición está esta fila?"
- NAVEGACIÓN: LAG(), LEAD(), FIRST_VALUE(), LAST_VALUE() — "¿qué valor tiene la fila anterior/siguiente?"
- AGREGACIÓN: SUM(), AVG(), COUNT(), MIN(), MAX() con OVER — las mismas que ya conoces, pero sin colapsar filas.
### Primer ejemplo completo: el informe que impresiona
El director de ventas te pide: "Para cada pedido, quiero ver el importe, qué porcentaje representa del total del cliente, cuántos pedidos ha hecho ese cliente en total, y si este pedido está por encima o por debajo de su media". Sin window functions, esto serían 3-4 subconsultas. Observa:
1SELECT2 id,3 cliente_id,4 fecha,5 importe,6 -- Total del cliente (sin perder filas)7 SUM(importe) OVER (PARTITION BY cliente_id) AS total_cliente,8 -- Porcentaje que este pedido representa9 ROUND(100.0 * importe / SUM(importe) OVER (PARTITION BY cliente_id), 1) AS pct_del_total,10 -- Numero de pedidos del cliente11 COUNT(*) OVER (PARTITION BY cliente_id) AS pedidos_cliente,12 -- Diferencia vs la media del cliente13 ROUND(importe - AVG(importe) OVER (PARTITION BY cliente_id), 2) AS diff_vs_media14FROM pedidos15WHERE estado = 'completado'16ORDER BY cliente_id, fecha;
Cuatro métricas de contexto en una sola pasada. Sin subconsultas, sin self-joins.
Consejo de senior: las window functions son LA PREGUNTA favorita en entrevistas técnicas de datos. Si dominas ROW_NUMBER + PARTITION BY y el patrón CTE + filtro, ya estás por encima del 80% de los candidatos. No exagero.
### El orden de ejecución de SQL (por qué no puedes filtrar por una window function)
Esto es crucial y la gente lo olvida constantemente. SQL NO se ejecuta en el orden en que lo escribes. Se ejecuta así:
- 01.FROM / JOIN — De dónde vienen las filas
- 02.WHERE — Filtro de filas (aquí NO existen las window functions todavía)
- 03.GROUP BY — Agrupación
- 04.HAVING — Filtro de grupos
- 05.SELECT — Cálculos, incluyendo WINDOW FUNCTIONS (aquí se calculan)
- 06.DISTINCT
- 07.ORDER BY — Ordenamiento final
- 08.LIMIT / OFFSET — Paginación
1-- → ESTO FALLA: WHERE se ejecuta ANTES que la window function2SELECT *, ROW_NUMBER() OVER (ORDER BY importe DESC) AS rn3FROM pedidos4WHERE rn <= 5; -- ERROR: WHERE no puede usar window functions56-- → SOLUCIÓN: envolver en CTE y filtrar después7WITH ranked AS (8 SELECT *, ROW_NUMBER() OVER (ORDER BY importe DESC, id ASC) AS rn9 FROM pedidos10)11SELECT * FROM ranked WHERE rn <= 5;
El patrón CTE + filtro externo. Memorízalo — lo usarás constantemente.
El error más común con window functions: intentar usar el alias de la window function en el WHERE de la misma query. Siempre necesitas un nivel adicional (CTE o subconsulta). Esto NO es un bug de SQL — es consecuencia lógica del orden de ejecución.
DuckDB tiene un atajo que no existe en PostgreSQL: la cláusula QUALIFY. Es a las window functions lo que HAVING es a GROUP BY — filtra directamente por el resultado de la ventana sin necesitar CTE. Ejemplo: SELECT *, ROW_NUMBER() OVER (ORDER BY importe DESC) AS rn FROM pedidos QUALIFY rn <= 5. Aprende primero el patrón CTE + filtro, porque funciona en cualquier motor. Usa QUALIFY cuando sepas que estás en DuckDB y quieras ahorrarte el envoltorio.
### WINDOW AS: nombrar ventanas para no repetirte
Cuando usas la misma ventana en múltiples columnas, puedes nombrarla una vez y reutilizarla. Es más limpio y reduce errores de copy-paste:
1-- En vez de repetir OVER (PARTITION BY cliente_id) tres veces:2SELECT3 cliente_id,4 fecha,5 importe,6 SUM(importe) OVER w AS total_cliente,7 AVG(importe) OVER w AS media_cliente,8 COUNT(*) OVER w AS num_pedidos9FROM pedidos10WINDOW w AS (PARTITION BY cliente_id)11ORDER BY cliente_id, fecha;
WINDOW w AS (...) define la ventana una vez. úsala en todas las columnas que la necesiten.
Piensa en las window functions como "columnas calculadas con contexto". No cambian las filas, no las filtran, no las agrupan — simplemente AÑADEN información derivada del grupo al que pertenece cada fila. Es información gratis.
## ejercicios
Total por cliente sin perder detalle
Finanzas necesita un listado donde cada pedido muestre: su importe, el gasto total de ese cliente, y cuántos pedidos ha hecho. Sin subconsultas — usa window functions.
Porcentaje del gasto de la ciudad
Marketing quiere saber qué porcentaje del gasto de cada ciudad representa cada cliente. Muestra: ciudad, nombre, gasto_total, pct_de_ciudad (redondeado a 1 decimal). Ordena por ciudad y porcentaje descendente.
Comparar cada pedido con la media global
El CEO quiere un informe simple: cada pedido completado con su importe, la media global de todos los pedidos, y una columna "vs_media" que diga si es "por encima" o "por debajo". Usa CASE + window function sin PARTITION BY.
Usar WINDOW AS para limpiar la query
Reescribe esta query usando WINDOW AS para no repetir la misma ventana: para cada pedido muestra total_cliente, media_cliente, min_cliente y max_cliente (todo particionado por cliente_id).
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...