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

Проверь себя · 1/3разбор после ответа
У пользователя price = 100 и discount = NULL. Что вернёт выражение SELECT price + discount FROM products для этой строки?

Зачем аналитику считать CAC в SQL

CAC — первое число, которое спрашивают инвесторы, CMO и CFO. Отношение LTV / CAC показывает, работает ли юнит-экономика. Если CAC выше, чем LTV × маржа — каждый клиент убыточен; если CAC сильно ниже — вы, скорее всего, недоинвестируете и уступаете рынок конкурентам.

На словах CAC простой: «spend / acquired». На практике вопросов сразу становится много. Считаем blended или paid-only? Как распределить спенд между каналами? Что делать с органикой, которая пересекалась с платными показами? Какое окно атрибуции — 7, 28 дней, всё время? Ответы меняют картину, и плохая постановка задачи даёт некорректные решения о перераспределении бюджета.

В этой статье — восемь готовых SQL-запросов под типовые задачи маркетинг-аналитика:

  • blended CAC (весь маркетинг / все новые пользователи)
  • paid-only CAC
  • CAC по каналам и по когортам
  • CAC по факту первой покупки (а не регистрации)
  • LTV / CAC в разрезе канала
  • fully-loaded CAC (с зарплатами и инструментами)
  • linear-attribution CAC по multi-touch-касаниям

Схема таблиц: ad_spend(day, channel, spend), users(user_id, signup_at, attribution_channel) и опционально first_orders(user_id, first_order_at).

Что такое CAC

CAC (Customer Acquisition Cost) — стоимость привлечения одного клиента.

CAC = Marketing spend / New customers acquired

Вариаций формулы несколько:

  • Blended CAC — весь маркетинговый бюджет делим на всех новых пользователей, включая органику.
  • Paid CAC — только платный трафик, используется для оценки эффективности рекламы.
  • Fully-loaded CAC — учитывает зарплаты маркетинговой команды, платформы и инструменты. Такой CAC любят инвесторы и CFO.

Схема данных

ad_spend        (day, channel, spend)
users           (user_id, signup_at, attribution_channel)
first_orders    (user_id, first_order_at)   -- из orders

1. Blended CAC

Весь маркетинговый спенд за период / все новые клиенты за тот же период:

WITH spend_total AS (
    SELECT SUM(spend) AS total_spend
    FROM ad_spend
    WHERE day BETWEEN '2026-01-01' AND '2026-03-31'
),
new_users AS (
    SELECT COUNT(*) AS new_users_cnt
    FROM users
    WHERE signup_at BETWEEN '2026-01-01' AND '2026-03-31'
)
SELECT
    st.total_spend,
    nu.new_users_cnt,
    st.total_spend::NUMERIC / NULLIF(nu.new_users_cnt, 0) AS blended_cac
FROM spend_total st, new_users nu;

Blended CAC удобен для обзора, но прячет реальную эффективность рекламы: если половина пользователей пришла органически, «blended» CAC получается намного лучше, чем настоящий стоимость привлечения платного трафика.

2. CAC по каналам

Чтобы понять, где рекламный рубль работает лучше:

WITH spend_by_channel AS (
    SELECT channel, SUM(spend) AS total_spend
    FROM ad_spend
    WHERE day BETWEEN '2026-01-01' AND '2026-03-31'
    GROUP BY channel
),
users_by_channel AS (
    SELECT attribution_channel AS channel, COUNT(*) AS new_users
    FROM users
    WHERE signup_at BETWEEN '2026-01-01' AND '2026-03-31'
    GROUP BY attribution_channel
)
SELECT
    sbc.channel,
    sbc.total_spend,
    ubc.new_users,
    sbc.total_spend::NUMERIC / NULLIF(ubc.new_users, 0) AS cac
FROM spend_by_channel sbc
LEFT JOIN users_by_channel ubc USING (channel)
ORDER BY cac;

Не забывайте, что attribution_channel — результат вашей модели атрибуции. Last-click и multi-touch дадут разные распределения.

3. CAC по когортам

Когортный CAC точнее: сравниваем спенд и пользователей того же месяца.

WITH cohorts AS (
    SELECT
        DATE_TRUNC('month', signup_at) AS cohort_month,
        attribution_channel,
        COUNT(*)                        AS new_users
    FROM users
    GROUP BY 1, 2
),
spend AS (
    SELECT
        DATE_TRUNC('month', day) AS month,
        channel,
        SUM(spend)               AS spend
    FROM ad_spend
    GROUP BY 1, 2
)
SELECT
    c.cohort_month,
    c.attribution_channel,
    c.new_users,
    s.spend,
    s.spend::NUMERIC / NULLIF(c.new_users, 0) AS cac
