Skip to content

Intermediate: combining and changing data · Window functions · topic 35 of 62 · 35 min

LAG, LEAD, FIRST_VALUE and LAST_VALUE

Explanation

LAG(x) reads x from the previous row and LEAD(x) from the next row, in the order you give inside OVER. They turn "change since yesterday" and "days since the last order" into simple subtraction. FIRST_VALUE and LAST_VALUE read the first and last row of the frame, and LAST_VALUE needs a full frame to work as you expect.

Looking at the neighbour row

Many questions compare a row with the one before it: how much did sales change since yesterday, how many days passed since a customer's last order, did the stock price go up or down. Without window functions you would need a self-join on "the previous date", which is awkward. LAG and LEAD reach into the neighbouring row directly.

  • LAG(expr) returns expr from the previous row in the window order. On the first row there is no previous row, so it returns NULL.
  • LEAD(expr) returns expr from the next row. On the last row it returns NULL.
  • Both take two optional arguments: LAG(expr, offset, default). LAG(revenue, 7) looks 7 rows back; LAG(revenue, 1, 0) returns 0 instead of NULL on the first row.

Day-over-day change and growth

sql
SELECT sale_date, revenue,
       LAG(revenue) OVER (ORDER BY sale_date)           AS prev_day,
       revenue - LAG(revenue) OVER (ORDER BY sale_date) AS change
FROM daily_sales
WHERE store = 'Pune'
ORDER BY sale_date;
-- 2025-03-01 | 12000 | NULL  | NULL
-- 2025-03-02 | 15000 | 12000 | 3000
-- 2025-03-03 | 9000  | 15000 | -6000
-- 2025-03-04 | 18000 | 9000  | 9000
-- 2025-03-05 | 14000 | 18000 | -4000
-- 2025-03-06 | 21000 | 14000 | 7000
The first day has nothing to compare with, so the change is NULL.

Growth in percent is the change divided by the previous value, times 100. If the previous value could be 0, wrap it in NULLIF(prev, 0) so you get NULL instead of a division error.

sql
ROUND(100.0 * (revenue - LAG(revenue) OVER w) / NULLIF(LAG(revenue) OVER w, 0), 1) AS growth_pct
-- 2025-03-02: 25    (12000 → 15000)
-- 2025-03-03: -40   (15000 → 9000)
-- 2025-03-04: 100   (9000 → 18000)
w is a named window, explained below.

Per customer: days between orders

Add PARTITION BY customer_id and LAG only looks back within the same customer. With SQLite's julianday(), which turns a date into a day number, you can subtract dates.

sql
SELECT customer_id, id, order_date,
       LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order,
       julianday(order_date)
         - julianday(LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date)) AS gap_days
FROM orders
ORDER BY customer_id, order_date;
-- 1 | 101 | 2025-01-05 | NULL       | NULL
-- 1 | 103 | 2025-02-14 | 2025-01-05 | 40.0
-- 1 | 108 | 2025-04-02 | 2025-02-14 | 47.0
-- 2 | 102 | 2025-01-09 | NULL       | NULL
-- 2 | 106 | 2025-03-15 | 2025-01-09 | 65.0
-- ...
julianday returns a real number; wrap the difference in CAST(... AS INTEGER) for whole days. PostgreSQL subtracts dates directly: order_date - prev_order.

FIRST_VALUE and LAST_VALUE

FIRST_VALUE(expr) returns expr from the first row of the frame; LAST_VALUE(expr) from the last row of the frame. FIRST_VALUE(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) puts each customer's first order date on every one of their orders, handy for "days since the customer joined us".

The LAST_VALUE trap

Remember the default frame from topic 3: from the start of the partition up to the current row. So the "last row of the frame" is the current row itself, and LAST_VALUE just returns the current value. To get the true last value of the partition, widen the frame to the end.

