Foundations — Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD
Before you can read a single line like ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC), you must know what a table, a row, a column, an ordering, a partition, and a function-that-returns-a-value actually are as pictures. This page builds all of them from zero, in the order they depend on each other.
1. A table = a grid of boxes
- The picture: think of a spreadsheet. The row is the whole horizontal band; the column is the whole vertical band; the cell is where they cross.
- Why the topic needs it: every window function reads cells (like
salary) belonging to rows, and treats columns as the things youORDER BYorPARTITION BY. If "row" and "column" are fuzzy, nothing later makes sense.
2. NULL = the empty box
- The picture: a cell drawn as a dashed, empty box (see figure above, the greyed cell).
- Why the topic needs it:
LAG(revenue, 1, 0)gives0for the first row precisely because without that default it would beNULL(there is no previous row → the box is empty). The parent's "sane fallback" only makes sense once you know what it is falling back from.
3. A function that returns a value
- The picture: a box with an in-arrow (arguments) and an out-arrow (the returned value).
- Empty brackets
()means "no arguments needed" —ROW_NUMBER()just counts, it needs nothing handed to it. LAG(revenue, 1, 0)has three arguments: which column to look at, how many rows back, and what to return if there's nobody there.- Why the topic needs it: all five stars of the show —
ROW_NUMBER,RANK,DENSE_RANK,LAG,LEAD— are function calls. Reading their argument list is step one.
4. Ordering — the ≺ symbol and "strictly ahead"

- The picture: a queue. Sort salaries descending (
ORDER BY salary DESC) and the biggest salary stands at the front. For the row holding700, the two900s are strictly ahead of it — they come before it in the line. - Why "strictly"? Strictly ahead means we do not count the row itself or its equals-at-the-same-value. This word is doing all the work in the parent's rank formulas.
- Why the topic needs it: the parent defines You cannot read that line until you know means "comes before in the ordering".
5. Set-size bars:
- Reading it slowly: = "the count of all values such that comes strictly before ".
- The picture: point at the queue in Figure 2, cover everyone from
rbackwards, and count the heads still visible in front. That count is the bars-value. - Why the topic needs it: this is the entire machinery of
RANKandDENSE_RANK. Once you can count the things strictly ahead, both formulas are just "1 + that count", differing only in what you count (all rows, vs. distinct values).
6. Distinct values

- The picture: rows are individual coins; distinct values are the denominations. Two ₹900 coins are two rows but one denomination.
- Why the topic needs it: this single word is the only difference between the two rank formulas:
RANKcounts rows strictly ahead → two 900s each count → the700has 2 ahead → rank 3.DENSE_RANKcounts distinct values strictly ahead → the 900s collapse to one → the700has 1 ahead → rank 2.
Recall Check yourself: 800, 800, 800, 600
For the 600 row (descending order): how many rows strictly ahead? ::: 3 (three 800s)
So RANK(600) = ? ::: 1 + 3 = 4
How many distinct values strictly ahead? ::: 1 (just the value 800)
So DENSE_RANK(600) = ? ::: 1 + 1 = 2
7. Partition = independent sub-queues

- The picture: one big line of employees splits into several shorter lines, one per department. Each shorter line gets its own counter starting at 1.
- Crucial: partitioning does not delete any rows (unlike
WHERE). Everyone still comes out — they are just grouped for the counting. - Why the topic needs it:
PARTITION BY deptis what makesrn = 1mean "top earner in this department" rather than "top earner in the whole company". Without it, the whole result set is one giant partition.
8. Offsets: "n rows before / after" and subscripts
- The picture: the queue again — stand on kid number ; is the kid directly in front, is the kid directly behind.
- is just "how many rows this partition has" — the last position.
- Why the topic needs it: the parent's LAG/LEAD definition is only readable once , , and "" (does a row that far back even exist?) are pictures. The condition just asks: is there still someone standing that far in front? If not → give the default (recall §2, the empty box).
9. Why not just GROUP BY? (the shape difference)
- Why the topic needs it: this shape difference is the parent's headline claim ("keep every row and get an aggregate"). Everything else — partitions, orderings, ranks — exists only because we refused to collapse the rows.
Prerequisite map
Read it top-down: tables give us rows/columns; ordering gives us "before"; counting-ahead plus distinct give us RANK/DENSE_RANK; partitions and offsets give us per-group and neighbour-peeking. All arrows land on the OVER clause — the parent's central object.
Equipment checklist
Self-test: cover the right side, answer, reveal.
In a table, what is a row vs a column?
What does NULL mean, and is it the same as 0?
What do the empty brackets in ROW_NUMBER() mean?
().Read $x \prec r$ in plain words.
What does "strictly ahead" exclude?
What do the bars in $\big|\{x : x \prec r\}\big|$ compute?
Rows vs distinct values in 900, 900, 700?
Does PARTITION BY delete any rows?
In $r_i$, what is the subscript i?
What does the condition $i-n \ge 1$ in LAG check?
GROUP BY shape vs window function shape?
When every reveal feels obvious, go read the parent: the topic note.
Connections
- GROUP BY and Aggregate Functions — the collapsing "funnel" this page contrasts with the window "stamp".
- SQL Logical Query Processing Order — where ordering and partitioning sit in the pipeline.
- Common Table Expressions (CTE) — clean containers once you filter on window results.
- Self Joins — the old way to fetch a "previous row" before offsets (
LAG/LEAD) existed. - Window Frames ROWS vs RANGE — the next layer: controlling how wide the window of neighbours is.
- Subqueries and Derived Tables — the wrapper that lets you filter on a computed rank.