FROM cohorts c
LEFT JOIN spend s
    ON s.month = c.cohort_month
   AND s.channel = c.attribution_channel
ORDER BY c.cohort_month, c.attribution_channel;

Дополнительная польза: видно, как меняется CAC канала от месяца к месяцу. Если он ползёт вверх — конкуренция выросла или аудитория выгорает.

4. Paid-only CAC

Отфильтровываем органику и прямые заходы:

WITH paid_spend AS (
    SELECT SUM(spend) AS paid_spend
    FROM ad_spend
    WHERE channel IN ('google_ads', 'yandex_direct', 'vk_ads', 'facebook_ads')
      AND day BETWEEN '2026-01-01' AND '2026-03-31'
),
paid_users AS (
    SELECT COUNT(*) AS paid_users
    FROM users
    WHERE attribution_channel IN ('google_ads', 'yandex_direct', 'vk_ads', 'facebook_ads')
      AND signup_at BETWEEN '2026-01-01' AND '2026-03-31'
)
SELECT
    paid_spend.paid_spend::NUMERIC / NULLIF(paid_users.paid_users, 0) AS paid_cac
FROM paid_spend, paid_users;

Список «платных» каналов стоит вынести в справочник или dbt-макрос — с ростом продукта каналов становится больше, и одна копипаста превращается в источник ошибок.

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

5. CAC по первой покупке

Более строгий CAC: считаем не регистрации, а платящих клиентов.

WITH spend AS (
    SELECT
        channel,
        DATE_TRUNC('month', day) AS month,
        SUM(spend)               AS spend
    FROM ad_spend
    GROUP BY 1, 2
),
first_buyers AS (
    SELECT
        u.attribution_channel AS channel,
        DATE_TRUNC('month', fo.first_order_at) AS month,
        COUNT(*)              AS new_buyers
    FROM first_orders fo
    JOIN users u ON u.user_id = fo.user_id
    GROUP BY 1, 2
)
SELECT
    s.channel,
    s.month,
    s.spend,
    fb.new_buyers,
    s.spend::NUMERIC / NULLIF(fb.new_buyers, 0) AS cac_per_buyer
FROM spend s
LEFT JOIN first_buyers fb
    ON fb.channel = s.channel AND fb.month = s.month
ORDER BY s.month, cac_per_buyer;

В SaaS-продуктах часто именно эта метрика попадает в отчётность — «cost per paying customer», — потому что бесплатные signup'ы в unit-экономику считать не совсем честно.

6. LTV / CAC в разрезе канала

Главное сравнение для маркетинга:

WITH spend_by_channel AS (
    SELECT channel, SUM(spend) AS total_spend
    FROM ad_spend
    GROUP BY channel
),
users_by_channel AS (
    SELECT attribution_channel AS channel, COUNT(*) AS new_users
    FROM users
    GROUP BY attribution_channel
),
cac_channel AS (
    -- агрегируем спенд и пользователей независимо и только потом делим,
    -- иначе после JOIN одна строка spend задвоится на число пользователей в канале
    SELECT
        s.channel,
        s.total_spend::NUMERIC / NULLIF(u.new_users, 0) AS cac
    FROM spend_by_channel s
    LEFT JOIN users_by_channel u USING (channel)
),
ltv_channel AS (
    SELECT
        u.attribution_channel AS channel,
        AVG(user_ltv.total_revenue) AS avg_ltv
    FROM users u
    JOIN (
        SELECT user_id, SUM(total) AS total_revenue
        FROM orders
        WHERE status = 'paid'
        GROUP BY user_id
    ) user_ltv ON user_ltv.user_id = u.user_id
    GROUP BY u.attribution_channel
)
SELECT
    c.channel,
    c.cac,
    l.avg_ltv,
    l.avg_ltv / NULLIF(c.cac, 0) AS ltv_cac_ratio
FROM cac_channel c
JOIN ltv_channel l USING (channel)
ORDER BY ltv_cac_ratio DESC;

Важный нюанс — агрегировать спенд и пользователей независимо и только потом делить. Если сделать JOIN на необработанных таблицах, одна строка спенда задвоится на количество пользователей в канале — и CAC окажется в десятки раз выше реального.

7. Fully-loaded CAC

Включает не только рекламу, но и зарплаты маркетинговой команды, лицензии инструментов и другие накладные расходы:

