Window functions and the frame nobody specifies
ROW_NUMBER, RANK and DENSE_RANK on ties, LAG and LEAD, and why a running total needs ROWS rather than the default RANGE frame.
After this lesson you can
- Pick ROW_NUMBER, RANK or DENSE_RANK by how ties must behave
- Write running totals and moving averages with an explicit ROWS frame
- Filter on a window function's result through a CTE or derived table
A window function computes a value for each row from a set of related
rows — its window — without collapsing them the way GROUP BY does.
SELECT id, dept, salary,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employees;
PARTITION BY splits the rows into independent groups; ORDER BY
inside OVER orders the rows within each partition. Window functions
are evaluated after WHERE, GROUP BY and HAVING, so you cannot
filter on one in WHERE. Compute it in a CTE or derived table and
filter outside:
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC, id) AS rn
FROM employees
)
SELECT * FROM ranked WHERE rn = 1;
Three ways to number rows
ROW_NUMBER()gives 1, 2, 3, ... with no repeats. Tied rows get different numbers in an undefined order unless theORDER BYis unique.RANK()gives tied rows the same number and then skips: 1, 2, 2, 4.DENSE_RANK()gives tied rows the same number without gaps: 1, 2, 2, 3.
CREATE TABLE results (id INT PRIMARY KEY, player NVARCHAR(10) NOT NULL, points INT NOT NULL);INSERT INTO results VALUES (1, N'Ana', 90), (2, N'Luka', 85), (3, N'Nino', 85), (4, N'Mari', 70);SELECT player, points, ROW_NUMBER() OVER (ORDER BY points DESC, id) AS row_number, RANK() OVER (ORDER BY points DESC) AS rank, DENSE_RANK() OVER (ORDER BY points DESC) AS dense_rank, LAG(points) OVER (ORDER BY points DESC, id) AS previous_pointsFROM resultsORDER BY points DESC, id;LAG(col, 1) OVER (ORDER BY ...) reads the previous row's value and
LEAD the next one; both return NULL (or a default you pass as the
third argument) at the edge of the partition. They replace the old
self-join on id - 1, which breaks as soon as ids have gaps.
The frame, and the default that bites
Aggregates such as SUM over an ordered window also have a frame:
which rows around the current one take part. If you write ORDER BY
and no frame, SQL Server uses
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. RANGE works on
values, not positions: every row that ties with the current row on the
ORDER BY value is inside the frame. Two ledger entries on the same day
therefore both show the total after both.
Write ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW and order by
something unique. It counts physical rows and gives a true running
total.
A ROWS frame can also have a fixed size: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW covers the current row and at most two before it in the
partition, which is how a moving average is written. Near the start of
a partition the frame simply holds fewer rows.
CREATE TABLE ledger (id INT PRIMARY KEY, posted_on DATE NOT NULL, amount INT NOT NULL);INSERT INTO ledger VALUES (1, '2024-03-01', 100), (2, '2024-03-02', 20), (3, '2024-03-02', 10), (4, '2024-03-03', -50);SELECT id, posted_on, amount, SUM(amount) OVER (ORDER BY posted_on) AS default_frame, SUM(amount) OVER (ORDER BY posted_on, id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_frameFROM ledgerORDER BY posted_on, id;Common mistakes
- Running totals with the default frame. The second example shows two rows on 2 March both reading 130.
ROW_NUMBER()over a non-unique order and then relying on which tied row got 1.- Forgetting
PARTITION BY. The total runs across all accounts instead of restarting for each one. - Filtering a window result in
WHERE. SQL Server rejects it; compute the value in a CTE and filter outside.
What interviewers ask
- Difference between
RANKandDENSE_RANK? Both share numbers on ties;RANKleaves a gap afterwards,DENSE_RANKdoes not. - What is the default window frame and why does it matter? With
ORDER BYand no frame it isRANGE ... CURRENT ROW, which includes all peers of the current row; running totals needROWS. - How do you get the second-highest salary per department?
DENSE_RANK()partitioned by department, then filter on 2 in an outer query.
Exercise · Pro
A three-reading moving average per sensor
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade