Пошук уроків, статей та іншого контенту
Зв’яжете таблицю саму з собою для роботи з ієрархіями, колегами та іншими рекурсивними структурами.
Self join — це об’єднання таблиці з нею самою. Для SQL це дві логічні копії однієї таблиці, тому кожній копії потрібно надати окремий псевдонім.
Self join застосовують, коли рядки однієї таблиці пов’язані між собою:
працівник має керівника;
категорія має батьківську категорію;
коментар є відповіддю на інший коментар;
користувач запросив іншого користувача;
міста або організаційні підрозділи утворюють ієрархію.
Найпоширеніша модель ієрархії має зовнішній ключ, який посилається на первинний ключ тієї самої таблиці:
employees.manager_id → employees.employee_idmanager_idNULLCREATE TEMP TABLE employees (
employee_id integer PRIMARY KEY,
full_name text NOT NULL,
department text NOT NULL,
manager_id integer REFERENCES employees(employee_id)
);
INSERT INTO employees (employee_id, full_name, department, manager_id)
VALUES
(1, 'Олена Коваль', 'Керівництво', NULL),
(2, 'Андрій Мельник', 'Розробка', 1),
(3, 'Марія Бондар', 'Розробка', 2),
(4, 'Ігор Шевченко', 'Розробка', 2),
(5, 'Наталія Литвин', 'Тестування', 1),
(6, 'Павло Романюк', 'Тестування', 5);Один і той самий рядок таблиці може відігравати різні ролі:
у ролі працівника;
у ролі його керівника.
Тому в запиті потрібно використати два псевдоніми:
SELECT
employee.full_name AS employee_name,
manager.full_name AS manager_name
FROM employees AS employee
JOIN employees AS manager
ON manager.employee_id = employee.manager_id;Результат:
employee_name | manager_name
---------------------+----------------
Андрій Мельник | Олена Коваль
Марія Бондар | Андрій Мельник
Ігор Шевченко | Андрій Мельник
Наталія Литвин | Олена Коваль
Павло Романюк | Наталія ЛитвинТут:
employee — перша логічна копія таблиці;
manager — друга логічна копія таблиці;
employee.manager_id містить посилання на manager.employee_id.
У попередньому прикладі використано звичайний JOIN, тобто INNER JOIN. Він повертає лише рядки, для яких знайдено відповідного керівника.
Тому Олена Коваль не потрапила до результату: її manager_id дорівнює NULL.
Щоб показати всіх працівників, включно з керівником найвищого рівня, використовуйте LEFT JOIN:
SELECT
employee.full_name AS employee_name,
employee.department,
manager.full_name AS manager_name
FROM employees AS employee
LEFT JOIN employees AS manager
ON manager.employee_id = employee.manager_id
ORDER BY employee.employee_id;Для Олени значення manager_name буде NULL.
LEFT JOIN особливо важливий для ієрархій, оскільки кореневий елемент зазвичай не має батьківського запису.
Self join можна використовувати і в протилежному напрямку: знайти всіх безпосередніх підлеглих певного керівника.
SELECT
manager.full_name AS manager_name,
employee.full_name AS employee_name
FROM employees AS manager
JOIN employees AS employee
ON employee.manager_id = manager.employee_id
WHERE manager.full_name = 'Андрій Мельник'
ORDER BY employee.full_name;Результат міститиме Марію Бондар та Ігоря Шевченка.
Умова з’єднання та сама, але роль псевдонімів інша:
manager — керівник;
employee — підлеглий.
Self join з’єднує два рівні ієрархії. Якщо потрібно показати, наприклад, працівника, його керівника та керівника керівника, таблицю можна з’єднати із самою собою ще раз.
SELECT
employee.full_name AS employee_name,
manager.full_name AS manager_name,
top_manager.full_name AS top_manager_name
FROM employees AS employee
LEFT JOIN employees AS manager
ON manager.employee_id = employee.manager_id
LEFT JOIN employees AS top_manager
ON top_manager.employee_id = manager.manager_id
ORDER BY employee.employee_id;Для Павла Романюка результат буде приблизно таким:
employee_name | manager_name | top_manager_name
-----------------+--------------------+-----------------
Павло Романюк | Наталія Литвин | Олена КовальКожен додатковий рівень потребує ще одного псевдоніма та ще одного JOIN. Такий підхід зручний, коли глибина відома наперед. Для довільної кількості рівнів зазвичай використовують рекурсивний запит, але базовий зв’язок між рядками все одно зберігається через self-reference — зовнішній ключ на ту саму таблицю.
Self join також дає змогу знайти працівників, які мають одного керівника.
SELECT
employee.full_name AS employee_name,
colleague.full_name AS colleague_name,
employee.manager_id
FROM employees AS employee
JOIN employees AS colleague
ON colleague.manager_id = employee.manager_id
AND colleague.employee_id > employee.employee_id
WHERE employee.manager_id IS NOT NULL
ORDER BY employee.manager_id, employee.full_name;Умова:
colleague.employee_id > employee.employee_idпотрібна для того, щоб:
не порівнювати працівника із самим собою;
не отримувати дублікати пар.
Без цієї умови для пари Марія Бондар — Ігор Шевченко з’явилися б обидва варіанти:
Марія Бондар | Ігор Шевченко
Ігор Шевченко | Марія БондарЗ умовою > залишиться лише один варіант.
Той самий підхід працює не лише для працівників. Наприклад, категорія товарів може мати батьківську категорію:
CREATE TEMP TABLE categories (
category_id integer PRIMARY KEY,
category_name text NOT NULL,
parent_category_id integer REFERENCES categories(category_id)
);
INSERT INTO categories (category_id, category_name, parent_category_id)
VALUES
(1, 'Електроніка', NULL),
(2, 'Комп’ютери', 1),
(3, 'Ноутбуки', 2),
(4, 'Телефони', 1);
SELECT
child.category_name AS category,
parent.category_name AS parent_category
FROM categories AS child
LEFT JOIN categories AS parent
ON parent.category_id = child.parent_category_id
ORDER BY child.category_id;Тут:
child — дочірня категорія;
parent — батьківська категорія.
Модель залишається такою самою:
categories.parent_category_id → categories.category_idВажливо відрізняти умову зв’язку в ON від фільтра в WHERE.
Наприклад, цей запит покаже всіх працівників, але лише керівників із відділу «Розробка»:
SELECT
employee.full_name AS employee_name,
manager.full_name AS manager_name,
manager.department AS manager_department
FROM employees AS employee
LEFT JOIN employees AS manager
ON manager.employee_id = employee.manager_id
WHERE manager.department = 'Розробка';Попри LEFT JOIN, працівники без керівника не потраплять до результату, оскільки умова WHERE manager.department = 'Розробка' відкидає рядки з NULL.
Якщо потрібно зберегти всіх працівників, а обмежити лише відповідних керівників, перенесіть умову до ON:
SELECT
employee.full_name AS employee_name,
manager.full_name AS manager_name,
manager.department AS manager_department
FROM employees AS employee
LEFT JOIN employees AS manager
ON manager.employee_id = employee.manager_id
AND manager.department = 'Розробка';Тоді працівники без керівника або з керівником з іншого відділу залишаться в результаті, але дані керівника будуть NULL.
Наступний запит показує працівника, його керівника та кількість безпосередніх підлеглих цього керівника:
SELECT
employee.full_name AS employee_name,
manager.full_name AS manager_name,
COUNT(colleague.employee_id) AS manager_subordinates
FROM employees AS employee
LEFT JOIN employees AS manager
ON manager.employee_id = employee.manager_id
LEFT JOIN employees AS colleague
ON colleague.manager_id = manager.employee_id
GROUP BY
employee.employee_id,
employee.full_name,
manager.employee_id,
manager.full_name
ORDER BY employee.employee_id;Тут таблиця employees використовується тричі:
employee — поточний працівник;
manager — його керівник;
colleague — працівники, підлеглі цьому керівнику.
Кожен псевдонім описує окрему роль рядка в запиті.
Некоректно або незрозуміло:
SELECT full_name
FROM employees
JOIN employees
ON employee_id = manager_id;Одна й та сама назва таблиці використовується двічі, тому PostgreSQL не може однозначно визначити, з якої копії потрібно взяти стовпець.
Правильніше:
SELECT employee.full_name
FROM employees AS employee
JOIN employees AS manager
ON manager.employee_id = employee.manager_id;Для моделі employee.manager_id → manager.employee_id правильна умова:
manager.employee_id = employee.manager_idЯкщо переплутати стовпці, можна отримати порожній або логічно неправильний результат.
Фільтр у WHERE над стовпцем правої таблиці може прибрати рядки з NULL. Якщо потрібно зберегти рядки без пов’язаного запису, умову слід розміщувати в ON.
Під час пошуку колег самоз’єднання може повернути кожну пару двічі. Для усунення цього використовуйте умову на ідентифікатори:
colleague.employee_id > employee.employee_idабо вибирайте унікальні пари за допомогою іншої логіки порівняння.
Self join спирається на дані таблиці. Якщо рядок прямо або опосередковано посилається сам на себе, ієрархія містить цикл.
Зовнішній ключ гарантує, що вказаний батьківський рядок існує, але сам по собі не гарантує відсутність циклів. Тому під час запису ієрархічних даних потрібно додатково перевіряти правила предметної області.
Self join — це з’єднання таблиці з нею самою.
Для кожної логічної ролі таблиці потрібно використовувати окремий псевдонім.
Модель ієрархії часто зберігається через зовнішній ключ на ту саму таблицю.
INNER JOIN повертає лише рядки з пов’язаним записом.
LEFT JOIN дає змогу показати також кореневі елементи без батьківського запису.
Self join можна застосувати для пошуку керівників, підлеглих, колег і батьківських категорій.
Для кількох відомих рівнів таблицю можна з’єднувати із собою кілька разів.
Під час пошуку пар потрібно запобігати порівнянню рядка із самим собою та дублюванню результатів.