Язык SQL. Формирование запросов к базе данных

19.04.2024

Как вы помните, различные элементы определения окна (секционирование, упорядочение и кадрирование) по сути являются различными вариантами фильтрации. Существуют другие потребности в фильтрации, которые эти определения не в состоянии удовлетворить. Некоторые из этих потребностей удается удовлетворить с помощью предложения FILTER , которое не было реализовано в SQL Server 2012. Есть также попытки решить эту проблему за счет внесения предложений по расширению стандарта, которые, я надеюсь, так или иначе появятся в стандарте и SQL Server.

Начну с предложения FILTER. В стандарте определено, что в функциях агрегирования это предложение позволяет фильтровать набор строк, к которому применяется агрегирование, с использованием предиката, формат этого предложения выглядит так:

В качестве примера приведу запрос, вычисляющий разницу между текущим количеством и среднемесячным количеством для сотрудника до текущей даты (не месяца текущей строки):

SQL Server 2012 пока не поддерживает предложение FILTER. Честно говоря, я не знаю СУБД, которая поддерживала его. Если вам нужна такая возможность, существует довольно простое альтернативное решение - использовать в качестве входных данных для функции агрегирования выражение CASE :

<функция агрегирования>(CASE WHEN <условие поиска> THEN <входное выражение> END)

Вот полный запрос, который решает ту же задачу:

SELECT empid, ordermonth, qty, qty - AVG(CASE WHEN ordermonth <= DATEADD(month, -3, CURRENT_TIMESTAMP) THEN qty END) OVER(PARTITION BY empid) AS diff FROM Sales.EmpOrders;

Чего все еще не хватает в стандарте (начиная с версии SQL 2008) и SQL Server 2012, так это возможности ссылаться на элементы текущей строки для целей фильтрации. Это можно было бы применять в предложении FILTER, в альтернативном решении с использованием выражения CASE, а также в других случаях, в которых нужна фильтрация.

Для демонстрации этой потребности представьте на секундочку, что на элемент текущей строки можно ссылаться с помощью префикса $current_row. А теперь представим себе, что нужно написать запрос представления Sales.OrderValues, который бы вычислял для каждого заказа разницу между значением текущего заказа и средним значением для определенного сотрудника для всех клиентов кроме того клиента, которому принадлежит этот заказ. Эта задача решается следующим запросом с предложением FILTER:

В качестве альтернативы можно воспользоваться выражением CASE:

Не работает в T-SQL SELECT orderid, orderdate, empid, custid, val, val - AVG(CASE WHEN custid <> $current_row.custid THEN val END) OVER(PARTITION BY empid) AS diff FROM Sales.OrderValues;

Ещё раз напомню, что это всего лишь мои выдумки для иллюстрации того, чего не хватает в стандарте языка, поэтому, как говорится, «не пытайтесь повторить это у себя дома».

Предложение по улучшению

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

Паттерны в последовательностях строк определяются с применением семантики, похожей на регулярные выражения. Этот механизм может применяться для определения табличного выражения, а также для фильтрации строк в определении окна. Он также может использоваться в технологиях потоковой передачи данных, например с StreamInsight в SQL Server, а также в запросах, которые работают с неперемещаемыми данными. Вот ссылка на предоставленный для всеобщего доступа документ: http://www.softwareworkshop.com/h2/SQL-RPR-review-paper.pdf . Прежде чем читать этот документ, я предлагаю освободить голову от лишних мыслей и хорошенько взбодриться кофе. Это непросто чтение, но идея исключительно интересна и я надеюсь, что она пробьет себе путь в стандарт SQL и будет использоваться не только для данных в движении, но и для неактивных данных.

Ключевое слово DISTINCT в функциях агрегирования

SQL Server 2012 не поддерживает параметр DISTINCT в оконных функциях агрегирования. Представьте, что вам нужно запрашивать представление Sales.OrderValues и получить для каждого заказа число конкретных клиентов, с которыми работал текущий сотрудник с начала и до текущей даты. Вам нужно выполнить такой запрос:

