Promedio móvil de 7 días
AvanzadoUn promedio móvil de 7 días es, para cada día, el promedio de ese día y los seis anteriores. Suaviza la variación diaria para ver la tendencia. Se escribe con avg(...) OVER (ORDER BY dia ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), sobre una serie que tenga una fila por cada día del calendario, también los días sin actividad.
Sintaxis
avg(valor) OVER (
ORDER BY dia
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)Ejemplo
WITH diario AS (
SELECT o.placed_at::date AS dia, count(*) AS pedidos
FROM orders AS o
INNER JOIN restaurants AS r ON r.id = o.restaurant_id
WHERE r.city_id = 8
AND o.placed_at >= '2025-08-01'
AND o.placed_at < '2025-08-15'
GROUP BY o.placed_at::date
), calendario AS (
SELECT d::date AS dia
FROM generate_series(date '2025-08-01', date '2025-08-14', interval '1 day') AS d
), serie AS (
SELECT
c.dia,
coalesce(di.pedidos, 0) AS pedidos,
round(avg(coalesce(di.pedidos, 0)) OVER (
ORDER BY c.dia
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS media_7_dias
FROM calendario AS c
LEFT JOIN diario AS di ON di.dia = c.dia
)
SELECT to_char(dia, 'YYYY-MM-DD') AS dia, pedidos, media_7_dias
FROM serie
WHERE dia >= '2025-08-07'
ORDER BY dia;Resultado
| dia | pedidos | media_7_dias |
|---|---|---|
| 2025-08-07 | 0 | 1.29 |
| 2025-08-08 | 3 | 1.57 |
| 2025-08-09 | 2 | 1.57 |
| 2025-08-10 | 3 | 1.86 |
| 2025-08-11 | 1 | 1.71 |
| 2025-08-12 | 1 | 1.71 |
| 2025-08-13 | 1 | 1.57 |
| 2025-08-14 | 2 | 1.86 |
Cómo leerlo
Son los pedidos diarios de los restaurantes de Montevideo (city_id = 8, días en UTC). El 7 de agosto no hubo pedidos: generate_series crea ese día en el calendario y el LEFT JOIN con coalesce lo deja en 0. El calendario arranca el 1 de agosto para que el 7 ya tenga seis días previos, y el filtro >= '2025-08-07' se aplica en la consulta de afuera, después de calcular la ventana.
Error común
Así no
SELECT
o.placed_at::date AS dia,
count(*) AS pedidos,
avg(count(*)) OVER (
ORDER BY o.placed_at::date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS media_7_dias
FROM orders AS o
INNER JOIN restaurants AS r ON r.id = o.restaurant_id
WHERE r.city_id = 8
GROUP BY o.placed_at::date;Calcular la ventana directamente sobre los días que tienen datos. ROWS cuenta filas, no días: como el 7 de agosto no tiene fila, la ventana del 8 toma los siete días con pedidos desde el 1 de agosto y da 1.71 en lugar de 1.57. Además, el 7 de agosto desaparece del resultado. Sin un calendario completo, la media queda inflada justo en las semanas con días vacíos.
En otros motores: PostgreSQL, BigQuery, Snowflake, SQL Server
PostgreSQL
RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROWsobre una columna de fecha toma solo los días dentro de la semana, aunque falten filas. Peroavgdivide por los días que tienen datos, no por 7: para promediar por día calendario sigues necesitando los ceros.BigQuerySnowflakeSQL Server
generate_seriespara fechas es de PostgreSQL. BigQuery usaUNNEST(GENERATE_DATE_ARRAY(inicio, fin)); Snowflake arma la serie con la función de tablaGENERATOR;GENERATE_SERIESde SQL Server 2022 solo genera números. Muchos equipos tienen una tabla calendario para esto.
Estos fragmentos no se ejecutan en el curso: se muestran como referencia de cada motor.