Desde que me metí de lleno en el mundo del Business Intelligence, me paso el día peleándome con consultas SQL kilométricas. El otro día, llegó una necesidad aparentemente inocente: "Sácame un listado con todas las ventas del mes, pero añade una columna que indique el ranking del comercial dentro de su región, y otra con el total acumulado de ventas hasta ese día".
Hace un par de años, habría resuelto esto creando tablas temporales, cursores (¡horror!) o haciendo auto-joins (JOIN de la tabla consigo misma) que dejarían la CPU del servidor echando humo. Pero buceando en la documentación de SQL Server, he descubierto lo que probablemente sea la mejor adición al estándar SQL desde el JOIN: las Window Functions (Funciones de Ventana).
¿Qué es una Función de Ventana?
Normalmente, cuando usas un GROUP BY, colapsas varias filas en una sola. Pierdes el detalle. La magia de las funciones de ventana es que te permiten realizar cálculos agregados sobre un conjunto de filas (la "ventana") que están relacionadas con la fila actual, pero sin colapsar las filas originales.
Es como tener tu pastel y comértelo a la vez: mantienes el nivel de detalle de la transacción, pero le inyectas métricas analíticas globales.
El código: Antes y Después
Para entender la potencia de esto, veamos cómo he resuelto la petición del jefe de ventas usando la cláusula OVER().
SELECT
FechaVenta,
NombreComercial,
Region,
ImporteVenta,
-- 1. Ranking del comercial dentro de su región
RANK() OVER (
PARTITION BY Region
ORDER BY ImporteVenta DESC
) AS RankingEnRegion,
-- 2. Total acumulado (Running Total) de ventas en la región hasta esta fecha
SUM(ImporteVenta) OVER (
PARTITION BY Region
ORDER BY FechaVenta
ROWS UNBOUNDED PRECEDING
) AS TotalAcumuladoRegion
FROM Fact_Ventas
WHERE MONTH(FechaVenta) = 11 AND YEAR(FechaVenta) = 2011
ORDER BY Region, RankingEnRegion;
Vamos a diseccionarlo. El PARTITION BY Region le dice a SQL que divida los cálculos en "ventanas" o bloques basados en la región. Cuando cambia la región (de 'Norte' a 'Sur'), el contador se reinicia.
El ORDER BY dentro de la función de ventana define cómo se evalúan las filas dentro de ese bloque. Y ese ROWS UNBOUNDED PRECEDING es magia pura: le dice que sume desde la primera fila de la ventana hasta la fila actual, calculando el acumulado sobre la marcha.
Todo esto en un solo barrido de la tabla. El motor de base de datos lo optimiza internamente de una forma que un auto-join jamás podría igualar. La consulta tardó menos de un segundo en devolverme cien mil filas perfectamente ordenadas y calculadas.
Reflexión: El SQL está más vivo que nunca
En plena moda de las bases de datos NoSQL, a veces parece que el viejo y confiable SQL es un dinosaurio a punto de extinguirse. Pero descubrimientos como las Window Functions me demuestran todo lo contrario.
El SQL analítico está evolucionando para ser profundamente declarativo: tú le dices a la máquina qué quieres calcular (un ranking, un acumulado, una media móvil) y dejas que el motor decida cómo es la forma más rápida de hacerlo en disco y memoria. Extraer este tipo de lógica de negocio del código de la aplicación (PHP, C#) y bajarlo directamente a la capa de base de datos no solo reduce el código espagueti, sino que aprovecha la fuerza bruta del servidor. Definitivamente, me queda mucho SQL por aprender.