Как посчитать конверсию в SQL

Проверь себя · 1/3разбор после ответа
Как получить начало текущей недели (недельный бакет) по текущей дате в PostgreSQL?

Формула

Базовая формула простая:

Конверсия = пользователи, выполнившие действие
           ----------------------------------- × 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»).

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

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%» сам по себе ничего не говорит — нужен либо тренд, либо бенчмарк, либо сравнение с предыдущим периодом.