Кафедра ИТКафедра ИТ
Блог
Обучение
  • О кафедре
  • Направления подготовки
  • Друзья и партнеры
  • Структура кафедры
  • Обращение к студентам
  • Официальный сайт «ВШП»
GitHub
Блог
Обучение
  • О кафедре
  • Направления подготовки
  • Друзья и партнеры
  • Структура кафедры
  • Обращение к студентам
  • Официальный сайт «ВШП»
  • ИТ.03 - 10 - Вложенные запросы: общее понимание

  1. Главная
  2. Учебные материалы
  3. ИТ.03 - Основы проектиро...
  4. Вложенные запросы: общее...

ИТ.03 - 10 - Вложенные запросы: общее понимание

Введение

В предыдущей теме мы научились описывать структуру таблиц. Теперь настало время комбинировать запросы между собой и научиться использовать результат одного SELECT внутри другого. Такие конструкции называют вложенными запросами или подзапросами.

В этой лекции рассмотрим:

  • типы подзапросов и ситуации, где они применяются;
  • правила использования подзапросов в SELECT, WHERE, FROM;
  • приёмы с одной таблицей: вычисления по агрегатам, динамические списки значений и коррелированные проверки.

Что такое вложенный запрос

Подзапрос — это оператор SELECT, заключённый в круглые скобки и встроенный в другой SQL-запрос. Он может появляться в блоках SELECT, WHERE, HAVING, FROM, а также в списке значений для INSERT.

-- Внешний запрос (уровень 0)
SELECT ...
FROM (
  -- Подзапрос уровня 1
  SELECT ...
  FROM (
    -- Подзапрос уровня 2 (самый глубокий)
    SELECT ...
    FROM источники_данных
    WHERE ...
  ) AS level_2
) AS level_1;

Заметка

СУБД вычисляет вложенные запросы от глубины к поверхности: сначала выполняется самый глубокий уровень, затем его результат передаётся подзапросу уровня выше, и так до тех пор пока не сформируется внешний запрос. Такой порядок помогает понимать, какие данные доступны на каждой ступени.

Учебная база данных «Trainer Workouts»

Таблицы
workouts
idathletesession_typedurationcaloriesefforttraining_date
1Анна П.Бег по парку45520medium2024-02-12
2Денис Р.Велопрогулка30360high2024-02-12
3Мария К.Силовая тренировка55610high2024-02-13
4Илья С.Плавание45470medium2024-02-14
5Анна П.Йога50280low2024-02-15
6Денис Р.Бег по парку38450medium2024-02-16
7Мария К.Велопрогулка65700high2024-02-16
8Илья С.Бег по парку30510high2024-02-17
9Павел Т.Бег по парку38540high2024-02-17
Описание

Журнал персонального тренера, который ведёт разных спортсменов: для каждой сессии фиксируются атлет, формат, длительность, калории и дата — идеальная база для практики вложенных запросов.

Особенности:

  • колонки позволяют сравнивать значения через скалярные и коррелированные подзапросы;
  • повторяющиеся занятия и даты удобно использовать в IN/EXISTS;
  • данные компактные, поэтому легко анализировать результаты подзапросов.
Поля и ограничения

Поля

  • workouts
    • id — целочисленный первичный ключ;
    • athlete — ФИО спортсмена;
    • session_type — текстовое описание занятия;
    • duration — длительность в минутах;
    • calories — приблизительные энергозатраты;
    • effort — субъективная оценка нагрузки (low, medium, high);
    • training_date — дата тренировки в формате YYYY-MM-DD.

Ограничения

  • Все столбцы обязательны (NOT NULL);
  • effort ограничен CHECK (effort IN ('low','medium','high'));
  • training_date хранится в ISO-формате YYYY-MM-DD.
Структура
SQL-код

Скачать в виде файла: trainer_workouts_sqlite.sql

CREATE TABLE
  workouts (
    id INTEGER PRIMARY KEY,
    athlete TEXT NOT NULL,
    session_type TEXT NOT NULL,
    duration INTEGER NOT NULL,
    calories INTEGER NOT NULL,
    effort TEXT NOT NULL CHECK (effort IN ('low', 'medium', 'high')),
    training_date TEXT NOT NULL
  )
;

INSERT INTO
  workouts (
    id,
    athlete,
    session_type,
    duration,
    calories,
    effort,
    training_date
  )
VALUES
  (1, 'Анна П.', 'Бег по парку', 45, 520, 'medium', '2024-02-12'),
  (2, 'Денис Р.', 'Велопрогулка', 30, 360, 'high', '2024-02-12'),
  (3, 'Мария К.', 'Силовая тренировка', 55, 610, 'high', '2024-02-13'),
  (4, 'Илья С.', 'Плавание', 45, 470, 'medium', '2024-02-14'),
  (5, 'Анна П.', 'Йога', 50, 280, 'low', '2024-02-15'),
  (6, 'Денис Р.', 'Бег по парку', 38, 450, 'medium', '2024-02-16'),
  (7, 'Мария К.', 'Велопрогулка', 65, 700, 'high', '2024-02-16'),
  (8, 'Илья С.', 'Бег по парку', 30, 510, 'high', '2024-02-17'),
  (9, 'Павел Т.', 'Бег по парку', 38, 540, 'high', '2024-02-17')
;

Чтобы убедиться в исходных данных, выполните простой SELECT:

SELECT * FROM workouts;

Типы подзапросов по возвращаемому результату

  • Скалярный — возвращает одну строку и один столбец. Подходит для сравнений вида duration > (SELECT AVG(...)).
  • Списковый — возвращает одну колонку, но несколько строк. Используется с IN.
  • Табличный — возвращает таблицу с несколькими столбцами. Подставляем во FROM и обязательно задаём псевдоним.
  • Коррелированный — подзапрос, который ссылается на текущую строку внешнего запроса. Выполняется повторно для каждой строки.

Примечание

Подзапрос всегда возвращает набор строк. Если попытаться сравнить его оператором = с одним значением, а внутри окажется несколько строк, СУБД вернёт ошибку. Поэтому важно указывать подходящие операторы (IN, EXISTS) и следить за тем, сколько значений возвращает подзапрос.

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

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

Пример: тренировки длиннее средней

SELECT athlete, session_type, duration
FROM workouts
WHERE duration > (
  SELECT AVG(duration)
  FROM workouts
);

Внутренний SELECT возвращает одно число — среднюю длительность. Внешний запрос получает готовый порог и сравнивает с ним каждую тренировку.

Пример: максимальные калории

SELECT athlete, session_type, calories
FROM workouts
WHERE calories = (
  SELECT MAX(calories)
  FROM workouts
);

Если несколько тренировок сожгли одинаковое максимальное количество калорий, условие вернёт их все без сортировки и LIMIT.

Списковые подзапросы (IN)

Когда подзапрос возвращает одну колонку и несколько строк, мы получаем динамический список значений. Его можно использовать как фильтр.

Заметка

SQLite допускает DISTINCT, GROUP BY, HAVING, ORDER BY, LIMIT внутри подзапроса-списка. Главное — чтобы результатом оставалась одна колонка.

Пример: повторяющиеся длительности

Найдём тренировки, у которых длительность встречается минимум два раза.

SELECT athlete, session_type, duration
FROM workouts
WHERE duration IN (
  SELECT duration
  FROM workouts
  GROUP BY duration
  HAVING COUNT(*) > 1
)
ORDER BY duration, training_date;

Сначала формируем список значений длительностей с повторами, затем подставляем его во внешний WHERE.

Пример: активные форматы тренировок

Менеджер хочет знать, какие типы занятий в среднем сжигают более 500 калорий. Получим такой список и используем его для выборки всех подходящих тренировок.

SELECT athlete, session_type, calories
FROM workouts
WHERE session_type IN (
  SELECT session_type
  FROM workouts
  GROUP BY session_type
  HAVING AVG(calories) > 500
)
ORDER BY session_type, calories DESC;

Внутренний GROUP BY сгруппировал строки по типу занятий и оставил только те форматы, где средние калории выше порога.

Табличные подзапросы (FROM)

Иногда удобно сначала подготовить агрегированные данные, а затем обращаться к ним как к временной таблице.

Пример: сводная статистика по длительности и калориям

Соберём средние значения по каждому уровню усилий, а затем сравним их с общей средней длительностью.

SELECT
  effort,
  ROUND(avg_duration, 1) AS avg_duration,
  ROUND(avg_calories, 1) AS avg_calories,
  ROUND(
    avg_duration - (
      SELECT AVG(duration)
      FROM workouts
    ),
    1
  ) AS duration_delta
FROM (
  SELECT
    effort,
    AVG(duration) AS avg_duration,
    AVG(calories) AS avg_calories
  FROM workouts
  GROUP BY effort
) AS stats
ORDER BY duration_delta DESC;

Совет

ROUND(значение, точность) округляет число до указанного количества знаков после запятой. Если второй аргумент опустить, результат будет округлён до целого.

Теперь подзапрос stats создаёт мини-таблицу со средними значениями по каждому уровню нагрузки. Внешний запрос обращается к этой таблице и для каждой строки вызывает скалярный подзапрос с общей средней длительностью, чтобы вычислить отклонение duration_delta и понять, какие тренировки заметно длиннее средней.

Пример: расчёт интенсивности тренировки

Пусть тренер оценивает эффективность занятия показателем intensity = calories / duration, то есть сколько килокалорий сжигается за одну минуту. Сначала посчитаем интенсивность каждого занятия, а затем оставим только те, что выше средней.

SELECT athlete, session_type, ROUND(intensity, 2) AS intensity
FROM (
  SELECT
    athlete,
    session_type,
    1.0 * calories / duration AS intensity
  FROM workouts
) AS energy
WHERE intensity > (
  SELECT AVG(1.0 * calories / duration)
  FROM workouts
)
ORDER BY intensity DESC;

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

Совет

Запись 1.0 * calories / duration заставляет SQLite выполнить вещественное деление. Если убрать множитель 1.0, произойдёт целочисленное деление и дробная часть интенсивности пропадёт, что исказит результирующие значения.

Коррелированные подзапросы (EXISTS)

Коррелированный подзапрос зависит от текущей строки внешнего запроса. Он полезен, когда нужно проверить наличие «пары» в той же таблице.

Пример: форматы, популярные у нескольких спортсменов

Инфо

EXISTS работает так, будто внутри запроса создаётся мини-таблица inner_w, доступная только для проверки условия. Как только в ней найдено хотя бы одно совпадение, условие становится истинным и строка из внешней таблицы outer_w попадает в результат.

SELECT DISTINCT
  outer_w.session_type,
  (
    SELECT COUNT(DISTINCT inner_w.athlete)
    FROM workouts AS inner_w
    WHERE inner_w.session_type = outer_w.session_type
  ) AS athlete_count
FROM workouts AS outer_w
WHERE EXISTS (
  SELECT 1
  FROM workouts AS inner_w
  WHERE inner_w.session_type = outer_w.session_type
    AND inner_w.athlete <> outer_w.athlete
)
ORDER BY athlete_count DESC, outer_w.session_type;

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

Ограничения и рекомендации

  • Не забывайте проверять тип результата подзапроса. Если нужен один столбец, но вы выбрали SELECT *, СУБД вернёт ошибку «подзапрос возвращает более одной колонки».
  • Чем глубже вложенность, тем сложнее читаемость. Делите большую конструкцию на несколько подзапросов и давайте им осмысленные псевдонимы.
  • Подзапросы позволяют выразить сложную логику, оставаясь в рамках одной таблицы.

Подведем итоги

  • Подзапрос — это обычный SELECT, который выполняется внутри другого запроса и передаёт ему результат.
  • Тип подзапроса определяется количеством столбцов и строк, которые он возвращает.
  • В следующих темах мы расширим этот подход на несколько таблиц.

Самопроверка

Практические задания

Задание 1. Скалярный подзапрос по максимальной длительности

Условие

Найдите дату тренировки с максимальной длительностью и выведите все тренировки, прошедшие в эту же дату (их может быть несколько).

-- Ваш код можете писать тут


Решение
SELECT athlete, session_type, training_date, duration
FROM workouts
WHERE training_date = (
  SELECT training_date
  FROM workouts
  ORDER BY duration DESC
  LIMIT 1
)
ORDER BY athlete;

Задание 2. Списковый подзапрос IN по средним значениям

Условие

Сформируйте список типов тренировок, где среднее количество калорий превышает 500. Затем выведите тренировки только этих типов, отсортировав по убыванию калорий.

-- Ваш код можете писать тут


Решение
SELECT athlete, session_type, calories
FROM workouts
WHERE session_type IN (
  SELECT session_type
  FROM workouts
  GROUP BY session_type
  HAVING AVG(calories) > 500
)
ORDER BY calories DESC;

Задание 3. Табличный подзапрос и отбор по агрегату

Условие

Создайте табличный подзапрос, который собирает по каждому дню сумму длительности (total_duration). Во внешнем запросе оставьте только те дни, где суммарное время превышает 60 минут.

-- Ваш код можете писать тут


Решение
SELECT training_date, total_duration
FROM (
  SELECT training_date, SUM(duration) AS total_duration
  FROM workouts
  GROUP BY training_date
) AS day_stats
WHERE total_duration > 60
ORDER BY training_date;

Задание 4. Коррелированный подзапрос для сравнения с максимумом

Условие

Используя коррелированный подзапрос, найдите тренировки, у которых есть другая тренировка того же спортсмена с большей длительностью. Выведите имя спортсмена, тип занятия и длительность такой «не самой длинной» тренировки.

-- Ваш код можете писать тут


Решение
SELECT outer_w.athlete, outer_w.session_type, outer_w.duration
FROM workouts AS outer_w
WHERE EXISTS (
  SELECT 1
  FROM workouts AS inner_w
  WHERE inner_w.athlete = outer_w.athlete
    AND inner_w.duration > outer_w.duration
)
ORDER BY outer_w.athlete, outer_w.duration;
Последнее обновление: 15.12.2025, 01:37
Предыдущая
ИТ.03 - 09 - Основные типы данных SQLite. Работа с датой и временем. Создание и удаление таблиц
Следующая
ИТ.03 - 11 - Модели БД и способы их представления. ER-диаграммы
© Кафедра информационных технологий ЧУВО «ВШП», 2025. Версия: 0.33.2
Материалы доступны в соответствии с лицензией: