- Anatomy
- fn() OVER (PARTITION BY group_cols ORDER BY sort_cols [frame]). PARTITION BY = restart per group (like GROUP BY without collapsing). ORDER BY = ordering within the partition. Omit PARTITION BY to window over the whole result.
- ROW_NUMBER vs RANK vs DENSE_RANK
- On ties: ROW_NUMBER gives 1,2,3 (arbitrary tiebreak), RANK gives 1,1,3 (gaps), DENSE_RANK gives 1,1,2 (no gaps). "Second highest salary" wants DENSE_RANK = 2; "top 3 per group, exactly 3" wants ROW_NUMBER <= 3.
- LAG / LEAD
- Previous / next row's value within the partition: LAG(price) OVER (PARTITION BY ticker ORDER BY day). The backbone of day-over-day deltas, sessionization, and gap detection. Third arg sets a default for the first row: LAG(price, 1, 0).
- Running totals / moving averages
- SUM(amount) OVER (ORDER BY day) = running total. AVG(amount) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) = 7-day moving average. With ORDER BY, the default frame is UNBOUNDED PRECEDING to CURRENT ROW.
- NTILE(n)
- Bucket rows into n equal groups - quartiles, deciles, percentile cohorts: NTILE(4) OVER (ORDER BY revenue).
- Where you can use them
- SELECT and ORDER BY only. Not in WHERE, GROUP BY, or HAVING - windows compute after grouping. To filter on one ("rows where ROW_NUMBER = 1"), wrap it in a subquery or CTE and filter the outer query. Interviewers test this constantly.