Как посчитать ROAS в SQL
Содержание:
Зачем ROAS
Маркетинг каждую неделю отчитывается: «потратили 1 млн на рекламу, принесли 4 млн revenue». Звучит хорошо. Но ROAS 4 для нового бренда — это хорошо, а для зрелого e-commerce с маржой 15% — провал (margin от 4 млн = 600 тыс., минус 1 млн на трафик = -400 тыс. убыток).
Аналитика просят: «ROAS по каналам», «ROAS Performance Max vs Search», «ROAS на 7-day vs 30-day window». В статье — SQL и нюансы (атрибуция, return-window, ROAS ≠ ROI).
Формула ROAS
ROAS = Revenue from ads / Ad spendROAS = 4 значит «1 рубль рекламы вернул 4 рубля выручки». ROAS = 1 — точка break-even (по выручке, не по марже).
Часто ROAS выражают коэффициентом (4.0) или процентом (400%). Это одно и то же.
Базовый расчёт
Данные: ad_spend(date, channel, spend), attributed_revenue(date, channel, revenue).
WITH spend AS (
SELECT channel, SUM(spend) AS total_spend
FROM ad_spend
WHERE DATE >= '2026-04-01' AND DATE < '2026-05-01'
GROUP BY channel
),
revenue AS (
SELECT channel, SUM(revenue) AS total_revenue
FROM attributed_revenue
WHERE DATE >= '2026-04-01' AND DATE < '2026-05-01'
GROUP BY channel
)
SELECT
s.channel,
s.total_spend,
r.total_revenue,
r.total_revenue::NUMERIC / NULLIF(s.total_spend, 0) AS roas
FROM spend s
JOIN revenue r ON r.channel = s.channel
ORDER BY roas DESC;Важно: агрегировать каждую сторону отдельно, потом join. JOIN до агрегации может умножить строки.
ROAS по каналам и кампаниям
WITH spend AS (
SELECT channel, campaign, SUM(spend) AS spend_amt
FROM ad_spend
WHERE DATE >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY channel, campaign
),
rev AS (
SELECT channel, campaign, SUM(revenue) AS revenue_amt
FROM attributed_revenue
WHERE DATE >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY channel, campaign
)
SELECT
COALESCE(s.channel, r.channel) AS channel,
COALESCE(s.campaign, r.campaign) AS campaign,
s.spend_amt,
r.revenue_amt,
r.revenue_amt::NUMERIC / NULLIF(s.spend_amt, 0) AS roas
FROM spend s
FULL OUTER JOIN rev r
ON r.channel = s.channel AND r.campaign = s.campaign
ORDER BY roas DESC NULLS LAST;FULL OUTER JOIN показывает кампании с тратами но без revenue (плохие) и наоборот (атрибуционные баги).
Return-window и атрибуция
ROAS зависит от окна атрибуции. Купил через 2 дня после клика — это «привлёк маркетинг». Через 60 дней — спорно.
7-day attribution window
WITH ad_clicks AS (
SELECT user_id, channel, campaign, click_date
FROM ad_events
WHERE event_type = 'click'
),
purchases AS (
SELECT user_id, total, purchase_date
FROM orders
WHERE status = 'paid'
)
SELECT
c.channel,
c.campaign,
SUM(p.total) AS attributed_revenue_7d
FROM ad_clicks c
JOIN purchases p
ON p.user_id = c.user_id
AND p.purchase_date >= c.click_date
AND p.purchase_date < c.click_date + INTERVAL '7 days'
GROUP BY c.channel, c.campaign;Last-click vs first-click
Last-click: считаем покупку каналу последнего клика. Самая частая модель — но завышает ретаргетинг.
WITH last_click AS (
SELECT
user_id,
channel,
click_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY click_date DESC) AS rn
FROM ad_events
WHERE event_type = 'click'
)
SELECT
lc.channel,
SUM(o.total) AS revenue
FROM orders o
JOIN last_click lc ON lc.user_id = o.user_id AND lc.rn = 1
WHERE o.status = 'paid'
GROUP BY lc.channel;First-click оценивает «топ воронки», linear / time-decay — компромиссы.
ROAS vs ROI vs MER
| Метрика | Формула | Что показывает |
|---|---|---|
| ROAS | revenue / ad spend | возврат выручки на каждый рубль рекламы |
| ROI | (revenue × margin - spend) / spend | прибыль на каждый рубль (с учётом маржи) |
| MER (Marketing Efficiency Ratio) | total revenue / total marketing spend | общая эффективность маркетинга |
ROAS 4 при марже 20% — это ROI = (4 × 0,2 - 1) / 1 = -20%, убыток. ROAS — выручка, ROI — прибыль.
MER считает по всей выручке (включая органику), удобен для оценки всего маркетинга, а не одного канала.
Частые ошибки
Ошибка 1. ROAS без учёта маржи
«ROAS 4 — отлично!» Если маржа 15%, это убыток. Всегда показывайте бизнесу ROAS + margin, или конвертируйте в ROI.
Ошибка 2. JOIN до агрегации
Если у канала 100 клика и 30 покупок, и сделаешь JOIN до агрегации — получишь 100×30 = 3000 строк, SUM умножен. Сначала агрегаты, потом JOIN — см. базовый пример.
Ошибка 3. NULL revenue для каналов с тратами
Кампания запущена, потратила деньги, но ни одной покупки. INNER JOIN потеряет её. Используйте FULL OUTER JOIN + COALESCE(revenue, 0) для честного среднего.
Ошибка 4. Не учитывать return-window
Сравнение ROAS Google Ads (7d) vs Facebook (28d) — некорректно. Зафиксируйте window для всех каналов.
Ошибка 5. Дабл-каунт revenue
Если один заказ атрибутирован двум кампаниям — revenue считается дважды. Решение: дедупликация по user_id × order_id, или использование одной модели атрибуции на отчёт.
Ошибка 6. Считать ROAS = 1 как break-even
Это break-even по выручке, но не по прибыли. Для маржи 30% break-even ROAS = 1/0,3 ≈ 3,3.
Связанные темы
FAQ
Какой ROAS считается хорошим?
Зависит от маржи. Если маржа 20% — break-even ROAS = 5, цель ROAS 8-10. Маржа 50% — break-even ROAS = 2, цель 3-4. Просите данные о марже у финансистов.
ROAS у новых vs повторных покупателей одинаков?
Нет. Повторные — обычно сильно выше, но они и так бы купили. Считайте ROAS на новых пользователях (new customer ROAS) — это «честная» метрика рекламы.
Как мерить ROAS в B2B с долгим циклом сделки?
Стандартный ROAS не работает — клик в январе, сделка в июне. Считают pipeline ROAS (по сумме открытых сделок) или используют длинное окно (90-180 дней) с проверкой через когорты.
ROAS в реальном времени или с задержкой?
С задержкой 24-48 часов (атрибуция «дозревает» — конверсии приходят после кликов). Real-time дашборды показывают направление тренда, а не финальные числа.
Что такое target ROAS в Google Ads?
Автоматическая стратегия: алгоритм оптимизирует ставки так, чтобы среднее ROAS было ≥ заданного. Работает только при достаточном объёме конверсий (50+ в месяц по кампании).