Основы языка SQL

Запросы с различными вариантами JOIN

В материале последовательно разбирается работа операторов соединения INNER JOIN и LEFT JOIN в SQL на примере базы данных AdventureWorks2017. Демонстрируется, как наличие или отсутствие пары в связанных таблицах влияет на результат выборки. Логика изложения строится от простого соединения двух таблиц к постепенному усложнению: добавление фильтрации по тексту, условие отбора непустых значений, соединение с дополнительной таблицей и, наконец, объединение трёх связанных таблиц. На каждом шаге сравнивается поведение внутреннего и левого внешнего соединений, что позволяет наглядно усвоить разницу между ними.

Основные мысли

В результате изучения лекции слушатель будет способен:
1. Объяснить разницу между внутренним соединением и левым внешним соединением.
2. Написать запрос с INNER JOIN для выборки данных из двух таблиц по условию равенства ключей.
3. Написать запрос с LEFT JOIN для получения полного набора записей из ведущей таблицы и дополнения их доступными данными из второй таблицы.
4. Применить фильтрацию с использованием LIKE для поиска подстроки в текстовом поле.
5. Интерпретировать появление значений NULL в результирующем наборе при использовании внешних соединений.
6. Отобрать строки с обязательным заполнением определённого столбца с помощью конструкции IS NOT NULL.
7. Строить многотабличные запросы, последовательно соединяя три таблицы через связи «первичный ключ — внешний ключ».
8. Сравнивать количество возвращаемых строк при замене INNER JOIN на LEFT JOIN и делать выводы о полноте данных.
9. Создавать псевдонимы таблиц для сокращения и упрощения синтаксиса запросов.
10. Комбинировать несколько условий в разделе WHERE с использованием логических операторов AND и OR.
Показывать лекцию целиком
Краткое изложение

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

Мы продолжаем работу с SQL и переходим к соединениям таблиц. Сначала рассмотрим внутреннее соединение — INNER JOIN, затем левое внешнее соединение — LEFT JOIN. Далее изучим агрегационные функции и группировку, но сейчас сосредоточимся на INNER JOIN. Этот тип соединения возвращает только те строки, для которых в обеих таблицах найдена пара по указанному условию. Для примеров будем использовать базу данных AdventureWorks2017.

Соединение Product и ProductReview

Рассмотрим простой запрос с внутренним соединением. Выполним выборку из таблицы Production.Product. Присвоим ей псевдоним P, чтобы сократить обращение к столбцам.

Из таблицы мы забираем:
Name — назовём это поле ProductName;
ProductID — идентификатор товара;
• комментарий из связанной таблицы Production.ProductReview.

Таблица ProductReview содержит отзывы о товарах и имеет столбец ProductID, который связывает её с таблицей Product. Мы соединяем эти две таблицы оператором INNER JOIN по равенству ProductID и присваиваем второй таблице псевдоним PR.

Условие соединения:

sql
INNER JOIN Production.ProductReview AS PR
ON P.ProductID = PR.ProductID

Сортируем результат по названию товара (ORDER BY P.Name). Запрос возвращает всего четыре строки, потому что только три продукта имеют отзывы, причём у одного из них — два отзыва. Остальные товары в выборку не попадают, так как INNER JOIN требует обязательного наличия пары.

Если заменить INNER JOIN на LEFT JOIN, запрос изменится. Левое внешнее соединение берёт все строки из левой таблицы (Product) и по возможности дополняет их данными из правой таблицы (ProductReview). Если для товара нет ни одного отзыва, в столбцах правой таблицы будет значение NULL. При выполнении LEFT JOIN мы получим уже 505 строк — по общему числу товаров. Только для четырёх из них будет заполнено поле комментария, у остальных оно останется пустым (NULL). Это наглядно демонстрирует разницу: INNER JOIN отсекает непарные записи, LEFT JOIN — сохраняет.

Добавление фильтрации по тексту

Усложним запрос, оставив внутреннее соединение. Вернём те же поля: название товара, его идентификатор и текст отзыва. Добавим условие в разделе WHERE, чтобы получить только те отзывы, которые содержат слово «heavy». Используем оператор LIKE с шаблоном '%heavy%' — это значит, что перед и после искомого слова могут находиться любые символы.

Запрос:
sql
SELECT P.Name AS ProductName, P.ProductID, PR.Comments AS ProductReviewComments
FROM Production.Product AS P
INNER JOIN Production.ProductReview AS PR
ON P.ProductID = PR.ProductID
WHERE PR.Comments LIKE '%heavy%'
ORDER BY P.ProductID;

