LAG и LEAD в SQL — доступ к соседним строкам

Коротко

LAG и LEAD — оконные функции, которые дают доступ к значению из предыдущей или следующей строки без self-join. LAG смотрит назад, LEAD — вперёд. Это основной инструмент для расчёта прироста между периодами, разницы между событиями и построения цепочек действий пользователя. На собеседованиях аналитиков LAG/LEAD встречается почти в каждой задаче на оконные функции.

Синтаксис

LAG(column, OFFSET, default) OVER (
    PARTITION BY partition_column
    ORDER BY order_column
)

LEAD(column, OFFSET, default) OVER (
    PARTITION BY partition_column
    ORDER BY order_column
)
  • column — столбец, значение которого нужно получить
  • offset — на сколько строк сдвинуться (по умолчанию 1)
  • default — значение, если соседней строки нет (по умолчанию NULL)
  • PARTITION BY — разбивка на группы (необязательно)
  • ORDER BY — порядок строк внутри окна (обязательно)
SELECT
    order_date,
    revenue,
    LAG(revenue) OVER (ORDER BY order_date)  AS prev_revenue,
    LEAD(revenue) OVER (ORDER BY order_date) AS next_revenue
FROM daily_revenue
order_date revenue prev_revenue next_revenue
2025-01-01 50000 NULL 62000
2025-01-02 62000 50000 48000
2025-01-03 48000 62000 71000
2025-01-04 71000 48000 NULL

Первая строка не имеет предыдущей — LAG вернул NULL. Последняя не имеет следующей — LEAD вернул NULL.

Offset и default

По умолчанию offset = 1 (одна строка). Можно указать другое значение — например, для сравнения с позапрошлым периодом.

SELECT
    month,
    revenue,
    LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
    LAG(revenue, 3) OVER (ORDER BY month) AS three_months_ago,
    LAG(revenue, 1, 0) OVER (ORDER BY month) AS prev_or_zero
FROM monthly_revenue

LAG(revenue, 3) — значение три строки назад. LAG(revenue, 1, 0) — если предыдущей строки нет, вернёт 0 вместо NULL. Default полезен, когда NULL ломает дальнейшие вычисления.

PARTITION BY — окна по группам

Без PARTITION BY окно — вся таблица. С PARTITION BY — отдельное окно для каждой группы.

SELECT
    user_id,
    order_date,
    amount,
    LAG(order_date) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS prev_order_date,
    order_date - LAG(order_date) OVER (
        PARTITION BY user_id
        ORDER BY order_date
    ) AS days_between_orders
FROM orders
user_id order_date amount prev_order_date days_between
1 2025-01-05 3000 NULL NULL
1 2025-01-12 1500 2025-01-05 7
1 2025-02-03 4200 2025-01-12 22
2 2025-01-08 800 NULL NULL
2 2025-01-20 2100 2025-01-08 12

PARTITION BY user_id создаёт отдельное окно для каждого пользователя. Первый заказ каждого пользователя не имеет предыдущего — LAG возвращает NULL. Так можно считать интервалы между покупками, сессиями, визитами.

Практические примеры

Прирост выручки день к дню

SELECT
    order_date,
    revenue,
    LAG(revenue) OVER (ORDER BY order_date) AS prev_revenue,
    ROUND(
        (revenue - LAG(revenue) OVER (ORDER BY order_date))::numeric
        / LAG(revenue) OVER (ORDER BY order_date) * 100, 1
    ) AS growth_pct
FROM daily_revenue

Классическая задача: рассчитать процент прироста выручки. Формула: (текущее - предыдущее) / предыдущее * 100. LAG даёт предыдущее значение без self-join.

Сравнение с прошлым месяцем (year-over-year)

SELECT
    month,
    revenue,
    LAG(revenue, 12) OVER (ORDER BY month) AS same_month_last_year,
    ROUND(
        (revenue - LAG(revenue, 12) OVER (ORDER BY month))::numeric
        / LAG(revenue, 12) OVER (ORDER BY month) * 100, 1
    ) AS yoy_growth_pct
FROM monthly_revenue

LAG(revenue, 12) — значение 12 строк назад. Если данные помесячные — это тот же месяц прошлого года. Удобно для year-over-year сравнений без JOIN.

Время между действиями пользователя

SELECT
    user_id,
    event_type,
    event_time,
    LAG(event_time) OVER (
        PARTITION BY user_id ORDER BY event_time
    ) AS prev_event_time,
    EXTRACT(EPOCH FROM (
        event_time - LAG(event_time) OVER (
            PARTITION BY user_id ORDER BY event_time
        )
    )) / 60 AS minutes_since_prev
FROM user_events
WHERE event_date = '2025-03-15'

Считает время в минутах между последовательными действиями пользователя. Полезно для анализа сессий, воронок и пользовательских путей.

Цепочка действий: предыдущий и следующий шаг

SELECT
    user_id,
    event_time,
    event_type,
    LAG(event_type) OVER (
        PARTITION BY user_id ORDER BY event_time
    ) AS prev_action,
    LEAD(event_type) OVER (
        PARTITION BY user_id ORDER BY event_time
    ) AS next_action
FROM user_events

Показывает, что пользователь делал до и после каждого события. Полезно для анализа поведения: «что делают пользователи после добавления в корзину?», «куда уходят после отказа?».

Первая и последняя покупка пользователя

SELECT DISTINCT
    user_id,
    FIRST_VALUE(amount) OVER w AS first_order_amount,
    LAST_VALUE(amount) OVER (
        w ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS last_order_amount,
    LAST_VALUE(amount) OVER (
        w ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) - FIRST_VALUE(amount) OVER w AS amount_change
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY order_date)

Хотя FIRST_VALUE/LAST_VALUE — не LAG/LEAD, они часто идут в связке. Для доступа к произвольной строке используйте NTH_VALUE. Подробнее — в гайде по оконным функциям.

LAG/LEAD vs self-join

До оконных функций ту же задачу решали через self-join:

-- Self-join (старый способ)
SELECT
    a.order_date,
    a.revenue,
    b.revenue AS prev_revenue
FROM daily_revenue a
LEFT JOIN daily_revenue b
    ON b.order_date = a.order_date - INTERVAL '1 day'

-- LAG (современный способ)
SELECT
    order_date,
    revenue,
    LAG(revenue) OVER (ORDER BY order_date) AS prev_revenue
FROM daily_revenue

LAG читабельнее, не дублирует таблицу, работает быстрее на больших объёмах. Self-join всё ещё нужен, если связь между строками не последовательная (например, join по конкретной дате, а не по порядку).

Типичные ошибки

Забыли ORDER BY. LAG/LEAD без ORDER BY — неопределённый порядок строк. Результат будет непредсказуемым. ORDER BY внутри OVER обязателен.

Не учли NULL. Если в столбце есть NULL, LAG вернёт NULL как значение, и его легко спутать с «нет предыдущей строки». Указывайте default или обрабатывайте через COALESCE.

Путают LAG и LEAD. LAG — назад, LEAD — вперёд. Мнемоника: LAG = отставание (прошлое), LEAD = опережение (будущее).

Не добавили PARTITION BY. Если данные содержат несколько сущностей (пользователей, продуктов), без PARTITION BY LAG будет брать значение от предыдущей строки другой сущности.

Вопросы с собеседований

-- Чем отличается LAG от LEAD? -- LAG возвращает значение из предыдущей строки (по ORDER BY), LEAD — из следующей. LAG(x, 1) эквивалентен LEAD(x, -1), если бы отрицательный offset был допустим (он не допустим в стандартном SQL).

-- Как рассчитать прирост выручки за месяц? -- revenue - LAG(revenue) OVER (ORDER BY month) — абсолютный прирост. Делим на LAG(revenue) и умножаем на 100 — процентный прирост. PARTITION BY не нужен, если данные по одной сущности.

-- Что вернёт LAG для первой строки? -- NULL, если не указан default. Если указан LAG(col, 1, 0) — вернёт 0. Первая строка в окне не имеет предыдущей.

-- Можно ли использовать LAG в WHERE? -- Нельзя напрямую. Оконные функции вычисляются на этапе SELECT, после WHERE. Для фильтрации по LAG нужен подзапрос или CTE: вычислить LAG во внутреннем запросе, отфильтровать во внешнем.

-- Напишите запрос: найти пользователей, у которых сумма второго заказа больше первого. -- Решение через LAG: WITH ordered AS (SELECT user_id, amount, LAG(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS prev_amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS rn FROM orders) SELECT user_id FROM ordered WHERE rn = 2 AND amount > prev_amount.


Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.

FAQ

LAG и LEAD работают во всех базах данных?

Да. LAG и LEAD — часть стандарта SQL:2003 и поддерживаются в PostgreSQL, MySQL 8+, ClickHouse, BigQuery, Redshift, Snowflake, MS SQL Server, Oracle. В MySQL 5.x оконных функций нет — нужен self-join.

Можно ли использовать LAG с агрегатными функциями?

Да. LAG(SUM(amount)) OVER (ORDER BY month) — сначала агрегация, потом оконная функция. Агрегатные и оконные функции вычисляются на разных этапах: GROUP BY → HAVING → SELECT (оконные функции).

Как получить значение не из соседней, а из произвольной строки?

Используйте NTH_VALUE(column, n) для n-й строки в окне или LAG/LEAD с offset > 1. Для первой строки — FIRST_VALUE, для последней — LAST_VALUE с правильным frame. Подробнее — в шпаргалке по оконным функциям.

Как тренироваться

LAG/LEAD — must-have для аналитика. Задачи на прирост, цепочки событий и сравнение периодов — в тренажёре Карьерник. Больше вопросов по всем темам — в разделе с примерами.