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 ...
)
OVERconvierte la función en analítica.PARTITION BYdivide filas en grupos sin resumirlas.ORDER BYdefine 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_NUMBERasigna posiciones únicas.RANKrepite la posición y deja huecos.DENSE_RANKrepite 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;
QUALIFYen 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