SQL LAG and LEAD Functions
LAG reads a value from the previous row and LEAD from the next row, in an order you choose, without a self join. They answer “how much did revenue change since last month?”, “how many days passed between a customer’s orders?” and “who was hired right after this person?”.
- Read the previous row with
LAGand the next withLEAD - Calculate changes and growth between rows
- Work per group with
PARTITION BY - Use offsets, defaults,
FIRST_VALUEandLAST_VALUE
Syntax
LAG(expression [, offset [, default]]) OVER ([PARTITION BY ...] ORDER BY ...)LEAD(expression [, offset [, default]]) OVER ([PARTITION BY ...] ORDER BY ...)offset: how many rows back or forward (default 1).default: the value when there is no such row (default NULL).
Sample data
| order_id | customer_id | order_date | status |
|---|---|---|---|
| 1001 | 1 | 2025-01-05 | Delivered |
| 1002 | 2 | 2025-01-17 | Delivered |
| 1003 | 1 | 2025-02-02 | Delivered |
| 1004 | 3 | 2025-02-14 | Cancelled |
| 1005 | 4 | 2025-03-08 | Delivered |
| 1006 | 5 | 2025-03-22 | Shipped |
| 1007 | 2 | 2025-04-10 | Delivered |
| 1008 | 1 | 2025-04-28 | Pending |
| 1009 | 3 | 2025-05-15 | Delivered |
| 1010 | 5 | 2025-06-01 | Delivered |
Previous and next order
SELECT order_id, order_date, LAG(order_date) OVER (ORDER BY order_date) AS previous_order, LEAD(order_date) OVER (ORDER BY order_date) AS next_orderFROM ordersORDER BY order_date;| order_id | order_date | previous_order | next_order |
|---|---|---|---|
| 1001 | 2025-01-05 | NULL | 2025-01-17 |
| 1002 | 2025-01-17 | 2025-01-05 | 2025-02-02 |
| 1003 | 2025-02-02 | 2025-01-17 | 2025-02-14 |
| 1004 | 2025-02-14 | 2025-02-02 | 2025-03-08 |
| 1005 | 2025-03-08 | 2025-02-14 | 2025-03-22 |
| 1006 | 2025-03-22 | 2025-03-08 | 2025-04-10 |
| 1007 | 2025-04-10 | 2025-03-22 | 2025-04-28 |
| 1008 | 2025-04-28 | 2025-04-10 | 2025-05-15 |
| 1009 | 2025-05-15 | 2025-04-28 | 2025-06-01 |
| 1010 | 2025-06-01 | 2025-05-15 | NULL |
OVER (ORDER BY order_date)puts the orders in date order.LAG(order_date)takes the date from the row just before; the first row has none, so it is NULL.LEAD(order_date)takes it from the row just after; the last row gets NULL.
Days between orders
SELECT order_id, order_date, DATEDIFF(order_date, LAG(order_date) OVER (ORDER BY order_date)) AS days_since_previousFROM ordersORDER BY order_date;| order_id | order_date | days_since_previous |
|---|---|---|
| 1001 | 2025-01-05 | NULL |
| 1002 | 2025-01-17 | 12 |
| 1003 | 2025-02-02 | 16 |
| 1004 | 2025-02-14 | 12 |
| 1005 | 2025-03-08 | 22 |
| 1006 | 2025-03-22 | 14 |
| 1007 | 2025-04-10 | 19 |
| 1008 | 2025-04-28 | 18 |
| 1009 | 2025-05-15 | 17 |
| 1010 | 2025-06-01 | 17 |
Per customer: PARTITION BY
Days between each customer’s own orders:
SELECT customer_id, order_id, order_date, LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order, DATEDIFF(order_date, LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date)) AS days_gapFROM ordersORDER BY customer_id, order_date;| customer_id | order_id | order_date | prev_order | days_gap |
|---|---|---|---|---|
| 1 | 1001 | 2025-01-05 | NULL | NULL |
| 1 | 1003 | 2025-02-02 | 2025-01-05 | 28 |
| 1 | 1008 | 2025-04-28 | 2025-02-02 | 85 |
| 2 | 1002 | 2025-01-17 | NULL | NULL |
| 2 | 1007 | 2025-04-10 | 2025-01-17 | 83 |
| 3 | 1004 | 2025-02-14 | NULL | NULL |
| 3 | 1009 | 2025-05-15 | 2025-02-14 | 90 |
| 4 | 1005 | 2025-03-08 | NULL | NULL |
| 5 | 1006 | 2025-03-22 | NULL | NULL |
| 5 | 1010 | 2025-06-01 | 2025-03-22 | 71 |
PARTITION BY customer_idmakes each customer’s orders a separate sequence.- The first order of every customer has no previous order: NULL.
- Customer 1 waited 28 days between their first two orders, then 85 days.
Month-over-month revenue change
First total revenue per month in a CTE, then compare each month with the previous one:
WITH monthly AS ( SELECT DATE_FORMAT(o.order_date, '%Y-%m') AS month, SUM(oi.quantity * p.price) AS revenue FROM orders o JOIN order_items oi ON oi.order_id = o.order_id JOIN products p ON p.product_id = oi.product_id WHERE o.status <> 'Cancelled' GROUP BY month)SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_revenue, revenue - LAG(revenue) OVER (ORDER BY month) AS change_amt, ROUND(100 * (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 1) AS change_pctFROM monthlyORDER BY month;| month | revenue | prev_revenue | change_amt | change_pct |
|---|---|---|---|---|
| 2025-01 | 13743 | NULL | NULL | NULL |
| 2025-02 | 4298 | 13743 | -9445 | -68.7 |
| 2025-03 | 8783 | 4298 | 4485 | 104.4 |
| 2025-04 | 21795 | 8783 | 13012 | 148.1 |
| 2025-05 | 3499 | 21795 | -18296 | -83.9 |
| 2025-06 | 1145 | 3499 | -2354 | -67.3 |
- The CTE gives one row per month with its revenue.
LAG(revenue)brings the previous month’s revenue onto each row.- The difference and the percentage are ordinary arithmetic on those two columns.
- January has no previous month, so its change is NULL.
Offsets and defaults
SELECT first_name, hire_date, LAG(first_name, 2, '-') OVER (ORDER BY hire_date) AS hired_two_before, LEAD(first_name, 1, '-') OVER (ORDER BY hire_date) AS hired_nextFROM employeesORDER BY hire_date;| first_name | hire_date | hired_two_before | hired_next |
|---|---|---|---|
| Meera | 2015-01-05 | – | Vikram |
| Vikram | 2017-11-20 | – | Asha |
| Asha | 2018-04-12 | Meera | Sneha |
| Sneha | 2019-02-25 | Vikram | Rahul |
| Rahul | 2019-07-01 | Asha | Kavya |
| Kavya | 2020-09-09 | Sneha | Priya |
| Priya | 2021-01-18 | Rahul | Neha |
| Neha | 2022-03-14 | Kavya | Karan |
| Karan | 2022-08-29 | Priya | Arjun |
| Arjun | 2023-06-05 | Neha | Rohan |
| Rohan | 2024-01-22 | Karan | Amit |
| Amit | 2025-06-02 | Arjun | – |
LAG(first_name, 2, '-')looks two rows back; for the first two rows there is no such row, so the default'-'is shown.LEAD(first_name, 1, '-')looks one row forward, with'-'for the last row.
FIRST_VALUE and LAST_VALUE
The first hire of each department next to every employee:
SELECT dept_id, first_name, hire_date, FIRST_VALUE(first_name) OVER (PARTITION BY dept_id ORDER BY hire_date) AS first_hire, LAST_VALUE(first_name) OVER (PARTITION BY dept_id ORDER BY hire_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS latest_hireFROM employeesWHERE dept_id IN (1, 2)ORDER BY dept_id, hire_date;| dept_id | first_name | hire_date | first_hire | latest_hire |
|---|---|---|---|---|
| 1 | Asha | 2018-04-12 | Asha | Amit |
| 1 | Rahul | 2019-07-01 | Asha | Amit |
| 1 | Priya | 2021-01-18 | Asha | Amit |
| 1 | Karan | 2022-08-29 | Asha | Amit |
| 1 | Amit | 2025-06-02 | Asha | Amit |
| 2 | Vikram | 2017-11-20 | Vikram | Arjun |
| 2 | Neha | 2022-03-14 | Vikram | Arjun |
| 2 | Arjun | 2023-06-05 | Vikram | Arjun |
With ORDER BY in the window, the default frame ends at the current row, so a plain LAST_VALUE(...) just returns the current row’s value. Add ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to see the whole partition.
Exercise: Salary step to the next person
Order employees by salary (lowest first) and show, for each, the next higher salary and the difference.
| first_name | salary | next_salary | step |
|---|---|---|---|
| Amit | 25000 | 45000 | 20000 |
| Rohan | 45000 | 48000 | 3000 |
| Arjun | 48000 | 54000 | 6000 |
| Neha | 54000 | 72000 | 18000 |
| Priya | 72000 | 72000 | 0 |
| Karan | 72000 | 78000 | 6000 |
| Sneha | 78000 | 88000 | 10000 |
| Kavya | 88000 | 98000 | 10000 |
| Rahul | 98000 | 105000 | 7000 |
| Vikram | 105000 | 125000 | 20000 |
| Asha | 125000 | 250000 | 125000 |
| Meera | 250000 | NULL | NULL |
Hint 1
LEAD(salary) OVER (ORDER BY salary).
SELECT first_name, salary, LEAD(salary) OVER (ORDER BY salary) AS next_salary, LEAD(salary) OVER (ORDER BY salary) - salary AS stepFROM employeesORDER BY salary;| first_name | salary | next_salary | step |
|---|---|---|---|
| Amit | 25000 | 45000 | 20000 |
| Rohan | 45000 | 48000 | 3000 |
| Arjun | 48000 | 54000 | 6000 |
| Neha | 54000 | 72000 | 18000 |
| Priya | 72000 | 72000 | 0 |
| Karan | 72000 | 78000 | 6000 |
| Sneha | 78000 | 88000 | 10000 |
| Kavya | 88000 | 98000 | 10000 |
| Rahul | 98000 | 105000 | 7000 |
| Vikram | 105000 | 125000 | 20000 |
| Asha | 125000 | 250000 | 125000 |
| Meera | 250000 | NULL | NULL |
Quick quiz
1. What does LAG(x) return for the first row of a partition?
There is no previous row.
2. Which function reads the next row?
LEAD looks forward; LAG looks back.
3. Why does LAST_VALUE often return the current row’s value?
Extend the frame to UNBOUNDED FOLLOWING.
LAG and LEAD FAQ
What does the LAG function do in SQL?
LAG(column) returns the value of the column from the previous row in the window’s order, letting you compare a row with the one before it.
How do I calculate month-over-month growth in SQL?
Aggregate by month, then use LAG(revenue) OVER (ORDER BY month) to get the previous month and compute (revenue - prev) / prev * 100.
How do I replace NULL in LAG?
Pass a default as the third argument: LAG(revenue, 1, 0) OVER (...).