SQL

БЛОК 2. Чтение данных — 10. GROUP BY + HAVING + DISTINCT

📚 22 вопросовПройти тест →
Лекция

БЛОК 2. Чтение данных — 10. GROUP BY + HAVING + DISTINCT

SQL

10. GROUP BY + HAVING + DISTINCT

🧭 Введение: как получить сводку, а не сырые строки

Когда данных много, команде почти никогда не нужен полный список записей.
Нужны итоги: сколько заказов в каждом статусе, сколько активных пользователей по странам, где выручка выше порога.
Для этого в SQL используют связку GROUP BY, HAVING и DISTINCT.
Это базовый уровень аналитики прямо в запросе, без отдельного BI-инструмента.
Главная ошибка новичка: смешивать роли WHERE, HAVING и DISTINCT, из-за чего запросы либо падают, либо дают неверную логику.
💡 Совет: Читая любой агрегатный запрос, разбирайте его по шагам: фильтр строк (WHERE) -> группировка (GROUP BY) -> фильтр групп (HAVING).
Вывод: GROUP BY + HAVING + DISTINCT превращают таблицу из набора строк в понятную бизнес-сводку.

⚠️ Проблема -> решение

Типичный старт: разработчик пишет отчёт в один проход и пытается:
  1. фильтровать агрегаты через WHERE,
  2. выбирать колонку, которой нет в GROUP BY,
  3. подменять GROUP BY словом DISTINCT, когда нужны метрики.
Результат:
  1. синтаксические ошибки,
  2. логические ошибки в отчёте,
  3. тяжёлые запросы без нужного смысла.
Решение:
  1. сначала определить уровень логики: строка или группа,
  2. правильно разделить WHERE и HAVING,
  3. использовать DISTINCT только для уникальности, а не вместо агрегатов.
🟢 Если совсем просто: WHERE фильтрует записи, GROUP BY собирает пачки, HAVING фильтрует пачки, DISTINCT убирает дубли.
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить каждую строку запроса словами бизнеса, а не только синтаксисом.

🛠️ Чем помогает и как работает

Эта связка нужна почти в каждом сервисе: отчёты по заказам, активности, воронкам, SLA.
Смысл в том, что SQL сам выполняет групповую обработку и сразу возвращает метрики.
🟢 Если совсем просто: Вы описываете правила группировки и фильтра, а БД считает итоговые цифры.
🎯 Как понять, что этап прошёл успешно: Вы стабильно пишете запросы вида «по каждой группе посчитать и отфильтровать».
Чем помогает:
  • строит сводки по категориям (status, country, source);
  • отбрасывает слабые группы (HAVING COUNT(*) >= ...);
  • считает уникальные значения (DISTINCT, COUNT(DISTINCT ...));
  • сокращает число отдельных запросов в API.
Как это работает:
  • Шаг 1: WHERE режет входной набор строк.
  • Шаг 2: GROUP BY собирает строки в группы.
  • Шаг 3: агрегаты (COUNT, SUM, AVG, MIN, MAX) считают итоги по группе.
  • Шаг 4: HAVING фильтрует уже готовые группы.
  • Шаг 5: ORDER BY и LIMIT подготавливают итог для интерфейса/дашборда.
Вывод: Сильный запрос с группировкой всегда строится в правильном порядке этапов.

📚 Ключевые термины (простыми словами)

Перед практикой синхронизируем словарь.
🟢 Если совсем просто: Термины ниже отвечают на вопрос «что именно SQL группирует и когда фильтрует».
🎯 Как понять, что этап прошёл успешно: Вы не путаете, где должно стоять условие: в WHERE или в HAVING.
  • GROUP BY (группировка) — объединение строк с одинаковым значением полей в одну группу.
  • Aggregate function (агрегат) — функция, которая считает итог по группе (COUNT, SUM, AVG, MIN, MAX).
  • HAVING — фильтр по группам после расчёта агрегатов.
  • WHERE — фильтр по строкам до группировки.
  • DISTINCT — удаление дублей в результирующем наборе.
  • COUNT(DISTINCT column) — количество уникальных не-NULL значений.
  • Grouping key (ключ группировки) — поле, по которому формируются группы.
  • Non-aggregated column (неагрегированная колонка) — колонка в SELECT, которая должна быть в GROUP BY.