В результате мы получим отзывы, в тексте которых встречается «heavy». Фильтрация применена уже после соединения, поэтому сначала находятся все товары с отзывами, а затем из них отбираются подходящие под условие.

Соединение с таблицей ProductModel

Расширим запрос, добавив третью таблицу — Production.ProductModel (псевдоним PM). Теперь мы хотим получить информацию о модели товара. В таблице Product есть столбец ProductModelID, который ссылается на ProductModelID в таблице ProductModel. Соединяем Product и ProductModel оператором INNER JOIN.

Из таблицы ProductModel выбираем:
ProductModelID как ModelID;
Name как ModelName.

Из таблицы Product помимо прежних полей дополнительно забираем:
StandardCost — цену закупки, преобразованную к типу DECIMAL с округлением до двух знаков после запятой;
ProductClass — класс продукта (столбец Class).

Поскольку столбец Name присутствует в обеих таблицах, уточняем его источник: P.Name и PM.Name. Соединение по-прежнему внутреннее, поэтому в результат попадут только те товары, у которых обязательно указан идентификатор модели. Сортируем по цене закупки.

При выполнении такого запроса возвращается 295 строк. Это меньше общего числа товаров (504), потому что у многих товаров модель не указана, и INNER JOIN их исключает. Если мы снова заменим INNER JOIN на LEFT JOIN для таблицы ProductModel, то получим все 504 строки: для товаров без модели столбцы ModelID и ModelName будут содержать NULL.

Отбор по непустому классу и названию модели

Продолжим работать с тем же набором полей и внутренним соединением с таблицей ProductModel. Добавим два условия фильтрации.

1. Поле Class (класс продукта) не должно быть пустым. Это достигается условием P.Class IS NOT NULL. Ранее в выборке из 295 строк присутствовали записи с незаполненным классом; теперь они исключаются, и количество строк сокращается до 229.
2. Название модели должно содержать слово «Fork» или слово «Front». Используем составное условие:

sql
AND (PM.Name LIKE '%Fork%' OR PM.Name LIKE '%Front%')

Оба условия объединяются оператором AND, то есть должны выполняться одновременно: класс указан, и название модели содержит одну из указанных подстрок.

Итоговый запрос вернёт всего девять строк — это товары, удовлетворяющие обоим критериям.

Соединение трёх таблиц: категории и подкатегории

Часто требуется соединить более двух таблиц. Рассмотрим связь товара с категориями. В базе AdventureWorks иерархия построена так:
• таблица Production.Product (псевдоним P) содержит товар и ссылку на подкатегорию через ProductSubcategoryID;
• таблица Production.ProductSubcategory (псевдоним PS) содержит название подкатегории и ссылку на категорию через ProductCategoryID;
• таблица Production.ProductCategory (псевдоним PC) содержит название категории.
Запрос с двумя внутренними соединениями:

sql
SELECT PC.Name AS CategoryName,
PS.Name AS SubcategoryName,
P.ProductID,
P.Name AS ProductName
FROM Production.Product AS P
INNER JOIN Production.ProductSubcategory AS PS
ON P.ProductSubcategoryID = PS.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS PC
ON PS.ProductCategoryID = PC.ProductCategoryID
ORDER BY CategoryName, SubcategoryName, ProductName;

Сначала Product соединяется с ProductSubcategory по идентификатору подкатегории. Затем результат соединяется с ProductCategory по идентификатору категории. Такой запрос вернёт 295 строк — только те товары, у которых заполнены и подкатегория, и категория.

Если изменить первый INNER JOIN на LEFT JOIN (для ProductSubcategory), сохранив второе соединение, результат увеличится до 504 строк. Товары без подкатегории будут включены, но столбцы подкатегории и категории для них окажутся равными NULL. Это иллюстрирует, что тип каждого соединения влияет на полноту итогового набора независимо, и можно гибко комбинировать INNER JOIN и LEFT JOIN в одном запросе.

Заключение

Внутреннее соединение (INNER JOIN) используется, когда нужны только сопоставленные данные. Левое внешнее соединение (LEFT JOIN) позволяет сохранить все записи ведущей таблицы и дополнить их при наличии связанных данных. Эти операции — основа для построения сложных запросов, с которыми мы продолжим работу.

Краткие итоги

Освоение соединений в SQL на практическом материале AdventureWorks выстраивает чёткое понимание того, как связанные таблицы взаимодействуют в реляционной модели. Ключевая развилка между INNER JOIN и LEFT JOIN становится не просто синтаксическим различием, а осознанным выбором бизнес-логики: показывать ли только обеспеченные данными объекты или демонстрировать полную картину с явными пробелами.

Разбор начинается с простейшего случая «товар–отзыв». Здесь сразу видно, что внутреннее соединение работает как фильтр по наличию пары, а левое внешнее — как механизм сохранения всех записей ведущей таблицы, независимо от того, нашлись ли для них отклики. Появление NULL в столбцах правой таблицы наглядно маркирует отсутствующие связи и учит интерпретировать такие ситуации в реальных отчётах.

Добавление текстового фильтра через LIKE показывает, что отбор по содержимому отзыва применяется уже к результату соединения, позволяя извлекать только осмысленные фрагменты данных. Переход к трём таблицам с участием ProductModel демонстрирует, как INNER JOIN последовательно отсекает всё, что не подкреплено ссылкой на модель. Условия IS NOT NULL и комбинация OR с LIKE внутри WHERE подводят к построению сложных критериев отбора на фоне уже соединённого набора.

Заключительный пример с трёхступенчатой иерархией категорий закрепляет навык связывания таблиц по цепочке внешних ключей. Практическое значение такого подхода трудно переоценить: любые аналитические выборки, будь то каталог товаров с рубрикацией или отчёт по отзывам, требуют точного определения типа соединения на каждом шаге. Итогом становится способность проектировать запрос, который вернёт ровно ту информацию, которая нужна пользователю — будь то строгий перечень с полными данными или реестр с явными лакунами, сигнализирующими о неполноте исходной информации. Эти навыки напрямую переносятся на задачи построения отчётности, интеграции данных и очистки массивов в любой прикладной области.
Соединения таблиц: INNER JOIN и LEFT JOIN

SQL-соединения позволяют объединять данные из нескольких таблиц. Основные типы — внутреннее соединение (INNER JOIN) и левое внешнее соединение (LEFT JOIN). Первое возвращает только строки, для которых найдено совпадение в обеих таблицах. Второе возвращает все строки из левой таблицы и дополняет их данными из правой, если таковые имеются; при отсутствии совпадений проставляется NULL.

Простой пример с товарами и отзывами

Используем базу AdventureWorks2017. Таблица Production.Product (псевдоним P) хранит товары. Таблица Production.ProductReview (PR) — отзывы, связанные с товарами через столбец ProductID. Запрос с INNER JOIN:

sql
SELECT P.Name AS ProductName, P.ProductID, PR.Comments
FROM Production.Product AS P
INNER JOIN Production.ProductReview AS PR
ON P.ProductID = PR.ProductID
ORDER BY P.Name;

Вернёт только товары, у которых есть отзывы (4 строки). Замена на LEFT JOIN даст все 504 товара, при этом для 500 из них поле Comments будет NULL.

Фильтрация с LIKE

Добавив WHERE PR.Comments LIKE '%heavy%', мы оставим только отзывы, содержащие слово «heavy». Шаблон % обозначает любое количество любых символов до и после искомого слова. Фильтрация применяется после соединения.

Соединение с ProductModel

Таблица Production.ProductModel (PM) содержит модели товаров и связана с Product через ProductModelID. Запрос:

sql
SELECT PM.ProductModelID AS ModelID, PM.Name AS ModelName,
P.ProductID, P.Name AS ProductName,
CAST(P.StandardCost AS DECIMAL(10,2)) AS StandardCost,
P.Class AS ProductClass
FROM Production.Product AS P
INNER JOIN Production.ProductModel AS PM
ON P.ProductModelID = PM.ProductModelID
ORDER BY StandardCost;

При INNER JOIN результат — 295 строк (только товары с моделью). При LEFT JOIN — 504 строки, для товаров без модели поля модели будут NULL.

Отбор по классу и названию модели

Добавим условия:
WHERE P.Class IS NOT NULL — исключает товары с пустым классом (остаётся 229 строк).
AND (PM.Name LIKE '%Fork%' OR PM.Name LIKE '%Front%') — оставляет модели, в названии которых встречается «Fork» или «Front».
Итог: 9 строк.

Соединение трёх таблиц: категории товаров

Иерархия: товар → подкатегория → категория. Таблицы: Product (P), ProductSubcategory (PS), ProductCategory (PC). Связи:
• P.ProductSubcategoryID = PS.ProductSubcategoryID
• PS.ProductCategoryID = PC.ProductCategoryID

Запрос с двумя INNER JOIN:

sql
SELECT PC.Name AS CategoryName, PS.Name AS SubcategoryName,
P.ProductID, P.Name AS ProductName
FROM Production.Product AS P
INNER JOIN Production.ProductSubcategory AS PS
ON P.ProductSubcategoryID = PS.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS PC
ON PS.ProductCategoryID = PC.ProductCategoryID
ORDER BY CategoryName, SubcategoryName, ProductName;

Возвращает 295 строк — товары с назначенными подкатегорией и категорией. При замене первого INNER JOIN на LEFT JOIN число строк возвращается к 504: товары без подкатегории включены, а поля подкатегории и категории получают NULL. Это демонстрирует возможность гибкой комбинации типов соединений в одном запросе.

Ключевые моменты

INNER JOIN — строгий фильтр: только сопоставленные записи.
LEFT JOIN — полный охват левой таблицы, незаполненные поля помечаются NULL.
• Псевдонимы упрощают обращение к таблицам.
• Фильтрация (WHERE) выполняется после соединений.
• Можно строить цепочки из многих таблиц, точно управляя полнотой данных на каждом шаге.

Выводы

1. INNER JOIN возвращает только те строки, для которых существуют совпадения в обеих соединяемых таблицах.
2. LEFT JOIN сохраняет все строки левой таблицы и заполняет столбцы правой таблицы значением NULL при отсутствии соответствия.
3. Замена INNER JOIN на LEFT JOIN увеличивает количество возвращаемых строк, если в правой таблице есть непарные записи левой.
4. Псевдонимы таблиц упрощают синтаксис и делают запрос более читаемым, особенно при соединении нескольких таблиц.
5. Условие соединения задаётся в предложении ON и обычно связывает первичный ключ одной таблицы с внешним ключом другой.
6. Фильтрация через WHERE выполняется после соединения и может как сужать выборку, так и исключать записи с NULL.
7. Оператор LIKE с шаблоном %text% позволяет искать подстроку в текстовых полях, не привязываясь к её позиции.
8. Проверка IS NOT NULL отбирает только те строки, в которых указанный столбец заполнен.
9. В одном запросе можно последовательно соединять три и более таблицы, комбинируя разные типы соединений.
10. При соединении трёх таблиц тип каждого JOIN влияет на полноту итогового набора независимо от остальных.
11. Сортировка результатов с помощью ORDER BY применяется к итоговому набору данных после всех соединений и фильтров.
12. Понимание различий между INNER JOIN и LEFT JOIN критически важно для получения корректных и полных данных из реляционной базы.

Вопросы для самопроверки

1. Какой тип соединения следует использовать, чтобы получить все товары и дополнить их отзывами, если они существуют, и почему?
2. Почему при использовании INNER JOIN между Product и ProductReview итоговое число строк может быть меньше количества записей в таблице Product?
3. Что произойдёт с количеством строк, если в запросе с LEFT JOIN добавить условие WHERE right_table.column IS NOT NULL?
4. Как повлияет на результат добавление фильтра LIKE '%heavy%' до соединения таблиц (если бы это было возможно) и после?
5. Каким образом проверить, что у товара не указан класс, и вывести такие товары?
6. Можно ли в одном запросе одновременно использовать INNER JOIN и LEFT JOIN для разных таблиц? Приведите примерную ситуацию.
7. В запросе соединены три таблицы: Product, Subcategory и Category с помощью INNER JOIN. Какие товары будут исключены из результата?
8. Как правильно записать условие для отбора товаров, названия моделей которых содержат «Fork» или «Front», при этом поле класса обязательно должно быть заполнено?
9. Почему в запросе с LEFT JOIN значения некоторых столбцов оказались NULL, хотя в исходных таблицах NULL вроде бы не было?
10. Каким псевдонимом можно сократить обращение к таблице Production.ProductModel и как это улучшает читаемость запроса?
11. При соединении двух таблиц через INNER JOIN по столбцу, содержащему NULL, как поведёт себя соединение?
12. Как отсортировать результат многотабличного запроса сначала по названию категории, затем по названию подкатегории и наконец по названию товара?
Вернуться к учебному плану