Продакт-менеджеру не нужно знать SQL как бэкенд-разработчик. Но базовые запросы для самостоятельной проверки гипотез — must have. Вот 10 паттернов, которые покрывают 80% задач.
1. DAU, WAU, MAU — активная аудитория
-- Уникальных активных пользователей за последние 30 дней (MAU)
SELECT COUNT(DISTINCT user_id) AS mau
FROM user_events
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days';
-- DAU по дням за последние 2 недели
SELECT
event_date::date AS day,
COUNT(DISTINCT user_id) AS dau
FROM user_events
WHERE event_date >= CURRENT_DATE - INTERVAL '14 days'
GROUP BY 1
ORDER BY 1;
2. Конверсия по воронке
-- Воронка: посетил страницу → нажал CTA → оформил заказ
SELECT
COUNT(DISTINCT user_id) AS visited,
COUNT(DISTINCT CASE WHEN event = 'cta_click' THEN user_id END) AS clicked,
COUNT(DISTINCT CASE WHEN event = 'order_placed' THEN user_id END) AS converted,
ROUND(
COUNT(DISTINCT CASE WHEN event = 'cta_click' THEN user_id END)::numeric
/ COUNT(DISTINCT user_id) * 100, 1
) AS visit_to_click_pct,
ROUND(
COUNT(DISTINCT CASE WHEN event = 'order_placed' THEN user_id END)::numeric
/ NULLIF(COUNT(DISTINCT CASE WHEN event = 'cta_click' THEN user_id END), 0) * 100, 1
) AS click_to_order_pct
FROM user_events
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days';
3. Retention: удержание пользователей
-- Day-1 retention: сколько пользователей вернулись на следующий день
WITH registrations AS (
SELECT user_id, DATE(created_at) AS reg_date
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
),
returns AS (
SELECT DISTINCT user_id, DATE(event_date) AS return_date
FROM user_events
)
SELECT
r.reg_date,
COUNT(DISTINCT r.user_id) AS registered,
COUNT(DISTINCT rt.user_id) AS returned_next_day,
ROUND(
COUNT(DISTINCT rt.user_id)::numeric / COUNT(DISTINCT r.user_id) * 100, 1
) AS d1_retention
FROM registrations r
LEFT JOIN returns rt
ON rt.user_id = r.user_id
AND rt.return_date = r.reg_date + 1
GROUP BY r.reg_date
ORDER BY r.reg_date;
4. Топ фичей по использованию
-- Какие функции используются чаще всего
SELECT
feature_name,
COUNT(*) AS total_uses,
COUNT(DISTINCT user_id) AS unique_users,
ROUND(COUNT(*) / COUNT(DISTINCT user_id), 1) AS uses_per_user
FROM feature_events
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY feature_name
ORDER BY unique_users DESC
LIMIT 20;
5. Новые vs возвращающиеся пользователи
SELECT
DATE_TRUNC('week', e.event_date)::date AS week,
COUNT(DISTINCT CASE WHEN e.event_date = u.created_at::date THEN e.user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN e.event_date > u.created_at::date THEN e.user_id END) AS returning_users
FROM user_events e
JOIN users u ON u.id = e.user_id
WHERE e.event_date >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY 1
ORDER BY 1;
6. Среднее время до конверсии
-- Сколько дней проходит от регистрации до первого заказа
SELECT
ROUND(AVG(days_to_first_order)) AS avg_days,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_to_first_order) AS median_days,
MIN(days_to_first_order) AS min_days,
MAX(days_to_first_order) AS max_days
FROM (
SELECT
u.id,
MIN(o.created_at::date - u.created_at::date) AS days_to_first_order
FROM users u
JOIN orders o ON o.user_id = u.id AND o.status = 'completed'
GROUP BY u.id
) sub;
7. A/B тест: базовый анализ
-- Сравнить конверсию в двух группах A/B теста
SELECT
experiment_group,
COUNT(DISTINCT user_id) AS users,
COUNT(DISTINCT CASE WHEN converted THEN user_id END) AS converted_users,
ROUND(
COUNT(DISTINCT CASE WHEN converted THEN user_id END)::numeric
/ COUNT(DISTINCT user_id) * 100, 2
) AS conversion_rate
FROM ab_test_users
WHERE experiment_id = 42
GROUP BY experiment_group;
8. Падение/рост метрики: быстрая диагностика
-- Сравнить текущую неделю с прошлой
SELECT
metric_name,
this_week,
last_week,
ROUND((this_week - last_week)::numeric / NULLIF(last_week, 0) * 100, 1) AS change_pct
FROM (
SELECT
'DAU' AS metric_name,
COUNT(DISTINCT CASE WHEN event_date >= CURRENT_DATE - 7 THEN user_id END) AS this_week,
COUNT(DISTINCT CASE WHEN event_date BETWEEN CURRENT_DATE - 14 AND CURRENT_DATE - 8 THEN user_id END) AS last_week
FROM user_events
WHERE event_date >= CURRENT_DATE - 14
UNION ALL
SELECT
'Orders',
COUNT(CASE WHEN created_at >= CURRENT_DATE - 7 THEN 1 END),
COUNT(CASE WHEN created_at BETWEEN CURRENT_DATE - 14 AND CURRENT_DATE - 8 THEN 1 END)
FROM orders
WHERE status = 'completed' AND created_at >= CURRENT_DATE - 14
) metrics;
9. Сегментация пользователей
-- Разбить пользователей по уровню активности за 30 дней
SELECT
segment,
COUNT(*) AS users,
ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER () * 100, 1) AS pct
FROM (
SELECT
user_id,
CASE
WHEN session_count = 0 THEN 'inactive'
WHEN session_count BETWEEN 1 AND 3 THEN 'low'
WHEN session_count BETWEEN 4 AND 10 THEN 'medium'
ELSE 'high'
END AS segment
FROM (
SELECT user_id, COUNT(*) AS session_count
FROM user_sessions
WHERE started_at >= CURRENT_DATE - 30
GROUP BY user_id
) s
) seg
GROUP BY segment
ORDER BY users DESC;
10. Топ-страницы/экраны с падением вовлечённости
-- Страницы с наибольшим bounced (вышли без действий)
SELECT
page_path,
COUNT(*) AS visits,
COUNT(*) FILTER (WHERE next_page IS NULL) AS bounces,
ROUND(
COUNT(*) FILTER (WHERE next_page IS NULL)::numeric
/ COUNT(*) * 100, 1
) AS bounce_rate
FROM page_views
WHERE visit_date >= CURRENT_DATE - 7
GROUP BY page_path
HAVING COUNT(*) > 100 -- только страницы с достаточным трафиком
ORDER BY bounce_rate DESC
LIMIT 20;
Советы по работе с данными
1. Проверяйте BASE RATE: конверсия 5% хорошо или плохо — зависит от контекста.
2. Смотрите медиану, не только среднее:
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY value) AS median
3. Всегда фильтруйте по периоду — случайные данные из прошлых тестов загрязняют метрики.
4. NULLIF для деления:
ROUND(a::numeric / NULLIF(b, 0) * 100, 1)
5. Используйте FILTER вместо CASE WHEN COUNT:
-- Вместо COUNT(CASE WHEN ... THEN 1 END)
COUNT(*) FILTER (WHERE condition)
Итог
10 паттернов покрывают большинство продуктовых задач:
- DAU/MAU — базовые метрики
- Воронка — где теряем пользователей
- Retention — удерживаем ли
- A/B тест — сравнение вариантов
- Сегментация — кто наши пользователи
- Диагностика — почему упала метрика
SQL — ваш прямой доступ к данным без ожидания аналитика. Начните с этих запросов и адаптируйте под свою схему данных.