Подзапросы

Учимся вкладывать один запрос внутрь другого.

Подзапрос — это SELECT, вложенный внутрь другого запроса; его результат используется внешним запросом.

Подзапрос в WHERE с IN

Иногда условие зависит от другой таблицы. Например, «выбрать клиентов, у которых был крупный заказ». Сначала подзапрос находит id таких клиентов, а внешний запрос выбирает их имена.

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);

INSERT INTO customers (id, name) VALUES (1, 'Аня'), (2, 'Борис'), (3, 'Вера');
INSERT INTO orders (customer_id, amount) VALUES (1, 500), (1, 1200), (2, 800);

SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE amount > 1000);

Вывод:

Аня

Подзапрос вернул id клиентов с заказом дороже 1000 (только id = 1), а внешний запрос показал их имена.

Скалярный подзапрос в сравнении

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

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);

INSERT INTO products (name, price) VALUES
    ('Клавиатура', 2500), ('Мышь', 1200), ('Монитор', 18000);

SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);

Вывод:

Монитор|18000

Средняя цена ≈ 7233, и только «Монитор» её превышает. Подзапрос (SELECT AVG(price) ...) вернул одно число, с которым мы сравниваем.

NOT IN — кого нет в списке

Зеркальная задача: найти клиентов без заказов. Подзапрос даёт id всех, у кого заказы есть, а NOT IN оставляет остальных.

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER);

INSERT INTO customers (id, name) VALUES (1, 'Аня'), (2, 'Борис'), (3, 'Вера');
INSERT INTO orders (customer_id) VALUES (1), (2);

SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);

Вывод:

Вера

Внимание: если столбец в подзапросе может содержать NULL, NOT IN ведёт себя коварно (из-за логики NULL) и может вернуть пусто. В таких случаях надёжнее NOT EXISTS или LEFT JOIN ... WHERE ... IS NULL.

Подзапрос в списке выбора

Подзапрос можно поставить и в сам SELECT, посчитав значение для каждой строки. Например, число заказов каждого клиента:

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER);

INSERT INTO customers (id, name) VALUES (1, 'Аня'), (2, 'Борис'), (3, 'Вера');
INSERT INTO orders (customer_id) VALUES (1), (1), (2);

SELECT name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders_count
FROM customers c
ORDER BY name;

Вывод:

Аня|2
Борис|1
Вера|0

Часто ту же задачу решают через JOIN + GROUP BY (следующий раздел) — это бывает эффективнее, но подзапрос нагляднее для понимания.

Итог

  • Подзапрос — это SELECT внутри запроса; его результат использует внешний запрос.
  • IN / NOT IN с подзапросом фильтруют по списку значений из другой таблицы.
  • Скалярный подзапрос (одно значение) сравнивают напрямую; подзапрос можно ставить и в список SELECT.
Проверьте себя
1. Что такое подзапрос?
AКоманда удаления
BSELECT, вложенный внутрь другого запроса
CСпособ сортировки
DТип индекса
2. Когда подзапрос можно сравнивать через price > (SELECT ...)?
AКогда он возвращает много строк
BКогда он возвращает одно (скалярное) значение
CНикогда
DТолько в INSERT
3. Почему NOT IN бывает коварен?
AОн медленный всегда
BЕсли в подзапросе есть NULL, он может неожиданно вернуть пустой результат
CОн не работает в SQLite
DОн удаляет строки
Поддержать проект