Как посчитать unit-экономику в SQL
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 (несколько месяцев) почти всегда здоровее длинного.
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.