Перейти к содержанию

Подзапросы (Subqueries)

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

📋 Типы подзапросов

1. Подзапрос в WHERE

Используется для фильтрации данных на основе результатов другого запроса.

-- Найти сотрудников с зарплатой выше средней
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- Найти продукты, которые заказывали
SELECT product_name
FROM products
WHERE id IN (SELECT DISTINCT product_id FROM order_items);

2. Подзапрос в SELECT

Возвращает значение для каждого результата основного запроса.

-- Показать зарплату сотрудника и среднюю по компании
SELECT 
    name,
    salary,
    (SELECT AVG(salary) FROM employees) AS avg_salary,
    salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg
FROM employees;

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

Используется как виртуальная таблица.

-- Найти департаменты со средней зарплатой выше общей средней
SELECT dept_id, avg_salary
FROM (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY dept_id
) AS dept_stats
WHERE avg_salary > (SELECT AVG(salary) FROM employees);

🔍 Операторы с подзапросами

IN / NOT IN

Проверяет, находится ли значение в списке результатов подзапроса.

-- Сотрудники из определённых департаментов
SELECT name, dept_id
FROM employees
WHERE dept_id IN (10, 20, 30);

-- Динамический список
SELECT name
FROM employees
WHERE dept_id IN (SELECT id FROM departments WHERE location = 'Moscow');

-- Исключение
SELECT name
FROM employees
WHERE dept_id NOT IN (SELECT dept_id FROM employees WHERE salary > 100000);

EXISTS / NOT EXISTS

Проверяет существование строк, возвращаемых подзапросом.

-- Клиенты, у которых есть заказы
SELECT customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

-- Клиенты без заказов
SELECT customer_name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

ANY / ALL

Сравнение с любым или всеми значениями из подзапроса.

-- Зарплата больше минимальной в любом департаменте
SELECT name, salary
FROM employees
WHERE salary > ANY (
    SELECT MIN(salary) FROM employees GROUP BY dept_id
);

-- Зарплата больше максимальной во всех департаментах
SELECT name, salary
FROM employees
WHERE salary > ALL (
    SELECT MAX(salary) FROM employees GROUP BY dept_id
);

💡 Практические примеры

Пример 1: Топ продуктов по продажам

SELECT 
    p.product_name,
    p.category,
    (SELECT SUM(oi.quantity) FROM order_items oi WHERE oi.product_id = p.id) AS total_sold
FROM products p
ORDER BY total_sold DESC
LIMIT 10;

Пример 2: Сотрудники с максимальной зарплатой в каждом департаменте

SELECT e.name, e.dept_id, e.salary
FROM employees e
WHERE e.salary = (
    SELECT MAX(e2.salary)
    FROM employees e2
    WHERE e2.dept_id = e.dept_id
);

Пример 3: Клиенты с общим объёмом заказов выше среднего

SELECT 
    c.customer_name,
    total_spent
FROM customers c
INNER JOIN (
    SELECT 
        customer_id,
        SUM(total_amount) AS total_spent
    FROM orders
    GROUP BY customer_id
) AS customer_totals ON c.id = customer_totals.customer_id
WHERE total_spent > (
    SELECT AVG(total_spent) FROM (
        SELECT SUM(total_amount) AS total_spent
        FROM orders
        GROUP BY customer_id
    ) AS all_totals
);

Пример 4: Обновление данных на основе подзапроса

-- Увеличить зарплату сотрудникам с зарплатой ниже средней
UPDATE employees
SET salary = salary * 1.1
WHERE salary < (SELECT AVG(salary) FROM employees);

Пример 5: Удаление дубликатов

DELETE FROM orders o1
WHERE EXISTS (
    SELECT 1 FROM orders o2
    WHERE o2.customer_id = o1.customer_id
      AND o2.order_date = o1.order_date
      AND o2.id < o1.id
);

⚠️ Частые ошибки

Ошибка Проблема Решение
Подзапрос возвращает несколько строк для одиночного сравнения Ошибка SQL Используйте IN, EXISTS или ограничьте результат
NULL в NOT IN Пустой результат Обрабатывайте NULL явно или используйте NOT EXISTS
Непроизводительные коррелированные подзапросы Медленное выполнение Используйте JOIN или CTE
Глубокая вложенность Сложность чтения Разбейте на CTE

🎯 Коррелированные vs Некоррелированные подзапросы

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

Выполняется один раз независимо от внешнего запроса.

-- Выполняется один раз
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Коррелированный подзапрос

Выполняется для каждой строки внешнего запроса (может быть медленным).

-- Выполняется для каждой строки
SELECT name, salary, dept_id
FROM employees e
WHERE salary > (
    SELECT AVG(salary) 
    FROM employees 
    WHERE dept_id = e.dept_id
);

Оптимизация через JOIN:

-- Более эффективная версия
SELECT e.name, e.salary, e.dept_id
FROM employees e
INNER JOIN (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY dept_id
) AS dept_avg ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.avg_salary;

📊 Сравнение подходов

Задача Подзапрос JOIN CTE
Простая фильтрация ✅ Отлично ⚠️ Избыточно ⚠️ Избыточно
Проверка существования ✅ EXISTS ⚠️ DISTINCT ✅ CTE
Агрегация для сравнения ✅ Хорошо ✅ Отлично ✅ Отлично
Множественные использования ❌ Плохо ⚠️ Повторение ✅ Отлично
Читаемость ⚠️ Средняя ✅ Хорошая ✅ Отличная

➡️ Что дальше?

Изучите оконные функции для ещё более мощной аналитики!