Window Functions
Per-row context without collapsing the table
Window functions compute values across a row's surrounding rows without collapsing them — enabling rankings, running totals, moving averages and value-of-peer comparisons within one query.
What you will learn
- Use PARTITION BY and ORDER BY inside OVER
- Apply row/range frames for running and moving computations
- Solve rankings, dedup and value-of-first patterns
What is this?
Window functions perform aggregation-like work over a window of rows relative to the current row, WITHOUT grouping them away: ROW_NUMBER and RANK for ordering, LAG/LEAD for neighbors, SUM/AVG with a frame for running totals and moving averages. They are the workhorse for analytical SQL that keeps every row.
Why do you need it?
- Group-by loses the surrounding context window functions preserve.
- Rankings, deduplication, deltas against neighbors and moving averages need per-row windowing.
- One query replaces a tangle of self-joins and subqueries — and usually runs faster.
- Moving averages smooth noisy operational metrics and trend signals.
Mental model
Progress at a timestamped magazine
Window over rows
GROUP BY is a chapter summary; window functions are the margin notes next to each line — you keep every line, and each note references what's above or around it.
How it works
- OVER defines the partition (PARTITION BY) and order (ORDER BY) of the window.
- Frames (ROWS/RANGE BETWEEN ...) bound which preceding/following rows compute with the current one.
- LAG/LEAD read neighbors; ROW_NUMBER/RANK assign positions within the partition.
- Window aggregates (SUM/AVG OVER) return a value per row while retaining all rows.
- Windows execute after joins/grouping but before ORDER BY in logical processing.
How a window attaches context per row.
Examples
Top region by year and running average
SELECT region, year, revenue, ROW_NUMBER() OVER (PARTITION BY year ORDER BY revenue DESC) AS rank_in_year, AVG(revenue) OVER (ORDER BY year ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS mavgFROM region_yearly;The ranking restarts each year (PARTITION BY) and the moving average uses a 3-row frame over the time-ordered table.
In production
- A ranking report shows each sales rep's rank within their team partition.
- A pipeline dedupes events with ROW_NUMBER partitioned by natural key, keeping the newest.
- A support SLA report smooths daily p95 response with a 7-day moving average.
Best practices
- Partition scope to the comparison (per team, per region) — windowing 'globally' misleads.
- Choose a frame deliberately; default frames differ between running and peer semantics.
- Use ROW_NUMBER for dedup, RANK for competitions with ties.
- Test on a tiny set where the expected window values are hand-computable.
Common mistakes
- Omitting ORDER BY inside OVER, giving a partition-wide aggregate, not a running one.
- Forgetting frame boundaries, so the moving average includes unintended rows.
- Calling ROW_NUMBER 'RANK' and breaking tied competitions.
- Using window functions where a simple GROUP BY would do, adding cost.
Common issues
Running total equals grand total on every rowNo ORDER BY in the window, so the frame is the entire partition
Add ORDER BY inside OVER to make the frame cumulative.
Dedup keeps the wrong rowORDER BY in the ranking picks the wrong 'newest' definition
Order the window by a monotonic vector (updated_at, event_id).
Debugging tips
- If ROW_NUMBER yields unexpected output, check the ORDER BY column and tie behavior.
- If a moving average looks flat, verify the frame (PRECEDING/FOLLOWING) direction.
- If a window aggregate equals the whole-partition total, the ORDER BY is missing.
Interview checkpoint
Hands-on lab
- Rank teams with PARTITION BY
- Dedupe events keeping the newest row
- Compute a 3-day moving average
Rank, Dedupe and Smooth with Windows
40 min · advanced
Quick quiz
Which function would you use to keep the newest row per natural key?
A window function always reduces the number of rows in the result.