Открыть сервис

Подзапрос

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

История и стандартизация

Концепция подзапросов была введена в языке SQL в 1980-х годах в рамках стандарта SQL-86. Первоначально подзапросы поддерживались лишь в ограниченном объёме — в основном в конструкции WHERE с операторами IN и EXISTS. Стандарт SQL-92 (SQL2) значительно расширил возможности подзапросов, разрешив их использование в предложениях FROM, SELECT и HAVING, а также добавив коррелированные подзапросы. Последующие версии стандарта (SQL:1999, SQL:2003, SQL:2016) уточнили поведение подзапросов, ввели поддержку подзапросов в предложении WITH (Common Table Expressions) и рекурсивных запросов. Все современные реляционные СУБД (PostgreSQL, MySQL, Oracle Database, Microsoft SQL Server, SQLite) реализуют подзапросы в соответствии со стандартом SQL, хотя могут иметь незначительные диалектные отличия.

Классификация подзапросов

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

По месту вхождения

  • Подзапрос в WHERE — самый распространённый тип. Используется для фильтрации строк внешнего запроса на основе результата внутреннего запроса. Пример: SELECT * FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'IT');.
  • Подзапрос в FROM — выступает в роли виртуальной таблицы (производной таблицы). Внешний запрос обращается к результату подзапроса как к обычной таблице. Пример: SELECT avg_salary FROM (SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id) AS dept_avg WHERE avg_salary > 50000;.
  • Подзапрос в SELECT — используется для вычисления скалярного значения для каждой строки внешнего запроса. Должен возвращать ровно одно значение (одну строку и один столбец). Пример: SELECT name, (SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id) AS order_count FROM customers;.
  • Подзапрос в HAVING — применяется для фильтрации групп, сформированных предложением GROUP BY. Аналогичен подзапросу в WHERE, но действует на уровне агрегированных данных. Пример: SELECT department_id, AVG(salary) FROM employees GROUP BY department_id HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);.

По типу возвращаемого результата

  • Скалярный подзапрос — возвращает одно значение (одну строку и один столбец). Может использоваться везде, где допустимо одиночное значение (например, в операторах сравнения =, >, <). Если скалярный подзапрос возвращает более одной строки, возникает ошибка выполнения.
  • Строковый подзапрос — возвращает одну строку, но несколько столбцов. Используется реже, обычно в сочетании с конструкциями сравнения строк ((col1, col2) = (SELECT ...)).
  • Табличный подзапрос — возвращает множество строк и столбцов. Используется в FROM (как производная таблица) или с операторами IN, ANY, ALL, EXISTS. Наиболее универсальный тип.

По зависимости от внешнего запроса

  • Некоррелированный (независимый) подзапрос — не содержит ссылок на столбцы внешнего запроса. Выполняется один раз, и его результат используется внешним запросом. Оптимизатор базы данных может выполнить его первым и закешировать результат. Пример: SELECT * FROM products WHERE price > (SELECT AVG(price) FROM products);.
  • Коррелированный подзапрос — содержит ссылку на столбец из внешнего запроса. Выполняется для каждой строки внешнего запроса (или для каждой группы, если используется в HAVING). Это может существенно снижать производительность на больших наборах данных, но позволяет реализовать логику, недоступную некоррелированным подзапросам. Пример: SELECT name FROM employees e WHERE salary > (SELECT AVG(salary) FROM employees WHERE department_id = e.department_id);.

Синтаксис и операторы

Подзапрос всегда заключается в круглые скобки. В зависимости от оператора, с которым он используется, к результату подзапроса предъявляются определённые требования.

Операторы сравнения

Скалярные подзапросы могут использоваться с операторами =, >, <, >=, <=, <> (не равно). Пример: SELECT * FROM students WHERE score = (SELECT MAX(score) FROM students);.

Операторы IN и NOT IN

Подзапрос с IN проверяет, содержится ли значение из внешнего запроса в результирующем наборе подзапроса. Подзапрос должен возвращать один столбец. Пример: SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);.

Операторы ANY, SOME, ALL

  • ANY (синоним SOME) — возвращает TRUE, если условие выполняется хотя бы для одного значения из подзапроса. Пример: SELECT * FROM products WHERE price > ANY (SELECT price FROM products WHERE category = 'Electronics');.
  • ALL — возвращает TRUE, только если условие выполняется для всех значений из подзапроса. Пример: SELECT * FROM products WHERE price > ALL (SELECT price FROM products WHERE category = 'Accessories');.

Оператор EXISTS

Проверяет, возвращает ли подзапрос хотя бы одну строку. Сам результат подзапроса (значения столбцов) не используется — важна только его мощность. EXISTS обычно работает быстрее IN на больших наборах данных, так как может остановить выполнение при нахождении первой строки. Пример: SELECT * FROM departments d WHERE EXISTS (SELECT 1 FROM employees e WHERE e.department_id = d.id AND e.salary > 100000);.

Производительность и оптимизация

Подзапросы, особенно коррелированные, могут быть менее производительными, чем эквивалентные соединения (JOIN) или общие табличные выражения (CTE). Оптимизаторы современных СУБД (например, PostgreSQL, Oracle) часто автоматически преобразуют подзапросы в JOIN при условии, что такая трансформация не меняет семантику запроса. Однако в некоторых случаях подзапрос остаётся единственным или наиболее читаемым способом решения задачи.

Рекомендации по оптимизации:

  • Некоррелированные подзапросы обычно выполняются быстро, так как вычисляются однократно.
  • Коррелированные подзапросы следует заменять на JOIN или CTE, если это возможно без потери логики.
  • Использование EXISTS вместо IN часто даёт выигрыш в производительности, особенно если подзапрос возвращает много строк.
  • Индексы на столбцах, участвующих в условиях подзапроса, критически важны для скорости выполнения.

Альтернативы подзапросам

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

  • JOIN (соединения) — часто эквивалентны подзапросам в WHERE и FROM, но позволяют оптимизатору строить более эффективные планы выполнения.
  • Common Table Expressions (CTE) с WITH — позволяют вынести подзапрос в начало запроса, улучшая читаемость, и поддерживают рекурсию. Пример: WITH high_salary AS (SELECT FROM employees WHERE salary > 100000) SELECT FROM high_salary WHERE department_id = 5;.
  • Временные таблицы — создаются явно, могут быть проиндексированы и использованы в нескольких последующих запросах, что даёт гибкость, недоступную подзапросам.

Ограничения и особенности

  • Подзапрос в SELECT должен быть скалярным; если он возвращает более одной строки, СУБД выдаёт ошибку.
  • В некоторых СУБД (например, MySQL) подзапросы в FROM требуют обязательного псевдонима (алиаса) для производной таблицы.
  • Подзапросы не могут напрямую ссылаться на внешние таблицы, которые находятся в более чем одном уровне вложенности, если это не поддерживается конкретной реализацией (ограничение глубины корреляции).
  • Вложенность подзапросов может быть практически любой глубины, но чрезмерная вложенность ухудшает читаемость и производительность.

Примеры использования

Пример 1. Подзапрос в WHERE с IN. Выборка сотрудников, работающих в отделах с названиями «Sales» или «Marketing»: SELECT first_name, last_name FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name IN ('Sales', 'Marketing'));

Пример 2. Коррелированный подзапрос с EXISTS. Выборка клиентов, которые сделали хотя бы один заказ на сумму более 5000: SELECT name FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 5000);

Пример 3. Подзапрос в FROM для расчёта средних значений. Средняя зарплата по отделам, где средняя зарплата выше 60000: SELECT department_id, avg_salary FROM (SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id) AS dept_stats WHERE avg_salary > 60000;

Пример 4. Скалярный подзапрос в SELECT. Вывод имени сотрудника и его зарплаты относительно средней зарплаты по компании: SELECT name, salary, (SELECT AVG(salary) FROM employees) AS company_avg FROM employees;

Источники

  • ISO/IEC 9075:2016 (SQL:2016) — стандарт языка SQL.
  • Документация PostgreSQL 16: «Subqueries» (PostgreSQL Global Development Group).
  • Документация MySQL 8.0: «Subqueries» (Oracle Corporation).
  • Документация Microsoft SQL Server 2022: «Subqueries» (Microsoft).
  • Elmasri R., Navathe S. B. «Fundamentals of Database Systems» (7th edition), Chapter 4: SQL.
  • Date C. J. «SQL and Relational Theory: How to Write Accurate SQL Code» (3rd edition).

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →