Ever since I got fully involved in the Business Intelligence world, I spend my days fighting with mile-long SQL queries. The other day, a seemingly innocent request came in: "Get me a list of all the sales for the month, but add a column showing the sales rep's ranking within their region, and another with the running total of sales up to that day".
A couple of years ago, I would have solved this by creating temporary tables, cursors (the horror!), or doing self-joins (JOINing the table to itself) that would leave the server's CPU smoking. But diving into the SQL Server documentation, I discovered what is probably the best addition to the SQL standard since the JOIN: Window Functions.
What is a Window Function?
Normally, when you use a GROUP BY, you collapse multiple rows into a single one. You lose the detail. The magic of window functions is that they allow you to perform aggregate calculations over a set of rows (the "window") that are related to the current row, but without collapsing the original rows.
It's like having your cake and eating it too: you keep the transaction-level detail, but you inject global analytical metrics into it.
The code: Before and After
To understand the power of this, let's see how I solved the sales manager's request using the OVER() clause.
SELECT
FechaVenta,
NombreComercial,
Region,
ImporteVenta,
-- 1. Ranking of the sales rep within their region
RANK() OVER (
PARTITION BY Region
ORDER BY ImporteVenta DESC
) AS RankingEnRegion,
-- 2. Running Total of sales in the region up to this date
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;
Let's dissect it. The PARTITION BY Region tells SQL to divide the calculations into "windows" or blocks based on the region. When the region changes (from 'North' to 'South'), the counter resets.
The ORDER BY inside the window function defines how the rows are evaluated within that block. And that ROWS UNBOUNDED PRECEDING is pure magic: it tells it to sum from the first row of the window up to the current row, calculating the running total on the fly.
All this in a single pass over the table. The database engine optimizes it internally in a way that a self-join could never match. The query took less than a second to return a hundred thousand perfectly ordered and calculated rows.
Reflection: SQL is more alive than ever
Amidst the NoSQL database hype, it sometimes seems like good old reliable SQL is a dinosaur on the verge of extinction. But discoveries like Window Functions prove me entirely wrong.
Analytical SQL is evolving to be deeply declarative: you tell the machine what you want to calculate (a ranking, a running total, a moving average) and you let the engine decide how is the fastest way to do it on disk and in memory. Extracting this kind of business logic from the application code (PHP, C#) and pushing it directly down to the database layer not only reduces spaghetti code, but leverages the server's brute force. I definitely have a lot of SQL left to learn.