Saltar al contenido

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 dia
2SELECT
3 fecha,
4 importe_diario,
5 SUM(importe_diario) OVER (
6 ORDER BY fecha
7 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
8 ) AS acumulado
9FROM ventas_diarias;
10
11-- Resultado:
12-- 2024-01-01 | 100 | acumulado: 100
13-- 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
El frame define cuántas filas "ve" tu función en cada paso.

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 repetidos
2WITH datos AS (
3 SELECT * FROM (VALUES
4 (1, 100), (2, 100), (3, 100), (4, 200), (5, 300)
5 ) AS t(id, importe)
6)
7SELECT
8 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_range
11FROM datos;
12
13-- Resultado:
14-- id=1 | 100 | sum_rows: 100 | sum_range: 300 ← RANGE incluye los 3 de valor 100
15-- id=2 | 100 | sum_rows: 200 | sum_range: 300 ← mismo: agrupa por VALOR
16-- id=3 | 100 | sum_rows: 300 | sum_range: 300
17-- 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)
2SELECT
3 fecha,
4 importe_diario,
5 ROUND(
6 AVG(importe_diario) OVER (
7 ORDER BY fecha
8 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
9 ), 2
10 ) AS media_movil_7d
11FROM ventas_diarias;
12
13-- Media movil de 30 dias (tendencia mensual)
14SELECT
15 fecha,
16 importe_diario,
17 ROUND(
18 AVG(importe_diario) OVER (
19 ORDER BY fecha
20 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
21 ), 2
22 ) AS media_movil_30d
23FROM 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 mes
2SELECT
3 fecha,
4 importe_diario,
5 SUM(importe_diario) OVER (
6 PARTITION BY DATE_TRUNC('month', fecha)
7 ORDER BY fecha
8 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
9 ) AS acumulado_mes
10FROM ventas_diarias
11ORDER BY fecha;
12
13-- Resultado:
14-- 2024-01-01 | 100 | acumulado_mes: 100
15-- 2024-01-02 | 150 | acumulado_mes: 250
16-- ...
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 ingresos
2WITH clientes_revenue AS (
3 SELECT
4 cliente_id,
5 SUM(importe) AS revenue
6 FROM pedidos
7 WHERE estado = 'completado'
8 GROUP BY cliente_id
9)
10SELECT
11 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 (), 1
17 ) AS pct_acumulado
18FROM clientes_revenue
19ORDER 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

[01]

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.

Cargando editor...
[02]

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.

Cargando editor...
[03]

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.

Cargando editor...
[04]

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).

Cargando editor...

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...