DML. JOIN

DML. JOIN

Дорогу осилит идущий

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

Для этого нам понадобится таблица, которая будет представлять связанную сущность. Пусть это будет сущность “билет”.

Создадим саму таблицу. Подробно познакомимся с синтаксисом в ближайшие пару уроков, пока - просто копируем и выполняем запрос ниже:

create table ticket (
  id                     bigserial,
  departure_airport      varchar(100),
  arrival_airport        varchar(100),
  departure_date         timestamp,
  arrival_date           timestamp,
  purchase_date          timestamp,
  passenger_id           bigint
);

Вы можете заметить поле passenger_id в таблице выше. Оно необходимо для обеспечения связи с таблицей пассажиров на уровне данных. Логично предположить, что у одного пассажира может быть несколько билетов, поэтому закладываем связь M2O (или O2M, если смотреть со стороны таблицы passenger).

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

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


Оператор JOIN

Основным способом связывания таблиц в SQL является оператор JOIN. Он используется вместе с дополнительными операторами: ON, USING, NATURAL. Чтобы избежать каши, в рамках статьи мы будем разбирать только вариант с первым вспомогательным оператором - ON

Оставшиеся два являются синтаксическим сахаром, позволяющим описывать способ связывания более лаконично, но имеют свои особенности, не привнося при этом никаких новых возможностей. Желающие могут ознакомиться самостоятельно (по ссылке - русскоязычная документация по JOIN, может быть полезна для закрепления материала в целом): ссылка

Простейший запрос с помощью JOIN в нашем случае может выглядеть так:

select * from passenger p
join ticket t on p.id = t.passenger_id;

После ON располагается условие связывания - оно подобно WHERE-условиям. Записи считаются связанными, если JOIN-условие вернет для них true. В большинстве случаев такое условие является проверкой равенства колонок, обеспечивающих связь (в нашем случае - p.id и t.passenger_id). Но иногда могут встречаться и дополнительные условия (как правило, связанные с фильтрацией выборки, но указанные в JOIN-, а не в WHERE-условиях).

Запрос выше вернет информацию по всем пассажирам и его билетам. Для каждого пассажира с N билетов, в итоговой выборке будет N строк. Т.е. если билетов у пассажира нет - в итоговую выборку он не попадет.

Теперь, когда перед глазами есть пример, разберемся в видах JOIN’ов - они определяются с помощью дополнительных модификаторов и определяют, как именно будет формироваться итоговая выборка.


INNER JOIN

Вид JOIN’а, который используется в запросе выше. INNER является не обязательным модификатором и его можно опустить. С ним запрос выше выглядел бы так:

select * from passenger p
inner join ticket t on p.id = t.passenger_id;

Результаты запроса идентичны результатам в примере выше.

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

Итак, INNER JOIN:

Для каждой строки R1 из T1 (в нашем случае - passenger) в результирующей таблице содержится строка для каждой строки в T2 (ticket), удовлетворяющей условию соединения с R1.

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


В силу того, что INNER не является обязательным модификатором, можно считать INNER JOIN - JOIN’ом по умолчанию.


LEFT OUTER JOIN

