При работе с реляционными базами данных неизбежно и постоянно придется работать с соединениями. Помимо понимания самих соединений очень важно - это понимание NULL. В базах данных Оракл NULL следует понимать, как отсутствие значения. Любое сравнение с NULL будет на выходе давать NULL (неопределнное значение).
Возможно, нелишним будет проверить это поведение на своей БД (есть версия, что в некоторых БД проверка NULL с NULL дает True).

Основные типы JOIN в SQL

Поведение различных типов соединений в таблицах, содержащих NULL, рассмотрим их на примере двух таблиц: table_a и table_b.

Создание и наполнение таблиц

DROP TABLE table_a;
CREATE TABLE table_a (
    id INT,
    value VARCHAR(10)
);

INSERT INTO table_a VALUES (1, 'A');
INSERT INTO table_a VALUES (2, NULL);
INSERT INTO table_a VALUES (3, 'B');
INSERT INTO table_a VALUES (4, NULL);
INSERT INTO table_a VALUES (5, 'C');
INSERT INTO table_a VALUES (6, NULL);
INSERT INTO table_a VALUES (7, 'D');
INSERT INTO table_a VALUES (8, NULL);
INSERT INTO table_a VALUES (9, 'E');
INSERT INTO table_a VALUES (10, NULL);
-- Добавим второе значение 'B'
INSERT INTO table_a VALUES (11, 'B');

DROP TABLE table_b;
CREATE TABLE table_b (
    id INT,
    value VARCHAR(10)
);

INSERT ALL
  INTO table_b VALUES (1, 'A')
  INTO table_b VALUES (2, NULL)
  INTO table_b VALUES (3, NULL)
  INTO table_b VALUES (4, 'B')
  INTO table_b VALUES (5, NULL)
  INTO table_b VALUES (6, 'C')
  INTO table_b VALUES (7, NULL)
  INTO table_b VALUES (8, NULL)
  INTO table_b VALUES (9, 'W')
  INTO table_b VALUES (10, NULL)
  INTO table_b VALUES (11, NULL)
  INTO table_b VALUES (12, 'V')
  -- Добавим второе значение 'C'
  INTO table_b VALUES (13, 'C')
SELECT * FROM dual;

Содержимое таблиц

table_a:

id value
1 A
2 NULL
3 B
4 NULL
5 C
6 NULL
7 D
8 NULL
9 E
10 NULL
11 B

table_b:

id value
1 A
2 NULL
3 NULL
4 B
5 NULL
6 C
7 NULL
8 NULL
9 W
10 NULL
11 NULL
12 V
13 C

Виды JOIN

1. INNER JOIN

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

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
INNER JOIN table_b b ON a.value = b.value;

Результат:

a_id a_value b_id b_value
1 A 1 A
3 B 4 B
5 C 6 C
5 C 13 C
11 B 4 B

2. LEFT JOIN

Описание: Возвращает все строки из левой таблицы (table_a) и совпадающие строки из правой (table_b). Если совпадения нет — значения из правой таблицы будут NULL.
При соединениях важно понимать, что при совпадении будут возвращаться все значения из второй таблицы.
Лично мне первое время было трудно к этому привыкнуть, как активному пользователю функции ВПР а Excel (VLOOKUP), я ожидал, что значение будет только одно.
Чаще всего такое соединение применяется, когда нужно на выходе гарантированно получить все строки исходной таблицы и проверить наличие совпадений во второй таблице по ключу.

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
LEFT JOIN table_b b ON a.value = b.value;

Результат (сокращённо):

a_id a_value b_id b_value
1 A 1 A
2 NULL NULL NULL
3 B 4 B
4 NULL NULL NULL
5 C 6 C
5 C 13 C
6 NULL NULL NULL
7 D NULL NULL
8 NULL NULL NULL
9 E NULL NULL
10 NULL NULL NULL
11 B 4 B

3. RIGHT JOIN

Описание: Возвращает все строки из правой таблицы (table_b) и совпадающие строки из левой (table_a). Если совпадения нет — значения из левой таблицы будут NULL.

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
RIGHT JOIN table_b b ON a.value = b.value;

Результат (сокращённо):

a_id a_value b_id b_value
1 A 1 A
NULL NULL 2 NULL
NULL NULL 3 NULL
3 B 4 B
11 B 4 B
NULL NULL 5 NULL
5 C 6 C
5 C 13 C
NULL NULL 7 NULL
NULL NULL 8 NULL
NULL NULL 9 W
NULL NULL 10 NULL
NULL NULL 11 NULL
NULL NULL 12 V

Эти два запроса эквивалентны по результату (если правильно указаны алиасы):

SELECT *
FROM table_a a
RIGHT JOIN table_b b ON a.value = b.value;

и

SELECT *
FROM table_b b
LEFT JOIN table_a a ON a.value = b.value;

Почему?
RIGHT JOIN и LEFT JOIN — это зеркальные операции.
RIGHT JOIN возвращает все строки из правой таблицы (table_b), а LEFT JOIN — все строки из левой (table_b в данном случае).
Если поменять местами таблицы и тип соединения, результат будет одинаковым по содержанию (но порядок столбцов может отличаться).

Вывод:
a RIGHT JOIN b ON ...b LEFT JOIN a ON ...

4. FULL OUTER JOIN

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

FULL OUTER JOIN по сути возвращает объединение результатов LEFT JOIN и RIGHT JOIN, но с одним важным отличием:
- Если одна и та же строка попадает и в LEFT JOIN, и в RIGHT JOIN (то есть есть совпадение по ключу), то в результате FULL OUTER JOIN эта строка будет только один раз.

Если просто написать:

SELECT ... FROM a LEFT JOIN b ON ...
UNION
SELECT ... FROM a RIGHT JOIN b ON ...

— это даст тот же результат, что и FULL OUTER JOIN, если вы используете UNION, а не UNION ALL.
UNION убирает дубликаты, а UNION ALL — нет.

Важно:
- В некоторых СУБД (например, MySQL) нет FULL OUTER JOIN, и такой подход с UNION используется как замена.
- В стандартном SQL лучше использовать FULL OUTER JOIN, если он поддерживается.

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
FULL OUTER JOIN table_b b ON a.value = b.value;

Результат (сокращённо):

a_id a_value b_id b_value
1 A 1 A
3 B 4 B
11 B 4 B
5 C 6 C
5 C 13 C
2 NULL NULL NULL
4 NULL NULL NULL
6 NULL NULL NULL
7 D NULL NULL
8 NULL NULL NULL
9 E NULL NULL
10 NULL NULL NULL
NULL NULL 2 NULL
NULL NULL 3 NULL
NULL NULL 5 NULL
NULL NULL 7 NULL
NULL NULL 8 NULL
NULL NULL 9 W
NULL NULL 10 NULL
NULL NULL 11 NULL
NULL NULL 12 V

5. CROSS JOIN

Описание: Декартово произведение — каждая строка из первой таблицы соединяется с каждой строкой из второй.

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
CROSS JOIN table_b b;

Результат:
В результате будет 11 (строк в table_a) × 13 (строк в table_b) = 143 строки.
Ниже приведён пример строк a_id = 1 и строк с a_id = 2 из первой таблицы, чтобы было понятно, что на выходе будут все строки, в том числе NULL:

a_id a_value b_id b_value
1 A 1 A
1 A 2 NULL
1 A 3 NULL
1 A 4 B
1 A 5 NULL
1 A 6 C
1 A 7 NULL
1 A 8 NULL
1 A 9 W
1 A 10 NULL
1 A 11 NULL
1 A 12 V
2 NULL 1 A
2 NULL 2 NULL
2 NULL 3 NULL
2 NULL 4 B
2 NULL 5 NULL
2 NULL 6 C
2 NULL 7 NULL
2 NULL 8 NULL
2 NULL 9 W
2 NULL 10 NULL
2 NULL 11 NULL
2 NULL 12 V

...


6. LEFT JOIN ON 1=1 (Декартово произведение через LEFT JOIN)

Описание:
Если использовать LEFT JOIN с условием ON 1=1, то каждая строка из левой таблицы соединяется с каждой строкой из правой таблицы, потому что условие соединения всегда истинно. Это фактически превращает результат в декартово произведение, аналогичное CROSS JOIN, НО если правая таблица пуста, то LEFT JOIN всё равно вернёт все строки из левой таблицы с NULL-значениями справа, а CROSS JOIN вернёт пустой результат.

Такой JOIN мне понадобился при разборе XML документа, в котором содержались модели техники и ее свойства в виде вложенной структуры. Для некоторы моделей свойства отсутствовали, но важно было, чтобы это можно было получить в результате.

SELECT a.id as a_id, a.value as a_value, b.id as b_id, b.value as b_value
FROM table_a a
LEFT JOIN table_b b ON 1=1;

Примечание:
- Для непустых таблиц результат совпадает с CROSS JOIN.
- Формально, LEFT JOIN ... ON 1=1 и CROSS JOIN — не одно и то же, но на практике для двух обычных таблиц результат одинаков.

Ниже еще пример, где это может пригодиться.

Пример: "Разворачивание" свойств для каждой строки

Задача:
У вас есть таблица товаров (products) и отдельная таблица возможных свойств (properties). Не для всех товаров есть значения всех свойств, но вы хотите получить для каждого товара полный список свойств (даже если значения отсутствуют).

Структура:

products
---------
id | name
---|------
1  | Телефон
2  | Ноутбук

properties
-----------
id | property_name
---|--------------
1  | Цвет
2  | Вес
3  | Размер

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

Решение:

SELECT p.id as product_id, p.name, pr.property_name
FROM products p
LEFT JOIN properties pr ON 1=1

Результат:
Каждый товар будет "размножен" по всем свойствам. Это удобно, если вы хотите, например, подготовить шаблон для ввода значений свойств для каждого товара (например, в интерфейсе администратора).


Вывод:
LEFT JOIN ... ON 1=1 полезен, когда нужно "развернуть" одну таблицу по всем строкам другой, даже если между ними нет прямой связи. Это часто используется для генерации шаблонов, отчетов или подготовки данных для дальнейшего заполнения.


Заключение

Использование различных видов JOIN позволяет гибко объединять данные из нескольких таблиц в зависимости от поставленных задач. Важно понимать, как работает каждый тип соединения, чтобы получать корректные результаты при работе с базами данных. И очень важно понимать, как работает NULL в базе данных.