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

SQL

Підзапити, CTE, множини

IN, EXISTS, WITH, UNION

Що таке підзапит і де його можна використовувати?

Підзапит — запит усередині іншого запиту, у дужках.

  • Скалярний — повертає одне значення, можна порівнювати: WHERE amount > (SELECT AVG(amount) …).
  • У WHERE з IN / EXISTS — перевірка входження чи існування.
  • У FROM — тимчасова таблиця, обов'язково з аліасом.
  • Корельований — посилається на зовнішній запит і логічно виконується для кожного його рядка.
-- транзакції, більші за середню
SELECT id, amount FROM transactions
WHERE amount > (SELECT AVG(amount) FROM transactions);

-- корельований: найбільша транзакція кожного рахунку
SELECT t.* FROM transactions t
WHERE t.amount = (SELECT MAX(t2.amount)
                  FROM transactions t2
                  WHERE t2.account_id = t.account_id);
На співбесіді: Підзапит — це запит у дужках усередині іншого запиту. Він може повертати одне значення для порівняння, список для IN, тимчасову таблицю у FROM, а може бути корельованим — посилатися на зовнішній запит і виконуватися для кожного його рядка. Наприклад, знайти транзакції, більші за середню, найпростіше скалярним підзапитом.

Чим IN відрізняється від EXISTS?

  • IN порівнює значення зі списком, який повертає підзапит.
  • EXISTS лише перевіряє, чи підзапит повертає хоча б один рядок, і може зупинитися на першому знайденому.

Для позитивних перевірок сучасні оптимізатори часто виконують обидва варіанти однаково. Реальна різниця — у роботі з NULL у заперечній формі: NOT EXISTS поводиться передбачувано, а NOT IN — ні (див. наступне питання).

-- користувачі, що мають хоч одну невдалу транзакцію
SELECT u.email FROM users u
WHERE EXISTS (
  SELECT 1 FROM accounts a
  JOIN transactions t ON t.account_id = a.id
  WHERE a.user_id = u.id AND t.status = 'failed'
);
На співбесіді: IN порівнює значення зі списком із підзапиту, а EXISTS лише перевіряє, чи є хоч один рядок, і може зупинитися на першому. У позитивній формі сучасні бази часто виконують їх однаково, тому головна різниця — у запереченні: NOT EXISTS безпечний, а NOT IN ламається, якщо в підзапиті є NULL.

Чому NOT IN може повернути нуль рядків?

Якщо підзапит у NOT IN повертає хоча б один NULL, умова для кожного рядка стає UNKNOWN, і результат — порожній. Без жодної помилки.

Логіка: 5 NOT IN (1, 2, NULL) означає 5 <> 1 AND 5 <> 2 AND 5 <> NULL. Останнє — UNKNOWN, отже вся умова не TRUE.

Рішення: NOT EXISTS, anti-join через LEFT JOIN або явно відфільтрувати NULL у підзапиті.

-- якщо хоч один account.user_id = NULL — поверне 0 рядків
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM accounts);

-- безпечно
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM accounts a WHERE a.user_id = u.id);
На співбесіді: Якщо підзапит у NOT IN повертає хоча б один NULL, порівняння з ним дає «невідомо», і весь запит повертає нуль рядків без помилки. Тому для пошуку відсутніх записів я використовую NOT EXISTS або LEFT JOIN з перевіркою на NULL. Такий баг легко пропустити: запит виглядає правильним і просто нічого не знаходить.

Що таке CTE (WITH) і навіщо він потрібен?

CTE (Common Table Expression) — іменований тимчасовий результат, оголошений через WITH перед основним запитом. Те саме, що підзапит у FROM, але з назвою.

  • Читабельність: складний запит розбивається на зрозумілі кроки зверху вниз.
  • Перевикористання: на один CTE можна послатися кілька разів.
  • Рекурсія: WITH RECURSIVE обходить ієрархії, наприклад дерево категорій чи підпорядкування.
WITH monthly AS (
  SELECT a.user_id, SUM(t.amount) AS deposits
  FROM transactions t
  JOIN accounts a ON a.id = t.account_id
  WHERE t.type = 'deposit' AND t.status = 'completed'
    AND t.created_at >= '2026-09-01' AND t.created_at < '2026-10-01'
  GROUP BY a.user_id
)
SELECT u.email, m.deposits
FROM monthly m
JOIN users u ON u.id = m.user_id
WHERE m.deposits > 10000
ORDER BY m.deposits DESC;
На співбесіді: CTE — це іменований проміжний результат, оголошений через WITH. Він робить складний запит читабельним: я розбиваю його на кроки, кожен з назвою, і можу послатися на крок кілька разів. Ще є рекурсивний варіант для ієрархій, наприклад дерева категорій.

Чим UNION відрізняється від UNION ALL? Що таке INTERSECT і EXCEPT?

  • UNION об'єднує результати двох запитів і прибирає дублікати — тому повільніший.
  • UNION ALL об'єднує все як є, з дублікатами. Якщо дублікатів бути не може або вони потрібні — бери його.
  • INTERSECT — рядки, що є в обох результатах.
  • EXCEPT — рядки з першого, яких немає в другому (в Oracle — MINUS).

Умова: однакова кількість стовпців і сумісні типи.

-- email-и, що є в старій системі, але відсутні в новій після міграції
SELECT email FROM legacy_users
EXCEPT
SELECT email FROM users;
На співбесіді: UNION об'єднує результати і прибирає дублікати, UNION ALL — без прибирання, тому швидший. INTERSECT дає спільні рядки, а EXCEPT — рядки першого запиту, яких немає в другому. EXCEPT дуже зручний для перевірки міграцій: одним запитом бачу, які записи не перенеслися.