Напиши какие виды join бывают в чем особенность каждого
SQL Ключевое слово JOIN
SQL INNER JOIN
Команда INNER JOIN возвращает строки, имеющие совпадающие значения в обеих таблицах.
Следующий SQL выбирает все заказы с информацией о клиенте:
Пример
Примечание: Ключевое слово INNER JOIN выбирает все строки из обеих таблиц до тех пор, пока существует соответствие между столбцами. Если в таблице «Заказы» есть записи, которые не имеют совпадений в разделе «Клиенты», то эти заказы не будут показаны!
Следующая инструкция SQL выбирает все заказы с информацией о клиенте и грузоотправителе:
Пример
SQL LEFT JOIN
Команда LEFT JOIN возвращает все строки из левой таблицы и соответствующие строки из правой таблицы. Результат будет NULL с правой стороны, если нет совпадения.
В следующем SQL будут выбраны все клиенты и любые заказы, которые они могут иметь:
Пример
Примечание: Ключевое слово LEFT JOIN возвращает все записи из левой таблицы (Клиенты), даже если в правой таблице (Заказы) нет совпадений.
SQL RIGHT JOIN
Команда RIGHT JOIN возвращает все строки из правой таблицы и соответствующие записи из левой таблицы. Результат будет NULL с левой стороны, когда нет совпадения.
Следующий SQL вернет всех сотрудников и все заказы, которые они могли бы разместить:
Пример
Примечание: Ключевое слово RIGHT JOIN ключевое слово возвращает все записи из правой таблицы (Сотрудники), даже если в левой таблице (Заказы) нет совпадений.
SQL FULL OUTER JOIN
Команда FULL OUTER JOIN возвращает все строки, когда есть совпадение либо в левой таблице, либо в правой таблице.
Следующая инструкция SQL выбирает всех клиентов и все заказы:
Примечание: Ключевое слово FULL OUTER JOIN возвращает все строки из левой таблицы (Клиенты) и все строки из правой таблицы (Заказы). Если есть строки в разделе «Клиенты», которые не имеют совпадений в разделе «Заказы», или если есть строки в разделе «Заказы», которые не имеют совпадений в разделе «Клиенты», то эти строки также будут перечислены.
Объяснение SQL объединений JOIN: LEFT/RIGHT/INNER/OUTER
Разберем пример. Имеем две таблицы: пользователи и отделы.
В результате отсутствуют:
Внутреннее объединение INNER JOIN (синоним JOIN, ключевое слово INNER можно опустить).
Выбираются только совпадающие данные из объединяемых таблиц.
Чтобы получить данные, которые подходят по условию частично, необходимо использовать
Такое объединение вернет данные из обеих таблиц (совпадающие по условию объединения) ПЛЮС дополнит выборку оставшимися данными из внешней таблицы, которые по условию не подходят, заполнив недостающие данные значением NULL.
Получаем полный список пользователей и сопоставленные департаменты.
в выборке останется только 3#Александр, так как у него не назначен департамент.
рис. Left outer join с фильтрацией по полю
RIGHT OUTER JOIN вернет полный список департаментов (правая таблица) и сопоставленных пользователей.
Дополнительно можно отфильтровать данные, проверяя их на NULL.
В нашем примере указав WHERE u.id IS null, мы выберем департаменты, в которых не числятся пользователи. (3#Финансы)
Все примеры вы можете протестировать здесь:
Cross/Full Join
FULL JOIN возвращает `объединение` объединений LEFT и RIGHT таблиц, комбинируя результат двух запросов.
CROSS JOIN возвращает перекрестное (декартово) объединение двух таблиц. Результатом будет выборка всех записей первой таблицы объединенная с каждой строкой второй таблицы. Важным моментом является то, что для кросса не нужно указывать условие объединения.
Дублирование строк при использовании JOIN
При использовании объединения новички часто забывают что результирующая выборка может содержать дублирующиеся данные!
Если вам нужна одна запись, делайте объединение с подзапросом
Self Join
Выборка из одной и той же таблицы для нескольких условий.
Рассмотрим задачку от яндекса:
Есть таблица товаров.
Она содержит следующие значения.
Напишите запрос, выбирающий уникальные пары `id` товаров с одинаковыми `name`, например:
При решении задачи необходимо учесть, что пары (x,y) и (y,x) — одинаковы.
— или без группировки (быстрее)
Объединяем таблицы ya_goods по одинаковому полю `name`, группируем по уникальным idентификаторам и получаем результат.
Множественное объединение multi join
Пригодится нам, если необходимо выбрать более одного значения из таблиц для нескольких условий.
Пример: выбрать товары,
добавленные после 17/01/2009 в следующих вариантах:
— вес=310, объем=300
— вес=35, объем=15
— вес=45, объем=25
— вес=200, объем=250
INNER JOIN product2options p2o1 ON p.id = p2o1.product_id
INNER JOIN product_options po1 ON po1.id = p2o1.option_id
INNER JOIN product2options p2o2 ON p.id = p2o2.product_id
INNER JOIN product_options po2 ON po2.id = p2o2.option_id
id title created_at P1 value P2 value
2 Ложка 2009-01-18 20:00:00 Вес 35 Объем 15
3 Тарелка 2009-01-19 20:00:00 Вес 310 Объем 300
2 Ложка 2009-01-18 20:00:00 Вес 45 Объем 25
— не попадает по дате
1 Кружка 2009-01-17 20:00:00 Объем 250 Вес 200
UPDATE и JOIN
Объединение можно использовать совместно с UPDATE.
Например, имеем таблицу houses (id, title, area). Нужно выбрать title, если в нем встречается `число м2`, заменить поле area, если оно меньше. Т.к. в mysql отстутсутствует поддержка регулярных выражений, нужно немного поколдовать с locate и substr.
В подзапросе выбираем интересующие нас данные, и в финальной стадии осуществляем обновление данных подходящий по критерию (p5 > area).
FROM ga_pageviews
WHERE title like ‘ % м2 % ‘
) calc USING ( `id` )
SET base. area = calc.p5
WHERE base. area calc.p5
DELETE и JOIN
Рассмотрим пример с удалением дубликатов. Есть таблица tableWithDups (id, email). Нужно удалить строки с одинаковыми email:
Последние два примера не совместимы с ANSI SQL, но работают в mySQL.
За бортом статьи остались смежные объединениям (а также специфичные для определенных базданных темы):
SELF JOIN, FULL OUTER JOIN, CROSS JOIN (CROSS [OUTER] APPLY), операции над множествами UNION [ALL], INTERSECT, EXCEPT и т.д.
@tags: sql, mysql, sql server, oracle, sqlite, postgresql
Осмысляем работу джойнов в SQL: от реляционной алгебры до наглядных картинок
Выбираем, какие фильмы посмотреть, с помощью соединения данных в SQL.
скриншот из игры team fortress 2 / valve
Опять эта проблема — выбрать кино на вечер. Благодаря стриминговым сервисам доступны едва ли не все фильмы мира: это бесконечное полотно с постерами и фильтры, фильтры, фильтры…
МОЗГ: Поставлю-ка я фильтр по стране: пусть будет Дания, и добавлю ограничение по жанру — триллер… Ну вот — другое дело, относительно небольшой список.
— Мозг, а знаешь почему? Да потому что здесь только фильмы, которые сняты в Дании И помечены как триллеры.
— Да не знаю я, как задать такие критерии в этом сервисе. Вот если бы можно было писать на SQL — тут бы решение нашлось для любой комбинации признаков.
— Легко! Ещё и картинки будут. У меня и база фильмов уже спарсена — тренируйся не хочу.
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Договоримся об обозначениях
Назовём множество датских фильмов — D, а множество триллеров — T. У каждого фильма будет уникальный номер, он же ключ. Раз ключ — пусть зовётся Key.
Заодно вспомним, как на SQL пишется простой запрос для связывания данных из двух таблиц:
INNER JOIN
Если не уточнить тип соединения ( JOIN), то по умолчанию применяется INNER JOIN — как раз тот вариант, который сработал в нашем кинофильтре. Это он выбирает и триллеры, и датские фильмы одновременно.
Учебник по языку SQL (DDL, DML) на примере диалекта MS SQL Server. Часть четвертая
Предыдущие части
В данной части мы рассмотрим
Добавим немного новых данных
Для демонстрационных целей добавим несколько отделов и должностей:
JOIN-соединения – операции горизонтального соединения данных
Здесь нам очень пригодится знание структуры БД, т.е. какие в ней есть таблицы, какие данные хранятся в этих таблицах и по каким полям таблицы связаны между собой. Первым делом всегда досконально изучайте структуру БД, т.к. нормальный запрос можно написать только тогда, когда ты знаешь, что откуда берется. У нас структура состоит из 3-х таблиц Employees, Departments и Positions. Приведу здесь диаграмму из первой части:
Если суть РДБ – разделяй и властвуй, то суть операций объединений снова склеить разбитые по таблицам данные, т.е. привести их обратно в человеческий вид.
Если говорить просто, то операции горизонтального соединения таблицы с другими таблицами используются для того, чтобы получить из них недостающие данные. Вспомните пример с еженедельным отчетом для директора, когда при запросе из таблицы Employees, нам для получения окончательного результата недоставало поля «Название отдела», которое находится в таблице Departments.
Понимание каждого вида соединения очень важно, т.к. от применения того или иного вида, результат запроса может отличаться. Сравните результаты одного и того же запроса с применением разного типа соединения, попробуйте пока просто увидеть разницу и идите дальше (мы сюда еще вернемся):
ID | Name | DepartmentID | ID | Name |
---|---|---|---|---|
1000 | Иванов И.И. | 1 | 1 | Администрация |
1001 | Петров П.П. | 3 | 3 | ИТ |
1002 | Сидоров С.С. | 2 | 2 | Бухгалтерия |
1003 | Андреев А.А. | 3 | 3 | ИТ |
1004 | Николаев Н.Н. | 3 | 3 | ИТ |
ID | Name | DepartmentID | ID | Name |
---|---|---|---|---|
1000 | Иванов И.И. | 1 | 1 | Администрация |
1001 | Петров П.П. | 3 | 3 | ИТ |
1002 | Сидоров С.С. | 2 | 2 | Бухгалтерия |
1003 | Андреев А.А. | 3 | 3 | ИТ |
1004 | Николаев Н.Н. | 3 | 3 | ИТ |
1005 | Александров А.А. | NULL | NULL | NULL |
ID | Name | DepartmentID | ID | Name |
---|---|---|---|---|
1000 | Иванов И.И. | 1 | 1 | Администрация |
1002 | Сидоров С.С. | 2 | 2 | Бухгалтерия |
1001 | Петров П.П. | 3 | 3 | ИТ |
1003 | Андреев А.А. | 3 | 3 | ИТ |
1004 | Николаев Н.Н. | 3 | 3 | ИТ |
NULL | NULL | NULL | 4 | Маркетинг и реклама |
NULL | NULL | NULL | 5 | Логистика |
ID | Name | DepartmentID | ID | Name |
---|---|---|---|---|
1000 | Иванов И.И. | 1 | 1 | Администрация |
1001 | Петров П.П. | 3 | 3 | ИТ |
1002 | Сидоров С.С. | 2 | 2 | Бухгалтерия |
1003 | Андреев А.А. | 3 | 3 | ИТ |
1004 | Николаев Н.Н. | 3 | 3 | ИТ |
1005 | Александров А.А. | NULL | NULL | NULL |
NULL | NULL | NULL | 4 | Маркетинг и реклама |
NULL | NULL | NULL | 5 | Логистика |
ID | Name | DepartmentID | ID | Name |
---|---|---|---|---|
1000 | Иванов И.И. | 1 | 1 | Администрация |
1001 | Петров П.П. | 3 | 1 | Администрация |
1002 | Сидоров С.С. | 2 | 1 | Администрация |
1003 | Андреев А.А. | 3 | 1 | Администрация |
1004 | Николаев Н.Н. | 3 | 1 | Администрация |
1005 | Александров А.А. | NULL | 1 | Администрация |
1000 | Иванов И.И. | 1 | 2 | Бухгалтерия |
1001 | Петров П.П. | 3 | 2 | Бухгалтерия |
1002 | Сидоров С.С. | 2 | 2 | Бухгалтерия |
1003 | Андреев А.А. | 3 | 2 | Бухгалтерия |
1004 | Николаев Н.Н. | 3 | 2 | Бухгалтерия |
1005 | Александров А.А. | NULL | 2 | Бухгалтерия |
1000 | Иванов И.И. | 1 | 3 | ИТ |
1001 | Петров П.П. | 3 | 3 | ИТ |
1002 | Сидоров С.С. | 2 | 3 | ИТ |
1003 | Андреев А.А. | 3 | 3 | ИТ |
1004 | Николаев Н.Н. | 3 | 3 | ИТ |
1005 | Александров А.А. | NULL | 3 | ИТ |
1000 | Иванов И.И. | 1 | 4 | Маркетинг и реклама |
1001 | Петров П.П. | 3 | 4 | Маркетинг и реклама |
1002 | Сидоров С.С. | 2 | 4 | Маркетинг и реклама |
1003 | Андреев А.А. | 3 | 4 | Маркетинг и реклама |
1004 | Николаев Н.Н. | 3 | 4 | Маркетинг и реклама |
1005 | Александров А.А. | NULL | 4 | Маркетинг и реклама |
1000 | Иванов И.И. | 1 | 5 | Логистика |
1001 | Петров П.П. | 3 | 5 | Логистика |
1002 | Сидоров С.С. | 2 | 5 | Логистика |
1003 | Андреев А.А. | 3 | 5 | Логистика |
1004 | Николаев Н.Н. | 3 | 5 | Логистика |
1005 | Александров А.А. | NULL | 5 | Логистика |
Настало время вспомнить про псевдонимы таблиц
Пришло время вспомнить про псевдонимы таблиц, о которых я рассказывал в начале второй части.
В многотабличных запросах, псевдоним помогает нам явно указать из какой именно таблицы берется поле. Посмотрим на пример:
В нем поля с именами ID и Name есть в обоих таблицах и в Employees, и в Departments. И чтобы их различать, мы предваряем имя поля псевдонимом и точкой, т.е. «emp.ID», «emp.Name», «dep.ID», «dep.Name».
Вспоминаем почему удобнее пользоваться именно короткими псевдонимами – потому что, без псевдонимов наш запрос бы выглядел следующим образом:
По мне, стало очень длинно и хуже читаемо, т.к. имена полей визуально потерялись среди повторяющихся имен таблиц.
В многотабличных запросах, хоть и можно указать имя без псевдонима, в случае если имя не дублируется во второй таблице, но я бы рекомендовал всегда использовать псевдонимы в случае соединения, т.к. никто не гарантирует, что поле с таким же именем со временем не добавят во вторую таблицу, а тогда ваш запрос просто сломается, ругаясь на то что он не может понять к какой таблице относится данное поле.
Только используя псевдонимы, мы сможем осуществить соединения таблицы самой с собой. Предположим встала задача, получить для каждого сотрудника, данные сотрудника, который был принят прямо до него (табельный номер отличается на единицу меньше). Допустим, что у нас табельные номера выдаются последовательно и без дырок, тогда мы можем это сделать примерно следующим образом:
Т.е. здесь одной таблице Employees, мы дали псевдоним «e1», а второй «e2».
Разбираем каждый вид горизонтального соединения
Для этой цели рассмотрим 2 небольшие абстрактные таблицы, которые так и назовем LeftTable и RightTable:
Посмотрим, что в этих таблицах:
LCode | LDescr |
---|---|
1 | L-1 |
2 | L-2 |
3 | L-3 |
5 | L-5 |
RCode | RDescr |
---|---|
2 | B-2 |
3 | B-3 |
4 | B-4 |
LCode | LDescr | RCode | RDescr |
---|---|---|---|
2 | L-2 | 2 | B-2 |
3 | L-3 | 3 | B-3 |
Здесь были возвращены объединения строк для которых выполнилось условие (l.LCode=r.RCode)
LEFT JOIN
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | NULL | NULL |
2 | L-2 | 2 | B-2 |
3 | L-3 | 3 | B-3 |
5 | L-5 | NULL | NULL |
Здесь были возвращены все строки LeftTable, которые были дополнены данными строк из RightTable, для которых выполнилось условие (l.LCode=r.RCode)
RIGHT JOIN
LCode | LDescr | RCode | RDescr |
---|---|---|---|
2 | L-2 | 2 | B-2 |
3 | L-3 | 3 | B-3 |
NULL | NULL | 4 | B-4 |
Здесь были возвращены все строки RightTable, которые были дополнены данными строк из LeftTable, для которых выполнилось условие (l.LCode=r.RCode)
По сути если мы переставим LeftTable и RightTable местами, то аналогичный результат мы получим при помощи левого соединения:
LCode | LDescr | RCode | RDescr |
---|---|---|---|
2 | L-2 | 2 | B-2 |
3 | L-3 | 3 | B-3 |
NULL | NULL | 4 | B-4 |
Я за собой заметил, что я чаще применяю именно LEFT JOIN, т.е. я сначала думаю, данные какой таблицы мне важны, а потом думаю, какая таблица/таблицы будет играть роль дополняющей таблицы.
FULL JOIN – это по сути одновременный LEFT JOIN + RIGHT JOIN
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | NULL | NULL |
2 | L-2 | 2 | B-2 |
3 | L-3 | 3 | B-3 |
5 | L-5 | NULL | NULL |
NULL | NULL | 4 | B-4 |
Вернулись все строки из LeftTable и RightTable. Строки для которых выполнилось условие (l.LCode=r.RCode) были объединены в одну строку. Отсутствующие в строке данные с левой или правой стороны заполняются NULL-значениями.
CROSS JOIN
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | 2 | B-2 |
2 | L-2 | 2 | B-2 |
3 | L-3 | 2 | B-2 |
5 | L-5 | 2 | B-2 |
1 | L-1 | 3 | B-3 |
2 | L-2 | 3 | B-3 |
3 | L-3 | 3 | B-3 |
5 | L-5 | 3 | B-3 |
1 | L-1 | 4 | B-4 |
2 | L-2 | 4 | B-4 |
3 | L-3 | 4 | B-4 |
5 | L-5 | 4 | B-4 |
Каждая строка LeftTable соединяется с данными всех строк RightTable.
Возвращаемся к таблицам Employees и Departments
Надеюсь вы поняли принцип работы горизонтальных соединений. Если это так, то возвратитесь на начало раздела «JOIN-соединения – операции горизонтального соединения данных» и попробуйте самостоятельно понять примеры с объединением таблиц Employees и Departments, а потом снова возвращайтесь сюда, обсудим это вместе.
Давайте попробуем вместе подвести резюме для каждого запроса:
Запрос | Резюме |
---|---|
По сути данный запрос вернет только сотрудников, у которых указано значение DepartmentID. Т.е. мы можем использовать данное соединение, в случае, когда нам нужны данные по сотрудникам числящихся за каким-нибудь отделом (без учета внештаткиков). | |
Вернет всех сотрудников. Для тех сотрудников у которых не указан DepartmentID, поля «dep.ID» и «dep.Name» будут содержать NULL. Вспоминайте, что NULL значения в случае необходимости можно обработать, например, при помощи ISNULL(dep.Name,’вне штата’). Этот вид соединения можно использовать, когда нам важно получить данные по всем сотрудникам, например, чтобы получить список для начисления ЗП. | |
Здесь мы получили дырки слева, т.е. отдел есть, но сотрудников в этом отделе нет. Такое соединение можно использовать, например, когда нужно выяснить, какие отделы и кем у нас заняты, а какие еще не сформированы. Эту информацию можно использовать для поиска и приема новых работников из которых будет формироваться отдел. | |
Этот запрос важен, когда нам нужно получить все данные по сотрудникам и все данные по имеющимся отделам. Соответственно получаем дырки (NULL-значения) либо по сотрудникам, либо по отделам (внештатники). Данный запрос, например, может использоваться в целях проверки, все ли сотрудники сидят в правильных отделах, т.к. может у некоторых сотрудников, которые числятся как внештатники, просто забыли указать отдел. | |
В таком виде даже сложно придумать где это можно применить, поэтому пример с CROSS JOIN я покажу ниже. |
Обратите внимание, что в случае повторения значений DepartmentID в таблице Employees, произошло соединение каждой такой строки со строкой из таблицы Departments с таким же ID, то есть данные Departments объединились со всеми записями для которых выполнилось условие (emp.DepartmentID=dep.ID):
В нашем случае все получилось правильно, т.е. мы дополнили таблицу Employees, данными таблицы Departments. Я специально заострил на этом внимание, т.к. бывают случаи, когда такое поведение нам не нужно. Для демонстрации поставим задачу – для каждого отдела вывести последнего принятого сотрудника, если сотрудников нет, то просто вывести название отдела. Возможно напрашивается такое решение – просто взять предыдущий запрос и поменять условие соединение на RIGHT JOIN, плюс переставить поля местами:
ID | Name | ID | Name |
---|---|---|---|
1 | Администрация | 1000 | Иванов И.И. |
2 | Бухгалтерия | 1002 | Сидоров С.С. |
3 | ИТ | 1001 | Петров П.П. |
3 | ИТ | 1003 | Андреев А.А. |
3 | ИТ | 1004 | Николаев Н.Н. |
4 | Маркетинг и реклама | NULL | NULL |
5 | Логистика | NULL | NULL |
Но мы для ИТ-отдела получили три строчки, когда нам нужна была только строчка с последним принятым сотрудником, т.е. Николаевым Н.Н.
Задачу такого рода, можно решить, например, при помощи использования подзапроса:
ID | Name | ID | Name |
---|---|---|---|
1 | Администрация | 1000 | Иванов И.И. |
2 | Бухгалтерия | 1002 | Сидоров С.С. |
3 | ИТ | 1004 | Николаев Н.Н. |
4 | Маркетинг и реклама | NULL | NULL |
5 | Логистика | NULL | NULL |
При помощи предварительного объединения Employees с данными подзапроса, мы смогли оставить только нужных нам для соединения с Departments сотрудников.
Здесь мы плавно переходим к использованию подзапросов. Я думаю использование их в таком виде должно быть для вас понятно на интуитивном уровне. То есть подзапрос подставляется на место таблицы и играет ее роль, ничего сложного. К теме подзапросов мы еще вернемся отдельно.
Посмотрите отдельно, что возвращает подзапрос:
MaxEmployeeID |
---|
1005 |
1000 |
1002 |
1004 |
Т.е. он вернул только идентификаторы последних принятых сотрудников, в разрезе отделов.
Соединения выполняются последовательно сверху-вниз, наращиваясь как снежный ком, который катится с горы. Сначала происходит соединение «Employees emp JOIN (Подзапрос) lastEmp», формируя новый выходной набор:
Потом идет объединение набора, полученного «Employees emp JOIN (Подзапрос) lastEmp» (назовем его условно «ПоследнийРезультат») с Departments, т.е. «ПоследнийРезультат RIGHT JOIN Departments dep»:
Самостоятельная работа для закрепления материала
Если вы новичок, то вам обязательно нужно прорабатывать каждую JOIN-конструкцию, до тех пор, пока вы на 100% не будете понимать, как работает каждый вид соединения и правильно представлять результат какого вида будет получен в итоге.
Для закрепления материала про JOIN-соединения сделаем следующее:
Посмотрим, что в таблицах:
LCode | LDescr |
---|---|
1 | L-1 |
2 | L-2a |
2 | L-2b |
3 | L-3 |
5 | L-5 |
RCode | RDescr |
---|---|
2 | B-2a |
2 | B-2b |
3 | B-3 |
4 | B-4 |
А теперь попытайтесь сами разобрать, каким образом получилась каждая строчка запроса с каждым видом соединения (Excel вам в помощь):
LCode | LDescr | RCode | RDescr |
---|---|---|---|
2 | L-2a | 2 | B-2a |
2 | L-2a | 2 | B-2b |
2 | L-2b | 2 | B-2a |
2 | L-2b | 2 | B-2b |
3 | L-3 | 3 | B-3 |
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | NULL | NULL |
2 | L-2a | 2 | B-2a |
2 | L-2a | 2 | B-2b |
2 | L-2b | 2 | B-2a |
2 | L-2b | 2 | B-2b |
3 | L-3 | 3 | B-3 |
5 | L-5 | NULL | NULL |
LCode | LDescr | RCode | RDescr |
---|---|---|---|
2 | L-2a | 2 | B-2a |
2 | L-2b | 2 | B-2a |
2 | L-2a | 2 | B-2b |
2 | L-2b | 2 | B-2b |
3 | L-3 | 3 | B-3 |
NULL | NULL | 4 | B-4 |
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | NULL | NULL |
2 | L-2a | 2 | B-2a |
2 | L-2a | 2 | B-2b |
2 | L-2b | 2 | B-2a |
2 | L-2b | 2 | B-2b |
3 | L-3 | 3 | B-3 |
5 | L-5 | NULL | NULL |
NULL | NULL | 4 | B-4 |
LCode | LDescr | RCode | RDescr |
---|---|---|---|
1 | L-1 | 2 | B-2a |
2 | L-2a | 2 | B-2a |
2 | L-2b | 2 | B-2a |
3 | L-3 | 2 | B-2a |
5 | L-5 | 2 | B-2a |
1 | L-1 | 2 | B-2b |
2 | L-2a | 2 | B-2b |
2 | L-2b | 2 | B-2b |
3 | L-3 | 2 | B-2b |
5 | L-5 | 2 | B-2b |
1 | L-1 | 3 | B-3 |
2 | L-2a | 3 | B-3 |
2 | L-2b | 3 | B-3 |
3 | L-3 | 3 | B-3 |
5 | L-5 | 3 | B-3 |
1 | L-1 | 4 | B-4 |
2 | L-2a | 4 | B-4 |
2 | L-2b | 4 | B-4 |
3 | L-3 | 4 | B-4 |
5 | L-5 | 4 | B-4 |
Еще раз про JOIN-соединения
Еще один пример с использованием нескольких последовательных операций соединении. Здесь повтор получился не специально, так получилось – не выбрасывать же материал. 😉 Но ничего «повторение – мать учения».
Если используется несколько операций соединения, то в таком случае они применяются последовательно сверху-вниз. Грубо говоря, после каждого соединения создается новый набор и следующее соединение уже происходит с этим расширенным набором. Рассмотрим простой пример:
Первым делом выбрались все записи таблицы Employees:
Дальше произошло соединение с таблицей Departments:
Дальше уже идет соединение этого набора с таблицей Positions:
Т.е. это выглядит примерно так:
И в последнюю очередь идет возврат тех данных, которые мы просим вывести:
Соответственно, ко всему этому полученному набору можно применить фильтр WHERE и сортировку ORDER BY:
ID | EmployeeName | PositionName | DepartmentName |
---|---|---|---|
1004 | Николаев Н.Н. | Программист | ИТ |
1001 | Петров П.П. | Программист | ИТ |
То есть последний полученный набор – представляет собой такую же таблицу, над которой можно выполнять базовый запрос:
То есть если раньше в роли источника выступала только одна таблица, то теперь на это место мы просто подставляем наше выражение:
В результате чего получаем тот же самый базовый запрос:
А теперь, применим группировку:
Видите, мы все так же крутимся вокруг да около базовых конструкций, теперь надеюсь понятно, почему очень важно в первую очередь хорошо понять их.
И как мы увидели, в запросе на месте любой таблицы может стоять подзапрос. В свою очередь подзапросы могут быть вложены в подзапросы. И все эти подзапросы тоже представляют из себя базовые конструкции. То есть базовая конструкция, это кирпичики, из которых строится любой запрос.
Обещанный пример с CROSS JOIN
Давайте используем соединение CROSS JOIN, чтобы подсчитать сколько сотрудников, в каком отделе и на каких должностях числится. Для каждого отдела перечислим все существующие должности:
В данном случае сначала выполнилось соединение при помощи CROSS JOIN, а затем к полученному набору сделалось соединение с данными из подзапроса при помощи LEFT JOIN. Вместо таблицы в LEFT JOIN мы использовали подзапрос.
Подзапрос заключается в скобки и ему присваивается псевдоним, в данном случае это «e». То есть в данном случае объединение происходит не с таблицей, а с результатом следующего запроса:
DepartmentID | PositionID | EmplCount |
---|---|---|
NULL | NULL | 1 |
2 | 1 | 1 |
1 | 2 | 1 |
3 | 3 | 2 |
3 | 4 | 1 |
Вместе с псевдонимом «e» мы можем использовать имена DepartmentID, PositionID и EmplCount. По сути дальше подзапрос ведет себя так же, как если на его месте стояла таблица. Соответственно, как и у таблицы,
все имена колонок, которые возвращает подзапрос, должны быть заданы явно и не должны повторяться.
Связь при помощи WHERE-условия
Для примера перепишем следующий запрос с JOIN-соединением:
Через WHERE-условие он примет следующую форму:
Здесь плохо то, что происходит смешивание условий соединения таблиц (emp.DepartmentID=dep.ID) с условием фильтрации (emp.DepartmentID=3).
Теперь посмотрим, как сделать CROSS JOIN:
Через WHERE-условие он примет следующую форму:
Т.е. в этом случае мы просто не указали условие соединения таблиц Employees и Departments. Чем плох этот запрос? Представьте, что кто-то другой смотрит на ваш запрос и думает «кажется тот, кто писал запрос забыл здесь дописать условие (emp.DepartmentID=dep.ID)» и с радостью, что обнаружил косяк, дописывает это условие. В результате чего задуманное вами может сломаться, т.к. вы подразумевали CROSS JOIN. Так что, если вы делаете декартово соединение, то лучше явно укажите, что это именно оно, используя конструкцию CROSS JOIN.
Для оптимизатора запроса может быть и без разницы как вы реализуете соединение (при помощи WHERE или JOIN), он их может выполнить абсолютно одинаково. Но из соображения понимаемости кода, я бы рекомендовал в современных СУБД стараться никогда не делать соединение таблиц при помощи WHERE-условия. Использовать WHERE-условия для соединения, в том случае, если в СУБД реализованы конструкции JOIN, я бы назвал сейчас моветоном. WHERE-условия служат для фильтрации набора, и не нужно перемешивать условия служащие для соединения, с условиями отвечающими за фильтрацию. Но если вы пришли к выводу, что без реализации соединения через WHERE не обойтись, то конечно приоритет за решеной задачей и «к черту все устои».
UNION-объединения – операции вертикального объединения результатов запросов
Я специально использую словосочетания горизонтальное соединение и вертикальное объединение, т.к. заметил, что новички часто недопонимают и путают суть этих операций.
Давайте первым делом вспомним как мы делали первую версию отчета для директора:
Так вот, если бы мы не знали, что существует операция группировки, но знали бы, что существует операция объединения результатов запроса при помощи UNION ALL, то мы могли бы склеить все эти запросы следующим образом:
Т.е. UNION ALL позволяет склеить результаты, полученные разными запросами в один общий результат.
Соответственно количество колонок в каждом запросе должно быть одинаковым, а также должны быть совместимыми и типы этих колонок, т.е. строка под строкой, число под числом, дата под датой и т.п.
Немного теории
В MS SQL реализованы следующие виды вертикального объединения:
Операция | Описание |
---|---|
UNION ALL | В результат включаются все строки из обоих наборов. (A+B) |
UNION | В результат включаются только уникальные строки двух наборов. DISTINCT(A+B) |
EXCEPT | В результат попадают уникальные строки верхнего набора, которые отсутствуют в нижнем наборе. Разница 2-х множеств. DISTINCT(A-B) |
INTERSECT | В результат включаются только уникальные строки, присутствующие в обоих наборах. Пересечение 2-х множеств. DISTINCT(A&B) |
Все это проще понять на наглядном примере.
Создадим 2 таблицы и наполним их данными:
Посмотрим на содержимое:
T1 | T2 |
---|---|
1 | Text 1 |
1 | Text 1 |
2 | Text 2 |
3 | Text 3 |
4 | Text 4 |
5 | Text 5 |
B1 | B2 |
---|---|
2 | Text 2 |
3 | Text 3 |
6 | Text 6 |
6 | Text 6 |
UNION ALL
UNION
По сути UNION можно представить, как UNION ALL, к которому применена операция DISTINCT: