В чём разница между WHERE и HAVING?
Запрос выполняется НЕ в том порядке, в котором написан:
🟢 Junior Level
WHERE и HAVING — оба используются для фильтрации данных, но на разных этапах:
- WHERE — фильтрует строки до группировки
- HAVING — фильтрует группы после группировки
Простая аналогия: Представьте, что вы сортируете монеты:
- WHERE — отбираете только монеты определённого номинала
- Группируете по годам
- HAVING — оставляете только группы, где монет больше 5
Пример:
-- WHERE: фильтруем заказы по статусу
-- HAVING: оставляем только пользователей с > 5 заказами
SELECT user_id, COUNT(*) as orders_count
FROM orders
WHERE status = 'COMPLETED' -- Фильтр ДО группировки
GROUP BY user_id
HAVING COUNT(*) > 5; -- Фильтр ПОСЛЕ группировки
Правило:
- Нет
GROUP BY→ используйтеWHERE - Есть
GROUP BYи нужно отфильтровать по агрегату (COUNT,SUM) →HAVING
🟡 Middle Level
Порядок выполнения запроса (Logical Query Processing)
Запрос выполняется НЕ в том порядке, в котором написан:
SELECT user_id, COUNT(*) as cnt -- 5. SELECT
FROM orders -- 1. FROM
WHERE status = 'COMPLETED' -- 2. WHERE (индексы работают!)
GROUP BY user_id -- 3. GROUP BY
HAVING COUNT(*) > 5 -- 4. HAVING
ORDER BY cnt DESC; -- 6. ORDER BY
Ключевое: WHERE выполняется ДО GROUP BY, а HAVING — ПОСЛЕ.
Почему это важно для производительности
-- ❌ ПЛОХО: фильтрация в HAVING вместо WHERE
SELECT city, COUNT(*)
FROM users
GROUP BY city
HAVING city = 'Moscow'; -- Группируем ВСЕ города, потом фильтруем!
-- ✅ ХОРОШО: фильтрация в WHERE
SELECT city, COUNT(*)
FROM users
WHERE city = 'Moscow' -- Фильтруем ДО группировки → меньше данных
GROUP BY city;
Разница: Первый запрос группирует миллионы строк, второй — только московских пользователей.
Сравнение
| Характеристика | WHERE | HAVING |
|---|---|---|
| Что фильтрует | Строки | Группы |
| Когда | ДО GROUP BY | ПОСЛЕ GROUP BY |
| Агрегаты | ❌ Нельзя | ✅ Можно |
| Индексы | ✅ Работают | ❌ Не работают |
| Производительность | Быстро | Медленнее (после агрегации) |
Типичные ошибки
- Агрегат в WHERE
-- ❌ ОШИБКА: агрегаты нельзя в WHERE SELECT user_id FROM orders WHERE COUNT(*) > 5; -- ✅ Правильно: через HAVING SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5; - Фильтрация неагрегатного поля в HAVING
-- ❌ Плохо: city не агрегат SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow'; -- ✅ Хорошо: в WHERE SELECT city, COUNT(*) FROM users WHERE city = 'Moscow' GROUP BY city; - Алиас из SELECT в HAVING
-- ⚠️ Работает в PostgreSQL, но НЕ в SQL Server/Oracle SELECT city, COUNT(*) as cnt FROM users GROUP BY city HAVING cnt > 10; -- cnt — алиас из SELECT -- ✅ Переносимо: SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 10;
HAVING без GROUP BY
-- Вся таблица = одна группа
SELECT 'Many orders'
FROM orders
HAVING COUNT(*) > 1000000;
-- Вернёт 1 строку, если заказов > 1 млн, иначе 0 строк
🔴 Senior Level
Когда НЕ использовать
- HAVING для неагрегатных фильтров —
HAVING city = 'Moscow'группирует ВСЕ города потом фильтрует. Используйте WHERE - WHERE с агрегатами —
WHERE COUNT(*) > 5нельзя. Используйте HAVING - HAVING для фильтрации по оконным функциям — оконные функции считаются ПОСЛЕ HAVING. Используйте подзапрос/CTE
Filter Push-down оптимизация
PostgreSQL 12+ пытается автоматически переносить условия из HAVING в WHERE:
До PG 12 Filter Push-down отсутствовал — условие в HAVING всегда оставалось в HAVING. Это означало, что HAVING city = 'Moscow' группирует все города, а затем фильтрует. На PG 12+ оптимизатор сам перенесёт city = 'Moscow' в WHERE, но полагаться на это нельзя.
-- Запрос
SELECT city, COUNT(*) FROM users
GROUP BY city
HAVING city = 'Moscow' AND COUNT(*) > 10;
-- Оптимизатор превращает в:
SELECT city, COUNT(*) FROM users
WHERE city = 'Moscow' -- Pushed down!
GROUP BY city
HAVING COUNT(*) > 10;
Но нельзя полагаться на это!
Когда Push-down НЕ работает:
- Сложные выражения с подзапросами
- Внутри Views
- При использовании CTE с
MATERIALIZED
Золотое правило: Всегда пишите неагрегатные фильтры в WHERE явно.
work_mem — параметр PostgreSQL, ограничивающий объём памяти для одной операции (сортировка, хэш-агрегация). По умолчанию 4MB. Если операция не влезает, данные сбрасываются на диск (spill to disk) во временные файлы (temp_files), что замедляет выполнение в 10-100 раз.
Влияние на work_mem и temp_files
-- Проблема: 100 млн строк без фильтра → GROUP BY
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- work_mem = 4MB → Hash Aggregate не влезает → spill на диск → temp_files!
-- Решение: фильтр в WHERE
SELECT status, COUNT(*) FROM orders
WHERE created_at > '2024-01-01' -- Сокращаем вход в 10 раз
GROUP BY status;
-- Теперь влезает в work_mem → в 100 раз быстрее
-- Операции в памяти (RAM) происходят в наносекундах, операции с диском —
-- в миллисекундах. Разница в 6 порядков. Даже с учётом того, что spill
-- не на каждую строку, итоговое замедление составляет 10-100 раз.
Мониторинг spill:
EXPLAIN (ANALYZE, BUFFERS)
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Ищите: "HashAggregate" + "Disk: X kB" → плохо!
WHERE vs HAVING vs Window Functions
Фильтрация по оконной функции невозможна в HAVING:
-- ❌ ОШИБКА: оконные функции считаются ПОСЛЕ HAVING
SELECT user_id, amount,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) as rn
FROM orders
GROUP BY user_id, amount, created_at
HAVING rn = 1; -- Ошибка: rn не существует на этапе HAVING
-- ✅ Решение: подзапрос или CTE
WITH ranked AS (
SELECT user_id, amount,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) as rn
FROM orders
)
SELECT user_id, amount FROM ranked WHERE rn = 1;
Архитектурный паттерн: WHERE + HAVING
-- Правильное разделение ответственности
SELECT
department,
COUNT(*) as emp_count,
AVG(salary) as avg_salary
FROM employees
WHERE status = 'ACTIVE' -- 1. Отсекаем уволенных (индекс!)
AND hire_date > '2020-01-01' -- 2. Сокращаем вход
GROUP BY department
HAVING COUNT(*) > 10 -- 3. Бизнес-логика: только крупные отделы
AND AVG(salary) > 100000; -- 4. Фильтр по агрегату
Edge Cases
- NULL в HAVING
SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 10; -- NULL — это отдельная группа! -- Если есть строки с status = NULL, они сгруппируются вместе - HAVING с несколькими агрегатами
SELECT product_id FROM orders GROUP BY product_id HAVING COUNT(*) > 100 -- Минимум 100 заказов AND AVG(amount) > 1000 -- Средний чек > 1000 AND MAX(amount) - MIN(amount) < 5000; -- Разброс цен < 5000 - FILTER в HAVING
SELECT project_id FROM tasks GROUP BY project_id HAVING COUNT(*) > 5 AND COUNT(*) FILTER (WHERE status = 'blocked') > 2; -- Проекты с > 5 задачами, из них > 2 заблокированы
Performance Impact
| Сценарий | WHERE | HAVING |
|---|---|---|
| Индексы | ✅ Index Scan | ❌ Нет |
| Объём данных | Сокращает ДО GROUP BY | Фильтрует ПОСЛЕ GROUP BY |
| work_mem | Меньше → быстрее | Больше → медленнее |
| temp_files | Меньше риск | Больше риск |
Production Experience
Реальный сценарий:
- Аналитика: отчёт по продажам (100 млн строк)
- Проблема: Запрос 2 минуты, spill на диск 5 ГБ
- EXPLAIN:
HashAggregate Disk: 5GB - Причина: ORM генерировал
HAVING status = 'ACTIVE'вместоWHERE - Решение: Перенесли
status = 'ACTIVE'в WHERE - Результат: Запрос 3 секунды (ускорение в 40 раз), без spill
Monitoring
-- 1. Проверка на spill в планах
EXPLAIN (ANALYZE, BUFFERS)
SELECT city, COUNT(*) FROM users GROUP BY city;
-- Ищите: "Disk: X kB" → увеличьте work_mem или добавьте WHERE
-- 2. Поиск запросов с HAVING в pg_stat_statements
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE query LIKE '%HAVING%'
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 3. Проверка Filter Push-down
EXPLAIN (VERBOSE)
SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';
-- В плане увидите, перенесён ли фильтр в WHERE
Best Practices
- WHERE для фильтрации строк (до GROUP BY)
- HAVING только для фильтрации по агрегатам
- Никогда не используйте HAVING для неагрегатных условий
- Проверяйте план на наличие spill (Disk usage)
- Фильтруйте как можно раньше — экономите CPU и RAM
- FILTER внутри HAVING для сложных условий
- Подзапрос/CTE для фильтрации по оконным функциям
Резюме для Senior
- WHERE фильтрует input, HAVING фильтрует output группировки
- Filter Push-down (PG 12+) не всегда работает — не полагайтесь
- work_mem давление: HAVING не спасает от переполнения при GROUP BY
- Оконные функции считаются ПОСЛЕ HAVING → нужен подзапрос
- temp_files — индикатор проблемы:
EXPLAIN (ANALYZE, BUFFERS) - Золотое правило: Всё, что можно → в
WHERE
🎯 Шпаргалка для интервью
Обязательно знать:
- WHERE фильтрует строки ДО GROUP BY, HAVING — группы ПОСЛЕ GROUP BY
- WHERE использует индексы, HAVING — нет
- Агрегаты (COUNT, SUM) нельзя в WHERE, можно в HAVING
- Filter Push-down (PG 12+): оптимизатор переносит неагрегатные условия из HAVING в WHERE
- FILTER внутри HAVING:
COUNT(*) FILTER (WHERE status = 'ACTIVE') > 5 - work_mem давление: HAVING не спасает — все группы уже созданы
- Оконные функции считаются ПОСЛЕ HAVING → подзапрос/CTE
- Алиасы из SELECT в HAVING: работает в PG, НЕ работает в SQL Server/Oracle
Частые уточняющие вопросы:
- «Почему HAVING медленнее WHERE?» → Группирует ВСЕ данные, потом фильтрует
- «Когда Filter Push-down не работает?» → CTE MATERIALIZED, Views, подзапросы
- «Можно ли HAVING без GROUP BY?» → Да, вся таблица = одна группа
- «Что лучше: FILTER или CASE WHEN в агрегате?» → FILTER (чистый SQL)
Красные флаги (НЕ говорить):
- ❌ «HAVING оптимизирует GROUP BY» (нет, фильтрует уже сгруппированные данные)
- ❌ «Можно полагаться на Filter Push-down» (не всегда работает!)
- ❌ «Оконные функции можно в HAVING» (считаются ПОСЛЕ!)
Связанные темы:
- [[Что делает GROUP BY]] → стратегии агрегации
- [[Когда использовать HAVING]] → сценарии использования
- [[Что такое оконные функции]] → почему нельзя в HAVING