Соединение источников (JOIN)

Соединения объединяют данные из двух источников (которые обычно называют “левый” и “правый”) в один набор данных. Соединение данных осуществляется для каждой строки и обычно включает в себя проверку условия соединения (join condition) для того, чтобы определить, какие строки должны быть объединены и оказаться в результирующем наборе данных.

Результат соединения также может быть соединён с другим набором данных с помощью следующего соединения.

Существует несколько типов (INNER, OUTER) и классов (квалифицированные, натуральные, и др.) соединений, каждый из которых имеет свой синтаксис и правила.

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

Таблица PROGRAMMER

LANG_ID

NAME

1

Андрей

2

Леонид

1

Сергей

4

Григорий

Таблица LANG

LANG_ID

NAME

1

C/C++

2

Java

3

Python

Общий синтаксис использования операторов соединения нескольких источников можно представить в следующем виде:

SELECT ... FROM <source1> <join1> <source2> <join2> <source3> ...

где :

  • таблица

  • представление

  • селективная хранимая процедура, с возможными аргументами

  • производный запрос

  • общее табличное выражение

а :

  • [INNER] JOIN {ON condition | USING ( column-list )}

  • {LEFT | RIGHT | FULL} [OUTER] JOIN {ON condition | USING ( column-list )}

  • NATURAL [INNER] JOIN

  • NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN

  • CROSS JOIN

Рассмотрим виды соединений более подробно и приведем примеры.

Внутреннее соединение (INNER JOIN)

Данный тип соединяет только строки, которые удовлетворяет условию соединения. Порядок соединения таблиц неважен.

Cамый очевидный, но наиболее понятный алгоритм работы такого соединения заключается в том, что каждая строка левого источника сопоставляется с каждой строкой правого источника и происходит проверка условия. Если условие выполняется, то строки соединяются и включаются в результатирующий набор. Такой алгоритм универсален, но не оптимален и существует множество других реализаций такого типа соединения. Оптимизатор СУБД выбирает наиболее подходящий в зависимости от множества факторов. Какую бы реализацию не выбрал оптимизатор, результат всегда должен быть одинаков.

Например, запрос

SELECT * FROM PROGRAMMER P INNER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_ID

NAME

LANG_ID1

NAME1

1

Андрей

1

C/C++

2

Леонид

2

Java

1

Сергей

1

C/C++

Обратите внимание, что возможны несколько совпадений.

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

Оператор левого внешнего соединения LEFT OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора важен.

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

Например, запрос

SELECT * FROM PROGRAMMER P LEFT OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_ID

NAME

LANG_ID1

NAME1

1

Андрей

1

C/C++

2

Леонид

2

Java

1

Сергей

1

C/C++

4

Григорий

NULL

NULL

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

Оператор правого внешнего соединения RIGHT OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора важен.

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

Например, запрос

SELECT * FROM PROGRAMMER P RIGHT OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_ID

NAME

LANG_ID1

NAME1

1

Андрей

1

C/C++

2

Леонид

2

Java

1

Сергей

1

C/C++

NULL

NULL

3

Python

Полное внешнее соединение (FULL OUTER JOIN)

Оператор полного внешнего соединения FULL OUTER JOIN соединяет две таблицы. Порядок таблиц для оператора неважен.

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

Например, запрос

SELECT * FROM PROGRAMMER P FULL OUTER JOIN LANG L ON P.LANG_ID = L.LANG_ID

Выдаст результат

LANG_ID

NAME

LANG_ID1

NAME1

1

Андрей

1

C/C++

2

Леонид

2

Java

1

Сергей

1

C/C++

4

Григорий

NULL

NULL

NULL

NULL

3

Python

Явные условия соединения

В синтаксисе явного соединения есть предложение ON, с условием соединения, в котором может быть указано любое логическое выражение, но, как правило, оно содержит условие сравнения между двумя участвующими источниками. Довольно часто, это условие — проверка на равенство (или ряд проверок на равенство объединённых оператором AND) использующая оператор “=”.

Такие соединения называются эквисоединениями. Рассмотренные выше соединения являлись таковыми.

Еще примеры соединений с явными условиями:

Выборка всех заказчиков из города Детройт, которые сделали покупку.

SELECT *
FROM customers c
JOIN sales s ON s.cust_id = c.id
WHERE c.city = 'Detroit'

Тоже самое, но включает в выборку заказчиков, которые не совершали покупки.

SELECT *
FROM customers c
LEFT JOIN sales s ON s.cust_id = c.id
WHERE c.city = 'Detroit'

Для каждого мужчины выбрать женщин, которые выше него. Мужчины, для которых такой женщины не существуют, не будут выключены в выборку.

