XNOVA
← Все статьи

СТАТЬЯ / data-engineering

SQL JOIN: INNER, LEFT, RIGHT и FULL JOIN с примерами

На одних и тех же таблицах разбираем INNER, LEFT, RIGHT и FULL JOIN: какие строки попадают в результат, откуда берутся NULL и почему ON и WHERE меняют результат.

Автор: XNova

В отчёте нужно вывести сотрудника и название его отдела. Сотрудники хранятся в таблице employees, отделы — в departments. Если в отчёт должны попасть и сотрудники без отдела, и отделы без сотрудников, выбор соединения становится важной частью запроса.

В SQL эти случаи зависят от вида соединения. JOIN сопоставляет строки таблиц по условию, а INNER, LEFT, RIGHT и FULL определяют, какие строки сохранить, если пара не найдена.

На небольшом наборе данных разберём каждый вариант, сравним сами результирующие строки и увидим, почему перенос фильтра из ON в WHERE способен изменить результат. Для примеров нужны только базовые знания SELECT и WHERE.

Две таблицы: сотрудники и отделы

В таблице employees три сотрудника. Анна и Борис работают в разработке, Вера не привязана ни к одному отделу. Таблица departments содержит два отдела — разработку и аналитику; в аналитике сотрудников нет.

employee_idemployeedepartment_id
1Анна10
2Борис10
3ВераNULL
department_iddepartment
10Разработка
20Аналитика

Стрелки на схеме показывают две найденные связи по значению 10. У Веры ключ отдела равен NULL, а на отдел аналитики не ссылается ни один сотрудник.

Связи сотрудников с отделами

Для работы с данными можно создать две таблицы и добавить записи:

CREATE TABLE departments (
    department_id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE employees (
    employee_id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    department_id INTEGER REFERENCES departments(department_id)
);

INSERT INTO departments (department_id, name) VALUES
    (10, 'Разработка'),
    (20, 'Аналитика');

INSERT INTO employees (employee_id, name, department_id) VALUES
    (1, 'Анна', 10),
    (2, 'Борис', 10),
    (3, 'Вера', NULL);

employees.department_id ссылается на ключ справочника departments.department_id. Он допускает NULL: отсутствие отдела здесь допустимо. При этом departments.department_id — первичный ключ, то есть каждому идентификатору отдела соответствует не более одной записи справочника.

Мы используем SQLite. SQLite поддерживает RIGHT и FULL OUTER JOIN начиная с версии 3.39.0. Это указано в примечаниях к выпуску SQLite 3.39.0. Если ваша версия ниже, обновите движок или выполните примеры в СУБД с поддержкой этих соединений, например PostgreSQL.

Как JOIN сопоставляет строки

Рассмотрим условие:

ON e.department_id = d.department_id

Оно сравнивает department_id сотрудника с идентификатором отдела. При значении 10 находятся две пары: «Анна — Разработка» и «Борис — Разработка». У Веры значение NULL, а у аналитики нет сотрудников с идентификатором отдела 20.

NULL означает отсутствие известного значения. Обычное сравнение NULL = 10 не истинно, поэтому такая пара не возникает. Это не специальное правило JOIN: оно следует из логики сравнения с NULL.

Соединение строит одну строку на каждую найденную пару. Если отдел 10 связан с двумя сотрудниками, он участвует в двух строках результата. JOIN не объединяет сотрудников в одну запись отдела.

Для внешних соединений есть дополнительный шаг: когда исходная строка не нашла пары, её можно сохранить, заполнив столбцы отсутствующей стороны значениями NULL. Сравнение типов соединения ниже следует из семантики JOIN в PostgreSQL.

INNER JOIN: только найденные пары

INNER JOIN возвращает только те пары строк, для которых условие ON истинно. Сотрудник без отдела и отдел без сотрудников в результате не появятся. В SQLite и PostgreSQL запись JOIN без слова INNER означает то же соединение.

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
INNER JOIN departments AS d
    ON e.department_id = d.department_id
ORDER BY e.employee_id, d.department_id;
employee_idemployeedepartment_iddepartment
1Анна10Разработка
2Борис10Разработка

Получилось две строки. Если вам нужны только сотрудники с существующим отделом — например, для списка назначенных сотрудников, — такой запрос отвечает задаче. При этом он намеренно не покажет Веру.

LEFT JOIN: сохраняем строки левой таблицы

LEFT JOIN сохраняет строки левой таблицы даже без подходящей строки справа. Здесь слева стоит employees, поэтому в ответ попадут все три сотрудника.

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
LEFT JOIN departments AS d
    ON e.department_id = d.department_id
ORDER BY e.employee_id, d.department_id;
employee_idemployeedepartment_iddepartment
1Анна10Разработка
2Борис10Разработка
3ВераNULLNULL

Получилось три строки. Вера сохранена, а столбцы отдела для неё заполнены NULL. Отдел «Аналитика» не добавляется сам по себе: правая таблица не является сохраняемой стороной этого запроса.

LEFT JOIN подходит, когда список всех сотрудников нужен независимо от заполненности справочника.

RIGHT JOIN: сохраняем строки правой таблицы

RIGHT JOIN сохраняет строки правой таблицы даже без подходящей строки слева. Справа теперь стоит departments, поэтому результат должен включать и пустой отдел аналитики.

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
RIGHT JOIN departments AS d
    ON e.department_id = d.department_id
ORDER BY d.department_id, e.employee_id;
employee_idemployeedepartment_iddepartment
1Анна10Разработка
2Борис10Разработка
NULLNULL20Аналитика

Здесь тоже три строки, но они отличаются от ответа LEFT JOIN: Вера отсутствует, зато отдел аналитики сохранён. Если нужны все отделы, в том числе пустые, это подходящий выбор.

Ту же связь можно записать через LEFT JOIN, поменяв таблицы местами (показан фрагмент FROM):

FROM departments AS d
LEFT JOIN employees AS e ON e.department_id = d.department_id

При выборе и сортировке одних и тех же столбцов обе записи дают одинаковые пары. Такая симметрия прямо следует из определений внешних соединений.

FULL JOIN: сохраняем несовпавшие строки обеих таблиц

FULL JOIN включает и совпавшие пары, и строки без пары с обеих сторон. В результате будут две найденные пары — Анна и Борис с разработкой — и две строки без пары: Вера без отдела и аналитика без сотрудника.

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
FULL OUTER JOIN departments AS d
    ON e.department_id = d.department_id
ORDER BY COALESCE(e.employee_id, 999), d.department_id;
employee_idemployeedepartment_iddepartment
1Анна10Разработка
2Борис10Разработка
3ВераNULLNULL
NULLNULL20Аналитика

Четыре строки охватывают все совпавшие и несовпавшие записи исходных таблиц. FULL JOIN и FULL OUTER JOIN — две записи одного вида соединения; необязательное слово OUTER описано в документации PostgreSQL.

Формула «сохранить обе стороны» не означает «вернуть сумму количества исходных строк». Совпавшие пары формируют общий результат, а одна исходная строка может иметь несколько пар.

Сравнение четырёх JOIN на одинаковых данных

Во всех запросах выше одна и та же пара таблиц и одно условие e.department_id = d.department_id. Меняется только правило сохранения строк без пары.

ВидНайденные парыВера без отделаАналитика без сотрудниковВсего строк
INNER JOIN2——2
LEFT JOIN2✓—3
RIGHT JOIN2—✓3
FULL JOIN2✓✓4

На одной схеме видны не только количества, но и конкретные строки, которые добавляет каждый вид соединения:

Результаты четырёх видов JOIN

Галочка означает, что исходная строка без пары остаётся в результате. Для неё столбцы другой таблицы принимают NULL.

Эти числа — результат именно нашего набора данных. Если отдел разработки получит третьего сотрудника, количество совпавших пар увеличится и все четыре результата изменятся. Чтобы увидеть это, смотрите не только на название JOIN, но и на условие ON и кратность отношений между таблицами.

В локальной проверке на SQLite 3.46.1 все четыре выборки вернули ожидаемые пары и количества строк. Для другого набора данных или другой версии СУБД результаты необходимо проверить в соответствующем окружении.

ON и WHERE: почему LEFT JOIN иногда теряет строки

ON определяет, какие строки двух таблиц подходят друг другу. WHERE фильтрует строки уже сформированного результата. Для INNER JOIN перенос простого фильтра нередко сохраняет результат, но при внешних соединениях эти места не равнозначны.

Фильтр в WHERE применяется после OUTER JOIN и может удалить добавленные строки с NULL. Например, запрос ниже оставит только Анну и Бориса:

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
LEFT JOIN departments AS d ON e.department_id = d.department_id
WHERE d.name = 'Разработка'
ORDER BY e.employee_id;

После соединения у Веры значение d.name равно NULL; сравнение NULL = 'Разработка' не истинно, поэтому строка не проходит WHERE.

Если вы хотите показать всех сотрудников, но присоединить сведения справа только об отделе разработки, поставьте условие в ON:

SELECT e.employee_id, e.name AS employee, d.department_id, d.name AS department
FROM employees AS e
LEFT JOIN departments AS d
    ON e.department_id = d.department_id AND d.name = 'Разработка'
ORDER BY e.employee_id;

Теперь результат содержит и Веру — всего три строки. Совпавшим строкам соответствует разработка, а для Веры столбцы d.* остаются NULL. Порядок обработки ON и WHERE для OUTER JOIN описан в документации SQLite.

Чтобы найти сотрудников без соответствующего отдела, проверяйте ключ правой таблицы:

WHERE d.department_id IS NULL

Для поиска отсутствующей пары проверяйте IS NULL по не допускающему NULL ключу другой таблицы. В нашем примере это d.department_id: он объявлен PRIMARY KEY. Так выбирается Вера. Условие WHERE e.department_id IS NULL проверяет другое: заполнен ли идентификатор отдела у самого сотрудника. Оно не найдёт сотрудника с ненулевой ссылкой на отсутствующий отдел, тогда как проверка d.department_id IS NULL найдёт отсутствие подходящей строки справа. Сравнение = NULL не подходит: оно даёт неизвестное логическое значение, а не TRUE. См. правила сравнений с NULL.

Как проверить и выбрать подходящий JOIN

Сначала сформулируйте, какие исходные записи должны сохраняться без пары. Если только записи с найденной связью — выбирайте INNER JOIN. Все сотрудники — LEFT JOIN от employees. Все отделы — RIGHT JOIN либо LEFT JOIN с переставленными таблицами. Обе стороны — FULL JOIN.

Для самостоятельной проверки выполните создание таблиц из примера, затем по очереди четыре запроса. Ожидаемые количества строк — 2, 3, 3 и 4; сверяйте также содержимое строк, а не только COUNT(*). При ошибке «неизвестный тип RIGHT/FULL» проверьте версию SQLite: нужна 3.39.0 или более новая. Если версия подходит, а запрос не работает, сначала убедитесь, что выполнены CREATE TABLE и INSERT, а имена и псевдонимы столбцов совпадают с листингом.

Без ORDER BY порядок возвращаемых строк не гарантируется. В примерах сортировка указана явно; это свойство описано в документации SQLite. Для диагностики важно сравнивать одинаковые наборы строк, не полагаясь на случайный порядок.

Наконец, вид JOIN не ограничивает число совпадений одной исходной строкой. Если позже соединять работников, задачи и другие таблицы со связями один-ко-многим, число строк может вырасти, а SUM — посчитать повторения. Это уже отдельная проблема агрегирования; здесь ключевой результат — умение определить, какие совпавшие и несовпавшие строки сохраняет каждый вид соединения.

Источники