SQL window functions: clasificación, totales acumulados y analítica

Published: 2026-03-24

Las window functions realizan cálculos sobre un conjunto de filas relacionadas con la fila actual, sin colapsar el resultado en una sola fila de salida como hace GROUP BY. Son una de las características más potentes del SQL moderno, compatibles con SQLite (3.25+), PostgreSQL, MySQL (8.0+), SQL Server y Oracle.

Datos de ejemplo

Usamos dos tablas en todos los ejemplos:

Tabla employees:

id name department hire_date salary
1 Alice Engineering 2023-01-15 95000
2 Bob Engineering 2023-06-01 85000
3 Carol Engineering 2024-03-10 85000
4 Dave Sales 2023-02-20 72000
5 Eve Sales 2023-09-05 68000
6 Frank Sales 2024-01-15 65000
7 Grace Marketing 2023-04-01 82000
8 Henry Marketing 2024-06-01 70000

Tabla monthly_sales:

id employee_id month revenue
1 1 2026-01 22000
2 1 2026-02 25000
3 1 2026-03 28000
4 2 2026-01 19000
5 2 2026-02 21000
6 2 2026-03 20000
7 3 2026-01 15000
8 3 2026-02 17000
9 3 2026-03 19000
10 4 2026-01 18000
11 4 2026-02 22000
12 4 2026-03 17000
13 5 2026-01 14000
14 5 2026-02 16000
15 5 2026-03 19000
16 6 2026-01 11000
17 6 2026-02 13000
18 6 2026-03 15000
19 7 2026-01 9000
20 7 2026-02 12000
21 7 2026-03 14000
22 8 2026-01 7000
23 8 2026-02 9000
24 8 2026-03 11000
Explora el conjunto de datos en DbGate Lite

Sintaxis de las window functions

Cada función de ventana usa la cláusula OVER para definir sobre qué filas operar:

function_name(...) OVER (
  [PARTITION BY column]
  [ORDER BY column]
  [frame_clause]
)
  • PARTITION BY: divide las filas en grupos (como GROUP BY, pero mantiene todas las filas)
  • ORDER BY: define el orden de las filas dentro de cada partición
  • Frame clause: limita la ventana a un rango de filas relativo a la fila actual

ROW_NUMBER()

ROW_NUMBER() asigna un número secuencial único a cada fila dentro de una partición. Los números siempre empiezan en 1 sin huecos ni empates.

Ejemplo: Numerar a todos los empleados por salario

Ejemplo: Numerar empleados dentro de cada departamento

Usando PARTITION BY, la numeración se reinicia en 1 para cada departamento:

RANK() y DENSE_RANK()

RANK() y DENSE_RANK() manejan los empates de forma diferente a ROW_NUMBER():

  • ROW_NUMBER(): siempre único, desempate arbitrario
  • RANK(): el mismo valor recibe el mismo rango y luego se saltan números (1, 2, 2, 4)
  • DENSE_RANK(): el mismo valor recibe el mismo rango, sin huecos (1, 2, 2, 3)

Ejemplo: Comparar las tres funciones de clasificación

Bob y Carol ganan 85000; observa cómo cada función maneja el empate:

Ejemplo: Clasificar empleados dentro del departamento

NTILE()

NTILE(n) distribuye las filas en n grupos (buckets) aproximadamente iguales, numerados del 1 al n.

Ejemplo: Dividir empleados en cuartiles de salario

Ejemplo: Dividir empleados en dos niveles por departamento

LAG() y LEAD()

LAG(column, offset) accede a un valor de una fila anterior. LEAD(column, offset) accede a un valor de una fila posterior. Ambos tienen por defecto un desplazamiento de 1 y devuelven NULL cuando no hay fila.

Ejemplo: Comparación de ingresos mes a mes

Compara los ingresos de cada mes con el mes anterior para cada empleado:

Ejemplo: Ver el ingreso del mes siguiente

Window functions de agregación

Las funciones de agregación estándar (SUM, AVG, COUNT, MIN, MAX) pueden usarse como window functions con OVER. A diferencia de GROUP BY, cada fila se conserva en la salida.

Ejemplo: Salario de cada empleado frente al promedio del departamento

Ejemplo: Porcentaje del salario total

Calcula qué fracción de la nómina total representa cada empleado:

Ejemplo: Total acumulado y conteo acumulado

Totales acumulados por partición

Combina PARTITION BY y ORDER BY para calcular totales acumulados dentro de grupos.

Ejemplo: Ingresos acumulados por empleado

Ejemplo: Clasificación de ingresos mensuales entre todos los empleados

Encuentra cómo se clasificó cada empleado cada mes:

Marcos de ventana (window frames)

Un marco de ventana define exactamente qué filas relativas a la fila actual se incluyen en el cálculo. La cláusula de marco viene después de ORDER BY dentro de OVER.

Sintaxis

ROWS BETWEEN <start> AND <end>

Límites comunes:

  • UNBOUNDED PRECEDING: desde la primera fila de la partición
  • N PRECEDING: N filas antes de la fila actual
  • CURRENT ROW: la fila actual
  • N FOLLOWING: N filas después de la fila actual
  • UNBOUNDED FOLLOWING: hasta la última fila de la partición

Ejemplo: Promedio móvil de 2 meses

Ejemplo: Mínimo y máximo de ingresos acumulados

FIRST_VALUE y LAST_VALUE

FIRST_VALUE(column) devuelve el primer valor en el marco de ventana. LAST_VALUE(column) devuelve el último valor. Cuidado: LAST_VALUE con el marco por defecto (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) solo ve hasta la fila actual.

Ejemplo: Comparar cada mes con el primer mes

Ejemplo: Comparar cada mes con el mejor mes

Usa ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING para ver la partición completa:

Ejemplo práctico: máximo perceptor por departamento

Un patrón común: usar una función de ventana en una subconsulta y luego filtrar por rango.

Ejemplo: Empleado mejor pagado en cada departamento

Ejemplo: Mes con mayores ingresos por empleado

Window functions vs. GROUP BY

Una diferencia clave: GROUP BY colapsa filas en filas de resumen, mientras que las window functions mantienen cada fila y añaden una columna calculada.

Ejemplo: Comparación lado a lado

GROUP BY devuelve una fila por departamento:

Las window functions mantienen todas las filas:

Casos de uso comunes de las window functions

  • Clasificaciones y rankings: ROW_NUMBER, RANK, DENSE_RANK para consultas top-N
  • Totales acumulados: SUM acumulativa para seguimiento financiero e inventarios
  • Comparación periodo a periodo: LAG/LEAD para análisis mes a mes o año contra año
  • Promedios móviles: suavizar datos con AVG basado en marcos
  • Análisis de percentiles: NTILE para dividir datos en grupos iguales
  • Eliminación de duplicados: ROW_NUMBER para elegir una fila por grupo
  • Análisis de huecos: LAG para detectar secuencias faltantes o cambios inusuales

Consejos de rendimiento

  1. Indexa las columnas de ORDER BY: las window functions ordenan datos internamente; los índices ayudan
  2. Minimiza las particiones: un gran número de particiones incrementa el cómputo
  3. Reutiliza definiciones de ventana: muchas bases de datos admiten cláusulas WINDOW para evitar repetir la misma especificación OVER
  4. Filtra después de aplicar ventanas: las window functions se ejecutan antes de WHERE, así que usa subconsultas o CTE para filtrar por rango
  5. Evita marcos innecesarios: especifica cláusulas de marco solo cuando necesites un comportamiento no predeterminado

Resumen

  • Las window functions calculan valores sobre filas relacionadas sin colapsar el conjunto de resultados
  • ROW_NUMBER, RANK, DENSE_RANK asignan posiciones ordinales a las filas
  • LAG y LEAD acceden a valores de filas anteriores o posteriores
  • NTILE distribuye filas en grupos de tamaño similar
  • FIRST_VALUE y LAST_VALUE devuelven valores de los extremos de la ventana
  • Las funciones de agregación (SUM, AVG, COUNT, MIN, MAX) con OVER mantienen todas las filas mientras calculan métricas a nivel de grupo
  • PARTITION BY crea grupos independientes; ORDER BY define la secuencia de filas
  • Los marcos de ventana (ROWS BETWEEN ... AND ...) afinan qué filas se incluyen
  • Usa subconsultas para filtrar por resultados de window functions (por ejemplo, top-N por grupo)
  • Compatibles con SQLite 3.25+, PostgreSQL, MySQL 8.0+, SQL Server y Oracle

Prueba a modificar los ejemplos para experimentar con diferentes particiones, ordenaciones y cláusulas de marco y ver cómo las window functions pueden simplificar tus consultas analíticas.