Не работает в T-SQL SELECT empid, orderdate, orderid, val, COUNT(DISTINCT custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM Sales.OrderValues;

Но поскольку этот запрос не поддерживается, нужно искать обходное решение. Один из вариантов - прибегнуть к помощи функции ROW_NUMBER. Я расскажу о ней подробнее чуть попозже, а пока достаточно будет сказать, что она возвращает уникальное целое значение для каждой строки секции, начиная с единицы и с шагом 1, в соответствии с определением упорядочения в окне. С помощью функции ROW_NUMBER можно назначить строкам номера, секционированные по empid и custid и упорядоченные по orderdate. Это означает, что строки с номером 1 относятся к первому случаю работы сотрудника с данным клиентом при упорядочении заказов по датам. Используя выражение CASE можно вернуть значение custid, только если номер строки равен 1, а в противном случае вернуть NULL. Вот запрос, реализующий описанную логику, с результатом его работы:

SELECT empid, orderdate, orderid, custid, val, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.OrderValues;

Заметьте, что для каждого сотрудника возвращается только первое значение custid при условии упорядочения по дате, а для последующих значений возвращаются NULL. Следующий шаг заключается в определении обобщенного табличного значения (CTE) на основе предыдущего запроса, а затем применении агрегирования текущего числа строк к результату выражения CASE:

WITH C AS (SELECT empid, orderdate, orderid, custid, val, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.OrderValues) SELECT empid, orderdate, orderid, val, COUNT(distinct_custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C;

Вложенные агрегаты

На данный момент вы знаете, что есть групповые и оконные агрегаты. Как уже говорилось, функции при этом используются одинаковые, но контекст разный. Групповые агрегаты работают на основе групп строк, определенных предложением GROUP BY и возвращают одно значение на группу. Оконные агрегаты действуют на основе окон строк и возвращают одно значение для каждой строки в базовом запросе. Вспомните рассказ о логической обработке запросов из статьи Запросы . Напомню порядок, в котором в соответствии с концепцией должны обрабатываться различные предложения запросов:

Групповые агрегаты используются, когда запрос является групповым, и они разрешены в фазах, которые обрабатываются после определения групп, а именно, начиная с фазы 4 и далее. Помните, что в результате запроса каждая группа представлена только одной строкой. Оконные агрегаты разрешены, начиная с фазы 5 и последующих, потому что они работают на основе строк базового запроса - после фазы HAVING.

Агрегаты двух типов - даже при том, что они используют те же имена функций и логику вычислений - работают в разных контекстах. Вернусь к важному моменту, который я хотел отметить в этом разделе: что, если нужно просуммировать значение, сгруппированное по идентификатору сотрудника, и одновременно агрегировать все эти суммы по всем сотрудникам?

Это совершенно легальный, но на первый взгляд странный подход - применять оконный агрегат к окну, содержащему строки с атрибутами, полученными с применением групповых агрегатов. Я сказал «странный», потому что на первый взгляд выражение SUM(SUM(val)) в запросе выглядит неуместным. Но оно имеет право на существование. Посмотрите на запрос, который решает поставленную задачу:

Групповой агрегат SUM(val) вычисляет общую сумму цены всех заказов для каждого сотрудника. Это означает, что в результате базового запроса есть строка для каждого сотрудника с этой общей суммой. После этого оконный агрегат вычисляет сумму сумм для отдельных сотрудников, иначе говоря вычисляет итоговую сумму, и делит групповой агрегат на оконный, чтобы вычислить в процентах долю каждого сотрудника и итоговой цифре.

Будет проще увидеть логику вложенных агрегатов, если анализ запроса разбить да два этапа. На первом вычисляется групповой агрегат:

SELECT empid, SUM(val) AS emptotal FROM Sales.OrderValues GROUP BY empid;

Этот результат можно считать начальной точкой для дальнейшего оконной агрегации. Таким образом, можно применить агрегат SUM к выражению, представленному псевдонимом emptotal. К сожалению нельзя применить его непосредственно к псевдониму по причинам, изложенным ранее (помните принцип «все сразу»?). Но его можно применить к базовому выражению так: SUM(SUM(val)) OVER(...) и можно считать, что это SUM(emptotal) OVER(...). Таким образом получаем следующее:

SELECT empid, SUM(val) AS emptotal, SUM(val) / SUM(SUM(val)) OVER() * 100. AS pct FROM Sales.OrderValues GROUP BY empid;

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

WITH C AS (SELECT empid, SUM(val) AS emptotal FROM Sales.OrderValues GROUP BY empid) SELECT empid, emptotal, emptotal / SUM(emptotal) OVER() * 100. AS pct FROM C;

Посмотрим на другой пример сложностей, связанных с оконными и групповыми функциями. Следующая задача является вариацией запроса, приведенного в предыдущей статье. Надо создать запрос таблицы Sales.Orders, возникающий для каждого сотрудника точные даты заказов и точные имена клиентов, с которыми работал текущий сотрудник с начала и до текущей даты. Первая попытка реализации:

WITH C AS (SELECT empid, orderdate, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.Orders) SELECT empid, orderdate, COUNT(distinct_custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C GROUP BY empid, orderdate;

Но при выполнении запроса вы получаете следующую ошибку:

Column "C.distinct_custid" is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

Внешняя функция COUNT является не групповыми, а оконным агрегатом. Таким образом она может работать только на основе элементов, которые действительны, только если определяются самостоятельно, то есть не как входные данные для оконного агрегата. Теперь на секундочку забудьте об оконном агрегате и скажите, корректен ли следующий запрос (для краткости определение CTE опущено)?

SELECT empid, orderdate, distinct_custid FROM C GROUP BY empid, orderdate;

Ясно, что ответ отрицательный. Атрибут distinct_custid в списке SELECT неверен, потому что не содержится ни в агрегирующей функции, ни в предложении GROUP BY, и примерно об этом говорится в сообщении об ошибке. Что вам нужно сделать, так это применить оконный агрегат SUM с кадром, реализующим принцип нарастающего итога, к групповому агрегату COUNT, который считает конкретные вхождения:

WITH C AS (SELECT empid, orderdate, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.Orders) SELECT empid, orderdate, SUM(COUNT(distinct_custid)) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C GROUP BY empid, orderdate;

Ясно, что это не единственный способ получения нужного результата, но моей задачей было проиллюстрировать принцип вложения групповых агрегатов в оконные. Как вы помните, в соответствии с порядком логической обработки запросов оконные функции обрабатываются на этапе SELECT или ORDER BY, то есть после GROUP BY. По этой причине групповые агрегаты видны в качестве входных выражений оконных агрегатов. Также вспомните, что если код становится сложным для понимания, всегда можно задействовать табличные выражения, чтобы избежать прямого сложения функций и повысить читабельность кода.

В SQL добавлены дополнительные функции, которые позволяют вычислять обобщенные групповые значения. Для применения агрегатных функций предполагается предварительная операция группировки. В чем состоит суть операции группировки? При группировке все множество кортежей отношения разбивается на группы, в которых собираются кортежи, имеющие одинаковые значения атрибутов, которые заданы в списке группировки.

Например, сгруппируем отношение R1 по значению столбца Дисциплина. Мы получим 4 группы, для которых можем вычислить некоторые групповые значения, например количество кортежей в группе, максимальное или минимальное значение столбца Оценка.

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

Таблица 5.7. Агрегатные функции

R1
ФИО Дисциплина Оценка
Группа 1 Петров Ф. И. Базы данных
Сидоров К. А. Базы данных
Миронов А. В. Базы данных
Степанова К. Е. Базы данных
Крылова Т. С. Базы данных
Владимиров В. А. Базы данных
Группа 2 Сидоров К. А. Теория информации
Степанова К. Е. Теория информации
Крылова Т. С. Теория информации
Миронов А. В. Теория информации Null
Группа 3 Трофимов П. А. Сети и телекоммуникации
Иванова Е. А. Сети и телекоммуникации
Уткина Н. В. Сети и телекоммуникации
Группа 4 Владимиров В. А. Английский язык
Трофимов П. А. Английский язык
Иванова Е. А. Английский язык
Петров Ф. И. Английский язык i

Агрегатные функции используются подобно именам полей в операторе SELECT, но с одним исключением: они берут имя поля как аргумент. С функциями SUM и AVG могут использоваться только числовые поля. С функциями COUNT, MAX и MIN могут использоваться как числовые, так и символьные поля. При использовании с символьными полями МАХ и MIN будут транслировать их в эквивалент ASCII кода и обрабатывать в алфавитном порядке. Некоторые СУБД позволяют использовать вложенные агрегаты, но это является отклонением от стандарта ANSI со всеми вытекающими отсюда последствиями.



Например, можно вычислить количество студентов, сдававших экзамены по каждой дисциплине. Для этого надо выполнить запрос с группировкой по полю «Дисциплина» и вывести в качестве результата название дисциплины и количество строк в группе по данной дисциплине. Применение символа * в качестве аргумента функции COUNT означает подсчет всех строк в группе.

SELECT R1.Дисциплина. СОUNТ(*)

GROUP BY R1 Дисциплина

Результат:

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

SELECT R1.Дисциплина. COUNT(*)

FROM R1 WHERE R1.

Оценка IS NOT NULL

GROUP BY Rl.Дисциплина

Получим результат:

В этом случае строка со студентом

Миронов А, В. Теория информации Null

не попадет в набор кортежей перед группировкой, поэтому количество кортежей в группе для дисциплины «Теория информации» будет на 1 меньше.

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



Обратившись снова к базе данных «Сессия» (таблицы Rl, R2, R3), найдем количество успешно сданных экзаменов:

WHERE Оценка> 2:

Это, конечно, отличается от выбора поля, поскольку всегда возвращается одиночное значение, независимо от того, сколько строк находится в таблице. Аргументом агрегатных функций могут быть отдельные столбцы таблиц. Но для того, чтобы вычислить, например, количество различных значений некоторого столбца в группе, необходимо применить ключевое слово DISTINCT совместно с именем столбца. Вычислим количество различных оценок, полученных по каждой дисциплине:

SELECT Rl.Дисциплина.

COUNT(DISTINCT R1.Оценка)

WHERE R1.Оценка IS NOT NULL

GROUP BY Rl.Дисциплина

Результат:

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

SELECT R2.Группа. R1.Дисциплина. COUNT(*), АVР(Оценка)

WHERE Rl.ФИО = R2.ФИО AND

Rl.Оценка IS NOT NULL AND

Rl.Оценка> 2

GROUP BY R2.Группа. Rl.Дисциплина

Результат:

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

Предложение GROUP BY позволяет определять подмножество значений в особом поле в терминах другого поля и применять функцию агрегата к подмножеству. Это дает возможность объединять поля и агрегатные функции в едином предложении SELECT. Агрегатные функции могут применяться как в выражении вывода результатов строки SELECT, так и в выражении условия обработки сформированных групп HAVING. В этом случае каждая агрегатная функция вычисляется для каждой выделенной группы. Значения, полученные при вычислении агрегатных функций, могут быть использованы для вывода соответствующих результатов или для условия отбора групп.

Построим запрос, который выводит группы, в которых по одной дисциплине на экзаменах получено больше одной двойки:

SELECT R2.Группа

WHERE Rl.ФИО = R2.ФИО AND

Rl.Оценка = 2

GROUP BY R2.Группа. R1.Дисциплина

HAVING count(*)> 1

В дальнейшем в качестве примера будем работать не с БД «Сессия», а с БД «Банк», состоящей из одной таблицы F, в которой хранится отношение F, содержащее информацию о счетах в филиалах некоторого банка:

F = ;

Q = (Филиал, Город);

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

Например, предположим, что мы хотим найти суммарный остаток на счетах в филиалах. Можно сделать раздельный запрос для каждого из них, выбрав SUM(Остаток) из таблицы для каждого филиала. GROUP BY, однако, позволит поместить их все в одну команду:

SELECT Филиал, SUM

GROUP BY Филиал:

GROUP BY применяет агрегатные функции независимо для каждой группы, определяемой с помощью значения поля Филиал. Группа состоит из строк с одинаковым значением поля Филиал, и функция SUM применяется отдельно для каждой такой группы, то есть суммарный остаток на счетах подсчитывается отдельно для каждого филиала. Значение поля, к которому применяется GROUP BY, имеет, по определению, только одно значение на группу вывода, как и результат работы агрегатной функции. Поэтому мы можем совместить в одном запросе агрегат и поле. Вы можете также использовать GROUP BY с несколькими полями.

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

Правильной командой будет следующая:

SELECT Филиал, SUM(Остаток)

GROUP BY Филиал

HAVING SUM(Остаток) > 5000;

Аргументы в предложении HAVING подчиняются тем же самым правилам, что и в предложении SELECT, где используется GROUP BY. Они должны иметь одно значение на группу вывода.

Следующая команда будет запрещена:

SELECT Филиал.SUM(Остаток)

FROM F GROUP BY Филиал

HAVING ДатаОткрытия = 27/12/1999;

Поле ДатаОткрытия не может быть использовано в предложении HAVING, потому что оно может иметь больше чем одно значение на группу вывода. Чтобы избежать такой ситуации, предложение HAVING должно ссылаться только на агрегаты и поля, выбранные GROUP BY. Имеется правильный способ сделать вышеупомянутый запрос:

SELECT Филиал,SUM(Остаток)

WHERE ДатаОткрытия = "27/12/1999"

GROUP BY Филиал;

Смысл данного запроса следующий: найти сумму остатков по каждому филиалу счетов, открытых 27 декабря 1999 года.

Как и говорилось ранее, HAVING может использовать только аргументы, которые имеют одно значение на группу вывода. Практически, ссылки на агрегатные функции - наиболее общие, но и поля, выбранные с помощью GROUP BY, также допустимы. Например, мы хотим увидеть суммарные остатки на счетах филиалов в Санкт-Петербурге, Пскове и Урюпинске:

SELECT Филиал.SUМ(Остаток)

WHERE F.Филиал = Q.Филиал

GROUP BY Филиал

HAVING Филиал IN ("Санкт-Петербург". "Псков". "Урюпинск");

Поэтому в арифметических выражениях предикатов, входящих в условие выборки раздела HAVING, прямо можно использовать только спецификации столбцов, указанных в качестве столбцов группирования в разделе GROUP BY. Остальные столбцы можно специфицировать только внутри спецификаций агрегатных функций COUNT, SUM, AVG, MIN и MAX, вычисляющих в данном случае некоторое агрегатное значение для всей группы строк. Аналогично обстоит дело с подзапросами, входящими в предикаты условия выборки раздела HAVING: если в подзапросе используется характеристика текущей группы, то она может задаваться только путем ссылки на столбцы группирования.

Результатом выполнения раздела HAVING является сгруппированная таблица, содержащая только те группы строк, для которых результат вычисления условия поиска есть TRUE. В частности, если раздел HAVING присутствует в табличном выражении, не содержащем GROUP BY, то результатом его выполнения будет либо пустая таблица, либо результат выполнения предыдущих разделов табличного выражения, рассматриваемый как одна группа без столбцов группирования.

Вложенные запросы SQL

Теперь вернемся к БД «Сессия» и рассмотрим на ее примере использование вложенных запросов.

С помощью SQL можно вкладывать запросы внутрь друг друга. Обычно внутренний запрос генерирует значение, которое проверяется в предикате внешнего запроса (в предложении WHERE или HAVING), определяющего, верно оно или нет. Совместно с подзапросом можно использовать предикат EXISTS, который возвращает истину, если вывод подзапроса не пуст.

В сочетании с другими возможностями оператора выбора, такими как группировка, подзапрос представляет собой мощное средство для достижения нужного результата. В части FROM оператора SELECT допустимо применять синонимы к именам таблицы, если при формировании запроса нам требуется более чем один экземпляр некоторого отношения. Синонимы задаются с использованием ключевого слова AS, которое может быть вообще опущено. Поэтому часть FROM может выглядеть следующим образом:

FROM Rl AS A, Rl AS В

FROM Rl A. Rl В:

оба выражения эквивалентны и рассматриваются как применения оператора SELECT к двум экземплярам таблицы R1.

Например, покажем, как выглядят на SQL некоторые запросы к БД «Сессия»:

  • Список тех, кто сдал все положенные экзамены.

WHERE Оценка> 2

HAVING COUNT(*) = (SELECT COUNT(*)

WHERE R2.Группа=R3.Группа AND ФИОа.ФИО)

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

  • Список тех, кто должен был сдавать экзамен по БД, но пока еще не сдавал.

SЕLЕСТФИО

WHERE R2.Fpynna=R3.Группа AND Дисциплина = "БД" AND NOT EXISTS

(SELECT ФИО FROM Rl WHERE ФИО=а.ФИО AND Дисциплина = "БД")

Предикат EXISTS (SubQuery) истинен, когда подзапрос SubQuery не пуст, то есть содержит хотя бы один кортеж, в противном случае предикат EXISTS ложен.

Предикат NOT EXISTS обратно - истинен только тогда, когда подзапрос SubQuery пуст.

Обратите внимание, каким образом NOT EXISTS с вложенным запросом позволяет обойтись без операции разности отношений. Например, формулировка запроса со словом «все» может быть выполнена как бы с двойным отрицанием. Рассмотрим пример базы, которая моделирует поставку отдельных деталей отдельными поставщиками, она представлена одним отношением SP «Поставщики-детали» со схемой

SP (Номер_поставщика. номер_детали) Р (номер_детали. наименование)

Вот каким образом формулируется ответ на запрос: «Найти поставщиков, которые поставляют все детали».

SELECT DISTINCT НОМЕР_ПОСТАВЩИКА FROM SP SP1 WHERE NOT EXISTS

(SELECT номер_детали

FROM P WHERE NOT EXISTS

(SELECT * FROM SP SP2

WHERE SР2.номер_поставщика=SР1.номер_поставщика AND

sр2.номер_детали = Р.номер_детали)):

Фактически мы переформулировали этот запрос так: «Найти поставщиков таких, что не существует детали, которую бы они не поставляли». Следует отметить, что этот запрос может быть реализован и через агрегатные функции с подзапросом:

SELECT DISTINCT Номер_поставщика

GROUP BY Номер_поставщика

HAVING CounKDISTINCT номер_детали) =

(SELECT Count(номер_детали)

В стандарте SQL92 операторы сравнения расширены до многократных сравнений с использованием ключевых слов ANY и ALL. Это расширение используется при сравнении значения определенного столбца со столбцом данных, возвращаемым вложенным запросом.

Ключевое слово ANY, поставленное в любом предикате сравнения, означает, что предикат будет истинен, если хотя бы для одного значения из подзапроса предикат сравнения истинен. Ключевое слово ALL требует, чтобы предикат сравнения был бы истинен при сравнении со всеми строками подзапроса.

Например, найдем студентов, которые сдали все экзамены на оценку не ниже чем «хорошо». Работаем с той же базой «Сессия», но добавим к ней еще одно отношение R4, которое характеризует сдачу лабораторных работ в течение семестра:

R 1 = (ФИО, Дисциплина, Оценка);

R 2 = (ФИО, Группа);

R 3 = (Группы, Дисциплина)

R 4 = (ФИО, Дисциплина, Номер_лаб_раб, Оценка);

Select R1.ФИО From R1 Where 4 > = All (Select Rl.Оценка

Where R1.Фио = R11.Фио)

Рассмотрим еще один пример:

Выбрать студентов, у которых оценка по экзамену не меньше, чем хотя бы одна оценка по сданным им лабораторным работам по данной дисциплины:

Select R1.Фио

From R1 Where R1.Оценка>= ANY (Select R4.Оценка

Where Rl.Дисциплина = R4. Дисциплина AND R1.Фио = R4.Фио)

Внешние объединения SQL

Стандарт SQL2 расширил понятие условного объединения. В стандарте SQL1 при объединении отношений использовались только условия, задаваемые в части WHERE оператора SELECT, и в этом случае в результирующее отношение попадали только сцепленные по заданным условиям кортежи исходных отношений, для которых эти условия были определены и истинны. Однако в действительности часто необходимо объединять таблицы таким образом, чтобы в результат попали все строки из первой таблицы, а вместо тех строк второй таблицы, для которых не выполнено условие соединения, в результат попадали бы неопределенные значения. Или наоборот, включаются все строки из правой (второй) таблицы, а отсутствующие части строк из первой таблицы дополняются неопределенными значениями. Такие объединения были названы внешними в противоположность объединениям, определенным стандартом SQL1, которые стали называться внутренними.

В общем случае синтаксис части FROM в стандарте SQL2 выглядит следующим образом:

FROM <список исходных таблиц> |

< выражение естественного объединения > |

< выражение объединения >

< выражение перекрестного объединения > |

< выражение запроса на объединение >

<список исходных таблиц>::= <имя_таблицы_1>

[ имя синонима таблицы_1] [ ...]

[,<имя_таблицы_п>[ <имя синонима таблицы_n> ] ]

<выражение естественного объединениям:: =

<имя_таблицы_1> NATURAL { INNER | FULL | LEFT | RIGHT } JOIN <имя_таблицы_2>

<выражение перекрестного объединениям: = <имя_таблицы_1> CROSS JOIN <имя_таблицы_2>

<выражение запроса на объединением:=

<имя_таблицы_1> UNION JOIN <имя_таблицы_2>

<выражение объединениям:= <имя_таблицы_1> { INNER |

FULL | LEFT | RIGHT } JOIN {ON условие } <имя_таблицы_2>

В этих определениях INNER - означает внутреннее объединение, LEFT - левое объединение, то есть в результат входят все строки таблицы 1, а части результирующих кортежей, для которых не было соответствующих значений в таблице 2, дополняются значениями NULL (неопределено). Ключевое слово RIGHT означает правое внешнее объединение, и в отличие от левого объединения в этом случае в результирующее отношение включаются все строки таблицы 2, а недостающие части из таблицы 1 дополняются неопределенными значениями, Ключевое слово FULL определяет полное внешнее объединение: и левое и правое. При полном внешнем объединении выполняются и правое и левое внешние объединения и в результирующее отношение включаются все строки из таблицы 1, дополненные неопределенными значениями, и все строки из таблицы 2, также дополненные неопределёнными значениями.

Ключевое слово OUTER означает внешнее, но если заданы ключевые слова FULL, LEFT, RIGHT, то объединение всегда считается внешним.

Рассмотрим примеры выполнения внешних объединений. Снова вернемся к БД «Сессия». Создадим отношение, в котором будут стоять все оценки, полученные всеми студентами по всем экзаменам, которые они должны были сдавать. Если студент не сдавал данного экзамена, то вместо оценки у него будет стоять неопределенное значение. Для этого выполним последовательно естественное внутреннее объединение таблиц R2 и R3 по атрибуту Группа, а полученное отношение соединим левым внешним естественным объединением с таблицей R1, используя столбцы ФИО и Дисциплина. При этом в стандарте разрешено использовать скобочную структуру, так как результат объединения может быть одним из аргументов в части FROM оператора SELECT.

SELECT Rl.ФИО, R1.Дисциплина. Rl.Оценка

FROM (R2 NATURAL INNER JOIN R3) LEFT JOIN Rl USING (ФИО. Дисциплина)

Результат:

ФИО Дисциплина Оценка
Петров Ф. И. Базы данных
Сидоров К. А. Базы данных 4
Миронов Л. В. Базы данных
Степанова К. Е. Базы данных
Крылова Т. С. Базы данных
Владимиров В. А. Базы данных
Петров Ф. И. Теория информации Null
Сидоров К. А. Теория информации
Миронов А. В. Теория информации Null
Степанова К. Е. Теория информации
Крылова Т. С. Теория информации
Владимиров В. А. Теория информации Null
Петров Ф. И. Английский язык
Сидоров К. А. Английский язык Null
Миронов А. В. Английский язык Null
Степанова К. Е. Английский язык Null
Крылова Т. С. Английский язык Null
Владимиров В. А. Английский язык
Трофимов П. А. Сети и телекоммуникации
Иванова Е. А. Сети и телекоммуникации

Рассмотрим еще один пример, для этого возьмем БД «Библиотека». Она состоит из трех отношений, имена атрибутов здесь набраны латинскими буквами, что является необходимым в большинстве коммерческих СУБД.

BOOKS(ISBN, TITL. AUTOR. COAUTOR. YEARJZD, PAGES)

READER(NUM_READER. NAME_READER, ADRESS. HOOM_PHONE. WORK_PHONE. BIRTH_DAY)

EXEMPLARE (INV, ISBN, YES_NO. NUM_READER. DATE_IN. DATE_DUT)

Здесь таблица BOOKS описывает все книги, присутствующие в библиотеке, она имеет следующие атрибуты:

  • ISBN - уникальный шифр книги;
  • TITL - название книги;
  • AUTOR - фамилия автора;
  • COAUTOR - фамилия соавтора;
  • YEARIZD - год издания;
  • PAGES - число страниц.

Таблица READER хранит сведения обо всех читателях библиотеки, и она содержит следующие атрибуты:

  • NUM_READER - уникальный номер читательского билета;
  • NAME_READER - фамилию и инициалы читателя;
  • ADRESS - адрес читателя;
  • HOOM_PHONE - номер домашнего телефона;
  • WORK_PHONE - номер рабочего телефона;
  • BIRTH_DAY - дату рождения читателя.

Таблица EXEMPLARE содержит сведения о текущем состоянии всех экземпляров всех книг. Она включает в себя следующие столбцы:

  • INV - уникальный инвентарный номер экземпляра книги;
  • ISBN - шифр книги, который определяет, какая это книга, и ссылается на сведения из первой таблицы;
  • YES_NO - признак наличия или отсутствия в библиотеке данного экземпляра в текущий момент;
  • NUM_READER - номер читательского билета, если книга выдана читателю, и Null в противном случае;
  • DATE_IN - если книга у читателя, то это дата, когда она выдана читателю; a DATE_OUT - дата, когда читатель должен вернуть книгу в библиотеку.

Определим перечень книг у каждого читателя; если у читателя нет книг, то номер экземпляра книги равен NULL. Для выполнения этого поиска нам надо использовать левое внешнее объединение, то есть мы берем все строки из таблицы READER и соединяем со строками из таблицы EXEMPLARE, если во второй таблице нет строки с соответствующим номером читательского билета, то в строке результирующего отношения атрибут EXEMPLARE.INV будет иметь неопределенное значение NULL:

SELECT READER.NAME_READER, EXEMPLARE.INV

FROM READER RIGHT JOIN EXEMPLARE ON READER.NUM_READER=EXEMPLARE.NUM_READER

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

FROM (BOOKS LEFT JOIN EXEMPLARE)

LEFT JOIN (READER NATURAL JOIN EXEMPLARE)

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

Перекрестное объединение в трактовке стандарта SQL2 соответствует операции расширенного декартова произведения, то есть операции соединения двух таблиц, при которой каждая строка первой таблицы соединяется с каждой строкой второй таблицы.

Операция запроса па объединение эквивалентна операции теоретико-множественного объединения в алгебре. При этом требование эквивалентности схем исходных отношений сохраняется. Запрос на объединение выполняется по следующей схеме:

SELECT - запрос

UNION SELECT - запрос

UNION SELECT - запрос

Все запросы, участвующие в операции объединения, не должны, содержать выражений, то есть вычисляемых полей.

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

SELECT READER. NAME_READER

FROM READER, EXEMPLARE.BOOKS

BOOKS.TITLE = "Идиот"

SELECT READER.NAME_READER

FROM READER, EXEMPLARE,BOOKS

WHERE EXEMPLARE.NUM_READER= READER.NUM_READER AND

EXEMPLRE.ISBN = BOOKS.ISBN AND

BOOKS.TITLE = "Преступлениеинаказание"

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

Запрос на объединение может объединять любое число исходных запросов.

Так, к предыдущему запросу можно добавить еще читателей, которые держат на руках книгу «Замок»:

SELECT READER. NAME_READER

FROM READER. EXEMPLARE,BOOKS

WHERE EXEMPLARE.NUM_READER= READER.NUM_READER AND .

EXEMPLRE.ISBN = BOOKS.ISBN AND

BOOKS.TITLE = "Замок"

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

SELECT - запрос

SELECT - запрос

SELECT - запрос

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

SELECT DISTINCT READER.NAME_READER

FROM READER. EXEMPLARE.BOOKS

WHERE EXEMPLARE.NUM_READER= READER.NUM_READER AND

EXEMPLRE.ISBN = BOOKS.ISBN AND

BOOKS.TITLE = "Идиот" OR

BOOKS.TITLE = "Преступление и наказание" OR

BOOKS.TITLE = "Замок"

Ни один из исходных запросов в операции UNION не должен содержать предложения упорядочения результата ORDER BY, однако результат объединения может быть упорядочен, для этого предложение ORDER BY с указанием списка столбцов упорядочения записывается после текста последнего исходного SELECT-запроса.

Пример 21 . Получить общее количество поставщиков (ключевое слово COUNT ):

SELECT COUNT(*) AS N

В результате получим таблицу с одним столбцом и одной строкой, содержащей количество строк из таблицы P:

Использование агрегатных функций с группировками

Пример 23 . Для каждой детали получить суммарное поставляемое количество (ключевое слово GROUP BY …):

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM;

Этот запрос будет выполняться следующим образом. Сначала строки исходной таблицы будут сгруппированы так, чтобы в каждую группу попали строки с одинаковыми значениями DNUM. Потом внутри каждой группы будет просуммировано поле VOLUME. От каждой группы в результатирующую таблицу будет включена одна строка:

Замечание . В списке отбираемых полей оператора SELECT, содержащего раздел GROUP BY можно включать только агрегатные функции и поля, которые входят в условие группировки . Следующий запрос выдаст синтаксическую ошибку:

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM;

Причина ошибки в том, что в список отбираемых полей включено поле PNUM, которое не входит в раздел GROUP BY. И действительно, в каждую полученную группу строк может входить несколько строк с различными значениями поля PNUM. Из каждой группы строк будет сформировано по одной итоговой строке. При этом нет однозначного ответа на вопрос, какое значение выбрать для поля PNUM в итоговой строке.

Замечание . Некоторые диалекты SQL не считают это за ошибку. Запрос будет выполнен, но предсказать, какие значения будут внесены в поле PNUM в результатирующей таблице, невозможно.

Пример 24 . Получить номера деталей, суммарное поставляемое количество которых превосходит 400 (ключевое слово HAVING …):

Замечание . Условие, что суммарное поставляемое количество должно быть больше 400 не может быть сформулировано в разделе WHERE, т.к. в этом разделе нельзя использовать агрегатные функции. Условия, использующие агрегатные функции должны быть размещены в специальном разделе HAVING:

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM

HAVING SUM(PD.VOLUME) > 400;

В результате получим следующую таблицу:

Замечание . В одном запросе могут встретиться как условия отбора строк в разделе WHERE, так и условия отбора групп в разделе HAVING. Условия отбора групп нельзя перенести из раздела HAVING в раздел WHERE. Аналогично и условия отбора строк нельзя перенести из раздела WHERE в раздел HAVING, за исключением условий, включающих поля из списка группировки GROUP BY.

Использование подзапросов

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

Пример 25 . Получить список поставщиков, статус которых меньше максимального статуса в таблице поставщиков (сравнение с подзапросом):

WHERE P.STATYS <

(SELECT MAX(P.STATUS)

Замечание . Т.к. поле P.STATUS сравнивается с результатом подзапроса, то подзапрос должен быть сформулирован так, чтобы возвращать таблицу, состоящую ровно из одной строки и одной колонки .

Замечание

    Выполнить один раз вложенный подзапрос и получить максимальное значение статуса.

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

Пример 26 . Использование предиката IN

(SELECT DISTINCT PD.PNUM

WHERE PD.DNUM = 2);

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

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

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

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

Пример 27 . Использование предиката EXIST . Получить список поставщиков, поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

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

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

Замечание . В отличие от двух предыдущих примеров, вложенный подзапрос содержит параметр (внешнюю ссылку), передаваемый из основного запроса - номер поставщика P.PNUM. Такие подзапросы называются коррелируемыми (correlated ). Внешняя ссылка может принимать различные значения для каждой строки-кандидата, оцениваемого с помощью подзапроса, поэтому подзапрос должен выполняться заново для каждой строки, отбираемой в основном запросе. Такие подзапросы характерны для предиката EXIST, но могут быть использованы и в других подзапросах.

Замечание . Может показаться, что запросы, содержащие коррелируемые подзапросы будут выполняться медленнее, чем запросы с некоррелируемыми подзапросами. На самом деле это не так, т.к. то, как пользователь, сформулировал запрос, не определяет , как этот запрос будет выполняться. Язык SQL является непроцедурным, а декларативным. Это значит, что пользователь, формулирующий запрос, просто описывает, каким должен быть результат запроса , а как этот результат будет получен - за это отвечает сама СУБД.

Пример 28 . Использование предиката NOT EXIST . Получить список поставщиков, не поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

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

Пример 29 . Получить имена поставщиков, поставляющих все детали:

SELECT DISTINCT PNAME

PD.DNUM = D.DNUM AND

PD.PNUM = P.PNUM));

Замечание . Данный запрос содержит два вложенных подзапроса и реализует реляционную операцию деления отношений .

Самый внутренний подзапрос параметризован двумя параметрами (D.DNUM, P.PNUM) и имеет следующий смысл: отобрать все строки, содержащие данные о поставках поставщика с номером PNUM детали с номером DNUM. Отрицание NOT EXIST говорит о том, что данный поставщик не поставляет данную деталь. Внешний к нему подзапрос, сам являющийся вложенным и параметризованным параметром P.PNUM, имеет смысл: отобрать список деталей, которые не поставляются поставщиком PNUM. Отрицание NOT EXIST говорит о том, что для поставщика с номером PNUM не должно быть деталей, которые не поставлялись бы этим поставщиком. Это в точности означает, что во внешнем запросе отбираются только поставщики, поставляющие все детали.

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

  • 1) какова сумма доходов у всех жителей?
  • 2) каков наибольший и наименьший общий доход отдельного жителя?
  • 3) каков среднедушевой доход жителя Зеленограда?
  • 4) каков среднедушевой доход жителей каждой квартиры?
  • 5) сколько жителей в каждой квартире?

