Как посчитать воронку конверсии в SQL
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Что такое воронка
Funnel — последовательность шагов, через которые проходит пользователь. На каждом шаге часть пользователей отваливается.
Примеры:
- E-commerce: landing → product page → cart → checkout → paid
- SaaS: signup → email confirm → first login → activation → paid
- Mobile: install → open → onboarding → main screen → purchase
Схема данных
Предположим таблицу событий:
events (user_id, event_name, event_at)1. Простая воронка (без учёта порядка)
Сколько уникальных пользователей на каждом шаге:
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'landing_view' THEN user_id END) AS step1,
COUNT(DISTINCT CASE WHEN event_name = 'product_view' THEN user_id END) AS step2,
COUNT(DISTINCT CASE WHEN event_name = 'add_to_cart' THEN user_id END) AS step3,
COUNT(DISTINCT CASE WHEN event_name = 'checkout_start' THEN user_id END) AS step4,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS step5
FROM events
WHERE event_at >= '2026-04-01'
AND event_at < '2026-05-01';Проблема: пользователь мог совершить purchase в прошлом месяце и add_to_cart в этом. Не учитывает последовательность.
2. Строгая воронка (с учётом порядка)
Каждый шаг — пользователи, которые прошли все предыдущие:
WITH step1 AS (
SELECT DISTINCT user_id, MIN(event_at) AS step1_at
FROM events WHERE event_name = 'landing_view' GROUP BY user_id
),
step2 AS (
SELECT s1.user_id, MIN(e.event_at) AS step2_at
FROM step1 s1
JOIN events e ON e.user_id = s1.user_id
AND e.event_name = 'product_view'
AND e.event_at > s1.step1_at
GROUP BY s1.user_id
),
step3 AS (
SELECT s2.user_id, MIN(e.event_at) AS step3_at
FROM step2 s2
JOIN events e ON e.user_id = s2.user_id
AND e.event_name = 'add_to_cart'
AND e.event_at > s2.step2_at
GROUP BY s2.user_id
),
step4 AS (
SELECT s3.user_id, MIN(e.event_at) AS step4_at
FROM step3 s3
JOIN events e ON e.user_id = s3.user_id
AND e.event_name = 'purchase'
AND e.event_at > s3.step3_at
GROUP BY s3.user_id
)
SELECT
(SELECT COUNT(*) FROM step1) AS step1,
(SELECT COUNT(*) FROM step2) AS step2,
(SELECT COUNT(*) FROM step3) AS step3,
(SELECT COUNT(*) FROM step4) AS step4;3. Воронка с окном времени
Только пользователи, прошедшие все шаги в течение 24 часов:
WITH first_events AS (
SELECT
user_id,
MIN(CASE WHEN event_name = 'landing_view' THEN event_at END) AS t1,
MIN(CASE WHEN event_name = 'product_view' THEN event_at END) AS t2,
MIN(CASE WHEN event_name = 'add_to_cart' THEN event_at END) AS t3,
MIN(CASE WHEN event_name = 'purchase' THEN event_at END) AS t4
FROM events
GROUP BY user_id
)
SELECT
COUNT(CASE WHEN t1 IS NOT NULL THEN 1 END) AS step1,
COUNT(CASE WHEN t2 > t1 AND t2 < t1 + INTERVAL '24 hours' THEN 1 END) AS step2,
COUNT(CASE WHEN t3 > t2 AND t3 < t1 + INTERVAL '24 hours' THEN 1 END) AS step3,
COUNT(CASE WHEN t4 > t3 AND t4 < t1 + INTERVAL '24 hours' THEN 1 END) AS step4
FROM first_events;4. Конверсии шагов
WITH funnel AS (
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'landing_view' THEN user_id END) AS step1,
COUNT(DISTINCT CASE WHEN event_name = 'product_view' THEN user_id END) AS step2,
COUNT(DISTINCT CASE WHEN event_name = 'add_to_cart' THEN user_id END) AS step3,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS step4
FROM events
)
SELECT
step1,
step2,
step3,
step4,
100.0 * step2 / NULLIF(step1, 0) AS cr_1_2,
100.0 * step3 / NULLIF(step2, 0) AS cr_2_3,
100.0 * step4 / NULLIF(step3, 0) AS cr_3_4,
100.0 * step4 / NULLIF(step1, 0) AS cr_total
FROM funnel;5. Воронка по когортам
Смотрим, как конверсия меняется в зависимости от месяца регистрации:
WITH cohorts AS (
SELECT user_id, DATE_TRUNC('month', MIN(event_at)) AS cohort_month
FROM events WHERE event_name = 'signup'
GROUP BY user_id
),
funnel_by_cohort AS (
SELECT
c.cohort_month,
COUNT(DISTINCT c.user_id) AS signups,
COUNT(DISTINCT CASE WHEN e.event_name = 'activation' THEN c.user_id END) AS activated,
COUNT(DISTINCT CASE WHEN e.event_name = 'purchase' THEN c.user_id END) AS paid
FROM cohorts c
LEFT JOIN events e ON e.user_id = c.user_id
GROUP BY c.cohort_month
)
SELECT
cohort_month,
signups,
activated,
paid,
100.0 * activated / signups AS cr_activation,
100.0 * paid / signups AS cr_paid
FROM funnel_by_cohort
ORDER BY cohort_month;6. Воронка с декомпозицией по сегменту
По каналу привлечения:
SELECT
u.attribution_channel,
COUNT(DISTINCT u.user_id) AS signups,
COUNT(DISTINCT CASE WHEN e.event_name = 'activation' THEN u.user_id END) AS activated,
COUNT(DISTINCT CASE WHEN e.event_name = 'purchase' THEN u.user_id END) AS paid,
100.0 * COUNT(DISTINCT CASE WHEN e.event_name = 'activation' THEN u.user_id END)
/ COUNT(DISTINCT u.user_id) AS cr_activation
FROM users u
LEFT JOIN events e ON e.user_id = u.user_id
GROUP BY u.attribution_channel
ORDER BY cr_activation DESC;7. Time-to-convert
Сколько времени пользователю нужно, чтобы дойти до каждого шага:
WITH user_funnel AS (
SELECT
user_id,
MIN(CASE WHEN event_name = 'signup' THEN event_at END) AS signup_at,
MIN(CASE WHEN event_name = 'activation' THEN event_at END) AS activation_at,
MIN(CASE WHEN event_name = 'purchase' THEN event_at END) AS purchase_at
FROM events
GROUP BY user_id
)
SELECT
AVG(EXTRACT(EPOCH FROM (activation_at - signup_at)) / 3600) AS avg_hrs_to_activate,
AVG(EXTRACT(EPOCH FROM (purchase_at - signup_at)) / 3600) AS avg_hrs_to_purchase,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY
EXTRACT(EPOCH FROM (activation_at - signup_at)) / 3600) AS median_hrs_to_activate
FROM user_funnel
WHERE activation_at IS NOT NULL
AND signup_at IS NOT NULL;8. Воронка в сессии (все шаги в одной сессии)
WITH sessions AS (
SELECT
user_id,
session_id,
event_name,
event_at
FROM events
WHERE event_at >= '2026-04-01'
),
session_funnel AS (
SELECT
session_id,
MAX(CASE WHEN event_name = 'landing_view' THEN 1 ELSE 0 END) AS has_step1,
MAX(CASE WHEN event_name = 'product_view' THEN 1 ELSE 0 END) AS has_step2,
MAX(CASE WHEN event_name = 'add_to_cart' THEN 1 ELSE 0 END) AS has_step3,
MAX(CASE WHEN event_name = 'purchase' THEN 1 ELSE 0 END) AS has_step4
FROM sessions
GROUP BY session_id
)
SELECT
SUM(has_step1) AS step1,
SUM(has_step1 * has_step2) AS step2,
SUM(has_step1 * has_step2 * has_step3) AS step3,
SUM(has_step1 * has_step2 * has_step3 * has_step4) AS step4
FROM session_funnel;9. Drop-off analysis
Пользователи, застрявшие на каждом шаге:
WITH user_progress AS (
SELECT
user_id,
MAX(CASE WHEN event_name = 'step1' THEN 1 ELSE 0 END) AS s1,
MAX(CASE WHEN event_name = 'step2' THEN 1 ELSE 0 END) AS s2,
MAX(CASE WHEN event_name = 'step3' THEN 1 ELSE 0 END) AS s3,
MAX(CASE WHEN event_name = 'step4' THEN 1 ELSE 0 END) AS s4
FROM events
GROUP BY user_id
)
SELECT
SUM(CASE WHEN s1=1 AND s2=0 THEN 1 ELSE 0 END) AS dropped_at_step1,
SUM(CASE WHEN s2=1 AND s3=0 THEN 1 ELSE 0 END) AS dropped_at_step2,
SUM(CASE WHEN s3=1 AND s4=0 THEN 1 ELSE 0 END) AS dropped_at_step3,
SUM(CASE WHEN s4=1 THEN 1 ELSE 0 END) AS completed
FROM user_progress;Частые ошибки
Ошибка 1. Игнорировать порядок
-- считает покупки до корзины
COUNT(DISTINCT CASE WHEN event_name = 'purchase' ...)Используйте строгую воронку через subsequent JOIN.
Ошибка 2. Не учитывать time window
Пользователь зарегистрировался в январе, купил в апреле. Считать ли это за «conversion»? Зависит от задачи — но обязательно думать про окно.
Ошибка 3. COUNT(*) вместо COUNT(DISTINCT)
-- пользователь, сделавший 5 покупок, учтётся 5 раз
COUNT(CASE WHEN event_name = 'purchase' THEN 1 END)
-- правильно — unique users
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END)Ошибка 4. Смешивать total и unique
На step1 — 1000 unique users. На step2 — 500 events (не users). Всегда consistent подход.
Связанные темы
FAQ
Сколько шагов в типичной воронке?
E-commerce: 4-6 шагов. SaaS: 5-8. Слишком много шагов — запутанный анализ.
Как выбрать time window?
Зависит от продукта. E-commerce: сессия / 24 часа. SaaS: 7 дней. B2B: 30 дней.
Строгая или мягкая воронка?
Для оптимизации UX — строгая (с порядком). Для общего monitoring — мягкая проще.
Что делать с drop-off между шагами?
Найти причину: session recording, surveys, A/B-тест альтернатив.
Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.