Funciones de ventana en SQL: guía práctica con ejemplos

Funciones de ventana en SQL: guía práctica con ejemplos

Las funciones de ventana en SQL calculan rankings, acumulados, medias móviles y comparaciones entre filas sin colapsar el resultado. A diferencia de GROUP BY, conservan cada registro y añaden una columna analítica.

Son una de las herramientas más valiosas de SQL para análisis: permiten obtener el último pedido por cliente, comparar ventas con el mes anterior y calcular el porcentaje de cada fila sobre su grupo.

Sintaxis de una función de ventana

funcion(...) OVER (
    PARTITION BY columna_grupo
    ORDER BY columna_orden
    ROWS BETWEEN ...
)
  • OVER convierte la función en analítica.
  • PARTITION BY divide filas en grupos sin resumirlas.
  • ORDER BY define la secuencia dentro de cada partición.
  • El frame limita qué filas participan alrededor de la actual.

No todos los elementos son obligatorios para todas las funciones.

GROUP BY vs funciones de ventana

Supón una tabla con una fila por venta. GROUP BY vendedor devuelve una fila por vendedor. Una ventana mantiene cada venta y repite el total del vendedor junto a ella.

SELECT
    venta_id,
    vendedor,
    total,
    SUM(total) OVER (PARTITION BY vendedor) AS total_vendedor
FROM ventas;
Necesidad Herramienta
Una fila por grupo GROUP BY
Mantener el detalle y añadir un total Ventana
Filtrar grupos agregados HAVING
Ranking dentro de cada grupo Ventana

ROW_NUMBER, RANK y DENSE_RANK

SELECT
    vendedor,
    fecha,
    total,
    ROW_NUMBER() OVER (
        PARTITION BY vendedor
        ORDER BY total DESC
    ) AS fila,
    RANK() OVER (
        PARTITION BY vendedor
        ORDER BY total DESC
    ) AS rango,
    DENSE_RANK() OVER (
        PARTITION BY vendedor
        ORDER BY total DESC
    ) AS rango_denso
FROM ventas;

Con empates:

  • ROW_NUMBER asigna posiciones únicas.
  • RANK repite la posición y deja huecos.
  • DENSE_RANK repite la posición sin huecos.

Si necesitas un resultado determinista con ROW_NUMBER, añade un desempate único al ORDER BY, como venta_id.

Obtener el registro más reciente por grupo

WITH ordenados AS (
    SELECT
        pedido_id,
        cliente_id,
        fecha,
        total,
        ROW_NUMBER() OVER (
            PARTITION BY cliente_id
            ORDER BY fecha DESC, pedido_id DESC
        ) AS rn
    FROM pedidos
)
SELECT *
FROM ordenados
WHERE rn = 1;

La CTE en SQL permite filtrar el ranking en una etapa posterior.

LAG y LEAD: comparar filas

LAG mira una fila anterior y LEAD, una posterior:

SELECT
    mes,
    ventas,
    LAG(ventas) OVER (ORDER BY mes) AS ventas_mes_anterior,
    ventas - LAG(ventas) OVER (ORDER BY mes) AS variacion
FROM ventas_mensuales
ORDER BY mes;

Para crecimiento porcentual:

100.0 * (
    ventas / NULLIF(LAG(ventas) OVER (ORDER BY mes), 0) - 1
) AS crecimiento_pct

NULLIF evita dividir entre cero. La primera fila no tiene anterior y devuelve NULL, lo cual es correcto.

Totales acumulados

SELECT
    fecha,
    total,
    SUM(total) OVER (
        ORDER BY fecha, venta_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS acumulado
FROM ventas;

Especificar ROWS evita sorpresas con valores de orden repetidos. El frame predeterminado puede comportarse como RANGE en algunos motores y sumar de golpe todas las filas empatadas.

Media móvil

Promedio de la fila actual y las seis anteriores:

SELECT
    fecha,
    ventas,
    AVG(ventas) OVER (
        ORDER BY fecha
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS media_movil_7
FROM ventas_diarias;

Esto suaviza ruido, pero las primeras filas usan menos de siete observaciones. Si necesitas ventanas completas, filtra después o cuenta las filas del frame.

Porcentaje sobre el total

SELECT
    categoria,
    producto,
    total,
    100.0 * total
        / NULLIF(SUM(total) OVER (PARTITION BY categoria), 0)
        AS porcentaje_categoria
FROM ventas_producto;

No requiere unir el detalle con una subconsulta agregada.

FIRST_VALUE y LAST_VALUE

SELECT
    cliente_id,
    fecha,
    total,
    FIRST_VALUE(total) OVER (
        PARTITION BY cliente_id
        ORDER BY fecha
    ) AS primera_compra
FROM ventas;

LAST_VALUE suele sorprender porque el frame predeterminado puede terminar en la fila actual. Para obtener la última del grupo:

LAST_VALUE(total) OVER (
    PARTITION BY cliente_id
    ORDER BY fecha
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)

Orden lógico y filtrado

Las ventanas se calculan después de WHERE, GROUP BY y HAVING, pero antes del ORDER BY final. Por eso un alias de ventana no suele estar disponible en WHERE.

Soluciones:

  • CTE;
  • subconsulta;
  • QUALIFY en motores que lo soportan, como BigQuery y Snowflake.

Rendimiento y buenas prácticas

  • Indexa columnas frecuentes de partición y orden cuando el motor pueda aprovecharlas.
  • Reduce filas y columnas antes de ventanas costosas.
  • Reutiliza una especificación de ventana si el dialecto admite WINDOW.
  • Evita ordenar por expresiones innecesarias.
  • Revisa derrames a disco en grandes volúmenes.
  • Define desempates.
  • Escribe el frame explícitamente para acumulados y móviles.
  • Inspecciona el plan de ejecución.

Las ventanas no sustituyen los fundamentos de SQL ni un modelo de datos limpio.

Preguntas frecuentes

¿Qué hace PARTITION BY?
Divide el resultado en grupos independientes para el cálculo, pero conserva todas las filas.

¿Cuál es la diferencia entre RANK y DENSE_RANK?
Ambos repiten posición en empates; RANK deja huecos posteriores y DENSE_RANK no.

¿Puedo usar una función de ventana en WHERE?
Normalmente no en el mismo nivel. Calcula primero en una CTE o subconsulta y filtra fuera.

¿Qué diferencia hay entre ROWS y RANGE?
ROWS cuenta filas físicas; RANGE agrupa pares según el valor de orden. Con empates pueden producir acumulados distintos.

¿Una función de ventana reduce el número de filas?
No. Añade resultados analíticos al detalle; GROUP BY sí reduce a una fila por grupo.

¿Qué motores soportan funciones de ventana?
Las versiones modernas de PostgreSQL, SQL Server, Oracle, MySQL, SQLite, BigQuery y Snowflake ofrecen un conjunto amplio, con diferencias de sintaxis y funciones.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir