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

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

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

КПКейсПрактика11 июля 2026 г.20 мин

Запрос от команды обычно звучит не как упражнение по 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.
  • Показывать цифру без периода, знаменателя и определения события.

Проверка SQL-метрики перед дашбордом

Перед публикацией запроса пройди короткий review. Назови зерно входа и выхода, период, timezone, событие, знаменатель и исключения. Затем сравни результат с независимым расчётом на маленьком срезе. Если запрос использует JOIN, проверь строки до и после соединения; если ratio — отдельно покажи числитель и знаменатель.

Попроси коллегу прочитать не только финальную цифру, но и первые строки промежуточного результата. Хороший review ловит не синтаксическую ошибку, а подмену сущности: события вместо пользователей, текущий день вместо полного или технический app_open вместо core action.

Сохрани запрос вместе с описанием метрики и датой изменения. SQL живёт дольше ноутбука, поэтому отсутствие контекста быстро превращает рабочий отчёт в набор непонятных фильтров.

SQL review перед публикацией
ПунктКонтрольный вопрос
grainчто означает одна строка результата?
periodобе границы заданы явно и период полный?
identityсчитаем user_id, account_id или события?
joinне размножились ли факты?
interpretationкакое решение меняет цифра?

Сквозной пример: недельная сводка продукта

Соберём теперь всё вместе на учебной базе «Маяка» — той же, что в SQL-тренажёре. Первая неделя июня, три таблицы, восемнадцать зарегистрированных пользователей. Менеджер просит «цифры по продукту за неделю», и под этим обычно понимают пять метрик: регистрации, активные пользователи, активация, платящие и выручка.

Маленький масштаб здесь помогает. Каждое число можно пересчитать вручную, а значит любое расхождение — это ошибка запроса, а не «особенности данных». На рабочих объёмах приём тот же, только сверяться придётся с независимым источником вроде биллинга.

Что показывает база «Маяк» за 1–7 июня
МетрикаЗначениеКак считается
Регистрации18строк в users за период
DAU по дням3 · 5 · 6 · 6 · 7 · 7 · 4уникальные user_id с событием в этот день
Активация10 из 18 (55,6%)создали workspace хотя бы раз
Плательщики8 из 18 (44,4%)уникальные user_id в payments
Выручка232сумма amount в payments

Пять метрик — пять разных знаменателей

Соблазн собрать всё одним запросом велик, но метрики живут на разном зерне. Регистрации считаются по пользователям, DAU — по парам «пользователь + день», выручка — по платежам. Смешать их в одном GROUP BY — прямой путь к числу, которое невозможно объяснить.

Практичнее собрать по одному запросу на зерно и только потом сводить результаты. Ниже — расчёт активности по дням: обратите внимание, что здесь считаются не события, а люди, поэтому пользователь с пятью открытиями приложения даёт единицу, а не пятёрку.

Числа в этой сводке не обязаны складываться в аккуратную воронку. Отчёт создали шесть пользователей, а заплатили восемь — среди плательщиков есть двое, которые до отчёта не дошли. Это не ошибка расчёта, а факт о продукте: оплата не стоит строго за тем шагом, который команда считает главным.

Ряд активности тоже стоит читать осторожно. За неделю DAU идёт 3, 5, 6, 6, 7, 7 и затем 4 — и последнее число само по себе выглядит как обвал на 43%. На деле база собрана по 7 июня включительно, а рост первых дней объясняется тем, что продукт только набирал аудиторию: каждый день добавлялись новые регистрации. Ни одно из этих чисел не является продуктовым выводом без ответа на два вопроса: полон ли последний день и сравниваем ли мы дни с одинаковым составом аудитории.

Активные пользователи по дням: люди, а не события
SELECT
  CAST(event_time AS DATE)  AS activity_date,
  count(*)                  AS events_total,
  count(DISTINCT user_id)   AS dau
FROM events
WHERE event_time >= TIMESTAMP '2026-06-01 00:00:00'
  AND event_time <  TIMESTAMP '2026-06-08 00:00:00'
GROUP BY activity_date
ORDER BY activity_date;

Одна витрина на пользователя вместо пяти отчётов

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

Такой слой решает сразу три задачи. Период и правила отбора задаются один раз. Любую метрику можно разложить по каналу, стране или дате регистрации, ничего не переписывая. И появляется место для проверок: сумма выручки из витрины обязана совпадать с суммой в payments, а число строк — с числом пользователей.

Витрина пользователя и метрики поверх неё
WITH user_facts AS (          -- зерно: один пользователь
  SELECT
    u.user_id,
    u.channel,
    u.signup_date,
    max(CASE WHEN e.event_name = 'workspace_created' THEN 1 ELSE 0 END) AS activated,
    coalesce(p.revenue, 0)                                             AS revenue
  FROM users u
  LEFT JOIN events e ON e.user_id = u.user_id
  LEFT JOIN (
    SELECT user_id, sum(amount) AS revenue
    FROM payments GROUP BY user_id
  ) p ON p.user_id = u.user_id
  GROUP BY u.user_id, u.channel, u.signup_date, p.revenue
)
SELECT
  channel,
  count(*)                                   AS signups,
  sum(activated)                             AS activated_users,
  count(*) FILTER (WHERE revenue > 0)        AS payers,
  sum(revenue)                               AS revenue,
  round(100.0 * sum(activated) / count(*), 1) AS activation_rate
FROM user_facts
GROUP BY channel
ORDER BY revenue DESC;

Метрика полезнее, когда её можно разложить

Одно число редко подсказывает действие. «Выручка 232» не говорит, что делать, а вот та же выручка, разложенная на плательщиков и средний чек, уже указывает направление: работать над конверсией в оплату или над тарифом.

Правило простое: любую денежную метрику раскладывайте на количество и величину, а любую долю показывайте вместе с числителем и знаменателем. На учебной базе выручка распадается на восемь плательщиков и средний чек 29 — и сразу видно, что при восемнадцати регистрациях запас роста лежит в конверсии, а не в цене.

Тот же приём работает с падениями. Если выручка снизилась, первый вопрос — это меньше плательщиков или меньше средний чек. Ответ на него меняет, кто пойдёт разбираться: продуктовая команда или те, кто отвечает за тарифы.

Выручка, разложенная на количество и величину
SELECT
  count(DISTINCT user_id)                      AS payers,
  count(*)                                     AS payments_count,
  sum(amount)                                  AS revenue,
  round(sum(amount) / count(DISTINCT user_id), 2) AS revenue_per_payer,
  round(sum(amount) / count(*), 2)             AS avg_payment
FROM payments
WHERE paid_at >= DATE '2026-06-01'
  AND paid_at <  DATE '2026-06-15';

Три подмены, из-за которых метрика тихо врёт

Ошибки в продуктовых метриках редко выглядят как ошибки. Запрос выполняется, число получается, график рисуется — и только через месяц выясняется, что считали не то.

Первая подмена — события вместо людей. count(*) по таблице событий отвечает на вопрос «сколько действий», а не «сколько пользователей». Разница растёт вместе с вовлечённостью: чем активнее аудитория, тем сильнее расходятся два числа, и тем убедительнее выглядит рост, которого нет.

Вторая — не тот период. У пользователя есть дата регистрации, у события — дата действия, у платежа — дата оплаты. «Пользователи за июнь» может означать зарегистрировавшихся в июне, активных в июне или заплативших в июне: три разных множества, которые почти никогда не совпадают. Формулировка «за июнь» без указания поля — источник половины споров на продуктовых встречах.

Третья — незакрытый период и мусорные аккаунты. Последний день почти всегда неполный, а внутренние и тестовые пользователи живут в тех же таблицах, что и настоящие. На маленьком продукте десяток тестовых аккаунтов способен изменить конверсию на проценты.

Как проверить себя за минуту
ПодменаСимптомПроверка
События вместо людейметрика растёт быстрее аудиториисравнить count(*) и count(DISTINCT user_id)
Не то поле датычисла не сходятся с соседним отчётомназвать поле периода вслух: signup_date или event_time
Неполный последний деньпадение в конце графикапосмотреть максимальное время события в периоде
Тестовые аккаунтыстранные значения у отдельных пользователейисключить внутренние домены и служебные id
Дубли событийактивность выше, чем возможна физическипроверить повторы по user_id + событие + время

Определение метрики стоит записать один раз

SQL — это исполняемая версия определения, но само определение живёт вне кода. Если оно не записано, каждый аналитик восстанавливает его заново, и через полгода в компании существует три «активации», которые не сходятся между собой.

Рабочий минимум занимает пять строк: что считаем (единица), за какой период и по какому полю, кого исключаем, каким событием подтверждается факт и что считается знаменателем. Такое описание помещается рядом с запросом в комментарии и переживает смену дашборда.

Отдельно стоит фиксировать момент изменения определения. Если вчера активацией считалось открытие приложения, а сегодня — создание рабочего пространства, ряд метрики нельзя сравнивать через эту границу. Пометка на графике в день смены правила стоит дешевле, чем разбор «почему в марте всё упало» через год.

  • Единица счёта: пользователь, сессия, событие или платёж.
  • Период и поле, по которому он берётся.
  • Кто исключён: тестовые, внутренние, боты.
  • Событие-подтверждение факта и окно, в котором оно засчитывается.
  • Знаменатель: от кого считается доля.
  • Дата последнего изменения определения.

Что сказать рядом с числами

Готовая сводка — половина работы. Вторая половина в том, чтобы честно назвать её границы, иначе метрику начнут использовать не так, как она посчитана.

Минимум, который стоит приложить: период и по какому полю он берётся, что считается активным действием, исключены ли тестовые аккаунты и насколько велика выборка. На учебной базе последний пункт решающий: 55,6% активации на восемнадцати пользователях — это десять человек, и один новый активированный сдвинет процент почти на шесть пунктов. Показывать такой процент без числителя и знаменателя — значит приглашать команду принять решение по шуму.

И отдельно — про свежесть. Последний день периода почти всегда неполный: данные ещё догружаются. Его либо исключают из сравнения, либо помечают явно, но никогда не подают как падение продукта.

От запроса к тренажёру и рабочему навыку

Лучший способ закрепить SQL — изменить один запрос и заранее предсказать, что произойдёт. Убери distinct и объясни рост числа строк, перенеси условие из ON в WHERE и найди потерянных пользователей, замени полный месяц на текущий и отметь несопоставимость. Такие эксперименты создают понимание, а не только память о синтаксисе.

В тренажёре можно идти от простой таблицы users к events, JOIN, CTE, воронкам и когортам. Для каждой задачи сначала напиши словами, что должна означать строка результата, затем собери запрос и проверь его на учебной схеме. Ошибка становится частью обучения, если после неё ты формулируешь правило.

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

  • Изменить один фильтр и предсказать эффект.
  • Проверить запрос на маленьком срезе.
  • Сравнить SQL-результат с независимым числом.
  • Описать зерно и знаменатель рядом с графиком.
  • Закрепить навык в SQL-тренажёре.

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

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

Продолжить чтение
Вся библиотека