Урок доступен в видеоформате 

Внешнее соединение OUTER JOIN

Внутреннее соединение оставляет только те строки, для которых нашлась пара во второй таблице. Внешнее соединение работает иначе: оно обязательно возвращает все строки одной таблицы или обеих, а недостающую половину заполняет значениями NULL.

Внешнее соединение бывает трёх типов: левое (LEFT), правое (RIGHT) и полное (FULL). Тип указывать обязательно — написать просто OUTER JOIN нельзя, это синтаксическая ошибка. А вот само слово OUTER необязательно: LEFT JOIN и LEFT OUTER JOIN означают одно и то же, и дальше используется короткая запись.

Внешнее левое соединение (LEFT OUTER JOIN)

Возвращает все строки левой таблицы. Если для строки нашлась пара в правой таблице, строки склеиваются; если не нашлась — поля правой таблицы заполняются NULL.

Для примера получим из базы данных расписание звонков, объединённое с соответствующими занятиями в расписании занятий.

Данные в таблице Timepair (расписание звонков):

idstart_pairend_pair
108:30:0009:15:00
209:20:0010:05:00
310:15:0011:00:00
411:05:0011:50:00
512:50:0013:35:00
613:40:0014:25:00
714:35:0015:20:00
815:25:0016:10:00

Данные в таблице Schedule (расписание занятий):

iddateclassnumber_pairteachersubjectclassroom
12019-09-01T00:00:00.000Z9111147
22019-09-01T00:00:00.000Z928213
32019-09-01T00:00:00.000Z934313
42019-09-02T00:00:00.000Z914313
52019-09-02T00:00:00.000Z922434
62019-09-02T00:00:00.000Z936535
72019-09-03T00:00:00.000Z915636
82019-09-03T00:00:00.000Z9213737
92019-09-03T00:00:00.000Z936838
102019-09-04T00:00:00.000Z919939
112019-09-04T00:00:00.000Z92101040
122019-09-04T00:00:00.000Z9331141
132019-09-05T00:00:00.000Z9131343
142019-09-05T00:00:00.000Z9211147
152019-09-05T00:00:00.000Z935636
162019-08-30T00:00:00.000Z912434
172019-08-30T00:00:00.000Z928213
182019-08-30T00:00:00.000Z936535
192019-08-30T00:00:00.000Z9410147
202019-09-03T00:00:00.000Z94101040
212019-08-30T00:00:00.000Z817953
222019-08-30T00:00:00.000Z827953
232019-08-30T00:00:00.000Z838238
242019-08-30T00:00:00.000Z8411143
252019-08-30T00:00:00.000Z858339
262019-09-01T00:00:00.000Z822434
272019-09-01T00:00:00.000Z836535
282019-09-01T00:00:00.000Z8412636
292019-09-01T00:00:00.000Z8513737
302019-09-02T00:00:00.000Z836838
312019-09-02T00:00:00.000Z847953
322019-09-03T00:00:00.000Z81101040
332019-09-03T00:00:00.000Z827953
342019-09-03T00:00:00.000Z837953
352019-09-04T00:00:00.000Z811114
362019-09-04T00:00:00.000Z8211242
372019-09-04T00:00:00.000Z8331343
382019-09-04T00:00:00.000Z848242
392019-09-04T00:00:00.000Z8511143
402019-09-05T00:00:00.000Z8211143
MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    LEFT JOIN Schedule ON Schedule.number_pair = Timepair.id;
timepair.idstart_pairend_pairschedule.iddateclassnumber_pairteachersubjectclassroom
108:30:0009:15:00352019-09-04T00:00:00.000Z811114
108:30:0009:15:00322019-09-03T00:00:00.000Z81101040
108:30:0009:15:00212019-08-30T00:00:00.000Z817953
108:30:0009:15:00162019-08-30T00:00:00.000Z912434
108:30:0009:15:00132019-09-05T00:00:00.000Z9131343
108:30:0009:15:00102019-09-04T00:00:00.000Z919939
108:30:0009:15:0072019-09-03T00:00:00.000Z915636
108:30:0009:15:0042019-09-02T00:00:00.000Z914313
108:30:0009:15:0012019-09-01T00:00:00.000Z9111147
209:20:0010:05:00402019-09-05T00:00:00.000Z8211143
209:20:0010:05:00362019-09-04T00:00:00.000Z8211242
209:20:0010:05:00332019-09-03T00:00:00.000Z827953
209:20:0010:05:00262019-09-01T00:00:00.000Z822434
209:20:0010:05:00222019-08-30T00:00:00.000Z827953
209:20:0010:05:00172019-08-30T00:00:00.000Z928213
209:20:0010:05:00142019-09-05T00:00:00.000Z9211147
209:20:0010:05:00112019-09-04T00:00:00.000Z92101040
209:20:0010:05:0082019-09-03T00:00:00.000Z9213737
209:20:0010:05:0052019-09-02T00:00:00.000Z922434
209:20:0010:05:0022019-09-01T00:00:00.000Z928213
310:15:0011:00:00372019-09-04T00:00:00.000Z8331343
310:15:0011:00:00342019-09-03T00:00:00.000Z837953
310:15:0011:00:00302019-09-02T00:00:00.000Z836838
310:15:0011:00:00272019-09-01T00:00:00.000Z836535
310:15:0011:00:00232019-08-30T00:00:00.000Z838238
310:15:0011:00:00182019-08-30T00:00:00.000Z936535
310:15:0011:00:00152019-09-05T00:00:00.000Z935636
310:15:0011:00:00122019-09-04T00:00:00.000Z9331141
310:15:0011:00:0092019-09-03T00:00:00.000Z936838
310:15:0011:00:0062019-09-02T00:00:00.000Z936535
310:15:0011:00:0032019-09-01T00:00:00.000Z934313
411:05:0011:50:00382019-09-04T00:00:00.000Z848242
411:05:0011:50:00312019-09-02T00:00:00.000Z847953
411:05:0011:50:00282019-09-01T00:00:00.000Z8412636
411:05:0011:50:00242019-08-30T00:00:00.000Z8411143
411:05:0011:50:00202019-09-03T00:00:00.000Z94101040
411:05:0011:50:00192019-08-30T00:00:00.000Z9410147
512:50:0013:35:00392019-09-04T00:00:00.000Z8511143
512:50:0013:35:00292019-09-01T00:00:00.000Z8513737
512:50:0013:35:00252019-08-30T00:00:00.000Z858339
613:40:0014:25:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>
714:35:0015:20:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>
815:25:0016:10:00<NULL><NULL><NULL><NULL><NULL><NULL><NULL>

