10. GROUP BY + HAVING + DISTINCT
🧭 Введение: как получить сводку, а не сырые строки
Когда данных много, команде почти никогда не нужен полный список записей.
Нужны итоги: сколько заказов в каждом статусе, сколько активных пользователей по странам, где выручка выше порога.
Нужны итоги: сколько заказов в каждом статусе, сколько активных пользователей по странам, где выручка выше порога.
Для этого в SQL используют связку
Это базовый уровень аналитики прямо в запросе, без отдельного BI-инструмента.
GROUP BY, HAVING и DISTINCT.Это базовый уровень аналитики прямо в запросе, без отдельного BI-инструмента.
Главная ошибка новичка: смешивать роли
WHERE, HAVING и DISTINCT, из-за чего запросы либо падают, либо дают неверную логику.💡 Совет:
Читая любой агрегатный запрос, разбирайте его по шагам: фильтр строк (
WHERE) -> группировка (GROUP BY) -> фильтр групп (HAVING).✅ Вывод:
GROUP BY + HAVING + DISTINCT превращают таблицу из набора строк в понятную бизнес-сводку.⚠️ Проблема -> решение
Типичный старт: разработчик пишет отчёт в один проход и пытается:
- фильтровать агрегаты через
WHERE, - выбирать колонку, которой нет в
GROUP BY, - подменять
GROUP BYсловомDISTINCT, когда нужны метрики.
Результат:
- синтаксические ошибки,
- логические ошибки в отчёте,
- тяжёлые запросы без нужного смысла.
Решение:
- сначала определить уровень логики: строка или группа,
- правильно разделить
WHEREиHAVING, - использовать
DISTINCTтолько для уникальности, а не вместо агрегатов.
🟢 Если совсем просто:
WHERE фильтрует записи, GROUP BY собирает пачки, HAVING фильтрует пачки, DISTINCT убирает дубли.🎯 Как понять, что этап прошёл успешно:
Вы можете объяснить каждую строку запроса словами бизнеса, а не только синтаксисом.
🛠️ Чем помогает и как работает
Эта связка нужна почти в каждом сервисе: отчёты по заказам, активности, воронкам, SLA.
Смысл в том, что SQL сам выполняет групповую обработку и сразу возвращает метрики.
Смысл в том, что 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 ONSELECT 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-отчётах: выбрать поле в
В строгих СУБД (например, PostgreSQL) запрос упадёт с ошибкой.
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 начинается с правильной модели данных в запросе: строка, группа, уникальность.