Как посчитать ROI в SQL

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

Зачем это нужно аналитику

ROI — главная метрика «стоит ли вкладываться». Маркетинг хочет знать, какие каналы окупаются. Продуктовая команда — стоил ли тот редизайн трёх месяцев разработки. Финансовый директор — какой возврат на софт для аналитики за 10 000$ в год. Все эти вопросы — ROI в разных одеждах.

Главная путаница: ROI ≠ ROAS. ROAS (Return on Ad Spend) = Revenue / Spend — грубая метрика, показывает выручку на рубль рекламы. ROI учитывает маржу и все расходы: ROI = (Profit − Investment) / Investment. ROAS 3 может быть убыточным, если margin 20% (выручка 3₽, profit 0.6₽, investment 1₽ → ROI = -40%).

На работе аналитика регулярно просят: «посчитай ROI кампании», «какой feature ROI в этом квартале», «сколько принесла скидочная акция». Без правильного SQL — выводы неверные.

В статье — готовые запросы:

  • Базовый ROI маркетинговой кампании
  • ROI по каналам и кампаниям
  • Feature ROI (value - cost of development)
  • ROI с учётом LTV (incremental)
  • ROI по когортам во времени
  • ROI vs ROAS — два отдельных запроса рядом

Схемы: ad_spend, orders, feature_usage.

1. Базовая формула

ROI = (Gain - Investment) / Investment × 100%

Пример: потратили 100 000 ₽ на рекламу, получили 150 000 ₽ profit → ROI = 50%.

2. ROI маркетинговой кампании

WITH campaign_revenue AS (
    SELECT
        o.attribution_campaign AS campaign,
        SUM(o.total * 0.25) AS gross_profit  -- 25% margin
    FROM orders o
    WHERE o.attribution_campaign = 'spring_sale'
      AND o.created_at >= '2026-04-01'
      AND o.status = 'paid'
    GROUP BY 1
),
campaign_spend AS (
    SELECT
        campaign,
        SUM(spend) AS total_spend
    FROM ad_spend
    WHERE campaign = 'spring_sale'
      AND day >= '2026-04-01'
    GROUP BY 1
)
SELECT
    cr.campaign,
    cs.total_spend AS investment,
    cr.gross_profit AS gain,
    (cr.gross_profit - cs.total_spend) AS net_profit,
    (cr.gross_profit - cs.total_spend) / NULLIF(cs.total_spend, 0) * 100 AS roi_pct
FROM campaign_revenue cr
JOIN campaign_spend cs USING (campaign);

3. ROI по каналам

WITH by_channel AS (
    SELECT
        channel,
        SUM(spend) AS spend,
        SUM(revenue * 0.25) AS profit  -- margin
    FROM (
        SELECT u.attribution_channel AS channel,
               SUM(o.total) AS revenue,
               0 AS spend
        FROM orders o
        JOIN users u ON u.id = o.user_id
        WHERE o.created_at >= '2026-01-01'
          AND o.status = 'paid'
        GROUP BY 1
        UNION ALL
        SELECT channel, 0, SUM(spend)
        FROM ad_spend
        WHERE day >= '2026-01-01'
        GROUP BY 1
    ) t
    GROUP BY channel
)
SELECT
    channel,
    spend,
    profit,
    (profit - spend) / NULLIF(spend, 0) * 100 AS roi_pct
FROM by_channel
WHERE spend > 0
ORDER BY roi_pct DESC;

4. ROI с учётом LTV (incremental)

Краткосрочный ROI обманывает: одна покупка в первый месяц, но пользователь возвращается 12 месяцев. Включаем LTV:

WITH cohort_ltv AS (
    SELECT
        DATE_TRUNC('month', first_order_at) AS acquired_month,
        u.attribution_channel AS channel,
        AVG(lifetime_revenue * 0.25) AS avg_profit_per_user  -- margin
    FROM users u
    JOIN (
        SELECT user_id, MIN(created_at) AS first_order_at, SUM(total) AS lifetime_revenue
        FROM orders WHERE status = 'paid'
        GROUP BY user_id
    ) user_rev ON user_rev.user_id = u.id
    GROUP BY 1, 2
),
spend_per_month AS (
    SELECT DATE_TRUNC('month', day) AS month, channel, SUM(spend) AS spend
    FROM ad_spend
    GROUP BY 1, 2
),
users_acquired AS (
    SELECT DATE_TRUNC('month', signup_at) AS month, attribution_channel AS channel,
           COUNT(*) AS acquired
    FROM users
    GROUP BY 1, 2
)
SELECT
    cl.acquired_month,
    cl.channel,
    cl.avg_profit_per_user * ua.acquired AS total_profit,
    sm.spend AS investment,
    (cl.avg_profit_per_user * ua.acquired - sm.spend) / NULLIF(sm.spend, 0) * 100 AS ltv_roi_pct
FROM cohort_ltv cl
JOIN spend_per_month sm ON sm.month = cl.acquired_month AND sm.channel = cl.channel
JOIN users_acquired ua ON ua.month = cl.acquired_month AND ua.channel = cl.channel;

5. Feature ROI

ROI на продуктовые фичи. Investment = часы разработки × rate. Gain = incremental revenue.

-- упрощённо: предположим value фичи = revenue пользователей, которые её используют
WITH feature_users AS (
    SELECT DISTINCT user_id FROM feature_usage WHERE feature_name = 'new_checkout'
),
feature_revenue AS (
    SELECT SUM(o.total) * 0.25 AS profit  -- margin
    FROM orders o
    WHERE o.user_id IN (SELECT user_id FROM feature_users)
      AND o.created_at >= '2026-04-01'
      AND o.status = 'paid'
),
feature_investment AS (
    SELECT 40 * 160 * 2500 AS dev_cost  -- 40 hours × 160 rate × 2.5 месяца ≈ 1.6M
)
SELECT
    profit,
    dev_cost AS investment,
    (profit - dev_cost) / NULLIF(dev_cost, 0) * 100 AS feature_roi_pct
FROM feature_revenue, feature_investment;

Caveat: это не incremental — нужно A/B-тест, чтобы понять, что фича реально дала прирост.

6. ROI vs ROAS — разница

SELECT
    campaign,
    SUM(revenue) AS revenue,
    SUM(spend) AS spend,
    -- ROAS — просто revenue / spend
    SUM(revenue) / NULLIF(SUM(spend), 0) AS roas,
    -- ROI учитывает margin
    (SUM(revenue) * 0.25 - SUM(spend)) / NULLIF(SUM(spend), 0) * 100 AS roi_pct
FROM campaign_stats
GROUP BY campaign;

ROAS 4 + margin 25% → ROI = 0%. Ноль прибыли. ROAS 4 + margin 30% → ROI = 20%.

Отсюда правило: breakeven ROAS = 1 / margin. При margin 25% breakeven ROAS = 4.

7. ROI бизнес-решения

Пример: стоит ли нанять customer success менеджера для enterprise?

-- investment: salary + benefits, 12 месяцев
-- gain: снижение churn × avg MRR

WITH churn_reduction AS (
    SELECT
        150 AS enterprise_customers,       -- количество enterprise
        0.05 AS current_monthly_churn,     -- 5%
        0.03 AS expected_monthly_churn,    -- 3% после найма CSM
        50000 AS avg_mrr_per_customer,
        1500000 AS csm_annual_cost          -- 1.5M в год
)
SELECT
    enterprise_customers * avg_mrr_per_customer *
        (current_monthly_churn - expected_monthly_churn) * 12 AS annual_saved_revenue,
    csm_annual_cost AS investment,
    (enterprise_customers * avg_mrr_per_customer *
        (current_monthly_churn - expected_monthly_churn) * 12 - csm_annual_cost) /
        NULLIF(csm_annual_cost, 0) * 100 AS roi_pct
FROM churn_reduction;

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

Путать ROI и ROAS

ROAS — выручка. ROI — profit (с margin).

Не учитывать LTV

Краткосрочный ROI отрицательный, долгосрочный — положительный. Нужны оба.

Не считать incremental

Post-hoc ROI часто завышен. Правильная проверка — A/B.

Игнорировать overhead

Кроме прямых затрат есть operational overhead. Fully-loaded ROI реалистичнее.

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

FAQ

ROI в процентах или в долях?

Обычно в процентах. ROI = 0.5 → 50%.

Margin не знаем — как считать?

Уточните у финансов. Без margin — ROAS, не ROI.

Когда ROI отрицательный — бросать канал?

Если ROI считается без LTV — возможно канал окупается позже.

Можно ли ROI для бренд-рекламы?

Сложно. Brand awareness эффекты долгие. Обычно ROI только для performance.


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