У чому різниця між 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