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

Глава 5. Запросы к нескольким таблицам

SQL Lab in JupyterLab

Data & BI Analyst
SQLAlchemy - подключение создано
JupySQL - успешно подключен через SQLAlchemy Engine!

Еще в главе 2 было продемонстрировано, как связанные концепции разбиваются на отдельные части с помощью процесса, известного как нормализация. Конечным результатом этого упражнения были две таблицы: person и favorite_food.

Если вы хотите создать единый отчет с указанием имени, адреса и любимой еды человека, вам понадобится механизм, который снова соберет данные из этих двух таблиц. Этот механизм известен как соединение (join), и в этой главе основное внимание уделяется простейшему и наиболее распространенному соединению – внутреннему (inner join).

В главе 10 демонстрируются все типы соединений.

Что такое соединение

Запросы к одной таблице – это, конечно, не редкость. Но вы обраружите, что для большинства ваших запросов потребуется две, три или даже больше таблиц.

Для иллюстрации рассмотрим определения таблиц customer и address, а затем определим запрос, который извлекает данные из обеих таблиц:

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

Допустим, вы хотите получить имя и фамилию каждого клиента, а также его почтовый адрес. Таким образом, ваш запрос должен будет получить столбцы customer.first_name, customer.last_name и address.address.

Но как получить данные из обеих таблиц в одном запросе? Ответ кроется в столбце customer.address_id, содержащем идентификатор записи клиента в таблице address (более формально – столбец customer.adderss_id является внешним ключом к таблице address). Запрос, который вы вскоре увидите, предписывает серверу использовать столбец customer.adderss_id в качестве транспорта между таблицами customer и address, что позволяет включать в результирующий набор запроса столбцы из обеих таблиц. Этот тип операции известе как соединение (join).

3.0.5

Декартово произведение

Самый простой способ начать решать поставленную задачу – поместить таблицы customer и address в предложение from запроса и посмотреть что получится.

Вот запрос, который извлекает имена и фамилии клиентов вместе с почтовым адресом; с предложением from, указывающим обе таблицы разделенные ключевым словом JOIN:

Loading...
Loading...

Гм... Всего имеется 599 клиентов, а в таблице address 603 строки. Так откуда же в результирующем набора 361 197 строк? Присмотревшись, можно увидеть, что многие клиенты имеют один и тот же адрес. Поскольку в запросе не указано как именно должны быть соединены две таблицы, сервер базы данных сгенерировал декартово произведение, которое представляет собой все возможные сочетания записей из двух таблиц (599 клиентов * 603 адреса = 361 197 сочетаний).

Этот тип соединения известен как перекрестное соединение (cross join) и используется крайне редко (по крайней мере, преднамеренно). Перекрестные соединения – один из типов соединений, которые рассмотрим в главе 10.

Внутренние соединения

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

Ранее я сообщил, что столбец customer.address_id служит связующим звеном между двумя таблицами. Поэтому необходимо добавить эту информацию в подпредложение on предложения from:

Loading...
Loading...

Теперь вместо 361 197 строк у нас имеются ожидаемые 599 строк – благодаря добавлению подпредложения on, которое указывает серверу необходимость соединения таблиц customer и address, используя столбец address_id для перехода от одной таблицы к другой.

Например, строка ‘MARY SMITH’ в таблице customer содержит значение 5 в столбце address_id (в примере не показано). Сервер использует это значение для поиска в таблице address строки, имеющей значение 5 в столбце address_id, а затем извлекает значение ‘1913 Hanoi Way’ из столбца address в этой строке.

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


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

Loading...
Loading...

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


Если имена столбцов, используемых для соединения двух таблиц, идентичны (как это было в предыдущем запросе), вместо подпредложения on можно использовать подпредложение using:

Синтаксис соединения ANSI

Loading...
Loading...

Преимущество синтаксиса соединения SQL92 проще увидеть для сложных запросов, которые включают как условия соединения, так и условия фильтрации.

Старый синтаксис соединения ANSI:

Loading...
Loading...

Запрос с использованием синтаксиса соединения SQL92:

Loading...
Loading...

Соединение трех и более таблиц

Соединение трех таблиц аналогично соединению двух таблиц, но с одним небольшим отличием. При соединении двух таблиц имеются две таблицы и один тип соединения в предложении from, а также одно подпредложение on определяющее как таблицы соединяются.

При соединении трех таблиц, в предложении from есть три таблицы, два типа соединения и два подпредложения on.

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

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

Чтобы показать город каждого клиента, необходимо перейти от таблицы customer к таблице address (используя столбец address_id), а затем – от таблицы address к таблице city с использованием столбца city_id.

Запрос будет выглядеть следующим образом:

Loading...
Loading...

Для этого запроса используются три таблицы, два соединения и два подпредложения on в from, так что все стало выглядеть более запутанным. На первый взгляд может показаться, что порядок, в котором таблицы появляются в предложении from важен. Но если вы измените порядок таблиц, то получите точно такие же результаты.

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

Единственное различие, которое можно увидеть – это порядок, в котором возвращаются строки, поскольку в запросах нет предложения order by, указывающего как должны быть упорядочены результаты.

Loading...
Loading...

Использование подзапросов в качестве таблиц

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

Следующий запрос соединяет таблицу customer с подзапросом к таблицам address и city:

Loading...
Loading...

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

Один из способов визуализировать происходящее – выполнить подзапрос сам по себе и посмотреть на полученные результаты. Вот результаты подзапроса из предыдущего примера:

Loading...
Loading...

Этот результирующий набор состоит из всех девяти адресов в Калифорнии. При соединении с таблицей customer через столбец address_id результирующий набор будет содержать информацию о клиентах, которым принадлежат эти адреса.

Использование одной таблицы дважды

Соединяя несколько таблиц, можем обнаружить, что нужно соединиться с одной и той же таблицей более одного раза. В рассматриваемом нами примере базы данных, например, актеры связаны с фильмами в которых они появлялись через таблицу film_actor. Если хотим найти все фильмы, в которых фигурирует два конкретных актера, можем написать запрос, который соединяет таблицу film с таблицей film_actor и с таблицей actor:

Loading...
Loading...

Этот запрос возвращает все фильмы в которых снимались Cate McQueen или Cuba Birch.

Предположим, что требуется получить только те фильмы, в которых появляются оба актера. Для этого нужно найти все строки в таблице film у которых есть две строки в таблице film_actor, одна из которых связана с Cate McQueen, а другая с Cuba Birch. Следовательно, требуется включить таблицы film_actor и actor дважды – каждый раз с иным псевдонимом, чтобы сервер знал на что именно мы ссылаемся в различных предложениях:

Loading...
Loading...

Эти два актера снялись в 54 разных фильмах, но есть всего два фильма, в которых снялись оба актера.

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


Самосоединение

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

Некоторые таблицы включают в себя самоссылающиеся внешние ключи (self-referencing foreign key). Это означает, что в таблице имеется столбец, ссылающийся на первичный ключ в той же таблице.

Хотя образец базы данных такую связь не включает, давайте представим, что в таблице film есть столбец prequel_film_id, который указывает на родительский фильм (например, фильм Потерянный скрипач 2 будет использовать этот столбец, чтобы указать на фильм Потерянный скрипач как на родительский).

Вот как выглядела бы таблица, если бы мы добавили этот дополнительный столбец:

DESC film;
FieldTypeNullKeyDefault
film_idsmallint unsignedNOPRINone
titlevarchar(128)NOMULNone
descriptiontextYESNone
release_yearyearYESNone
language_idtinyint unsignedNOMULNone
original_language_idtinyint unsignedYESMULNone
rental_durationtinyint unsignedNO3
rental_ratedecimal(4,2)NO4.99
lengthsmallint unsignedYESNone
replacement_costdecimal(5,2)NO19.99
ratingenum(‘G’,‘PG’,‘PG-13’,‘R’,‘NC-17’)YESG
special_featuresset(‘Trailers’,‘Commentaries’,‘Deleted Scenes’,‘Behind the Scenes’)YESNone
last_updatetimestampNOCURRENT_TIMESTAMP
prequel_film_idsmallint(5)YESMULNULL

Используя самосоединение можно написать запрос, в котором будут перечислены все фильмы с приквелами, включая название приквела:

SELECT f.title, f_prnt.title prequel
FROM film f
    INNER JOIN film f_prnt
        ON f_prnt.film_id = f.prequel_film_id
WHERE f.prequel_film_id IS NOT NULL;
| title           | prequel      |
| --------------- | ------------ |
| FIDDLER LOST II | FIDDLER LOST |

Этот запрос соединяет таблицу film с самой собой с помощью внешнего ключа prequel_film_id. Псевдонимы таблицы f и f_print используются для того, чтобы было понятно, какая таблица и для какой цели используется.


Упражнения

Упражнение 5.1

Заполните пропущенные места (обозначенные как <#>) в следующем запросе так, чтобы получить показанные результаты.

SELECT c.first_name, c.last_name, a.address, ct.city
FROM customer c
  INNER JOIN address <1>
  ON c.address_id = a.address_id
  INNER JOIN city ct
  ON a.city_id = <2>
WHERE a.district = 'California';
| first_name | last_name | address                | city           |
| ---------- | --------- | ---------------------- | -------------- |
| PATRICIA   | JOHNSON   | 1121 Loja Avenue       | San Bernardino |
| BETTY      | WHITE     | 770 Bydgoszcz Avenue   | Citrus Heights |
| ALICE      | STEWART   | 1135 Izumisano Parkway | Fontana        |
| ROSA       | REYNOLDS  | 793 Cam Ranh Avenue    | Lancaster      |
| RENEE      | LANE      | 533 al-Ayn Boulevard   | Compton        |
| KRISTIN    | JOHNSTON  | 226 Brest Manor        | Sunnyvale      |
| CASSANDRA  | WALTERS   | 920 Kumbakonam Loop    | Salinas        |
| JACOB      | LANCE     | 1866 al-Qatif Avenue   | El Monte       |
| RENE       | MCALISTER | 1895 Zhezqazghan Drive | Garden Grove   |
Loading...
Loading...

Упражнение 5.2

Напишите запрос, который выводил бы названия всех фильмов, в которых играл актер с именем JOHN.

Loading...
Loading...

Упражнение 5.3

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

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