Бран · продюсер «Хроник Пограничья»
Всё, переезжаем. С понедельника события и сессии живут в ClickHouse, PostgreSQL выключаем через месяц. Перепиши отчёты, которые ты мне строил: retention, воронку, DAU и липкость. Совет директоров сверит цифры со старыми, и если поедет хоть одна, виноват будешь ты.
В прошлом уроке ты разобрал функции ClickHouse по одной. Сегодня переводим отчёты курса целиком и после каждого сверяем числа со старыми. Все запросы ниже проверены на ClickHouse с данными «Пограничья» и «Эпохи Корон»: числа совпали с уроками, где мы их считали.
Перевод с проверкой
Переводчики договоров делают обратный перевод: текст переводят на английский, другой человек переводит его обратно на русский, и смысл сверяют с оригиналом. Если «аренда на год» вернулась как «аренда навсегда», где-то ошибка.
С запросами так же. Перевод не готов, пока числа на старой и новой базе не совпали. Разница в одну десятую это не округление, а сигнал: где-то поменялся смысл. Ниже пять отчётов курса и что в каждом пришлось поменять.
Отчёт 1: DAU, MAU и липкость
Из урока про DAU и MAU, с чисткой ботов и тестовых аккаунтов:
WITH fake AS (
SELECT id FROM players WHERE email LIKE '%@studio.dev'
UNION DISTINCT
SELECT player_id FROM sessions WHERE ended_at - started_at > 20 * 3600
),
daily AS (
SELECT toDate(started_at) AS day, uniqExact(player_id) AS dau
FROM sessions WHERE player_id NOT IN fake
GROUP BY day
),
monthly AS (
SELECT toStartOfMonth(started_at) AS month, uniqExact(player_id) AS mau
FROM sessions WHERE player_id NOT IN fake
GROUP BY month
)
SELECT m.month, round(avg(d.dau), 1) AS avg_dau, any(m.mau) AS mau,
round(100 * avg(d.dau) / any(m.mau), 1) AS stickiness
FROM daily d
JOIN monthly m ON toStartOfMonth(d.day) = m.month
GROUP BY m.month
ORDER BY m.month;
Что поменялось:
- разность двух
DateTimeв ClickHouse это число секунд, а не интервал, поэтомуinterval '20 hours'стал20 * 3600; UNIONв ClickHouse пишут явно:UNION DISTINCTилиUNION ALL;count(DISTINCT …)сталuniqExact, а неuniq: сверяем до игрока;toStartOfMonthсразу даёт дату,::dateне нужен;100.0не нужен, деление и так дробное;mauне входит вGROUP BY, поэтому берём его черезany(m.mau): на месяц значение одно.
Майская липкость 16,5%, MAU 1021, средний DAU 169: сошлось с уроком 49 по всем шести месяцам.
Отчёт 2: воронка до и после релиза
Из урока про воронки: кто из начавших обучение его закончил, по платформам и периодам.
SELECT platform, after_release,
countIf(started) AS n_started,
countIf(completed) AS n_completed,
round(100 * n_completed / n_started, 1) AS rate
FROM (
SELECT p.id,
any(p.platform) AS platform,
any(p.registered_at) >= '2026-05-14' AS after_release,
max(e.name = 'tutorial_start') AS started,
max(e.name = 'tutorial_complete') AS completed
FROM events e
JOIN players p ON p.id = e.player_id
WHERE p.email NOT LIKE '%@studio.dev'
GROUP BY p.id
)
GROUP BY platform, after_release
ORDER BY after_release, platform;
bool_or стал max от условия: условие в ClickHouse это число 0 или 1, и максимум равен 1, если хоть одно событие подошло. count(*) FILTER стал countIf. Новые имена столбцов n_started и n_completed не случайны: ClickHouse разрешает ссылаться на псевдоним в соседних выражениях, и псевдоним started поверх столбца started легко запутает и тебя, и базу. Android после релиза 34,8%, как в уроке 48.
players здесь стоит справа от JOIN, и это правильно. Почему, объясняю в чек-листе переезда ниже.
Отчёт 3: когортный треугольник
Из урока про retention, недели жизни игрока:
SELECT cohort_week,
uniqExact(r.id) AS players,
round(100 * uniqExactIf(r.id, week_no = 1) / uniqExact(r.id), 1) AS w1,
round(100 * uniqExactIf(r.id, week_no = 2) / uniqExact(r.id), 1) AS w2
FROM (
SELECT id, toMonday(registered_at) AS cohort_week
FROM players
WHERE email NOT LIKE '%@studio.dev'
) r
LEFT JOIN (
SELECT s.player_id,
intDiv(toMonday(s.started_at) - toMonday(p.registered_at), 7) AS week_no
FROM sessions s
JOIN players p ON p.id = s.player_id
) a ON a.player_id = r.id
GROUP BY cohort_week
ORDER BY cohort_week;
Две правки, и обе тихие. date_trunc('week', …) стал toMonday, потому что toStartOfWeek по умолчанию начинает неделю с воскресенья. А / 7 стал intDiv(…, 7). Здесь разность понедельников всегда кратна семи, и обычное деление дало бы тот же номер недели. Но стоит посчитать неделю жизни от дня регистрации, (toDate(started_at) - toDate(registered_at)) / 7, и / вернёт 0,857 вместо 0. Условие week_no = 1 тогда поймает только тех, кто зашёл ровно на седьмой день: 615 игроков вместо 1212. Неделя 1 молча превратится в D7.
У когорты 5 января 45,1% на первой неделе и 29,6% на второй, как в уроке 47.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.