Сегодня мы разберём одну из фундаментальных, но при этом вызывающую вопросы тему — подзапросы в SQL. Понимание этой конструкции — это переход от простого умения писать запросы к способности решать по-настоящему сложные аналитические задачи.
Если вы хотите не просто извлекать данные, а гибко и мощно их трансформировать, эта статья для вас.
Что такое подзапрос?
Начнём с простой аналогии. Представьте, что ваш руководитель просит вас сначала узнать, кто из клиентов совершил покупку на сумму больше 1000 рублей в этом месяце, а потом принести их полные контактные данные. Это и называется подзапрос.
Таким образом, подзапрос или внутренний запрос — это запрос, который вложен внутрь другого запроса (внешнего или основного запроса). Его результат используется основным запросом для дальнейших операций. Подзапрос всегда заключён в круглые скобки и выполняется первым, до внешнего запроса.
Подзапрос имеет следующий синтаксис:
SELECT client_uk, client_name
FROM customers
WHERE client_uk = (SELECT client_uk FROM sales where amt_sales >= 1000);
Здесь выражение в скобках (SELECT client_uk FROM sales where amt_sales >= 1000) – это и есть подзапрос.
Зачем нужны подзапросы? Основные сценарии применения
Многие новички спрашивают: «А нельзя ли обойтись JOIN'ами?». Часто можно. Но есть целый класс задач, где подзапросы делают код гораздо более читаемым, логичным и лаконичным. Давайте разберём основные случаи их использования.
Подзапросы в условии WHERE (с операторами сравнения)
Это, пожалуй, самый частый сценарий. Когда вам нужно отфильтровать записи по условию, которое само требует отдельного запроса к базе.
Например, нам нужно найти всех клиентов, операционная прибыль которых выше средней по клиентской базе.
- Без подзапроса пришлось бы делать два запроса: сначала узнать среднюю операционную прибыль (ОП) по клиентской базе, запомнить число, а затем сделать второй запрос WHERE op > полученного значения.
- С подзапросом мы делаем это одним изящным запросом:
SELECT client_uk, client_name, op
FROM cutomer_op
WHERE op > (SELECT AVG(op) FROM cutomer_op);
Здесь подзапрос (SELECT AVG(op) FROM cutomer_op) вычисляет единственное значение — среднюю ОП. Внешний запрос затем использует это значение для фильтрации всех клиентов. Это наглядно и не требует ручного вмешательства.
Важный нюанс: когда мы используем операторы сравнения (=, >, <, >=, <=, <>), подзапрос должен возвращать только одно значение (одну строку и одну колонку). Такой подзапрос называется скалярным.
Подзапросы с операторами IN и NOT IN
А что, если подзапрос целый список значений? Например, несколько ID. Здесь на помощь приходит оператор IN.
Например, нам нужно найти всех менеджеров, которые курируют отделы, расположенные в Лондоне.
Для этого нам нужно:
- Сначала найти ID всех отделов в Лондоне.
- Потом найти всех менеджеров, чей department_id находится в полученном списке.
SELECT employee_id, first_name, last_name
FROM employees
WHERE department_id IN (
SELECT department_id
FROM departments
WHERE location_id IN (
SELECT DISTINCT location_id FROM locations WHERE UPPER(city) = 'LONDON'
)
);
Обратите внимание, что здесь мы видим вложенный подзапрос — подзапрос внутри подзапроса. Сначала находится location_id для Лондона, затем по этому ID находятся все department_id, и, наконец, по списку этих ID находятся сотрудники.
Оператор NOT IN работает аналогично, но исключает записи, попадающие в список.
Подзапросы в качестве вычисляемого столбца (в SELECT)
Это мощный инструмент для обогащения основной выборки дополнительными, вычисляемыми на лету данными.
Например, мы хотим получить список всех сотрудников, и для каждого увидеть его зарплату и среднюю зарплату по его отделу.
SELECT
employee_ccode,
employee_name,
salary,
department_name,
(SELECT AVG(salary) FROM employees e2 WHERE e2.department_ccode = e1.department_ccode) AS avg_department_salary
FROM employee e1;
Здесь для каждой строки из внешнего запроса (e1) выполняется коррелированный подзапрос. Он вычисляет среднюю зарплату, но не по всей компании, а именно по тому отделу, к которому принадлежит текущий сотрудник.
Обратите внимание на условие e2.department_ccode = e1.department_ccode — оно связывает внешний и внутренний запросы.
Здесь стоит оговориться — такой вид запроса удобен, но не оптимален с точки зрения работы базы данных.
Подзапросы в FROM (представления на лету)
Иногда нам нужно использовать результат сложного запроса как таблицу. Это идеальная работа для подзапроса в FROM.
Например, нам нужно найти среднюю зарплату по отделам, а затем выбрать из этого списка только те отделы, где средняя зарплата превышает 80000.
SELECT *
FROM (
SELECT
department_ccode,
AVG(salary) AS avg_sal
FROM employees
GROUP BY department_ccode
) AS department_avg
WHERE avg_sal > 80000;
В этом примере подзапрос создает временную «виртуальную» таблицу с псевдонимом department_avg, которая содержит два столбца: department_id и avg_sal. Внешний запрос работает уже с этой временной таблицей как с обычной. Это отличный способ разбивать сложные задачи на простые этапы.
Также этот тип подзапросов помогает при валидации данных, позволяя быстро группировать необходимые данные для сравнения с источником.
Коррелированные vs Некоррелированные подзапросы
Это ключевая классификация, которую должен понимать каждый аналитик.
- Некоррелированный подзапрос — независимый. Он выполняется один раз, и его результат передаётся во внешний запрос. Все примеры выше, кроме подзапроса в SELECT, были некоррелированными. Они, как правило, более эффективны.
- Коррелированный подзапрос зависит от данных внешнего запроса. Он выполняется не один раз, а для каждой строки результата внешнего запроса — как в примере с avg_department_salary. Внутри подзапроса есть ссылка на столбец из внешней таблицы (e1.department_ccode). Такие запросы могут быть медленными на больших объёмах данных, так как являются аналогом цикла for each.
EXISTS и NOT EXISTS: особая магия
Операторы EXISTS и NOT EXISTS используются исключительно с подзапросами и проверяют не сам результат, а его наличие.
Например, нам нужно найти покрытие баннерами клиентов, которые делали хотя бы один раз видели баннер.
SELECT banner_name, coverage
FROM banners c
WHERE EXISTS (
SELECT 1
FROM banner_views o
WHERE o. banner_name = c. banner_name
);
Здесь подзапрос не возвращает данные во внешний запрос. Он лишь возвращает TRUE, если для текущего клиента найдётся хотя бы одна строка в таблице banner_views, и FALSE в противном случае. Обратите внимание, что в SELECT подзапроса часто пишут 1 или * — значение не важно, важна только проверка на существование.
NOT EXISTS работает с точностью до наоборот и часто используется для поиска «сирот» — записей, для которых нет связей в другой таблице.
Рекомендации и предостережения
- Производительность. Подзапросы, особенно коррелированные, могут быть медленными. Всегда смотрите на EXPLAIN PLAN (план выполнения запроса). Часто запрос с JOIN может быть эффективнее.
- Читаемость. Грамотно используемые подзапросы делают код логичным и легко сопровождаемым. Но глубоко вложенные «спагетти-подзапросы» могут стать кошмаром. Не злоупотребляйте вложенностью. Лучше используйте временные таблицы или CTE. CTE — это современный и очень читаемый способ заменить сложный подзапрос. Он позволяет дать имя подзапросу и использовать его несколько раз в основном запросе, что улучшает структуру кода.
Подзапросы — это не просто синтаксическая конструкция. Это способ мышления, позволяющий декомпозировать сложную бизнес-задачу на последовательные логические шаги. Они открывают путь к написанию мощных, гибких и выразительных SQL-запросов.
Не бойтесь их использовать. Практикуйтесь, сравнивайте с альтернативами, анализируйте планы запросов, и вы быстро почувствуете, в какой ситуации какой инструмент — подзапрос, JOIN или CTE — будет наиболее уместным и эффективным.
