Как посчитать unit-экономику в SQL

Проверь себя · 1/3разбор после ответа
Без оконных функций нужно для каждого дня посчитать изменение выручки по сравнению с предыдущим днём. Таблица daily_revenue содержит date и revenue. Как это сделать?

Зачем это нужно

Unit-экономика отвечает на главный вопрос бизнеса: «зарабатываем мы на каждом клиенте или теряем?». LTV / CAC > 3 обычно считают здоровым для SaaS, LTV / CAC < 1 — откровенной проблемой. Инвесторы смотрят на юнит-экономику до того, как начинают обсуждать total revenue.

Чтобы собрать честную юнит-экономику, аналитик объединяет users, orders, ad_spend и subscriptions, аккуратно атрибутирует CAC и считает LTV по когорте. BI-инструменты так сходу это не собирают — поэтому всё обычно живёт в SQL-модели.

В статье — связанный набор запросов:

  • CAC по каналам;
  • LTV по когорте;
  • LTV / CAC ratio;
  • CAC payback period;
  • contribution margin;
  • сводный дашборд.

Полная формула CAC и LTV подробно разобрана в отдельных статьях (CAC, LTV), здесь — в контексте сводной юнит-экономики.

1. CAC по каналам (per-channel)

WITH spend_by_channel AS (
    SELECT channel, SUM(spend) AS total_spend
    FROM ad_spend
    WHERE day >= '2026-01-01' AND day < '2026-04-01'
    GROUP BY channel
),
users_by_channel AS (
    SELECT
        attribution_channel AS channel,
        COUNT(*) AS new_users
    FROM users
    WHERE signup_at >= '2026-01-01' AND signup_at < '2026-04-01'
    GROUP BY attribution_channel
)
SELECT
    s.channel,
    s.total_spend,
    u.new_users,
    s.total_spend::NUMERIC / NULLIF(u.new_users, 0) AS cac
FROM spend_by_channel s
LEFT JOIN users_by_channel u USING (channel)
ORDER BY cac;

Важно агрегировать спенд и пользователей независимо и только потом делить — иначе при JOIN'е сырых данных одна строка спенда задвоится на количество пользователей.

2. LTV (исторический с маржой)

WITH user_ltv AS (
    SELECT
        user_id,
        SUM(total) * 0.25 AS ltv_profit   -- 25% gross margin
    FROM orders
    WHERE status = 'paid'
    GROUP BY user_id
)
SELECT AVG(ltv_profit) AS avg_ltv_profit
FROM user_ltv;

Маржу имеет смысл хранить в отдельной таблице / параметре dbt — магическое число 0.25 внутри запроса быстро превращается в источник ошибок.

3. LTV / CAC по каналам

WITH
spend_by_channel AS (
    SELECT channel, SUM(spend) AS total_spend
    FROM ad_spend
    WHERE day >= '2026-01-01'
    GROUP BY channel
),
users_by_channel AS (
    SELECT
        attribution_channel AS channel,
        COUNT(*)            AS new_users
    FROM users
    WHERE signup_at >= '2026-01-01'
    GROUP BY attribution_channel
),
cac AS (
    SELECT
        s.channel,
        s.total_spend::NUMERIC / NULLIF(u.new_users, 0) AS cac_value
    FROM spend_by_channel s
    LEFT JOIN users_by_channel u USING (channel)
),
ltv AS (
    SELECT
        u.attribution_channel             AS channel,
        AVG(user_rev.revenue * 0.25)      AS avg_ltv_profit
    FROM users u
    LEFT JOIN (
        SELECT user_id, SUM(total) AS revenue
        FROM orders
        WHERE status = 'paid'
        GROUP BY user_id
    ) user_rev ON user_rev.user_id = u.user_id
    GROUP BY u.attribution_channel
)
SELECT
    c.channel,
    c.cac_value                                       AS cac,
    l.avg_ltv_profit                                  AS ltv,
    l.avg_ltv_profit / NULLIF(c.cac_value, 0)         AS ltv_cac_ratio,
    CASE
        WHEN l.avg_ltv_profit / NULLIF(c.cac_value, 0) >= 3 THEN 'healthy'
        WHEN l.avg_ltv_profit / NULLIF(c.cac_value, 0) >= 1 THEN 'acceptable'
        ELSE 'unprofitable'
    END AS health
FROM cac c
JOIN ltv l USING (channel)
ORDER BY ltv_cac_ratio DESC;

Такой взгляд сразу показывает, на каких каналах имеет смысл увеличивать бюджет, а где лучше вообще не тратить — если LTV / CAC ниже 1, каждый привлечённый пользователь убыточен.

4. CAC payback period

Сколько месяцев окупается CAC за счёт регулярной маржи от пользователя:

WITH monthly_user_profit AS (
    SELECT
        user_id,
        DATE_TRUNC('month', created_at)::DATE AS month,
        SUM(total) * 0.25 AS monthly_profit
    FROM orders
    WHERE status = 'paid'
    GROUP BY user_id, month
),
avg_profit AS (
    SELECT AVG(monthly_profit) AS avg_monthly_profit
    FROM monthly_user_profit
),
avg_cac AS (
    SELECT AVG(cac_value) AS avg_cac
    FROM (
        SELECT
            s.channel,
            s.total_spend::NUMERIC / NULLIF(u.new_users, 0) AS cac_value
        FROM (
            SELECT channel, SUM(spend) AS total_spend
            FROM ad_spend
            WHERE day >= '2026-01-01'
            GROUP BY channel
        ) s
        LEFT JOIN (
            SELECT
                attribution_channel AS channel,
                COUNT(*) AS new_users
            FROM users
            WHERE signup_at >= '2026-01-01'
            GROUP BY attribution_channel
        ) u USING (channel)
    ) c
)
SELECT
    a.avg_cac,
    p.avg_monthly_profit,
    a.avg_cac / NULLIF(p.avg_monthly_profit, 0) AS payback_months
