Если хочешь посчитать количество заказов по каждому клиенту, средний чек по городам или найти продавцов с выручкой больше миллиона — тебе нужен GROUP BY. А если ещё и отфильтровать результат — HAVING.
GROUP BY: группируем строки
GROUP BY схлопывает строки с одинаковым значением в группу, к которой применяется агрегатная функция.
SELECT city, COUNT(*) AS orders_count
FROM orders
GROUP BY city;
| city | orders_count |
|---|---|
| Москва | 142 |
| Санкт-Петербург | 87 |
| Казань | 34 |
Агрегатные функции, которые работают с GROUP BY:
| Функция | Что делает |
|---|---|
COUNT(*) | Количество строк (включая NULL) |
COUNT(col) | Количество строк, где col не NULL |
SUM(col) | Сумма |
AVG(col) | Среднее |
MIN(col) / MAX(col) | Минимум / максимум |
COUNT(*)иCOUNT(col)— не одно и то же. Если в столбце есть NULL-значения,COUNT(col)посчитает меньше строк. Это удобно:COUNT(phone)покажет, у скольких клиентов заполнен телефон.
Важное правило
В SELECT при использовании GROUP BY можно указывать только:
- Столбцы из
GROUP BY - Агрегатные функции
-- ✅ Правильно
SELECT city, COUNT(*) FROM orders GROUP BY city;
-- ❌ Ошибка: customer_name не в GROUP BY и не агрегирован
SELECT city, customer_name, COUNT(*) FROM orders GROUP BY city;
Нюанс про MySQL: в MySQL при отключённом режиме
ONLY_FULL_GROUP_BYтакой запрос не упадёт с ошибкой, а вернёт случайное значениеcustomer_nameиз группы. Это классический источник трудноуловимых багов. Всегда придерживайтесь стандарта и явно агрегируйте всё, что хотите видеть.
Группировка по нескольким полям
SELECT city, status, COUNT(*) AS cnt
FROM orders
GROUP BY city, status
ORDER BY city, cnt DESC;
Это создаст группу для каждой уникальной комбинации city + status. Например: ('Москва', 'completed') — одна группа, ('Москва', 'cancelled') — другая, ('Казань', 'completed') — третья.
HAVING: фильтруем группы
WHERE фильтрует строки до группировки. HAVING — после.
-- Города с более чем 50 заказами
SELECT city, COUNT(*) AS cnt
FROM orders
GROUP BY city
HAVING COUNT(*) > 50;
WHERE vs HAVING — в чём разница?
-- WHERE: убирает строки до группировки
SELECT city, COUNT(*) AS cnt
FROM orders
WHERE status = 'completed' -- сначала берём только выполненные
GROUP BY city
HAVING COUNT(*) > 10; -- затем фильтруем города
-- HAVING нельзя использовать без GROUP BY (почти всегда)
Запомни: WHERE — для строк, HAVING — для групп.
Совет по оптимизации: старайтесь максимально фильтровать в
WHERE, а не вHAVING.WHEREвыполняется до группировки и может использовать индексы — это уменьшает объём данных, попадающих в дорогостоящую операцию агрегации.HAVINGфильтрует уже сгруппированный результат, когда индексы бесполезны.
Практические примеры
Топ клиентов по сумме покупок
SELECT customer_id, SUM(amount) AS total
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(amount) > 10000
ORDER BY total DESC
LIMIT 10;
Среднее время выполнения заказа по менеджерам
SELECT manager_id,
COUNT(*) AS orders_cnt,
ROUND(AVG(duration_days), 1) AS avg_days
FROM orders
GROUP BY manager_id
HAVING COUNT(*) >= 5 -- только те, у кого достаточно данных
ORDER BY avg_days;
Дни с аномальным количеством заказов
SELECT DATE(created_at) AS day,
COUNT(*) AS cnt
FROM orders
GROUP BY DATE(created_at)
HAVING COUNT(*) > (SELECT AVG(daily_cnt) * 2
FROM (SELECT DATE(created_at), COUNT(*) AS daily_cnt
FROM orders GROUP BY DATE(created_at)) sub)
ORDER BY day;
Порядок выполнения SQL
Важно понимать, в каком порядке база выполняет запрос:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Поэтому нельзя использовать алиас из SELECT в HAVING:
-- ❌ Не сработает в большинстве СУБД
SELECT city, COUNT(*) AS cnt
FROM orders
GROUP BY city
HAVING cnt > 50; -- cnt ещё не определён на этом этапе
-- ✅ Правильно
SELECT city, COUNT(*) AS cnt
FROM orders
GROUP BY city
HAVING COUNT(*) > 50;
Исключение: PostgreSQL и некоторые другие СУБД разрешают использовать алиасы из SELECT в ORDER BY, но не в HAVING.
Типичные вопросы на собеседовании
В чём разница между WHERE и HAVING? WHERE фильтрует строки до группировки, HAVING — группы после GROUP BY.
Можно ли использовать HAVING без GROUP BY? Технически да — тогда вся таблица считается одной группой. Но на практике это редкость.
Почему нельзя использовать агрегатные функции в WHERE? Потому что WHERE выполняется до агрегации — значений ещё нет.