LAG и LEAD в SQL: шпаргалка

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Что делают LAG и LEAD

  • LAG(col) — значение предыдущей строки
  • LEAD(col) — значение следующей строки

Оконные функции, «смотрящие» вперёд или назад в окне.

Синтаксис

LAG(column [, OFFSET [, default]]) OVER (
    PARTITION BY group_col
    ORDER BY sort_col
)

LEAD(column [, OFFSET [, default]]) OVER (...)

Параметры:

  • column — значение для «взгляда»
  • offset — на сколько строк смотреть (по умолчанию 1)
  • default — что вернуть, если строки нет (NULL по умолчанию)

1. Разница между соседними записями

Доход по дням и дельта к предыдущему дню:

SELECT
    day,
    revenue,
    revenue - LAG(revenue) OVER (ORDER BY day) AS delta_vs_prev
FROM daily_revenue;

2. % рост (growth rate)

SELECT
    day,
    revenue,
    (revenue - LAG(revenue) OVER (ORDER BY day)) * 100.0
        / LAG(revenue) OVER (ORDER BY day) AS growth_pct
FROM daily_revenue;

3. Предыдущий заказ пользователя

SELECT
    user_id,
    order_id,
    created_at,
    LAG(created_at) OVER (
        PARTITION BY user_id ORDER BY created_at
    ) AS prev_order_at
FROM orders;

4. Время между заказами

SELECT
    user_id,
    order_id,
    created_at,
    EXTRACT(EPOCH FROM created_at
        - LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at)
    ) / 86400 AS days_since_prev_order
FROM orders;

5. Следующий заказ (LEAD)

SELECT
    user_id,
    order_id,
    created_at,
    LEAD(created_at) OVER (
        PARTITION BY user_id ORDER BY created_at
    ) AS next_order_at
FROM orders;

6. Сравнение с N дней назад

-- LAG со смещением 7 — неделя назад
SELECT
    day,
    revenue,
    LAG(revenue, 7) OVER (ORDER BY day) AS revenue_week_ago,
    revenue - LAG(revenue, 7) OVER (ORDER BY day) AS wow_diff
FROM daily_revenue;

7. Default-значение вместо NULL

Для первой строки LAG() возвращает NULL. Можно указать default:

LAG(revenue, 1, 0) OVER (ORDER BY day)

Первая строка получит 0, не NULL.

8. Определение начала сессии

Если между событиями > 30 минут — новая сессия.

WITH events_marked AS (
    SELECT
        user_id,
        event_at,
        CASE
            WHEN LAG(event_at) OVER (PARTITION BY user_id ORDER BY event_at)
                 < event_at - INTERVAL '30 minutes'
              OR LAG(event_at) OVER (PARTITION BY user_id ORDER BY event_at) IS NULL
            THEN 1 ELSE 0
        END AS is_new_session
    FROM events
)
SELECT
    user_id,
    event_at,
    SUM(is_new_session) OVER (
        PARTITION BY user_id ORDER BY event_at
    ) AS session_id
FROM events_marked;

9. Первый и последний заказ через LAG/LEAD

SELECT
    user_id,
    order_id,
    CASE
        WHEN LAG(order_id) OVER (PARTITION BY user_id ORDER BY created_at) IS NULL
        THEN 'first'
        WHEN LEAD(order_id) OVER (PARTITION BY user_id ORDER BY created_at) IS NULL
        THEN 'last'
        ELSE 'middle'
    END AS order_position
FROM orders;

10. Fill forward (заполнить пропуски последним значением)

-- если нет значения в строке, взять предыдущее
SELECT
    day,
    COALESCE(
        value,
        LAG(value) IGNORE NULLS OVER (ORDER BY day)
    ) AS filled_value
FROM measurements;

(IGNORE NULLS доступно в BigQuery, Snowflake; в Postgres — обходной путь через first_value).

Частые ошибки

Ошибка 1. Забыли PARTITION BY

-- считает предыдущий заказ по ВСЕМ пользователям вперемешку
LAG(created_at) OVER (ORDER BY created_at)

-- правильно
LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at)

Ошибка 2. LAG без ORDER BY

Без ORDER BY результат недетерминирован — порядок строк непредсказуем.

Ошибка 3. Деление на ноль в growth_pct

-- если revenue = 0 в предыдущем дне — деление на ноль
(revenue - LAG(revenue) ...) / LAG(revenue) ...

-- безопаснее через NULLIF
(revenue - LAG(revenue) ...) / NULLIF(LAG(revenue) ..., 0)

Ошибка 4. LAG для первой строки

Первая строка не имеет предыдущей — LAG возвращает NULL. Учитывайте:

COALESCE(LAG(revenue) OVER (...), 0)

LAG vs FIRST_VALUE / LAST_VALUE

Разные цели:

  • LAG — предыдущая строка
  • FIRST_VALUE — первая в окне
  • LAST_VALUE — последняя в окне
-- первый и последний заказ пользователя в одной строке
SELECT DISTINCT
    user_id,
    FIRST_VALUE(created_at) OVER (
        PARTITION BY user_id ORDER BY created_at
    ) AS first_order,
    LAST_VALUE(created_at) OVER (
        PARTITION BY user_id ORDER BY created_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS last_order
FROM orders;

Производительность

  • LAG/LEAD — встроенные функции, быстрые
  • PARTITION BY на большой группе — может быть тяжело
  • Индекс на (partition_col, order_col) сильно ускоряет

Связанные темы

FAQ

Чем LAG отличается от LEAD?

LAG — предыдущая строка (смотрим назад). LEAD — следующая (смотрим вперёд). Остальной синтаксис идентичен.

Можно ли LAG в агрегате?

LAG — оконная функция, работает без GROUP BY. В агрегате обычной функцией — нет.

Работает ли LAG в MySQL?

Да, с MySQL 8.0+. В более старых версиях оконных функций нет, используйте self-join или переменные.

LAG на первой строке возвращает NULL. Как задать default?

Третий аргумент: LAG(col, 1, 0) — для первой строки вернётся 0.

Можно ли LAG с NULL в ORDER BY?

Да, но будьте аккуратны с сортировкой NULL — используйте NULLS FIRST / LAST.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.