Lesión 7.3 · Tiempo de lectura: ~9 minutos
Esta lección se centra en los marcos de ventana, el mecanismo que define qué filas participan en el cálculo de una función de ventana en relación con la fila actual. Explorarás los modos ROWS, RANGE y GROUPS, revisarás límites comunes y verás escenarios prácticos con datos de Sakila. Al final de la lección, podrás elegir los límites del marco de manera deliberada y evitar errores comunes.
Marcos de Ventana y Límites
En las lecciones anteriores, utilizamos funciones de ventana con PARTITION BY y ORDER BY. Pero la cláusula OVER ofrece un tercer componente igualmente poderoso: el marco de ventana. Un marco de ventana te permite definir con precisión qué filas alrededor de la fila actual se incluyen en el cálculo, habilitando totales acumulados, promedios móviles y muchos otros patrones de series temporales.
¿Qué es un Marco de Ventana?
Cuando escribes OVER (ORDER BY ...), muchas bases de datos aplican un marco predeterminado del que quizás no estés consciente. Especificar un marco explícitamente te da control total sobre la ventana de cálculo.
La sintaxis completa de la cláusula OVER es:
function_name() OVER (
[PARTITION BY partition_expression]
[ORDER BY sort_expression]
[frame_clause]
)
Donde frame_clause es:
{ ROWS | RANGE | GROUPS }
BETWEEN frame_start AND frame_end
Y cada límite (frame_start, frame_end) es uno de:
| Palabra clave de límite | Significado |
|---|---|
UNBOUNDED PRECEDING | La primera fila de la partición |
n PRECEDING | n filas (o unidades de rango) antes de la fila actual |
CURRENT ROW | La fila actual |
n FOLLOWING | n filas (o unidades de rango) después de la fila actual |
UNBOUNDED FOLLOWING | La última fila de la partición |
Modos de Marco: ROWS, RANGE y GROUPS
El modo de marco controla cómo se miden los límites.
Modo ROWS
ROWS cuenta filas físicas. 1 PRECEDING siempre significa exactamente la fila que viene inmediatamente antes de la fila actual en el ordenamiento.
Mejor utilizado cuando necesitas una ventana deslizante de ancho fijo (por ejemplo, un promedio móvil de 7 días sobre filas diarias).
Modo RANGE
RANGE cuenta valores lógicos. 1 PRECEDING significa todas las filas cuyo valor de ORDER BY está dentro de 1 unidad del valor de la fila actual, no necesariamente solo una fila física.
Mejor utilizado cuando deseas agregar todas las filas con el mismo valor que la fila actual, o todas las filas dentro de un rango de valores.
Importante: El marco predeterminado cuando especificas ORDER BY pero no una cláusula de marco explícita es:
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
Esto significa que la ventana incluye todas las filas desde el inicio de la partición hasta e incluyendo todas las filas con el mismo valor de ORDER BY que la fila actual.
Modo GROUPS
GROUPS cuenta grupos pares (conjuntos de filas con valores de ORDER BY idénticos). 1 PRECEDING significa el grupo completo de filas que tiene el siguiente valor más bajo. Este modo es compatible en PostgreSQL 11+ y algunas otras bases de datos, pero no en MySQL/MariaDB.
Patrones Comunes de Marco
Total Acumulado (Suma Acumulativa)
Incluye todas las filas desde el inicio de la partición hasta la fila actual:
SELECT
customer_id,
payment_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY payment_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM
payment
WHERE
customer_id = 1
ORDER BY
payment_date;
Ejemplo de Salida:
customer_id | payment_date | amount | running_total
1 | 2005-05-25 | 2.99 | 2.99
1 | 2005-06-15 | 4.99 | 7.98
1 | 2005-07-08 | 11.99 | 19.97
1 | 2005-08-01 | 11.99 | 31.96
Punto Clave: El running_total de cada fila acumula todos los pagos anteriores para ese cliente. El marco ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW significa: comienza en la fila 1 de esta partición, termina en la fila actual.
Promedio Móvil (Ventana Deslizante)
Calcula el promedio móvil de 3 pagos para cada cliente:
SELECT
customer_id,
payment_date,
amount,
ROUND(
AVG(amount) OVER (
PARTITION BY customer_id
ORDER BY payment_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2
) AS moving_avg_3
FROM
payment
WHERE
customer_id = 1
ORDER BY
payment_date;
Ejemplo de Salida:
customer_id | payment_date | amount | moving_avg_3
1 | 2005-05-25 | 2.99 | 2.99
1 | 2005-06-15 | 4.99 | 3.99
1 | 2005-07-08 | 11.99 | 6.66
1 | 2005-08-01 | 11.99 | 9.66
1 | 2005-08-23 | 5.99 | 9.99
Punto Clave: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW crea una ventana de exactamente 3 filas: la fila actual y las 2 filas anteriores. Cuando existen menos de 3 filas (al inicio de una partición), la ventana se reduce en consecuencia.
Mirar Adelante (Incluyendo Filas Futuras)
Calcula el promedio de la fila actual y las siguientes 2 filas:
SELECT
customer_id,
payment_date,
amount,
ROUND(
AVG(amount) OVER (
PARTITION BY customer_id
ORDER BY payment_date
ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
), 2
) AS forward_avg
FROM
payment
WHERE
customer_id = 1
ORDER BY
payment_date;
Punto Clave: CURRENT ROW AND 2 FOLLOWING desplaza la ventana hacia adelante. Las dos últimas filas en la partición promediarán menos valores porque no hay filas después de ellas.
Agregado de Partición Completa (como Ventana)
Compara cada pago con el promedio general del cliente:
SELECT
customer_id,
payment_date,
amount,
ROUND(
AVG(amount) OVER (
PARTITION BY customer_id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
), 2
) AS customer_avg,
amount - ROUND(
AVG(amount) OVER (
PARTITION BY customer_id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
), 2
) AS deviation
FROM
payment
WHERE
customer_id IN (1, 2)
ORDER BY
customer_id, payment_date;
Punto Clave: UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING abarca toda la partición, equivalente a un agregado GROUP BY pero sin colapsar filas.
ROWS vs RANGE: Una Comparación Directa
Entender la diferencia entre ROWS y RANGE es crítico cuando las filas comparten valores idénticos de ORDER BY.
SELECT
customer_id,
amount,
SUM(amount) OVER (
ORDER BY amount
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS sum_rows,
SUM(amount) OVER (
ORDER BY amount
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS sum_range
FROM
payment
WHERE
customer_id IN (1, 2, 3)
ORDER BY
amount;
Ejemplo de Salida:
customer_id | amount | sum_rows | sum_range
3 | 9.99 | 9.99 | 9.99
2 | 10.99 | 20.98 | 20.98
1 | 11.99 | 32.97 | 55.94
2 | 11.99 | 44.96 | 55.94
1 | 11.99 | 55.94 | 55.94
Observaciones:
- Con
ROWS: cada fila física se cuenta individualmente, independientemente de los empates. La suma acumulativa avanza una fila a la vez. - Con
RANGE: todas las filas con el mismo valor de cantidad se incluyen juntas. Ambas filas de 11.99 se tratan como parte del mismo grupo lógico, por lo quesum_rangesalta al total completo de inmediato.
Ventanas Nombradas (Cláusula WINDOW)
Si utilizas la misma definición de marco varias veces en una consulta, puedes nombrarla con la cláusula WINDOW para evitar repeticiones:
SELECT
customer_id,
payment_date,
amount,
SUM(amount) OVER w AS running_total,
AVG(amount) OVER w AS running_avg,
COUNT(amount) OVER w AS payment_count
FROM
payment
WHERE
customer_id = 1
WINDOW w AS (
PARTITION BY customer_id
ORDER BY payment_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
ORDER BY
payment_date;
Punto Clave: La cláusula WINDOW w AS (...) define el marco una vez. Todas las tres llamadas a funciones de ventana lo referencian con OVER w. Esto es más limpio, menos propenso a errores y más fácil de mantener.
Nota: La cláusula WINDOW es compatible en PostgreSQL, MySQL 8.0+ y MariaDB 10.2+.
Referencia de Límites de Marco
| Definición de marco | Lo que incluye |
|---|---|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | Todas las filas desde el inicio de la partición hasta la fila actual |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING | Todas las filas en la partición (agregado completo) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | Fila actual más una fila a cada lado (ventana de 3 filas) |
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW | Fila actual y las 2 filas anteriores (ventana deslizante de 3 filas) |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | Fila actual hasta el final de la partición |
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | Predeterminado cuando ORDER BY está presente; incluye todas las filas con el mismo valor de ORDER BY que la actual |
Aplicación Práctica: Ventas Diarias con Métricas Acumuladas y Móviles
Combina múltiples marcos de ventana en una sola consulta para obtener una imagen completa:
SELECT
DATE(payment_date) AS payment_day,
SUM(amount) AS daily_total,
SUM(SUM(amount)) OVER (
ORDER BY DATE(payment_date)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_total,
ROUND(AVG(SUM(amount)) OVER (
ORDER BY DATE(payment_date)
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS rolling_7day_avg
FROM
payment
GROUP BY
DATE(payment_date)
ORDER BY
payment_day;
Punto Clave: El agregado externo (SUM(SUM(amount))) anida una función de ventana sobre resultados agrupados, un patrón poderoso para tableros de series temporales.
Cuándo Usar Cada Opción de Marco
| Objetivo | Marco recomendado |
|---|---|
| Total acumulado / acumulativo | ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| Agregado de partición completo junto con datos de fila | ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
| Promedio móvil de N períodos | ROWS BETWEEN N-1 PRECEDING AND CURRENT ROW |
| Ventana de suavizado simétrico | ROWS BETWEEN N PRECEDING AND N FOLLOWING |
| Agregación de rango basada en valores (manejar empates como un grupo) | RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| Reutilizar el mismo marco en múltiples funciones | Cláusula WINDOW nombrada |
Preguntas Frecuentes
¿Qué marco se utiliza por defecto?
En muchos sistemas de gestión de bases de datos, si ORDER BY está presente, el predeterminado es RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Debido a que esto puede ser sorprendente, es mejor especificar el marco explícitamente.
¿Cuándo debo elegir ROWS y cuándo debo elegir RANGE?
Usa ROWS para un control exacto fila por fila. Usa RANGE cuando necesites trabajar con rangos de valores y tratar valores iguales de ORDER BY juntos.
¿Por qué necesito UNBOUNDED FOLLOWING?
Extiende el marco hasta el final de la partición. Eso es importante cuando una función necesita ver no solo las filas hasta la fila actual, sino también todas las filas siguientes.
Conclusiones Clave
- Un marco de ventana define el conjunto de filas en relación con la fila actual que se incluyen en el cálculo de una función de ventana.
- Los tres modos de marco son ROWS (filas físicas), RANGE (rangos de valores lógicos) y GROUPS (grupos pares de valores iguales).
- El marco predeterminado cuando
ORDER BYestá presente esRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW— conocer este predeterminado previene errores sutiles con valores empatados. - Usa
ROWSpara ventanas deslizantes de ancho fijo (por ejemplo, promedio móvil de 7 días); usaRANGEcuando los valores empatados deben ser agregados juntos. - Palabras clave de límite:
UNBOUNDED PRECEDING,n PRECEDING,CURRENT ROW,n FOLLOWING,UNBOUNDED FOLLOWING. - La cláusula
WINDOWte permite nombrar y reutilizar una definición de marco, manteniendo las consultas complejas legibles. - Los marcos de ventana no afectan
PARTITION BY— solo estrechan el marco dentro de una partición.
En la próxima lección, exploraremos las funciones de ventana de desplazamiento LAG, LEAD, FIRST_VALUE y LAST_VALUE, que te permiten comparar el valor de una fila con valores en otras filas sin uniones auto-referenciales.