WITH all_marketing_cost AS (
    -- рекламный спенд
    SELECT SUM(spend) AS cost
    FROM ad_spend
    WHERE day BETWEEN '2026-01-01' AND '2026-03-31'

    UNION ALL

    -- зарплаты команды
    SELECT SUM(monthly_salary)
    FROM marketing_payroll
    WHERE month BETWEEN '2026-01-01' AND '2026-03-31'

    UNION ALL

    -- SaaS и инструменты
    SELECT SUM(cost)
    FROM marketing_tools
    WHERE month BETWEEN '2026-01-01' AND '2026-03-31'
),
total_cost AS (
    SELECT SUM(cost) AS total_marketing_cost FROM all_marketing_cost
),
new_users AS (
    SELECT COUNT(*) AS new_users_cnt
    FROM users
    WHERE signup_at BETWEEN '2026-01-01' AND '2026-03-31'
)
SELECT
    tc.total_marketing_cost,
    nu.new_users_cnt,
    tc.total_marketing_cost::NUMERIC / NULLIF(nu.new_users_cnt, 0) AS fully_loaded_cac
FROM total_cost tc, new_users nu;

Fully-loaded CAC почти всегда заметно выше blended — и это нормально. Именно эту цифру разумно использовать в board-репортах.

8. Linear attribution

Last-click — самый простой, но не самый справедливый способ. Linear attribution в SQL:

-- каждому касанию до регистрации присваиваем вес 1 / число касаний пользователя
WITH touches AS (
    SELECT
        t.user_id,
        t.channel,
        t.touched_at
    FROM marketing_touches t
    JOIN users u ON u.user_id = t.user_id
    WHERE t.touched_at <= u.signup_at
),
user_touch_counts AS (
    SELECT user_id, COUNT(*) AS total_touches
    FROM touches
    GROUP BY user_id
),
attributed AS (
    SELECT
        t.user_id,
        t.channel,
        1.0 / utc.total_touches AS attribution_weight
    FROM touches t
    JOIN user_touch_counts utc USING (user_id)
)
SELECT
    channel,
    SUM(attribution_weight) AS attributed_users
FROM attributed
GROUP BY channel
ORDER BY attributed_users DESC;

Дальше полученные «атрибуцированные пользователи» на канал подставляем вместо сырого COUNT(*) в расчёт CAC.

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

1. Путать blended и paid

Blended ≠ paid. Если 50% пользователей приходят органически, blended CAC = paid CAC / 2 — выглядит лучше, чем есть. В board-отчётах обязательно подписываем, какой CAC показываем.

2. Сравнивать CAC и LTV без маржи

LTV 3000 ₽ и CAC 1000 ₽ выглядят как 3:1. Но при 30% марже настоящий payoff — LTV 900 ₽ при CAC 1000 ₽, то есть убыток. Сравнивайте с маржой или с gross profit per user.

3. Забыть про NULLIF

spend / new_users            -- упадёт при new_users = 0
spend / NULLIF(new_users, 0) -- безопасно, вернёт NULL

4. Смешивать регистрации и покупки

CAC per signup ≠ CAC per paying customer. Для SaaS и многих подписочных продуктов отчёт держится на second.

5. Игнорировать окно атрибуции

Если пользователь кликнул рекламу в январе, а купил в марте, у разных команд будут разные мнения, кому её засчитать. Окно 7 или 28 дней — индустриальный стандарт, но зафиксируйте это в определении метрики и не меняйте его ретроспективно.

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

FAQ

Blended или paid CAC — что важнее?

Paid — для решений по бюджету рекламы. Blended — для общей юнит-экономики и board-репортинга. В идеале показывайте обе.

Нужен ли fully-loaded CAC?

Для малого бизнеса часто хватает paid + немного overhead. Для инвесторов и enterprise — обязательно, потому что зарплаты и инструменты сильно меняют картину.

Как учитывать длинное окно атрибуции?

Зафиксируйте разумный дефолт (например, 28 дней) и параллельно держите несколько моделей атрибуции для сравнения. Не меняйте окно задним числом — иначе сравнения с историей становятся бессмысленными.

CAC должен быть меньше LTV?

Общая рекомендация — LTV / CAC ≥ 3:1. Ниже — проблема либо в CAC, либо в LTV. Слишком высокая ratio (5:1 и выше) часто означает, что вы недоинвестируете и пропускаете рост.

Куда относить бренд-кампании?

Чаще всего — в fully-loaded CAC: это инвестиция в долгосрочное привлечение, и она редко поддаётся точной атрибуции к конкретным пользователям.