Как посчитать конверсию в SQL
Содержание:
Формула
Базовая формула простая:
Конверсия = пользователи, выполнившие действие
----------------------------------- × 100%
все пользователиНа практике важно держать в голове три вещи:
- считать именно уникальных пользователей, а не события;
- ограничивать период окном, в котором конверсия имеет смысл (сессия, сутки, 7 дней);
- заботиться о дроблении по сегментам, иначе среднее будет врать.
1. Конверсия за период
SELECT
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'purchase')
* 100.0
/ NULLIF(COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'landing'), 0)
AS conversion_pct
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01';В FILTER фильтруются только те строки, которые попадают в знаменатель и числитель. COUNT(DISTINCT user_id) исключает повторные события одного пользователя, NULLIF(..., 0) защищает от падения запроса, если пользователей на лендинге не оказалось.
2. Универсальный вариант через CASE WHEN
FILTER есть не во всех СУБД. Эквивалент на CASE WHEN работает везде:
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END)
* 100.0
/ NULLIF(
COUNT(DISTINCT CASE WHEN event_name = 'landing' THEN user_id END),
0
) AS conversion_pct
FROM events;3. Воронка view → cart → purchase
WITH user_events AS (
SELECT
user_id,
MAX(CASE WHEN event_name = 'view' THEN 1 ELSE 0 END) AS viewed,
MAX(CASE WHEN event_name = 'cart' THEN 1 ELSE 0 END) AS carted,
MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS bought
FROM events
GROUP BY user_id
)
SELECT
SUM(viewed) AS step1_view,
SUM(carted) AS step2_cart,
SUM(bought) AS step3_purchase,
SUM(carted) * 100.0 / NULLIF(SUM(viewed), 0) AS cr_view_to_cart,
SUM(bought) * 100.0 / NULLIF(SUM(carted), 0) AS cr_cart_to_purchase,
SUM(bought) * 100.0 / NULLIF(SUM(viewed), 0) AS cr_overall
FROM user_events;Такая «простая» воронка игнорирует порядок событий — если важно учитывать, что cart случился после view, используйте запрос из статьи про воронку (как посчитать воронку в SQL).
4. Конверсия по дням
SELECT
event_at::DATE AS day,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'purchase')
* 100.0
/ NULLIF(COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'landing'), 0)
AS conversion_pct
FROM events
GROUP BY 1
ORDER BY 1;5. Конверсия по каналам
SELECT
u.channel,
COUNT(DISTINCT e.user_id) FILTER (WHERE e.event_name = 'purchase') AS buyers,
COUNT(DISTINCT e.user_id) AS visitors,
COUNT(DISTINCT e.user_id) FILTER (WHERE e.event_name = 'purchase')
* 100.0
/ NULLIF(COUNT(DISTINCT e.user_id), 0) AS conversion_pct
FROM users u
JOIN events e USING (user_id)
GROUP BY u.channel
ORDER BY conversion_pct DESC;Важно: «посетители» здесь — все пользователи канала, у которых есть хоть какой-то event. Для paid-каналов имеет смысл ограничить ещё и типом события (например, «landing»).
6. Конверсия с временным окном
Покупка должна случиться не когда угодно, а за разумный срок после первого визита:
WITH first_visit AS (
SELECT
user_id,
MIN(event_at) AS first_visit_at
FROM events
WHERE event_name = 'landing'
GROUP BY user_id
),
purchases AS (
SELECT DISTINCT user_id
FROM events e
JOIN first_visit fv USING (user_id)
WHERE e.event_name = 'purchase'
AND e.event_at <= fv.first_visit_at + INTERVAL '7 days'
)
SELECT
COUNT(DISTINCT p.user_id) * 100.0
/ NULLIF(COUNT(DISTINCT fv.user_id), 0) AS cr_7d
FROM first_visit fv
LEFT JOIN purchases p USING (user_id);Окно — ключевая деталь. «Конверсия за весь год» обычно бессмысленна, если цикл покупки в среднем занимает несколько дней.
7. Конверсия A/B-групп
SELECT
ab_group,
COUNT(*) AS users,
COUNT(*) FILTER (WHERE purchased) AS buyers,
100.0 * COUNT(*) FILTER (WHERE purchased) / COUNT(*) AS conversion_pct
FROM ab_test_users
GROUP BY ab_group;Чтобы не делать выводов на глаз, сравнение удобно расширить до классического A/B-анализа.
8. Конверсия регистрации → первая покупка
SELECT
DATE_TRUNC('month', u.registered_at)::DATE AS cohort,
COUNT(DISTINCT u.user_id) AS signups,
COUNT(DISTINCT o.user_id) AS first_buyers,
100.0 * COUNT(DISTINCT o.user_id) / NULLIF(COUNT(DISTINCT u.user_id), 0) AS conversion_pct
FROM users u
LEFT JOIN orders o
ON o.user_id = u.user_id
AND o.status = 'paid'
GROUP BY 1
ORDER BY 1;Такой запрос удобно положить в дашборд: сразу видно, как конвертится каждая новая месячная когорта.
Типичные ошибки
1. Целочисленное деление
-- 0: integer / integer
COUNT(*) * 100 / COUNT(*)
-- OK: 100.0 гарантирует дробную арифметику
COUNT(*) * 100.0 / COUNT(*)2. События вместо пользователей
COUNT(*) считает события, а не пользователей. Один покупатель с пятью заказами даст «+5» в числителе — конверсия может превысить 100%. Для корректного расчёта используйте COUNT(DISTINCT user_id).
3. Отсутствие временного окна
«Конверсия за весь год» без окна почти всегда выглядит выше реальной. Ограничьте период, в котором событие «покупка» имеет смысл после «визита».
4. Сегменты
Средняя конверсия 3% может быть собрана из iOS с 5% и Android с 1%. Средняя по больнице редко бывает полезной — всегда показывайте сегментированную картину хотя бы по платформе и стране.
5. Нет защиты от деления на ноль
Если в знаменателе может оказаться 0, запрос упадёт. Всегда оборачивайте в NULLIF(x, 0).
Связанные темы
FAQ
Конверсия или CTR — это одно и то же?
Нет. CTR — клики / показы, метрика верхней части воронки. Конверсия — доля пользователей, дошедших до целевого действия (покупки, регистрации, активации).
Макро или микро конверсия?
Макро — конечное действие (покупка, первая подписка). Микро — промежуточные (клик, просмотр товара, добавление в корзину). В отчётах полезно показывать оба уровня.
Как обрабатывать повторные покупки?
Зависит от определения метрики. «Конверсия в первую покупку» измеряется по MIN(created_at) по каждому пользователю. «Совершил покупку хоть раз за период» — по наличию любого события purchase в окне. Согласуйте определение с PM перед тем, как строить дашборд.
Когда конверсия перестаёт быть информативной?
Когда её не с чем сравнить. «CR 3%» сам по себе ничего не говорит — нужен либо тренд, либо бенчмарк, либо сравнение с предыдущим периодом.