Задачи SQL Справочник Подзапросы

Подзапросы, CTE и EXISTS

Подзапрос в WHERE и SELECT, IN, EXISTS и NOT EXISTS, таблица во FROM и WITH — на учебной базе интернет-магазина.

Подзапрос

Подзапрос — это SELECT внутри другого запроса, в скобках. Сначала считается внутренний, его результат подставляется наружу. Так сравнивают с агрегатом, проверяют вхождение в список или подставляют готовую таблицу во FROM.

Снаружи подзапрос ставят в WHERE, SELECT, FROM, HAVING. Внутри него доступны те же операторы, что и в обычном запросе.

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

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

SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC, name;
Товары дороже средней цены по всей витрине
nameprice
Ноутбук89000
Смартфон45000
Планшет22000

Внутренний AVG(price) считается один раз: он не ссылается на внешнюю строку. Такой подзапрос называют некоррелированным.

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

SELECT p.name, p.price
FROM products p
WHERE p.category_id = 1
  AND p.price > (
    SELECT AVG(p2.price)
    FROM products p2
    WHERE p2.category_id = p.category_id
  )
ORDER BY p.price DESC, p.name;
Электроника дороже средней цены своей категории
nameprice
Ноутбук89000
Смартфон45000

Наушники и планшет из электроники дешевле средней по категории — их нет в результате.

IN

IN проверяет, входит ли значение в список. Список можно написать вручную: city IN ('Казань', 'Самара'). Или получить подзапросом — тогда подзапрос возвращает одну колонку и сколько угодно строк.

SELECT name
FROM customers
WHERE id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'paid'
)
ORDER BY name;
Клиенты, у которых есть хотя бы один оплаченный заказ
name
Анна Смирнова
Артём Кузнецов
Елена Новикова
Иван Петров
Наталья Лебедева

Повторы во внутреннем списке не важны: Елена с двумя оплаченными заказами попадает один раз.

EXISTS

EXISTS отвечает на вопрос «нашлась ли хотя бы одна строка», а не «какое у неё значение». Внутри обычно пишут SELECT 1: колонку всё равно не читают, важно только наличие строки.

Подзапрос коррелированный: для каждого клиента смотрят его заказы.

SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
)
ORDER BY c.name;
Тот же смысл, что у IN: есть оплаченный заказ
name
Анна Смирнова
Артём Кузнецов
Елена Новикова
Иван Петров
Наталья Лебедева

IN удобен, когда сравнивают с готовым набором значений. EXISTS — когда проверяют, что связанная строка есть, особенно с несколькими условиями внутри.

NOT EXISTS

NOT EXISTS — «не нашлось ни одной строки». Клиенты без заказов: в первой главе это был LEFT JOIN и WHERE o.id IS NULL. Тот же смысл через NOT EXISTS:

SELECT c.name, c.city
FROM customers c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
)
ORDER BY c.name;
Клиенты без единого заказа
namecity
Кирилл БеловСамара
Павел МорозовМосква

NOT IN с подзапросом здесь даст то же, потому что customer_id не бывает NULL. Если внутренний список может содержать пустое значение, NOT IN не найдёт никого. Для «нет строк» надёжнее NOT EXISTS.

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

Во FROM подзапрос ведёт себя как таблица: у него должны быть имена колонок и алиас. В PostgreSQL без алиаса запрос не соберётся.

SELECT city, customers
FROM (
  SELECT city, COUNT(*) AS customers
  FROM customers
  GROUP BY city
) t
WHERE customers >= 2
ORDER BY customers DESC, city;
Сначала посчитали по городам, потом оставили крупные
citycustomers
Москва4
Казань2
Санкт-Петербург2

Это тот же фильтр, что HAVING COUNT(*) >= 2 в первой главе. Подзапрос во FROM нужен, когда промежуточную таблицу дальше джойнят или фильтруют по уже посчитанным колонкам.

WITH

WITH даёт подзапросу имя — CTE, common table expression. Дальше к этому имени обращаются как к таблице. Смысл тот же, что у подзапроса во FROM, читать обычно проще.

WITH city_counts AS (
  SELECT city, COUNT(*) AS customers
  FROM customers
  GROUP BY city
)
SELECT city, customers
FROM city_counts
WHERE customers >= 2
ORDER BY customers DESC, city;
Тот же результат, что у подзапроса во FROM
citycustomers
Москва4
Казань2
Санкт-Петербург2

Несколько CTE пишут через запятую. Следующий может ссылаться на предыдущий. Рекурсивный WITH RECURSIVE — для иерархий вроде «сотрудник и его руководитель»; его разберём отдельно.

Всё вместе

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

WITH big_cities AS (
  SELECT city
  FROM customers
  GROUP BY city
  HAVING COUNT(*) >= 2
)
SELECT c.name, c.city
FROM customers c
JOIN big_cities b ON b.city = c.city
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.status = 'paid'
)
ORDER BY c.name;
Москва, Казань, Санкт-Петербург и оплаченный заказ
namecity
Анна СмирноваМосква
Артём КузнецовКазань
Елена НовиковаМосква
Иван ПетровСанкт-Петербург

Наталья Лебедева заказ оплатила, но в Екатеринбурге она одна — big_cities её не берёт. Сергей из Казани в городе проходит, а оплаченного заказа нет.

Дальше — окна, UNION, INTERSECT и EXCEPT.