Типичные ошибки: WHERE vs HAVING
Типичные ошибки: WHERE vs HAVING
В этом уроке мы разберём пограничные случаи, которые регулярно путают разработчиков при работе с WHERE и HAVING. Вы уже знаете, что WHERE фильтрует строки до группировки, а HAVING — группы после. Теория простая, но на практике случаются ошибки даже у опытных разработчиков. Рассмотрим самые распространённые, их симптомы и правильные решения.
Ошибка 1: агрегатная функция в WHERE
Самая частая и самая однозначная ошибка — попытка использовать агрегат в WHERE:
-- Ошибка
SELECT department, COUNT(*) AS cnt
FROM employees
WHERE COUNT(*) > 2
GROUP BY department;
-- ERROR: aggregate functions are not allowed in WHERE
PostgreSQL выдаёт ясное сообщение: агрегатные функции не разрешены в WHERE. Это не ограничение PostgreSQL — это стандарт SQL. WHERE работает до группировки, когда группы ещё не существуют, а значит, и агрегировать нечего.
Правило: если нужно фильтровать по результату агрегата — всегда HAVING.
-- Правильно
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
HAVING COUNT(*) > 2;
Ошибка 2: псевдоним из SELECT в HAVING
В PostgreSQL нельзя использовать псевдоним из SELECT в HAVING — хотя интуиция подсказывает обратное:
-- Ошибка (нестандартно, не работает в PostgreSQL)
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
HAVING cnt > 2;
-- ERROR: column "cnt" does not exist
PostgreSQL обрабатывает HAVING раньше, чем «знает» псевдонимы из SELECT. Поэтому cnt в HAVING — несуществующая колонка.
Правило: в HAVING повторяйте агрегатное выражение целиком.
-- Правильно
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department
HAVING COUNT(*) > 2;
Единственное исключение — ORDER BY, где PostgreSQL разрешает использовать псевдонимы из SELECT. Но не HAVING и не WHERE.
Ошибка 3: псевдоним из SELECT в WHERE
Та же логика работает для WHERE. Это удивляет новичков ещё сильнее:
-- Ошибка
SELECT name, salary * 12 AS annual_salary
FROM employees
WHERE annual_salary > 1000000;
-- ERROR: column "annual_salary" does not exist
WHERE выполняется до вычисления SELECT-выражений. Когда WHERE проверяет условие, псевдоним annual_salary ещё не существует.
Решение 1: повторить выражение в WHERE:
SELECT name, salary * 12 AS annual_salary
FROM employees
WHERE salary * 12 > 1000000;
Решение 2: использовать подзапрос (тема модуля 7), чтобы псевдоним «закрепился»:
SELECT name, annual_salary
FROM (
SELECT name, salary * 12 AS annual_salary
FROM employees
) sub
WHERE annual_salary > 1000000;
Проверь себя: почему ORDER BY annual_salary DESC работает, а WHERE annual_salary > 1000000 — нет?
Ошибка 4: WHERE и HAVING перепутаны по смыслу
Технически запрос корректен, но результат неожиданный. Это самая коварная ошибка — нет синтаксической ошибки, просто неправильные данные.
Задача: найти отделы, где средняя зарплата выше 80 000.
-- Неправильный подход: WHERE фильтрует строки, а не группы
SELECT department, AVG(salary) AS avg_salary
FROM employees
WHERE salary > 80000
GROUP BY department;
Этот запрос не выдаст ошибку. Но он отвечает на другой вопрос: «средняя зарплата среди тех, кто зарабатывает более 80 000, по отделам». В результат не попадут сотрудники с зарплатой ≤ 80 000, и среднее будет завышено.
-- Правильный подход: HAVING фильтрует группы по результату AVG
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 80000;
Здесь в каждую группу попадают все сотрудники, потом СУБД считает среднюю, и только потом применяет фильтр.
Как не ошибиться: задайте себе вопрос — «фильтрую я отдельные строки или целые группы?». Если группы — нужен HAVING.
Ошибка 5: WHERE используется там, где нужны оба
Иногда задача требует и WHERE, и HAVING. Разработчики иногда забывают один из них:
Задача: найти отделы с более чем 2 активными сотрудниками.
-- Не хватает WHERE: считаем всех сотрудников, включая уволенных
SELECT department, COUNT(*) AS active_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 2;
-- Не хватает HAVING: показываем все отделы без фильтрации по количеству
SELECT department, COUNT(*) AS active_count
FROM employees
WHERE status = 'active'
GROUP BY department;
-- Правильно: сначала фильтруем строки, потом фильтруем группы
SELECT department, COUNT(*) AS active_count
FROM employees
WHERE status = 'active'
GROUP BY department
HAVING COUNT(*) > 2;
Первый вариант посчитает уволенных сотрудников в COUNT(*). Второй покажет все отделы, включая те, где активный сотрудник только один. Третий — правильный.
Ошибка 6: GROUP BY без нужных колонок в SELECT
Технически это не ошибка WHERE/HAVING, но часто встречается рядом. Попытка добавить колонку в SELECT, которой нет в GROUP BY:
-- Ошибка
SELECT department, name, COUNT(*) AS cnt
FROM employees
GROUP BY department;
-- ERROR: column "employees.name" must appear in the GROUP BY clause
Если нужно видеть имена сотрудников и количество по отделам — это разные уровни агрегации. Варианты:
- Добавить
nameвGROUP BY(тогда группировка поdepartment + name, одна строка на сотрудника) - Убрать
nameизSELECT - Использовать подзапрос или оконную функцию (модуль 10)
Ошибка 7: HAVING с условием, которое должно быть в WHERE
Обратная ошибке 4 ситуация — технически возможно, но семантически неверно и медленно:
-- Работает, но неправильно (медленно и нелогично)
SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING department = 'IT';
-- Правильно: условие на строку → WHERE
SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
WHERE department = 'IT'
GROUP BY department;
В первом варианте СУБД сгруппирует все строки, потом выбросит всё кроме IT. Во втором — сначала отберёт только IT, потом сгруппирует. Второй вариант быстрее (меньше строк для группировки) и логичнее (фильтр по значению строки — это WHERE).
Правило: если условие можно проверить на уровне строки (не требует агрегата) — используйте WHERE, а не HAVING. Это и правильнее семантически, и быстрее.
Порядок выполнения — схема-памятка
Вот полный порядок логического выполнения запроса, который помогает понять, где что применимо:
FROM → 1. Выбираем источник данных
WHERE → 2. Фильтруем строки (до группировки)
GROUP BY → 3. Группируем
агрегаты → 4. Вычисляем агрегатные функции для каждой группы
HAVING → 5. Фильтруем группы (после агрегации)
SELECT → 6. Формируем список колонок (псевдонимы появляются здесь)
ORDER BY → 7. Сортируем (псевдонимы уже доступны)
LIMIT → 8. Обрезаем
Именно из-за этого порядка:
- Псевдонимы из
SELECTнедоступны вWHEREиHAVING(шаг 6 позже шагов 2 и 5) - Агрегаты недоступны в
WHERE(шаг 4 позже шага 2) - Псевдонимы доступны в
ORDER BY(шаг 7 позже шага 6)
Диагностика: что написать, если не уверен
Когда не уверены, куда поставить условие, проверьте по этому алгоритму:
- Это агрегатная функция? →
HAVING(обязательно) - Это условие на значение конкретной строки? →
WHERE - Нужно ли вычислить агрегат, чтобы проверить условие? →
HAVING - Можно ли проверить условие, глядя на одну строку без группировки? →
WHERE
Например:
salary > 50000→ строку можно проверить саму по себе →WHERECOUNT(*) > 3→ нужно агрегировать →HAVINGAVG(salary) > 80000→ нужно агрегировать →HAVINGdepartment = 'IT'→ строку можно проверить саму по себе →WHERESUM(amount) > 1000000→ нужно агрегировать →HAVING
Практический пример: все ошибки вместе
Задача: найти категории товаров, в которых продано более 50 единиц со средней ценой выше 1000 рублей, только для завершённых заказов, отсортированные по выручке.
-- Неправильный вариант (несколько ошибок)
SELECT category, SUM(quantity * price) AS revenue
FROM order_items
WHERE COUNT(*) > 50 -- ошибка: агрегат в WHERE
GROUP BY category
HAVING AVG(price) > 1000
AND status = 'completed'; -- ошибка: условие на строку в HAVING
-- Правильный вариант
SELECT category,
COUNT(*) AS orders_count,
SUM(quantity) AS units_sold,
SUM(quantity * price) AS revenue,
AVG(price) AS avg_price
FROM order_items
WHERE status = 'completed' -- строковое условие → WHERE
GROUP BY category
HAVING SUM(quantity) > 50 -- агрегат → HAVING
AND AVG(price) > 1000 -- агрегат → HAVING
ORDER BY revenue DESC;
Краткий итог
- Агрегатные функции нельзя использовать в
WHERE— только вHAVINGиSELECT - Псевдонимы из
SELECTнедоступны вWHEREиHAVING— повторяйте выражение целиком - Псевдонимы из
SELECTдоступны вORDER BY— это исключение из правила - Если условие проверяется без агрегации —
WHERE; если требует агрегата —HAVING WHERE+HAVINGвместе — распространённый паттерн, не забывайте использовать оба- Условие на значение строки в
HAVINGработает, но медленнее и семантически неправильно
Что дальше
Вы завершили модуль 5 — агрегатные функции, GROUP BY, HAVING и все их подводные камни. Следующий модуль — соединения таблиц (JOIN). Это один из центральных инструментов реляционных баз данных: как объединять данные из нескольких таблиц в один запрос.