В результат попали все восемь звонков — как и обещает левое соединение. Но строк получилось 43, а не 8.

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

В конце выборки есть строки, где все поля занятия заполнены NULL. Это звонки, для которых не нашлось ни одного занятия: пары нет, но строка левой таблицы обязана попасть в результат.

Строки без пары

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

MySQL 8.1
SELECT Timepair.id, start_pair, end_pair
FROM Timepair
    LEFT JOIN Schedule ON Schedule.number_pair = Timepair.id
WHERE Schedule.number_pair IS NULL;
idstart_pairend_pair
613:40:0014:25:00
714:35:0015:20:00
815:25:0016:10:00

Остались три звонка, на которые не поставлено ни одного занятия.

Соединение, из которого оставляют только строки без пары, называют анти-джойном (ANTI JOIN). Собственного оператора у него нет: и в MySQL, и в PostgreSQL его записывают именно так — соединением и условием IS NULL.

Внешнее правое соединение (RIGHT OUTER JOIN)

Зеркальное отражение левого: в результат обязательно попадают все строки правой таблицы, а недостающие поля левой заполняются NULL.

MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    RIGHT JOIN Schedule ON Schedule.number_pair = Timepair.id;

В результате 40 строк — ровно столько, сколько записей в Schedule, и ни одной строки с NULL. То есть он полностью совпал с внутренним соединением.

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

Внешнее полное соединение (FULL OUTER JOIN)

Возвращает все строки обеих таблиц. Строки с найденной парой склеиваются, а непарные строки левой и правой таблиц попадают в результат с NULL вместо второй половины.

Результат полного соединения складывается из трёх частей:

  • строки, вошедшие во внутреннее соединение (INNER JOIN);
  • строки левой таблицы, для которых не нашлось пары;
  • строки правой таблицы, для которых не нашлось пары.
MySQL 8.1
SELECT Timepair.id "timepair.id", start_pair, end_pair,
    Schedule.id "schedule.id", date, class, number_pair, teacher, subject, classroom
FROM Timepair
    FULL OUTER JOIN Schedule ON Schedule.number_pair = Timepair.id;

На данных этой базы результат совпадёт с левым соединением — те же 43 строки: непарные строки здесь есть только слева.

MySQL не поддерживает FULL OUTER JOIN, но тот же результат можно собрать вручную: взять левое соединение и добавить к нему строки правой таблицы, которым не нашлось пары.

MySQL 8.1
SELECT поля_таблиц
FROM левая_таблица
    LEFT JOIN правая_таблица ON правая_таблица.ключ = левая_таблица.ключ

UNION ALL

SELECT поля_таблиц
FROM левая_таблица
    RIGHT JOIN правая_таблица ON правая_таблица.ключ = левая_таблица.ключ
WHERE левая_таблица.ключ IS NULL;

Условие во второй части обязательно: без него совпавшие строки попали бы в результат дважды.

Все виды соединений таблиц

Все строки левой таблицы
Левая таблица
ключ
1
2
3
Правая таблица
ключ
1
1
2
4
Результат: 4 строки
левая.ключ
правая.ключ
1
1
1
1
2
2
3
NULL
SELECT поля_таблиц
FROM левая_таблица
    LEFT JOIN правая_таблица ON правая_таблица.ключ = левая_таблица.ключ

Давайте проверим себя. В левой таблице 8 строк. Сколько строк вернёт LEFT JOIN с правой таблицей?