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_revenueLAG(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_revenueLAG(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_revenueLAG читабельнее, не дублирует таблицу, работает быстрее на больших объёмах. 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 для аналитика. Задачи на прирост, цепочки событий и сравнение периодов — в тренажёре Карьерник. Больше вопросов по всем темам — в разделе с примерами.