SQL (PostgreSQL, MySQL, SQLite) Module 8 – Window functions
LAG, LEAD, FIRST_VALUE and NTH_VALUE
Compare each row with the one before or after it using LAG and LEAD, and read the first or nth row of a window with FIRST_VALUE and NTH_VALUE.
What you will learn
- Compare a row with the previous or next rows of its window using LAG and LEAD
- Retrieve the first, last and nth value of a window frame
- Avoid the LAST_VALUE default-frame trap
- Compute gaps in days and month-over-month growth that stay right when rows are missing
Before you start
What you’ll learn · Before you start
What you will learn
- Compare a row with the previous or next rows of its window using LAG and LEAD
- Retrieve the first, last and nth value of a window frame
- Avoid the LAST_VALUE default-frame trap
- Compute gaps in days and month-over-month growth that stay right when rows are missing
Before you start
On this page
LAG and LEAD are window functions that read a value from another row of the same window. lag(revenue) returns the
revenue of the row before the current one, and lead(revenue) that of the row after it, in the order set by the
window’s ORDER BY. FIRST_VALUE and NTH_VALUE read a value from a fixed position instead: the first row of the window
frame, or its nth row. With them a query can set each month next to the month before, count the days since a
customer’s previous order or show the product each customer bought first, all without joining the table to itself.
They work in PostgreSQL, in MySQL 8.0 and later and in SQLite 3.25 and later, and the examples run in your browser on
SQLite 3.49.1.
Looking at the row before and the row after
Here is a shop’s revenue for six months, each month next to the one before it and the one after it:
-- A shop's revenue by month, in rupees.
CREATE TABLE monthly (month TEXT PRIMARY KEY, revenue INTEGER NOT NULL);
INSERT INTO monthly VALUES
('2026-01', 52000), ('2026-02', 61000), ('2026-03', 58000),
('2026-04', 70000), ('2026-05', 66500), ('2026-06', 79800);
-- Each month next to the month before and the month after.
SELECT month, revenue,
lag(revenue) OVER (ORDER BY month) AS prev_month,
revenue - lag(revenue) OVER (ORDER BY month) AS change,
lead(revenue) OVER (ORDER BY month) AS next_month
FROM monthly
ORDER BY month; Output
┌─────────┬─────────┬────────────┬────────┬────────────┐ │ month │ revenue │ prev_month │ change │ next_month │ ├─────────┼─────────┼────────────┼────────┼────────────┤ │ 2026-01 │ 52000 │ │ │ 61000 │ │ 2026-02 │ 61000 │ 52000 │ 9000 │ 58000 │ │ 2026-03 │ 58000 │ 61000 │ -3000 │ 70000 │ │ 2026-04 │ 70000 │ 58000 │ 12000 │ 66500 │ │ 2026-05 │ 66500 │ 70000 │ -3500 │ 79800 │ │ 2026-06 │ 79800 │ 66500 │ 13300 │ │ └─────────┴─────────┴────────────┴────────┴────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < month_over_month.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
LAG found January’s 52000 for February, so February’s change is 9000, and LEAD found February’s 61000 for January.
The first month has no month before it, so its LAG is NULL and so is its change; the last month has no LEAD. Both
functions take two more arguments: lag(revenue, 3) reads three rows back, and a third argument replaces the NULL,
as in lag(revenue, 1, 0). Nothing has to be numbered or joined first: the window’s ORDER BY alone decides what
“before” means, so over ORDER BY month DESC the same LAG would read the following month. With a PARTITION BY,
every partition starts afresh, and the first row of each partition has no LAG of its own.
The rows that the value functions read on April's row
Text description of the diagram
The diagram lists the six months of the lesson's monthly revenue in order, with April as the current row, and labels each month with the function that reads it.
- January, 52000: FIRST_VALUE(revenue) reads the first row of the frame.
- February, 61000: NTH_VALUE(revenue, 2) reads the second row of the frame.
- March, 58000: LAG(revenue) reads the row just before the current one.
- April, 70000: the current row. With the default frame, which runs from the first row to the current row, LAST_VALUE(revenue) reads April itself.
- May, 66500: LEAD(revenue) reads the row just after the current one.
- June, 79800: LEAD(revenue, 2) reads the row two places after it.
January to April are drawn inside a box that marks the default frame on April's row. LAG and LEAD look at the rows of the whole partition, inside or outside the frame; FIRST_VALUE, NTH_VALUE and LAST_VALUE read only the frame.
The five functions of this lesson, one line each:
- LAG reads a value from a row before the current one: one row back, unless an offset says more.
- LEAD reads a value from a row after the current one, in the same way.
- FIRST_VALUE reads the value of the first row of the window frame.
- LAST_VALUE reads the last row of the frame; by default that is the current row or its last tie, not the partition’s last row.
- NTH_VALUE reads the nth row of the frame, and NULL while the frame has fewer than n rows.
LAG and LEAD always look at the whole partition, whatever the frame. FIRST_VALUE, LAST_VALUE and NTH_VALUE look only at the frame, which by default runs from the first row of the partition to the current row and its ties. That one difference explains most surprises with these functions.
Growth rates, offsets and defaults, with numbers Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Days between orders, and each customer’s first product
- When the previous row is not the previous month
- LAST_VALUE and a frame that stops at the current row
- Missing values: IGNORE NULLS and a portable fill
- The same functions in PostgreSQL, MySQL and SQLite
- Your turn: days since the previous login
- Days since each user's previous login, and long gaps
- Check yourself
- Interview questions
- What do LAG and LEAD return on the first and the last row of a partition, and how do you change that?
- A month-over-month revenue report uses LAG. The shop sold nothing in April and has no row for it, and the report shows May as 14.7 % up on the previous month. What is wrong, how do you fix it, and what should April and May show?
- For each user, flag the logins that come more than 7 days after the same user's previous login, then count the users who came back at least once after such a gap. Users with a single login must not be flagged, and a user may log in several times a day.
- LAST_VALUE(price) OVER (PARTITION BY product ORDER BY changed_on) returns each row's own price instead of the product's latest price. Why, and what are three ways to show the latest price on every row?
- Key takeaways
5 runnable examples (SQL) · 1 exercise with sample tests · 3 interview answers · about 22 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- PostgreSQL documentation: Window Functions (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Date/Time Functions and Operators (The PostgreSQL Global Development Group)
- PostgreSQL 19 release notes (draft): window functions with IGNORE NULLS (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: Window Function Descriptions (Oracle Corporation)
- MySQL 8.4 Reference Manual: Window Function Restrictions (Oracle Corporation)
- MySQL 8.4 Reference Manual: Date and Time Functions (DATEDIFF) (Oracle Corporation)
- SQLite: Window Functions (SQLite)
- SQLite: Date And Time Functions (SQLite)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress