GROUP BY, HAVING и WHERE: порядок выполнения запроса
Второй обязательный SQL-вопрос: чем WHERE отличается от HAVING и почему это не «одно и то же, но для агрегатов».
GROUP BY схлопывает множество строк в одну по значению ключа.
WHEREработает до схлопывания,HAVING— после.
Вопрос 1. В чём разница между WHERE и HAVING?
Что проверяет интервьюер. Понимаете ли вы порядок выполнения запроса. Аналитик, который держит его в голове, не пишет заведомо неработающих конструкций и умеет объяснить, почему в WHERE нельзя использовать алиас из SELECT.
Логический порядок обработки такой:
FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Отсюда сразу следуют три вывода: WHERE не видит агрегатов (их ещё не посчитали), HAVING видит, а алиасы из SELECT не видны ни в WHERE, ни в GROUP BY, зато видны в ORDER BY.
CREATE TABLE sales (id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT, manager TEXT, amount INTEGER, dt TEXT);
INSERT INTO sales (city, manager, amount, dt) VALUES
('Москва','Анна',1500,'2026-03-01'),
('Москва','Анна',2500,'2026-03-04'),
('Москва','Борис',800,'2026-03-02'),
('Казань','Вера',1200,'2026-03-03'),
('Казань','Глеб',400,'2026-03-05'),
('Пермь','Дина',300,'2026-02-27');
SELECT city, SUM(amount) AS total, COUNT(*) AS deals
FROM sales
WHERE dt >= '2026-03-01'
GROUP BY city
ORDER BY total DESC;
Вывод:
Москва 4800 3
Казань 1600 2
Пермь исчезла ещё до группировки: её единственная сделка от 27 февраля отсеялась в WHERE. Теперь добавим порог на уже посчитанную сумму:
CREATE TABLE sales (id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT, manager TEXT, amount INTEGER, dt TEXT);
INSERT INTO sales (city, manager, amount, dt) VALUES
('Москва','Анна',1500,'2026-03-01'),
('Москва','Анна',2500,'2026-03-04'),
('Москва','Борис',800,'2026-03-02'),
('Казань','Вера',1200,'2026-03-03'),
('Казань','Глеб',400,'2026-03-05'),
('Пермь','Дина',300,'2026-02-27');
SELECT city, SUM(amount) AS total
FROM sales
WHERE dt >= '2026-03-01'
GROUP BY city
HAVING SUM(amount) > 2000
ORDER BY total DESC;
Вывод:
Москва 4800
Казань отсеялась уже после группировки — её сумма не дотянула до порога. Записать это условие в WHERE невозможно: на момент WHERE суммы ещё не существует.
Практическое правило
| Условие | Куда писать |
| по значению строки (дата, город, статус) | WHERE |
| по результату агрегата (SUM, COUNT, AVG) | HAVING |
| и то и другое | оба — и это правильно |
Есть и вопрос производительности: WHERE отсекает строки до группировки, значит группировать придётся меньше данных. Поэтому фильтр, который можно записать в WHERE, туда и пишут — HAVING оставляют только для агрегатов.
Вопрос 2. Что можно класть в SELECT при GROUP BY?
Только то, что перечислено в GROUP BY, и агрегатные функции. Всё остальное — либо ошибка (PostgreSQL, MS SQL), либо, что хуже, случайное значение из группы (MySQL в нестрогом режиме, SQLite). Второе опаснее: запрос отработает, отчёт уедет заказчику, а число будет взято «с потолка».
-- PostgreSQL: ошибка "column sales.manager must appear in the GROUP BY clause"
SELECT city, manager, SUM(amount)
FROM sales
GROUP BY city;
-- корректно: либо добавить в GROUP BY, либо агрегировать
SELECT city, MAX(manager) AS any_manager, SUM(amount)
FROM sales
GROUP BY city;
Вопрос 3. Как ведут себя агрегаты с NULL?
Это любимое продолжение вопроса про GROUP BY. Правило: все агрегаты, кроме COUNT(*), игнорируют NULL. И AVG делит не на общее число строк, а на число не-NULL значений.
CREATE TABLE checks (id INTEGER PRIMARY KEY, amount REAL);
INSERT INTO checks VALUES (1, 100), (2, 200), (3, NULL), (4, 300);
SELECT
COUNT(*) AS rows_total,
COUNT(amount) AS not_null,
SUM(amount) AS total,
AVG(amount) AS avg_ignoring_null,
SUM(amount)/COUNT(*) AS avg_over_all
FROM checks;
Вывод:
4 3 600.0 200.0 150.0
Два «средних» отличаются в полтора раза. Какое из них правильное — вопрос не к SQL, а к бизнесу: NULL здесь означает «данных нет» или «было ноль»? Если «ноль» — считать надо через COALESCE(amount, 0). Умение задать этот вопрос вслух на собеседовании ценится выше, чем знание синтаксиса.
Типичные ошибки кандидатов
- Отвечают «HAVING — это WHERE для GROUP BY» и останавливаются. Формально близко, но не объясняет главного — порядка выполнения.
- Пишут
WHERE SUM(amount) > 1000и удивляются ошибке. - Складывают всё в
HAVING, включая фильтр по дате: запрос работает, но группирует лишние данные. - Забывают, что
AVGне учитывает NULL, и получают завышенное среднее. - Не могут объяснить, почему
SELECT city, manager … GROUP BY cityв одной СУБД падает, а в другой молча врёт.
Как ответить кратко
«Запрос выполняется в порядке FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Поэтому
WHEREфильтрует отдельные строки до группировки и агрегатов не видит, аHAVINGфильтрует уже готовые группы по значению агрегата. Фильтр по дате или городу всегда вWHERE— так группируется меньше строк; порог поSUMилиCOUNT— вHAVING. И помню, что все агрегаты, кромеCOUNT(*), пропускают NULL, поэтомуAVGможет неожиданно завысить результат».