All 40 lessons

Lesson 28 of 40 · Window Functions

SQL LAG and LEAD

By Skillancy Editorial Team

Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.

LAG reads a value from the previous row and LEAD reads one from the next row, within a window you define. They make "compared with last time" questions easy: month-over-month growth, days since a customer's previous order, or the change in a price.

Syntax

LAG(column, offset, default)  OVER (PARTITION BY ... ORDER BY ...)
LEAD(column, offset, default) OVER (PARTITION BY ... ORDER BY ...)
  • offset (optional, default 1) is how many rows to look back or ahead.
  • default (optional) replaces NULL when there's no such row.

Examples

1. Previous and next order dates

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_order
FROM orders
ORDER BY order_date
LIMIT 5;
Try it in the SQL Compiler

Result:

order_idorder_dateprevious_ordernext_order
10012025-01-05NULL2025-01-18
10022025-01-182025-01-052025-02-02
10032025-02-022025-01-182025-02-20
10042025-02-202025-02-022025-03-11
10052025-03-112025-02-202025-03-29

(5 rows)

The first row has no previous order, so LAG returns NULL.

2. Days between a customer's orders

Add PARTITION BY customer_id so the comparison stays within each customer:

SELECT customer_id,
       order_id,
       order_date,
       LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order_date,
       order_date - LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS days_since_prev
FROM orders
WHERE customer_id IN (1, 3)
ORDER BY customer_id, order_date;
Try it in the SQL Compiler

Result:

customer_idorder_idorder_dateprev_order_datedays_since_prev
110012025-01-05NULLNULL
110042025-02-202025-01-0546
110112025-06-302025-02-20130
110192025-11-152025-06-30138
310032025-02-02NULLNULL
310092025-05-212025-02-02108
310212025-12-202025-05-21213

(7 rows)

3. Average repeat-purchase gap

Wrap the previous query in a CTE to summarise it:

WITH gaps AS (
  SELECT customer_id,
         order_date - LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS days_since_prev
  FROM orders
)
SELECT ROUND(AVG(days_since_prev), 1) AS avg_days_between_orders,
       COUNT(days_since_prev) AS repeat_orders
FROM gaps;
Try it in the SQL Compiler

Result:

avg_days_between_ordersrepeat_orders
162.713

(1 row)

4. Month-over-month growth

The classic analyst question. Aggregate by month first, then compare each month with the one before:

WITH monthly AS (
  SELECT DATE_TRUNC('month', o.order_date)::date AS month,
         SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100)) AS revenue
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.order_id
  WHERE o.status = 'Delivered'
    AND o.order_date < '2025-08-01'
  GROUP BY 1
)
SELECT month,
       ROUND(revenue, 0) AS revenue,
       ROUND(LAG(revenue) OVER (ORDER BY month), 0) AS prev_month,
       ROUND((revenue - LAG(revenue) OVER (ORDER BY month))
             / LAG(revenue) OVER (ORDER BY month) * 100, 1) AS mom_growth_pct
FROM monthly
ORDER BY month;
Try it in the SQL Compiler

Result:

monthrevenueprev_monthmom_growth_pct
2025-01-0132271NULLNULL
2025-02-012883932271-10.6
2025-03-01349228839-87.9
2025-04-0129883492-14.4
2025-05-013685298823.3
2025-06-01107463685191.6

(6 rows)

ShopKart is small, so growth jumps around. On real data this query feeds a growth chart directly.

5. A default value instead of NULL

SELECT order_id,
       order_date,
       LAG(order_id, 1, 0) OVER (ORDER BY order_date) AS previous_order_id
FROM orders
ORDER BY order_date
LIMIT 3;
Try it in the SQL Compiler

Result:

order_idorder_dateprevious_order_id
10012025-01-050
10022025-01-181001
10032025-02-021002

(3 rows)

6. FIRST_VALUE: compare with the first row

FIRST_VALUE returns the first value in the window. Each customer's first order date next to every order:

SELECT customer_id,
       order_id,
       order_date,
       FIRST_VALUE(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS first_order_date
FROM orders
WHERE customer_id IN (1, 4)
ORDER BY customer_id, order_date;
Try it in the SQL Compiler

Result:

customer_idorder_idorder_datefirst_order_date
110012025-01-052025-01-05
110042025-02-202025-01-05
110112025-06-302025-01-05
110192025-11-152025-01-05
410052025-03-112025-03-11
410132025-08-022025-03-11
410232026-01-262025-03-11

(7 rows)

LAST_VALUE needs a frame clause to work as expected; that's covered in the next lesson.

Common mistakes

  • Missing PARTITION BY. Without it, customer A's first order is compared with customer B's last order.
  • Gaps in the data. LAG looks at the previous row, not the previous month. If a month has no orders, it compares with two months ago. Build a complete month list first (see CROSS JOIN).
  • Dividing by a zero previous value. Wrap the denominator in NULLIF(..., 0).

Practice question

For each product, list its order lines in date order with the quantity ordered on the previous line for the same product.

Show solution
SELECT oi.product_id,
       o.order_date,
       oi.quantity,
       LAG(oi.quantity) OVER (PARTITION BY oi.product_id ORDER BY o.order_date) AS prev_quantity
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
WHERE oi.product_id IN (101, 102)
ORDER BY oi.product_id, o.order_date;
Try it in the SQL Compiler

Result:

product_idorder_datequantityprev_quantity
1012025-01-051NULL
1012025-07-1411
1012025-10-2811
1022025-01-052NULL
1022025-05-0312
1022025-10-2811
1022025-12-2021

(7 rows)

Key takeaways

  • LAG looks back and LEAD looks ahead within an ordered window.
  • PARTITION BY keeps comparisons inside each customer, product or group.
  • Month-over-month growth = aggregate by month, then LAG.
  • FIRST_VALUE returns the first value in the window.
Want every variation on one page?Window functions in SQL: the complete guide

Frequently asked questions

What does LAG do in SQL?

LAG(column) returns the value of that column from the previous row in the window's order. LEAD returns the value from the next row. Both return NULL when there is no such row.

How do I calculate month-over-month growth in SQL?

Aggregate to one row per month in a CTE, then compute (value - LAG(value) OVER (ORDER BY month)) / LAG(value) OVER (ORDER BY month).

Can LAG look back more than one row?

Yes. LAG(column, 2) looks two rows back. A third argument sets a default instead of NULL, for example LAG(revenue, 1, 0).

Newsletter

Keep learning SQL

One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Prefer a one-page summary? Download the free SQL cheat sheet (PDF)