Как сравнить два периода в SQL

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

Зачем это знать

«Выручка выросла» — вырасла по сравнению с чем? Без сравнения с прошлым периодом цифра бессмысленна. WoW (week-over-week), MoM (month-over-month), YoY (year-over-year) — базовые операции в любом BI-отчёте.

На собесах часто: «сравните MoM revenue» или «найди, где падение WoW > 10%». Типичный middle-вопрос.

Способ 1: LAG

SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS prev_month,
    revenue - LAG(revenue) OVER (ORDER BY month) AS mom_abs,
    (revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 /
      LAG(revenue) OVER (ORDER BY month) AS mom_pct
FROM monthly_revenue;

LAG даёт предыдущую строку. Простое MoM.

Способ 2: Self-join

SELECT
    t.month, t.revenue, p.revenue AS prev, t.revenue - p.revenue AS diff
FROM monthly_revenue t
LEFT JOIN monthly_revenue p ON p.month = t.month - INTERVAL '1 month';

Менее элегантно, но явно видно сравнение.

Способ 3: conditional aggregation

SELECT
    SUM(CASE WHEN month = '2026-03-01' THEN revenue END) AS mar,
    SUM(CASE WHEN month = '2026-04-01' THEN revenue END) AS apr
FROM monthly_revenue;

Для конкретных периодов — удобно.

YoY

LAG с 12 months:

SELECT month, revenue,
       LAG(revenue, 12) OVER (ORDER BY month) AS yoy_prev
FROM monthly_revenue;

Второй аргумент LAG — шаг назад.

WoW

Если data weekly:

SELECT week, metric, LAG(metric) OVER (ORDER BY week) AS prev
FROM weekly_stats;

Или daily → сначала aggregate по week:

WITH weekly AS (
    SELECT DATE_TRUNC('week', day) AS week, SUM(revenue) AS rev
    FROM daily_revenue GROUP BY 1
)
SELECT week, rev, LAG(rev) OVER (ORDER BY week) AS prev_week
FROM weekly;

% change

(CURRENT - previous) * 100.0 / NULLIF(previous, 0)

NULLIF(previous, 0) защищает от деления на 0.

Сравнение по группам

MoM по продуктам:

SELECT
    product,
    month,
    revenue,
    LAG(revenue) OVER (PARTITION BY product ORDER BY month) AS prev
FROM product_revenue;

PARTITION BY — отдельно для каждого продукта.

Период vs Период (две произвольные даты)

«March 1-15 vs April 1-15»:

WITH periods AS (
    SELECT
        SUM(CASE WHEN DATE(created_at) BETWEEN '2026-03-01' AND '2026-03-15' THEN total END) AS mar,
        SUM(CASE WHEN DATE(created_at) BETWEEN '2026-04-01' AND '2026-04-15' THEN total END) AS apr
    FROM orders
)
SELECT mar, apr, apr - mar AS diff, (apr - mar) * 100.0 / mar AS pct_change
FROM periods;

Seasonality considerations

Сравнение April vs March может врать из-за seasonality. Честное сравнение:

  • YoY (year-over-year) убирает seasonal effects
  • Сравнение с ожидаемым baseline, не с прошлым месяцем

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

Деление на 0

Если previous = 0 → division error. Используйте NULLIF.

Не учли partitioning

LAG без PARTITION BY смешает продукты/каналы/cohorts.

Неверные границы

BETWEEN '2026-04-01' AND '2026-04-30' — включит весь день 30-го? Да, но timestamp с временем — нет. Используйте >= '2026-04-01' AND < '2026-05-01'.

На собесе

«Найди месяцы, где MoM revenue упал больше чем на 20%».

WITH mom AS (
    SELECT month, revenue,
           LAG(revenue) OVER (ORDER BY month) AS prev,
           (revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 /
             LAG(revenue) OVER (ORDER BY month) AS pct
    FROM monthly_revenue
)
SELECT * FROM mom WHERE pct < -20;

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

FAQ

LAG или self-join?

LAG — чище и быстрее на одной таблице. Self-join — если нужно сравнивать с другой.

Как считать % для отрицательного baseline?

Формула та же, но интерпретировать осторожно. Лучше absolute diff.

YoY всегда лучше MoM?

Нет. YoY убирает seasonality, MoM — показывает momentum.


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