Мы возвращаемся в Пограничье. В боссе блока C мы считали деньги магазина, потом были магазин настолок, доставка и стратегия. Теперь у нас в руках JOIN, даты и окна, и пора собрать главные продуктовые метрики игры. На этот раз ставка выше одного заказчика: тот же отчёт прочтёт Фрея, и по нему «Монетка» решит, брать ли агентство на свой тест. Начнём с метрики, о которой спрашивают первой: возвращаются ли новички.
Мерный стакан для новичков
Рост числа игроков ничего не говорит о том, держит ли игра людей. Его может давать реклама: каждую неделю приходят сотни новых и тут же уходят. Это как ведро с дырой: если лить воду быстро, уровень растёт, хотя вода утекает.
Дыру в ведре меряет retention. Определение мы дали в уроке про метрики: retention Dn это доля когорты, которая зашла в игру ровно на n-й день после регистрации, день регистрации нулевой. Когорты мы уже строили в уроке про «Гонца», теперь применим их к игре.
Шаг 1: день жизни игрока
Для каждой сессии считаем, на какой день жизни игрока она пришлась. Разность двух дат в PostgreSQL это целое число дней:
SELECT p.id,
s.started_at::date - p.registered_at::date AS day_no
FROM players p
JOIN sessions s ON s.player_id = p.id
ORDER BY p.id, day_no
LIMIT 4;
id | day_no
----+--------
1 | 0
2 | 0
2 | 6
2 | 8
Игрок 1 сыграл только в день регистрации. Игрок 2 вернулся на шестой и восьмой день. Даты берём в UTC, как они лежат в базе: регистрация и сессии должны жить в одном календаре.
Шаг 2: D1, D7, D30 по когортам
Игрок «вернулся на день n», если у него есть хотя бы одна сессия с day_no = n. Это проверка существования, поэтому подходит EXISTS. Тестовые аккаунты студии с почтой @studio.dev выбрасываем, как в уроке про чистку строк.
WITH reg AS (
SELECT id, registered_at::date AS d0,
date_trunc('month', registered_at)::date AS cohort
FROM players
WHERE email NOT LIKE '%@studio.dev'
AND registered_at < '2026-06-01'
),
act AS (
SELECT DISTINCT player_id, started_at::date AS d FROM sessions
)
SELECT cohort,
count(*) AS players,
round(100.0 * count(*) FILTER (WHERE EXISTS (
SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 1)) / count(*), 1) AS d1,
round(100.0 * count(*) FILTER (WHERE EXISTS (
SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 7)) / count(*), 1) AS d7,
round(100.0 * count(*) FILTER (WHERE EXISTS (
SELECT 1 FROM act WHERE act.player_id = reg.id AND act.d = reg.d0 + 30)) / count(*), 1) AS d30
FROM reg
GROUP BY cohort
ORDER BY cohort;
cohort | players | d1 | d7 | d30
------------+---------+------+------+-----
2026-01-01 | 539 | 23.7 | 18.4 | 8.9
2026-02-01 | 587 | 23.5 | 21.3 | 8.0
2026-03-01 | 842 | 21.7 | 18.4 | 6.8
2026-04-01 | 694 | 22.3 | 17.4 | 8.9
2026-05-01 | 703 | 21.5 | 16.4 | 6.7
Почему только до 1 июня? Игрок, пришедший 20 июня, физически не мог зайти на 30-й день: данные кончаются 30 июня. Если оставить июнь, его D30 будет нулём, и график покажет обвал, которого нет. Мы уже встречали это в уроке про когорты: недозревшая когорта не годится для сравнения.
Мартовская когорта самая большая, 842 игрока: в марте шла кампания у блогеров. Если разложить игроков по каналу, у пришедших от блогеров D30 всего 3,6%, а у остальных каналов 8,4-9%. Блогеры приводят много людей, которые быстро уходят. Это ещё всплывёт в юнит-экономике.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.