4.4.9Databases

Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD

1,929 words9 min readdifficulty · medium2 backlinks

WHY do window functions exist?

  • GROUP BYN rows in, fewer rows out (collapse).
  • Window function → N rows in, N rows out (annotate).

That single difference is 80% of the value. Memorise it.


WHAT is the anatomy of a window?

  • PARTITION BY = "restart the calculation for each group" (like a per-group reset).
  • ORDER BY = "in what sequence do I walk through the rows" (needed for ranking & lag/lead).
  • If you omit PARTITION BY, the whole result set is one partition.

The five core functions

HOW the three rankers differ (derive it!)

Take ordered salaries (DESC): 900, 900, 700, 500.

salary ROW_NUMBER RANK DENSE_RANK
900 1 1 1
900 2 1 1
700 3 3 2
500 4 4 3

Derivation logic:

  • ROW_NUMBER just increments: position in the line.
  • RANK = 1 + (number of rows strictly ahead of you). Two 900s → the 700 has 2 rows ahead → rank 3.
  • DENSE_RANK = 1 + (number of distinct values strictly ahead). The 700 has 1 distinct value ahead (900) → rank 2.

That formula is the definition — no memorising the gap rule.

RANK(r)=1+{x:xr},DENSE_RANK(r)=1+{distinct v:vr}\text{RANK}(r) = 1 + \big|\{x : x \prec r\}\big|, \qquad \text{DENSE\_RANK}(r) = 1 + \big|\{\text{distinct } v : v \prec r\}\big|

Figure — Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD

Worked Example 1 — Top earner per department

SELECT name, dept, salary
FROM (
  SELECT name, dept, salary,
         ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
  FROM employees
) t
WHERE rn = 1;
  • Why PARTITION BY dept? We want the counter to restart for each department, so rn=1 means "top in this dept".
  • Why ORDER BY salary DESC? rn=1 must land on the highest salary.
  • Why the subquery? You ==cannot use a window function in WHERE== (it's computed after WHERE). So compute it in an inner query, filter outside.

Worked Example 2 — RANK vs DENSE_RANK pitfall

SELECT name, salary,
       RANK()       OVER (ORDER BY salary DESC) AS r,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dr
FROM employees;
  • Why might "top 3 salaries" break with RANK? If two people tie for 2nd, RANK gives 1,2,2,4 — so WHERE r <= 3 returns only 3 rows but skips the value that would've been 3rd-distinct.
  • Why this step matters: "Top 3 distinct salary levels" → use DENSE_RANK. "Top 3 people by position" → use ROW_NUMBER. Choosing the wrong ranker is the #1 interview trap.

Worked Example 3 — Day-over-day change with LAG

SELECT day, revenue,
       LAG(revenue, 1, 0) OVER (ORDER BY day) AS prev_rev,
       revenue - LAG(revenue, 1, 0) OVER (ORDER BY day) AS delta
FROM sales;
  • Why LAG? We need the previous row's value beside the current row to compute a difference — exactly what window functions give without a self-join.
  • Why the 0 default? The very first day has no predecessor; without a default it'd be NULL, making delta NULL. 0 is a sane fallback.
  • Why ORDER BY day? "Previous" is meaningless without an order — LAG follows the window's ordering.

Recall Feynman: explain to a 12-year-old

Imagine kids standing in a line sorted by height. ROW_NUMBER is just counting heads: 1, 2, 3… RANK gives medals — if two kids are equally tall they both get silver (2nd), and the next kid gets 4th because two silvers were handed out. DENSE_RANK is a friendlier judge: two silvers, but the next kid still gets bronze (3rd), no skipping. LAG is asking "how tall is the kid in front of me?" and LEAD is asking "how tall is the kid behind me?" — all while everyone stays standing in line (nobody gets merged into a single blob like GROUP BY would do).


Common mistakes


Flashcards

Window function vs GROUP BY: key difference?
GROUP BY collapses N rows into fewer; a window function keeps all N rows and annotates each.
What does the OVER clause define?
The window — the set of related rows the function sees, via PARTITION BY, ORDER BY, and an optional frame.
ROW_NUMBER on ties 900,900,700?
1, 2, 3 — strict counter, ties broken arbitrarily.
RANK on 900,900,700?
1, 1, 3 — ties share rank, then skip.
DENSE_RANK on 900,900,700?
1, 1, 2 — ties share rank, no gaps.
Formula for RANK of row r?
1 + (number of rows strictly ahead of r in the ordering).
Formula for DENSE_RANK of row r?
1 + (number of distinct values strictly ahead of r).
What does LAG(col, 2) return?
The value of col from 2 rows before the current row in the window order.
What does LEAD do?
Returns a value from a following row (n rows ahead, default 1).
Why can't you put a window function in WHERE?
Window functions are evaluated after WHERE in the logical query order; use a subquery/CTE/QUALIFY.
Does PARTITION BY remove rows?
No — it only resets the calculation per group; all rows are returned.
"Top 3 distinct salary levels" → which function?
DENSE_RANK (then filter <= 3).
Purpose of the default arg in LAG(col, 1, 0)?
Value to return when no predecessor exists (e.g., first row), avoiding NULL.

Connections

  • GROUP BY and Aggregate Functions — the collapsing cousin; windows annotate instead.
  • Common Table Expressions (CTE) — clean way to filter on window results.
  • SQL Logical Query Processing Order — explains why windows run after WHERE.
  • Self Joins — what LAG/LEAD replace for "previous/next row" problems.
  • Window Frames ROWS vs RANGE — deeper control over running totals/moving averages.
  • Subqueries and Derived Tables — wrapping windows to filter them.

Concept Map

N in fewer out

keeps N rows in N out

requires

splits groups

orders rows

enables

enables

enables

enables

skips numbers on ties

no ties, pick rn=1

restart per dept

GROUP BY collapses rows

Window function

OVER clause

PARTITION BY

ORDER BY

ROW_NUMBER

RANK

DENSE_RANK

LAG and LEAD

Top earner per dept

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, window functions ka basic funda ye hai: normal GROUP BY saare rows ko ek blob mein squeeze kar deta hai, par window function har row ko zinda rakhta hai aur uske saath-saath ek extra answer bhi de deta hai. Matlab tum apni salary bhi dekh sakte ho aur company mein apna rank bhi — dono ek hi row mein. Yahi 80% kaam hai inka.

OVER(...) clause hi asli jaadu hai. PARTITION BY ka matlab hai "har group ke liye calculation reset kar do" (jaise har department ka apna rank), aur ORDER BY batata hai ki rows kis order mein chalengi (ranking aur LAG/LEAD ke liye zaroori hai). Yaad rakho — PARTITION BY rows ko hataata nahi, sirf boundary banaata hai.

Teen rankers ka difference ties pe dikhta hai. Maan lo 900, 900, 700 hai: ROW_NUMBER seedha ginti — 1,2,3. RANK ties ko same number deta hai par phir number skip karta hai — 1,1,3. DENSE_RANK skip nahi karta — 1,1,2. Interview mein "top 3 distinct salaries" maange to DENSE_RANK, aur "top 3 log positionwise" maange to ROW_NUMBER. LAG pichli row ka value laata hai (jaise aaj vs kal ka revenue), LEAD agli row ka. Ek important trap: window function ko WHERE mein nahi likh sakte, kyunki ye WHERE ke baad chalti hai — isliye subquery ya CTE mein daal ke bahar filter karo.

Go deeper — visual, from zero

Test yourself — Databases

Connections