КПКейсПрактика
Все материалы
Продуктовая аналитика

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

С чего начать в SQL, как читать grain, фильтровать события, считать уникальных пользователей, соединять таблицы и превращать результат в вывод по продукту.

11 июля 2026 г.11 мин
SQL
Аналитика
Метрики
SQL для аналитика
SELECT
WHERE
GROUP BY
JOIN
CTE
DAU
retention
гайд · старт · SQL
Иллюстрация с надписью SQL: два аналитика смотрят на запрос и таблицу продуктовых данных.

Запрос от команды обычно звучит не как упражнение по SQL. “Покажи DAU по каналам”, “найди пользователей, которые не дошли до оплаты”, “почему retention просел в июне” — это продуктовые вопросы. SQL нужен, чтобы превратить их в проверяемый расчёт, но хороший аналитик начинает не с клавиатуры, а с определения того, какие строки он собирается считать.

Коротко

SQL для аналитика — это не набор команд, которые нужно запомнить отдельно. Это последовательность решений: определить единицу анализа, выбрать таблицу и grain, отфильтровать нужные строки, агрегировать их и проверить, что результат отвечает на исходный вопрос.

  • Сначала назови объект подсчёта: пользователь, аккаунт, событие, заказ или сессия.
  • Проверь grain таблицы: одна строка может быть пользователем, событием или платежом.
  • Используй `count(distinct user_id)`, когда вопрос про уникальных людей.
  • После JOIN сравни количество строк и количество уникальных пользователей.
  • Заканчивай запрос не цифрой, а следующим продуктовым вопросом.

Из вопроса в запрос

Перед SQL полезно переписать задачу одним предложением. Например: “Сколько уникальных пользователей сделали core action в каждый календарный день за последние 30 полных дней?” В этой фразе уже есть метрика, единица, событие, период и границы окна.

Если такой формулировки нет, запрос может быть технически правильным, но аналитически бесполезным. Он посчитает что-то из базы, а команда всё равно не поймёт, какое решение принимать.

Пять вопросов перед первой строкой SQL
ВопросЧто зафиксировать
Что считаем?user_id, account_id, event_id, order_id или session_id
Где лежит факт?users, events, payments, orders или отдельный view
Что значит активность?конкретный core event, а не любое техническое событие
За какой период?полные дни, недели, месяцы и продуктовая таймзона
Как проверим?контрольный сегмент, размер базы и пример строк

Начни с grain таблицы

Grain — это смысл одной строки. В users одна строка обычно описывает пользователя, в events — одно событие, в payments — одну денежную операцию. Если соединить events и payments по user_id, один пользователь может дать десятки строк. Это нормально для промежуточного результата, но опасно, если потом считать строки как пользователей.

Простейшая проверка: до каждого JOIN посмотри `count(*)`, `count(distinct user_id)` и несколько строк вручную. Разница между ними часто сразу показывает, где в расчёте появилась мультипликация.

Минимальная схема для продуктовых задач
ТаблицаОдна строкаЧто считаем
usersодин пользовательрегистрация, канал, платформа, тариф
eventsодно событиеактивность, шаг воронки, core action
paymentsодин платёж или возвратвыручка, payer, ARPU, LTV

SELECT, WHERE и GROUP BY на примере DAU

Первый полезный запрос можно собрать из трёх частей. `SELECT` выбирает дату и метрику, `WHERE` оставляет нужные события и период, `GROUP BY` собирает строки по дню. Важно считать уникальных пользователей: один человек может выполнить core action несколько раз.

DAU по календарным дням на core events
select
  date_trunc('day', occurred_at)::date as day,
  count(distinct user_id) as dau
from events
where event_name in ('report_viewed', 'report_created')
  and occurred_at >= current_date - interval '30 days'
  and occurred_at < current_date
group by 1
order by 1;

COUNT DISTINCT защищает от лишних событий

Если считать `count(*)`, получится число событий, а не DAU. Такой показатель может вырасти потому, что активные пользователи стали кликать чаще, хотя сам охват не изменился. Для вопроса “сколько людей” нужен `count(distinct user_id)`, а для вопроса “сколько действий” — обычный `count(*)`.