SELECT
m.fullname AS man,
f.fullname AS woman
FROM males m
JOIN females f ON f.height > m.height

Соединение с именованными столбцами

Эквисоединения часто сравнивают столбцы, которые имеют одно и то же имя в обеих таблицах. Для таких соединений мы можем использовать второй тип явных соединений, называемый соединением именованными столбцами (Named Columns Joins). Соединение именованными столбцами осуществляются с помощью предложения USING, в котором перечисляются только имена столбцов.

Таким образом, один из запросов выше

SELECT * FROM PROGRAMMER P INNER JOIN LANG L ON P.LANG_ID = L.LANG_ID

можно переписать следующим образом

SELECT * FROM PROGRAMMER P INNER JOIN LANG L USING (LANG_ID)

что значительно короче.

Результирующий набор несколько отличается, по крайней мере, при использовании SELECT *:

  • Результат соединения с явным условием соединения в предложении ON будет содержать каждый из столбцов дважды: один раз для левой таблицы и один раз для правой. Очевидно, что они будут иметь они и те же значения;

  • Результат соединения именованными столбцами, с помощью предложения USING, будет содержать эти столбцы один раз.

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

SELECT P.*, L.* FROM PROGRAMMER P INNER JOIN LANG L USING (LANG_ID)

Еще пример с соединением по именованным столбцам:

SELECT *
FROM flotsam f
JOIN jetsam j
ON f.sea = j.sea AND f.ship = j.ship

можно переписать так:

SELECT *
FROM flotsam
JOIN jetsam USING (sea, ship)

Естественное соединение (NATURAL JOIN)

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

Например, даны две таблицы:

CREATE TABLE TA (
    a BIGINT,
    s VARCHAR(12),
    ins_date DATE
);

CREATE TABLE TB (
    a BIGINT,
    descr VARCHAR(12),
    x FLOAT,
    ins_date DATE
);

Естественное соединение таблиц TA и TB будет происходить по столбцам a и ins_date , а два следующих оператора дадут один и тот же результат:

SELECT * FROM TA NATURAL JOIN TB;

SELECT * FROM TA JOIN TB USING (a, ins_date);

Как и все соединения, естественные соединения являются внутренними соединениями по умолчанию, но вы можете превратить их во внешние соединения, указав LEFT, RIGHT или FULL перед ключевым словом JOIN.

Если в двух исходных таблицах не будут найдены одноименные столбцы, то будет выполнен CROSS JOIN.

Неявное соединение

В стандарте SQL-89 таблицы, участвующие в соединении, задаются списком с разделяющими запятыми в предложении FROM. Условия соединения задаются в предложении WHERE среди других условий поиска. Такие соединения называются неявными.

Синтаксис неявного соединения может осуществлять только внутренние соединения.

Пример неявного соединения:

SELECT * FROM PROGRAMMER P, LANG L WHERE P.LANG_ID = LANG.LANG_ID

Фактически данный запрос внутри СУБД будет преобразован ко внутреннему соединению, по условию после WHERE. В настоящее время неявные соединения не рекомендованы для использования.

Перекрестные соединения (CROSS JOIN)

Такое соединение еще называют декартовым произведением двух отношений. Каждая строка левой таблицы соединяется с каждой строкой правой таблицы. Условий при этом никаких нет. Можно добавить условия в части WHERE по аналогии с неявным соединением.

Порядок таблиц для оператора неважен.

Перекрёстное соединение двух наборов эквивалентно их соединению по условию тавтологии (условие, которое всегда верно).

Следующие два запроса дадут один и тот же результат:

SELECT * FROM TA CROSS JOIN TB;
SELECT * FROM TA JOIN TB ON 1 = 1;

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

Для нашего примера запрос

SELECT * FROM PROGRAMMER CROSS JOIN LANG

вернул бы результат

LANG_ID

NAME

LANG_ID1

NAME1

1

Андрей

1

C/C++

1

Андрей

2

Java

1

Андрей

3

Python

2

Леонид

1

C/C++

2

Леонид

2

Java

2

Леонид

3

Python

1

Сергей

1

C/C++

1

Сергей

2

Java

1

Сергей

3

Python

4

Григорий

1

C/C++

4

Григорий

2

Java

4

Григорий

3

Python

Перекрёстные соединения редко полезны, кроме случаев, когда вы хотите получить список всех возможных комбинаций двух или более переменных. Предположим, вы продаёте продукт, который поставляется в различных размерах, различных цветов и из различных материалов. Если для каждой переменной значения перечислены в собственной таблице, то этот запрос будет возвращать все комбинации:

SELECT
    m.name,
    s.size,
    c.name
FROM materials m
    CROSS JOIN sizes s
    CROSS JOIN colors c