Window functions
Running totals and window frames
Window functions become especially useful when a calculation depends on the rows that come before or around the current row.
A common example is a running total.
Suppose we want to show each order together with the total amount accumulated up to that order.
Start with the first five orders:
123456SELECT id, total_amountFROM ordersWHERE id <= 5ORDER BY id;You should see:
1234567 id | total_amount----+-------------- 1 | 10.01 2 | 10.02 3 | 10.03 4 | 10.04 5 | 10.05Creating a running total
Run:
12345678910SELECT id, total_amount, SUM(total_amount) OVER ( ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_totalFROM ordersWHERE id <= 5ORDER BY id;You should see:
1234567 id | total_amount | running_total----+--------------+--------------- 1 | 10.01 | 10.01 2 | 10.02 | 20.03 3 | 10.03 | 30.06 4 | 10.04 | 40.10 5 | 10.05 | 50.15The first row contains:
110.01The second contains:
110.01 + 10.02 = 20.03The third contains:
110.01 + 10.02 + 10.03 = 30.06and so on.
The window frame
The important new part is:
1ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWThis defines the window frame.
A partition tells PostgreSQL which broad group of rows belongs to the window.
A frame determines which rows inside that partition are used for the calculation for the current row.
This distinction is important.
Consider:
123partition────────────────────────────────────────────row 1 row 2 row 3 row 4 row 5When PostgreSQL is evaluating row 3, our frame is:
123456partition────────────────────────────────────────────row 1 row 2 row 3 row 4 row 5└──────── frame ───────┘ ▲ current rowFor row 5:
123456partition────────────────────────────────────────────row 1 row 2 row 3 row 4 row 5└──────────────── frame ──────────────┘ ▲ current rowThe frame grows as PostgreSQL moves through the ordered rows.
UNBOUNDED PRECEDING
In:
1ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWUNBOUNDED PRECEDING means:
Start at the first row of the partition.
CURRENT ROW means:
End at the current row.
So for every row, PostgreSQL calculates:
12345first row ↓... ↓current rowThat is why this frame produces a running total.
12345row 1 10.01 frame: [1] → 10.01row 2 10.02 frame: [1, 2] → 20.03row 3 10.03 frame: [1, 2, 3] → 30.06row 4 10.04 frame: [1, 2, 3, 4] → 40.10row 5 10.05 frame: [1, 2, 3, 4, 5] → 50.15Moving calculations
A frame doesn't have to start at the beginning of the partition.
For example, we can calculate an average using the current row and the previous two rows:
12345678910111213SELECT id, total_amount, ROUND( AVG(total_amount) OVER ( ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 2 ) AS moving_averageFROM ordersWHERE id <= 5ORDER BY id;You should see:
1234567 id | total_amount | moving_average----+--------------+---------------- 1 | 10.01 | 10.01 2 | 10.02 | 10.02 3 | 10.03 | 10.02 4 | 10.04 | 10.03 5 | 10.05 | 10.04For row 5, the frame contains:
123row 3row 4row 5so PostgreSQL calculates the average of:
12310.0310.0410.05PRECEDING and FOLLOWING
Frame boundaries can move relative to the current row.
For example:
1ROWS BETWEEN 2 PRECEDING AND CURRENT ROWmeans:
12345two rows before ↓previous row ↓current rowWhile:
1ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWINGmeans:
123previous rowcurrent rownext rowThis can be useful for moving averages and other calculations based on nearby rows.
The boundaries you will commonly encounter are:
12345UNBOUNDED PRECEDINGn PRECEDINGCURRENT ROWn FOLLOWINGUNBOUNDED FOLLOWINGPartition and frame are different
Suppose we calculate a running total separately for every customer:
12345SUM(total_amount) OVER ( PARTITION BY customer_id ORDER BY placed_at, id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)Here:
1PARTITION BY customer_iddetermines which customer's orders belong together.
Then:
1ORDER BY placed_at, iddetermines their sequence inside that partition.
Finally:
1ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWdetermines which of those ordered rows participate in the calculation for each current row.
A useful mental model is:
12345678PARTITION BY→ Which group does this row belong to?ORDER BY→ In what sequence should rows in that group be processed?FRAME→ Which part of that ordered group should this calculation use?What happens if you omit the frame?
Consider:
123SUM(total_amount) OVER ( ORDER BY id)When a window contains ORDER BY but no explicit frame, PostgreSQL uses a default frame that runs from the beginning of the partition through the current row, including any rows that are peers of the current row according to the window ordering.
For an ordering column such as our unique id, this often looks exactly like a running total.
So this shorter query:
123SUM(total_amount) OVER ( ORDER BY id)would produce the running total we expect here.
However, the default behavior becomes more important when multiple rows have the same ORDER BY value.
For row-by-row calculations, explicitly writing:
1ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWmakes the intended frame clear.
ROWS and tied values
ROWS counts actual rows.
For example:
1ROWS BETWEEN 2 PRECEDING AND CURRENT ROWmeans exactly:
123up to two physical result rows before the current row+the current rowPostgreSQL also supports other frame modes such as:
12RANGEGROUPSwhich treat ordering values and peer groups differently.
For most application-level running totals and moving calculations, understanding ROWS first gives you the most useful mental model.
The frame matters for LAST_VALUE
PostgreSQL also provides functions such as:
123FIRST_VALUE()LAST_VALUE()NTH_VALUE()These operate on the current window frame, not automatically on the entire partition.
This is especially important with LAST_VALUE().
Consider:
123LAST_VALUE(total_amount) OVER ( ORDER BY id)Because the default frame ends around the current row, LAST_VALUE() may return the value from the current row rather than the last value from the entire partition.
If we explicitly want the last value from the whole partition, we can define a frame that extends to the end:
1234LAST_VALUE(total_amount) OVER ( ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)Now the frame contains the entire ordered partition.
You can see both side by side:
12345678910111213SELECT id, total_amount, LAST_VALUE(total_amount) OVER ( ORDER BY id ) AS default_frame, LAST_VALUE(total_amount) OVER ( ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS whole_partitionFROM ordersWHERE id <= 5ORDER BY id;You should see:
1234567 id | total_amount | default_frame | whole_partition----+--------------+---------------+----------------- 1 | 10.01 | 10.01 | 10.05 2 | 10.02 | 10.02 | 10.05 3 | 10.03 | 10.03 | 10.05 4 | 10.04 | 10.04 | 10.05 5 | 10.05 | 10.05 | 10.05This is why understanding frames is important even when the window function itself looks simple.
Running total versus ordinary SUM
Compare:
12SELECT SUM(total_amount)FROM orders;with:
12345678SELECT id, total_amount, SUM(total_amount) OVER ( ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )FROM orders;The ordinary aggregate gives one final total.
The window version gives a total for every row based on that row's frame.
The important point is:
A window frame controls which rows inside a partition participate in the calculation for the current row.