Window functions — ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD
WHY do window functions exist?
GROUP BY→ N 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.

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, sorn=1means "top in this dept". - Why
ORDER BY salary DESC?rn=1must land on the highest salary. - Why the subquery? You ==cannot use a window function in
WHERE== (it's computed afterWHERE). 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,
RANKgives1,2,2,4— soWHERE r <= 3returns 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" → useROW_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
0default? The very first day has no predecessor; without a default it'd beNULL, makingdeltaNULL.0is 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?
What does the OVER clause define?
ROW_NUMBER on ties 900,900,700?
RANK on 900,900,700?
DENSE_RANK on 900,900,700?
Formula for RANK of row r?
Formula for DENSE_RANK of row r?
What does LAG(col, 2) return?
What does LEAD do?
Why can't you put a window function in WHERE?
Does PARTITION BY remove rows?
"Top 3 distinct salary levels" → which function?
Purpose of the default arg in LAG(col, 1, 0)?
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
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.