On this page
Syntax
LAST_VALUE(expression) OVER (ORDER BY ...)LAST_VALUE(expression) OVER (PARTITION BY ... ORDER BY ...)LAST_VALUE(expression) OVER (PARTITION BY ... ORDER BY ... ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) | Parameter | Type | Required | Description |
|---|---|---|---|
expression |
column or expression | yes | The value to read from the last row of the frame. Usually a single column. |
PARTITION BY |
clause | no | Splits the rows into independent groups; the last value is computed separately per partition. |
ORDER BY |
clause | yes | Defines the order of rows inside each partition. Without it there is no meaningful "last" row. |
frame |
clause | no | The ROWS or RANGE clause that bounds the frame. Almost always required in practice: use ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to reach the true last value. |
How it works
LAST_VALUE() is a window function: it returns the value of an expression evaluated on the last row of the current window frame, while keeping every input row instead of collapsing them like an aggregate. It is the mirror image of FIRST_VALUE(), and both belong to the same family as NTH_VALUE().
Here is the single most important thing to understand, and the reason most LAST_VALUE() queries look broken. When you write an ORDER BY inside OVER() but do not specify a frame, SQL applies a default frame of RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. That frame ends at the current row, so LAST_VALUE() returns the value of the current row on every line, not the last row of the partition. It looks like the function does nothing useful.
To get the true last value of the whole partition you must widen the frame yourself with ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. This tells the engine to look ahead to the final row of the partition. See the window functions guide for how frames work in general, and browse the full SQL functions library for related tools.
Examples
The default-frame trap: WRONG vs RIGHT side by side
-- WRONG: no frame, so the default RANGE ... CURRENT ROW
-- makes LAST_VALUE return the CURRENT row's amount
SELECT id, amount,
LAST_VALUE(amount) OVER (ORDER BY id) AS wrong_last
FROM orders;
-- RIGHT: widen the frame to the whole partition
SELECT id, amount,
LAST_VALUE(amount) OVER (
ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS right_last
FROM orders;
id | amount | wrong_last | right_last ---+--------+------------+----------- 1 | 100 | 100 | 450 2 | 250 | 250 | 450 3 | 450 | 450 | 450
Latest status per entity
-- Show the most recent status next to every event row
SELECT ticket_id, changed_at, status,
LAST_VALUE(status) OVER (
PARTITION BY ticket_id
ORDER BY changed_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS current_status
FROM ticket_history;
ticket_id | changed_at | status | current_status
----------+------------+---------+---------------
10 | 2026-01-02 | open | closed
10 | 2026-01-05 | pending | closed
10 | 2026-01-09 | closed | closedMost recent order amount on every row
-- Every customer row carries their latest order amount
SELECT customer_id, order_date, amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS latest_amount
FROM orders;
customer_id | order_date | amount | latest_amount
------------+------------+--------+--------------
7 | 2026-02-01 | 120 | 90
7 | 2026-03-14 | 300 | 90
7 | 2026-04-20 | 90 | 90FIRST_VALUE with reversed ORDER BY: the simpler alternative
-- Same result as LAST_VALUE with a full frame, but no
-- frame clause needed: just reverse the sort and take
-- the first row. Often easier to read.
SELECT customer_id, order_date, amount,
FIRST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date DESC
) AS latest_amount
FROM orders;
customer_id | order_date | amount | latest_amount
------------+------------+--------+--------------
7 | 2026-02-01 | 120 | 90
7 | 2026-03-14 | 300 | 90
7 | 2026-04-20 | 90 | 90Common mistakes
-- The #1 LAST_VALUE bug: no frame clause.
-- The default frame ends at the CURRENT row, so this
-- returns the current row's value, not the last one.
SELECT id, amount,
LAST_VALUE(amount) OVER (ORDER BY id) AS last_amount
FROM orders;
Right
-- Add an explicit full frame so the window reaches
-- the final row of the partition.
SELECT id, amount,
LAST_VALUE(amount) OVER (
ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_amount
FROM orders;
This is the single most common LAST_VALUE() mistake. An ORDER BY inside OVER() with no frame defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which stops at the current row. Always add ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to get the true last value.
-- No ORDER BY, so there is no defined "last" row.
-- The result is arbitrary and engine dependent.
SELECT id, amount,
LAST_VALUE(amount) OVER (PARTITION BY customer_id) AS last_amount
FROM orders;
Right
-- ORDER BY gives the rows a meaningful sequence,
-- so "last" actually means something.
SELECT id, amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_amount
FROM orders;
Without ORDER BY the window has no ordering, so "the last row" is undefined and the result you get is not reliable. LAST_VALUE() only makes sense with an explicit ORDER BY.
-- Fighting with the frame clause when all you want
-- is the most recent value per group.
SELECT customer_id, amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS latest
FROM orders;
Right
-- Reverse the sort and use FIRST_VALUE: no frame
-- clause, and the intent is clearer.
SELECT customer_id, amount,
FIRST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date DESC
) AS latest
FROM orders;
When you only need the last value, FIRST_VALUE() with a reversed ORDER BY is often simpler: it needs no frame clause and reads more clearly. Reach for LAST_VALUE() when the natural sort must stay ascending.
Performance
LAST_VALUE() requires the engine to build and sort each partition, so an index that matches your PARTITION BY and ORDER BY columns lets the optimiser avoid an extra sort step. Without a supporting index, large partitions mean a full sort in memory or spilling to disk.
Using the full frame ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING is cheap once the partition is sorted, because the last row is already known. If you need the same last value on many rows, computing it once with a window function is usually faster than a correlated subquery that re-scans the table for every row. See the window functions guide for more.
Interview questions
Why does LAST_VALUE() often return the current row instead of the last row?
Because an ORDER BY inside OVER() with no frame defaults to RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. That frame ends at the current row, so LAST_VALUE() reads the current row. You must widen the frame with ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
How do you make LAST_VALUE() return the true last value of a partition?
Add an explicit frame of ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING so the window covers every row in the partition, from the first to the last.
SELECT customer_id, amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS latest
FROM orders;
What is the difference between FIRST_VALUE() and LAST_VALUE()?
FIRST_VALUE() returns the value from the first row of the frame and works correctly with the default frame, because that frame already starts at the first row. LAST_VALUE() returns the value from the last row of the frame, but the default frame ends at the current row, so it usually needs an explicit full frame.
When would you use FIRST_VALUE() instead of LAST_VALUE()?
When you only need the most recent value, reversing the ORDER BY and using FIRST_VALUE() avoids the frame clause entirely and is easier to read. Use LAST_VALUE() when the ascending sort must be preserved for other columns in the same query.
Does LAST_VALUE() require an ORDER BY?
In practice yes. Without an ORDER BY inside OVER() the rows have no defined order, so "the last row" is undefined and the result is arbitrary. Always provide an ORDER BY so that "last" has a clear meaning.