sqlметрикианалитикадинамикаоконные функции

YoY, QoQ, MoM, CAGR: формулы и SQL динамики

2026-07-20 8 мин

Короткий ответ. YoY, QoQ, MoM и CAGR — это способы измерить, насколько метрика выросла или упала за период. Все считаются по одной базовой формуле темпа роста: (текущий период − базовый период) / базовый период. Разница только в том, какой период берётся за базу: YoY (Year-over-Year) сравнивает с тем же периодом год назад, QoQ (Quarter-over-Quarter) — с прошлым кварталом, MoM (Month-over-Month) — с прошлым месяцем. CAGR (Compound Annual Growth Rate) стоит особняком: это среднегодовой темп за несколько лет с учётом сложного процента, формула (конец / начало)^(1/число лет) − 1.

Ниже — определение, формула словами и выражением, SQL на оконных функциях и типичные ошибки для каждой метрики. В конце — сводная таблица, когда какую применять.

Что такое YoY и как его считать?

YoY (Year-over-Year, «год к году») — изменение метрики относительно того же периода прошлого года. Это самый частый способ смотреть на бизнес, потому что он автоматически гасит сезонность: декабрь сравнивается с декабрём, а не с ноябрём.

Формула словами: разницу между текущим значением и значением год назад делим на значение год назад.

YoY = (Значение_текущий_год − Значение_прошлый_год) / Значение_прошлый_год × 100%

Пример: выручка за 2025 год — 15 000 000 ₽, за 2024 — 12 000 000 ₽.

YoY = (15 000 000 − 12 000 000) / 12 000 000 = 0.25 = +25%

В SQL для месячных данных «тот же месяц год назад» — это лаг на 12 строк. Используем LAG(...) OVER (ORDER BY month) с offset 12:

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

Важно: LAG(revenue, 12) работает корректно только если в таблице нет пропущенных месяцев. Если за какой-то месяц строки нет, 12-я строка назад окажется не «12 месяцев назад», а «12 имеющихся точек назад» — и вы сравните июнь 2025 с, например, апрелем 2024. Об этом ниже в ошибках.

Что такое MoM и когда он полезен?

MoM (Month-over-Month, «месяц к месяцу») — изменение относительно предыдущего месяца. Это самая чувствительная метрика: она первой ловит разворот тренда, всплеск после запуска фичи или провал после сбоя.

Формула словами: разницу между текущим месяцем и прошлым делим на прошлый.

MoM = (Значение_текущий_месяц − Значение_прошлый_месяц) / Значение_прошлый_месяц × 100%

Пример: март — 1 200 000 ₽, февраль — 1 000 000 ₽.

MoM = (1 200 000 − 1 000 000) / 1 000 000 = 0.20 = +20%

SQL — тот же LAG, но с offset 1:

SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS prev_month,
    ROUND(
        (revenue - LAG(revenue) OVER (ORDER BY month))
        / LAG(revenue) OVER (ORDER BY month)::numeric * 100,
    1) AS mom_pct
FROM monthly_revenue
ORDER BY month;

MoM хорош для быстрорастущих продуктов и оперативного мониторинга, но у него есть цена: в него зашита сезонность. Январь почти в любом B2C-бизнесе просядет к декабрю, и MoM покажет «−30%», хотя год к году всё в порядке. Поэтому MoM всегда читают в паре с YoY.

Что такое QoQ и чем он отличается от MoM?

QoQ (Quarter-over-Quarter, «квартал к кварталу») — изменение относительно предыдущего квартала. Это компромисс между чувствительностью MoM и стабильностью YoY: квартал достаточно длинный, чтобы сгладить месячный шум, но достаточно короткий, чтобы вовремя заметить тренд.

Формула словами: разницу между текущим кварталом и прошлым делим на прошлый.

QoQ = (Значение_текущий_квартал − Значение_прошлый_квартал) / Значение_прошлый_квартал × 100%

Пример: Q2 — 3 300 000 ₽, Q1 — 3 000 000 ₽.

QoQ = (3 300 000 − 3 000 000) / 3 000 000 = 0.10 = +10%

Если исходные данные месячные, кварталы сначала агрегируют через date_trunc('quarter', ...), а потом применяют LAG:

WITH quarterly AS (
    SELECT
        date_trunc('quarter', order_date) AS quarter,
        SUM(amount) AS revenue
    FROM orders
    GROUP BY date_trunc('quarter', order_date)
)
SELECT
    quarter,
    revenue,
    LAG(revenue) OVER (ORDER BY quarter) AS prev_quarter,
    ROUND(
        (revenue - LAG(revenue) OVER (ORDER BY quarter))
        / LAG(revenue) OVER (ORDER BY quarter)::numeric * 100,
    1) AS qoq_pct
FROM quarterly
ORDER BY quarter;

Обратите внимание: сначала CTE агрегирует по кварталам, и только потом окно LAG идёт по уже свёрнутым строкам. Если наложить LAG(revenue, 1) прямо на месячную таблицу, получится MoM, а не QoQ — частая путаница.

Что такое CAGR и почему нельзя просто взять среднее?

CAGR (Compound Annual Growth Rate) — среднегодовой темп роста за несколько лет с учётом сложного процента. Он отвечает на вопрос: «На сколько процентов в год метрика должна была стабильно расти, чтобы из начального значения получить конечное?»

