SQLLab
Все статьи

SQL для продакт-менеджера: 10 запросов которые нужно знать

Практические SQL-запросы для продактов: DAU/MAU, конверсия, retention, воронка, топ-фичи, когортный анализ. Без лишней теории — только рабочие паттерны.

17 марта 2026 г.·5 мин чтения·

Продакт-менеджеру не нужно знать 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 — ваш прямой доступ к данным без ожидания аналитика. Начните с этих запросов и адаптируйте под свою схему данных.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →