Перейти к содержанию
Educora
Средний18 мин9 / 22

Подзапросы

Пиши запрос внутри запроса: подзапросы в WHERE, SELECT и FROM, IN, EXISTS, коррелированные подзапросы и конструкция WITH.

Проверь себя
В этом уроке ты узнаешь
  • Писать подзапросы, возвращающие одно значение или список
  • Использовать EXISTS и коррелированные подзапросы
  • Разбивать сложный запрос на части с помощью подзапроса в FROM и WITH
  • Избегать ловушки NOT IN и NULL

«Какие товары дороже средней цены?» В этом вопросе два шага: сначала найти среднюю цену (293,56), затем сравнить с ней. Число можно вписать в запрос вручную, но завтра цены изменятся, и запрос устареет. Лучше сделать оба шага в одном запросе. Для этого используют подзапрос.

Подзапрос в WHERE

Определение
Подзапрос

Запрос SELECT, записанный в скобках внутри другого запроса. Его результат внешний запрос использует как значение, список или таблицу.

SQL
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
▸ Ожидаемый результат
name | price
Laptop | 1450
Smartphone | 899.99
Monitor | 310

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

SQL
SELECT name, price
FROM products
WHERE price = (SELECT MAX(price) FROM products);
▸ Ожидаемый результат
name | price
Laptop | 1450

Подзапросы с IN и NOT IN

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

SQL
SELECT first_name, last_name
FROM students
WHERE id IN (
  SELECT student_id
  FROM enrollments
  WHERE course_id IN (SELECT id FROM courses WHERE subject = 'Physics')
)
ORDER BY id;
▸ Ожидаемый результат
first_name | last_name
Murad | Əliyev
Elvin | Quliyev
Rəşad | Kərimov
Fidan | Cəfərova
SQL
SELECT name, category
FROM products
WHERE id NOT IN (SELECT product_id FROM orders)
ORDER BY id;
▸ Ожидаемый результат
name | category
Monitor | Electronics
Товар, который ни разу не заказывали, — тот же результат, что и LEFT JOIN ... IS NULL в прошлом уроке.
SQL
SELECT COUNT(*) AS found
FROM products
WHERE id NOT IN (1, 2, NULL);
▸ Ожидаемый результат
found
0
Убери NULL из списка и запусти снова — получится 8.

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

Коррелированный подзапрос ссылается на столбец внешнего запроса, например o.customer_id = c.id. Логически он вычисляется заново для каждой строки внешнего запроса. EXISTS (...) истинно, когда подзапрос возвращает хотя бы одну строку, — неважно, что в этих строках, поэтому внутри обычно пишут SELECT 1.

SQL
SELECT c.name, c.country
FROM customers AS c
WHERE EXISTS (
  SELECT 1
  FROM orders AS o
  JOIN products AS p ON p.id = o.product_id
  WHERE o.customer_id = c.id
    AND p.category = 'Electronics'
)
ORDER BY c.id;
▸ Ожидаемый результат
name | country
Anar Mustafayev | Azerbaijan
Lalə Əhmədova | Azerbaijan
Emre Yılmaz | Türkiye
Zəhra Hüseynli | Azerbaijan
Покупатели, которые хотя бы раз купили электронику.

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

SQL
SELECT s.first_name,
       (SELECT COUNT(*)   FROM enrollments AS e WHERE e.student_id = s.id) AS courses,
       (SELECT MAX(score) FROM enrollments AS e WHERE e.student_id = s.id) AS best
FROM students AS s
WHERE s.grade = 10
ORDER BY s.id;
▸ Ожидаемый результат
first_name | courses | best
Murad | 2 | 81
Rəşad | 2 | 99
Səbinə | 2 | 88
Пример задачи

В каких зачислениях балл выше среднего балла именно этого курса?

