<? phpukraine СПІВБЕСІДИ
Пошук по платформі
SQL · JUNIOR ЧАСТО ПИТАЮТЬ

Чим HAVING відрізняється від WHERE і як працює GROUP BY?

WHERE фільтрує рядки до групування й не бачить агрегатів, GROUP BY згортає рядки в групи, HAVING фільтрує вже готові групи за значенням агрегату.

Чому запит із WHERE COUNT(*) > 3 падає з помилкою?
У якому порядку база виконує WHERE, GROUP BY і HAVING?
Чи можна писати HAVING без GROUP BY і що тоді буде?
Чому MySQL лається на колонку, якої немає в GROUP BY?
GROUP BY HAVING агрегати MySQL PostgreSQL

Щоб зрозуміти різницю, треба тримати в голові логічний порядок виконання запиту: FROM і JOIN збирають рядки, WHERE фільтрує їх по одному, GROUP BY згортає рядки в групи за однаковими значеннями ключа групування, всередині груп обчислюються агрегати (COUNT, SUM, AVG, MIN, MAX), HAVING відкидає непотрібні групи, і тільки потім працюють SELECT, ORDER BY та LIMIT. Це не порядок написання, а порядок сенсу — фізично планувальник може робити по-своєму, але результат зобовʼязаний збігатися з цією моделлю.

Звідси головна відповідь: WHERE не бачить COUNT(*), бо на момент його роботи груп ще не існує — є лише окремі рядки, і запит WHERE COUNT(*) >= 3 падає з помилкою (Invalid use of group function у MySQL, aggregate functions are not allowed in WHERE у PostgreSQL). HAVING, навпаки, виконується після агрегації, тому оперує вже порахованими значеннями. Симетрично HAVING майже не має сенсу для звичайної колонки: після групування окремих рядків уже немає, є ключ групування й агрегати.

Другий бік — продуктивність. Технічно можна винести умову на звичайну колонку в HAVING (MySQL це проковтне, бо дозволяє в HAVING посилатися на колонки й аліаси SELECT; PostgreSQL стандартно вимагатиме агрегат або колонку з GROUP BY). Але тоді база згрупує зайві рядки й лише потім їх викине, а індекс по цій колонці не спрацює для звуження пошуку. Правило просте: умова про значення рядка — у WHERE, умова про результат агрегації — у HAVING.

Після GROUP BY у SELECT дозволені тільки колонки з ключа групування та агрегати. MySQL до 5.7.5 із вимкненим ONLY_FULL_GROUP_BY мовчки повертав довільне значення з групи, і це давало нестабільні результати між запусками; з 5.7.5 режим увімкнений за замовчуванням, а PostgreSQL поводився так завжди. Виняток — функційна залежність: якщо групувати за первинним ключем (GROUP BY u.id), інші колонки цієї таблиці брати можна, бо вони визначені однозначно (PostgreSQL з 9.1, MySQL 8).

Дві деталі, на яких найчастіше помиляються джуни. Перша: COUNT(*) рахує рядки групи, а COUNT(column), SUM, AVG ігнорують NULL — тому COUNT(o.coupon) менший за COUNT(*), і це не баг. Друга: GROUP BY не гарантує сортування. У старих MySQL воно було побічним ефектом реалізації, у 8.0 неявне сортування й модифікатори ASC/DESC для GROUP BY прибрані, тож потрібний порядок задають явним ORDER BY.

-- Питання: користувачі, у яких від 3 оплачених замовлень за 2026 рік,
-- на суму понад 10000, найбільші зверху.
SELECT
    u.id,
    u.email,
    COUNT(*)        AS paid_orders,   -- рядків у групі
    SUM(o.total)    AS revenue,       -- SUM ігнорує NULL
    COUNT(o.coupon) AS with_coupon    -- рахує лише не-NULL купони
FROM users u
JOIN orders o ON o.user_id = u.id
-- WHERE працює до групування: відсікає рядки, може використати індекс
WHERE o.status = 'paid'
  AND o.created_at >= '2026-01-01'
-- GROUP BY згортає рядки в групи; u.email можна брати,
-- бо u.id — первинний ключ (функційна залежність)
GROUP BY u.id, u.email
-- HAVING працює після агрегації: тут і тільки тут доступні COUNT/SUM
HAVING COUNT(*) >= 3 AND SUM(o.total) > 10000
-- GROUP BY не сортує сам: у MySQL 8.0 неявне сортування прибрано
ORDER BY revenue DESC
LIMIT 20;

-- Помилка: агрегата у WHENE ще не існує, груп немає
-- WHERE COUNT(*) >= 3        -- ERROR: Invalid use of group function / aggregate not allowed

-- Кілька лічильників за один прохід (PostgreSQL)
SELECT user_id,
       COUNT(*)                                   AS all_orders,
       COUNT(*) FILTER (WHERE status = 'paid')    AS paid_orders
FROM orders
GROUP BY user_id;
Логічний порядок виконання: FROM/JOIN → WHERE → GROUP BY → агрегати → HAVING → SELECT → ORDER BY → LIMIT; звідси все інше випливає.
Що WHERE не бачить COUNT/SUM, бо на момент його роботи груп ще немає, а HAVING бачить, бо працює вже після агрегації.
Що умову на звичайну колонку треба ставити у WHERE, а не в HAVING: менше рядків для групування і доступний індекс.
Що після GROUP BY у SELECT можна брати лише колонки з GROUP BY або агрегати — ONLY_FULL_GROUP_BY у MySQL з 5.7.5 увімкнено за замовчуванням.
Що COUNT(*) рахує рядки, а COUNT(col) і SUM/AVG ігнорують NULL — це різні числа.
Писати WHERE COUNT(*) > 3 — синтаксична помилка, агрегат у WHERE неможливий.
Переносити всі умови в HAVING «щоб працювало»: запит дає правильний результат, але групує зайві рядки й не використовує індекс.
Плутати «фільтр до групування» і «фільтр після»: WHERE status = 'paid' і HAVING SUM(...) > 0 дають різні відповіді на різні питання.
Вважати, що GROUP BY сам сортує результат: у MySQL 8.0 неявне сортування прибрано, без ORDER BY порядок не визначений.
Використовувати COUNT(column) там, де треба порахувати всі рядки групи, і дивуватися меншому числу через NULL.
ПОРАДА

Скажіть одним реченням: «WHERE — про рядки, HAVING — про групи», і одразу назвіть порядок виконання. Далі додайте, що умову на звичайну колонку завжди тримають у WHERE, бо це менше роботи для GROUP BY.

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

База спершу застосовує WHERE до окремих рядків, потім GROUP BY будує групи й рахує агрегати, і лише після цього HAVING відкидає непотрібні групи.