sql
SELECT sale_date, revenue,
       LAST_VALUE(revenue) OVER (ORDER BY sale_date) AS wrong_last,
       LAST_VALUE(revenue) OVER (ORDER BY sale_date
         ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS real_last
FROM daily_sales WHERE store = 'Pune' ORDER BY sale_date;
-- 2025-03-01 | 12000 | 12000 | 21000
-- 2025-03-02 | 15000 | 15000 | 21000
-- ...
-- 2025-03-06 | 21000 | 21000 | 21000
wrong_last just copies revenue. An easier fix: FIRST_VALUE with ORDER BY ... DESC.

The WINDOW clause

When several functions share the same OVER, name it once with a WINDOW clause after WHERE / GROUP BY / HAVING and before ORDER BY, then write OVER w. It works in SQLite, PostgreSQL, MySQL 8 and SQL Server 2022.

sql
SELECT sale_date, revenue,
       LAG(revenue)  OVER w AS prev_day,
       LEAD(revenue) OVER w AS next_day
FROM daily_sales
WHERE store = 'Pune'
WINDOW w AS (ORDER BY sale_date)
ORDER BY sale_date;

Common mistakes

  • Forgetting PARTITION BY, so LAG takes the previous row of a different customer or store.
  • Forgetting that the first row gives NULL. Use the third argument (a default) or handle NULL in the report.
  • Using LAST_VALUE with the default frame and getting the current row. Widen the frame or use FIRST_VALUE with DESC.
  • Dividing by a previous value of 0. Use NULLIF(previous, 0).
  • Assuming LAG(x, 1) means "yesterday" when days can be missing. It means the previous row.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

LAG, LEAD, FIRST_VALUE and LAST_VALUE · 1/7

LAG and LEAD

Many questions compare a row with its neighbour: the change since yesterday, or the days since the last order.

Prefer a video by a teacher? Find free videos on “sql lag lead first_value last_value window function tutorial” on YouTube ↗

In real life: Spotting customers who are slipping away

Food delivery and e-commerce teams often watch the gap between a customer's orders. If someone who ordered every week has not ordered for a month, they may get a reminder or an offer. The gap is a LAG over each customer's orders, and the "days since first order" uses FIRST_VALUE. The same pattern shows month-on-month change in a bank statement or a UPI spending summary.

sql
SELECT customer_id, id, order_date,
       FIRST_VALUE(order_date) OVER w AS first_order,
       LAG(order_date)         OVER w AS prev_order
FROM orders
WINDOW w AS (PARTITION BY customer_id ORDER BY order_date)
ORDER BY customer_id, order_date;
Customer 1: first_order 2025-01-05 on all three orders; prev_order NULL, 2025-01-05, 2025-02-14.

Practice (0/4 done)

Task 1 · Code · SQL

For every order, show the date of the same customer's previous order. Return customer_id, id, order_date and the previous order date named prev_order (NULL for a customer's first order), sorted by customer_id, then order_date.

Task 2 · Code · SQL

For the Mumbai store, return sale_date, revenue and the change from the previous day named change (revenue minus the previous day's revenue; NULL on the first day), sorted by sale_date.

Task 3 · Code · SQL

Put each customer's first order date next to every one of their orders. Return customer_id, id, order_date and first_order, sorted by customer_id, then order_date.

Task 4 · Code · SQL

How many days passed between each order and the same customer's previous order? Return customer_id, id and gap_days as a whole number (CAST of the julianday difference AS INTEGER; NULL for a first order). Only return orders of customers 1 and 2, sorted by customer_id, then order_date.

Quick quiz

Question 1

1.What does LAG(revenue) return on the first row of its window?

Question 2

2.Why does LAST_VALUE(revenue) OVER (ORDER BY sale_date) just return each row's own revenue?

Question 3

3.You compute days between orders with LAG(order_date) OVER (ORDER BY order_date), without PARTITION BY. What goes wrong?

Question 4

4.Which expression gives the growth percentage from the previous row safely, even if the previous value is 0?

Question 5

5.What is the WINDOW clause for?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does LAG(revenue) return on the first row of its window?

Answer: NULL, unless a default is given as the third argument

There is no previous row. LAG(revenue, 1, 0) would give 0 instead.

Why does LAST_VALUE(revenue) OVER (ORDER BY sale_date) just return each row's own revenue?

Answer: The default frame ends at the current row, so the current row is the last one in the frame

Widen the frame to UNBOUNDED FOLLOWING, or use FIRST_VALUE with ORDER BY ... DESC.

You compute days between orders with LAG(order_date) OVER (ORDER BY order_date), without PARTITION BY. What goes wrong?

Answer: Each customer's first order gets compared with another customer's order

Partition by customer_id so the previous row is always the same customer.

Which expression gives the growth percentage from the previous row safely, even if the previous value is 0?

Answer: 100.0 * (revenue - LAG(revenue) OVER w) / NULLIF(LAG(revenue) OVER w, 0)

Change divided by previous value, with NULLIF turning a zero divisor into NULL.

What is the WINDOW clause for?

Answer: Naming a window once so several functions can say OVER w

It saves repeating the same PARTITION BY and ORDER BY.

Which frame gives a 3-day moving average when there is one row per day?Related

Answer: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

Two rows before plus the current row is three rows.

From Running totals, moving averages and frames

Which window function reads a value from the previous row?Related

Answer: LAG

LAG looks back; LEAD looks forward.

From Classic interview queries, part 2

employees has 11 rows. How many rows does SELECT name, AVG(salary) OVER (PARTITION BY dept_id) FROM employees return?Related

Answer: 11

A window function never removes rows; each of the 11 rows gets its department average.

From OVER and PARTITION BY

orders JOIN order_items: what does one row of the result stand for?Related

Answer: One order line

The grain becomes the more detailed table, order_items.

From Joining three or more tables

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Window functions” before its exam: Top-N per group and de-duplication.
  2. Module 7 exam: Window functions · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Changing data, transactions and ACID. 62 topics left in the course before the final exam.

← Previous: Running totals, moving averages and frames