Window Functions Explained by Example
Ranking, running totals and month-over-month change, built one clause at a time, plus the frame default that quietly breaks running totals on ties.
The question GROUP BY cannot answer
"Show me every employee alongside the average salary in their department."
Try it with GROUP BY and you hit a wall immediately. Grouping collapses rows to
one row per department, but the question wants all twenty employees and a
per-department number attached to each. You end up writing a subquery, joining
it back, and repeating yourself.
Window functions solve exactly this: they compute across a set of related rows without collapsing them. Every input row still comes out.
Twenty rows in, twenty rows out, each carrying its department's average.
The anatomy of OVER
Every window function has the same shape.
FUNCTION(argument) OVER (
PARTITION BY grouping_column -- split rows into independent windows
ORDER BY sort_column -- order rows inside each window
ROWS BETWEEN ... AND ... -- which rows in the window to include
)PARTITION BY is the window equivalent of GROUP BY, but it divides rather
than collapses. ORDER BY inside OVER decides the sequence within each
partition, which matters for anything cumulative or positional. Leave both out
and the window is the entire result set.
Ranking inside a partition
The classic use: highest earner per department. ROW_NUMBER() numbers the rows
in each partition according to the window's ORDER BY.
To keep only the top earner per department you cannot add
WHERE rank_in_dept = 1, because window functions are evaluated after WHERE, so
the alias does not exist yet. Wrap the query and filter outside it.
ROW_NUMBER, RANK and DENSE_RANK
On the values 100, 90, 90, 80: ROW_NUMBER gives 1, 2, 3, 4, always distinct.
RANK gives 1, 2, 2, 4: ties share a rank and the next value skips ahead.
DENSE_RANK gives 1, 2, 2, 3: ties share, nothing is skipped. Pick
ROW_NUMBER for deduplication, RANK for leaderboards.
Running totals, and the frame that ruins them
A cumulative revenue line is a window with an ORDER BY and no partition.
Look closely at the two orders on the same date. Their running totals are
identical: the total jumps past both at once instead of stepping through them.
That is not a bug, it is the default frame
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, where "current row" means
every row with the same sort value as the current row. Peers are treated as a
single step.
If you want a strict row-by-row accumulation, say ROWS instead of RANGE.
Spell the frame out
RANGE and ROWS differ only when the ORDER BY column has duplicates,
which is exactly what happens with dates. Writing the frame explicitly costs
one line and removes the ambiguity for whoever reads the query next.
The same frame syntax gives you moving averages.
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW is a three-point trailing average;
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING is a centred one.
Comparing a row to its neighbour
LAG reaches backwards in the window, LEAD forwards. Month-over-month growth
is a GROUP BY for the monthly totals, then a LAG over the result.
The first row's prev_month is NULL because there is nothing before it. Wrap
the expression in COALESCE if a zero reads better in your report.
Where windows fit in the query
Order of evaluation, which explains most window function errors:
FROMandJOINassemble the rowsWHEREfilters themGROUP BYandHAVINGaggregate them- Window functions run here, over what survived
SELECTprojects,ORDER BYsorts,LIMITtruncates
Two consequences worth memorising. Window functions can be applied on top of
aggregates: SUM(revenue) OVER (...) in a query that already has a GROUP BY
is legal and useful. And they can never be referenced in WHERE or HAVING;
you need a CTE or a subquery, as in the top-earner example above.
Where to go next
Try rewriting a self-join you already have. Almost any query that joins a table to itself to compare a row with the previous one, or to a group summary, is a window function waiting to be simplified: usually shorter, usually faster, and read once instead of twice.
Cite this resource
SQLSimplified. "Window Functions Explained by Example". Available at: https://sqlsimplified.online/blog/window-functions-explained-by-example