Это не значит, что `distinct` нужно добавлять в каждый запрос без размышлений. Он меняет смысл метрики и может скрыть дубли идентичности. Если часть людей анонимна, сначала нужно договориться, как склеиваются anonymous_id и user_id.

Одна таблица событий — разные метрики
ВыражениеЧто отвечает
count(*)Сколько строк или событий попало в выборку?
count(distinct user_id)Сколько уникальных пользователей сделали действие?
count(distinct session_id)Сколько сессий содержали действие?
count(*) filter (where ...)Сколько строк прошло отдельное условие?

JOIN: место, где метрика часто ломается

JOIN нужен, когда событие нужно обогатить свойствами пользователя или связать с оплатой. Например, events хранит действие, а users — канал. Но после соединения нужно помнить о grain: у одного пользователя может быть много событий, поэтому `count(*)` после JOIN не станет количеством пользователей.

Для обычной сегментации можно присоединить users к событиям и оставить `count(distinct e.user_id)`. Если соединяются две таблицы с несколькими строками на пользователя, сначала агрегируй каждую сторону до нужного grain, а уже потом соединяй.

DAU по каналу с контролем уникальных пользователей
select
  u.channel,
  count(distinct e.user_id) as active_users
from events e
join users u on u.user_id = e.user_id
where e.event_name in ('report_viewed', 'report_created')
  and e.occurred_at >= current_date - interval '30 days'
  and e.occurred_at < current_date
group by 1
order by active_users desc;

CTE превращает длинный запрос в последовательность шагов

Когда в запросе появляется несколько смысловых действий, CTE помогает назвать их. Например, сначала собрать активных пользователей, потом присоединить их канал и только затем получить итоговую таблицу. Это не делает расчёт автоматически правильным, но облегчает проверку каждого промежуточного результата.

Каждый CTE должен отвечать на один понятный вопрос. Если блок называется `active_users`, в нём не должно быть логики оплаты, retention и рекламных расходов одновременно.

Два шага: активные пользователи и сегментация
with active_users as (
  select distinct user_id
  from events
  where event_name in ('report_viewed', 'report_created')
    and occurred_at >= current_date - interval '30 days'
    and occurred_at < current_date
),
active_by_channel as (
  select u.channel, count(*) as active_users
  from active_users a
  join users u using (user_id)
  group by 1
)
select *
from active_by_channel
order by active_users desc;

SQL-результат ещё не является выводом

Запрос сообщает, что paid social дал 42% активных пользователей. Но без сравнения с размером канала, retention и качеством core action это только описание объёма. Аналитический вывод появляется, когда результат связывается с контекстом и следующим решением.

Полезный формат финального абзаца: “что изменилось → где именно → какая гипотеза → что проверяем дальше”. Такой формат одинаково работает для DAU, воронки, retention и монетизации.

Как превратить результат запроса в решение
РезультатСледующий вопрос
DAU снизилсяПадение пришло из новых или вернувшихся пользователей?
Канал дал больше активныхКак изменились retention и core action rate этого канала?
После JOIN стало больше строкЭто реальные события или мультипликация one-to-many?
Выручка вырослаИзменились payer conversion, ARPPU или состав пользователей?

Частые ошибки в первых запросах

Большинство проблем в начале связано не с синтаксисом. Запрос запускается, но считает не ту сущность, смешивает незрелые периоды или использует техническое событие вместо действия, которое несёт ценность.

  • Считать строки events как пользователей.
  • Соединять две one-to-many таблицы и не проверять размер результата.
  • Использовать `select *` в итоговом отчёте вместо явного списка полей.
  • Сравнивать текущий неполный день с полными историческими днями.
  • Не фиксировать таймзону и границу календарного дня.
  • Считать любое открытие приложения meaningful activity.
  • Показывать цифру без периода, знаменателя и определения события.

Что изучать дальше

После базовой выборки следующий уровень — научиться собирать реальные продуктовые расчёты. Сначала закрепи фильтрацию и уникальных пользователей на DAU, потом переходи к воронке, когортам, retention и деньгам. Оконные функции оставь на момент, когда уже уверенно понимаешь grain и агрегации.