SQL-запросы для маркетинга
Схема данных
Типичная для маркетинга:
users (user_id, channel, registered_at, first_purchase_at)marketing_spend (date, channel, spend)orders (order_id, user_id, amount, created_at)events (user_id, event, source, created_at)
1. CAC по каналам
WITH spend AS (
SELECT channel, SUM(spend) AS total_spend
FROM marketing_spend
WHERE DATE >= '2026-04-01' AND DATE < '2026-05-01'
GROUP BY channel
),
customers AS (
SELECT channel, COUNT(DISTINCT user_id) AS new_paying
FROM users
WHERE first_purchase_at >= '2026-04-01'
AND first_purchase_at < '2026-05-01'
GROUP BY channel
)
SELECT s.channel, s.total_spend, c.new_paying,
s.total_spend / NULLIF(c.new_paying, 0) AS cac
FROM spend s
LEFT JOIN customers c USING (channel)
ORDER BY cac;2. ROAS (Return on Ad Spend)
WITH spend AS (
SELECT channel, SUM(spend) AS spend FROM marketing_spend
WHERE DATE >= '2026-04-01' GROUP BY channel
),
revenue AS (
SELECT u.channel, SUM(o.amount) AS revenue
FROM orders o
JOIN users u USING (user_id)
WHERE o.created_at >= '2026-04-01' GROUP BY u.channel
)
SELECT s.channel, s.spend, r.revenue,
ROUND(r.revenue / s.spend, 2) AS roas
FROM spend s JOIN revenue r USING (channel)
ORDER BY roas DESC;ROAS > 3 — канал работает. ROAS < 1 — убыточен.
3. LTV/CAC по каналам
WITH ltv_90 AS (
SELECT u.channel,
AVG(total_rev) AS avg_ltv_90d
FROM (
SELECT user_id, SUM(amount) AS total_rev
FROM orders
WHERE created_at <= registered_at + INTERVAL '90 day'
GROUP BY user_id
) t
JOIN users u USING (user_id)
GROUP BY u.channel
),
cac AS (
SELECT channel, SUM(spend) / NULLIF(COUNT(DISTINCT user_id), 0) AS cac
FROM marketing_spend s
JOIN users u USING (channel)
WHERE u.first_purchase_at >= s.DATE
AND u.first_purchase_at < s.DATE + INTERVAL '30 day'
GROUP BY channel
)
SELECT l.channel, l.avg_ltv_90d, c.cac,
l.avg_ltv_90d / c.cac AS ltv_cac_ratio
FROM ltv_90 l JOIN cac c USING (channel)
ORDER BY ltv_cac_ratio DESC;4. Воронка регистрации по каналам
SELECT channel,
COUNT(DISTINCT user_id) FILTER (WHERE event = 'landing_visit') AS visits,
COUNT(DISTINCT user_id) FILTER (WHERE event = 'form_open') AS form_opens,
COUNT(DISTINCT user_id) FILTER (WHERE event = 'signup') AS signups,
ROUND(
COUNT(DISTINCT user_id) FILTER (WHERE event = 'signup') * 100.0 /
NULLIF(COUNT(DISTINCT user_id) FILTER (WHERE event = 'landing_visit'), 0),
2) AS conv_pct
FROM events e
JOIN users u USING (user_id)
GROUP BY channel;Прокачать тему на реальных задачах удобно в боте @kariernik_bot — база вопросов собрана с собеседований в Яндексе, Авито, Ozon, Тинькофф.
5. Last-click атрибуция
WITH last_touch AS (
SELECT user_id, source,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM events
WHERE event = 'landing_visit'
AND created_at < (
SELECT first_purchase_at FROM users u WHERE u.user_id = events.user_id
)
)
SELECT source, COUNT(*) AS conversions
FROM last_touch
WHERE rn = 1
GROUP BY source
ORDER BY conversions DESC;
6. First-click атрибуция
WITH first_touch AS (
SELECT user_id, source,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
FROM events WHERE event = 'landing_visit'
)
SELECT source, COUNT(*) AS conversions
FROM first_touch WHERE rn = 1
GROUP BY source;7. Retention по когортам привлечения
WITH cohort AS (
SELECT user_id, channel,
DATE_TRUNC('month', registered_at)::DATE AS cohort_month
FROM users
),
active_d30 AS (
SELECT DISTINCT u.user_id, u.channel, u.cohort_month
FROM cohort u
WHERE EXISTS (
SELECT 1 FROM events
WHERE user_id = u.user_id
AND created_at::DATE = (u.cohort_month + 30)
)
)
SELECT c.channel, c.cohort_month,
COUNT(*) AS cohort_size,
COUNT(a.user_id) * 1.0 / COUNT(*) AS retention_d30
FROM cohort c
LEFT JOIN active_d30 a USING (user_id)
GROUP BY c.channel, c.cohort_month
ORDER BY c.cohort_month, retention_d30 DESC;8. CTR рассылок
SELECT campaign_id,
COUNT(*) FILTER (WHERE event = 'email_sent') AS sent,
COUNT(*) FILTER (WHERE event = 'email_opened') AS opened,
COUNT(*) FILTER (WHERE event = 'email_clicked') AS clicked,
ROUND(
COUNT(*) FILTER (WHERE event = 'email_opened') * 100.0 /
NULLIF(COUNT(*) FILTER (WHERE event = 'email_sent'), 0),
2) AS open_rate,
ROUND(
COUNT(*) FILTER (WHERE event = 'email_clicked') * 100.0 /
NULLIF(COUNT(*) FILTER (WHERE event = 'email_opened'), 0),
2) AS ctr
FROM email_events
GROUP BY campaign_id;9. Повторные покупки по каналам
SELECT u.channel,
COUNT(DISTINCT u.user_id) AS total_customers,
COUNT(DISTINCT o.user_id) FILTER (WHERE order_num >= 2) AS repeat_customers,
ROUND(
COUNT(DISTINCT o.user_id) FILTER (WHERE order_num >= 2) * 100.0 /
NULLIF(COUNT(DISTINCT u.user_id), 0),
2) AS repeat_rate
FROM users u
LEFT JOIN (
SELECT user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
FROM orders WHERE status = 'paid'
) o USING (user_id)
GROUP BY u.channel;10. ROI кампании
WITH campaign_revenue AS (
SELECT c.campaign_id, SUM(o.amount) AS revenue
FROM campaigns c
JOIN users u ON u.attribution_campaign = c.campaign_id
JOIN orders o USING (user_id)
WHERE o.status = 'paid'
GROUP BY c.campaign_id
)
SELECT c.name, c.budget, r.revenue,
ROUND((r.revenue - c.budget) * 100.0 / c.budget, 2) AS roi_pct
FROM campaigns c
JOIN campaign_revenue r USING (campaign_id)
ORDER BY roi_pct DESC;На собесе такие штуки часто спрашивают. Быстрый способ довести до автоматизма — тренажёр в Telegram с задачами из реальных интервью.
11. Дневная динамика spend vs installs
SELECT s.DATE, s.spend, i.installs,
s.spend / NULLIF(i.installs, 0) AS cpi
FROM marketing_spend s
LEFT JOIN (
SELECT DATE(registered_at) AS DATE, COUNT(*) AS installs
FROM users GROUP BY 1
) i USING (DATE)
WHERE s.channel = 'mobile_ads'
ORDER BY s.DATE;12. Топ-5 кампаний по новым пользователям
SELECT campaign_id, COUNT(DISTINCT user_id) AS new_users
FROM users
WHERE registered_at >= CURRENT_DATE - INTERVAL '30 day'
AND attribution_campaign IS NOT NULL
GROUP BY campaign_id
ORDER BY new_users DESC
LIMIT 5;Как использовать
Запросы — шаблоны для Data Mart в маркетинговых дашбордах.
Читайте также
FAQ
Last-click или first-click?
Last-click — для оптимизации performance. First-click — для понимания, что двигает user к продукту. Оба ограничены — для сложных флоу нужна multi-touch atribution.
Как считать CAC с учётом зарплат маркетинга?
Включайте в total spend: media + agency + зарплаты + подписки на инструменты.
ROAS vs ROI?
ROAS = revenue / spend (без учёта costs). ROI = profit / spend (с учётом всех costs).
Как считать с time-lag?
Пользователь увидел рекламу в январе, купил в марте. Атрибутируйте по first-touch или ставьте cutoff (60 дней).