FROM avg_cac a, avg_profit p;

Ориентиры зависят от бизнеса, но короткий payback (несколько месяцев) почти всегда здоровее длинного.

Закрепи формулу unit economics в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать unit economics в Telegram

5. Contribution margin

SELECT
    user_id,
    SUM(total)                                     AS revenue,
    SUM(total) * 0.25                              AS gross_profit,           -- 25% margin
    SUM(total) * 0.25
      - SUM(total) * 0.03                                                     -- 3% payment fees
      - COALESCE(SUM(shipping_cost), 0)            AS contribution_margin
FROM orders
WHERE status = 'paid'
GROUP BY user_id;

Contribution margin отличается от gross margin тем, что учитывает все переменные затраты (комиссии платёжных систем, доставку, иногда — часть CAC), а не только себестоимость товара.

6. Сводный дашборд

WITH cohorts AS (
    SELECT
        user_id,
        DATE_TRUNC('month', signup_at)::DATE AS cohort_month
    FROM users
    WHERE signup_at >= '2025-01-01'
),
spend_by_month AS (
    SELECT
        DATE_TRUNC('month', day)::DATE AS month,
        SUM(spend) AS spend
    FROM ad_spend
    GROUP BY 1
),
ltv_by_cohort AS (
    SELECT
        c.cohort_month                         AS month,
        COUNT(c.user_id)                       AS new_users,
        SUM(user_rev.revenue * 0.25)           AS total_ltv_profit,
        AVG(user_rev.revenue * 0.25)           AS avg_ltv_profit
    FROM cohorts c
    LEFT JOIN (
        SELECT user_id, SUM(total) AS revenue
        FROM orders
        WHERE status = 'paid'
        GROUP BY user_id
    ) user_rev USING (user_id)
    GROUP BY c.cohort_month
)
SELECT
    l.month                                                      AS cohort,
    l.new_users,
    s.spend                                                      AS spend,
    s.spend::NUMERIC / NULLIF(l.new_users, 0)                    AS cac,
    l.avg_ltv_profit                                             AS avg_ltv,
    l.avg_ltv_profit / NULLIF(s.spend::NUMERIC / NULLIF(l.new_users, 0), 0)
                                                                 AS ltv_cac_ratio,
    l.total_ltv_profit - s.spend                                 AS net_profit
FROM ltv_by_cohort l
LEFT JOIN spend_by_month s ON s.month = l.month
ORDER BY l.month;

Одна таблица отвечает на четыре вопроса одновременно: «сколько пришло», «сколько потратили», «какой LTV» и «какая итоговая маржа».

7. Break-even

Сколько выручки нужно, чтобы выйти в ноль, при известных fixed и variable costs:

WITH assumptions AS (
    SELECT
        5000000 AS monthly_fixed,       -- фиксированные расходы в месяц
        0.75    AS variable_pct          -- доля переменных расходов в выручке
)
SELECT
    monthly_fixed / NULLIF(1 - variable_pct, 0) AS revenue_to_break_even
FROM assumptions;

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

1. Сравнивать CAC с LTV-выручкой

Для юнит-экономики имеет смысл сравнивать CAC именно с прибылью на клиента, а не с выручкой. Иначе кажется, что всё прекрасно, а в итоге бизнес в минусе.

2. LTV по всем клиентам

Смесь старых (жили долго) и новых (ещё не успели принести выручку) завышает LTV свежих когорт. Всегда смотрите когортно.

3. CAC без накладных расходов

Только ad spend не даёт настоящего CAC — добавляйте зарплаты команды, инструменты, иногда долю общекорпоративных расходов (fully-loaded CAC).

4. Игнорировать payback

LTV / CAC 4:1 выглядит красиво, но если payback 24 месяца — cash flow страдает, и компания банкротится не из-за экономики, а из-за ликвидности.

5. Gross margin вместо contribution margin

Gross margin = revenue − COGS. Contribution margin вычитает ещё комиссии, логистику и иногда часть CAC — именно это число определяет, сколько в действительности остаётся у компании с каждого клиента.

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

FAQ

Всегда ли нужно LTV / CAC ≥ 3?

Это универсальная эвристика, но в быстрых e-commerce-нишах с коротким payback меньшие значения тоже могут быть рабочими. В B2B и SaaS с длинным payback — ориентируйтесь на 3 или выше.

Как считать для enterprise-сегмента?

Отдельно: один enterprise-клиент может покрывать десятки мелких, и смешивать их в одном среднем — смазывать картину.

Payback или LTV / CAC важнее?

Оба. Payback отвечает за cash flow, LTV / CAC — за стратегию. В молодых компаниях payback часто критичнее, потому что ограничен раннерей.

Contribution margin или gross margin?

Gross — revenue минус себестоимость. Contribution — минус все переменные расходы (комиссии, доставка, переменная часть CAC). Для юнит-экономики ближе contribution.