Основы языка SQL

Оператор SELECT. Часть 2

Рассматриваются продвинутые возможности запросов SELECT: вложенные подзапросы в условиях WHERE и на уровне FROM для создания виртуальных таблиц, ограничение объёма возвращаемых строк (TOP, FIRST…SKIP, ROWS…TO), явное преобразование типов функцией CAST. Значительное внимание уделено встроенным функциям работы с датой и временем, а также всестороннему разбору поведения неопределённых значений NULL — правилам взаимодействия в выражениях, логических операциях и агрегатных функциях. Логика изложения движется от механизма вложенных запросов к практическим средствам контроля выборки и надёжной обработке данных.

Основные мысли

В результате изучения лекции слушатель будет способен:
1. Объяснять синтаксис и назначение вложенных подзапросов в разделах WHERE и FROM.
2. Сравнивать два способа фильтрации по агрегированным данным — через подзапрос в WHERE и через виртуальную таблицу — и выбирать подходящий в зависимости от необходимости вывода вычисляемого поля.
3. Применять операторы TOP, FIRST … SKIP, ROWS … TO для ограничения числа записей и пропуска строк в результирующей выборке.
4. Использовать функцию CAST для преобразования целочисленного столбца к типу с плавающей точкой при вычислении средних значений, чтобы избежать целочисленного деления.
5. Извлекать компоненты даты и времени (год, месяц, день, часы) с помощью встроенных функций, таких как YEAR, MONTH, DATEPART.
6. Вычислять интервал между двумя датами в годах или других единицах с помощью DATEDIFF.
7. Прогнозировать результат любого выражения, содержащего NULL, включая поведение логических операторов AND/OR по правилам сокращённого вычисления.
8. Корректно применять конструкции IS DISTINCT FROM, IS NULL, IS NOT NULL и COALESCE для безопасного сравнения и замены NULL в условиях запроса.
9. Выявлять потенциальные логические ошибки в запросах, вызванные особенностями сравнения с NULL, и предлагать исправленные варианты.
10. Разрабатывать запросы с производными таблицами, возвращающие вычисляемые столбцы (например, средний балл) наряду с полями из основной таблицы.
Показывать лекцию целиком
Краткое изложение

Продвинутые конструкции SELECT: подзапросы, ограничения, типы и NULL

Вложенные подзапросы в условии WHERE

Условие фильтрации в операторе WHERE может содержать другой запрос — подзапрос. Например, чтобы найти всех самых старших студентов, вместо подстановки конкретного числа в Age = 18 можно написать:

sql
SELECT FirstName, LastName
FROM Students
WHERE Age = (SELECT MAX(Age) FROM Students);

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

Возможен вариант, когда само условие целиком выражено подзапросом без явного указания столбца. Например, нужно получить имена и фамилии студентов, чей средний балл превышает 3.5. Средний балл отсутствует в таблице Students, он вычисляется по связанной таблице StudentResults. Запрос можно построить так:

sql
SELECT FirstName, LastName
FROM Students
WHERE (SELECT AVG(Mark)
FROM StudentResults
WHERE StudentResults.StudentID = Students.StudentID) > 3.5;

Фильтрация выполняется по агрегированному значению из другой таблицы. Глубина вложенности подзапросов может быть любой.

Подзапросы в разделе FROM (виртуальные таблицы)

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

Предположим, нужно вернуть имя, фамилию и средний балл каждого студента, у которого средний балл больше 3.5. Создадим виртуальную таблицу с идентификатором студента и его средним баллом, затем соединим её с основной таблицей:

sql
SELECT st.FirstName, st.LastName, vt.SMark
FROM Students AS st
JOIN (SELECT StudentID AS SID, AVG(Mark) AS SMark
FROM StudentResults
GROUP BY StudentID) AS vt
ON st.StudentID = vt.SID
WHERE vt.SMark > 3.5;

Здесь (SELECT …) AS vt — это производная (виртуальная) таблица, которой мы присвоили псевдоним vt. Она содержит средний балл SMark, и мы одновременно фильтруем по нему и выводим в финальном наборе. В предыдущем подходе с подзапросом в WHERE вывести SMark было невозможно, потому что запрос обращался только к Students.

Таким образом, выбор между WHERE и FROM определяется потребностью: нужен только фильтр — используем WHERE, нужно и значение — формируем виртуальную таблицу.

Ограничение количества возвращаемых строк