Формула словами: корень степени «число лет» из отношения конечного значения к начальному, минус единица.

CAGR = (Значение_конец / Значение_начало)^(1 / число_лет) − 1

Пример: выручка выросла с 10 000 000 ₽ в 2021 году до 24 410 000 ₽ в 2025-м. Между началом и концом — 4 полных года (2021→2025).

CAGR = (24 410 000 / 10 000 000)^(1/4) − 1 = 2.441^0.25 − 1 ≈ 0.25 = 25%

Проверка: 10 млн × 1.25⁴ = 10 × 2.4414 ≈ 24.41 млн. Сходится.

Почему не среднее арифметическое годовых приростов? Потому что рост мультипликативен. Если метрика за год удвоилась (+100%), а на следующий упала вдвое (−50%), простое среднее даст +25%, хотя по факту вы вернулись в исходную точку — истинный CAGR равен 0%. Среднее арифметическое темпов роста систематически завышает результат.

Ключевой момент — правильно считать число лет. Это разница годов (или периодов), а не количество точек. Пять годовых значений (2021, 2022, 2023, 2024, 2025) дают 4 интервала, поэтому показатель степени 1/4, а не 1/5.

SQL: берём первое и последнее значение окна и число лет между ними.

WITH yearly AS (
    SELECT
        EXTRACT(YEAR FROM order_date)::int AS yr,
        SUM(amount) AS revenue
    FROM orders
    GROUP BY EXTRACT(YEAR FROM order_date)
),
bounds AS (
    SELECT
        FIRST_VALUE(revenue) OVER w AS first_rev,
        LAST_VALUE(revenue)  OVER w AS last_rev,
        MAX(yr) OVER () - MIN(yr) OVER () AS years
    FROM yearly
    WINDOW w AS (ORDER BY yr ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
)
SELECT DISTINCT
    ROUND((POWER(last_rev / first_rev::numeric, 1.0 / years) - 1) * 100, 1) AS cagr_pct
FROM bounds;

Здесь важна рамка окна ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING: без неё LAST_VALUE по умолчанию берёт текущую строку, а не реальный конец, и результат окажется неверным.

Какую метрику динамики когда применять?

МетрикаБаза сравненияГасит сезонностьЧувствительностьКогда применять
MoMПрошлый месяцНетВысокаяОперативный мониторинг, ранний сигнал, быстрорастущие продукты
QoQПрошлый кварталЧастичноСредняяКвартальные отчёты, сглаживание месячного шума
YoYТот же период год назадДаНизкаяСезонный бизнес, оценка «здорового» роста, инвесторам
CAGRНачало и конец за N летДаИтоговаяДолгосрочный тренд, сравнение сценариев, 3+ года истории

Практическое правило: MoM и QoQ отвечают «что происходит прямо сейчас», YoY — «растём ли мы на самом деле», CAGR — «какой у нас темп в долгую». В нормальном дашборде выручки стоят рядом минимум YoY и MoM — они дополняют друг друга.

Какие типичные ошибки в расчёте динамики?

1. Сравнение несравнимых периодов. Неполный месяц против полного: если сегодня 10-е число, выручка текущего месяца физически меньше прошлого — MoM покажет ложный провал. Сравнивайте только закрытые периоды или используйте MTD-к-MTD (month-to-date к тому же числу прошлого месяца).

2. Игнорирование сезонности в MoM/QoQ. Январский спад в ритейле — это норма, а не катастрофа. Если метрика сезонная, основной вывод делайте по YoY, а MoM держите как оперативный сигнал.

3. LAG(x, 12) на данных с пропусками. Оконная функция считает строки, а не календарные месяцы. Если в ряду нет строки за какой-то месяц, LAG(x, 12) уедет. Решение — сначала достроить непрерывный ряд дат через generate_series и LEFT JOIN, заполнив пропуски нулями, и только потом брать лаг.

4. Деление на ноль и на отрицательную базу. Если базовый период равен нулю, темп роста не определён. Если база отрицательная (например, прибыль ушла в минус), знак процента становится бессмысленным. Такие случаи обрабатывайте через NULLIF(base, 0) и отдельно комментируйте.

5. Среднее арифметическое вместо CAGR. Уже разобрали выше: для многолетнего роста усреднять годовые проценты нельзя, нужен геометрический подход через степень.

6. Проценты вместо абсолютов. «+200% MoM» звучит громко, но если база была 3 пользователя и стало 9 — это шум. Всегда показывайте абсолютную дельту рядом с процентом.

Как потренироваться на реальных задачах?

Формулы запоминаются, когда пишешь их руками на живых данных. Оконные LAG, date_trunc и агрегации по периодам — базовый навык аналитика, который спрашивают почти на каждом собеседовании.

Если готовишься к собеседованию на аналитика системно, в тренажёре открыты 425 SQL- и 402 Python-задачи, кейсы и разбор метрик — тарифа Pro хватает, чтобы дойти от SELECT до оконных функций и продуктовой аналитики без пробелов. Начать можно с бесплатных задач и решить первую динамику уже сегодня.

Потренируй SQL с оконными функциями
Оконные функции, date_trunc, реальные задачи с автопроверкой. 5 задач без регистрации.
Открыть SQL-тренажёр →