На языке SQL запросы такого типа можно создавать с помощью агрегатных функций и предложений GROUP BY и HAVING, используемых в операторе SELECT.

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

Для подведения итогов по информации, содержащейся в БД, в SQL предусмотрены агрегатные функции. Агрегатная функция принимает в качестве аргумента какой-либо столбец данных целиком, а возвращает одно значение, которое определенным образом подытоживает этот столбец.

Например, агрегатная функция AVG() принимает в качестве аргумента столбец чисел и вычисляет их среднее значение.

Чтобы вычислить среднедушевой доход жителя Зеленограда, нужен такой запрос:

SELECT ‘СРЕДНЕДУШЕВОЙ ДОХОДА, AVG(SUMD) FROM PERSON

В SQL имеется шесть агрегатных функций, которые позволяют получать различные виды итоговой информации (рис. 3.16):

SUM() вычисляет сумму всех значений, содержащихся в столбце;

AVG() вычисляет среднее среди значений, содержащихся в столбце;

  • - MIN() находит наименьшее среди всех значений, содержащихся в столбце;
  • - МАХ() находит наибольшее среди всех значений, содержащихся в столбце;
  • - COUNT() подсчитывает количество значений, содержащихся в столбце;

COUNT(*) подсчитывает количество строк в таблице результатов запроса.

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

