Короткий ответ. 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 и агрегации по периодам — базовый навык аналитика, который спрашивают почти на каждом собеседовании.
- Отработать
LAG,date_truncи расчёт темпов роста на настоящем PostgreSQL прямо в браузере: SQL-тренажёр. - Посчитать те же метрики на pandas через
pct_changeиresample: Python-тренажёр. - Собрать динамику ключевых показателей бизнеса: карточки выручки и MRR с формулами и разбором.
- Тестовые задания «посчитай YoY по когортам» и «построй QoQ-воронку»: раздел заданий.
- Глубже про оконные функции — гайд «Оконные функции SQL: полный разбор».
Если готовишься к собеседованию на аналитика системно, в тренажёре открыты 425 SQL- и 402 Python-задачи, кейсы и разбор метрик — тарифа Pro хватает, чтобы дойти от SELECT до оконных функций и продуктовой аналитики без пробелов. Начать можно с бесплатных задач и решить первую динамику уже сегодня.