Этот тип JOIN’а обеспечивает наличие всех строк из левой таблицы. Если есть подходящие связанные строки в правой - они будут добавлены (поведение как в INNER JOIN). Строкам левой таблицы, которым не найдено соответствие в правой таблице, будет соответствовать по одной строке в результирующей выборке (вместо значений из правой таблицы будут подставлены NULL'ы):

select * from passenger p
left outer join ticket t on p.id = t.passenger_id;

При этом модификатор OUTER является опциональным, его можно опустить:

select * from passenger p
left join ticket t on p.id = t.passenger_id;

Описание из документации:

Сначала выполняется внутреннее соединение (INNER JOIN). Затем в результат добавляются все строки из T1, которым не соответствуют никакие строки в T2, а вместо значений столбцов T2 вставляются NULL. Таким образом, в результирующей таблице всегда будет минимум одна строка для каждой строки из T1.

Визуализация:


RIGHT OUTER JOIN

Данный вид JOIN’а зеркален предыдущему (OUTER, опять же, опционален):

select * from passenger p
right join ticket t on p.id = t.passenger_id;

Такой запрос вернет все билеты, если какому-то из них не будет соответствовать пассажира - колонки, ему соответствующие, будут заполнены NULL’ами.

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

select * from ticket t
left join passenger p on p.id = t.passenger_id;

Описание из документации:

Сначала выполняется внутреннее соединение (INNER JOIN). Затем в результат добавляются все строки из T2, которым не соответствуют никакие строки в T1, а вместо значений столбцов T1 вставляются NULL. Это соединение является обратным к левому (LEFT JOIN): в результирующей таблице всегда будет минимум одна строка для каждой строки из T2.

Визуализация:



FULL OUTER JOIN

FULL JOIN (OUTER - опционален) позволяет получить из двух таблиц пересекающиеся записи (как в INNER JOIN), а также записи каждой из таблиц, для которых не нашлось записей в связанных таблицах (колонки связанных таблиц будут заполнены NULL’ами):

select * from passenger p
full join ticket t on p.id = t.passenger_id;

Для этого вида JOIN’ов порядок таблиц (первая и вторая или же левая и правая) не имеют значения по очевидным причинам. Как, впрочем, и для INNER JOIN.


Для наглядности, запрос выше можно также представить как UNION-запрос с LEFT JOIN и RIGHT JOIN. Результирующая выборка будет идентичной*:

select * from passenger p
left join ticket t on p.id = t.passenger_id
union
select * from passenger p
right join ticket t on p.id = t.passenger_id;
*При кратно худшей производительности. В реальных задачах так извращаться не стоит.

Описание из документации:

Сначала выполняется внутреннее соединение. Затем в результат добавляются все строки из T1, которым не соответствуют никакие строки в T2, а вместо значений столбцов T2 вставляются NULL. И наконец, в результат включаются все строки из T2, которым не соответствуют никакие строки в T1, а вместо значений столбцов T1 вставляются NULL.

Визуализация:



CROSS JOIN

Особняком от остальных видов JOIN стоит CROSS JOIN.

Результатом связи таблиц через CROSS JOIN является декартово произведение всех записей - иными словами, каждой записи из таблицы1 будет соответствовать N записей из таблицы2, где N - число записей в таблице2.

Еще проще: это связь всех со всеми.

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

Пример:

select * from passenger p
cross join ticket t;

В силу специфики подобного запроса, он является достаточно дорогостоящим. Благо, необходимость в таком виде соединения, как правило, не возникает.

Описание из документации:

Соединенную таблицу образуют все возможные сочетания строк из T1 и T2 (т. е. их декартово произведение), а набор ее столбцов объединяет в себе столбцы T1 со следующими за ними столбцами T2. Если таблицы содержат N и M строк, соединенная таблица будет содержать N * M строк.


Несколько JOIN’ов и другое

Зачастую при работе с БД требуется в одном запросе связать более, чем две таблицы - как в формате таблица1-таблица2 + таблица2-таблица3 (классический сценарий для M2M, явного или неявного), так и в формате таблица1-таблица2 + таблица1-таблица3 - в общем-то, любая выборка, где условия требуют получения информации/фильтрации/сортировки из трех и более таблиц одновременно.

Ничего сложного в таких запросах нет, но рассмотрим пример на базе абстрактных таблиц (в нашей текущей осознанный запрос с несколькими JOIN’ами построить проблематично):

select * from table1 t1
join table2 t2 on t2.id = t1.t2_id
join table3 t3 on t3.id = t2.t3_id;

Виды JOIN’ов могут быть любыми, в т.ч. разными от таблицы к таблице.


Кроме того, можно делать JOIN не только для обычных таблиц, но и для подзапросов:

select * from table1 t1
join (select * from table2) t2 on t2.id = t1.t2_id;

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


Бонус. Порядок блоков в рамках SELECT-запроса с учетом JOIN:

select count(*), p.male from passenger p
left join ticket t on p.id = t.passenger_id
where p.last_purchase is not null
group by p.male
having count(*)> 1
order by p.male
limit 1 offset 0;

Также прилагаю картинку с визуализацией видов JOIN’ов (и, бонусом UNION). Она не идеальна, но может выступить небольшой шпаргалкой:


С теорией на сегодня все!

Переходим к практике:

Задача 1

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


Задача 2

Найдите наиболее популярный аэропорт прибытия для каждой из фамилий пассажиров.


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/ViamSupervadetVadens

Мой тг: https://t.me/ironicMotherfucker

 

Дорогу осилит идущий!

Report Page