Вывод: Если чётко различать «строку» и «группу», ошибки в этой теме резко сокращаются.

🔢 1. Принцип группировки GROUP BY

GROUP BY нужен, когда вы хотите результат «по каждой категории», а не по каждой записи.
SQL собирает строки с одинаковым ключом группировки и считает агрегаты внутри каждой группы.
🟢 Если совсем просто: Одинаковые значения ключа группировки превращаются в одну строку отчёта.
🎯 Как понять, что этап прошёл успешно: Вы умеете получать таблицу вида группа -> количество/сумма/среднее.
Назначение: Перейти от сырых данных к агрегированной сводке по категориям.
Простыми словами: GROUP BY status отвечает на вопрос «что происходит в каждом статусе отдельно».
Для новичка: Любая колонка в SELECT, которая не агрегируется, должна быть в GROUP BY.
Аналогия: Сортировка документов по папкам: в каждой папке считаем, сколько листов.
Пример:
SELECT  status,  COUNT(*) AS orders_count,  SUM(total_amount) AS status_revenueFROM ordersWHERE created_at >= DATE '2026-03-01'  AND created_at < DATE '2026-04-01'GROUP BY statusORDER BY status_revenue DESC;
🔎 Как это происходит на практике:
  • Контекст: продуктовая команда смотрит структуру заказов за месяц.
  • Действия: группирует по статусу и считает объём и выручку.
  • Результат: быстро видно, какие статусы дают основной вклад.
Характеристики:
  • количество строк в результате = количество групп;
  • агрегаты считаются внутри каждой группы;
  • чем уже фильтр в WHERE, тем меньше данных для группировки.
Когда использовать: Когда нужен отчёт «по каждому типу/статусу/категории».
Вывод: GROUP BY — базовый инструмент любой SQL-аналитики.

⚖️ 2. Разница WHERE и HAVING

WHERE и HAVING выглядят похоже, но работают на разных этапах.
Если перепутать их роли, запрос либо не выполнится, либо будет считать не то.
🟢 Если совсем просто: WHERE фильтрует строки, HAVING фильтрует готовые группы.
🎯 Как понять, что этап прошёл успешно: Вы никогда не пишете COUNT(*) в WHERE.
Назначение: Правильно разделить фильтрацию до и после агрегации.
Простыми словами: Сначала выбираем, какие записи участвуют в расчёте, потом решаем, какие группы оставить. Чем больше условий можно применить в WHERE, тем меньше строк попадёт в агрегацию, и запрос обычно работает быстрее.
Для новичка: Условие про «количество», «сумму», «среднее» почти всегда живёт в HAVING.
Аналогия: WHERE — кого пустить в аудиторию, HAVING — какие группы студентов допустить к следующему этапу.
Пример:
SELECT  manager_id,  COUNT(*) AS deals_count,  SUM(amount) AS deals_amountFROM dealsWHERE created_at >= DATE '2026-03-01'  AND created_at < DATE '2026-04-01'GROUP BY manager_idHAVING COUNT(*) >= 10ORDER BY deals_amount DESC;
🔎 Как это происходит на практике:
  • Контекст: руководитель хочет список активных менеджеров.
  • Действия: период фильтруется в WHERE, порог активности — в HAVING.
  • Результат: отчёт содержит только релевантные группы.
Характеристики:
  • WHERE уменьшает входной объём данных и обычно ускоряет запрос, потому что агрегировать нужно меньше строк;
  • HAVING работает после GROUP BY;
  • HAVING может использовать агрегаты.
Когда использовать: WHERE — для условий по полям строки, HAVING — для условий по итогу группы.
Вывод: Правильное разделение WHERE и HAVING делает запрос и корректным, и предсказуемым.

🧬 3. DISTINCT: когда нужен и когда мешает

DISTINCT полезен, когда нужно убрать дубли или посчитать уникальные значения.
Но он не заменяет GROUP BY, если вам нужна агрегированная аналитика по группам.
🟢 Если совсем просто: DISTINCT отвечает на вопрос «какие уникальные значения есть».
🎯 Как понять, что этап прошёл успешно: Вы отличаете «уникальный список» от «сводки с агрегатами».
Назначение: Возвращать только уникальные строки или значения.
Простыми словами: SELECT DISTINCT city уберёт повторы городов, оставив по одному значению. SELECT DISTINCT country, city убирает дубли по паре (country, city), а не отдельно по каждой колонке.
Для новичка: Если нужна метрика уникальности, чаще всего используйте COUNT(DISTINCT ...).
Аналогия: Это как убрать дубли контактов в телефонной книге.
Пример:
SELECT DISTINCT  cityFROM customersWHERE city IS NOT NULLORDER BY city;
Мини-пример комбинации колонок:
SELECT DISTINCT  country,  cityFROM customers;
💡 Совет: В SELECT DISTINCT сортируйте по колонкам из SELECT; если нужна иная сортировка, используйте подзапрос.
Пример для метрики:
SELECT  COUNT(DISTINCT user_id) AS unique_buyersFROM ordersWHERE status = 'paid';
🔎 Как это происходит на практике:
  • Контекст: маркетинг считает уникальную аудиторию кампании.
  • Действия: вместо общего COUNT(*) использует COUNT(DISTINCT user_id).
  • Результат: метрика отражает реальное число людей, а не число событий.
Характеристики:
  • DISTINCT работает по комбинации выбранных колонок;
  • COUNT(DISTINCT ...) игнорирует NULL;
  • DISTINCT может быть дорогим на больших выборках.
PostgreSQL-бонус: DISTINCT ON
SELECT DISTINCT ON (user_id)  user_id,  id,  created_at,  total_amountFROM ordersORDER BY user_id, created_at DESC, id DESC;
DISTINCT ON — PostgreSQL-специфика: удобно выбрать по одной «лучшей» строке в каждой группе.
Когда использовать: Для списков уникальных значений и метрик «сколько уникальных».
Вывод: DISTINCT — инструмент уникальности, а не универсальная замена группировки.

🧨 4. Типичная ошибка: колонка вне GROUP BY

Одна из самых частых ошибок в SQL-отчётах: выбрать поле в SELECT, которое не агрегировано и не включено в GROUP BY.
В строгих СУБД (например, PostgreSQL) запрос упадёт с ошибкой.
🟢 Если совсем просто: В агрегатном запросе нельзя «просто так» вывести случайную колонку.
🎯 Как понять, что этап прошёл успешно: Вы сразу замечаете, что неагрегированные поля должны попасть в GROUP BY.
Назначение: Избежать синтаксических и логических ошибок при построении сводок.
Простыми словами: Если группируем по status, то для каждой группы может быть много user_id, и БД не знает, какой выбрать.
Для новичка: Есть три безопасных варианта: добавить колонку в GROUP BY, агрегировать её, либо вынести задачу в отдельный запрос.
Аналогия: Нельзя назвать «одного представителя» группы, если правило выбора не задано.
Пример ошибки и исправления:
-- Ошибка: user_id не в GROUP BY и не агрегированSELECT  status,  user_id,  COUNT(*) AS orders_countFROM ordersGROUP BY status; -- Верный вариант: оставляем только статус + агрегатSELECT  status,  COUNT(*) AS orders_countFROM ordersGROUP BY status;
🔎 Как это происходит на практике:
  • Контекст: разработчик спешит собрать отчёт для дашборда.
  • Действия: добавляет «ещё одно поле для удобства», не меняя GROUP BY.
  • Результат: ошибка СУБД и потеря времени на отладку.