SELECT AVG(SUMD*0.13)

Рис. 3.16.

При выполнении этого запроса создается временный столбец, содержащий значения (SUMD*0.13) для каждой строки таблицы PERSON, а затем вычисляется среднее значение временного столбца.

Сумму доходов у всех жителей Зеленограда можно вычислить с помощью агрегатной функции SUM:

SELECT SUM(SUMD) FROM PERSON

Агрегатная функция может быть использована и для вычисления итогов по таблице результатов, полученной соединением нескольких исходных таблиц. Например, можно вычислить общую сумму дохода, которая получена жителями от источника с названием «Стипендия»:

SELECT SUM(MONEY)

FROM PROFIT, HAVE_D

WHERE PROFIT.ID=HAVE_D.ID

AND PROFIT.SOURCE^Стипендия’

Агрегатные функции MIN() и MAX() позволяют найти соответственно наименьшее и наибольшее значения в таблице. При этом столбец может содержать числовые или строковые значения либо значения даты или времени.

Например, можно определить:

(а) наименьший общий доход, полученный жителями, и наибольший налог, подлежащий уплате:

SELECT MIN(SUMD), MAX(SUMD*0.13)

(б) даты рождения самого старого и самого молодого жителя:

SELECT MIN(RDATE), MAX(RDATE)

(в) фамилии, имена и отчества самого первого и самого последнего жителей в списке, упорядоченном по алфавиту:

SELECT MIN(FIO), MAX(FIO)

Применяя эти агрегатные функции, нужно помнить, что числовые данные сравниваются по арифметическим правилам, сравнение дат происходит последовательно (более ранние значения дат считаются меньшими, чем более поздние), сравнение интервалов времени выполняется на основании их продолжительности.

При использовании функции MIN() и МАХ() со строковыми данными результат сравнения двух строк зависит от используемой таблицы кодировки символов.

Агрегатная функция COUNT() подсчитывает количество значений в столбце любого типа:

(а) сколько квартир в 1-м микрорайоне?

SELECT COUNT(ADR)

WHERE ADR LIKE *%, 1_

(б) сколько жителей имеют источники дохода?

SELECT C0UNT(DISTINCT NOM)

(в) сколько источников дохода используются жителями?

SELECT COUNT(DISTINCT ID)

Ключевое слово «DISTINCT» указывает, что подсчитываются неповторяющиеся значения в столбце.

Специальная агрегатная функция COUNT(*) подсчитывает строки в таблице результатов, а не значения данных:

(а) сколько квартир во 2-м микрорайоне?

WHERE ADR LIKE "%, 2_-%’

(б) сколько источников дохода у Иванова Ивана Ивановича?

FROM PERSON, HAVE_D

WHERE FIO = "Иванов Иван Иванович"

AND PERSON.NOM = HAVE_D.NOM

(в) сколько жителей проживает в квартире по определенному адресу?

SELECT COUNT(*) FROM PERSON WHERE ADR = "Зеленоград, 1001-45’

Один из способов понять, как выполняются итоговые запросы с агрегатными функциями, - это представить выполнение запроса разбитым на две части. Сначала определяется, как бы запрос работал без агрегатных функций, возвращая несколько строк результатов. Затем применяются агрегатные функции к результатам запроса, возвращая одну итоговую строку.

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

SELECT AVG(SUMD), SUM(SUMD), (100*AVG(MONEY/SUMD))

FROM PERSON, PROFIT, HAVE_D WHERE PERSON.NOM = HAVE_D.NOM AND HAVE_D.ID = PROFIT.ID

Без агрегатных функций запрос выглядел бы так:

SELECT SUMD, SUMD, M0NEY/SUMD FROM PERSON, PROFIT, HAVE_D WHERE PERSON.NOM = HAVE_D.NOM AND HAVE_D.ID = PROFIT.ID

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

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

SELECT МАХ(SUMD)-MIN(SUMD)

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

Кроме того, в списке возвращаемых столбцов нельзя одновременно использовать агрегатные функции и обычные имена столбцов, поскольку в этом нет смысла, например:

SELECT FIO, SUM(SUMD)

Здесь первый элемент списка указывает, чтобы СУБД создала таблицу, которая будет состоять из нескольких строк и содержать по одной строке для каждого жителя. Второй элемент списка просит СУБД получить одно результирующее значение, являющееся суммой значений столбца SUMD. Эти два указания противоречат друг другу, что приводит к ошибке.

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

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

Например, сгруппируем отношение R1 по значению столбца Дисциплина. Мы получим 4 группы, для которых можем вычислить некоторые групповые значения, например количество кортежей в группе, максимальное или минимальное значение столбца Оценка.

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

Агрегатные функции используются подобно именам полей в операторе SELECT, но с одним исключением: они берут имя поля как аргумент. С функциями SUM и AVG могут использоваться только числовые поля. С функциями COUNT, MAX и MIN могут использоваться как числовые, так и символьные поля. При использовании с символьными полями MAX и MIN будут транслировать их в эквивалент ASCII кода и обрабатывать в алфавитном порядке. Некоторые СУБД позволяют использовать вложенные агрегаты, но это является отклонением от стандарта ANSI со всеми вытекающими отсюда последствиями.

Например, можно вычислить количество студентов, сдававших экзамены по каждой дисциплине. Для этого надо выполнить запрос с группировкой по полю "Дисциплина" и вывести в качестве результата название дисциплины и количество строк в группе по данной дисциплине. Применение символа * в качестве аргумента функции COUNT означает подсчет всех строк в группе.

SELECT R1.Дисциплина, COUNT(*)FROM R1GROUP BY R1.Дисциплина

Результат:

Пример. Получить список дисциплин, по которым сдали экзамен не менее 5 человек:

SELECT R1.ДисциплинаFROM R1GROUP BY R1.ДисциплинаHAVING COUNT(*) >= 5 Результат: Здесь инструкция HAVING выбирает группы, удовлетворяющие заданному условию.

Вложенные запросы

С помощью SQL можно вкладывать запросы внутрь друг друга. Обычно внутренний запрос генерирует значение, которое проверяется в предикате внешнего запроса (в предложении WHERE или HAVING), определяющего, верно оно или нет. Совместно с подзапросом можно использовать предикат EXISTS, который возвращает истину, если вывод подзапроса не пуст.

Отношение D (Детали)

Отношение PD (Поставки)

1. Получить список поставщиков, статус которых меньше максимального статуса в таблице поставщиков (сравнение с подзапросом):

WHERE P.STATYS <

(SELECT MAX(P.STATUS)

Замечание . Т.к. поле P.STATUS сравнивается с результатом подзапроса, то подзапрос должен быть сформулирован так, чтобы возвращать таблицу, состоящую ровно из одной строки и одной колонки .

Замечание

  1. Выполнить один раз вложенный подзапрос и получить максимальное значение статуса.
  2. Просканировать таблицу поставщиков P, каждый раз сравнивая значение статуса поставщика с результатом подзапроса, и отобрать только те строки, в которых статус меньше максимального.

2. Использование предиката IN

(SELECT DISTINCT PD.PNUM

WHERE PD.DNUM = 2);

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

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

  1. Выполнить один раз вложенный подзапрос и получить список номеров поставщиков, поставляющих деталь номер 2.
  2. Просканировать таблицу поставщиков P, каждый раз проверяя, содержится ли номер поставщика в результате подзапроса.

3. Использование предиката EXISTS . Получить список поставщиков, поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

  1. Просканировать таблицу поставщиков P, каждый раз выполняя подзапрос с новым значением номера поставщика, взятым из таблицы P.
  2. В результат запроса включить только те строки из таблицы поставщиков, для которых вложенный подзапрос вернул непустое множество строк.

Замечание . В отличие от двух предыдущих примеров, вложенный подзапрос содержит параметр (внешнюю ссылку), передаваемый из основного запроса - номер поставщика P.PNUM. Такие подзапросы называются коррелируемыми (correlated ). Внешняя ссылка может принимать различные значения для каждой строки-кандидата, оцениваемого с помощью подзапроса, поэтому подзапрос должен выполняться заново для каждой строки, отбираемой в основном запросе. Такие подзапросы характерны для предиката EXISTS, но могут быть использованы и в других подзапросах.

Замечание . Может показаться, что запросы, содержащие коррелируемые подзапросы будут выполняться медленнее, чем запросы с некоррелируемыми подзапросами. На самом деле это не так, т.к. то, как пользователь, сформулировал запрос, не определяет , как этот запрос будет выполняться. Язык SQL является непроцедурным, а декларативным. Это значит, что пользователь, формулирующий запрос, просто описывает, каким должен быть результат запроса , а как этот результат будет получен - за это отвечает сама СУБД.

4. Использование предиката NOT EXISTS . Получить список поставщиков, не поставляющих деталь номер 2:

WHERE NOT EXISTS

PD.PNUM = P.PNUM AND

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

5. Получить имена поставщиков, поставляющих все детали:

SELECT DISTINCT PNAME

WHERE NOT EXISTS

WHERE NOT EXISTS

PD.DNUM = D.DNUM AND

PD.PNUM = P.PNUM));

Замечание . Данный запрос содержит два вложенных подзапроса и реализует реляционную операцию деления отношений .

Самый внутренний подзапрос параметризован двумя параметрами (D.DNUM, P.PNUM) и имеет следующий смысл: отобрать все строки, содержащие данные о поставках поставщика с номером PNUM детали с номером DNUM. Отрицание NOT EXISTS говорит о том, что данный поставщик не поставляет данную деталь. Внешний к нему подзапрос, сам являющийся вложенным и параметризованным параметром P.PNUM, имеет смысл: отобрать список деталей, которые не поставляются поставщиком PNUM. Отрицание NOT EXISTS говорит о том, что для поставщика с номером PNUM не должно быть деталей, которые не поставлялись бы этим поставщиком. Это в точности означает, что во внешнем запросе отбираются только поставщики, поставляющие все детали.

Внешние соединения

Часто необходимо объединять таблицы таким образом, чтобы в результат попали все строки из первой таблицы, а вместо тех строк второй таблицы, для которых не выполнено условие соединения, в результат попадали бы неопределенные значения. Или наоборот, включаются все строки из правой (второй) таблицы, а отсутствующие части строк из первой таблицы дополняются неопределенными значениями. Такие объединения были названы внешними.

В общем случае синтаксис части FROM в стандарте SQL2 выглядит следующим образом:

FROM <список исходных таблиц>< выражение естественного соединения >< выражение соединения >< выражение перекрестного соединения >< выражение запроса на объединение ><список исходных таблиц>::= <имя_таблицы_1> [ имя синонима таблицы_1] [ …] [,<имя_таблицы_n>[ <имя синонима таблицы_n> ] ]<выражение естественного соединения>:: =<имя_таблицы_1> NATURAL { INNER | FULL LEFT | RIGHT } JOIN <имя_таблицы_2><выражение перекрестного соединения>:: = <имя_таблицы_1> CROSS JOIN <имя_таблицы_2><выражение запроса на объединение>::=<имя_таблицы_1> UNION JOIN <имя_таблицы_2><выражение соединения>::= <имя_таблицы_1> { INNERFULL | LEFT | RIGHT } JOIN {ON условие | } <имя_таблицы_2>

В этих определениях INNER - означает внутреннее (естественное) соединение, LEFT - левое соединение, то есть в результат входят все строки таблицы 1, а части результирующих кортежей, для которых не было соответствующих значений в таблице 2, дополняются значениями NULL (неопределено). Ключевое слово RIGHT означает правое внешнее соединение, и в отличие от левого соединения в этом случае в результирующее отношение включаются все строки таблицы 2, а недостающие части из таблицы 1 дополняются неопределенными значениями. Ключевое слово FULL определяет полное внешнее соединение: и левое и правое. При полном внешнем соединении выполняются и правое и левое внешние соединения и в результирующее отношение включаются все строки из таблицы 1, дополненные неопределенными значениями, и все строки из таблицы 2, также дополненные неопределенными значениями.

Ключевое слово OUTER означает внешнее, но если заданы ключевые слова FULL, LEFT, RIGHT, то соединение всегда считается внешним.

Рассмотрим примеры выполнения внешних соединений. Снова вернемся к БД "Сессия". Создадим отношение, в котором будут стоять все оценки, полученные всеми студентами по всем экзаменам, которые они должны были сдавать. Если студент не сдавал данного экзамена, то вместо оценки у него будет стоять неопределенное значение. Для этого выполним последовательно естественное внутреннее соединение таблиц R2 и R3 по атрибуту Группа, а полученное отношение соединим левым внешним естественным соединением с таблицей R1, используя столбцы ФИО и Дисциплина. При этом в стандарте разрешено использовать скобочную структуру, так как результат соединения может быть одним из аргументов в части FROM оператора SELECT.

SELECT R1.ФИО, R1.Дисциплина, R1.ОценкаFROM (R2 NATURAL INNER JOIN R3) LEFT JOIN R1 USING (ФИО, Дисциплина)

Результат:

ФИО Дисциплина Оценка
Петров Ф. И. Базы данных
Сидоров К. А. Базы данных
Миронов А. В. Базы данных
Степанова К. Е. Базы данных
Крылова Т. С Базы данных
Владимиров В. А. Базы данных
Петров Ф. И. Теория информации Null
Сидоров К. А. Теория информации
Миронов А. В. Теория информации Null
Степанова К. Е. Теория информации
Крылова Т. С Теория информации