Диалект — PostgreSQL. Подзапросы в WHERE, FROM, SELECT, коррелированные подзапросы, CTE, рекурсивные CTE.
Подзапрос — это запрос, вложенный внутрь другого запроса. Он может находиться в SELECT, FROM, WHERE, HAVING.
Зачем нужны подзапросы?
IN, =, сравненияIN: значение из спискаНайдём сотрудников, которые работают в отделах с бюджетом больше 2 500 000.
SELECT name, salary
FROM employees
WHERE department_id IN (
SELECT id
FROM departments
WHERE budget > 2500000
);Как это работает:
SELECT id FROM departments WHERE budget > 2500000 → возвращает [1, 2] (IT и Продажи).WHERE department_id IN (1, 2) → берёт сотрудников из этих отделов.→ Анна (120 000), Иван (80 000), Мария (130 000), Елена (90 000), Павел (110 000), Сергей (85 000).
💡 Важно: подзапрос с
INдолжен возвращать один столбец. Иначе будет ошибка.
=, >, <: скалярный результатНайдём сотрудников, которые зарабатывают больше средней зарплаты по компании.
SELECT name, salary
FROM employees
WHERE salary > (
SELECT AVG(salary)
FROM employees
);Внутренний запрос возвращает одно число (95 000), и внешний запрос сравнивает с ним зарплату каждого сотрудника.
→ Анна (120 000), Мария (130 000), Павел (110 000).
⚠️ Осторожно: если подзапрос вернёт больше одной строки, будет ошибка. Используй
IN, если ожидаешь несколько значений.
EXISTS: проверка существованияEXISTS возвращает TRUE, если подзапрос вернул хотя бы одну строку.
Найдём отделы, в которых есть хотя бы один сотрудник с премией.
SELECT d.name
FROM departments d
WHERE EXISTS (
SELECT 1
FROM employees e
WHERE e.department_id = d.id
AND e.bonus IS NOT NULL
);→ IT, Продажи, Маркетинг.
💡
EXISTSvsIN:
EXISTSбыстрее, если подзапрос возвращает много строк (он останавливается, как только нашёл первую).INудобнее, если подзапрос возвращает мало значений.EXISTSработает сNULL, аINсо списком, содержащимNULL, может дать неожиданный результат.
Подзапрос в FROM создаёт временную таблицу, с которой работает внешний запрос.
SELECT dept_avg.department_name, dept_avg.avg_salary
FROM (
SELECT d.name AS department_name, AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name
) AS dept_avg
WHERE dept_avg.avg_salary > 80000;→ IT, 120000.
Как это работает:
dept_avg с двумя столбцами.avg_salary > 80000.⚠️ Важно: производная таблица обязательно должна иметь псевдоним (
AS dept_avg), иначе будет ошибка.💡 Когда использовать: когда нужно сначала агрегировать данные, а потом отфильтровать результат агрегации (то, что нельзя сделать через
HAVING, если фильтр зависит от нескольких агрегатов).
Подзапрос в SELECT должен возвращать одно значение для каждой строки.
SELECT
e.name,
e.salary,
(SELECT AVG(salary) FROM employees WHERE department_id = e.department_id) AS dept_avg_salary
FROM employees e;→ Анна (120 000, 120 000), Иван (80 000, 85 000), Мария (130 000, 120 000), Андрей (70 000, 72 500), Елена (90 000, 85 000), Павел (110 000, 120 000), Дарья (75 000, 72 500), Сергей (85 000, 85 000).
💡 Когда использовать: когда нужно добавить к каждой строке агрегированное значение из связанной таблицы.
⚠️ Осторожно: если подзапрос вернёт больше одной строки, будет ошибка.
Коррелированный подзапрос — это подзапрос, который ссылается на столбцы из внешнего запроса. Он выполняется для каждой строки внешнего запроса.
SELECT e1.name, e1.salary, e1.department_id
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e1.department_id
);Как это работает:
e1) выполняется внутренний запрос.e1.department_id).→ Анна (120 000, отдел 1), Мария (130 000, отдел 1), Елена (90 000, отдел 2), Дарья (75 000, отдел 3).
💡 Производительность: коррелированные подзапросы могут быть медленными, потому что выполняются для каждой строки. Если можно переписать через
JOIN+GROUP BY— делай это.
JOIN:SELECT e.name, e.salary, e.department_id
FROM employees e
JOIN (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) AS dept_avg ON e.department_id = dept_avg.department_id
WHERE e.salary > dept_avg.avg_salary;Этот запрос выполнится быстрее, потому что агрегация считается один раз, а не для каждой строки.
WITHCTE — это именованный временной набор данных, который можно использовать в запросе. Делает код гораздо чище и читаемее.
WITH cte_name AS (
SELECT ...
)
SELECT * FROM cte_name;Вместо подзапроса в FROM:
WITH dept_avg AS (
SELECT d.name AS department_name, AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name
)
SELECT department_name, avg_salary
FROM dept_avg
WHERE avg_salary > 80000;Результат такой же, как в примере с подзапросом в FROM, но код гораздо понятнее.
💡 Преимущества CTE:
- Код читается сверху вниз, как инструкция.
- Можно использовать CTE несколько раз в одном запросе.
- Легче отлаживать (можно выполнить CTE отдельно).
WITH
dept_avg AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
),
high_earners AS (
SELECT e.name, e.salary, e.department_id
FROM employees e
JOIN dept_avg d ON e.department_id = d.department_id
WHERE e.salary > d.avg_salary
)
SELECT name, salary
FROM high_earners;Здесь мы создали два CTE: dept_avg и high_earners, и использовали dept_avg внутри high_earners.
Рекурсивные CTE используются для работы с иерархическими данными (например, организационная структура, дерево категорий).
WITH RECURSIVE org_chart AS (
-- Базовый случай: топ-менеджер
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Рекурсивный шаг: подчинённые
SELECT e.id, e.name, e.manager_id, o.level + 1
FROM employees e
JOIN org_chart o ON e.manager_id = o.id
)
SELECT * FROM org_chart;💡 Когда использовать: когда нужно обойти дерево (например, «найди всех подчинённых этого менеджера на всех уровнях»).
⚠️ Осторожно: рекурсивные CTE могут зациклиться, если в данных есть цикл. Всегда добавляй условие остановки.
| Ситуация | Что использовать |
|---|---|
| Нужно один раз получить агрегат и использовать его | Подзапрос в WHERE или SELECT |
| Нужно агрегировать, а потом отфильтровать результат | CTE или подзапрос в FROM |
| Нужно сравнить строку с агрегатом по группе | Коррелированный подзапрос или JOIN + GROUP BY |
| Сложная логика с несколькими шагами | CTE (чище и понятнее) |
| Нужно обойти дерево | Рекурсивный CTE |
| Простое объединение таблиц | JOIN (быстрее) |
💡 Общее правило: если можешь написать через
JOIN— пиши черезJOIN. Если логика сложная и требует промежуточных шагов — используй CTE.
Попробуй решить самостоятельно, не подглядывая ниже.
IN).>).EXISTS).SELECT).JOIN + GROUP BY (без коррелированного подзапроса).-- 1
SELECT name, salary
FROM employees
WHERE department_id IN (
SELECT id
FROM departments
WHERE budget > 3000000
);
-- 2
SELECT name, salary
FROM employees
WHERE salary > (
SELECT AVG(salary)
FROM employees
);
-- 3
SELECT d.name
FROM departments d
WHERE EXISTS (
SELECT 1
FROM employees e
WHERE e.department_id = d.id
AND e.salary > 100000
);
-- 4
SELECT
e.name,
e.salary,
(SELECT AVG(salary) FROM employees WHERE department_id = e.department_id) AS dept_avg_salary
FROM employees e;
-- 5
SELECT e1.name, e1.salary, e1.department_id
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e1.department_id
);
-- 6
SELECT e.name, e.salary, e.department_id
FROM employees e
JOIN (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) AS dept_avg ON e.department_id = dept_avg.department_id
WHERE e.salary > dept_avg.avg_salary;
-- 7
WITH dept_avg AS (
SELECT d.name AS department_name, AVG(e.salary) AS avg_salary, COUNT(e.id) AS employee_count
FROM employees e
JOIN departments d ON e.department_id = d.id
GROUP BY d.name
HAVING AVG(e.salary) > 90000
)
SELECT department_name, employee_count
FROM dept_avg;
-- 8
WITH dept_avg AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT e.name, e.salary, d.name AS department_name
FROM employees e
JOIN dept_avg da ON e.department_id = da.department_id
JOIN departments d ON e.department_id = d.id
WHERE e.salary > da.avg_salary;Ты готова к пятому этапу, если можешь без подсказки объяснить:
SELECT должен возвращать одно значение;EXISTS отличается от IN;JOIN вместо подзапроса.