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 — так він відпрацює до групування й буде ефективнішим.