← все задачи

SQL · задача 7 из 10

Вернувшиеся пользователи

Продвинутый 20 минут self joinкогортыDISTINCT

Условие

Таблица events (user_id, event_at). Посчитайте, сколько пользователей, впервые пришедших в каждый день, вернулись на следующий день.

Что требуется

  • Когорта — день первого визита пользователя
  • Вернувшийся — тот, у кого есть событие на следующий календарный день
  • Результат: день когорты, размер когорты, сколько вернулось, доля

Пример

cohort_day   users   returned   retention
2026-09-01     100         40      0.40
2026-09-02      80         28      0.35

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Возврат именно на следующий день или в течение семи дней?
  • Считаем по календарным дням или по 24 часам от первого визита?
  • Учитываем ли служебные события — например, фоновые пинги приложения?
Показать решение Скрыть решение

Решение

WITH first_visit AS (
    SELECT
        user_id,
        MIN(event_at)::date AS cohort_day
    FROM events
    GROUP BY user_id
),
activity AS (
    SELECT DISTINCT user_id, event_at::date AS active_day
    FROM events
)
SELECT
    f.cohort_day,
    COUNT(*) AS users,
    COUNT(a.user_id) AS returned,
    ROUND(COUNT(a.user_id)::numeric / COUNT(*), 2) AS retention
FROM first_visit f
LEFT JOIN activity a
       ON a.user_id = f.user_id
      AND a.active_day = f.cohort_day + 1
GROUP BY f.cohort_day
ORDER BY f.cohort_day;

Почему так

Почему COUNT(*) и COUNT(a.user_id) в одном запросе

  • COUNT(*) считает все строки группы — это размер когорты
  • COUNT(колонка) пропускает NULL, а после LEFT JOIN они как раз у невернувшихся
  • Разница между COUNT(*) и COUNT(col) — любимый вопрос, и здесь он решает задачу

Зачем DISTINCT в активности

  • У пользователя за день десятки событий: без DISTINCT LEFT JOIN размножит строки когорты
  • Размноженные строки испортят и COUNT(*), и долю — метрика станет больше единицы
  • Сведение к паре «пользователь, день» делает соединение однозначным

Почему LEFT JOIN, а не INNER

  • INNER выкинул бы невернувшихся, и знаменатель посчитался бы только по вернувшимся — retention всегда 100%
  • Нам нужна вся когорта в знаменателе, поэтому соединение только добавляет признак возврата
  • Ошибка со знаменателем в метриках — самая дорогая: цифра выглядит правдоподобно и никто не перепроверяет

Что спросят дальше

  • Спросят про N-й день: cohort_day + N, а для кривой удержания — generate_series по смещениям
  • Спросят про «вернулись в течение недели»: BETWEEN +1 AND +7 и обязательно DISTINCT, иначе двойной счёт
  • Спросят про объём: events на десятки миллионов строк — тут нужен индекс (user_id, event_at) и, возможно, отдельная витрина

Следующая задача

Почему запрос медленный — Вместо написания запроса — разбор чужого: что не так и как чинить.