Показать решение
У каждого курса своё среднее, поэтому подзапрос должен знать курс внешней строки.
Используем одну и ту же таблицу дважды и различаем копии псевдонимами: внешняя e, внутренняя e2.
Условие: e.score > (SELECT AVG(e2.score) FROM enrollments AS e2 WHERE e2.course_id = e.course_id).
Остаются зачисления выше среднего по каждому курсу — всего 9 строк.
SQL
SELECT e.student_id, e.course_id, e.score
FROM enrollments AS e
WHERE e.score > (
  SELECT AVG(e2.score)
  FROM enrollments AS e2
  WHERE e2.course_id = e.course_id
)
ORDER BY e.course_id, e.score DESC;
▸ Ожидаемый результат
student_id | course_id | score
11 | 1 | 97
1 | 1 | 92
3 | 2 | 95
11 | 3 | 94
2 | 3 | 81
4 | 4 | 72
5 | 5 | 93
6 | 6 | 99
3 | 7 | 90

Подзапрос в FROM и WITH

Результат подзапроса — таблица, поэтому его можно написать и в FROM, где он работает как временная таблица. Большинство СУБД требуют дать такому подзапросу псевдоним (AS t). Ниже мы сначала находим среднее по каждому курсу, а затем среднее этих средних и лучшее из них.

SQL
SELECT ROUND(AVG(avg_score), 1) AS avg_of_courses,
       ROUND(MAX(avg_score), 1) AS best_course
FROM (
  SELECT course_id, AVG(score) AS avg_score
  FROM enrollments
  GROUP BY course_id
) AS t;
▸ Ожидаемый результат
avg_of_courses | best_course
82 | 92.7

Когда подзапросов, вложенных друг в друга, становится много, запрос трудно читать. Конструкция WITH (CTE — обобщённое табличное выражение) позволяет заранее дать подзапросу имя и затем использовать его как обычную таблицу. WITH поддерживают SQLite, PostgreSQL, SQL Server и MySQL начиная с версии 8.0.

SQL
WITH course_avg AS (
  SELECT course_id, ROUND(AVG(score), 1) AS avg_score
  FROM enrollments
  GROUP BY course_id
)
SELECT c.title, ca.avg_score
FROM course_avg AS ca
JOIN courses AS c ON c.id = ca.course_id
WHERE ca.avg_score > 85
ORDER BY ca.avg_score DESC;
▸ Ожидаемый результат
title | avg_score
Python Basics | 92.7
World History | 90.5
Algebra | 87.3
Задание

Выведи имя и возраст учеников, которые старше среднего возраста всех учеников. Отсортируй по возрасту по убыванию, при равном возрасте — по имени. Не вписывай среднее вручную — вычисли его подзапросом.

Задание · SQL
SELECT first_name, age
FROM students
WHERE age > ( /* average age here */ )
ORDER BY age DESC, first_name;
▸ Ожидаемый результат
first_name | age
Elvin | 17
Fidan | 17
Tural | 17
Murad | 16
Rəşad | 16
Səbinə | 16
Задание

Выведи названия товаров, которые заказывали покупатели из Турции (country = 'Türkiye'). Не используй JOIN — примени вложенные подзапросы с IN. Отсортируй по названию.

Задание · SQL
SELECT name
FROM products
WHERE id IN (
  -- product ids from the orders of Turkish customers
)
ORDER BY name;
▸ Ожидаемый результат
name
Chess set
Pen set
Smartphone

Главное

  • Подзапрос — это SELECT в скобках; его результат используют как значение, список или таблицу.
  • Скалярный подзапрос возвращает ровно одно значение и сравнивается операторами вроде = и >.
  • IN (SELECT ...) работает со списком; NOT IN ничего не вернёт, если в списке есть NULL.
  • Коррелированный подзапрос ссылается на внешнюю строку; EXISTS проверяет, есть ли хотя бы одна строка.
  • Давай подзапросу в FROM псевдоним; разбивай сложные запросы на части с помощью WITH.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что такое подзапрос?