Для управления объёмом выборки используются операторы, лимитирующие число строк:
TOP (SQL Server) – указывает, сколько первых строк вернуть. SELECT TOP 6 * FROM Students возвращает не более шести записей.
FIRST … SKIP – в стандарте SQL позволяют задать количество строк и смещение. SELECT FIRST 10 SKIP 2 * FROM Students пропускает первые две строки и возвращает следующие десять.
ROWS … TO – прямое указание диапазона номеров строк. ROWS 2 TO 3 возвращает вторую и третью строки.

Конкретный синтаксис зависит от реализации СУБД, но логика остаётся общей: ограничить выдачу и/или задать смещение.

Преобразование типов данных: функция CAST

Агрегатные функции, например AVG, при работе с целочисленными столбцами могут давать неточный результат из-за целочисленного деления. Если столбец Mark имеет тип INTEGER, то AVG(Mark) вернёт целое число: для оценок 1 и 2 среднее станет равным 2, а не 1.5. Чтобы получить истинное среднее, необходимо преобразовать значения к типу с плавающей точкой.

Функция CAST выполняет явное приведение типа:

sql
SELECT StudentID, AVG(CAST(Mark AS FLOAT)) AS AvgMark
FROM StudentResults
GROUP BY StudentID;

Синтаксис: CAST(выражение AS целевой_тип). После приведения к FLOAT деление становится вещественным, и AVG вернёт 1.5.

Функции для работы с датой и временем

Стандарт SQL требует наличия встроенных функций для манипуляций с датами, чтобы избавить разработчика от ручного разбора строк. Примеры для SQL Server (в других СУБД могут быть аналоги):
Получение текущих даты и времени: SYSDATETIME() возвращает текущий момент на сервере.
Извлечение компонентов:
o MONTH(дата) – месяц (число);
o YEAR(дата) – год;
o DATEPART(часть, дата) – универсальная функция: DATEPART(hour, SYSDATETIME()) извлекает часы.
Разница между датами: DATEDIFF(year, '2015-10-21', SYSDATETIME()) вычисляет количество полных лет между двумя датами. Обратите внимание на формат строки: в некоторых региональных настройках сначала идёт месяц, потом день.

Эти функции позволяют легко решать задачи вроде вычисления возраста по дате рождения.

Особенности работы с NULL

NULL обозначает отсутствие значения, это не ноль и не пустая строка. Работа с NULL подчиняется строгим правилам, влияющим на все операции.

Поведение в выражениях

• Любая арифметическая операция или сравнение с участием NULL даёт NULL: 1 + NULL = NULL, 1 = NULL → NULL, NULL = NULL → NULL.
• Логические операторы используют сокращённое вычисление:
o NULL OR TRUE → TRUE (достаточно одной истины).
o NULL AND FALSE → FALSE (достаточно одной лжи).
o NULL OR FALSE → NULL.
o NULL AND TRUE → NULL.
• Проверка NOT NULL также возвращает NULL.

Влияние на агрегатные функции

SUM, AVG, MAX, MIN игнорируют строки с NULL в соответствующем столбце — как будто этих строк нет.
COUNT(*) учитывает строку целиком, поэтому она будет посчитана даже при наличии NULL в каких-то полях.
COUNT(столбец) не засчитывает строки, где этот столбец равен NULL.

Корректные проверки с NULL

Обычные операторы сравнения =, != всегда возвращают NULL, если один из операндов NULL. Это может привести к неожиданным логическим ошибкам. Например, ветвление:

sql
IF A != B
результат = 'Not Equal'
ELSE
результат = 'Equal'

Если A = 1, а B = NULL, то A != B вернёт NULL, условие не сработает как истина, и выполнится ELSE, выдав Equal, что неверно.

Для безопасного сравнения неравенства с учётом NULL применяется конструкция IS DISTINCT FROM:

sql
IF A IS DISTINCT FROM B
результат = 'Not Equal'
ELSE
результат = 'Equal'

Здесь 1 IS DISTINCT FROM NULL даст TRUE, а NULL IS DISTINCT FROM NULL — FALSE.

Другие полезные конструкции:
IS NULL / IS NOT NULL — прямая проверка на NULL.
COALESCE(выражение1, выражение2, …) — возвращает первый не-NULL аргумент. Удобна для замены NULL значением по умолчанию: COALESCE(MiddleName, '—').

NULL может появляться не только в данных, но и в результате подзапросов, не вернувших ни одной строки, поэтому его поведение необходимо учитывать при построении условий WHERE, HAVING и логики ветвлений.

Краткие итоги

Центральной темой является расширение арсенала разработчика при построении запросов SELECT — от использования вложенных запросов до надёжной обработки неопределённых значений. Изложение раскрывает два фундаментальных способа интеграции агрегированных данных: помещение подзапроса в условие WHERE, позволяющее фильтровать строки по вычисляемому скаляру, и размещение подзапроса во FROM в роли виртуальной таблицы, благодаря которому вычисляемый показатель становится частью результирующего набора и доступен для дальнейшего соединения и вывода. Практическая ценность этого разграничения очевидна: когда требуется лишь отсеять записи по среднему баллу, достаточно подзапроса в WHERE; когда же необходимо показать и сам балл, единственным выходом остаётся формирование производной таблицы.

Параллельно рассматриваются средства управления размером ответа: синтаксические вариации TOP, FIRST с пропуском строк и нумерация ROWS. Хотя конкретные формы различаются от СУБД к СУБД, стоящая за ними задача — выдача первых N записей или записей со смещением — едина.

Отдельный акцент сделан на детерминированности вычислений. Преобразование типов через CAST предстаёт не просто синтаксической деталью, а необходимым приёмом для получения точного среднего из целых чисел, что прямо влияет на достоверность аналитики. Аналогично, функции даты и времени освобождают от ручного разбора, стандартизируя извлечение года, месяца, разницы дат, и тем самым снижают риск ошибок локализации.

Кульминацией становится анализ NULL. Раскрывается его коварство: любое арифметическое или сравнительное взаимодействие с NULL даёт NULL, что при неаккуратном использовании приводит к логически противоречивым исходам в условиях. Правила сокращённого вычисления OR и AND смягчают картину лишь частично. Исключение NULL из агрегатных функций (кроме COUNT(*)) объясняет, почему результаты одних и тех же запросов могут не совпадать с интуитивными ожиданиями. Выходом служат инструменты безопасного сравнения — IS DISTINCT FROM, явные проверки IS NULL и подстановки через COALESCE. Совокупность этих приёмов формирует культуру защитного программирования на SQL, где неучтённый NULL перестаёт быть источником скрытых дефектов и превращается в управляемый элемент модели данных.
Вложенные подзапросы

Подзапрос можно поместить прямо в условие WHERE. Например, чтобы выбрать студентов с максимальным возрастом, сравниваем Age с результатом (SELECT MAX(Age) FROM Students). Подзапрос должен возвращать одиночное значение. Возможен и вариант, когда всё условие WHERE представляет собой подзапрос: WHERE (SELECT AVG(Mark) FROM StudentResults WHERE StudentID = Students.StudentID) > 3.5. Так фильтруются строки по агрегированным данным из других таблиц.

Виртуальные таблицы во FROM

Подзапрос в разделе FROM создаёт производную таблицу, которую можно именовать через псевдоним (AS vt). Это позволяет не только фильтровать по агрегату, но и включать агрегированное значение в итоговый набор. Сравните:
• Через WHERE: SELECT FirstName, LastName FROM Students WHERE (подзапрос_среднего) > 3.5 — средний балл не виден в выводе.
• Через FROM: SELECT st.FirstName, st.LastName, vt.SMark FROM Students AS st JOIN (SELECT StudentID, AVG(Mark) AS SMark FROM StudentResults GROUP BY StudentID) AS vt ON st.StudentID = vt.StudentID WHERE vt.SMark > 3.5 — возвращает и имена, и балл.
Выбор подхода зависит от того, нужен ли сам вычисленный показатель в результате.

Ограничение числа строк

TOP N (SQL Server) – первые N строк.
FIRST N SKIP M – пропустить M, вернуть следующие N.
ROWS n TO m – строки с n-й по m-ю.

Конкретные ключевые слова зависят от СУБД, но задача одна: лимитирование и смещение.

Преобразование типов: CAST

Функция CAST(выражение AS тип) принудительно меняет тип. Критически важна при вычислении средних по целочисленным столбцам: AVG(CAST(Mark AS FLOAT)) даст точное 1.5 вместо 2. Без CAST целочисленное деление приводит к округлению.

Функции даты и времени

SYSDATETIME() – текущая дата и время.
MONTH(), YEAR(), DATEPART(part, date) – извлечение компонентов.
DATEDIFF(unit, date1, date2) – разница (например, в годах).

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

Обработка NULL

NULL – отсутствие значения. Правила:
• Арифметика и сравнения с NULL дают NULL (1 + NULL = NULL, NULL = NULL → NULL).
• Логические операторы используют короткое вычисление: NULL OR TRUE = TRUE, NULL AND FALSE = FALSE. Остальные комбинации с NULL возвращают NULL.
• Агрегатные функции (SUM, AVG, MAX, MIN) игнорируют NULL. COUNT(*) считает строку целиком, COUNT(column) – только не-NULL значения.
• Сравнения =, != с NULL всегда NULL, поэтому IF A != B ... ELSE ... при B=NULL ложно интерпретирует неравенство как равенство.
• Безопасное сравнение: A IS DISTINCT FROM B – истина, если значения различны или одно из них NULL (но если оба NULL, то ложь).
• Явные проверки: IS NULL, IS NOT NULL.
• Замена NULL: COALESCE(expr1, expr2, ...) возвращает первый не-NULL аргумент.

Игнорирование этих особенностей ведёт к неверным фильтрациям и логическим ошибкам, поэтому каждый запрос должен учитывать присутствие NULL.

Выводы

1. Подзапрос в WHERE позволяет сравнивать строки с агрегированным результатом другого запроса, например с максимальным значением.
2. Условие WHERE может быть полностью заменено коррелированным подзапросом, возвращающим скаляр для каждой строки внешнего запроса.
3. Подзапрос в разделе FROM создаёт виртуальную таблицу, к которой можно присоединяться и чьи столбцы доступны для вывода.
4. Виртуальные таблицы необходимы, когда нужно одновременно фильтровать по агрегату и показывать его значение в итоговой выборке.
5. Операторы TOP, FIRST … SKIP, ROWS … TO управляют количеством и смещением строк; их синтаксис зависит от конкретной СУБД.
6. Функция CAST преобразует тип столбца, позволяя избежать целочисленного деления и получить точное среднее (например, 1.5 вместо 2).
7. Встроенные функции SYSDATETIME, MONTH, YEAR, DATEPART и DATEDIFF упрощают извлечение и расчёт временных компонентов.
8. NULL — это отсутствие значения; любая операция с NULL (кроме сокращённых логических OR TRUE / AND FALSE) даёт NULL.
9. Агрегатные функции SUM, AVG, MAX, MIN игнорируют NULL, в то время как COUNT(*) учитывает всю строку независимо от NULL в столбцах.
10. Сравнение через = или != с NULL всегда возвращает NULL; для корректного результата нужно использовать IS DISTINCT FROM.
11. Функция COALESCE заменяет NULL на указанное значение по умолчанию, предотвращая нежелательные последствия.
12. Игнорирование NULL в условиях WHERE без специальных проверок может привести к ошибочным выводам о равенстве или неравенстве строк.

Вопросы для самопроверки

1. Чем различаются результаты подзапроса в WHERE и подзапроса во FROM при необходимости вернуть одновременно имя и средний балл студента?
2. Как с помощью виртуальной таблицы получить список студентов, у которых средняя оценка выше 4, с отображением самой оценки?
3. Зачем нужны операторы FIRST и SKIP, если уже есть TOP, и в каких СУБД они применяются?
4. Почему AVG(CAST(Mark AS FLOAT)) даёт более точный результат, чем AVG(Mark), при целочисленном столбце Mark?
5. Какие встроенные функции позволяют узнать текущую дату и время на сервере и извлечь из них год, месяц и час?
6. Что вернёт выражение 5 + NULL и как это повлияет на результат агрегатной функции SUM, если в столбце есть NULL?
7. Объясните, почему условие WHERE Age != NULL не работает для поиска строк с известным возрастом, и как нужно переписать запрос.
8. Как ведёт себя агрегатная функция COUNT(столбец) при наличии NULL и в чём её отличие от COUNT(*)?
9. В каком случае NULL OR FALSE вернёт NULL, а в каком — TRUE, и почему?
10. Для чего используется конструкция IS DISTINCT FROM и как она помогает избежать логической ошибки сравнения с NULL?
11. Каким образом COALESCE помогает вывести средний балл, заменяя NULL на 0, и к каким последствиям это может привести?
12. Приведите пример запроса, где наличие NULL в подзапросе, используемом в условии WHERE, может привести к пропуску нужных строк.
Вернуться к учебному плану