FIRST_VALUE(), LAST_VALUE(), and NTH_VALUE() reach into a
window and pull back a value from a specific position within it: the first row, the last row, or
the nth row. They sound simple, and FIRST_VALUE() is. But LAST_VALUE()
is the single most misunderstood window function in SQL, because its answer depends entirely on a
frame most people never realise is there. Understanding these three functions is really about
understanding the frame they read from.
Value by Position, Not by Offset
These three belong to the family of value functions, functions that fetch a value from
another row rather than computing something new. Where LAG() and LEAD()
reach a relative distance from the current row (one row back, two rows forward),
FIRST_VALUE(), LAST_VALUE(), and NTH_VALUE() reach an
absolute position within the frame: the first row in view, the last row in view,
the nth row in view. That phrase, "within the frame," is the whole story. These functions do not
look at the partition as a whole; they look at whatever slice of it the window frame currently
exposes, and the frame has a default you did not write.
All three are order-sensitive: if you want "first," "last," or "nth" to have a meaningful and
reproducible interpretation, define an ORDER BY in the window specification. Each
takes one expression to return; NTH_VALUE() also takes the position to fetch.
One mental model covers all three. PARTITION BY decides which rows belong together,
ORDER BY sequences them, the frame decides which part of that sequence the current row
can see, and the function simply reads one position of that frame:
The Sample Dataset
We use a small table of monthly revenue, read in calendar order. month is already
unique, so ordering by it is deterministic on its own. revenue is not: February and
March share 1200.00, and April and May share 1500.00. Whenever we order by revenue
and the specific row matters, the id column serves as a tiebreaker. Those ties also
matter for how the frame behaves, as we will see.
monthly_sales
| id | month | revenue |
|---|---|---|
| 1 | 2024-01 | 1000.00 |
| 2 | 2024-02 | 1200.00 |
| 3 | 2024-03 | 1200.00 |
| 4 | 2024-04 | 1500.00 |
| 5 | 2024-05 | 1500.00 |
| 6 | 2024-06 | 1800.00 |
FIRST_VALUE(): the First Row in the Frame
FIRST_VALUE() returns the value of the expression from the first row of the frame.
Under the default frame, which starts at UNBOUNDED PRECEDING (the very start of the
partition), that first row never changes as you move down the rows, so the result is stable on
every row. This makes FIRST_VALUE() the easy, well-behaved member of the trio:
① Ordered input
| month | revenue |
|---|---|
| 2024-01 | 1000.00 |
| 2024-02 | 1200.00 |
| 2024-03 | 1200.00 |
| 2024-04 | 1500.00 |
| 2024-05 | 1500.00 |
| 2024-06 | 1800.00 |
② Result with first_rev
| month | revenue | first_rev |
|---|---|---|
| 2024-01 | 1000.00 | 1000.00 |
| 2024-02 | 1200.00 | 1000.00 |
| 2024-03 | 1200.00 | 1000.00 |
| 2024-04 | 1500.00 | 1000.00 |
| 2024-05 | 1500.00 | 1000.00 |
| 2024-06 | 1800.00 | 1000.00 |
Every row reports 1000.00, January's revenue and the first row in the ordering. The default frame
runs from the start of the partition through the current row, and because it always begins at the
same place, "the first row in the frame" is always January. FIRST_VALUE() works as
expected precisely because the default frame's start is fixed. As we are about to see,
LAST_VALUE() is undone by the fact that the default frame's end is not.
FIRST_VALUE() with PARTITION BY
The real power of FIRST_VALUE() shows when you order within groups. For this example
we use a small regional_sales table: three months of revenue for each of two regions,
North and South (every row appears in the result below). Order each partition by revenue
descending, and the first row of the frame becomes the top row of the group, a compact way to put
"the best month in this region" on every row. Because the top value sits at the fixed start of the
frame, the default frame is enough here:
| region | month | revenue | best_month |
|---|---|---|---|
| North | 2024-02 | 1200.00 | 2024-02 |
| North | 2024-03 | 1100.00 | 2024-02 |
| North | 2024-01 | 1000.00 | 2024-02 |
| South | 2024-03 | 1050.00 | 2024-03 |
| South | 2024-02 | 950.00 | 2024-03 |
| South | 2024-01 | 800.00 | 2024-03 |
Each region's rows now carry the month of its highest revenue: February for North, March for
South. Ordering by revenue DESC puts the best month at the top of each partition, and
FIRST_VALUE() reads it off. The rows are shown ordered by revenue within each region
so the effect is easy to follow.
LAST_VALUE(): the Function That Surprises Everyone
You would expect LAST_VALUE() to be the mirror of FIRST_VALUE(): the
last row of the partition, on every row. It is not, and this is the classic SQL gotcha. Run the
obvious query:
① What you expected
| month | revenue | last_rev |
|---|---|---|
| 2024-01 | 1000.00 | 1800.00 |
| 2024-02 | 1200.00 | 1800.00 |
| 2024-03 | 1200.00 | 1800.00 |
| 2024-04 | 1500.00 | 1800.00 |
| 2024-05 | 1500.00 | 1800.00 |
| 2024-06 | 1800.00 | 1800.00 |
② What you actually get
| month | revenue | last_rev |
|---|---|---|
| 2024-01 | 1000.00 | 1000.00 |
| 2024-02 | 1200.00 | 1200.00 |
| 2024-03 | 1200.00 | 1200.00 |
| 2024-04 | 1500.00 | 1500.00 |
| 2024-05 | 1500.00 | 1500.00 |
| 2024-06 | 1800.00 | 1800.00 |
The actual result gives each row its own revenue back, not the final 1800.00. The reason
is the default frame. With ORDER BY present, the frame is
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW in engines such as PostgreSQL,
MySQL, SQL Server, SQLite, and Oracle. Because month is unique, that frame ends
exactly at the current row, so "the last row in the frame" is the current row itself, and
LAST_VALUE() faithfully returns it. The function is not broken; it is answering a question about a frame that stops where you are
standing.
The fix is to say what you actually mean: a frame that extends all the way to the end of the partition.
| month | revenue | last_rev |
|---|---|---|
| 2024-01 | 1000.00 | 1800.00 |
| 2024-02 | 1200.00 | 1800.00 |
| 2024-03 | 1200.00 | 1800.00 |
| 2024-04 | 1500.00 | 1800.00 |
| 2024-05 | 1500.00 | 1800.00 |
| 2024-06 | 1800.00 | 1800.00 |
With the frame extended to UNBOUNDED FOLLOWING, every row can see through to the last
row of the partition, and LAST_VALUE() returns the genuine final revenue of 1800.00
everywhere. The rule to remember is about intention: when you mean "the last value in the
entire partition," say so with a frame ending in UNBOUNDED FOLLOWING.
When the Ordering Has Ties
There is one more subtlety hiding in that default frame. It is a RANGE frame, and
with RANGE, CURRENT ROW does not mean the current physical row: it means
the current row and all of its peers, the rows that tie on the ORDER BY
value. Ordering by the unique month column hides this. Order by revenue
instead, and the effect becomes visible and measurable.
The clearest way to see it is to count how many rows are actually inside the frame under each
mode, using COUNT(*). Unlike asking "which row is last," a row count does not depend
on how the engine breaks ties within a peer group, so the comparison is fully deterministic:
| id | month | revenue | rows_count | range_count |
|---|---|---|---|---|
| 1 | 2024-01 | 1000.00 | 1 | 1 |
| 2 | 2024-02 | 1200.00 | 2 | 3 |
| 3 | 2024-03 | 1200.00 | 3 | 3 |
| 4 | 2024-04 | 1500.00 | 4 | 5 |
| 5 | 2024-05 | 1500.00 | 5 | 5 |
| 6 | 2024-06 | 1800.00 | 6 | 6 |
rows_count grows by exactly one at each row, because the id tiebreaker
makes every row its own peer, and ROWS simply counts physical position.
range_count tells a different story: id 2 and id 3, the two 1200.00 rows, both report
3, because RANGE treats them as one indivisible peer group and the
frame jumps straight from 1 to 3 without stopping at 2. The same happens to the 1500.00 pair,
jumping from 3 to 5. This is the concrete shape of "peers share a frame": the frame boundary moves
in steps of a whole peer group, not one row at a time.
Now put a value function in that same frame instead of a count, and the ambiguity from the section above becomes visible:
For February, last_month may come back as 2024-03 rather than
2024-02, because, as the table above just showed, March sits inside the same
three-row peer group. Worse, which of the two tied rows counts as "last" is not guaranteed at all:
ORDER BY revenue says nothing about how February and March are ordered relative to
each other, so the engine is free to put either one at the end of the peer group.
ORDER BY revenue, id) and state the frame with ROWS, which
counts physical rows instead of peer groups. Note that adding id changes the definition
of peers: every row becomes its own peer group, so the tie behaviour disappears.
NTH_VALUE(): the Nth Row in the Frame
NTH_VALUE(expression, n) generalises the idea: it returns the expression from the
nth row of the frame. The position n is 1-based, so NTH_VALUE(x, 1)
reads the same row as FIRST_VALUE(x), and it must be a positive integer: engines
reject 0 or negative values rather than interpreting them. Like LAST_VALUE(), it reads the frame, so it
is subject to the same default-frame effect, and it additionally returns NULL
whenever the frame does not yet contain n rows. Here we ask for the third month's revenue, with
the frame extended so every row can see it:
| month | revenue | third_rev |
|---|---|---|
| 2024-01 | 1000.00 | 1200.00 |
| 2024-02 | 1200.00 | 1200.00 |
| 2024-03 | 1200.00 | 1200.00 |
| 2024-04 | 1500.00 | 1200.00 |
| 2024-05 | 1500.00 | 1200.00 |
| 2024-06 | 1800.00 | 1200.00 |
The third row in calendar order is March, at 1200.00, so every row reports 1200.00. Because the frame spans the whole partition, the "third row" is the same for all of them.
Watch what happens with the default frame instead. Now the frame grows one row at a time, and until it contains at least three rows, there is no third row to return:
| month | revenue | third_rev |
|---|---|---|
| 2024-01 | 1000.00 | NULL |
| 2024-02 | 1200.00 | NULL |
| 2024-03 | 1200.00 | 1200.00 |
| 2024-04 | 1500.00 | 1200.00 |
| 2024-05 | 1500.00 | 1200.00 |
| 2024-06 | 1800.00 | 1200.00 |
January and February return NULL: their frames (one row, then two rows) have no third
element yet. From March on, the frame holds at least three rows and NTH_VALUE()
settles on March's 1200.00. This is the same default-frame lesson as LAST_VALUE(),
with an extra wrinkle: an nth position that the frame has not reached yields NULL, not
an error.
The Three Together
Running all three with the same extended frame shows how they carve different positions out of the same window:
| month | revenue | first_rev | third_rev | last_rev |
|---|---|---|---|---|
| 2024-01 | 1000.00 | 1000.00 | 1200.00 | 1800.00 |
| 2024-02 | 1200.00 | 1000.00 | 1200.00 | 1800.00 |
| 2024-03 | 1200.00 | 1000.00 | 1200.00 | 1800.00 |
| 2024-04 | 1500.00 | 1000.00 | 1200.00 | 1800.00 |
| 2024-05 | 1500.00 | 1000.00 | 1200.00 | 1800.00 |
| 2024-06 | 1800.00 | 1000.00 | 1200.00 | 1800.00 |
All three read the same frame here (start through the very end of the partition), so each simply picks off a different position within it. Each row now carries the first (1000.00), third (1200.00), and last (1800.00) revenues of the whole series side by side.
WINDOW clause and reuse it across several functions, which avoids retyping the same
frame specification three times. It is convenient, but support is uneven and recent in places
(Oracle added it in Oracle Database 21c, SQL Server in SQL Server 2022) and Amazon Redshift does
not support it at all. Repeating the full specification inside each OVER (...), as
above, works identically on every engine in the comparison table later in this article.
A Practical Use Case: Comparing Against the Best
Once the first or last value of a group sits on every row, comparing each row against it is
ordinary arithmetic. A common request is "how far is each month from the best month in the
series?" Combine FIRST_VALUE() over a descending order with a subtraction:
| month | revenue | best_rev | gap_to_best |
|---|---|---|---|
| 2024-06 | 1800.00 | 1800.00 | 0.00 |
| 2024-04 | 1500.00 | 1800.00 | 300.00 |
| 2024-05 | 1500.00 | 1800.00 | 300.00 |
| 2024-02 | 1200.00 | 1800.00 | 600.00 |
| 2024-03 | 1200.00 | 1800.00 | 600.00 |
| 2024-01 | 1000.00 | 1800.00 | 800.00 |
Ordering by revenue DESC places the best month (June, 1800.00) at the top of the
frame, so FIRST_VALUE() reads 1800.00 onto every row, and gap_to_best
measures how far each month falls short of it. June's gap is zero because it is the best;
January trails by 800.00. Because FIRST_VALUE() reads the fixed start of the frame,
the default frame needs no extension here.
Note that this measures the gap to the best month of the whole series, not the best month
so far. A running "best so far" is a different question, and it is a job for
MAX() over a growing frame rather than FIRST_VALUE():
The distinction is worth internalising: FIRST_VALUE() returns whatever row happens to
sit at the start of the frame, so it only equals a maximum when you order the frame by that same
value. It is not a general substitute for MAX().
Skipping NULLs with IGNORE NULLS
The SQL standard defines an optional { RESPECT | IGNORE } NULLS modifier for these
functions, but support varies significantly by database, so check your engine's documentation
before using it. Where it exists, RESPECT NULLS is the default and treats a
NULL-valued row like any other: if the first row of the frame has a NULL in the target column,
FIRST_VALUE() returns NULL. IGNORE NULLS instead skips over NULL-valued
rows and returns the first (or last, or nth) non-null value it finds:
This is genuinely useful for "last known value" style queries, carrying the most recent non-null
reading forward over gaps in a series. The support picture is uneven: Oracle and Amazon Redshift
have long offered it; SQL Server added it for FIRST_VALUE() and
LAST_VALUE() in SQL Server 2022; PostgreSQL added it in version 19, and in earlier
versions these functions always behave as RESPECT NULLS; MySQL accepts only
RESPECT NULLS; and SQLite does not support the modifier. On engines without it, there is a portable pattern that produces
the same result, shown next.
A Portable Alternative: Carrying the Last Known Value
Because IGNORE NULLS is not universal, it is worth knowing a pattern that gives the
same "last known value" result on every engine. Consider a version of the monthly figures where
some months were never recorded:
monthly_sales_gaps
| month | revenue |
|---|---|
| 2024-01 | 1000.00 |
| 2024-02 | NULL |
| 2024-03 | NULL |
| 2024-04 | 1500.00 |
| 2024-05 | NULL |
| 2024-06 | 1800.00 |
The goal is to fill each gap with the most recent revenue that was actually recorded. The trick
takes two steps. First, a running COUNT(revenue): because COUNT() of a
column skips NULLs, the count only increases on rows that have a value, so every NULL row inherits
the count of the recorded row before it. That count works as a group label. Second, each group
contains exactly one non-null revenue, so MAX() over the group returns it:
| month | revenue | grp | last_known |
|---|---|---|---|
| 2024-01 | 1000.00 | 1 | 1000.00 |
| 2024-02 | NULL | 1 | 1000.00 |
| 2024-03 | NULL | 1 | 1000.00 |
| 2024-04 | 1500.00 | 2 | 1500.00 |
| 2024-05 | NULL | 2 | 1500.00 |
| 2024-06 | 1800.00 | 3 | 1800.00 |
January starts group 1. February and March have no revenue, so the running count stays at 1 and
they join January's group, where the only recorded value is 1000.00. April's value moves the count
to 2 and starts a new group, which May joins, and June starts group 3. The explicit
ROWS frame on the running count keeps the query portable to engines that require one.
One edge case: if a series begins with NULLs, those rows fall into group 0, which has no recorded
value, so their last_known stays NULL. There is simply nothing earlier to carry
forward.
On engines that support it, this is the same result as
LAST_VALUE(revenue) IGNORE NULLS OVER (ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).
The two-step version is longer, but it runs unchanged everywhere.
Common Pitfalls
-
Assuming LAST_VALUE() returns the partition's last row. By default it returns
the current row, because the default frame ends at
CURRENT ROW. Add an explicitROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGframe to get the true last value. -
Forgetting that NTH_VALUE() can return NULL. If the frame does not yet contain
n rows, the result is
NULL, not an error. With the default growing frame the first n − 1 rows will be NULL; extend the frame if you want the nth value on every row. -
Omitting ORDER BY inside OVER. Without an ordering, "first," "last," and "nth"
have no defined meaning. These functions need an
ORDER BYto be sensible. -
Ordering by a non-unique column and expecting a stable pick. When rows tie on
the
ORDER BYvalue, which one counts as "first," "last," or "nth" is not guaranteed, and under the defaultRANGEframe the frame itself extends through all the peers of the current row. Add a unique tiebreaker (such asid) and aROWSframe when the specific row matters. -
Assuming IGNORE NULLS is portable. Its availability and syntax differ across engines;
verify before relying on it, or use the portable
COUNT()/MAX()pattern instead.
Cross-Database Support
The three functions are widely available, but not uniformly. Compatibility details here refer to the current documentation for each engine at the time of writing:
| engine | FIRST_VALUE | LAST_VALUE | NTH_VALUE | IGNORE NULLS |
|---|---|---|---|---|
| PostgreSQL | yes | yes | yes | since 19 |
| MySQL 8.x | yes | yes | yes | no (RESPECT NULLS only) |
| SQL Server | since 2012 | since 2012 | no | since 2022 (FIRST/LAST) |
| SQLite | since 3.25 | since 3.25 | since 3.25 | no |
| Oracle | yes | yes | yes | yes |
| Amazon Redshift | yes | yes | yes | yes |
The most notable gap is that SQL Server does not implement NTH_VALUE();
there you emulate it, for example by numbering rows with ROW_NUMBER() in a subquery
and filtering on the position you want. Default frames also deserve care: the
RANGE ... CURRENT ROW default described in this article applies to PostgreSQL, MySQL,
SQL Server, SQLite, and Oracle, but frame rules are exactly where dialects diverge, and some
engines, Amazon Redshift among them, are stricter about requiring an explicit frame. Spelling the
frame out is the portable choice.
Choosing Among the Three
The trio reads three fixed positions out of the frame, and the frame is the one thing that ties them together:
| function | reads | typical frame concern |
|---|---|---|
| FIRST_VALUE() | first row of the frame | usually straightforward: the default frame's start is fixed |
| LAST_VALUE() | last row of the frame | for the partition's last value, end the frame at UNBOUNDED FOLLOWING |
| NTH_VALUE() | nth row of the frame | NULL until the frame reaches n rows; extend the frame for a stable value |
Decide what you mean, then write the frame that says it. These functions are not quirky; they are simply honest about the frame they read.
Main References
- PostgreSQL Global Development Group — PostgreSQL Documentation: Window Functions — postgresql.org/docs/current/functions-window.html
- PostgreSQL Global Development Group — PostgreSQL Documentation: Window Function Calls, Frame Clause — postgresql.org/docs/current/sql-expressions.html
- Oracle Corporation — MySQL 8.4 Reference Manual: Window Function Descriptions — dev.mysql.com/doc/refman/8.4/en/window-function-descriptions.html
- Microsoft — FIRST_VALUE (Transact-SQL), SQL Server Documentation — learn.microsoft.com/en-us/sql/t-sql/functions/first-value-transact-sql
- SQLite Consortium — SQLite Documentation: Window Functions — sqlite.org/windowfunctions.html
- Oracle Corporation — Oracle Database SQL Language Reference: Analytic Functions — docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Analytic-Functions.html
- Amazon Web Services — Amazon Redshift Developer Guide: Window functions — docs.aws.amazon.com/redshift/latest/dg/c_Window_functions.html