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

SQL GROUP BY и COUNT: как считать пользователей по каналам и дням

Разбираем GROUP BY, COUNT и COUNT DISTINCT на задачах аналитика: пользователи по каналам, DAU по дням и фильтрация агрегатов через HAVING.

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

В первой SQL-выборке мы смотрели на отдельные строки. Но продуктовые вопросы почти всегда звучат как “сколько пользователей пришло из каждого канала”, “какой DAU был по дням” или “какие сегменты дают больше заказов”. Здесь в дело вступают GROUP BY и COUNT: они превращают длинный список строк в компактную сводку, с которой уже можно работать в отчёте.

Коротко

GROUP BY собирает строки в группы, а агрегатная функция считает или суммирует значение внутри каждой группы. Главное — заранее понять, какой должна быть одна строка результата: один канал, один день, одна когорта или одна связка “день × канал”.

  • `GROUP BY channel` вернёт по одной строке на канал.
  • `COUNT(*)` считает строки, а не обязательно уникальных людей.
  • `COUNT(DISTINCT user_id)` отвечает на вопрос про уникальных пользователей.
  • Все выбранные поля без агрегата должны попасть в `GROUP BY`.
  • `HAVING` фильтрует уже собранные группы, а `WHERE` — исходные строки.

Что именно должна означать строка

До запроса сформулируй зерно результата. Если вопрос “сколько регистраций было по каналам”, одна строка должна означать один канал. Если вопрос “как менялся DAU”, одна строка должна означать один день. Если добавить в SELECT ещё и платформу, зерно изменится на “канал × платформа”, и строк станет больше — это не ошибка, если изменение было намеренным.

Примеры зерна агрегированного результата
ВопросОдна строка результатаГруппировка
Сколько пользователей по каналам?один каналchannel
Какой DAU по дням?один календарный деньevent_date
Какой DAU по каналам и дням?день × каналevent_date, channel
Какие каналы дали минимум 100 пользователей?один канал после фильтраchannel + HAVING

GROUP BY на таблице users

В учебной таблице `users` одна строка соответствует одному пользователю, поэтому `COUNT(*)` здесь действительно считает пользователей. Для более явного запроса можно использовать `COUNT(user_id)`: оба варианта дадут одинаковый результат, если user_id заполнен в каждой строке.

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

Количество регистраций по каналам
select
  channel,
  count(*) as users_count
from users
group by channel
order by users_count desc, channel;

COUNT(*) и COUNT(DISTINCT) — это не одно и то же

На таблице событий разница становится критичной. Один пользователь может открыть приложение несколько раз за день, поэтому `COUNT(*)` посчитает события, а `COUNT(DISTINCT user_id)` — людей. Выбирай функцию не по привычке, а по существительному в вопросе: “сколько событий” или “сколько пользователей”.

Как выбрать агрегат
ВыражениеСмысл результатаКогда применять
COUNT(*)число строксобытия, заказы или платежи
COUNT(user_id)число непустых user_idесли строки могут быть анонимными
COUNT(DISTINCT user_id)число уникальных пользователейDAU, MAU, buyers, активированные пользователи
SUM(amount)сумма значенийвыручка, скидка или количество единиц
События и уникальные активные пользователи по дням
select
  event_date,
  count(*) as events_count,
  count(distinct user_id) as active_users
from events
where event_name = 'core_action'
group by event_date
order by event_date;

DAU по дням: агрегация продуктовой метрики

Для DAU сначала нужно договориться, какое событие считается активностью. В примере берём только `core_action`, потому что техническое открытие приложения может создавать шум. Зерно `events` — одно событие, а зерно результата — один день и число уникальных пользователей в этот день.

DAU за период по core action
select
  event_date,
  count(distinct user_id) as dau
from events
where event_name = 'core_action'
  and event_date >= date '2026-06-01'
  and event_date < date '2026-07-01'
group by event_date
order by event_date;
Проверь календарь

Если в данных нет событий в какой-то день, запрос не создаст строку с нулём автоматически. Для отчёта с непрерывной осью дат понадобится календарная таблица или generate_series, а не только GROUP BY по events.

WHERE и HAVING решают разные задачи

`WHERE` сокращает исходные строки до группировки. `HAVING` применяется после GROUP BY и может использовать агрегат, например оставить только каналы, где больше двух пользователей. Перепутать их легко: условие по `channel` обычно живёт в WHERE, а условие по `COUNT(*)` — в HAVING.

Каналы с минимум двумя пользователями
select
  channel,
  count(*) as users_count
from users
where signup_date < date '2026-07-01'
group by channel
having count(*) >= 2
order by users_count desc;
  • `WHERE` уменьшает объём данных до агрегации.
  • `HAVING` фильтрует уже сформированные группы.
  • Фильтр по дате регистрации обычно относится к WHERE.
  • Фильтр “оставь каналы с 100 пользователями” относится к HAVING.

Ошибки, из-за которых метрика меняет смысл

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

Контрольная сверка

Для users сумма users_count по каналам должна совпадать с числом строк, которые прошли WHERE. Если не совпадает — ищи NULL, дубли или неверную границу периода.

  • Добавить поле в SELECT и забыть добавить его в GROUP BY.
  • Посчитать COUNT(*) в events и назвать результат DAU.
  • Отфильтровать группы через WHERE с агрегатной функцией вместо HAVING.
  • Смешать полный месяц с текущим неполным месяцем.
  • Не заметить NULL-канал и потерять его из группировки или отчёта.
  • Округлить значения слишком рано и получить расхождение между строками и итогом.

Мини-практика и следующий шаг

В SQL-тренажёре начни с GROUP BY channel и COUNT(*), затем попробуй добавить сортировку по количеству пользователей. После этого измени запрос для events: сравни число core actions и число уникальных пользователей по дням. Разница между двумя столбцами — отличный способ увидеть, почему DAU нельзя считать обычным COUNT(*).

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