lección 5
Acumulados y medias móviles: FRAMES de ventana
ROWS BETWEEN, running totals, moving averages. El "marco" de la ventana explicado visualmente.
⏱ 50 min
Hasta ahora hemos usado window functions donde cada fila veía TODO su grupo (PARTITION BY sin restricciones). Pero hay situaciones donde no quieres el total de todo el grupo — quieres el total HASTA ESTA FILA (un acumulado), o el promedio de LAS ÚLTIMAS 3 FILAS (una media móvil). Aquí es donde entran los frames de ventana: la parte más poderosa y menos entendida de las window functions.
La analogía: imagina que estás en un tren mirando por la ventana. La ventana tiene un tamaño fijo — quizás ves 3 vagones hacia atrás y 1 hacia adelante. A medida que el tren avanza, la ventana se mueve contigo, siempre mostrándote los mismos N elementos relativos a tu posición. Eso es exactamente un frame: una sub-ventana que se desliza con cada fila.
### Running total: el acumulado progresivo
El running total (suma acumulada) es probablemente el caso de uso más común de frames. "¿Cuánto llevamos vendido HASTA HOY?" No el total del mes completo, sino el total que crece día a día. Es lo que ves en tu cuenta bancaria: el saldo es un running total de todos tus movimientos.
1-- Running total: suma acumulada dia a dia2SELECT3 fecha,4 importe_diario,5 SUM(importe_diario) OVER (6 ORDER BY fecha7 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW8 ) AS acumulado9FROM ventas_diarias;1011-- Resultado:12-- 2024-01-01 | 100 | acumulado: 10013-- 2024-01-02 | 150 | acumulado: 250 (100+150)14-- 2024-01-03 | 80 | acumulado: 330 (100+150+80)15-- 2024-01-04 | 200 | acumulado: 530 (100+150+80+200)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW = "desde el inicio hasta aquí"
### La sintaxis del frame: ROWS BETWEEN ... AND ...
El frame define qué filas de la partición participan en el cálculo para cada fila. La sintaxis completa es ROWS BETWEEN [inicio] AND [fin], donde inicio y fin pueden ser:
- UNBOUNDED PRECEDING — desde la primera fila de la partición
- N PRECEDING — N filas antes de la actual
- CURRENT ROW — la fila actual
- N FOLLOWING — N filas después de la actual
- UNBOUNDED FOLLOWING — hasta la última fila de la partición
Trampa sutil: cuando usas SUM() OVER (PARTITION BY x ORDER BY y) SIN especificar frame, SQL aplica implícitamente RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — es decir, un acumulado. Si quieres el total completo de la partición, omite el ORDER BY o especifica ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
Consejo práctico: ROWS cuenta filas literalmente (la anterior, la de hace 2, etc.) — es rápido y predecible. RANGE agrupa por valor: si hay 5 filas con la misma fecha, las incluye todas aunque solo pidieras "1 fila anterior". En la práctica, si tus datos están ordenados por fecha sin duplicados, ambos dan lo mismo. Usa ROWS cuando quieras un número fijo de filas y RANGE cuando te importe el valor (ej: "todos los del mismo día").
1-- ROWS vs RANGE: la diferencia se ve con valores repetidos2WITH datos AS (3 SELECT * FROM (VALUES4 (1, 100), (2, 100), (3, 100), (4, 200), (5, 300)5 ) AS t(id, importe)6)7SELECT8 id, importe,9 SUM(importe) OVER (ORDER BY importe ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_rows,10 SUM(importe) OVER (ORDER BY importe RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sum_range11FROM datos;1213-- Resultado:14-- id=1 | 100 | sum_rows: 100 | sum_range: 300 ← RANGE incluye los 3 de valor 10015-- id=2 | 100 | sum_rows: 200 | sum_range: 300 ← mismo: agrupa por VALOR16-- id=3 | 100 | sum_rows: 300 | sum_range: 30017-- id=4 | 200 | sum_rows: 500 | sum_range: 500 ← aquí coinciden (no hay empates)18-- id=5 | 300 | sum_rows: 800 | sum_range: 800
ROWS avanza fila a fila (100, 200, 300...). RANGE salta al final del grupo de valores iguales (300, 300, 300...).
### Media móvil: suavizar el ruido
Las ventas diarias son ruidosas: un lunes vende poco, un viernes vende mucho, el Black Friday dispara todo. Para ver la TENDENCIA real, usas una media móvil: el promedio de los últimos N días. Suaviza los picos y muestra hacia dónde vas realmente. Es exactamente lo que hacen los gráficos de bolsa.
1-- Media movil de 7 dias (suaviza ruido semanal)2SELECT3 fecha,4 importe_diario,5 ROUND(6 AVG(importe_diario) OVER (7 ORDER BY fecha8 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW9 ), 210 ) AS media_movil_7d11FROM ventas_diarias;1213-- Media movil de 30 dias (tendencia mensual)14SELECT15 fecha,16 importe_diario,17 ROUND(18 AVG(importe_diario) OVER (19 ORDER BY fecha20 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW21 ), 222 ) AS media_movil_30d23FROM ventas_diarias;
6 PRECEDING + CURRENT ROW = 7 filas en total. Para 30 días: 29 PRECEDING.
### Acumulado con reset mensual
Un patrón muy útil: quieres el acumulado de ventas pero que se reinicie cada mes. Así ves "ícuánto llevamos este mes?" en vez del total desde el inicio de los tiempos. La clave: PARTITION BY el mes.
1-- Acumulado que se reinicia cada mes2SELECT3 fecha,4 importe_diario,5 SUM(importe_diario) OVER (6 PARTITION BY DATE_TRUNC('month', fecha)7 ORDER BY fecha8 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW9 ) AS acumulado_mes10FROM ventas_diarias11ORDER BY fecha;1213-- Resultado:14-- 2024-01-01 | 100 | acumulado_mes: 10015-- 2024-01-02 | 150 | acumulado_mes: 25016-- ...17-- 2024-01-31 | 200 | acumulado_mes: 4500 (total de enero)18-- 2024-02-01 | 180 | acumulado_mes: 180 → se reinicio!
PARTITION BY mes hace que el acumulado se reinicie al cambiar de mes.
Consejo de senior: las medias móviles de 7 y 30 días son estándar en dashboards de producto. Si estás construyendo un dashboard de métricas de negocio, las medias móviles son la forma correcta de mostrar tendencias sin que un día atípico distorsione la historia.
### Porcentaje acumulado: la curva de Pareto
Combinar un running total con el total global te da el porcentaje acumulado — la base del análisis de Pareto (el famoso 80/20). "¿Qué porcentaje de mis clientes genera el 80% de los ingresos?"
1-- Analisis Pareto: que % de clientes genera el 80% de ingresos2WITH clientes_revenue AS (3 SELECT4 cliente_id,5 SUM(importe) AS revenue6 FROM pedidos7 WHERE estado = 'completado'8 GROUP BY cliente_id9)10SELECT11 cliente_id,12 revenue,13 SUM(revenue) OVER (ORDER BY revenue DESC) AS acumulado,14 ROUND(15 100.0 * SUM(revenue) OVER (ORDER BY revenue DESC)16 / SUM(revenue) OVER (), 117 ) AS pct_acumulado18FROM clientes_revenue19ORDER BY revenue DESC;
Cuando pct_acumulado llega a 80, has encontrado a tus clientes "vitales" (Pareto).
El análisis de Pareto con window functions es una pregunta CLÁSICA de entrevistas en empresas de datos. Apréndela bien: agrupar → ordenar por valor DESC → acumulado → porcentaje acumulado → filtrar al 80%.
## ejercicios
Acumulado de ventas diarias
El dashboard de dirección necesita una línea que muestre el acumulado de ventas día a día para 2024. Agrupa por día, calcula el total diario, y añade una columna con el acumulado progresivo.
Media móvil de 7 días por ciudad
El equipo de operaciones necesita la media móvil de 7 días de las ventas, separada por ciudad. Para cada ciudad y día: total_dia, media_movil_7d.
Análisis de Pareto: el 80/20 de tus clientes
Encuentra qué porcentaje de clientes genera el 80% de los ingresos. Muestra: cliente_id, revenue, acumulado, pct_acumulado. Filtra hasta que pct_acumulado <= 80.
Acumulado con reset mensual
El controller quiere ver el acumulado de ventas que se reinicia cada mes. Para cada día de 2024: fecha, importe_dia, acumulado_mes (que vuelve a 0 al empezar un nuevo mes).
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...