Зарегистрируйтесь, чтобы продолжить обучение

Подзапросы SQL: Join

В этом уроке мы познакомимся с подзапросами. Их используют, когда в один запрос надо передать результат другого запроса.

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

Применение подзапросов

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

departments

department_id name city
1 Development Department Moscow
2 Marketing Department Vladivostok
3 Finance Department Omsk
4 HR Department Moscow
5 Sales Department Vladivostok

employees

employee_id department_id name salary
1 1 John Smith 50000.00
2 1 Peter Johnson 60000.00
3 2 Mary Brown 55000.00
4 2 Alex Miller 70000.00
5 3 Helen White 48000.00

View on DB Fiddle

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

Разобьем решение на два этапа:

Первый этап. Найдем идентификатор отдела по его названию:

Первый запрос

SELECT dep.department_id
FROM departments AS dep
WHERE dep.name = 'Development Department'

View on DB Fiddle

Названия отделов уникальны, поэтому запрос вернет одну строку с идентификатором отдела:

department_id
1

Второй этап. Воспользуемся найденным идентификатором и построим список сотрудников отдела:

Второй запрос

SELECT emp.name AS employee
FROM employees AS emp
WHERE emp.department_id = 1

Результат выполнения запроса:

employee
John Smith
Peter Johnson

View on DB Fiddle

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

Теперь рассмотрим решение этой же задачи с помощью подзапроса:

SELECT emp.name AS employee
FROM employees AS emp
WHERE emp.department_id = (
    SELECT dep.department_id
    FROM departments AS dep
    WHERE dep.name = 'Development Department'
)

View on DB Fiddle

Вместо того чтобы переносить результат первого запроса во второй, мы встроили первый запрос внутрь второго. Теперь СУБД сама определит значение и подставит его.

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

Мы используем подзапрос вместо значения, значит, это обязательно должен быть SELECT-запрос с одним столбцом. При этом внешний запрос может быть любого типа: SELECT, INSERT, UPDATE, DELETE.

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

Подзапросы, которые возвращают набор значений

Подзапросом можно заменить не только простое значение, но и набор значений. Рассмотрим такой пример.

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

SELECT emp.name AS employee
FROM employees AS emp
WHERE emp.department_id IN (
    SELECT dep.department_id
    FROM departments AS dep
    WHERE dep.city = 'Vladivostok'
);

Подзапрос здесь возвращает список идентификаторов отделов, расположенных в определенном городе:

department_id
2
5

View on DB Fiddle

Этот результат будет подставлен в основной запрос, то есть условие фильтрации станет таким: emp.department_id IN (2, 5). Оно проверяет, что emp.department_id равен одному из значений в списке (2, 5).

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

А вот обратное — не верно. Допустим, мы работаем с подзапросом как с простым значением, например, используем операторы сравнения =, >, < и так далее. Если такой запрос вернет больше одной строки, то СУБД не сможет выполнить основной запрос и вернет ошибку:

ERROR: ОШИБКА: подзапрос в выражении вернул больше одной строки

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

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

SELECT dep.department_id
FROM departments AS dep
WHERE dep.city = 'Vladivostok';

SELECT emp.name AS employee
FROM employees AS emp
WHERE emp.department_id IN (
    SELECT *
    FROM departments AS dep
    WHERE dep.city = 'Vladivostok'
);

https://www.db-fiddle.com/f/qX1gcqrUQCAtP1DwiRRiXU/6

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

Query Error: error: subquery has too many columns

Коррелированные подзапросы

В предыдущих примерах подзапросы не зависели от основного запроса. Поэтому СУБД может один раз выполнить подзапрос, запомнить результат и затем выполнить основной запрос.

Но бывает и другой тип подзапросов — коррелированные подзапросы. Они обращаются к полям основного запроса. Рассмотрим такой пример.

Наша задача — для всех отделов получить название и количество сотрудников. Эту задачу можно решить с помощью соединений, но в этом уроке мы воспользуемся подзапросом:

SELECT
    main_query_department.name AS department_title,
    (
        SELECT COUNT(*)
        FROM employees AS subquery_employee
        WHERE
            subquery_employee.department_id
            = main_query_department.department_id
    ) AS employees_count
FROM departments AS main_query_department

Результат выполнения запроса:

department_title employees_count
Development Department 2
Marketing Department 2
Finance Department 1
HR Department 0
Sales Department 0

View on DB Fiddle

Подзапрос считает количество сотрудников в отделе, но вот идентификатор отдела заранее не известен — мы берем его из основного запроса: subquery_employee.department_id = main_query_department.department_id. Поэтому для каждого отдела подзапрос будет выполнен заново.

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

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

Выводы

Подзапросы — это запросы внутри запросов. Они позволяют использовать результат одного запроса внутри другого запроса.

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

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

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff