Как соединить таблицы в sql ключами
Перейти к содержимому

Как соединить таблицы в sql ключами

  • автор:

SQL JOIN — соединение таблиц базы данных

Оператор языка SQL JOIN предназначен для соединения двух или более таблиц базы данных по совпадающему условию. Этот оператор существует только в реляционных базах данных. Именно благодаря JOIN реляционные базы данных обладают такой мощной функциональностью, которая позволяет вести не только хранение данных, но и их, хотя бы простейший, анализ с помощью запросов. Разберём основные нюансы написания SQL-запросов с оператором JOIN, которые являются общими для всех СУБД (систем управления базами данных). Для соединения двух таблиц оператор SQL JOIN имеет следующий синтаксис:

После одного или нескольких звеньев с оператором JOIN может следовать необязательная секция WHERE или HAVING, в которой, также, как в простом SELECT-запросе, задаётся условие выборки. Общим для всех СУБД является то, что в этой конструкции вместо JOIN может быть указано INNER JOIN, LEFT OUTER JOIN, RIGHT OUTER JOIN, FULL OUTER JOIN, CROSS JOIN (или, как вариант, запятая).

INNER JOIN (внутреннее соединение)

Запрос с оператором INNER JOIN предназначен для соединения таблиц и вывода результирующей таблицы, в которой данные полностью пересекаются по условию, указанному после ON.

То же самое делает и просто JOIN. Таким образом, слово INNER — не обязательное.

Есть база данных портала объявлений — 2. В ней есть таблица Categories (категории объявлений) и Parts (части, или иначе — рубрики, которые и относятся к категориям). Например, части Квартиры, Дачи относятся к категории Недвижимость, а части Автомобили, Мотоциклы — к категории Транспорт.

Если вы хотите выполнить запросы к базе данных из этого урока на MS SQL Server, но эта СУБД не установлена на вашем компьютере, то ее можно установить, пользуясь инструкцией по этой ссылке .

Скрипт для создания базы данных портала объявлений — 2, её таблиц и заполения таблиц данными — в файле по этой ссылке .

Таблицы этой базы данных с заполненными данными имеют следующий вид.

Catnumb Cat_name Price
10 Стройматериалы 105,00
505 Недвижимость 210,00
205 Транспорт 160,00
30 Мебель 77,00
45 Техника 65,00
Part_ID Part Cat
1 Квартиры 505
2 Автомашины 205
3 Доски 10
4 Шкафы 30
5 Книги 160

Заметим, что в таблице Parts Книги имеют Cat — ссылку на категорию, которой нет в таблице Categories, а в таблице Categories Техника имеет номер категории Catnumb — значение, ссылки на которое нет в таблице Parts.

Пример 1. Требуется соединить данные этих двух таблиц так, чтобы в результирующей таблице были поля Part (Часть), Cat (Категория) и Price (Цена подачи объявления) и чтобы данные полностью пересекались по условию. Условие — совпадение номера категории (Catnumb) в таблице Categories и ссылки на категорию в таблице Parts. Для этого пишем следующий запрос:

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

Part Cat Price
Квартиры 505 210,00
Автомашины 205 160,00
Доски 10 105,00
Шкафы 30 77,00

В результирующей таблице нет Книг, так как эта запись ссылается на категорию, которой нет в таблице Categories, и Техники, так как эта запись имеет внешний ключ в таблице Categories, на который нет ссылки в таблице Parts.

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

Написать запросы SQL с JOIN самостоятельно, а затем посмотреть решения

Есть база данных «Театр». Таблица Play содержит данные о постановках. Таблица Team — о ролях актёров. Таблица Actor — об актёрах. Таблица Director — о режиссёрах. Поля таблиц, первичные и внешние ключи можно увидеть на рисунке ниже (для увеличения нажать левой кнопкой мыши).

Пример 2. Определить самого востребованного актёра за последние 5 лет.

Оператор JOIN использовать 2 раза. Использовать COUNT(), CURDATE(), LIMIT 1.

Пример 3. Вывести список актеров, которые в одном спектакле играют более одной роли, и количество их ролей.

Оператор JOIN использовать 1 раз. Использовать HAVING, GROUP BY.

Подсказка. Оператор HAVING применяется к числу ролей, подсчитанных агрегатной функцией COUNT.

