39 Window Functions
Window functions compute a value for each row using a window — a set of related rows defined by an OVER (...) clause. Unlike GROUP BY, which collapses rows, window functions add a per-row result alongside the original columns. They are the right tool for rankings, running totals, moving averages, percentile bands, “previous row” comparisons — anything you used to do in your application code by sorting and iterating.
What you’ll learn
- The shape of a window function:
f() OVER (PARTITION BY ... ORDER BY ...) - The most useful functions:
row_number,rank,dense_rank,lag,lead,sum,avg,first_value,last_value - Partitioning vs ordering inside
OVER - Reusable windows with
WINDOW name AS (...) - The frame clause:
ROWS BETWEEN ... AND ...
Example
The window is defined by three pieces:
PARTITION BY— splits the rows into independent groups. LikeGROUP BY, but each group keeps its rows.ORDER BY— orders rows inside each partition. Determinesrow_number,lag, running totals, etc.- Frame clause (optional) —
ROWS BETWEEN ... AND ...orRANGE BETWEEN .... Defines which rows around the current row are in the window. Defaults differ by function family: ranking functions ignore the frame; withORDER BY, the default frame includes all rows through the current row’s last peer; withoutORDER BY, it covers the partition.
The differences between row_number, rank, and dense_rank matter when there are ties. row_number always assigns 1, 2, 3, … with ties broken arbitrarily. rank assigns 1, 2, 2, 4 (gap after a tie). dense_rank assigns 1, 2, 2, 3 (no gap).
The “top-N per group” pattern (PARTITION BY group_col ORDER BY rank_col DESC + filter rn = 1) is the canonical use of row_number. It replaces convoluted self-joins and subqueries.
Chapter 38 archives order 3. The output here follows that chapter order and has two remaining orders. Rerunning chapter 29 restores all three original orders.
Run the example
To run (requires orders_example):
Common pitfalls
- Window functions are not allowed in
WHEREorHAVING(they are computed after those clauses). Wrap the query in a CTE or subquery and filter outside. - The default frame for
sum() OVER (ORDER BY ...)isRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW— peer rows with equalORDER BYvalues are summed into the same total. UseROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWto be strict about row-by-row running. - Without
ORDER BYinsideOVER,lag/leaduse an unspecified row order and PostgreSQL does not warn. Specify the order and any tie-breaker. - Performance: window functions sort the partition. With a matching index on
(partition_col, order_col), the sort can be skipped.
“Show the change vs. previous row” is total - lag(total) OVER (ORDER BY ...). “Show as percent of total” is 100.0 * total / sum(total) OVER (). Both replace a self-join or subquery with a single readable expression.
Add ntile(4) to put each order into quartiles: ntile(4) OVER (ORDER BY total). Then compute “today’s total vs. yesterday’s” on a time-series table by lag(daily_total) OVER (ORDER BY day). Then try first_value(total) OVER (PARTITION BY customer_id ORDER BY id) to see the customer’s first order amount on every row.
Downloads
SQL file: window-functions.sql · All lesson files · Setup guide