Типичные ошибки: 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)

Диагностика: что написать, если не уверен

Когда не уверены, куда поставить условие, проверьте по этому алгоритму:

  1. Это агрегатная функция?HAVING (обязательно)
  2. Это условие на значение конкретной строки?WHERE
  3. Нужно ли вычислить агрегат, чтобы проверить условие?HAVING
  4. Можно ли проверить условие, глядя на одну строку без группировки?WHERE

Например:

  • salary > 50000 → строку можно проверить саму по себе → WHERE
  • COUNT(*) > 3 → нужно агрегировать → HAVING
  • AVG(salary) > 80000 → нужно агрегировать → HAVING
  • department = 'IT' → строку можно проверить саму по себе → WHERE
  • SUM(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). Это один из центральных инструментов реляционных баз данных: как объединять данные из нескольких таблиц в один запрос.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике