Window Functions
Aggregate without collapsing, compare without self-joining
Window functions compute ranks, running totals and row-over-row comparisons over a window without collapsing rows — the key to moving averages, dedup and session analysis.
What you will learn
- Write ROW_NUMBER, RANK, LAG/LEAD and SUM() OVER
- Control the window with PARTITION BY and ORDER BY
- Deduplicate rows by a natural key with row_number
What is this?
A window function computes a value for each row using the surrounding rows in its window, defined by PARTITION BY (groups) and ORDER BY (order within a group). Unlike GROUP BY it does not collapse rows — each input row gets an output row with the window value attached. This powers ranks, running totals, moving averages and row-over-row diffs.
Why do you need it?
- Row-over-row comparisons (LAG/LEAD) replace fragile self-joins.
- ROW_NUMBER dedup on a natural key is the canonical 'keep the latest per key' pattern.
- Moving averages smooth noisy metrics without GROUP BY collapse.
Mental model
Scorekeepers on each lane
Window functions
Each bowler still gets their own scorecard row (rows aren't deleted), but each lane has a running total and a rank shown alongside. Window functions attach group-derived numbers to individual rows without merging anyone.
How it works
- PARTITION BY splits rows into independent groups; the window resets at each partition boundary.
- ORDER BY inside OVER sets the order used by ranking and running totals.
- ranking: ROW_NUMBER (unique 1..n), RANK (ties share rank, then gap), DENSE_RANK (ties share, no gap).
- LAG(col, n)/LEAD(col, n) fetch the nth previous/next row's value — NULL at the edges.
- Frames (ROWS BETWEEN ... AND ...) bound the window for moving/aggregate computations.
A windowed running total partitioned by channel.
Examples
Deduplicate to the latest record per key
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) AS rn FROM order_updates)SELECT order_id, statusFROM rankedWHERE rn = 1;Each order_id ranks its updates by recency; rn = 1 selects the latest, deduping without a self-join.
In production
- A dedup layer keeps one row per natural key using ROW_NUMBER over a compact partition.
- A trend table computes a 7-day moving average with AVG() OVER (... ROWS BETWEEN 6 PRECEDING AND CURRENT ROW).
- Session analysis marks a row as 'session start' when LAG(ts) exceeds a gap.
Best practices
- Partition by the grain you mean; forget the partition and ranks span the whole table.
- Anchor ORDER BY inside OVER explicitly — ranking without a deterministic order is non-reproducible.
- Filter window output (WHERE rn = 1) inside a CTE/subquery, not in the same SELECT.
- Use DENSE_RANK for bucket numbering where gaps would mislabel levels.
Common mistakes
- Omitting PARTITION BY and ranking across a whole table.
- Putting the window in WHERE directly (dialect errors): wrap in a subquery/CTE first.
- ORDER BY inside OVER omitted, so 'latest' is an arbitrary row.
- Using fruitless ROWS BETWEEN defaults when the frame includes the whole partition (fine) but forgetting to now proper bounds for moving windows.
Common issues
rn = 1 filter returns more than one row per keyPseudo-duplicate keys (case/whitespace) or wrong partition columns
Normalize the partition key and re-test with COUNT(*) per key.
LAG of first row is not NULL as documentedFrame or ORDER BY omitted changes default behavior
Explicitly write ORDER BY in OVER and rely on default frame; check the dialect's null handling.
Debugging tips
- Print the raw window value before filtering on rn to see why a row was kept.
- Compare RANK vs ROW_NUMBER counts when 'duplicates' unexpectedly vanish.
- Take a 3-row partition and check the window output by hand to validate frame logic.
Interview checkpoint
Hands-on lab
- Rank rows within partitions
- Dedup to latest-per-key with row_number
- Compute running totals and row-over-row diffs
Deduplicate, Rank, and Compute Running Totals
30 min · intermediate
Quick quiz
ROW_NUMBER() OVER (PARTITION BY cust ORDER BY ts DESC): which row of cust has rn = 1?
Window functions collapse rows the way GROUP BY does.