Как посчитать WoW в SQL
Содержание:
Зачем WoW
Менеджер: «У нас вчера упала выручка на 15% — что делать?». Аналитик смотрит WoW: вчерашняя пятница vs прошлая пятница — рост +8%. День просто «слабый» в принципе. Падение было нормальным dip от среды к пятнице, а WoW убирает сезонность дня недели.
WoW (week-over-week) — рабочая метрика для продуктов с недельной сезонностью: e-com, доставка, контент. Помогает увидеть тренд без шума weekend/weekday. В статье — SQL через LAG и нюансы (рваные недели, праздники).
Что такое WoW
WoW (Week-over-Week) — изменение метрики vs аналогичный период неделей раньше.
WoW growth (%) = (current_week_value / previous_week_value - 1) × 100%
WoW abs = current_week_value - previous_week_valueМожно считать в двух режимах:
- WoW по неделям: сравнение totals недель (W vs W-1)
- WoW по дням: сравнение каждого дня с тем же днём недели неделей раньше (Mon vs Mon, Tue vs Tue)
Второй вариант учитывает сезонность дня недели и обычно полезнее.
Базовый расчёт
WoW по неделям
Данные: daily_revenue(day, revenue).
WITH weekly AS (
SELECT
DATE_TRUNC('week', day) AS week,
SUM(revenue) AS revenue
FROM daily_revenue
WHERE day >= CURRENT_DATE - INTERVAL '12 weeks'
GROUP BY 1
)
SELECT
week,
revenue,
LAG(revenue) OVER (ORDER BY week) AS prev_week_revenue,
revenue - LAG(revenue) OVER (ORDER BY week) AS wow_abs,
(revenue::NUMERIC / NULLIF(LAG(revenue) OVER (ORDER BY week), 0) - 1) * 100 AS wow_pct
FROM weekly
ORDER BY week;Важно: DATE_TRUNC('week', ...) начинает неделю с понедельника в Postgres. Если нужна неделя с воскресенья — рассчитайте смещение.
WoW по дням
SELECT
day,
revenue,
LAG(revenue, 7) OVER (ORDER BY day) AS same_day_prev_week,
revenue - LAG(revenue, 7) OVER (ORDER BY day) AS wow_abs,
(revenue::NUMERIC / NULLIF(LAG(revenue, 7) OVER (ORDER BY day), 0) - 1) * 100 AS wow_pct
FROM daily_revenue
WHERE day >= CURRENT_DATE - INTERVAL '90 days'
ORDER BY day;LAG(revenue, 7) — значение 7 строк назад в order by day. Сравнивает Mon vs prev Mon без агрегации.
WoW по сегментам
«Какие категории растут быстрее»:
WITH weekly_cat AS (
SELECT
DATE_TRUNC('week', created_at) AS week,
category,
SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
AND created_at >= CURRENT_DATE - INTERVAL '4 weeks'
GROUP BY 1, 2
)
SELECT
week,
category,
revenue,
(revenue::NUMERIC / NULLIF(
LAG(revenue) OVER (PARTITION BY category ORDER BY week), 0
) - 1) * 100 AS wow_pct
FROM weekly_cat
ORDER BY category, week;PARTITION BY category — LAG считается отдельно для каждой категории.
WoW vs MoM vs YoY
| Метрика | Сравнение | Когда использовать |
|---|---|---|
| WoW | Текущая неделя vs предыдущая | Высокочастотные продукты, weekly tracking |
| MoM | Текущий месяц vs предыдущий | Месячные циклы, payments, subscriptions |
| YoY | Текущий период vs тот же годом раньше | Сезонные бизнесы (праздники, школа, лето) |
YoY автоматически смывает сезонность — Q4 этого года vs Q4 прошлого года. В сезонных бизнесах (туризм, ритейл) MoM или WoW могут вводить в заблуждение.
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue,
LAG(SUM(total), 1) OVER (ORDER BY DATE_TRUNC('month', created_at)) AS prev_month,
LAG(SUM(total), 12) OVER (ORDER BY DATE_TRUNC('month', created_at)) AS prev_year,
(SUM(total)::NUMERIC
/ NULLIF(LAG(SUM(total), 1) OVER (ORDER BY DATE_TRUNC('month', created_at)), 0) - 1) * 100 AS mom_pct,
(SUM(total)::NUMERIC
/ NULLIF(LAG(SUM(total), 12) OVER (ORDER BY DATE_TRUNC('month', created_at)), 0) - 1) * 100 AS yoy_pct
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;WoW с сглаживанием
WoW бывает шумным. Сглаживание через 4-week rolling смягчает разовые провалы:
WITH weekly AS (
SELECT
DATE_TRUNC('week', created_at) AS week,
SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
)
SELECT
week,
revenue,
AVG(revenue) OVER (
ORDER BY week
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
) AS revenue_4w_avg,
(AVG(revenue) OVER (ORDER BY week ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)
/ NULLIF(AVG(revenue) OVER (ORDER BY week ROWS BETWEEN 7 PRECEDING AND 4 PRECEDING), 0) - 1) * 100 AS smoothed_4w_growth
FROM weekly
ORDER BY week;Сравнение 4-week avg с предыдущими 4-week avg — устойчивее, чем raw WoW.
Частые ошибки
Ошибка 1. Сравнивать неполную неделю
Сегодня среда. «WoW: эта неделя vs прошлая» — но эта неделя ещё не закончилась. Сравнивайте только полные недели или Mon-сегодня vs Mon-(сегодня-7).
Ошибка 2. Праздники как обычные дни
23 февраля или 8 марта изменили поведение. WoW резко прыгает. Решение: либо отметить holiday в данных, либо использовать YoY вместо WoW.
Ошибка 3. Integer division
revenue / prev_revenue для integer — округление до целого. Используйте ::NUMERIC или 1.0 *.
Ошибка 4. NULL при первой неделе
Первая запись в LAG возвращает NULL, и WoW=NULL — это корректно. Не filler-те нулями, отображайте как «нет данных».
Ошибка 5. DATE_TRUNC сдвигается по часовому поясу
Если данные хранятся в UTC, а компания в МСК — неделя начинается в воскресенье 21:00 UTC. Сначала приведите к нужной timezone: DATE_TRUNC('week', created_at AT TIME ZONE 'Europe/Moscow').
Ошибка 6. WoW от очень маленьких чисел
База 5 → 15 = +200%. Звучит впечатляюще, но абсолютно — это +10. Всегда показывайте и pct, и абсолютные значения.
Связанные темы
- Как посчитать MRR в SQL
- Как посчитать накопительный итог в SQL
- LAG и LEAD в SQL
- Оконные функции в SQL — шпаргалка
FAQ
WoW или WoW% — что показывать?
Обе. Абсолютные — про реальные числа. Процентные — про темп. Только % обманчиво при маленькой базе (5 → 15 = +200%).
Как считать WoW для метрик с порогом нуля?
Если в одной неделе 0 — WoW не определён. Используйте absolute change или WoW with baseline (например, среднее за 4 недели).
WoW vs 7-day rolling — в чём разница?
WoW — точечное сравнение. 7-day rolling avg — сглаживание шума. Часто используют вместе: WoW на rolling avg вместо raw value.
Можно ли WoW на даже более коротких периодах (HoH)?
«Hour-over-hour» или «Day-over-day» — да, но шум огромный. Для трендов лучше 7DMA или WoW.
Как объяснить менеджменту, почему WoW колеблется?
Декомпозиция факторов: трафик, конверсия, AOV, отдельные сегменты. Один shock в одной размерности маскирует тренды в других.