Пошук уроків, статей та іншого контенту
Найпоширеніша плутанина в SQL-запитах: чому LEFT JOIN іноді повертає рядки з NULL, а INNER JOIN їх мовчки прибирає.
JOINJOIN об’єднує рядки з двох таблиць за певною умовою. Найчастіше таблиці пов’язані через зовнішній ключ.
Наприклад, є таблиці:
customers — клієнти;
orders — замовлення.
Один клієнт може мати багато замовлень, а деякі клієнти можуть ще не мати жодного замовлення.
customers
+----+---------+
| id | name |
+----+---------+
| 1 | Олена |
| 2 | Андрій |
| 3 | Марія |
+----+---------+
orders
+----+-------------+--------+
| id | customer_id | amount |
+----+-------------+--------+
| 101| 1 | 500 |
| 102| 1 | 250 |
| 103| 2 | 800 |
+----+-------------+--------+У цьому прикладі:
Олена має два замовлення;
Андрій має одне замовлення;
Марія не має замовлень.
Умова об’єднання виглядатиме так:
customers.id = orders.customer_idСаме тип JOIN визначає, які рядки залишаться у результаті, якщо відповідності немає.
INNER JOIN: тільки рядки з відповідністюINNER JOIN повертає лише ті рядки, для яких знайдено пару в обох таблицях.
SELECT
customers.name,
orders.amount
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;Результат:
name | amount
--------+-------
Олена | 500
Олена | 250
Андрій | 800Марія не потрапила до результату, тому що для неї немає відповідного рядка в orders.
INNER JOINЗапит можна описати так:
Візьми клієнтів і залиш тільки тих, для кого існує замовлення.
INNER JOIN не створює рядок із NULL, якщо відповідність не знайдена. Він просто відкидає такий рядок.
Це зручно, коли потрібні лише сутності, пов’язані з іншою таблицею:
клієнти, які зробили замовлення;
товари, які хоча б раз продавалися;
користувачі, які мають профілі;
працівники, призначені на проєкти.
LEFT JOIN: усі рядки з лівої таблиціLEFT JOIN, або LEFT OUTER JOIN, повертає:
усі рядки з лівої таблиці;
відповідні рядки з правої таблиці;
NULL у колонках правої таблиці, якщо відповідності немає.
SELECT
customers.name,
orders.amount
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id;Результат:
name | amount
--------+-------
Олена | 500
Олена | 250
Андрій | 800
Марія | NULLМарія залишилася в результаті, хоча замовлення для неї не знайдено. Значення NULL означає не нуль і не порожній рядок, а відсутність значення.
LEFT JOINЗапит можна описати так:
Покажи всіх клієнтів і додай їхні замовлення, якщо вони є.
Саме тому LEFT JOIN часто використовують для пошуку записів без пов’язаних даних:
SELECT
customers.id,
customers.name
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.id IS NULL;Цей запит поверне клієнтів без замовлень:
id | name
---+------
3 | МаріяУмова orders.id IS NULL означає, що для клієнта не знайшлося жодного рядка в orders.
RIGHT JOIN: усі рядки з правої таблиціRIGHT JOIN, або RIGHT OUTER JOIN, працює дзеркально до LEFT JOIN:
повертає всі рядки з правої таблиці;
додає відповідні рядки з лівої таблиці;
підставляє NULL для колонок лівої таблиці, якщо відповідності немає.
Наприклад, нехай існує таблиця payments:
SELECT
customers.name,
payments.amount
FROM customers
RIGHT JOIN payments
ON customers.id = payments.customer_id;Усі платежі залишаться в результаті, навіть якщо для якогось платежу не знайдено клієнта. У такому випадку customers.name матиме значення NULL.
Практично RIGHT JOIN використовують рідше, тому що його легко переписати через LEFT JOIN, помінявши таблиці місцями:
SELECT
customers.name,
payments.amount
FROM payments
LEFT JOIN customers
ON customers.id = payments.customer_id;Такий варіант часто читається простіше: спочатку вказано таблицю, усі рядки якої потрібно зберегти.
JOINНехай є дві множини:
ліва таблиця — customers;
права таблиця — orders.
Тоді:
INNER JOIN — тільки перетин двох множин;
LEFT JOIN — усі рядки лівої таблиці та збіги з правої;
RIGHT JOIN — усі рядки правої таблиці та збіги з лівої.
У термінах результату:
INNER JOIN: тільки A ∩ B
LEFT JOIN: усі A + відповідні рядки B
RIGHT JOIN: усі B + відповідні рядки AВажливо: LEFT JOIN не означає, що в результаті буде рівно по одному рядку на кожен рядок лівої таблиці. Якщо праворуч знайдено кілька відповідностей, лівий рядок повториться кілька разів.
Для Олени з двома замовленнями результат містить два рядки.
NULLNULL з’являється тоді, коли зовнішній JOIN не може знайти відповідний рядок.
SELECT
customers.name,
orders.id AS order_id
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id;Результат:
name | order_id
--------+---------
Олена | 101
Олена | 102
Андрій | 103
Марія | NULLNULL тут означає:
Для цього клієнта немає відповідного замовлення.
Це не те саме, що:
0;
'';
FALSE;
текст 'NULL'.
SQL має спеціальні правила для роботи з NULL. Наприклад, перевірка на відсутність значення виконується через IS NULL, а не через = NULL:
-- Правильна перевірка
WHERE orders.id IS NULL
-- Неправильна перевірка
WHERE orders.id = NULLПорівняння = NULL не повертає TRUE, тому такий рядок умови не працює як очікується.
ON і WHEREОдна з найпоширеніших помилок — перенесення умови з ON у WHERE.
Припустімо, потрібно отримати всіх клієнтів і лише їхні замовлення на суму понад 500.
ONSELECT
customers.name,
orders.amount
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
AND orders.amount > 500;Результат:
name | amount
--------+-------
Олена | NULL
Андрій | 800
Марія | NULLУсі клієнти збережені. Замовлення Олени не відповідають умові amount > 500, тому для неї з’явився NULL.
WHERESELECT
customers.name,
orders.amount
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.amount > 500;Результат:
name | amount
--------+-------
Андрій | 800Чому зникли Олена і Марія? Для них orders.amount має значення NULL, а умова orders.amount > 500 не виконується.
Фактично такий запит поводиться подібно до INNER JOIN щодо цієї умови.
ON визначає, які рядки вважаються відповідністю під час об’єднання;
WHERE фільтрує вже сформований результат.
Якщо потрібно зберегти всі рядки лівої таблиці, фільтр для правої таблиці часто варто розміщувати в ON.
LEFT JOIN і фільтр за значеннямРозглянемо ще один приклад. Потрібно показати всіх клієнтів, але додати тільки замовлення, створені у 2026 році.
Варіант із фільтром у ON:
SELECT
customers.name,
orders.id,
orders.created_at
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
AND orders.created_at >= '2026-01-01'
AND orders.created_at < '2027-01-01';Клієнти без замовлень у потрібному періоді залишаться в результаті з NULL.
Якщо ж поставити ці умови у WHERE, клієнти без відповідних замовлень будуть вилучені:
SELECT
customers.name,
orders.id,
orders.created_at
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.created_at >= '2026-01-01'
AND orders.created_at < '2027-01-01';Цей варіант підходить лише тоді, коли потрібно отримати клієнтів, у яких такі замовлення справді є.
JOININNER JOIN, якщо:потрібні тільки записи з відповідністю;
відсутність пов’язаного запису означає, що рядок треба виключити;
потрібно отримати клієнтів із замовленнями;
потрібно показати товари, які мають категорію.
Приклад:
SELECT
products.name,
categories.name AS category_name
FROM products
INNER JOIN categories
ON products.category_id = categories.id;LEFT JOIN, якщо:потрібно зберегти всі записи основної таблиці;
пов’язаний запис може бути відсутнім;
потрібно знайти записи без зв’язку;
потрібно показати повний список сутностей із додатковими даними, якщо вони існують.
Приклад:
SELECT
products.name,
categories.name AS category_name
FROM products
LEFT JOIN categories
ON products.category_id = categories.id;Товар без категорії теж буде показано, але category_name матиме значення NULL.
RIGHT JOIN, якщо:потрібно зберегти всі рядки правої таблиці;
переписування через LEFT JOIN зробить запит менш зрозумілим.
У більшості випадків той самий результат простіше отримати, помінявши таблиці місцями та використавши LEFT JOIN.
INNER JOIN поверне всі рядкиSELECT
customers.name,
orders.id
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;Цей запит не поверне клієнтів без замовлень. Якщо потрібно бачити всіх клієнтів, використовуйте LEFT JOIN.
LEFT JOIN на INNER JOIN через WHERE`SELECT
customers.name,
orders.id
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.status = 'paid';Усі клієнти без замовлень будуть відфільтровані. Якщо потрібно залишити їх у результаті, перенесіть умову:
SELECT
customers.name,
orders.id
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
AND orders.status = 'paid';= NULL-- Неправильно
WHERE orders.id = NULL
-- Правильно
WHERE orders.id IS NULLДля перевірки на наявність значення використовуйте IS NOT NULL:
SELECT customers.name
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.id IS NOT NULL;Цей запит фактично залишить лише клієнтів із замовленнями.
Якщо в правій таблиці кілька відповідностей, рядок лівої таблиці повторюється:
SELECT
customers.name,
orders.id
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id;Олена з двома замовленнями з’явиться двічі. Це не помилка JOIN, а нормальна логіка об’єднання один-до-багатьох.
Якщо потрібен лише список унікальних клієнтів, можна використати DISTINCT:
SELECT DISTINCT
customers.id,
customers.name
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;Але DISTINCT не варто додавати автоматично. Спочатку потрібно зрозуміти, чому виникли повтори та який результат насправді потрібен.
Якщо умова JOIN занадто загальна або відсутня, можна отримати декартів добуток: кожен рядок однієї таблиці поєднається з кожним рядком іншої.
Завжди перевіряйте, що умова ON використовує правильні колонки та справді описує зв’язок між таблицями.
-- Лише клієнти, у яких є замовлення
SELECT customers.name
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;-- Усі клієнти, навіть без замовлень
SELECT customers.name
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id;-- Клієнти без жодного замовлення
SELECT customers.name
FROM customers
LEFT JOIN orders
ON customers.id = orders.customer_id
WHERE orders.id IS NULL;-- Лише клієнти з оплаченими замовленнями
SELECT DISTINCT customers.name
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id
WHERE orders.status = 'paid';INNER JOIN повертає лише рядки, для яких є відповідність в обох таблицях.
LEFT JOIN зберігає всі рядки лівої таблиці, а для відсутніх відповідностей підставляє NULL.
RIGHT JOIN зберігає всі рядки правої таблиці.
NULL у результаті зовнішнього JOIN означає, що пов’язаний рядок не знайдено.
Для перевірки NULL використовують IS NULL та IS NOT NULL.
Умова в ON впливає на пошук відповідностей, а умова в WHERE фільтрує готовий результат.
LEFT JOIN часто використовують для пошуку записів без пов’язаних даних.
RIGHT JOIN зазвичай можна замінити на LEFT JOIN, помінявши таблиці місцями.
Повторення рядків після JOIN часто є наслідком зв’язку один-до-багатьох, а не помилкою запиту.