🔥 0
0
Lesson 4 of 6 20 min +200 XP

Value Functions: LAG, LEAD, FIRST_VALUE

Need to compare this month's sales to last month? Show the difference from a row's value to the first in its group? Value functions let you access data from other rows without self-joins.

LAG: Look Backward

LAG accesses data from a previous row.

LAG(column, offset, default) OVER (ORDER BY ...)
-- offset: how many rows back (default: 1)
-- default: value if no previous row (default: NULL)

Month-Over-Month Comparison

SELECT
  month,
  revenue,
  LAG(revenue) OVER (ORDER BY month) as prev_month,
  revenue - LAG(revenue) OVER (ORDER BY month) as change
FROM monthly_sales;
month revenue prev_month change
2024-01 50000 NULL NULL
2024-02 55000 50000 +5000
2024-03 48000 55000 -7000
2024-04 62000 48000 +14000

Year-Over-Year (Looking Back 12 Rows)

SELECT
  month,
  revenue,
  LAG(revenue, 12) OVER (ORDER BY month) as same_month_last_year,
  ROUND(
    100.0 * (revenue - LAG(revenue, 12) OVER (ORDER BY month))
    / LAG(revenue, 12) OVER (ORDER BY month),
    1
  ) as yoy_growth_pct
FROM monthly_sales;
month revenue same_month_last_year yoy_growth_pct
2024-01 55000 50000 +10.0%
2024-02 58000 52000 +11.5%
2024-03 51000 55000 -7.3%

LEAD: Look Forward

LEAD accesses data from a future row - the opposite of LAG.

LEAD(column, offset, default) OVER (ORDER BY ...)

Time Until Next Event

SELECT
  customer_id,
  order_date,
  LEAD(order_date) OVER (
    PARTITION BY customer_id
    ORDER BY order_date
  ) as next_order,
  LEAD(order_date) OVER (
    PARTITION BY customer_id
    ORDER BY order_date
  ) - order_date as days_until_next
FROM orders;
customer_id order_date next_order days_until_next
C001 2024-01-15 2024-02-02 18 days
C001 2024-02-02 2024-03-20 47 days
C001 2024-03-20 NULL NULL
C002 2024-01-20 2024-01-28 8 days

Using Default Values

-- Use 0 instead of NULL when no previous value exists
SELECT
  month,
  revenue,
  LAG(revenue, 1, 0) OVER (ORDER BY month) as prev_revenue
FROM monthly_sales;

FIRST_VALUE and LAST_VALUE

Access the first or last value in a partition.

Compare to First Value

SELECT
  employee,
  department,
  hire_date,
  salary,
  FIRST_VALUE(salary) OVER (
    PARTITION BY department
    ORDER BY hire_date
  ) as first_hire_salary,
  salary - FIRST_VALUE(salary) OVER (
    PARTITION BY department
    ORDER BY hire_date
  ) as salary_vs_first
FROM employees;
employee department hire_date salary first_hire_salary salary_vs_first
Alice Engineering 2020-01 80000 80000 0
Bob Engineering 2021-06 95000 80000 +15000
Carol Engineering 2023-03 110000 80000 +30000

LAST_VALUE Gotcha

Warning: LAST_VALUE Default Behavior

By default, LAST_VALUE only sees up to the current row (because of the default frame). To see the true last value, you must specify the frame explicitly.

-- Wrong: returns current row's value (default frame)
LAST_VALUE(salary) OVER (PARTITION BY dept ORDER BY hire_date)

-- Correct: sees all rows in partition
LAST_VALUE(salary) OVER (
  PARTITION BY dept
  ORDER BY hire_date
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
)

Comparing to Min/Max

SELECT
  product,
  category,
  price,
  -- Lowest price in category (first when sorted by price)
  FIRST_VALUE(price) OVER (
    PARTITION BY category
    ORDER BY price
  ) as min_price,
  -- Highest price (last in price order, need full frame)
  LAST_VALUE(price) OVER (
    PARTITION BY category
    ORDER BY price
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) as max_price,
  -- How this product compares
  ROUND(100.0 * price / FIRST_VALUE(price) OVER (
    PARTITION BY category ORDER BY price
  ), 0) as pct_of_min
FROM products;
product category price min_price max_price pct_of_min
Basic Chair Furniture $50 $50 $500 100%
Office Desk Furniture $200 $50 $500 400%
Executive Desk Furniture $500 $50 $500 1000%

Real-World: Session Analysis

-- Calculate time spent on each page
SELECT
  user_id,
  page,
  timestamp,
  LEAD(timestamp) OVER (
    PARTITION BY user_id
    ORDER BY timestamp
  ) - timestamp as time_on_page
FROM page_views;

Real-World: Gap Analysis

-- Find days with no orders
SELECT
  order_date,
  LEAD(order_date) OVER (ORDER BY order_date) as next_order_date,
  LEAD(order_date) OVER (ORDER BY order_date) - order_date - 1 as gap_days
FROM (
  SELECT DISTINCT order_date FROM orders
) dates
WHERE LEAD(order_date) OVER (ORDER BY order_date) - order_date > 1;

Key Takeaways

  • LAG looks backward - Previous row values, period comparisons
  • LEAD looks forward - Next row values, time-to-event
  • FIRST_VALUE/LAST_VALUE - Compare to extremes in partition
  • LAST_VALUE needs explicit frame - Otherwise only sees up to current row

Next up: Window Frames - Fine-tune exactly which rows your window function sees.

🧠 Quick Quiz

Test your understanding of this lesson.

1

LAG(revenue, 1) OVER (ORDER BY date) returns:

2

What does LEAD(price, 2, 0) return when there are fewer than 2 rows ahead?

3

To get the first order date for each customer, you would use:

Aggregate Window Functions