SQL JOIN: як поєднати таблиці — INNER, LEFT, RIGHT
Реальні дані ніколи не лежать в одній таблиці. Замовлення — в одній, клієнти — в другій, товари — в третій. Щоб відповісти навіть на просте запитання «хто з клієнтів зробив замовлення минулого місяця», потрібно поєднати кілька таблиць. Саме для цього в SQL існує оператор JOIN.

Це урок 2 безкоштовного курсу аналітики даних від SEOWORK. У попередньому уроці ви навчилися писати SELECT-запити до однієї таблиці. Тепер зробимо наступний крок: розберемо реляційну модель, первинні та зовнішні ключі, чотири типи JOIN і self-join.
Після цього уроку ви зможете писати запити на 2–3 таблиці, розуміти різницю між INNER і LEFT JOIN і не потрапляти в дві найпоширеніші пастки — дублікати рядків і втрачені NULL-рядки. Усі приклади — на демо-базі інтернет-магазину в MySQL.
Реляційна модель: чому дані розкладені по таблицях
Уявіть, що всі дані магазину зберігаються в одній гігантській таблиці: у кожному рядку замовлення — ще й ім’я клієнта, його місто, назва товару, категорія, ціна. Якщо клієнт зробив 50 замовлень, його ім’я повторюється 50 разів. Клієнт змінив місто — доведеться оновити 50 рядків, і достатньо пропустити один, щоб дані стали суперечливими.
Реляційна модель розв’язує цю проблему: кожна сутність живе у своїй таблиці, а таблиці посилаються одна на одну через ключі. У нашій демо-базі магазину чотири таблиці:
- customers (id, name, city, created_at) — клієнти;
- orders (id, customer_id, order_date, amount, status) — замовлення;
- products (id, name, category, price) — товари;
- order_items (order_id, product_id, qty) — позиції замовлення: який товар і в якій кількості входить у замовлення.
Первинний і зовнішній ключі
Первинний ключ (primary key) — стовпець, який унікально ідентифікує рядок. У таблиці customers це id: двох клієнтів з однаковим id бути не може.
Зовнішній ключ (foreign key) — стовпець, який посилається на первинний ключ іншої таблиці. У таблиці orders стовпець customer_id — зовнішній ключ до customers.id. Так СУБД «знає», що замовлення з customer_id = 7 належить клієнту з id = 7. Саме по цих парах ключів ми і поєднуємо таблиці в JOIN.
INNER JOIN: тільки збіги з обох таблиць
INNER JOIN повертає лише ті рядки, для яких знайшовся збіг в обох таблицях. Синтаксис такий:
SELECT c.name, c.city, o.id AS order_id, o.amount
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.id
WHERE o.status = 'completed';Розберемо по частинах. У FROM вказуємо першу таблицю, в INNER JOIN — другу, а в ON — умову поєднання: рядки «склеюються», коли o.customer_id дорівнює c.id. Псевдоніми o та c скорочують запис і роблять зрозумілим, з якої таблиці кожен стовпець.
Важливий наслідок: якщо клієнт ще не зробив жодного замовлення, у результат INNER JOIN він не потрапить — збігу для нього немає. Це не помилка, а властивість INNER JOIN, і про неї треба пам’ятати.
LEFT JOIN: усі рядки лівої таблиці
LEFT JOIN повертає всі рядки лівої таблиці (тієї, що у FROM), а з правої підтягує збіги. Якщо збігу немає, стовпці правої таблиці заповнюються NULL. Класична задача — знайти клієнтів без замовлень:
SELECT c.id, c.name, c.city, o.id AS order_id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
WHERE o.id IS NULL;Тут ми беремо всіх клієнтів, приєднуємо замовлення, а потім фільтром o.id IS NULL залишаємо тільки тих, у кого замовлень не знайшлося. INNER JOIN таку задачу розв’язати не може в принципі — він би просто викинув цих клієнтів.
Правило вибору просте: якщо вам потрібні тільки рядки зі збігом — беріть INNER JOIN. Якщо потрібні всі рядки однієї з таблиць незалежно від збігу — LEFT JOIN.
RIGHT JOIN: дзеркальний варіант
RIGHT JOIN працює так само, як LEFT, тільки зберігає всі рядки правої таблиці. Ці два запити повертають однаковий результат:
-- варіант 1: RIGHT JOIN
SELECT c.name, o.amount
FROM orders AS o
RIGHT JOIN customers AS c ON o.customer_id = c.id;
-- варіант 2: те саме через LEFT JOIN
SELECT c.name, o.amount
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id;На практиці RIGHT JOIN використовують рідко: будь-який RIGHT можна переписати як LEFT, помінявши таблиці місцями, а запити з LEFT читаються природніше — «беремо основну таблицю і додаємо до неї дані». Знати RIGHT JOIN потрібно, щоб читати чужий код, але у власних запитах простіше триматися LEFT.
CROSS JOIN: усі можливі комбінації
CROSS JOIN поєднує кожен рядок першої таблиці з кожним рядком другої — без умови ON. Результат — декартів добуток: 100 клієнтів × 50 товарів = 5000 рядків.
SELECT c.name, p.name AS product_name
FROM customers AS c
CROSS JOIN products AS p;Звучить безглуздо? В аналітиці CROSS JOIN корисний, коли потрібна повна «сітка» комбінацій — наприклад, усі пари «клієнт × товар», щоб потім позначити, хто що купував, а хто ні. Але випадковий CROSS JOIN (коли ви просто забули умову ON) на великих таблицях породжує мільйони рядків і кладе запит. Про це — у блоці помилок.
Порівняння типів JOIN
| Тип JOIN | Що повертає | Коли використовувати |
|---|---|---|
| INNER JOIN | Тільки рядки зі збігом в обох таблицях | Потрібні лише пов’язані дані: замовлення разом із клієнтами |
| LEFT JOIN | Усі рядки лівої таблиці + збіги з правої (інакше NULL) | Потрібні всі рядки основної таблиці: клієнти, зокрема без замовлень |
| RIGHT JOIN | Усі рядки правої таблиці + збіги з лівої (інакше NULL) | Рідко; майже завжди переписується через LEFT JOIN |
| CROSS JOIN | Усі комбінації рядків (декартів добуток) | Повна сітка комбінацій, наприклад «клієнт × товар» |
Поєднуємо три таблиці: замовлення, позиції, товари
JOIN-и можна ланцюжком з’єднувати скільки завгодно. Типова задача: показати, які товари входять у кожне завершене замовлення. Тут потрібні orders, order_items і products:
SELECT o.id AS order_id,
o.order_date,
p.name AS product_name,
p.category,
oi.qty,
oi.qty * p.price AS line_total
FROM orders AS o
INNER JOIN order_items AS oi ON oi.order_id = o.id
INNER JOIN products AS p ON p.id = oi.product_id
WHERE o.status = 'completed'
ORDER BY o.order_date DESC;Логіка проста: спершу до замовлень приєднуємо позиції (order_items — «місток» між замовленнями й товарами), потім до позицій — товари. Стовпець line_total рахуємо прямо в запиті: кількість × ціна.
Зверніть увагу: одне замовлення з трьома позиціями дасть три рядки в результаті. Це нормально — так і має працювати зв’язок «один до багатьох».
Self-join: таблиця поєднується сама з собою
Іноді порівнювати треба рядки однієї таблиці між собою. Наприклад, знайти пари клієнтів з одного міста — для аналізу регіональних сегментів:
SELECT c1.name AS customer_1,
c2.name AS customer_2,
c1.city
FROM customers AS c1
INNER JOIN customers AS c2
ON c1.city = c2.city
AND c1.id < c2.id;Трюк у псевдонімах: ми беремо ту саму таблицю customers двічі, як c1 і c2, і СУБД працює з ними як із двома різними таблицями. Умова c1.id < c2.id прибирає пари «клієнт сам із собою» і дзеркальні дублікати (Іван–Марія та Марія–Іван).
Поширені помилки
1. Дублікати через зв’язок «один до багатьох»
Ви приєднали order_items до orders і порахували суму замовлень — а вона вдвічі більша за реальну. Причина: кожне замовлення розмножилося на кількість своїх позицій, і o.amount тепер повторюється в кожному рядку. Правило: перш ніж агрегувати, перевірте, на якому рівні деталізації ваш результат, і рахуйте суми по таблиці відповідного рівня.
2. WHERE «вбиває» LEFT JOIN
Запит нижче виглядає як LEFT JOIN, а працює як INNER:
-- ПОМИЛКА: клієнти без замовлень зникнуть
SELECT c.name, o.amount
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'completed';
-- ПРАВИЛЬНО: умову по правій таблиці — в ON
SELECT c.name, o.amount
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
AND o.status = 'completed';У клієнтів без замовлень o.status дорівнює NULL, а умова NULL = 'completed' не є істинною — WHERE відкидає такі рядки. Якщо хочете зберегти всі рядки лівої таблиці, фільтруйте праву таблицю в ON, а не у WHERE.
3. Порівняння з NULL через «=»
Умова WHERE o.id = NULL не поверне жодного рядка: NULL не дорівнює нічому, навіть іншому NULL. Для перевірки на NULL використовуйте тільки IS NULL та IS NOT NULL.
4. Забута умова ON — випадковий декартів добуток
Якщо перелічити таблиці через кому без умови поєднання (FROM orders, customers), MySQL поверне всі комбінації рядків. На таблицях по 10 000 рядків це 100 мільйонів рядків результату. Завжди пишіть явний JOIN … ON — так помилку видно одразу.
5. Неоднозначні імена стовпців
Стовпець id є і в orders, і в customers. Запит SELECT id FROM orders o JOIN customers c ... впаде з помилкою «Column ‘id’ in field list is ambiguous». Звикайте завжди писати стовпці з префіксом таблиці: o.id, c.id — це і помилку прибирає, і код читається легше.
FAQ: часті питання про SQL JOIN
Чим JOIN відрізняється від UNION?
JOIN поєднує таблиці «вшир»: додає стовпці однієї таблиці до рядків іншої за умовою збігу. UNION складає результати двох запитів «у висоту»: рядки другого запиту дописуються під рядками першого, а набір стовпців має збігатися. Клієнтів і їхні замовлення поєднують JOIN-ом, а, скажімо, два однакові за структурою звіти за різні роки — UNION-ом.
Що швидше — INNER JOIN чи LEFT JOIN?
Це різні за змістом операції, тому порівнювати їх за швидкістю некоректно: обирати тип JOIN треба за потрібним результатом. За однакових умов INNER JOIN зазвичай не повільніший, бо повертає менше або стільки ж рядків. Реальний вплив на швидкість мають індекси на стовпцях із умови ON — переконайтеся, що зовнішні ключі проіндексовані.
Чи існує FULL JOIN у MySQL?
Ні, MySQL не підтримує FULL OUTER JOIN (усі рядки обох таблиць). Його емулюють через комбінацію LEFT JOIN і RIGHT JOIN, об’єднану оператором UNION. У більшості практичних задач аналітика достатньо LEFT JOIN, тому відсутність FULL JOIN рідко стає проблемою.
Скільки таблиць можна поєднати в одному запиті?
Технічний ліміт MySQL — 61 таблиця в одному запиті, але на практиці ви впретеся в читабельність значно раніше. Запити на 3–5 таблиць — норма для аналітика. Якщо таблиць стає більше, зазвичай варто розбити логіку на підзапити — про них ітиметься в наступному уроці.
Що далі
Ви розібрали реляційну модель, ключі та всі основні типи JOIN — тепер вам доступні запити до кількох таблиць одразу. Потренуйтеся: напишіть запит, який виводить назви товарів і міста клієнтів, що їх купували (знадобляться всі чотири таблиці демо-бази).
- Наступний урок: GROUP BY, підзапити та CASE у SQL: практичний гайд — навчитеся агрегувати дані та рахувати метрики по групах.
- Попередній урок: Що таке SQL: основи для початківців з прикладами — якщо потрібно повторити SELECT, WHERE і ORDER BY.
- Повна програма: курс аналітики даних з нуля.
Це урок 2 з 9 курсу аналітики даних.
Уроки виходять щодня о 09:30 — програма і навігація на сторінці курсу.