QA Interview Hub · База знань · Тести

SQL

Агрегація

COUNT, GROUP BY, HAVING, CASE

Які є агрегатні функції?

Агрегатна функція зводить багато рядків до одного значення:

  • COUNT — кількість
  • SUM — сума
  • AVG — середнє
  • MIN, MAX — мінімум і максимум (працюють і з датами, і з рядками)
SELECT COUNT(*)    AS total,
       SUM(amount) AS volume,
       AVG(amount) AS avg_amount,
       MAX(created_at) AS last_tx
FROM transactions
WHERE status = 'completed';
На співбесіді: Агрегатні функції зводять багато рядків до одного значення: COUNT рахує кількість, SUM — суму, AVG — середнє, MIN і MAX — крайні значення, і не лише для чисел, а й для дат. Наприклад, MAX від дати створення швидко показує, коли була остання транзакція.

Чим відрізняються COUNT(*), COUNT(col) і COUNT(DISTINCT col)?

  • COUNT(*) — кількість рядків, включно з тими, де є NULL.
  • COUNT(phone) — кількість рядків, де phone не NULL.
  • COUNT(DISTINCT user_id) — кількість унікальних непорожніх значень.

Якщо в таблиці 100 користувачів і 30 без телефону: COUNT(*) = 100, COUNT(phone) = 70. Різниця між ними — швидкий спосіб порахувати порожні значення.

SELECT COUNT(*)                AS users_total,
       COUNT(phone)            AS with_phone,
       COUNT(*) - COUNT(phone) AS without_phone
FROM users;
На співбесіді: COUNT зірочка рахує всі рядки, COUNT від стовпця — лише ті, де значення не NULL, а COUNT DISTINCT — кількість унікальних непорожніх значень. Різниця між першими двома одразу показує, скільки записів мають порожнє поле, — я часто так перевіряю повноту даних після міграції.

Як працює GROUP BY?

GROUP BY розбиває рядки на групи з однаковими значеннями і рахує агрегат для кожної групи окремо.

Правило: кожен стовпець у SELECT має бути або в GROUP BY, або всередині агрегатної функції. Інакше незрозуміло, яке з багатьох значень групи показати, і база поверне помилку.

-- кількість і обсяг транзакцій за типом і статусом
SELECT type, status, COUNT(*) AS cnt, SUM(amount) AS volume
FROM transactions
GROUP BY type, status
ORDER BY type, status;

-- помилка: email не в GROUP BY і не в агрегаті
SELECT country, email, COUNT(*) FROM users GROUP BY country;
На співбесіді: GROUP BY розбиває рядки на групи з однаковими значеннями і рахує агрегат для кожної групи. Правило просте: кожен стовпець у SELECT має бути або в GROUP BY, або в агрегатній функції. Я часто використовую групування за статусом, щоб побачити розподіл даних і помітити аномалії, наприклад неочікувано багато failed.

Чим WHERE відрізняється від HAVING?

  • WHERE фільтрує рядки до групування. Агрегати в ньому використовувати не можна.
  • HAVING фільтрує групи після групування — за результатами агрегатів.

Якщо умову можна поставити в WHERE, її ставлять туди: менше рядків іде в групування, запит швидший.

-- користувачі, у яких більше 3 невдалих транзакцій за вересень
SELECT a.user_id, COUNT(*) AS failed_cnt
FROM transactions t
JOIN accounts a ON a.id = t.account_id
WHERE t.status = 'failed'                      -- фільтр рядків
  AND t.created_at >= '2026-09-01'
  AND t.created_at <  '2026-10-01'
GROUP BY a.user_id
HAVING COUNT(*) > 3;                           -- фільтр груп
На співбесіді: WHERE фільтрує окремі рядки до групування, а HAVING — уже готові групи за результатами агрегатів. Тому умову на кшталт «кількість більше трьох» можна написати тільки в HAVING. А все, що можна відфільтрувати до групування, я ставлю в WHERE — так запит обробляє менше даних.

Як агрегати поводяться з NULL? Що таке COALESCE?

  • SUM, AVG, MIN, MAX ігнорують NULL. AVG від (100, NULL, 200) — це 150, а не 100.
  • Агрегат від порожнього набору рядків повертає NULL, а не 0. Виняток — COUNT, він поверне 0.

COALESCE(a, b, …) повертає перше значення, що не NULL. Ним підставляють значення за замовчуванням.

-- сума депозитів користувача без транзакцій — NULL, а не 0
SELECT SUM(amount) FROM transactions WHERE account_id = 999;          -- NULL
SELECT COALESCE(SUM(amount), 0) FROM transactions WHERE account_id = 999; -- 0
На співбесіді: Агрегатні функції ігнорують NULL, тому середнє рахується лише по заповнених значеннях. А сума по порожньому набору рядків повертає не нуль, а NULL — і якщо код цього не очікує, на екрані з'явиться порожнє поле або помилка. Для перетворення NULL на значення за замовчуванням використовую COALESCE.

Як використовувати CASE WHEN і умовну агрегацію?

CASE — умовна логіка в запиті, аналог if/else. Особливо корисна разом з агрегатами: можна порахувати кілька показників за один прохід.

SELECT
  type,
  COUNT(*) AS total,
  COUNT(*) FILTER (WHERE status = 'failed') AS failed,        -- PostgreSQL
  SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) AS completed_volume,
  ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'failed') / COUNT(*), 1) AS failed_pct
FROM transactions
GROUP BY type;

-- категорії сум
SELECT id,
  CASE WHEN amount < 100  THEN 'small'
       WHEN amount < 1000 THEN 'medium'
       ELSE 'large' END AS bucket
FROM transactions;
На співбесіді: CASE WHEN — це умовна логіка в запиті, як if-else. Найкорисніше поєднувати його з агрегатами: за один запит я можу порахувати загальну кількість, кількість невдалих і їхній відсоток для кожного типу транзакцій. Зверни увагу на 100.0 — без дробової частини ділення цілих чисел дасть нуль.

WHERE vs HAVING і групування

WHERE фільтрує окремі рядки до групування, тому в ньому не можна використовувати агрегатні функції. HAVING фільтрує вже сформовані групи після GROUP BY і працює з агрегатами: COUNT, SUM, AVG. Порядок виконання: FROM, JOIN, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.

Запит — кількість замовлень на користувача, лише де більше 5:

SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY orders_count DESC;

У SELECT з GROUP BY можна вказувати лише поля з групування та агрегати. Якщо треба ще й ім'я користувача, додаю JOIN з users і групую за id та name. Фільтр на зразок «лише оплачені замовлення» ставлю у WHERE — так він відпрацює до групування й буде ефективнішим.