Un promedio móvil en SQL se calcula con AVG como función de ventana: AVG(valor) OVER (ORDER BY fecha ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) promedia cada fila con las dos anteriores, es decir, un promedio móvil de 3 periodos. Funciona en PostgreSQL, SQL Server 2012 o posterior, MySQL 8, Oracle y SQLite 3.25 o posterior. Agrega PARTITION BY para calcularlo por separado en cada producto, tienda o sitio.
Sintaxis
AVG(valor) OVER (
[PARTITION BY grupo]
ORDER BY fecha
ROWS BETWEEN n_menos_1 PRECEDING AND CURRENT ROW
)
| Marco | Qué promedia |
|---|---|
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | La fila actual y las 6 anteriores (7 filas) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | Centrado: anterior, actual y siguiente |
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | Promedio acumulado desde el inicio |
RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW | Los últimos 7 días de calendario (PostgreSQL 11+; en MySQL 8, INTERVAL 6 DAY) |
Sin la cláusula ROWS, el marco por omisión con ORDER BY es desde el inicio hasta la fila actual (con empates incluidos), o sea, un promedio acumulado y no móvil.
Paso a paso
- Asegúrate de tener una fila por periodo (día, semana, mes). Si hay varias por día, agrégalas antes con
GROUP BY. - Elige el tamaño de la ventana
ny escribeROWS BETWEEN n − 1 PRECEDING AND CURRENT ROW. - Ordena la ventana por la fecha con
ORDER BYdentro delOVER. - Si hay varias series en la tabla, agrega
PARTITION BY. - Decide qué mostrar en las primeras
n − 1filas, que tienen ventanas incompletas.
Tabla de ejemplo:
CREATE TABLE visitas (
dia DATE,
sitio VARCHAR(10),
usuarios INT
);
INSERT INTO visitas VALUES
('2026-10-01', 'web', 320), ('2026-10-02', 'web', 410), ('2026-10-03', 'web', 380),
('2026-10-04', 'web', 290), ('2026-10-05', 'web', 450), ('2026-10-06', 'web', 470),
('2026-10-01', 'app', 150), ('2026-10-02', 'app', 180), ('2026-10-03', 'app', 165),
('2026-10-04', 'app', 200);
Ejemplos resueltos
Ejemplo 1: promedio móvil de 3 días, solo con ventanas completas
SELECT dia, usuarios,
CASE WHEN COUNT(*) OVER w = 3
THEN ROUND(AVG(usuarios * 1.0) OVER w, 2)
END AS movil_3d
FROM visitas
WHERE sitio = 'web'
WINDOW w AS (ORDER BY dia ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
ORDER BY dia;
| dia | usuarios | movil_3d |
|---|---|---|
| 2026-10-01 | 320 | NULL |
| 2026-10-02 | 410 | NULL |
| 2026-10-03 | 380 | 370.0 |
| 2026-10-04 | 290 | 360.0 |
| 2026-10-05 | 450 | 373.33 |
| 2026-10-06 | 470 | 403.33 |
El 3 de octubre promedia 320, 410 y 380 (1,110 / 3 = 370). Sin el CASE, el 1 de octubre mostraría 320 y el 2, 365: promedios de 1 y 2 días que en una gráfica parecen comparables con los demás y no lo son. La cláusula WINDOW evita repetir la definición; existe en PostgreSQL, MySQL 8 y SQLite, y en SQL Server desde la versión 2022. En versiones anteriores, escribe el OVER (...) completo dos veces. El * 1.0 evita que SQL Server trunque el promedio de una columna INT.
Ejemplo 2: una serie por sitio con PARTITION BY
SELECT sitio, dia, usuarios,
ROUND(AVG(usuarios * 1.0) OVER (
PARTITION BY sitio
ORDER BY dia
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 2) AS movil_2d
FROM visitas
ORDER BY sitio, dia;
| sitio | dia | usuarios | movil_2d |
|---|---|---|---|
| app | 2026-10-01 | 150 | 150.0 |
| app | 2026-10-02 | 180 | 165.0 |
| app | 2026-10-03 | 165 | 172.5 |
| app | 2026-10-04 | 200 | 182.5 |
| web | 2026-10-01 | 320 | 320.0 |
| web | 2026-10-02 | 410 | 365.0 |
| web | 2026-10-03 | 380 | 395.0 |
| web | 2026-10-04 | 290 | 335.0 |
| web | 2026-10-05 | 450 | 370.0 |
| web | 2026-10-06 | 470 | 460.0 |
Con PARTITION BY sitio, la ventana se reinicia en cada sitio: el primer día de web no se mezcla con el último de app.
Ejemplo 3: días faltantes y ventanas por fecha
Si un día no tiene fila, ROWS cuenta filas, no días, y la ventana abarca más tiempo del que crees. Con RANGE y un intervalo, la ventana se mide en días de calendario:
-- PostgreSQL 11 o posterior
AVG(v) OVER (ORDER BY dia RANGE BETWEEN INTERVAL '2 days' PRECEDING AND CURRENT ROW)
-- MySQL 8
AVG(v) OVER (ORDER BY dia RANGE BETWEEN INTERVAL 2 DAY PRECEDING AND CURRENT ROW)
-- SQLite: ordena por un número de día
AVG(v) OVER (ORDER BY julianday(dia) RANGE BETWEEN 2 PRECEDING AND CURRENT ROW)
Con datos del 1, 2, 5 y 6 de enero (10, 20, 30 y 40), el 5 de enero la ventana por fecha (días 3 a 5) solo contiene el 30 y da 30.0; ROWS BETWEEN 2 PRECEDING mezclaría los días 1, 2 y 5 y daría 20.0. SQL Server no admite RANGE con desplazamientos: allí, completa primero los días faltantes con una tabla calendario y un LEFT JOIN, y usa ROWS.
Errores comunes
- Olvidar
ROWS BETWEEN. Con soloORDER BYobtienes un promedio acumulado, no móvil. ORDER BYde la consulta sinORDER BYen elOVER. El orden del resultado no define la ventana. La ventana necesita su propioORDER BY.- Fechas guardadas como texto con otro formato.
'05/10/2026'no se ordena bien como texto. Usa el tipoDATEo el formatoAAAA-MM-DD. - Promedio truncado en SQL Server.
AVGde una columnaINTdevuelveINTtambién como función de ventana. UsaAVG(col * 1.0); ver AVG con enteros. - Filtrar con
WHEREantes de la ventana sin querer. ElWHEREse aplica primero: si quitas días, la ventana ya no los ve. Para mostrar solo un periodo pero calcular con días previos, calcula en una subconsulta y filtra afuera.
Comprueba series cortas con la calculadora de promedio móvil. El mismo cálculo con pandas está en promedio móvil en Python.
Fuentes
- PostgreSQL, llamadas a funciones de ventana. Marcos ROWS y RANGE con desplazamiento. Consultado el 11 de noviembre de 2026.
- Microsoft Learn, cláusula OVER (Transact-SQL). ROWS y RANGE en SQL Server. Consultado el 11 de noviembre de 2026.
- SQLite, funciones de ventana. Marcos ROWS, RANGE y GROUPS. Consultado el 11 de noviembre de 2026.