FIRST_VALUE & LAST_VALUE
FIRST_VALUE() and LAST_VALUE() are window functions that return the value of an expression from the first or last row of the current window frame, respectively. They are useful for questions like "what was the opening price of the day?" or "what is the most recent status for this record, shown next to every row?"
FIRST_VALUE(expression) OVER (PARTITION BY ... ORDER BY ... [frame_clause]) LAST_VALUE(expression) OVER (PARTITION BY ... ORDER BY ... [frame_clause])
A straightforward FIRST_VALUE example
Given hourly stock price ticks for a single day:
tick_time | price ----------+------ 09:00 | 100 10:00 | 103 11:00 | 101 12:00 | 105
SELECT tick_time, price, FIRST_VALUE(price) OVER (ORDER BY tick_time) AS opening_price FROM stock_ticks;
tick_time | price | opening_price ----------+-------+-------------- 09:00 | 100 | 100 10:00 | 103 | 100 11:00 | 101 | 100 12:00 | 105 | 100
FIRST_VALUE() behaves as expected here: every row sees the same opening price, 100, taken from the first row of the window.
The LAST_VALUE trap
Trying the equivalent query with LAST_VALUE() to fetch the day's closing price often surprises people the first time:
SELECT tick_time, price, LAST_VALUE(price) OVER (ORDER BY tick_time) AS closing_price FROM stock_ticks;
tick_time | price | closing_price ----------+-------+-------------- 09:00 | 100 | 100 10:00 | 103 | 103 11:00 | 101 | 101 12:00 | 105 | 105
The fix: an explicit frame clause
To make LAST_VALUE() see the entire partition regardless of which row is currently being evaluated, extend the frame explicitly to UNBOUNDED FOLLOWING:
SELECT
tick_time,
price,
LAST_VALUE(price) OVER (
ORDER BY tick_time
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS closing_price
FROM stock_ticks;tick_time | price | closing_price ----------+-------+-------------- 09:00 | 100 | 105 10:00 | 103 | 105 11:00 | 101 | 105 12:00 | 105 | 105
Now every row correctly sees 105, the price from the last tick of the day, as the closing price. The frame clause ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING tells the database to consider the whole partition for every row, not just the rows up to the current one.
Partitioned example
Combine PARTITION BY with the corrected frame to get, say, the first and last price of the day per stock symbol:
SELECT
symbol,
tick_time,
price,
FIRST_VALUE(price) OVER (
PARTITION BY symbol ORDER BY tick_time
) AS opening_price,
LAST_VALUE(price) OVER (
PARTITION BY symbol ORDER BY tick_time
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS closing_price
FROM stock_ticks;