🙏 Realmente necesitamos su apoyo. Necesitamos financiamiento para continuar nuestra misión: publicar nuevas lecciones y mejorar la plataforma. Si puede, por favor apoye el proyecto con una donación. Ayude al proyecto ahora →
Código SQL copiado al portapapeles

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ímiteSignificado
UNBOUNDED PRECEDINGLa primera fila de la partición
n PRECEDINGn filas (o unidades de rango) antes de la fila actual
CURRENT ROWLa fila actual
n FOLLOWINGn filas (o unidades de rango) después de la fila actual
UNBOUNDED FOLLOWINGLa ú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 que sum_range salta 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 marcoLo que incluye
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWTodas las filas desde el inicio de la partición hasta la fila actual
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGTodas las filas en la partición (agregado completo)
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWINGFila actual más una fila a cada lado (ventana de 3 filas)
ROWS BETWEEN 2 PRECEDING AND CURRENT ROWFila actual y las 2 filas anteriores (ventana deslizante de 3 filas)
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGFila actual hasta el final de la partición
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWPredeterminado 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

ObjetivoMarco recomendado
Total acumulado / acumulativoROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
Agregado de partición completo junto con datos de filaROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
Promedio móvil de N períodosROWS BETWEEN N-1 PRECEDING AND CURRENT ROW
Ventana de suavizado simétricoROWS 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 funcionesClá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 BY está presente es RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — conocer este predeterminado previene errores sutiles con valores empatados.
  • Usa ROWS para ventanas deslizantes de ancho fijo (por ejemplo, promedio móvil de 7 días); usa RANGE cuando 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 WINDOW te 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.