Введение
Многие архитектуры баз данных разделяют информацию по разным таблицам на основе взаимосвязей между конкретными точками данных. Даже в таких случаях, вероятно, возникнет необходимость извлечь информацию из нескольких таблиц одновременно.
Распространенным способом доступа к данным из нескольких таблиц в SQL-операциях является объединение таблиц с помощью предложения JOIN. Основанное на операции соединения в реляционной алгебре, предложение JOIN объединяет отдельные таблицы, сопоставляя строки в каждой таблице, которые связаны друг с другом. Как правило, эта связь основана на паре столбцов (по одному из каждой таблицы), имеющих общие значения, например, внешний ключ одной таблицы и первичный ключ другой таблицы, на которую ссылается внешний ключ.
В этом руководстве объясняется, как создавать различные SQL-запросы с предложением JOIN. Здесь также показаны различные типы предложений JOIN, как объединять данные из нескольких таблиц и как создавать псевдонимы столбцов для упрощения написания операций JOIN.
Предпосылки
Для выполнения этого руководства вам понадобится компьютер с установленной системой управления реляционными базами данных (СУРБД), использующей SQL. Инструкции и примеры в этом руководстве были проверены в следующей среде:
- Сервер под управлением Ubuntu 20.04 с пользователем без прав root, обладающим правами администратора, и брандмауэром, настроенным с помощью UFW.
- MySQL установлен и защищен на сервере.
- Вам также понадобится база данных с несколькими таблицами, загруженными демонстрационными данными, которые вы сможете использовать для практики использования операций JOIN. Рекомендуем вам ознакомиться с разделом «Подключение к MySQL и настройка демонстрационной базы данных» ниже, чтобы узнать, как подключиться к серверу MySQL и создать тестовую базу данных, используемую в примерах этого руководства.
Подключение к MySQL и настройка образца базы данных
Если ваша система базы данных SQL работает на удаленном сервере, подключитесь к серверу по SSH с локальной машины:
ssh sammy@your_server_ip
Затем откройте командную строку сервера MySQL и замените Sami на свое имя пользователя MySQL:
mysql -u sammy -p
Создайте базу данных с именем joinsDB:
CREATE DATABASE joinsDB;
Если база данных создана успешно, вы получите следующий вывод:
Output
Query OK, 1 row affected (0.01 sec)Чтобы выбрать базу данных joinsDB, выполните следующий оператор USE:
USE joinsDB;Output
Database changedВыбрав joinsDB, создайте в ней несколько таблиц. Для примеров, используемых в этом руководстве, представьте, что вы управляете фабрикой и решили отслеживать информацию о производственной линии, сотрудниках отдела продаж и продажах вашей компании в базе данных SQL. Вы начнёте с трёх таблиц, первая из которых будет хранить информацию о ваших продуктах. Вы решаете, что для этой первой таблицы потребуется три столбца:
- Идентификатор продукта: идентификационный номер каждого продукта, выраженный типом данных int. Этот столбец служит первичным ключом таблицы, то есть каждое значение служит уникальным идентификатором соответствующей строки. Поскольку каждое значение в первичном ключе должно быть уникальным, к этому столбцу также применяется ограничение UNIQUE.
- productName: Название каждого продукта выражается с использованием типа данных varchar и длиной не более 20 символов.
- Цена: цена каждого товара, выраженная в десятичном формате. Это означает, что длина каждого значения в этом столбце ограничена четырьмя цифрами, причём две из этих цифр находятся справа от десятичной точки. Таким образом, диапазон допустимых значений в этом столбце составляет от -99,99 до 99,99.
Создайте таблицу под названием «Продукты», которая будет содержать три столбца:
CREATE TABLE products (
productID int UNIQUE,
productName varchar(20),
price decimal (4,2),
PRIMARY KEY (productID)
);Вторая таблица хранит информацию о сотрудниках отдела продаж вашей компании. Вы решаете, что в этой таблице также должны быть три столбца:
- empID: Подобно столбцу productID, этот столбец содержит уникальный идентификационный номер каждого сотрудника отдела продаж, выраженный типом данных int. Таким образом, этот столбец будет иметь ограничение UNIQUE и будет выступать в качестве первичного ключа для таблицы team.
- empName: Имя каждого продавца, выраженное с использованием типа данных varchar и длиной не более 20 символов.
- productSpecialty: Каждому сотруднику вашего отдела продаж назначена специализация на определённом продукте. Они могут продавать любой продукт вашей компании, но их основное внимание сосредоточено на продукте, на котором они специализируются. Чтобы отразить это в таблице, создайте столбец, содержащий значение productID для каждого продукта, на котором специализируется каждый сотрудник.
Чтобы столбец productSpecialty содержал только значения, соответствующие допустимым идентификаторам продуктов, вы решили применить ограничение внешнего ключа к столбцу, ссылающемуся на столбец productID таблицы Products. Ограничение внешнего ключа — это способ выражения связи между двумя таблицами, требующий, чтобы значения в столбце, к которому оно применяется, существовали в столбце, на который оно ссылается. В следующем операторе CREATE TABLE ограничение FOREIGN KEY требует, чтобы любое значение, добавляемое в столбец productSpecialty в таблице Team, существовало в столбце productID таблицы Products.
Создайте таблицу под названием «Команда» со следующими тремя столбцами:
CREATE TABLE team (
empID int UNIQUE,
empName varchar(20),
productSpecialty int,
PRIMARY KEY (empID),
FOREIGN KEY (productSpecialty) REFERENCES products (productID)
);
Последняя созданная вами таблица будет содержать данные о продажах компании. Эта таблица будет состоять из четырёх столбцов:
- saleID: Подобно столбцам productID и empID, этот столбец содержит уникальный идентификационный номер каждой продажи, выраженный типом данных int. Этот столбец также имеет ограничение UNIQUE, поэтому может выступать в качестве первичного ключа таблицы продаж.
- Количество: количество проданных единиц каждого продукта выражается типом данных int.
- Идентификатор продукта: идентификационный номер проданного продукта, выраженный в виде целого числа.
- Продавец: идентификационный номер сотрудника, совершившего продажу.
Как и в случае со столбцом productSpecialty из таблицы team, вы решили применить ограничения FOREIGN KEY к столбцам productID и salesperson. Это гарантирует, что эти столбцы будут содержать только значения, присутствующие в столбце productID таблицы products и столбце empID таблицы team соответственно.
Создайте таблицу под названием «Продажи» со следующими четырьмя столбцами:
CREATE TABLE sales (
saleID int UNIQUE,
quantity int,
productID int,
salesperson int,
PRIMARY KEY (saleID),
FOREIGN KEY (productID) REFERENCES products (productID),
FOREIGN KEY (salesperson) REFERENCES team (empID)
);Затем загрузите таблицу Products образцами данных, выполнив следующую операцию INSERT INTO:
INSERT INTO products
VALUES
(1, 'widget', 18.99),
(2, 'gizmo', 14.49),
(3, 'thingamajig', 39.99),
(4, 'doodad', 11.50),
(5, 'whatzit', 29.99);Затем загрузите таблицу команды с примерами данных:
INSERT INTO team
VALUES
(1, 'Florence', 1),
(2, 'Mary', 4),
(3, 'Diana', 3),
(4, 'Betty', 2);Загрузите также таблицу продаж с примерами данных:
INSERT INTO sales
VALUES
(1, 7, 1, 1),
(2, 10, 5, 4),
(3, 8, 2, 4),
(4, 1, 3, 3),
(5, 5, 1, 3);Наконец, представьте, что ваша компания совершает несколько продаж без участия кого-либо из отдела продаж. Чтобы зарегистрировать эти продажи, выполните следующую операцию, чтобы добавить в таблицу «Продажи» три строки, не содержащие значения в столбце «Продавец»:
INSERT INTO sales (saleID, quantity, productID)
VALUES
(6, 1, 5),
(7, 3, 1),
(8, 4, 5);После этого вы готовы приступить к выполнению оставшейся части руководства и начать изучать, как объединять таблицы в SQL.
Понимание синтаксиса операции JOIN
Предложения JOIN могут использоваться в различных операторах SQL, включая операции UPDATE и DELETE. Однако в качестве иллюстративных примеров в данном руководстве используются запросы SELECT, демонстрирующие работу предложений JOIN.
В следующем примере показан общий синтаксис оператора SELECT, включающего предложение JOIN:
SELECT table1.column1, table2.column2
FROM table1 JOIN table2
ON search_condition;Этот синтаксис начинается с оператора SELECT, который возвращает два столбца из двух отдельных таблиц. Обратите внимание: поскольку предложения JOIN сравнивают содержимое нескольких таблиц, в этом примере синтаксиса указывается, из какой таблицы должен быть выбран каждый столбец, при этом перед именем столбца ставится имя таблицы и точка. Это называется полной ссылкой на столбец.
Подобные полные ссылки на столбцы можно использовать в любой операции, но технически это необходимо только в тех случаях, когда два столбца из разных таблиц имеют одинаковые имена. Тем не менее, рекомендуется использовать их при работе с несколькими таблицами, поскольку они облегчают чтение и понимание операций JOIN.
После SELECT следует предложение FROM. В любом запросе предложение FROM определяет набор данных, который необходимо найти для возврата нужных данных. Единственное отличие заключается в том, что предложение FROM состоит из двух таблиц, разделённых ключевым словом JOIN. Полезный способ продумать написание запросов — не забыть выбрать столбцы, которые будут возвращены из той таблицы, которую вы хотите запросить.
Затем следует предложение ON, которое объясняет, как запрос должен объединить две таблицы, определяя условие поиска. Условие поиска — это набор из одного или нескольких операторов или выражений, которые могут быть результатом «истина», «ложь» или «неизвестно» при выполнении определённого условия. Операцию JOIN можно представить как объединение всех строк из обеих таблиц с последующим возвратом всех строк, для которых условие поиска в предложении ON даёт результат “истина”.
В предложение ON обычно имеет смысл включить условие поиска, которое проверяет, равны ли значения двух связанных столбцов, например, внешнего ключа одной таблицы и первичного ключа другой таблицы, на которую ссылается внешний ключ. Иногда это называется эквисоединением.
В качестве примера того, как equi может объединять совпадающие данные из нескольких таблиц, выполните следующий запрос, используя добавленный ранее пример данных. Он объединяет таблицы Products и Team с условием поиска, которое проверяет совпадающие значения в соответствующих столбцах productID и productSpecialty. Затем возвращается имя каждого сотрудника отдела продаж, название каждого продукта, на котором он специализируется, и цена этих продуктов:
SELECT team.empName, products.productName, products.price
FROM products JOIN team
ON products.productID = team.productSpecialtyدر اینجا مجموعه نتایج این پرس و جو است:
Output
+----------+-------------+-------+
| empName | productName | price |
+----------+-------------+-------+
| Florence | widget | 18.99 |
| Mary | doodad | 11.50 |
| Diana | thingamajig | 39.99 |
| Betty | gizmo | 14.49 |
+----------+-------------+-------+
4 rows in set (0.00 sec)Чтобы проиллюстрировать, как SQL объединяет эти таблицы для формирования результирующего набора, давайте подробнее рассмотрим этот процесс. Для ясности, следующее не совсем соответствует тому, как система управления базами данных объединяет две таблицы, но операцию JOIN можно рассматривать как процедуру.
Сначала запрос выводит продукты для каждой строки и столбца в первой таблице в предложении FROM:
JOIN Process Example
+-----------+-------------+-------+
| productID | productName | price |
+-----------+-------------+-------+
| 1 | widget | 18.99 |
| 2 | gizmo | 14.49 |
| 3 | thingamajig | 39.99 |
| 4 | doodad | 11.50 |
| 5 | whatzit | 29.99 |
+-----------+-------------+-------+JOIN Process Example
+-----------+-------------+-------+-------+----------+------------------+
| productID | productName | price | empID | empName | productSpecialty |
+-----------+-------------+-------+-------+----------+------------------+
| 1 | widget | 18.99 | 1 | Florence | 1 |
| 2 | gizmo | 14.49 | 4 | Betty | 2 |
| 3 | thingamajig | 39.99 | 3 | Diana | 3 |
| 4 | doodad | 11.50 | 2 | Mary | 4 |
| 5 | whatzit | 29.99 | | | |
+-----------+-------------+-------+-------+----------+------------------+JOIN Process Example
+----------+-------------+-------+
| empName | productName | price |
+----------+-------------+-------+
| Florence | widget | 18.99 |
| Mary | doodad | 11.50 |
| Diana | thingamajig | 39.99 |
| Betty | gizmo | 14.49 |
+----------+-------------+-------+
4 rows in set (0.00 sec)Использование эквисоединений — наиболее распространённый способ объединения таблиц, но в условии поиска в предложении ON можно использовать и другие операторы SQL, такие как <, >, LIKE, NOT LIKE и даже BETWEEN. Однако имейте в виду, что использование более сложных условий поиска может затруднить прогнозирование данных, которые появятся в результирующем наборе.
В большинстве реализаций таблицы можно объединять по любому набору столбцов, имеющих тип данных, который стандарт SQL называет «квалифицированным объединением» (qualified JOIN). Это означает, что, как правило, можно объединить столбец с числовыми данными с любым другим столбцом с числовыми данными, независимо от соответствующих типов данных. Аналогично, обычно можно объединить любой столбец с символьными значениями с любым другим столбцом с символьными данными. Как упоминалось ранее, столбцы, сопоставляемые для объединения двух таблиц, обычно представляют собой столбцы, представляющие связь между таблицами, например, внешний ключ и первичный ключ другой таблицы, на которую он ссылается.
Многие реализации SQL позволяют объединять столбцы с одинаковыми именами с помощью ключевого слова USING вместо ON. Синтаксис такой операции может быть следующим:
SELECT table1.column1, table2.column2
FROM table1 JOIN table2
USING (related_column);В этом примере синтаксиса оператор USING эквивалентен ON table1.related_column = table2.related_column;.
Поскольку у каждой записи о продажах и товарах есть столбец с именем productID, вы можете объединить эти столбцы, сопоставив их с ключевым словом USING. Следующий оператор делает это и возвращает saleID каждой продажи, количество проданных единиц, название каждого проданного товара и его цену. Он также сортирует результат по значению saleID в порядке возрастания:
SELECT sales.saleID, sales.quantity, products.productName, products.price
FROM sales JOIN products
USING (productID)
ORDER BY saleID;Output
+--------+----------+-------------+-------+
| saleID | quantity | productName | price |
+--------+----------+-------------+-------+
| 1 | 7 | widget | 18.99 |
| 2 | 10 | whatzit | 29.99 |
| 3 | 8 | gizmo | 14.49 |
| 4 | 1 | thingamajig | 39.99 |
| 5 | 5 | widget | 18.99 |
| 6 | 1 | whatzit | 29.99 |
| 7 | 3 | widget | 18.99 |
| 8 | 4 | whatzit | 29.99 |
+--------+----------+-------------+-------+
8 rows in set (0.00 sec)При объединении таблиц система базы данных иногда упорядочивает строки непредсказуемым образом. Добавление такого предложения ORDER BY может помочь сделать наборы результатов более связными и удобочитаемыми.
Объединение более двух таблиц
Иногда может потребоваться объединить данные из более чем двух таблиц. Вы можете объединить любое количество таблиц, вложив предложения JOIN в другие предложения JOIN. Следующий синтаксис — пример того, как это будет выглядеть при объединении трёх таблиц:
SELECT table1.column1, table2.column2, table3.column3
FROM table1 JOIN table2
ON table1.related_column = table2.related_column
JOIN table3
ON table3.related_column = table1_or_2.related_column;Предложение FROM в этом примере оператора начинается с объединения таблицы 1 с таблицей 2. После этого предложения ON начинается второе объединение, которое объединяет исходный набор объединенных таблиц с таблицей 3. Обратите внимание, что третья таблица может быть присоединена к столбцу как в первой, так и во второй таблице.
Например, представьте, что вы хотите узнать объем продаж вашего сотрудника, но вас интересуют только те записи о продажах, которые включают продажи продукта, на котором специализируется этот сотрудник.
Чтобы получить эту информацию, выполните следующий запрос. Этот запрос начинается с объединения таблиц Products и Sales путем сопоставления соответствующих столбцов productID. Затем он присоединяет таблицу Team к первым двум таблицам, сопоставляя каждую строку в начальном операторе JOIN со столбцом productSpecialty. Затем запрос фильтрует результаты с помощью предложения WHERE, чтобы вернуть только строки, в которых сотрудник, соответствующий запросу, также является лицом, совершившим продажу. Запрос также включает предложение ORDER BY, которое сортирует результаты по возрастанию значения столбца saleID:
SELECT sales.saleID,
team.empName,
products.productName,
(sales.quantity * products.price)
FROM products JOIN sales
USING (productID)
JOIN team
ON team.productSpecialty = sales.productID
WHERE team.empID = sales.salesperson
ORDER BY sales.saleID;
Output
+--------+----------+-------------+-----------------------------------+
| saleID | empName | productName | (sales.quantity * products.price) |
+--------+----------+-------------+-----------------------------------+
| 1 | Florence | widget | 132.93 |
| 3 | Betty | gizmo | 115.92 |
| 4 | Diana | thingamajig | 39.99 |
+--------+----------+-------------+-----------------------------------+
3 rows in set (0.00 sec)Во всех примерах до сих пор использовался один тип выражения JOIN: внутреннее JOIN. В следующем разделе вы найдете обзор внутренних и внешних соединений, а также их различий.
Внутренние и внешние операции JOIN
Существует два основных типа операторов JOIN: внутренние соединения (INNER) и внешние (OUTER). Разница между этими двумя типами соединений заключается в возвращаемых данных. Внутренние соединения возвращают только совпадающие строки из каждой объединённой таблицы, тогда как внешние (OUTER) соединения возвращают как совпадающие, так и несовпадающие строки.
Синтаксис и примеры запросов из предыдущих разделов используют операторы INNER JOIN, хотя ни один из них не содержит ключевого слова INNER. Большинство реализаций SQL рассматривают каждый оператор JOIN как INNER-соединение, если явно не указано иное.
Запросы с внешним JOIN объединяют несколько таблиц и возвращают как совпадающие, так и несовпадающие строки. Это может быть полезно для поиска строк с пропущенными значениями или в случаях, когда допустимо частичное совпадение.
Операции внешнего соединения можно разделить на три типа: LEFT OUTER соединения, RIGHT OUTER соединения и FULL OUTER соединения. LEFT OUTER соединения, или просто левые соединения, возвращают каждую совпадающую строку из двух соединенных таблиц, а также каждую несовпадающую строку из “левой” таблицы. В контексте операции JOIN “левая” таблица — это всегда первая таблица, указанная сразу после ключевого слова FROM и слева от ключевого слова JOIN. Аналогично, «правая» таблица — это вторая таблица или таблица, которая идет сразу после JOIN, и RIGHT OUTER соединение возвращает каждую совпадающую строку из соединенных таблиц, а также каждую несовпадающую строку из «правой» таблицы. FULL OUTER JOIN возвращает каждую строку из обеих таблиц, включая строки из любой из таблиц, которые не совпадают.
Чтобы продемонстрировать, как эти различные типы предложений JOIN возвращают данные, выполните следующие примеры запросов к таблицам, созданным в предыдущем разделе «Подключение к образцу базы данных и его настройка». Эти запросы идентичны, за исключением того, что каждый из них указывает разный тип предложения JOIN.
В этом первом примере используется внутренний JOIN для объединения таблиц sales и team путем сопоставления соответствующих столбцов salesperson и empID. Ключевое слово INNER, опять же, подразумевается, хотя оно и не указано явно:
SELECT sales.saleID, sales.quantity, sales.salesperson, team.empName
FROM sales JOIN team
ON sales.salesperson = team.empID;Output +--------+----------+-------------+----------+ | saleID | quantity | salesperson | empName | +--------+----------+-------------+----------+ | 1 | 7 | 1 | Florence | | 4 | 1 | 3 | Diana | | 5 | 5 | 3 | Diana | | 2 | 10 | 4 | Betty | | 3 | 8 | 4 | Betty | +--------+----------+-------------+----------+ 5 rows in set (0.00 sec)
SELECT sales.saleID, sales.quantity, sales.salesperson, team.empName
FROM sales LEFT OUTER JOIN team
ON sales.salesperson = team.empID;Output
+--------+----------+-------------+----------+
| saleID | quantity | salesperson | empName |
+--------+----------+-------------+----------+
| 1 | 7 | 1 | Florence |
| 2 | 10 | 4 | Betty |
| 3 | 8 | 4 | Betty |
| 4 | 1 | 3 | Diana |
| 5 | 5 | 3 | Diana |
| 6 | 1 | NULL | NULL |
| 7 | 3 | NULL | NULL |
| 8 | 4 | NULL | NULL |
+--------+----------+-------------+----------+
8 rows in set (0.00 sec)SELECT sales.saleID, sales.quantity, sales.salesperson, team.empName
FROM sales RIGHT JOIN team
ON sales.salesperson = team.empID;Обратите внимание, что предложение JOIN в этом запросе читается как RIGHT JOIN, а не RIGHT OUTER JOIN. Так же, как ключевое слово INNER не требуется для указания предложения INNER JOIN, OUTER подразумевается при использовании LEFT JOIN или RIGHT JOIN.
Результат этого запроса противоположен предыдущему, поскольку он возвращает каждую строку из обеих таблиц, но только уникальные строки из “правильной” таблицы:
Output
+--------+----------+-------------+----------+
| saleID | quantity | salesperson | empName |
+--------+----------+-------------+----------+
| 1 | 7 | 1 | Florence |
| NULL | NULL | NULL | Mary |
| 4 | 1 | 3 | Diana |
| 5 | 5 | 3 | Diana |
| 2 | 10 | 4 | Betty |
| 3 | 8 | 4 | Betty |
+--------+----------+-------------+----------+
6 rows in set (0.00 sec)Псевдонимы таблиц и столбцов в предложениях JOIN
При объединении таблиц с длинными или слишком описательными именами написание нескольких полных ссылок на столбцы может стать утомительным. Чтобы избежать этого, пользователи иногда используют более короткий псевдоним имени таблицы или столбца.
Это можно сделать в SQL, добавив после каждого определения таблицы в предложении FROM ключевое слово AS, а затем псевдоним по вашему выбору:
SELECT t1.column1, t2.column2
FROM table1 AS t1 JOIN table2 AS t2
ON t1.related_column = t2.related_column;В этом примере синтаксиса в предложении SELECT используются псевдонимы, хотя они не определены перед предложением FROM. Это возможно, поскольку в SQL-запросах порядок выполнения начинается с предложения FROM. Это может сбивать с толку, но полезно помнить об этом и учитывать псевдонимы перед написанием запроса.
Например, выполните следующий запрос, который объединяет таблицы Sales и Products, присваивая им псевдонимы S и P соответственно:
SELECT S.saleID, S.quantity,
P.productName,
(P.price * S.quantity) AS revenue
FROM sales AS S JOIN products AS P
USING (productID);Output
+--------+----------+-------------+---------+
| saleID | quantity | productName | revenue |
+--------+----------+-------------+---------+
| 1 | 7 | widget | 132.93 |
| 2 | 10 | whatzit | 299.90 |
| 3 | 8 | gizmo | 115.92 |
| 4 | 1 | thingamajig | 39.99 |
| 5 | 5 | widget | 94.95 |
| 6 | 1 | whatzit | 29.99 |
| 7 | 3 | widget | 56.97 |
| 8 | 4 | whatzit | 119.96 |
+--------+----------+-------------+---------+
8 rows in set (0.00 sec)SELECT S.saleID, S.quantity, P.productName, (P.price * S.quantity) revenue
FROM sales S JOIN products P
USING (productID);Хотя ключевое слово AS не является обязательным для определения псевдонима, его использование считается хорошей практикой. Это помогает сохранить ясность смысла запроса и улучшить его читаемость.
Результат
Прочитав это руководство, вы узнали, как использовать операцию JOIN для объединения отдельных таблиц в единый набор результатов запроса. Хотя представленные здесь команды должны работать в большинстве реляционных баз данных, учтите, что каждая база данных SQL использует собственную уникальную реализацию языка. Более подробное описание каждой команды и её полного набора параметров см. в документации к вашей СУБД.









