LAG, LEAD, FIRST_VALUE & LAST_VALUE
Read a value from the previous, next, first or last row of the window — for match-over-match changes and comparisons without a self-join.
"Did Kohli score more than in his previous match?" To answer that you need two rows at once — this match and the last one. Before window functions this needed a self-join. Value functions simply reach into another row of the window and bring its value to the current row.
Definition
A value (offset) window function returns a value taken from a different row of the window, located relative to the current row (LAG / LEAD) or at the window's edges (FIRST_VALUE / LAST_VALUE). ORDER BY inside OVER is required, because "previous" and "first" only make sense in a defined order.
| Function | Returns the value from… |
|---|---|
LAG(expr, offset, default) | the row offset rows before the current row (previous) |
LEAD(expr, offset, default) | the row offset rows after the current row (next) |
FIRST_VALUE(expr) | the first row of the window frame |
LAST_VALUE(expr) | the last row of the window frame |
- offset — how many rows to move; optional, default 1.
- default — the value returned when that row does not exist (before the first / after the last row of the partition); optional, default NULL.
- Rows are never removed; partitions are respected — LAG never looks into another player's rows.
Syntax
sql
LAG(expression [, offset [, default]]) OVER ([PARTITION BY ...] ORDER BY ...)
LEAD(expression [, offset [, default]]) OVER ([PARTITION BY ...] ORDER BY ...)
FIRST_VALUE(expression) OVER ([PARTITION BY ...] ORDER BY ... [frame])
LAST_VALUE(expression) OVER ([PARTITION BY ...] ORDER BY ... [frame])LAG/LEAD do not allow a frame; FIRST_VALUE/LAST_VALUE do (and LAST_VALUE usually needs one — see below).
Example — LAG and LEAD: previous and next match
sql
SELECT id, player, match_date, runs,
LAG(runs) OVER (PARTITION BY player ORDER BY match_date) AS prev_runs,
LEAD(runs) OVER (PARTITION BY player ORDER BY match_date) AS next_runs
FROM innings;| id | player | match_date | runs | prev_runs | next_runs |
|---|---|---|---|---|---|
| 1 | Virat Kohli | 2025-03-22 | 82 | NULL | 59 |
| 2 | Virat Kohli | 2025-03-29 | 59 | 82 | 73 |
| 3 | Virat Kohli | 2025-04-05 | 73 | 59 | NULL |
| 4 | Rohit Sharma | 2025-03-23 | 45 | NULL | 76 |
| 5 | Rohit Sharma | 2025-03-30 | 76 | 45 | 0 |
| 6 | Rohit Sharma | 2025-04-06 | 0 | 76 | NULL |
| 7 | MS Dhoni | 2025-03-24 | 26 | NULL | NULL |
| 8 | MS Dhoni | 2025-03-31 | NULL | 26 | 45 |
| 9 | MS Dhoni | 2025-04-07 | 45 | NULL | NULL |
| 10 | Jos Buttler | 2025-03-25 | 73 | NULL | 59 |
| 11 | Jos Buttler | 2025-04-01 | 59 | 73 | 107 |
| 12 | Jos Buttler | 2025-04-08 | 107 | 59 | NULL |
Step by step: (1) partition by player, order by date; (2) LAG fetches the row before, LEAD the row after. The first match of each player has no previous row → NULL; the last match has no next row → NULL. Dhoni's rows 7 and 9 also show NULL — not because the row is missing, but because the neighbouring row's value (2025-03-31) is NULL.
Example — LAG with offset and default
sql
SELECT id, player, runs,
LAG(runs, 2, 0) OVER (PARTITION BY player ORDER BY match_date) AS two_back
FROM innings;| id | player | runs | two_back |
|---|---|---|---|
| 1 | Virat Kohli | 82 | 0 |
| 2 | Virat Kohli | 59 | 0 |
| 3 | Virat Kohli | 73 | 82 |
| 4 | Rohit Sharma | 45 | 0 |
| 5 | Rohit Sharma | 76 | 0 |
| 6 | Rohit Sharma | 0 | 45 |
| 7 | MS Dhoni | 26 | 0 |
| 8 | MS Dhoni | NULL | 0 |
| 9 | MS Dhoni | 45 | 26 |
| 10 | Jos Buttler | 73 | 0 |
| 11 | Jos Buttler | 59 | 0 |
| 12 | Jos Buttler | 107 | 73 |
Offset 2 = two rows back; default 0 replaces the NULL you would get when there is no row two back (the first two matches of each player).
Careful: the default is used only when the target row does not exist. If the row exists but its value is NULL, you still get NULL. E.g.
LAG(runs, 1, 0)on Dhoni's 2025-04-07 innings returns NULL (the previous row exists; its runs are NULL). UseCOALESCE(LAG(runs) OVER (...), 0)to replace both cases.
Example — match-over-match change
sql
SELECT id, player, runs,
runs - LAG(runs) OVER (PARTITION BY player ORDER BY match_date) AS change
FROM innings;| id | player | runs | change |
|---|---|---|---|
| 1 | Virat Kohli | 82 | NULL |
| 2 | Virat Kohli | 59 | -23 |
| 3 | Virat Kohli | 73 | 14 |
| 4 | Rohit Sharma | 45 | NULL |
| 5 | Rohit Sharma | 76 | 31 |
| 6 | Rohit Sharma | 0 | -76 |
| 7 | MS Dhoni | 26 | NULL |
| 8 | MS Dhoni | NULL | NULL |
| 9 | MS Dhoni | 45 | NULL |
| 10 | Jos Buttler | 73 | NULL |
| 11 | Jos Buttler | 59 | -14 |
| 12 | Jos Buttler | 107 | 48 |
Step by step: (1) LAG gives last match's runs; (2) subtract it from this row. Positive = better form, negative = worse. This is time-series analysis — the same pattern gives month-over-month sales growth, day-over-day price change, etc.
Example — FIRST_VALUE and LAST_VALUE
sql
SELECT id, player, runs,
FIRST_VALUE(runs) OVER (PARTITION BY player ORDER BY match_date) AS first_runs,
LAST_VALUE(runs) OVER (PARTITION BY player ORDER BY match_date) AS last_wrong,
LAST_VALUE(runs) OVER (
PARTITION BY player
ORDER BY match_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_runs
FROM innings;| id | player | runs | first_runs | last_wrong | last_runs |
|---|---|---|---|---|---|
| 1 | Virat Kohli | 82 | 82 | 82 | 73 |
| 2 | Virat Kohli | 59 | 82 | 59 | 73 |
| 3 | Virat Kohli | 73 | 82 | 73 | 73 |
| 4 | Rohit Sharma | 45 | 45 | 45 | 0 |
| 5 | Rohit Sharma | 76 | 45 | 76 | 0 |
| 6 | Rohit Sharma | 0 | 45 | 0 | 0 |
| 7 | MS Dhoni | 26 | 26 | 26 | 45 |
| 8 | MS Dhoni | NULL | 26 | NULL | 45 |
| 9 | MS Dhoni | 45 | 26 | 45 | 45 |
| 10 | Jos Buttler | 73 | 73 | 73 | 107 |
| 11 | Jos Buttler | 59 | 73 | 59 | 107 |
| 12 | Jos Buttler | 107 | 73 | 107 | 107 |
Why is last_wrong wrong? With ORDER BY and no frame, the default frame is "first row → current row". So the "last row of the frame" is the current row itself, and LAST_VALUE just returns the current runs. LAST_VALUE needs the full frame (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) to see the true last match. FIRST_VALUE works with the default frame because the frame always starts at the first row.
Tip: an easy alternative for the last value is
FIRST_VALUE(runs) OVER (PARTITION BY player ORDER BY match_date DESC)— reverse the order and take the first.
Common mistakes
- ❌ LAST_VALUE with the default frame → returns the current row ✅ add
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. - ❌ Forgetting PARTITION BY → LAG would take Rohit's previous value from Kohli's last match ✅ partition by the entity (player, customer…).
- ❌ Expecting the LAG default to replace a NULL value → ✅ it only covers a missing row; use COALESCE for NULL values.
- ❌ LAG without ORDER BY → ✅ ORDER BY is required.
SQL Server vs MySQL / PostgreSQL
All support LAG/LEAD/FIRST_VALUE/LAST_VALUE with the same syntax (SQL Server 2012+, MySQL 8+, PostgreSQL). SQL Server 2022 also added IGNORE NULLS for FIRST_VALUE/LAST_VALUE to skip NULL values.
Interview tip
Q: How do you calculate the change from the previous row without a self-join?
A: value - LAG(value) OVER (PARTITION BY entity ORDER BY date). LAG brings the previous row's value onto the current row.
Key points to remember
- Value functions read a value from another row: LAG (previous), LEAD (next), FIRST_VALUE (first in frame), LAST_VALUE (last in frame).
- ORDER BY in OVER is required for all value functions; LAG/LEAD do not allow a frame.
- LAG(expr, offset, default): offset defaults to 1, default defaults to NULL.
- The default only replaces a MISSING row — a NULL value in an existing row stays NULL.
- runs - LAG(runs) OVER (PARTITION BY player ORDER BY match_date) = match-over-match change.
- LAST_VALUE needs ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, otherwise it returns the current row.
- Always PARTITION BY the entity so LAG/LEAD never cross into another player's rows.
Try it yourself
Previous and next match (LAG / LEAD)
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Offset, default, and the NULL trap
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
FIRST_VALUE & LAST_VALUE (with the full frame)
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Practice: change vs next match
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Revision Flashcards
Tap a card to flip it and see the answer.