Как соединить таблицы в sql ключами

JOIN is a method of combining (joining) information from two tables. The result is a stitched set of columns from both tables, defined by the join type (INNER/OUTER/CROSS and LEFT/RIGHT/FULL, explained below) and join criteria (how rows from both tables relate).

A table may be joined to itself or to any other table. If information from more than two tables needs to be accessed, multiple joins can be specified in a FROM clause.

# Self Join

A table may be joined to itself, with different rows matching each other by some condition. In this use case, aliases must be used in order to distinguish the two occurrences of the table.

In the below example, for each Employee in the example database Employees table

(opens new window) , a record is returned containing the employee’s first name together with the corresponding first name of the employee’s manager. Since managers are also employees, the table is joined with itself:

This query will return the following data:

Employee Manager
John James
Michael James
Johnathon John

# So how does this work?

The original table contains these records:

Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate
1 James Smith 1234567890 NULL 1 1000 01-01-2002
2 John Johnson 2468101214 1 1 400 23-03-2005
3 Michael Williams 1357911131 1 2 600 12-05-2009
4 Johnathon Smith 1212121212 2 1 500 24-07-2016

The first action is to create a Cartesian product of all records in the tables used in the FROM clause. In this case it’s the Employees table twice, so the intermediate table will look like this (I’ve removed any fields not used in this example):

e.Id e.FName e.ManagerId m.Id m.FName m.ManagerId
1 James NULL 1 James NULL
1 James NULL 2 John 1
1 James NULL 3 Michael 1
1 James NULL 4 Johnathon 2
2 John 1 1 James NULL
2 John 1 2 John 1
2 John 1 3 Michael 1
2 John 1 4 Johnathon 2
3 Michael 1 1 James NULL
3 Michael 1 2 John 1
3 Michael 1 3 Michael 1
3 Michael 1 4 Johnathon 2
4 Johnathon 2 1 James NULL
4 Johnathon 2 2 John 1
4 Johnathon 2 3 Michael 1
4 Johnathon 2 4 Johnathon 2

The next action is to only keep the records that meet the JOIN criteria, so any records where the aliased e table ManagerId equals the aliased m table Id :

e.Id e.FName e.ManagerId m.Id m.FName m.ManagerId
2 John 1 1 James NULL
3 Michael 1 1 James NULL
4 Johnathon 2 2 John 1

Then, each expression used within the SELECT clause is evaluated to return this table:

e.FName m.FName
John James
Michael James
Johnathon John

Finally, column names e.FName and m.FName are replaced by their alias column names, assigned with the AS

Employee Manager
John James
Michael James
Johnathon John

# Differences between inner/outer joins

SQL has various join types to specify whether (non-)matching rows are included in the result: INNER JOIN , LEFT OUTER JOIN , RIGHT OUTER JOIN , and FULL OUTER JOIN (the INNER and OUTER keywords are optional). The figure below underlines the differences between these types of joins: the blue area represents the results returned by the join, and the white area represents the results that the join will not return.

Venn diagrams representing SQL inner/outer joins

Cross Join SQL Pictorial Presentation (reference

enter image description here

Below are examples from this

For instance there are two tables as below :

Note that (1,2) are unique to A, (3,4) are common, and (5,6) are unique to B.

# Inner Join

An inner join using either of the equivalent queries gives the intersection of the two tables, i.e. the two rows they have in common:

# Left outer join

A left outer join will give all rows in A, plus any common rows in B:

# Right outer join

Similarly, a right outer join will give all rows in B, plus any common rows in A:

# Full outer join

A full outer join will give you the union of A and B, i.e., all the rows in A and all the rows in B. If something in A doesn’t have a corresponding datum in B, then the B portion is null, and vice versa.

# JOIN Terminology: Inner, Outer, Semi, Anti.

Let’s say we have two tables (A and B) and some of their rows match (relative to the given JOIN condition, whatever it may be in the particular case):

Join Terminology Overview

We can use various join types to include or exclude matching or non-matching rows from either side, and correctly name the join by picking the corresponding terms from the diagram above.

The examples below use the following test data:

# Inner Join

Combines left and right rows that match.

Inner Join

# Left Outer Join

Sometimes abbreviated to "left join". Combines left and right rows that match, and includes non-matching left rows.

Left Outer Join

# Right Outer Join

Sometimes abbreviated to "right join". Combines left and right rows that match, and includes non-matching right rows.

Right Outer Join

# Full Outer Join

Sometimes abbreviated to "full join". Union of left and right outer join.

Full Outer Join

# Left Semi Join

Includes left rows that match right rows.

Left Semi Join

# Right Semi Join

Includes right rows that match left rows.

Right Semi Join

As you can see, there is no dedicated IN syntax for left vs. right semi join — we achieve the effect simply by switching the table positions within SQL text.

# Left Anti Semi Join

Includes left rows that do not match right rows.

Left Anti Semi Join

WARNING: Be careful if you happen to be using NOT IN on a NULL-able column! More details here

# Right Anti Semi Join

Includes right rows that do not match left rows.

Right Anti Semi Join

As you can see, there is no dedicated NOT IN syntax for left vs. right anti semi join — we achieve the effect simply by switching the table positions within SQL text.

# Cross Join

A Cartesian product of all left with all right rows.

Cross join is equivalent to an inner join with join condition which always matches, so the following query would have returned the same result:

# Self-Join

This simply denotes a table joining with itself. A self-join can be any of the join types discussed above. For example, this is a an inner self-join:

# Left Outer Join

A Left Outer Join (also known as a Left Join or Outer Join) is a Join that ensures all rows from the left table are represented; if no matching row from the right table exists, its corresponding fields are NULL .

The following example will select all departments and the first name of employees that work in that department. Departments with no employees are still returned in the results, but will have NULL for the employee name:

This would return the following from the example database

Departments.Name Employees.FName
HR James
HR John
HR Johnathon
Sales Michael
Tech NULL

# So how does this work?

There are two tables in the FROM clause:

Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate
1 James Smith 1234567890 NULL 1 1000 01-01-2002
2 John Johnson 2468101214 1 1 400 23-03-2005
3 Michael Williams 1357911131 1 2 600 12-05-2009
4 Johnathon Smith 1212121212 2 1 500 24-07-2016
Id Name
1 HR
2 Sales
3 Tech

First a Cartesian product is created from the two tables giving an intermediate table.
The records that meet the join criteria (Departments.Id = Employees.DepartmentId) are highlighted in bold; these are passed to the next stage of the query.

As this is a LEFT OUTER JOIN all records are returned from the LEFT side of the join (Departments), while any records on the RIGHT side are given a NULL marker if they do not match the join criteria. In the table below this will return Tech with NULL

Id Name Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate
1 HR 1 James Smith 1234567890 NULL 1 1000 01-01-2002
1 HR 2 John Johnson 2468101214 1 1 400 23-03-2005
1 HR 3 Michael Williams 1357911131 1 2 600 12-05-2009
1 HR 4 Johnathon Smith 1212121212 2 1 500 24-07-2016
2 Sales 1 James Smith 1234567890 NULL 1 1000 01-01-2002
2 Sales 2 John Johnson 2468101214 1 1 400 23-03-2005
2 Sales 3 Michael Williams 1357911131 1 2 600 12-05-2009
2 Sales 4 Johnathon Smith 1212121212 2 1 500 24-07-2016
3 Tech 1 James Smith 1234567890 NULL 1 1000 01-01-2002
3 Tech 2 John Johnson 2468101214 1 1 400 23-03-2005
3 Tech 3 Michael Williams 1357911131 1 2 600 12-05-2009
3 Tech 4 Johnathon Smith 1212121212 2 1 500 24-07-2016

Finally each expression used within the SELECT clause is evaluated to return our final table:

Departments.Name Employees.FName
HR James
HR John
Sales Richard
Tech NULL

# Implicit Join

Joins can also be performed by having several tables in the from clause, separated with commas , and defining the relationship between them in the where clause. This technique is called an Implicit Join (since it doesn’t actually contain a join clause).

All RDBMSs support it, but the syntax is usually advised against. The reasons why it is a bad idea to use this syntax are:

  • It is possible to get accidental cross joins which then return incorrect results, especially if you have a lot of joins in the query.
  • If you intended a cross join, then it is not clear from the syntax (write out CROSS JOIN instead), and someone is likely to change it during maintenance.

The following example will select employee’s first names and the name of the departments they work for:

This would return the following from the example database

e.FName d.Name
James HR
John HR
Richard Sales

# Basic explicit inner join

A basic join (also called "inner join") queries data from two tables, with their relationship defined in a join clause.

The following example will select employees’ first names (FName) from the Employees table and the name of the department they work for (Name) from the Departments table:

This would return the following from the example database

Employees.FName Departments.Name
James HR
John HR
Richard Sales

# CROSS JOIN

Cross join does a Cartesian product of the two members, A Cartesian product means each row of one table is combined with each row of the second table in the join. For example, if TABLEA has 20 rows and TABLEB has 20 rows, the result would be 20*20 = 400 output rows.

d.Name e.FName
HR James
HR John
HR Michael
HR Johnathon
Sales James
Sales John
Sales Michael
Sales Johnathon
Tech James
Tech John
Tech Michael
Tech Johnathon

It is recommended to write an explicit CROSS JOIN if you want to do a cartesian join, to highlight that this is what you want.

# Joining on a Subquery

Joining a subquery is often used when you want to get aggregate data from a child/details table and display that along with records from the parent/header table. For example, you might want to get a count of child records, an average of some numeric column in child records, or the top or bottom row based on a date or numeric field. This example uses aliases, which arguable makes queries easier to read when you have multiple tables involved. Here’s what a fairly typical subquery join looks like. In this case we are retrieving all rows from the parent table Purchase Orders and retrieving only the first row for each parent record of the child table PurchaseOrderLineItems.

# CROSS APPLY & LATERAL JOIN

A very interesting type of JOIN is the LATERAL JOIN (new in PostgreSQL 9.3+),
which is also known as CROSS APPLY/OUTER APPLY in SQL-Server & Oracle.

The basic idea is that a table-valued function (or inline subquery) gets applied for every row you join.

This makes it possible to, for example, only join the first matching entry in another table.
The difference between a normal and a lateral join lies in the fact that you can use a column that you previously joined in the subquery that you "CROSS APPLY".

left | right | inner JOIN LATERAL

CROSS | OUTER APPLY

INNER JOIN LATERAL is the same as CROSS APPLY
and LEFT JOIN LATERAL is the same as OUTER APPLY

Example usage (PostgreSQL 9.3+):

And for SQL-Server

# FULL JOIN

One type of JOIN that is less known, is the FULL JOIN.
(Note: FULL JOIN is not supported by MySQL as per 2016)

A FULL OUTER JOIN returns all rows from the left table, and all rows from the right table.

If there are rows in the left table that do not have matches in the right table, or if there are rows in right table that do not have matches in the left table, then those rows will be listed, too.

Note that if you’re using soft-deletes, you’ll have to check the soft-delete status again in the WHERE-clause (because FULL JOIN behaves kind-of like a UNION);
It’s easy to overlook this little fact, since you put AP_SoftDeleteStatus = 1 in the join clause.

Also, if you are doing a FULL JOIN, you’ll usually have to allow NULL in the WHERE-clause; forgetting to allow NULL on a value will have the same effects as an INNER join, which is something you don’t want if you’re doing a FULL JOIN.

# Recursive JOINs

Recursive joins are often used to obtain parent-child data. In SQL, they are implemented with recursive common table expressions

# Syntax
  • [ < INNER | < < LEFT | RIGHT | FULL >[ OUTER ] > > ] JOIN
# Remarks

Joins, as their name suggests, are a way of querying data from several tables in a joint fashion, with the rows displaying columns taken from more than one table.

SQL Snippet: Foreign Keys

Managing relationships between database attributes.

Udara Bibile

If you have worked with SQL based database, I guess y’all are familiar with usage of foreign keys to manage relationship between tables.

Relationship Types

Lets dive into a famous example involving book — author connection to gain insight. There are variety of connection between these entities, in a graphical:

  • one to one →an author can write only one book, and a book only have one author.
  • one to many →an author can write multiple books, but book can only have one author.
  • many to many →an author can write multiple books, and also a book can be written by multiple authors.

Lets look into above scenarios for ease of this tutorial, even though some scenarios doesn't seem accurate in real world.

SQL snippets found in this article is written to support SQLite and can be tested in https://sqliteonline.com/.

SQLite Browser — SQL Online

SQLite Browser — SQL Online on SQLite, MySql. User-friendly interface. No registration for start, No DownLoad, No…

One-to-One relationship

Each of author and book table will have primary keys to identify each value uniquely. Author table will have Pk_author_id, and Book table will have Pk_book_Id as unique identifier for each table. To form relationship between two tables, foreign key references should be added. Here each entry in Book table, it will refer author_Id to keep track of connected entry identifier in Author table.

Lets dig into SQL code as to how it can be done:

Here when setting up Book table, a connection to author is attached via an id. Lets add some values to test one-to-one relationship.

Thereby after inserting these values, formed relationship can be queried:

However since this is one-to-one relationship implementation, there should be restriction of adding another book for this author.

Due to UNIQUE keyword introduced to Fk_author_Id attribute in Book table, it would restrict adding any record by used author_Id. Thereby making it one-to-one relationship.

One-to-Many relationship

Building upon previous example, this scenario should allow an author to write multiple books. By looking into how one-to-one relationship was implemented using UNIQUE keyword, it can be seen that removing UNIQUE keyword would make it one-to-many relationship. Book table should have been created as such.

And add values as such without facing an issue like previously.

Lets query and see that one-to-many relationship exists. Here an author have been attached to multiple books.

Many-to-Many relationship

Simply in this occasion an author can be attached to multiple books, and a book can be attached to multiple authors. This is more closer to real world.

In document based databases such as MongoDB or such this can be simple due to usage of array. Simple example can be as follows:

With just using flexibility of NoSQL and arrays many-to-many relationship is simply made. However due to unavailability of array in SQL, different approach is taken to handle such relationship.

In order to create many-to-many relationship, a join table is created to record unique identifiers of connected values. As per example in order to join Book and Author tables, another table called Author_Book is created.

Lets create two simple tables without any relationship for Book and Author.

Then create joining table, adding parent tables’ primary keys as foreign keys in join table. Additional property related to relationship can also be stored here such as Extra_value.

Each entry in this join table, will include primary keys of parent tables. As an example, Fk_author_id is unique identifier in Author table, and Fk_book_id is unique identifier in Book table. Thereby (Fk_author_id, Fk_book_id) tuple or composite key will be unique itself, and thereby separate unique identifier or primary key for Author_Book is not essential. However integer auto incrementing value can be added as primary key, but it’ll be redundant.

Here this table just contains each combination of given many-to-many relationship. Lets enter some data and query.

Lets add many-to-many relationship between them:

Lets query the database by join all three tables and extract required values:

Foreign Key Constraints

Here connections are made between tables, and now records are connected. Now lets try to remove these connection:

Here Author value is deleted without any issue, but these raises concern about relationships made. Thereby inconsistent data is still available, such as this author already referred in Author_Book.

SQL allows functionality to avoid such inconsistent data, by disabling or handling such deletion. Foreign key constraints are there to handle it, and lets create new tables to test these.

RESTRICT

Restricting any deletion of references can be an approach foreign keys constraints can be added.

Enable foreign key constraints and try to delete connected record.

As seen when foreign keys constraints are enforced, any deletion or updates are not allowed. That behavior is called RESTRICT, added by default.

ON DELETE CASCADE

However when creating the table, required behavior for foreign key constraints can be decided. Such action is ON DELETE CASCADE, which indicates that action (DELETE) should be propagated by related foreign keys from parent table (Author) to child table (Book). Simply if parent value is deleted, all other records keeping reference to that parent value is to be deleted.

ON DELETE SET NULL

If values on child tables, are not to be deleted but rather be altered according to deleted values in parent table this approach can be used. Here references in child table to parent table will be set to null, and row wouldn't be deleted.

Moreover, it should be noted that restriction happened only when deleting from Author table, and not from Book table. This is the expected behavior where foreign key relationship is only one way, from parent table (Author) towards child table (Book). Here Book can’t be created unless Author is explicitly connected, and at deletion Book wouldn't be referred in any other tables. However when trying to delete Author, it could be referred elsewhere in Book table, so constraints applied.

From this SQL Snippet, I think y’all are able to insight into managing data relationship in SQL database tables. Note that SQLite code segments are used here, and other implementation might vary little.

Учебник по языку SQL (DDL, DML) на примере диалекта MS SQL Server. Часть четвертая

Для демонстрационных целей добавим несколько отделов и должностей:

JOIN-соединения – операции горизонтального соединения данных

Здесь нам очень пригодится знание структуры БД, т.е. какие в ней есть таблицы, какие данные хранятся в этих таблицах и по каким полям таблицы связаны между собой. Первым делом всегда досконально изучайте структуру БД, т.к. нормальный запрос можно написать только тогда, когда ты знаешь, что откуда берется. У нас структура состоит из 3-х таблиц Employees, Departments и Positions. Приведу здесь диаграмму из первой части:

Если суть РДБ – разделяй и властвуй, то суть операций объединений снова склеить разбитые по таблицам данные, т.е. привести их обратно в человеческий вид.

Если говорить просто, то операции горизонтального соединения таблицы с другими таблицами используются для того, чтобы получить из них недостающие данные. Вспомните пример с еженедельным отчетом для директора, когда при запросе из таблицы Employees, нам для получения окончательного результата недоставало поля «Название отдела», которое находится в таблице Departments.

  1. JOIN – левая_таблица JOIN правая_таблица ON условия_соединения
  2. LEFT JOIN – левая_таблица LEFT JOIN правая_таблица ON условия_соединения
  3. RIGHT JOIN – левая_таблица RIGHT JOIN правая_таблица ON условия_соединения
  4. FULL JOIN – левая_таблица FULL JOIN правая_таблица ON условия_соединения
  5. CROSS JOIN – левая_таблица CROSS JOIN правая_таблица
  1. Это короче и не засоряет запрос лишними словами;
  2. По словам LEFT, RIGHT, FULL и CROSS и так понятно о каком соединении идет речь, так же и в случае просто JOIN;
  3. Считаю слова INNER и OUTER в данном случае ненужными рудиментами, которые больше путают начинающих.

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

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:

EXCEPT

INTERSECT

Завершаем разговор о UNION-соединениях

Вот в принципе и все, что касается вертикальных объединений, это намного проще, чем JOIN-соединения.

Чаще всего в моей в практике находит применение UNION ALL, но и другие виды вертикальных объединений находят свое применение.

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

Например, если мы напишем просто:

То мы получим:

x y
1 Text 1
2 Text 2
3 Text 3
4 Text 4
5 Text 5

Т.е. получается сначала выполнился INTERSECT, а после EXCEPT. Хотя логически будто должно было быть наоборот, т.е. идти сверху-вниз.

Я редко использую эти операции объединений, а тем более в таком виде, поэтому, чтобы не думать не гадать, в какой очередности он выполняет объединения, можно просто при помощи скобок явно указать последовательность объединений, давайте скажем, что сначала нужно сделать EXCEPT, а потом INTERSECT:

x y
1 Text 1
4 Text 4

Вот теперь я получил то, что и хотел.

Я не знаю работает ли такой синтаксис в других СУБД, но если что используйте подзапрос:

При использовании ORDER BY сортировка применяется к окончательному набору:

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

Самое главное про UNION-объединения я вроде написал, если что поиграйте с UNION-объединениями самостоятельно.

Использование подзапросов

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

Косвенно мы уже использовали подзапросы в блоке FROM. Там результат, возвращаемый подзапросом по сути играет роль новой таблицы. Думаю, большого смысла останавливаться здесь нет смысла. Просто рассмотрим абстрактный пример с объединением 2-х подзапросов:

Если не понятно, сразу, то разбирайте такие запросы по частям. Т.е. сначала посмотрите, что возвращает первый подзапрос «q1», потом, что возвращает второй подзапрос «q2», а затем выполните операцию JOIN над результатами подзапросов «q1» и «q2».

Конструкция WITH

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

То же самое написанное при помощи WITH:

Как видим большие подзапросы вынесены и поименованы в блоке WITH, что дало возможность разгрузить текст основного запроса и сделать его понятным.

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

И запрос, который использовал данное представление:

По сути WITH дает нам возможность разместить текст из представления непосредственно в запросе, т.е. смысл один и тот же:

Только в случае созданного представления мы можем использовать его из разных запросов, т.к. представление создается на уровне БД. Тогда как подзапрос оформленный в блоке WITH виден только в рамках этого запроса.

Использование WITH по-другому называет CTE-выражениями:
Общие табличные выражения (CTE — Common Table Expressions) позволяют существенно уменьшить объем кода, если многократно приходится обращаться к одним и тем же запросам. CTE играет роль представления, которое создается в рамках одного запроса и, не сохраняется как объект схемы.

У CTE есть еще одно важное назначение, с его помощью можно написать рекурсивный запрос.

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

ID Name EmpLevel
1000 Иванов И.И. 1
1002 _____Сидоров С.С. 2
1003 _____Андреев А.А. 2
1005 _____Александров А.А. 2
1001 __________Петров П.П. 3
1004 __________Николаев Н.Н. 3

Для наглядности пробелы заменены знаками подчеркивания.

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

Продолжаем разговор про подзапросы

Давайте теперь рассмотрим, как можно использовать подзапросы еще, а также передавать в них параметры при помощи псевдонима из основного запроса.

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

Подзапрос можно использовать в блоке SELECT

Вернемся к нашему отчету:

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

В данном случае подзапрос (SELECT Name FROM Departments dep WHERE dep.ID=emp.DepartmentID) выполнится 4 раза, т.е. для каждого значения emp.DepartmentID

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

Не хорошо правда ведь? Т.к. каждый из трех подзапросов выполнится по 4 раза (для каждой возвращенной строки), итого выполнится 12 подзапросов.

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

Подзапросы с конструкцией APPLY

В MS SQL для последнего примера:

можно применить конструкцию APPLY, которая имеет 2 формы – CROSS APPLY и OUTER APPLY.

Конструкция APPLY позволяет избавиться от множества подзапросов, как в данном примере, когда требуется получить и ID и Name последнего принятого сотрудника для каждого отдела:

ID Name LastEmpID LastEmpName
1 Администрация 1000 Иванов И.И.
2 Бухгалтерия 1002 Сидоров С.С.
3 ИТ 1004 Николаев Н.Н.

Здесь подзапрос блока CROSS APPLY выполнится для каждого значения строки из таблицы Departments. Если подзапрос строки не вернет, то данный отдел исключается из результирующего списка.

Если требуется, чтобы были возвращены все строки таблицы Departments, то используйте следующую форму этого оператора OUTER APPLY:

ID Name LastEmpID LastEmpName
1 Администрация 1000 Иванов И.И.
2 Бухгалтерия 1002 Сидоров С.С.
3 ИТ 1004 Николаев Н.Н.
4 Маркетинг и реклама NULL NULL
5 Логистика NULL NULL

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

https://amdy.su/wp-admin/options-general.php?page=ad-inserter.php#tab-8

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

Для примера получим отделы, в которых числится более двух сотрудников:

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

Конструкции EXISTS и NOT EXISTS

Позволяют проверить есть ли соответствующие условию записи в подзапросе:

Здесь все просто – EXISTS возвращает True, если подзапрос возвращает хотя бы одну строку, и False, если подзапрос не возвращает строк. NOT EXISTS – инверсия результата.

Конструкция IN и NOT IN с подзапросом

До этого мы рассматривали IN с перечислением значений. Так же можно использовать его с подзапросом, который возвращает перечень этих значений:

Обратите внимание, что я исключил NULL значение используя условие (DepartmentID IS NOT NULL) в подзапросе. NULL значения в данном случае так же опасны – смотрите об этом в описании конструкции IN во второй части.

Операции группового сравнения ALL и ANY

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

Операторы ALL и ANY используются в тех случаях, когда необходимо проверить условие на соответствие, с каждым значением которое вернул подзапрос. Они, как и оператор EXISTS работают только с подзапросами.

Для примера в каждом отделе выберем сотрудника, у которого ЗП больше ЗП всех сотрудников работающих в этом же отделе. Для этой цели применим ALL:

ID Name DepartmentID Salary
1000 Иванов И.И. 1 5000
1002 Сидоров С.С. 2 2500
1003 Андреев А.А. 3 2000
1005 Александров А.А. NULL 2000

Здесь происходит проверка на то, что e1.Salary больше значений e2.Salary, которые вернул подзапрос.

Как думаете, почему здесь вернулись даже те сотрудники, для которых подзапрос не вернул ни одной строки? А потому что логика такая – нет записей, не с чем проверять, а значит я и так больше всех. ))) Вот такая хитрость здесь скрыта.

Для большего понимания, давайте посмотрим, как можно здесь оператор ALL заменить оператором NOT EXISTS:

Т.е. мы тут выразили то же самое только другими словами «Верни сотрудников для которых нет сотрудников из того же отдела с большей ЗП чем у него».

Здесь становится понятно почему ALL возвращает истинное значение в том случае если подзапрос не возвращает данных.

Так же обратите внимание, что для ALL важно исключить NULL-значения из подзапроса, иначе результат проверки на каждое значение может оказаться неопределенным. Сравнивайте в этом случае логика ALL логикой при использовании AND, т.е. выражение (Salary>1000 AND Salary>1500 AND Salary>NULL) вернет NULL.

А вот с ANY (он же SOME) будет по-другому:

ID Name DepartmentID Salary
1003 Андреев А.А. 3 2000

C оператором ANY важно, чтобы подзапрос вернул записи, с которыми можно сравнить на любое выполнение условия. Т.к. во всех отделах сидят только по одному сотруднику, кроме ИТ-отдела, то вернулся только Андреев А.А., чью ЗП удалось сравнить с ЗП других сотрудников этого же отдела. Т.е. мы вытащили здесь тех, чья ЗП больше любой ЗП сотрудника из этого же отдела.

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

Смысл здесь стал «есть ли хоть какой-то сотрудник из этого отделу у которого ЗП ниже ЗП данного сотрудника».

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

Наличие NULL-значений в подзапросе здесь не так опасно, т.к. мы сравниваем на любое значение. Сравнивайте в этом случае логика ANY логикой при использовании OR, т.е. выражение (Salary>1000 OR Salary>1500 OR Salary>NULL) может вернуть истинное значение если выполнится хотя бы одно условие.

Если ANY используется для сравнения на равенство, то его можно представить при помощи IN:

Здесь мы возвращаем все отделы, в которых есть сотрудники. Соответственно это будет эквивалентно:

Как видите ALL и ANY можно выразить при помощи других операторов. Но в некоторых случаях их использование может сделать запрос более читабельным, поэтому для полноты картины их тоже стоит знать и применять в подходящих для этого случаях. Т.е. при написании запроса вы можете написать его так как вас попросили «выбери сотрудника у которого ЗП больше всех»:

не заменяя смысл на аналогичный «выбери сотрудников для которых нет сотрудников с ЗП больше чем у него»:

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

Еще пара слов про подзапросы

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

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

Поэтому в данном учебнике я уделил целых три части на рассмотрение базовых конструкций и только один раздел выделил подзапросам. Я считаю, что нельзя начинать объяснение SELECT с подзапросов, т.к. зная, что есть подзапросы, но не владея базовыми конструкциями, новички могут нагородить такие трехэтажные конструкции (подзапросы в подзапросах-подзапросах), которые даже профессионалам потом бывает трудно разобрать. Но если разобраться, то в некоторых случаях, зная основы все эти трехэтажные конструкции можно было бы выразить при помощи одного запроса с использованием, например, соединений и группировок.

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

Заключение

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

Данный материал был создан, опираясь на свой собственный практический опыт работы с языком SQL, которому уже более 10 лет, в разных СУБД (начиная с СУБД Paradox). В данном учебнике, я постарался максимально простым образом объяснить суть всех основных конструкций языка SQL служащих для выборки данных. Я старался объяснять так, чтобы данный учебник был понятен широкому кругу людей, а не только ИТ-специалистам. Надеюсь, что это у меня получилось и что данный материал поможет вам сделать первые шаги или же поможет понять какую-нибудь отдельную конструкцию, которая возможно вам не давалась ранее. В любом случае, спасибо всем, кто уделил свое время на ознакомление с этим материалом.

В следующей части я уже в общих чертах расскажу о операторах модификации данных. В общих чертах, так как эта информация, как и знание DDL, нужна не всем (в основном ИТ-специалистам) – большинство людей изучают SQL именно в целях научиться делать выборку данных при помощи оператора SELECT. Думаю, следующая часть будет заключительной. Все знания, полученные до этого момента, нам так же пригодятся в следующей части, т.к. для правильного написания сложных конструкций по модификации данных, нужно уверено пользоваться конструкциями оператора SELECT. Например, перед тем как удалить или изменить группу строк таблицы, мы должны правильно выбрать эти данные. Поэтому следующая часть так же будет содержать конструкции SELECT и думаю будет интересна тем людям, которые изучают SQL именно из-за оператора выборки SELECT.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *