Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Technical Portfolio
GitHub & GitVerse Pages

Глава 10. Соединения

SQL Lab in JupyterLab

Data & BI Analyst
Pandas ver. 3.0.5: порог усечения строк уменьшен до 20
SQLAlchemy ver. 2.0.52: подключение создано
JupySQL ver. 0.11.1: подключен через SQLAlchemy Engine

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

INNER JOIN

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

В этой главе рассматриваются другие способы соединения таблиц, включая внешнее соединение и перекрестное соединение.

Внешние соединения

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

Например, таблица inventory содержит строку для каждого фильма, доступного для проката, но из 1000 строк в таблице film только 958 имеют одну или несколько строк в таблице inventory. Остальные 42 фильма для проката недоступны (возможно это новинки, которые должны прибыть в пункты проката со дня на день), поэтому идентификаторы этих фильмов в таблице inventory отсутствуют.

Следующий запрос подсчитывает количество доступных копий каждого фильма с помощью соединения этих двух таблиц:

Loading...
Loading...

Хотя можно было бы ожидать, что будет возвращено 1000 строк (по одной для каждого фильма), запрос возвращает только 958 строк.

Дело в том, что запрос использует внутреннее соединение, которое возвращает только строки, удовлетворяющие условию соединения. Например, фильм Alice Fantasia (film_id равен 14) не отображается в результатах, потому что для него нет строк в таблице inventory.

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

Loading...
Loading...

Как видите, запрос теперь возвращает все 1000 строк из таблицы film, при этом 42 строки из общего количества строк (включая фильм Alice Fantasia) имеют значение 0 в столбце num_copies, что указывает на отсутствие доступных для проката копий.

Вот описание изменений по сравнению с предыдущей версией запроса.

  • Определение соединения было изменено с inner на left outer, что указывает серверу на необходимость включения всех строк из таблицы в левой части соединения (в данном случае film) и включения столбцов из таблицы с правой стороны соединения (inventory), если соединение прошло успешно.

  • Определение столбца num_copies было изменено с count(*) на count(i.inventory_id: это выражение подсчитывает количество значений столбца inventory.inventory_id не равных null.

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

Loading...
Loading...

Результаты показывают, что в прокате имеется четыре копии Ali Forever и шесть копий Alien Center.

Вот так выглядит тот же запрос. но с использованием внешнего соединения:

Loading...
Loading...

Результаты для Ali Forever и Alien Center остаются прежними, но появляется одна новая строка для Alice Fantasia со значением null (NaN) в столбце inventory.inventory_id. В этом примере показано, как внешнее соединение добавляет значения столбцов без ограничения на количество строк, возвращаемых запросом. Если условие соединения не выполняется (как в случае Alice Fantasia), то любые столбцы, извлеченные из внешне соединенной таблицы, будут иметь значения null.

Левое и правое внешние соединения

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

LEFT JOIN

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

Однако можно указать и правое внешнее соединение – в этом случае таблица с правой стороны от right outer join отвечает за определение количества строк в результирующем наборе, тогда как таблица с левой стороны используется для предоставления значений столбцов.

RIGHT JOIN

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

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

Loading...
Loading...

Заметим, что обе версии запроса выполняют внешнее соединение. Ключевые слова left и right служат только для того, чтобы сообщить серверу, в какой таблице разрешено иметь пробелы в данных.

Если вы хотите внешне соединить таблицы А и В и хотите, чтобы в результирующем наборе присутствовали все строки из А (с дополнительными столбцами из В всякий раз, когда есть соответствующие данные), то можете указать либо А left outer join B либо B right outer join A.

Трехсторонние внешние соединения

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

Loading...
Loading...

Резульаты включают в себя все фильмы, имеющиеся в наличии, но у фильма Alice Fantasia в столбцах из обеих таблиц, соединенных внешним соединением находятся значения null (None).

Перекрестные соединения

Еще в главе 5 была представлена концепция декартова произведения, которая по сути представляет собой результат соединения нескольких таблиц без указания каких-либо условий соединения.

Декартово произведение довольно часто используется случайно (например, если вы забыли добавить условие соединения в предложение from), но преднамеренное его применение не так уж распространено. Если все же намереваетесь сгенерировать декартово произведение двух таблиц, то должны указать перекрестное соединение как в следующем примере:

Loading...
Loading...

Этот запрос генерурует декартово произведение таблиц category и language, в результате чего получается результирующий набор из 96 строк (16 строк category x 6 строк language).

Теперь, когда вы знаете что такое перекрестное соединение и как его указать, возникает вопрос А для чего оно используется? В большинстве книг по SQL после описания что такое перекрестное соединение, просто говорится что оно редко бывает полезным. Но я хотел бы поделиться с вами ситуацией, в которой считаю перекрестное соединение весьма полезным.

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

Loading...
Loading...

Хотя эта таблица – именно то, что нужно для разделения клиентов на три группы на основе их платежей, такая стратегия объединения однострочных таблиц с использованием оператора union all – все же не самый лучший вариант в случае больших таблиц.

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

SELECT '2020-01-01' dt
UNION ALL
SELECT '2020-01-02' dt
UNION ALL
SELECT '2020-01-03' dt
UNION ALL
...
SELECT '2020-12-29' dt
UNION ALL
SELECT '2020-12-30' dt
UNION ALL
SELECT '2020-12-31' dt

Создание запроса, объединяющего результаты 366 запросов, несколько утомительно, поэтому пожалуй нужна другая стратегия.

Что если вы созданите таблицу с 366 строками (2020 – високосный год) с одним столбцом, содержащим числа от 0 до 366, а затем добавите это количество дней к 1 января 2020 года? Вот один из возможных способов создания такой таблицы:

Loading...
Loading...

Если возьмете декартово произведение трех множеств {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {0, 10, 20, 30, 40, 50, 60, 70, 80, 90} и {0, 100, 200, 300} и сложите значения в трех столбцах, то получите набор результатов из 400 строк, содержащий все числа от 0 до 399. Хотя это больше, чем 366 строк, необходимых для создания набора дней в 2020 году, избавиться от лишних строк очень легко, и вскоре я это продемонстрирую.

Следующим шагом является преобразование набора чисел в набор дат. Для этого используем функцию date_add для добавления каждого числа в результирующем набора к 1 января 2020 года. Затем добавим условие фильтрации, чтобы отбросить все даты, относящиеся к 2021 году:

Loading...
Loading...

Приятным в этом подходе является то, что набор результатов автоматически включает дополнительный високосный день (29 февраля) без вашего вмешательства, поскольку сервер базы данных вычисляет его, когда добавляет 59 дней к 1 января 2020 года.

Теперь, когда у вас есть механизм для создания всех дней в 2020 году, что же с ним делать? Ну, вас могут попросить создать отчет, который будет показывать каждый день в 2020 году вместе с количеством прокатов фильмов в этот день. Отчет должен содержать каждый день года, включая дни, когда фильмы напрокат никто не брал.

Вот как может выглядеть такой запрос (с использованием 2005 года для сопоставления данных в таблице rental):

Loading...
Loading...

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

Естественные соединения

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

Этот тип соединения, известный как естественное соединение, основывается при выводе условий соединения на идентичности имен столбцов в нескольких таблицах.

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

Loading...

Поскольку мы указали естественное соединение, сервер проверяет определения таблиц и добавляет условие r.customer_id = c.customer_id для соединения двух таблиц. Это могло бы получиться, но в схеме Sakila все таблицы включают столбец last_update, чтобы знать когда каждая строка была в последний раз изменена. Поэтому сервер добавляет еще одно улсовие соединения r.last_update = c.last_update, которое и приводит к тому, что запрос не возвращает никакие данные.

Единственный способ обойти эту проблему – использовать подзапрос, чтобы ограничить столбцы как минимум одной таблицей:

Loading...
Loading...

Подумайте и решите, стоит ли снижение износа пальцев и клавиатуры (благодаря отказу от указания условия соединения) дополнительных хлопот? Вряд ли! Так что следует избегать этого типа соединения и использовать внутренние соединения с явными условиями.


Упражнения

Упражнение 10.1

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

            Customer:
Customer_id Name
----------- ---------------
1           John Smith
2           Kathy Jones
3           Greg Oliver

            Payment:
Payment_id Customer_id Amount
---------- ----------- --------
101        1           8.99
102        3           4.99
103        1           7.99

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

-- Решение
SELECT c.name, SUM(p.amount)
FROM customer c
    LEFT JOIN payment p
    	ON c.customer_id = p.customer_id
GROUP BY c.name;
Loading...
Loading...

Упражнение 10.2

Измените запрос из упражнения 10.1 таким образом, чтобы использовать другой тип внешнего соединения (например, если вы использовали левое внешнее соединение в упражнении 10.1, на этот раз используйте правое внешнее соединение) так, чтобы результаты были идентичны полученным ранее.

-- Решение
SELECT c.name, SUM(p.amount)
FROM payment p
    RIGHT JOIN customer c
        ON p.customer_id = c.customer_id
GROUP BY c.name;
Loading...
Loading...

Упражнение 10.3

Разработайте запрос, который будет генерировать набор {1, 2, 3, ... 99, 100}. Указание: используйте перекрестное соединение как минимум с двумя подзапросами в предложении from.

Loading...
Loading...

Мои решения возможно менее красивые, но хороши для тренировки:

Loading...
Loading...
Loading...
Loading...
Loading...
Loading...