Как посчитать funnel step conversion в SQL

Проверь себя · 1/3разбор после ответа
Вы хотите сравнить текущую метрику с метрикой следующего периода во временном ряду. Какая оконная функция возвращает «следующее» значение относительно текущей строки по заданному порядку сортировки?

Зачем step conversion

Funnel step conversion — пошаговая воронка с CR на каждом переходе. Раскрывает слабое звено: если landing→signup 50%, signup→trial 80%, trial→paid 8% — проблема в trial→paid (paywall, цена, ценность не донесена).

Структура воронки

Минимум 3–5 этапов: например, для SaaS landing → signup → trial → paid → renewed. Каждый этап = событие в логе.

event_name     | user_id | event_timestamp
landing        | 42      | 2026-05-01 10:00
signup         | 42      | 2026-05-01 10:15
trial_started  | 42      | 2026-05-01 10:17
paid           | 42      | 2026-05-15 09:00

Абсолютная конверсия

CR от верха воронки до каждого шага:

WITH funnel AS (
    SELECT
        user_id,
        MAX(CASE WHEN event_name = 'landing'       THEN 1 ELSE 0 END) AS landing,
        MAX(CASE WHEN event_name = 'signup'        THEN 1 ELSE 0 END) AS signup,
        MAX(CASE WHEN event_name = 'trial_started' THEN 1 ELSE 0 END) AS trial,
        MAX(CASE WHEN event_name = 'paid'          THEN 1 ELSE 0 END) AS paid
    FROM events
    WHERE event_timestamp >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY user_id
)
SELECT
    SUM(landing) AS landing_users,
    SUM(signup)  AS signup_users,
    SUM(trial)   AS trial_users,
    SUM(paid)    AS paid_users,
    SUM(signup) * 100.0 / NULLIF(SUM(landing), 0) AS landing_to_signup_pct,
    SUM(trial)  * 100.0 / NULLIF(SUM(landing), 0) AS landing_to_trial_pct,
    SUM(paid)   * 100.0 / NULLIF(SUM(landing), 0) AS landing_to_paid_pct
FROM funnel;

Это и есть «общая конверсия» — от всех визитов до оплаты.

Относительная конверсия

CR между соседними шагами — показывает, где именно проваливается:

WITH funnel_counts AS (
    SELECT
        SUM(CASE WHEN event_name = 'landing'       THEN 1 ELSE 0 END) AS landing_n,
        SUM(CASE WHEN event_name = 'signup'        THEN 1 ELSE 0 END) AS signup_n,
        SUM(CASE WHEN event_name = 'trial_started' THEN 1 ELSE 0 END) AS trial_n,
        SUM(CASE WHEN event_name = 'paid'          THEN 1 ELSE 0 END) AS paid_n
    FROM (
        SELECT DISTINCT user_id, event_name FROM events
        WHERE event_timestamp >= CURRENT_DATE - INTERVAL '30 days'
    ) u
)
SELECT
    signup_n * 100.0 / NULLIF(landing_n, 0) AS step_landing_signup,
    trial_n  * 100.0 / NULLIF(signup_n, 0)  AS step_signup_trial,
    paid_n   * 100.0 / NULLIF(trial_n, 0)   AS step_trial_paid
FROM funnel_counts;

Шаг с самым низким % — узкое место.

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

Сегментация по когортам

CR на каждом шаге по каналу подписания:

WITH user_funnel AS (
    SELECT
        u.user_id,
        u.utm_source AS channel,
        MAX(CASE WHEN e.event_name = 'signup'        THEN 1 ELSE 0 END) AS signup,
        MAX(CASE WHEN e.event_name = 'trial_started' THEN 1 ELSE 0 END) AS trial,
        MAX(CASE WHEN e.event_name = 'paid'          THEN 1 ELSE 0 END) AS paid
    FROM users u
    LEFT JOIN events e ON e.user_id = u.user_id
    WHERE u.created_at >= CURRENT_DATE - INTERVAL '60 days'
    GROUP BY u.user_id, u.utm_source
)
SELECT
    channel,
    COUNT(*) AS cohort_size,
    SUM(signup) * 100.0 / COUNT(*) AS signup_pct,
    SUM(trial)  * 100.0 / NULLIF(SUM(signup), 0) AS trial_after_signup_pct,
    SUM(paid)   * 100.0 / NULLIF(SUM(trial), 0)  AS paid_after_trial_pct
FROM user_funnel
GROUP BY channel
HAVING COUNT(*) >= 100
ORDER BY paid_after_trial_pct DESC;

HAVING COUNT(*) >= 100 — иначе CR с шумом.

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

Ошибка 1. Не учитывать порядок. Пользователь «paid» без «trial» — это либо бесплатные подарки, либо баг трекинга. WHERE шага i должен включать факт прохождения шагов 1..i−1.

Ошибка 2. Считать без DISTINCT user_id. Если один пользователь сделал signup дважды (баг), он задваивается в счётчике. Берите SELECT DISTINCT user_id, event_name.

Ошибка 3. Считать за всё время без окна. В воронку нужно брать конкретную когорту (signup_month) — иначе сравниваете пользователей разного возраста.

Ошибка 4. Игнорировать время до шага. Пользователь из мая мог не успеть дойти до paid → CR кажется ниже. Учитывайте, что для paid нужно минимум 14 дней триала.

Ошибка 5. Все шаги в одной строке вместо LEFT JOIN. В большой воронке (8+ шагов) пишите через CTE и LEFT JOIN по шагам, а не одну огромную таблицу с CASE WHEN.

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

FAQ

Строгий порядок или просто факт шага?

Строгий порядок (пользователь прошёл шаги 1→2→3 именно последовательно) нужен для продуктовой UX-воронки, где важна траектория. Свободный вариант (был на шаге хоть когда-нибудь) годится для маркетинговой воронки, где считают охват шага, а не путь до него.

Какое окно между шагами?

Зависит от продукта. В SaaS переход signup→trial обычно мгновенный, а trial→paid занимает 14–30 дней. Окно надо задавать под реальную длину каждого перехода, иначе конверсия «недозреет» и покажется ниже, чем есть.

Что если шаги ветвятся?

Если после шага A путь расходится на B1 или B2, а потом сходится в C, это по сути две отдельные воронки. Анализируйте каждую ветку отдельно, иначе смешаете разные сценарии в одну усреднённую цифру.

Сколько шагов оптимально?

Обычно 3–7 шагов. Более длинные воронки тяжело читать, и в них легко потерять, где именно проседает конверсия. Крупные этапы стоит дробить только там, где детализация реально нужна для решения.

Воронка или когортное удержание?

Воронка меряет переходы между шагами внутри одной когорты — путь от входа до целевого действия. Удержание (retention) меряет, возвращается ли пользователь со временем. Первое про разовый путь, второе про повторное использование.