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

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

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

КейсПрактика11 июля 2026 г.обновлено 2 октября 2026 г.18 мин

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

Запросы статьи выполняются на учебной базе «Маяк» в песочнице симулятора. Выгрузка в ней закрыта 30 августа 2026 года, поэтому 30 полных дней заданы датами — с 31 июля по 29 августа; в рабочем запросе правой границей была бы current_date. Core action здесь — создание рабочего пространства или отчёта. В учебной базе оба действия случаются у человека один раз, в день регистрации, так что этот DAU считает только новичков, создавших пространство: 47 из 70 пришедших 31 июля, 22 из 38 — 29 августа.

DAU по календарным дням на core events
select
  cast(event_time as date) as day,
  count(distinct user_id) as dau
from events
where event_name in ('workspace_created', 'report_created')
  and event_time >= timestamp '2026-07-31 00:00:00'
  and event_time < timestamp '2026-08-30 00:00:00'
group by 1
order by 1;

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

Если считать count(*), получится число событий, а не DAU: в запросе выше за 31 июля это 79 событий на 47 человек. Такой показатель может вырасти потому, что активные пользователи стали кликать чаще, хотя сам охват не изменился. Для вопроса «сколько людей» нужен 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, а уже потом соединяйте.

Активные за 30 дней по каналу с контролем уникальных пользователей
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 ('workspace_created', 'report_created')
  and e.event_time >= timestamp '2026-07-31 00:00:00'
  and e.event_time < timestamp '2026-08-30 00:00:00'
group by 1
order by active_users desc, channel;

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

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

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

Два шага: активные пользователи и сегментация
with active_users as (
  select distinct user_id
  from events
  where event_name in ('workspace_created', 'report_created')
    and event_time >= timestamp '2026-07-31 00:00:00'
    and event_time < timestamp '2026-08-30 00:00:00'
),
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, channel;

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

Запрос сообщает, что organic дал 41% активных пользователей: 470 из 1 136. Но без сравнения с размером канала, 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какое решение меняет цифра?

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

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

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

Что показывает база «Маяк» за 24–30 августа
МетрикаЗначениеКак считается
Регистрации471строк в users с signup_date за период; за 30 августа регистраций в выгрузке нет
DAU по дням484 · 529 · 568 · 556 · 586 · 530 · 223уникальные user_id с событием в этот день; 30 августа записано до 12:59
Активация265 из 471 (56,3%)зарегистрированные за неделю, создавшие workspace
Плательщики174уникальные user_id в payments с paid_at за период
Выручка4 286 $сумма amount в payments с paid_at за период

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

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

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

Числа в этой сводке не обязаны складываться в аккуратную воронку. Из 174 заплативших за неделю только 10 на этой же неделе зарегистрировались, а отчёт создавали 73 — остальные 101 до отчёта не дошли. Это не ошибка расчёта, а свойство учебных данных: оплата в них не привязана к отчёту. В своём продукте проверьте, стоит ли оплата за шагом, который команда считает главным.

Ряд активности тоже стоит читать осторожно. За неделю DAU идёт 484, 529, 568, 556, 586, 530 и затем 223 — и последнее число само по себе выглядит как обвал на 58%. На деле события 30 августа обрываются в 12:59: день неполный. А суббота 29-го ниже пятницы в основном потому, что в выходной меньше новичков: 38 регистраций против 88. Ни одно из этих чисел не является продуктовым выводом без ответа на два вопроса: полон ли последний день и сравниваем ли мы дни с одинаковым составом аудитории.

Активные пользователи по дням: люди, а не события
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-08-24 00:00:00'
  AND event_time <  TIMESTAMP '2026-08-31 00:00:00'
GROUP BY activity_date
ORDER BY activity_date;

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

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

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

Витрина пользователя и метрики поверх неё
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;

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

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

Правило простое: любую денежную метрику раскладывайте на количество и величину, а любую долю показывайте вместе с числителем и знаменателем. На учебной базе выручка недели распадается на 174 плательщика и средний платёж 24,63. Тарифов три — 19, 29 и 39 долларов, — поэтому средний платёж меняется только вместе с их составом, а 174 платежа — это 99 первых оплат и 75 продлений: привлечение и удержание. За неделю никто не платит дважды, поэтому выручка на плательщика равна среднему платежу; уберите условие по дате — и на всей базе получится 33,60 против 24,49.

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

Выручка, разложенная на количество и величину
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-08-24'
  AND paid_at <  DATE '2026-08-31';

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

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

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

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

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

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

Событие «оплата нажата» — ещё не оплаченный заказ

В учебной базе «Маяка» таблица payments уже приведена к факту: одна строка — один состоявшийся платёж. В ней 1 251 платёж, 912 плательщиков, 30 639 долларов. В продуктовой базе такой таблицы может не быть вовсе. Вместо неё лежит поток payment_events, где живут payment_started, payment_retry, payment_failed и payment_success. Условный пример: 12 000 строк за месяц на 4 100 разных payment_id. Посчитать «оплаты» как count(*) по этому потоку — значит завысить их почти втрое.

После отбрасывания повторных доставок и строк без payment_id из 12 000 остаётся 9 300 событий. Это по-прежнему 4 100 платежей, то есть в среднем 2,3 события на один платёж, и такое отношение нормально: попытка, повтор после отказа банка, успех. Три слоя с разными числами — сырые события, очищенные события и бизнес-факты — обязаны называться по-разному, иначе в разговоре «у нас четыре тысячи оплат» и «у нас двенадцать тысяч оплат» окажутся одинаково правдивыми.

Бизнес-факт собирается из четырёх обязательных частей: устойчивый ключ (payment_id, а не event_id), финальный статус, время, к которому факт привязан, и правило отмены. Ключ превращает поток в сущности, статус отвечает на вопрос «деньги получены или нет», а правило отмены говорит, что делать с возвратом. Технически это ROW_NUMBER() по ключу с сортировкой по времени убывающе и фильтр rn = 1. Потока событий оплаты в учебной базе нет, поэтому в запросе ниже он собран учебной вставкой: 11 строк, 4 платежа, из них оплачены 3 на 77 долларов.

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

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

Если команда не может проговорить словами, какие переходы состояний бывают у платежа и какой из них считается завершённым, строить KPI поверх событий рано. Сначала договорённость о факте и его владельце, потом дашборд — иначе метрика будет реагировать на обновление SDK и политику повторов, а не на поведение людей.

Что доказывает каждое событие платежа
СобытиеЧто доказываетГодится как факт оплаты
payment_startedпользователь открыл форму оплатынет, это шаг воронки
payment_retryпопытка повториласьнет, но объясняет число событий
payment_failedпровайдер отказалнет, но нужен в знаменателе конверсии
payment_successденьги полученыда, один раз на payment_id
refund_createdденьги возвращеныотдельный факт со своей датой
Из потока событий — один факт на платёж (поток задан в запросе)
-- учебная вставка: поток событий оплаты, в учебной базе его нет
WITH payment_events(event_id, payment_id, status, amount, server_at) AS (
  VALUES (1, 501, 'payment_started', 29, TIMESTAMP '2026-06-03 10:00:00'),
         (2, 501, 'payment_success', 29, TIMESTAMP '2026-06-03 10:01:00'),
         (3, 502, 'payment_started', 19, TIMESTAMP '2026-06-05 14:20:00'),
         (4, 502, 'payment_failed',  19, TIMESTAMP '2026-06-05 14:21:00'),
         (5, 502, 'payment_retry',   19, TIMESTAMP '2026-06-05 14:25:00'),
         (6, 502, 'payment_success', 19, TIMESTAMP '2026-06-05 14:26:00'),
         (7, 503, 'payment_started', 39, TIMESTAMP '2026-06-11 09:05:00'),
         (8, 503, 'payment_failed',  39, TIMESTAMP '2026-06-11 09:06:00'),
         (9, 504, 'payment_started', 29, TIMESTAMP '2026-06-18 18:40:00'),
         (10, 504, 'payment_success', 29, TIMESTAMP '2026-06-18 18:41:00'),
         (11, NULL, 'payment_started', 19, TIMESTAMP '2026-06-20 12:00:00')
),
ranked AS (                   -- зерно: событие платежа
  SELECT
    payment_id,
    status,
    amount,
    server_at,
    ROW_NUMBER() OVER (
      PARTITION BY payment_id ORDER BY server_at DESC, event_id DESC
    ) AS rn
  FROM payment_events
  WHERE payment_id IS NOT NULL
),
payment_facts AS (            -- зерно: один платёж, ожидаем 4 строки
  SELECT payment_id, status, amount, server_at
  FROM ranked
  WHERE rn = 1
)
SELECT
  count(*)                                                    AS payments,
  count(*) FILTER (WHERE status = 'payment_success')           AS paid,
  sum(amount) FILTER (WHERE status = 'payment_success')        AS revenue,
  round(100.0 * count(*) FILTER (WHERE status = 'payment_success')
        / count(*), 1)                                        AS success_rate
FROM payment_facts
WHERE server_at >= TIMESTAMP '2026-06-01 00:00:00'
  AND server_at <  TIMESTAMP '2026-07-01 00:00:00';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Продолжить чтение
Вся библиотека
Продуктовая аналитика17 июля 2026 г.17 мин
Три блока разной ширины, соединённые вертикальной линией.

CTE в SQL: WITH, несколько шагов, рекурсивный запрос и проверка каждого слоя

CTE и оператор WITH на задачах аналитика: как разложить расчёт конверсии на именованные шаги, проверить каждый контрольным числом, собрать календарь без пропусков через WITH RECURSIVE и понять, когда CTE материализуется, а когда его пора превратить во view.

Читать материал