Пошук уроків, статей та іншого контенту
Побудуєте запити з кількома JOIN і проконтролюєте порядок та умови об’єднання таблиць.
JOINJOIN об’єднує рядки двох таблиць за умовою. У запиті можна послідовно об’єднати три й більше таблиць:
SELECT ...
FROM table_a
JOIN table_b ON ...
JOIN table_c ON ...;Кожен наступний JOIN працює з результатом попереднього об’єднання. Тому важливо розуміти:
яку таблицю додаємо на кожному кроці;
за якими стовпцями її об’єднуємо;
чи повинні зберегтися рядки без відповідностей;
де саме розміщуємо додаткові умови.
Розглянемо інтернет-магазин із такими таблицями:
customers — покупці;
orders — замовлення;
order_items — товари в замовленнях;
products — товари;
categories — категорії товарів.
Наведений приклад можна виконати в PostgreSQL цілком:
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
full_name text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE categories (
category_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE products (
product_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
category_id integer REFERENCES categories(category_id),
price numeric(10, 2) NOT NULL CHECK (price > 0)
);
CREATE TABLE orders (
order_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id integer NOT NULL REFERENCES customers(customer_id),
ordered_at date NOT NULL,
status text NOT NULL
);
CREATE TABLE order_items (
order_id integer NOT NULL REFERENCES orders(order_id),
product_id integer NOT NULL REFERENCES products(product_id),
quantity integer NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, product_id)
);
INSERT INTO customers (full_name, email)
VALUES
('Олена Коваль', 'olena@example.com'),
('Андрій Мельник', 'andrii@example.com'),
('Марія Бондар', 'maria@example.com');
INSERT INTO categories (name)
VALUES
('Ноутбуки'),
('Аксесуари'),
('Монітори');
INSERT INTO products (name, category_id, price)
VALUES
('ThinkBook 14', 1, 32000.00),
('Безпровідна миша', 2, 850.00),
('USB-C хаб', 2, 1400.00),
('Монітор 27"', 3, 12500.00);
INSERT INTO orders (customer_id, ordered_at, status)
VALUES
(1, '2026-08-01', 'paid'),
(1, '2026-08-12', 'shipped'),
(2, '2026-08-15', 'new');
INSERT INTO order_items (order_id, product_id, quantity)
VALUES
(1, 1, 1),
(1, 2, 2),
(2, 4, 1),
(3, 3, 1);Спочатку об’єднаємо customers і orders:
SELECT
c.customer_id,
c.full_name,
o.order_id,
o.ordered_at,
o.status
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;Тут:
customers є основною таблицею;
orders додається за умовою o.customer_id = c.customer_id;
кожен результат містить покупця та одне з його замовлень.
У JOIN без уточнення типу мається на увазі INNER JOIN. Він повертає лише рядки, для яких відповідність знайдена в обох таблицях.
У наших даних покупець Марія не має замовлень, тому в результаті її не буде.
Щоб отримати товари кожного замовлення, потрібно додати order_items, а потім products:
SELECT
c.full_name,
o.order_id,
p.name AS product_name,
oi.quantity,
p.price
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_id
ORDER BY o.order_id, p.name;Логічно цей запит можна прочитати так:
знайти замовлення кожного покупця;
знайти позиції в кожному замовленні;
знайти дані товару для кожної позиції;
вивести об’єднані дані.
Для замовлення з двома товарами буде створено два рядки. Це очікувана поведінка: зв’язок між orders і products реалізовано через таблицю order_items.
До ланцюжка можна додати ще одну таблицю:
SELECT
c.full_name,
o.order_id,
p.name AS product_name,
cat.name AS category_name,
oi.quantity,
p.price,
oi.quantity * p.price AS item_total
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_id
JOIN categories AS cat
ON cat.category_id = p.category_id
ORDER BY o.order_id, cat.name, p.name;Вираз:
oi.quantity * p.price AS item_totalобчислює загальну вартість конкретної позиції замовлення.
Псевдоніми c, o, oi, p і cat допомагають:
скоротити запит;
явно вказати, з якої таблиці походить стовпець;
уникнути неоднозначності, якщо кілька таблиць мають стовпці з однаковими назвами.
У запиті можна змінити текстовий порядок деяких JOIN, якщо умови залишаються коректними:
SELECT
o.order_id,
c.full_name,
o.status
FROM orders AS o
JOIN customers AS c
ON c.customer_id = o.customer_id;Цей запит повертає той самий тип результату, що й об’єднання з customers у першій позиції. Для INNER JOIN PostgreSQL може самостійно змінити фізичний порядок виконання з міркувань продуктивності.
Тому важливо розрізняти:
текстовий порядок — порядок запису JOIN у SQL;
логічний результат — які рядки повинні відповідати умовам;
фізичний план виконання — як PostgreSQL фактично виконає запит.
Для INNER JOIN перестановка таблиць часто не змінює результат. Але для LEFT JOIN, RIGHT JOIN і FULL JOIN порядок має значення.
Таблиці не обов’язково мають безпосередній зовнішній ключ одна до одної. Наприклад, orders і products пов’язані через order_items:
orders → order_items → productsТому неправильним буде намагатися об’єднати orders і products за неіснуючим або непов’язаним стовпцем. Потрібно пройти через проміжну таблицю:
FROM orders AS o
JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_idINNER JOIN і LEFT JOIN у ланцюжкуЯкщо потрібно зберегти покупців без замовлень, починаємо з customers і використовуємо LEFT JOIN:
SELECT
c.full_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
ORDER BY c.full_name, o.order_id;У результаті Марія буде присутня, але значення order_id і status для неї будуть NULL.
Якщо потрібно зберегти покупців і замовлення навіть тоді, коли в замовленні немає позицій, усі наступні об’єднання також зазвичай роблять лівими:
SELECT
c.full_name,
o.order_id,
p.name AS product_name,
oi.quantity
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
LEFT JOIN order_items AS oi
ON oi.order_id = o.order_id
LEFT JOIN products AS p
ON p.product_id = oi.product_id
ORDER BY c.full_name, o.order_id, p.name;Такий запит зберігає рядки з лівої частини кожного LEFT JOIN. Якщо відповідності немає, стовпці правої таблиці матимуть значення NULL.
Якщо замінити останній LEFT JOIN на INNER JOIN, рядки без відповідного товару буде відкинуто:
SELECT
c.full_name,
o.order_id,
p.name AS product_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
LEFT JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_id;Фактично останній INNER JOIN вимагає, щоб для рядка існував товар. Через це частина переваги попередніх LEFT JOIN втрачається.
ON і WHEREРозташування умови особливо важливе для LEFT JOIN.
WHEREПрипустімо, потрібно показати всіх покупців і лише оплачені замовлення:
SELECT
c.full_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid';На перший погляд це схоже на пошук оплачених замовлень. Але рядки без замовлення мають o.status = NULL, а умова o.status = 'paid' для них не виконується. Тому покупці без оплачених замовлень зникають із результату.
ONЯкщо потрібно зберегти всіх покупців, а приєднати лише оплачені замовлення, умову треба розмістити в ON:
SELECT
c.full_name,
o.order_id,
o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
ORDER BY c.full_name;Тепер:
покупці без оплачених замовлень залишаються;
для них поля замовлення дорівнюють NULL;
неоплачені замовлення не приєднуються.
Це одна з найважливіших відмінностей між ON і WHERE:
ON визначає, які рядки правої таблиці приєднати;
WHERE фільтрує вже сформований результат.
Наприклад, потрібно показати всіх покупців, але вивести лише товари з категорії «Аксесуари»:
SELECT
c.full_name,
o.order_id,
p.name AS product_name,
cat.name AS category_name
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
LEFT JOIN order_items AS oi
ON oi.order_id = o.order_id
LEFT JOIN products AS p
ON p.product_id = oi.product_id
LEFT JOIN categories AS cat
ON cat.category_id = p.category_id
AND cat.name = 'Аксесуари'
ORDER BY c.full_name, o.order_id;Покупці та замовлення збережуться, але для товарів інших категорій category_name буде NULL. Якщо ж додати WHERE cat.name = 'Аксесуари', рядки без такої категорії буде видалено з результату.
Умова JOIN може містити кілька частин:
SELECT
c.full_name,
o.order_id,
o.ordered_at,
o.status
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.ordered_at >= DATE '2026-08-10'
AND o.status IN ('paid', 'shipped')
ORDER BY o.ordered_at;Тут замовлення приєднуються лише тоді, коли одночасно виконуються всі умови:
замовлення належить покупцеві;
дата замовлення не раніше 2026-08-10;
статус — paid або shipped.
Для читабельності довгі умови краще розташовувати на окремих рядках.
JOINПісля об’єднання можна групувати дані. Наприклад, порахуємо загальну суму кожного замовлення:
SELECT
o.order_id,
c.full_name,
o.ordered_at,
SUM(oi.quantity * p.price) AS order_total
FROM orders AS o
JOIN customers AS c
ON c.customer_id = o.customer_id
JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_id
GROUP BY
o.order_id,
c.full_name,
o.ordered_at
ORDER BY o.order_id;Усі неагреговані стовпці з SELECT мають бути в GROUP BY. Агрегат SUM додає вартість усіх позицій одного замовлення.
Щоб порахувати замовлення для кожного покупця, групувати потрібно за покупцем:
SELECT
c.customer_id,
c.full_name,
COUNT(DISTINCT o.order_id) AS orders_count,
COALESCE(SUM(oi.quantity * p.price), 0) AS total_spent
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
LEFT JOIN order_items AS oi
ON oi.order_id = o.order_id
LEFT JOIN products AS p
ON p.product_id = oi.product_id
GROUP BY
c.customer_id,
c.full_name
ORDER BY c.customer_id;LEFT JOIN дає змогу включити покупців без замовлень, а COALESCE замінює NULL на 0.
COUNT(DISTINCT o.order_id) потрібен тому, що після приєднання позицій одне замовлення може повторюватися в кількох рядках — по одному рядку на товар.
ON-- Неправильно: ідентифікатор товару порівнюється з ідентифікатором замовлення
JOIN products AS p
ON p.product_id = o.order_idПотрібно приєднувати товар через order_items:
JOIN order_items AS oi
ON oi.order_id = o.order_id
JOIN products AS p
ON p.product_id = oi.product_idЯкщо кілька таблиць містять стовпець name, запис SELECT name стає неоднозначним. Вказуйте псевдонім:
SELECT
p.name AS product_name,
cat.name AS category_nameLEFT JOIN на INNER JOIN через WHERESELECT c.full_name, o.order_id
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid';Такий WHERE відкидає рядки з NULL і фактично прибирає покупців без відповідного замовлення. Якщо їх потрібно зберегти, перенесіть умову до ON.
Під час об’єднання таблиць «один-до-багатьох» рядок батьківської таблиці повторюється:
один покупець може мати багато замовлень;
одне замовлення може мати багато позицій;
одна категорія може містити багато товарів.
Це не помилка JOIN. Якщо потрібен один рядок на замовлення або покупця, використовуйте GROUP BY, агрегатні функції або DISTINCT відповідно до завдання.
JOIN через комуТакий запис ускладнює читання і може створити декартовий добуток:
SELECT *
FROM customers AS c, orders AS o
WHERE c.customer_id = o.customer_id;Краще явно вказувати тип і умову об’єднання:
SELECT *
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;Коли потрібно об’єднати кілька таблиць:
Визначте, який рядок має описувати результат: покупця, замовлення чи позицію замовлення.
Почніть FROM з таблиці, яка містить цей рядок.
Додавайте таблиці по одній.
Для кожного JOIN знайдіть відповідні зовнішній і первинний ключі.
Вирішіть, чи потрібен INNER JOIN, чи треба зберегти рядки без відповідності через LEFT JOIN.
Усі умови зв’язку розмістіть в ON.
Перевірте, чи не створює зв’язок дублікати рядків.
Якщо потрібен підсумок, додайте GROUP BY та агрегатні функції.
Окремо перевірте умови в WHERE, особливо після LEFT JOIN.
Кілька JOIN утворюють послідовний ланцюжок об’єднань.
Для зв’язку через проміжну таблицю потрібно додати всі необхідні кроки.
INNER JOIN повертає лише рядки з відповідностями.
LEFT JOIN зберігає всі рядки лівої таблиці.
Текстовий порядок INNER JOIN часто можна змінити, але порядок зовнішніх об’єднань впливає на результат.
Умова в ON визначає спосіб приєднання, а WHERE фільтрує готовий результат.
Після зв’язку «один-до-багатьох» рядки можуть дублюватися, тому для підсумків потрібні GROUP BY, COUNT, SUM та інші агрегатні функції.
Псевдоніми таблиць роблять складні запити коротшими й однозначними.