Коли потрібно використовувати HAVING?
Правильний розподіл фільтрів — ознака хорошого SQL:
🟢 Junior Level
HAVING використовується, коли потрібно відфільтрувати результати після групування за агрегатними функціями (COUNT, SUM, AVG і т.д.).
Просте правило:
- Фільтр за звичайними полями →
WHERE - Фільтр за результатами агрегації →
HAVING
Приклад:
-- Знайти користувачів з більш ніж 5 замовленнями
SELECT user_id, COUNT(*) as orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5; -- Фільтр ПІСЛЯ групування
-- Знайти відділи із середньою зарплатою > 100,000
SELECT department, AVG(salary) as avg_sal
FROM employees
GROUP BY department
HAVING AVG(salary) > 100000;
Часті сценарії:
- Пошук дублікатів (
HAVING COUNT(*) > 1) - Пошук груп, що перевищують поріг
- Фільтрація за середніми/сумарними значеннями
🟡 Middle Level
Архітектурний патерн: WHERE + HAVING
Правильний розподіл фільтрів — ознака хорошого SQL:
SELECT customer_id, AVG(amount) as avg_amount
FROM orders
WHERE status = 'delivered' -- 1. Фільтр ДО групування (індекс!)
AND created_at > '2024-01-01' -- 2. Скорочуємо вхідні дані
GROUP BY customer_id
HAVING COUNT(*) > 10 -- 3. Фільтр ПІСЛЯ групування
AND AVG(amount) > 5000; -- 4. Бізнес-логіка
Чому так важливо:
WHEREскорочує кількість рядків ДО GROUP BY → менше роботиHAVINGфільтрує вже згруповані дані
Основні сценарії
1. Агреговані пороги:
-- Продавці з виручкою > 1 млн
SELECT seller_id, SUM(price) as revenue
FROM sales
GROUP BY seller_id
HAVING SUM(price) > 1000000;
2. Пошук дублікатів:
-- Дублікати email
SELECT email, COUNT(*) as cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
3. Перевірка властивостей групи:
-- Проєкти з проблемами (більше 2 заблокованих задач)
SELECT project_id
FROM tasks
GROUP BY project_id
HAVING COUNT(*) > 5
AND COUNT(*) FILTER (WHERE status = 'blocked') > 2;
4. HAVING без GROUP BY:
-- Перевірка умови по всій таблиці
SELECT 'Warning: High avg salary'
FROM employees
HAVING AVG(salary) > 200000;
-- Поверне 1 рядок якщо умова істинна, інакше 0 рядків
Типові помилки
- Фільтрація неагрегатного поля в 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; - Надлишковість в ORM
-- ORM часто генерує: SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow'; -- Це антипатерн! Місто має бути у WHERE -- Дозволяє використати індекс і скоротити дані для агрегації
Порівняння WHERE та HAVING
| Сценарій | WHERE | HAVING |
|---|---|---|
status = 'ACTIVE' |
✅ | ❌ (якщо немає GROUP BY) |
COUNT(*) > 10 |
❌ Не можна | ✅ |
SUM(amount) > 1000 |
❌ Не можна | ✅ |
city = 'Moscow' |
✅ | ⚠️ Можна, але погано |
🔴 Senior Level
Коли НЕ використовувати HAVING
- Для неагрегатних фільтрів —
HAVING city = 'Moscow'групує ВСІ міста, потім фільтрує. Перенесіть у WHERE - Для фільтрації за віконними функціями — віконні функції обчислюються ПІСЛЯ HAVING. Використовуйте підзапит/CTE
- Коли груп занадто багато — мільйони груп → spill на диск. Пре-фільтруйте у WHERE або використовуйте Materialized Views
Проблема huge number of groups
Якщо після WHERE залишається 100 млн рядків і 10 млн груп:
-- Проблема: база повинна виділити пам'ять для ВСІХ 10 млн груп
SELECT user_id, SUM(amount)
FROM orders
WHERE created_at > '2023-01-01' -- 100 млн рядків
GROUP BY user_id -- 10 млн груп
HAVING SUM(amount) > 10000; -- Фільтр після
Що відбувається:
- HashAggregate намагається розмістити 10 млн груп у
work_mem(work_mem — пам’ять для однієї операції, за замовчуванням 4MB) - Якщо не влізає → spill на диск (temp_files — тимчасові файли на диску)
- Продуктивність падає у 10-100 разів (операції з диском у мільйони разів повільніше RAM)
Рішення:
-- 1. Попередня фільтрація через CTE
WITH filtered_orders AS (
SELECT user_id, amount
FROM orders
WHERE created_at > '2023-01-01'
AND amount > 100 -- Додатковий фільтр
)
SELECT user_id, SUM(amount)
FROM filtered_orders
GROUP BY user_id
HAVING SUM(amount) > 10000;
-- 2. Матеріалізоване подання для частих запитів
CREATE MATERIALIZED VIEW mv_user_stats AS
SELECT user_id, SUM(amount) as total, COUNT(*) as cnt
FROM orders
GROUP BY user_id;
-- Потім швидкий запит
SELECT user_id FROM mv_user_stats WHERE total > 10000;
FILTER у HAVING для декларативності
-- Елегантна перевірка складних умов
SELECT project_id
FROM tasks
GROUP BY project_id
HAVING
COUNT(*) > 10 -- Мінімум 10 задач
AND COUNT(*) FILTER (WHERE status = 'done') >= 8 -- 80% виконано
AND COUNT(*) FILTER (WHERE status = 'blocked') = 0 -- Немає блокувань
AND MAX(due_date) > CURRENT_DATE; -- Дедлайн не минув
Redundancy та оптимізатор
PostgreSQL намагається робити Filter Push-down — переносити умови з HAVING у WHERE:
-- Запит
SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';
-- Оптимізатор перетворює на:
SELECT city, COUNT(*) FROM users WHERE city = 'Moscow' GROUP BY city;
АЛЕ: Це працює не завжди!
- Складні вирази з підзапитами
- Всередині Views
- CTE з
MATERIALIZED
Золоте правило: Завжди пишіть неагрегатні фільтри у WHERE явно.
Стандарти SQL та переносимість
-- ⚠️ Працює в 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;
Edge Cases
- HAVING з підзапитом
-- Користувачі, у яких сума замовлень більша за середню SELECT user_id, SUM(amount) as total FROM orders GROUP BY user_id HAVING SUM(amount) > (SELECT AVG(total) FROM ( SELECT SUM(amount) as total FROM orders GROUP BY user_id ) sub); - 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; -- Помилка! -- ✅ Рішення: 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; - HAVING з кількома GROUP BY
-- ROLLUP + HAVING SELECT department, city, COUNT(*) FROM employees GROUP BY ROLLUP(department, city) HAVING COUNT(*) > 5; -- Фільтрує ВСІ рівні (детальні та підсумкові рядки)
Performance Impact
-- Моніторинг тиску на work_mem
EXPLAIN (ANALYZE, BUFFERS)
SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 100;
-- Шукайте:
-- HashAggregate (memory usage: X kB) → якщо X близько до work_mem → spill ризик
-- HashAggregate (disk: Y kB) → spill стався! Збільште work_mem
Production Experience
Реальний сценарій:
- E-commerce: звіт з продажів (500 млн рядків)
- ORM генерував
HAVING status = 'COMPLETED'замістьWHERE - HashAggregate: 500 млн рядків → spill 30 ГБ на диск
- Час: 15 хвилин
- Рішення:
WHERE status = 'COMPLETED'до GROUP BY - Результат: 10 секунд (прискорення у 90 разів)
- Чому: ORM генерував
HAVING status = 'COMPLETED'— база спочатку групувала всі 500 млн рядків (включно зі скасованими, повернутими і т.д.), створюючи мільйони груп, і лише потім фільтрувала. Перенесення у WHERE скоротило вхідні дані для GROUP BY у 10 разів + дозволило використати індекс за status.
Monitoring
-- 1. Пошук запитів з HAVING у pg_stat_statements
SELECT query, mean_exec_time, calls, shared_blks_read
FROM pg_stat_statements
WHERE query LIKE '%HAVING%'
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 2. Перевірка на spill
EXPLAIN (ANALYZE, BUFFERS)
SELECT ... GROUP BY ... HAVING ...;
-- "Disk: X kB" → проблема!
-- 3. Перевірка Filter Push-down
EXPLAIN (VERBOSE)
SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';
-- У плані побачите, чи перенесено фільтр
Best Practices
- WHERE для неагрегатних фільтрів, HAVING для агрегатних
- FILTER (WHERE …) всередині HAVING для складних умов
- Уникайте аліасів з SELECT у HAVING (непереносимо)
- Моніторте work_mem при великих GROUP BY
- CTE/Materialized Views для попередньої фільтрації
- Перевіряйте план на spill (Disk usage)
- Пам’ятайте: HAVING не рятує від переповнення пам’яті під час GROUP BY
Резюме для Senior
- HAVING — останній рубіж фільтрації після GROUP BY
- Не економить ресурси на етапі агрегації — всі групи вже створені
- Filter Push-down (PG 12+) не завжди працює — пишіть WHERE явно
- work_mem тиск: HAVING фільтрує ВИХІД, не ВХІД
- Віконні функції недоступні у HAVING → CTE/підзапит
- FILTER для декларативності складних умов
- Переносимість: не використовуйте аліаси з SELECT у HAVING
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- HAVING фільтрує РЕЗУЛЬТАТ GROUP BY, не вхідні дані
- WHERE скорочує дані ДО GROUP BY → менше work_mem, швидше
- Filter Push-down (PG 12+): автоматичний перенос з HAVING у WHERE, але НЕ завжди
- Huge number of groups: 10 млн груп → spill на disk → CTE/Materialized View
- FILTER у HAVING:
COUNT(*) FILTER (WHERE status = 'blocked') > 2 - Віконні функції обчислюються ПІСЛЯ HAVING → підзапит/CTE
- Аліаси з SELECT у HAVING: НЕ переносимо (SQL Server/Oracle не підтримують)
- HAVING з підзапитом: можливо, але повільно
Часті уточнюючі запитання:
- «Чому ORM генерує HAVING замість WHERE?» → ORM антипатерн, не оптимізує
- «Як уникнути spill при 10 млн груп?» → Фільтр у WHERE, work_mem ↑, Materialized View
- «Чи можна FILTER у HAVING?» → Так, елегантно для складних умов
- «Коли HAVING з підзапитом виправданий?» → Порівняння групи із середньою по всіх
Червоні прапорці (НЕ говорити):
- ❌ «HAVING скорочує дані для GROUP BY» (ні, фільтрує ВИХІД)
- ❌ «Можна покластися на Filter Push-down» (не завжди працює!)
- ❌ «Віконні функції можна у HAVING» (ПІСЛЯ HAVING обчислюються!)
Пов’язані теми:
- [[В чому різниця між WHERE та HAVING]] → порядок виконання
- [[Що робить GROUP BY]] → huge number of groups
- [[Що таке віконні функції]] → чому не можна у HAVING