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

У чому різниця між WHERE та HAVING?

Запит виконується НЕ в тому порядку, в якому написаний:

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

🟢 Junior Level

WHERE і HAVING — обидва використовуються для фільтрації даних, але на різних етапах:

  • WHERE — фільтрує рядки до групування
  • HAVING — фільтрує групи після групування

Проста аналогія: Уявіть, що ви сортуєте монети:

  1. WHERE — відбираєте лише монети певного номіналу
  2. Групуєте за роками
  3. 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
Агрегати ❌ Не можна ✅ Можна
Індекси ✅ Працюють ❌ Не працюють
Продуктивність Швидко Повільніше (після агрегації)

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

  1. Агрегат у WHERE
    -- ❌ ПОМИЛКА: агрегати не можна у WHERE
    SELECT user_id FROM orders WHERE COUNT(*) > 5;
    
    -- ✅ Правильно: через HAVING
    SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5;
    
  2. Фільтрація неагрегатного поля в 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;
    
  3. Аліас з 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

Коли НЕ використовувати

  1. HAVING для неагрегатних фільтрівHAVING city = 'Moscow' групує ВСІ міста потім фільтрує. Використовуйте WHERE
  2. WHERE з агрегатамиWHERE COUNT(*) > 5 не можна. Використовуйте HAVING
  3. 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

  1. NULL у HAVING
    SELECT status, COUNT(*)
    FROM orders
    GROUP BY status
    HAVING COUNT(*) > 10;
    
    -- NULL — це окрема група!
    -- Якщо є рядки з status = NULL, вони згрупуються разом
    
  2. 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
    
  3. 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

  1. WHERE для фільтрації рядків (до GROUP BY)
  2. HAVING лише для фільтрації за агрегатами
  3. Ніколи не використовуйте HAVING для неагрегатних умов
  4. Перевіряйте план на наявність spill (Disk usage)
  5. Фільтруйте якомога раніше — економте CPU та RAM
  6. FILTER всередині HAVING для складних умов
  7. Підзапит/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