← все задачи
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) и, возможно, отдельная витрина
Следующая задача
Почему запрос медленный — Вместо написания запроса — разбор чужого: что не так и как чинить.