Характеристики:
  • правило действует для всех неагрегированных колонок;
  • ошибка появляется до выполнения тяжёлой части запроса;
  • чаще всего исправление — пересборка структуры SELECT.
Когда использовать: Всегда при ревью агрегатных запросов и при написании отчётных API.
Вывод: Правило «все неагрегированные поля должны быть в GROUP BY» — обязательная база.

📊 5. Сборка полноценного отчётного запроса

В реальных задачах GROUP BY, HAVING и DISTINCT редко живут отдельно.
Обычно их комбинируют в одном запросе, где важны и корректность, и читаемость.
🟢 Если совсем просто: Строим запрос по этапам: фильтр -> группировка -> агрегаты -> фильтр групп -> сортировка.
🎯 Как понять, что этап прошёл успешно: Вы можете быстро собрать рабочий отчёт по новой таблице без хаотичных правок.
Назначение: Собирать метрики в одном SQL-запросе с понятной бизнес-логикой.
Простыми словами: Один хороший запрос заменяет несколько «кусочных» запросов в коде.
Для новичка: Сначала пишите минимальную версию (GROUP BY + COUNT), потом добавляйте HAVING и дополнительные метрики.
Аналогия: Это как конструктор: сначала каркас, потом фильтры и декоративные детали.
Пример:
SELECT  country,  COUNT(*) AS users_count,  COUNT(DISTINCT company_id) AS unique_companiesFROM usersWHERE created_at >= DATE '2026-01-01'  AND created_at < DATE '2027-01-01'GROUP BY countryHAVING COUNT(*) >= 100ORDER BY users_count DESCLIMIT 20;
🔎 Как это происходит на практике:
  • Контекст: BI-команда строит витрину по географии клиентов.
  • Действия: добавляет порог значимости и считает уникальные компании внутри страны.
  • Результат: компактный и полезный отчёт для продуктовых решений.
Характеристики:
  • структура запроса читается слева направо по этапам;
  • каждая новая метрика добавляется предсказуемо;
  • если после WHERE не осталось строк, GROUP BY вернёт 0 строк, а не строку с нулями;
  • алиасы делают результат стабильным для API.
Когда использовать: Для регулярных отчётов, аналитических эндпоинтов и дашбордов.
Вывод: Комбинация GROUP BY + HAVING + DISTINCT закрывает большую часть повседневной SQL-аналитики.

🆚 Сравнение: WHERE vs HAVING vs DISTINCT

КонструкцияРаботает на этапеЧто делаетТипичная ошибка
WHEREДо GROUP BYФильтрует строкиПисать агрегаты в условии
HAVINGПосле GROUP BYФильтрует группыДублировать условия, которые должны быть в WHERE
DISTINCTПосле выбора колонокУбирает дубли строк/значенийИспользовать вместо агрегатной логики
GROUP BYЭтап агрегацииФормирует группыВыбирать поля вне GROUP BY
Вывод: Правильная роль каждой конструкции — основа корректного аналитического SQL.

🧠 Must-Know (запомнить)

  • WHERE работает со строками, HAVING — с группами.
  • Любая неагрегированная колонка в SELECT должна быть в GROUP BY.
  • DISTINCT и GROUP BY похожи по эффекту устранения дублей, но решают разные задачи.
  • COUNT(DISTINCT ...) — базовый способ считать уникальность в метриках.
  • Часто выгодно сначала сузить данные в WHERE, а потом группировать.
Вывод: Если держать эти 5 правил в голове, большинство ошибок в отчётных запросах исчезают.

❌ Частые мифы

Миф: HAVING можно всегда использовать вместо WHERE. ✅ Как правильно: WHERE фильтрует раньше и обычно эффективнее для условий по строкам. 📎 Почему это важно: перенос строковых условий в HAVING часто ухудшает производительность.
Миф: DISTINCT и GROUP BY — это одно и то же. ✅ Как правильно: DISTINCT убирает дубли, а GROUP BY формирует группы для агрегатов. 📎 Почему это важно: неверный выбор конструкции даёт неправильную бизнес-метрику.
Миф: если запрос выполняется, значит логика группировки верна. ✅ Как правильно: выполнение не гарантирует корректность метрики, проверяйте смысл каждой колонки. 📎 Почему это важно: «тихие» ошибки в аналитике стоят бизнесу дороже синтаксических ошибок.
Миф: COUNT(*) всегда достаточно для отчёта по уникальным пользователям. ✅ Как правильно: для уникальности используйте COUNT(DISTINCT user_id). 📎 Почему это важно: иначе вы считаете события, а не людей.

🎤 Часто спрашивают на собеседованиях

Вопрос: В чём разница между WHERE и HAVING? ✅ Ответ: WHERE фильтрует строки до группировки, HAVING фильтрует группы после расчёта агрегатов.
Вопрос: Почему нельзя выбрать user_id при GROUP BY status, если user_id не агрегирован? ✅ Ответ: потому что внутри одной группы status может быть много user_id, и без правила агрегации выбор значения неоднозначен.
Вопрос: Когда использовать DISTINCT, а когда GROUP BY? ✅ Ответ: DISTINCT — когда нужен уникальный список, GROUP BY — когда нужны агрегаты по группам.
Вопрос: Что делает COUNT(DISTINCT column)? ✅ Ответ: считает количество уникальных не-NULL значений в колонке.
Вопрос: Где лучше фильтровать период отчёта? ✅ Ответ: обычно в WHERE, чтобы уменьшить объём данных до этапа группировки.

🚨 Типичные ошибки

  • Писать WHERE COUNT(*) > 10 вместо HAVING COUNT(*) > 10.
  • Добавлять в SELECT поля, которых нет в GROUP BY.
  • Использовать DISTINCT как «костыль», когда нужна полноценная агрегация.
  • Забывать, что COUNT(DISTINCT ...) и COUNT(*) отвечают на разные вопросы.
  • Не давать алиасы агрегатам, из-за чего отчёт плохо читается в API.
  • Фильтровать большие таблицы только через HAVING, хотя условие можно вынести в WHERE.
Вывод: Большинство ошибок здесь несложные, но они напрямую ломают аналитику продукта.

✅ Best Practices

  • Формулируйте метрику словами до написания SQL.
  • Держите базовый шаблон: WHERE -> GROUP BY -> HAVING -> ORDER BY.
  • Проверяйте каждую колонку в SELECT: это ключ группировки или агрегат.
  • Для уникальности используйте COUNT(DISTINCT ...), а не COUNT(*).
  • Называйте агрегаты понятными алиасами (orders_count, total_revenue).
  • Начинайте с маленького запроса и усложняйте пошагово.
  • На ревью проверяйте не только синтаксис, но и бизнес-смысл метрик.
Вывод: Чистая структура запроса и чёткая роль каждой конструкции дают стабильные отчёты в проде.

🏁 Заключение

GROUP BY + HAVING + DISTINCT — фундаментальный набор для чтения и анализа данных в SQL.
Он позволяет быстро перейти от «сырых строк» к решениям: увидеть структуру, выделить значимые группы и посчитать уникальность.
Если вы уверенно разделяете роли WHERE, HAVING и DISTINCT, то уже пишете SQL как инженер, а не как «подборщик синтаксиса».
Вывод: Сильный аналитический SQL начинается с правильной модели данных в запросе: строка, группа, уникальность.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 2. Чтение данных — 10. GROUP BY + HAVING + DISTINCT»

Пройти тест →