Питання 13 · Розділ 1

Коли потрібно використовувати HAVING?

Правильний розподіл фільтрів — ознака хорошого SQL:

Мовні версії: English Russian Ukrainian

🟢 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 рядків

Типові помилки

  1. Фільтрація неагрегатного поля в 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;
    
  2. Надлишковість в 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

  1. Для неагрегатних фільтрівHAVING city = 'Moscow' групує ВСІ міста, потім фільтрує. Перенесіть у WHERE
  2. Для фільтрації за віконними функціями — віконні функції обчислюються ПІСЛЯ HAVING. Використовуйте підзапит/CTE
  3. Коли груп занадто багато — мільйони груп → 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;       -- Фільтр після

Що відбувається:

  1. HashAggregate намагається розмістити 10 млн груп у work_mem (work_mem — пам’ять для однієї операції, за замовчуванням 4MB)
  2. Якщо не влізає → spill на диск (temp_files — тимчасові файли на диску)
  3. Продуктивність падає у 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

  1. 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);
    
  2. 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;
    
  3. 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

  1. WHERE для неагрегатних фільтрів, HAVING для агрегатних
  2. FILTER (WHERE …) всередині HAVING для складних умов
  3. Уникайте аліасів з SELECT у HAVING (непереносимо)
  4. Моніторте work_mem при великих GROUP BY
  5. CTE/Materialized Views для попередньої фільтрації
  6. Перевіряйте план на spill (Disk usage)
  7. Пам’ятайте: 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