Типичные ошибки: 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-наставник — бесплатный старт без карты

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