|
7.1 О средствах одновременной работы с множеством таблиц
Затрагивая вопросы проектирования баз данных [2], мы выяснили, что базы данных - это множество взаимосвязанных сущностей или отношений (таблиц) в терминологии реляционных СУБД. При проектировании стремятся создавать таблицы, в каждой из которых содержалась бы информация об одном и только об одном типе сущностей. Это облегчает модификацию базы данных и поддержание ее целостности. Но такой подход тяжело усваивается начинающими проектантами, которые пытаются привязать проект к будущим приложениям и так организовать таблицы, чтобы в каждой из них хранилось все необходимое для реализации возможных запросов. Типичен вопрос: как же получить сведения о том, где купить продукты для приготовления того или иного блюда и определить его калорийность и стоимость, если нужные данные "рассыпаны" по семи различным таблицам? Не лучше ли иметь одну большую таблицу, содержащую все сведения базы данных ПАНСИОН ?
Даже при отсутствии средств одновременного доступа ко многим таблицам нежелателен проект, в котором информация о многих типах сущностей перемешана в одной таблице. SQL же обладает великолепным механизмом для одновременной или последовательной обработки данных из нескольких взаимосвязанных таблиц. В нем реализованы возможности "соединять" или "объединять" несколько таблиц и так называемые "вложенные подзапросы". Например, чтобы получить перечень поставщиков продуктов, необходимых для приготовления Сырников, возможен запрос
SELECT Продукт, Цена, Название, Статус
FROM Продукты, Состав, Блюда, Поставки, Поставщики
WHERE Продукты.ПР = Состав.ПР
AND Состав.БЛ = Блюда.БЛ
AND Поставки.ПР = Состав.ПР
AND Поставки.ПС = Поставщики.ПС
AND Блюдо = 'Сырники'
AND Цена IS NOT NULL;
Продукт | Цена | Название | Статус |
Яйца | 1.8 | ПОРТОС | кооператив |
Яйца | 2. | КОРЮШКА | кооператив |
Сметана | 3.6 | ПОРТОС | кооператив |
Сметана | 2.2 | ОГУРЕЧИК | ферма |
Творог | 1. | ОГУРЕЧИК | ферма |
Мука | 0.5 | УРОЖАЙ | коопторг |
Сахар | 0.94 | ТУЛЬСКИЙ | универсам |
Сахар | 1. | УРОЖАЙ | коопторг |
Он получен следующим образом: СУБД последовательно формирует строки декартова произведения таблиц, перечисленных во фразе FROM, проверяет, удовлетворяют ли данные сформированной строки условиям фразы WHERE, и если удовлетворяют, то включает в ответ на запрос те ее поля, которые перечислены во фразе SELECT.
Следует подчеркнуть, что в SELECT и WHERE (во избежание двусмысленности) ссылки на все (*) или отдельные столбцы могут (а иногда и должны) уточняться именем соответствующей таблицы, например, Поставки.ПС, Поставщики.ПС, Меню.*, Состав.БЛ, Блюда.* и т.п.
Очевидно, что с помощью соединения несложно сформировать запрос на обработку данных из нескольких таблиц. Кроме того, в такой запрос можно включить любые части предложения SELECT, рассмотренные в главе 2 (выражения с использованием функций, группирование с отбором указанных групп и упорядочением полученного результата). Следовательно, соединения позволяют обрабатывать множество взаимосвязанных таблиц как единую таблицу, в которой перемешана информация о многих типах сущностей. Поэтому начинающий проектант базы данных может спокойно создавать маленькие нормализованные таблицы, так как он всегда может получить из них любую "большую" таблицу.
Кроме механизма соединений в SQL есть механизм вложенных подзапросов, позволяющий объединить несколько простых запросов в едином предложении SELECT. Иными словами, вложенный подзапрос - это уже знакомый нам подзапрос (с небольшими огра-ничениями), который вложен в WHERE фразу другого вложенного подзапроса или WHERE фразу основного запроса.
Для иллюстрации вложенного подзапроса вернемся к предыдущему примеру и попробуем получить перечень тех поставщиков продуктов для Сырников, которые поставляют нужные продукты за минимальную цену.
SELECT Продукт, Цена, Название, Статус
FROM Продукты, Состав, Блюда, Поставки, Поставщики
WHERE Продукты.ПР = Состав.ПР
AND Состав.БЛ = Блюда.БЛ
AND Поставки.ПР = Состав.ПР
AND Поставки.ПС = Поставщики.ПС
AND Блюдо = 'Сырники'
AND Цена = ( SELECT MIN(Цена)
FROM Поставки X
WHERE X.ПР = Поставки.ПР );
Результат запроса имеет вид
Продукт | Цена | Название | Статус |
Яйца | 1.8 | ПОРТОС | кооператив |
Сахар | 0.94 | ТУЛЬСКИЙ | универсам |
Мука | 0.5 | УРОЖАЙ | коопторг |
Сметана | 2.2 | ОГУРЕЧИК | ферма |
Творог | 1. | ОГУРЕЧИК | ферма |
Здесь с помощью подзапроса, размещенного в трех последних строках запроса, описывается процесс определения минимальной цены каждого продукта для Сырников и поиск поставщика, предлагающего этот продукт за такую цену. Механизм реализации подзапросов будет подробно описан в п.3.3. Там же будет рассмотрено, как и для чего вводится псевдоним X для имени таблицы Поставки.
7.2 Запросы, использующие соединения
Декартово произведение таблиц
В литературе [2] показано, что соединения - это подмножества декартова произведения. Так как декартово произведение n таблиц - это таблица, содержащая все возможные строки r, такие, что r является сцеплением какой-либо строки из первой таблицы, строки из второй таблицы, ... и строки из n-й таблицы (а мы уже научились выделять с помощью SELECT любое подмножество реляционной таблицы), то осталось лишь выяснить, можно ли с помощью SELECT получить декартово произведение.
Для получения декартова произведения нескольких таблиц надо указать во фразе FROM перечень перемножаемых таблиц, а во фразе SELECT – все их столбцы.
Так, для получения декартова произведения Вид_блюд и Трапезы надо выдать запрос
SELECT Вид_блюд.*, Трапезы.*
FROM Вид_блюд, Трапезы;
Получим таблицу, содержащую 5 х 3 = 15 строк:
В | Вид | Т | Трапеза |
З | Закуска | 1 | Завтрак |
З | Закуска | 2 | Обед |
З | Закуска | 3 | Ужин |
С | Суп | 1 | Завтрак |
С | Суп | 2 | Обед |
С | Суп | 3 | Ужин |
Г | Горячее | 1 | Завтрак |
Г | Горячее | 2 | Обед |
Г | Горячее | 3 | Ужин |
Д | Десерт | 1 | Завтрак |
Д | Десерт | 2 | Обед |
Д | Десерт | 3 | Ужин |
Н | Напиток | 1 | Завтрак |
Н | Напиток | 2 | Обед |
Н | Напиток | 3 | Ужин |
В другом примере, где перемножаются таблицы Меню, Трапезы, Вид_блюд, Блюда:
SELECT Меню.*, Трапезы.*, Вид_блюд.*, Блюда.*
FROM Меню, Трапезы, Вид_блюд, Блюда;
образуется таблица (рис 3.1), содержащая 21 х 3 х 5 х 33 = 10395 строк.
Из первых 39 строк этой таблицы лишь две актуальных (отмечены "*"): в них совпадают номера блюд таблиц Меню и Блюда. В остальных – полная чепуха: к закускам отнесены супы и напитки, на завтрак предлагается незапланированный суп и т.д.
Эквисоединение таблиц
Если из декартова произведения убрать ненужные строки и столбцы, то можно получить актуальные таблицы, соответствующие любому из соединений.
Меню | Трапезы | Вид_блюд | Блюда |
Т | В | БЛ | Т | Трапеза | В | Вид | БЛ | Блюдо | В | Основа | Выход | Труд |
1 | З | 3 | 1 | Завтрак | З | Закуска | 1 | Салат летний | З | Овощи | 200. | 3 |
1 | З | 3 | 1 | Завтрак | З | Закуска | 2 | Салат мясной | З | Мясо | 200. | 4 |
1 | З | 3 | 1 | Завтрак | З | Закуска | 3 | Салат витаминный | З | Овощи | 200. | 4 * |
. . . |
1 | З | 3 | 1 | Завтрак | З | Закуска | 12 | Суп молочный | С | Молоко | 500. | 3 |
1 | З | 3 | 1 | Завтрак | З | Закуска | 13 | Бастурма | Г | Мясо | 300. | 5 |
. . . |
1 | З | 3 | 1 | Завтрак | З | Закуска | 32 | Кофе черный | Н | Кофе | 100. | 1 |
1 | З | 3 | 1 | Завтрак | З | Закуска | 33 | Кофе на молоке | Н | Кофе | 200. | 2 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 1 | Салат летний | З | Овощи | 200. | 3 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 2 | Салат мясной | З | Мясо | 200. | 4 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 3 | Салат витаминный | З | Овощи | 200. | 4 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 4 | Салат рыбный | З | Рыба | 200. | 4 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 5 | Паштет из рыбы | З | Рыба | 120. | 5 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 6 | Мясо с гарниром | З | Мясо | 250. | 3 * |
. . . |
Рис. 3.1. Иллюстрация декартова произведения
Очевидно, что отбор актуальных строк обеспечивается вводом в запрос WHERE фразы, в которой устанавливается соответствие между:
- кодами трапез (Т) в таблицах Меню и Трапезы (Меню.Т = Трапезы.Т),
- кодами видов блюд (В) в таблицах Меню и Вид_блюд (Меню.В = Вид_блюд.В),
- номерами блюд (БЛ) в таблицах Меню и Блюда (Меню.БЛ = Блюда.БЛ).
Такой скорректированный запрос
SELECT Меню.*, Трапезы.*, Вид_блюд.*, Блюда.*
FROM Меню, Трапезы, Вид_блюд, Блюда
WHERE Меню.Т = Трапезы.Т
AND Меню.В = Вид_блюд.В
AND Меню.БЛ = Блюда.БЛ;
позволит получить эквисоединение таблиц Меню, Трапезы, Вид_блюд и Блюда:
Т | В | БЛ | Т | Трапеза | В | Вид | БЛ | Блюдо | В | Основа | Выход |
Труд |
1 | З | 3 | 1 | Завтрак | З | Закуска | 3 | Салат витаминный | З | Овощи | 200. | 4 |
1 | З | 6 | 1 | Завтрак | З | Закуска | 6 | Мясо с гарниром | З | Мясо | 250. | 3 |
1 | Г | 19 | 1 | Завтрак | Г | Горячее | 19 | Омлет с луком | Г | Яйца | 200. | 5 |
. . . |
3 | Г | 16 | 3 | Ужин | Г | Горячее | 16 | Драчена | Г | Яйца | 180. | 4 |
3 | Н | 30 | 3 | Ужин | Н | Напиток | 30 | Компот | Н | Фрукты | 200. | 2 |
3 | Н | 31 | 3 | Ужин | Н | Напиток | 31 | Молочный напиток | Н | Молоко | 200. | 2 |
Естественное соединение таблиц
Легко заметить, что в эквисоединение таблиц вошли дубликаты столбцов, по которым проводилось соединение (Т, В и БЛ). Для исключения этих дубликатов можно создать естественное соединение тех же таблиц:
SELECT Т, В, БЛ, Трапеза, Вид, Блюдо, Основа, Выход, Труд
FROM Меню, Трапезы, Вид_блюд, Блюда
WHERE Меню.Т = Трапезы.Т
AND Меню.В = Вид_блюд.В
AND Меню.БЛ = Блюда.БЛ;
Реализация естественного соединения таблиц имеет вид
Т | В | БЛ | Трапеза | Вид | Блюдо | Основа | Выход | Труд |
1 | З | 3 | Завтрак | Закуска | Салат витаминный | Овощи | 200. | 4 |
1 | З | 6 | Завтрак | Закуска | Мясо с гарниром | Мясо | 250. | 3 |
1 | Г | 19 | Завтрак | Горячее | Омлет с луком | Яйца | 200. | 5 |
... |
3 | Г | 16 | Ужин | Горячее | Драчена | Яйца | 180. | 4 |
3 | Н | 30 | Ужин | Напиток | Компот | Фрукты | 200. | 2 |
3 | Н | 31 | Ужин | Напиток | Молочный напиток | Молоко | 200. | 2 |
Композиция таблиц
Для исключения всех столбцов, по которым проводится соединение таблиц, надо создать композицию
SELECT Трапеза, Вид, Блюдо, Основа, Выход, Труд
FROM Меню, Трапезы, Вид_блюд, Блюда
WHERE Меню.Т = Трапезы.Т
AND Меню.В = Вид_блюд.В
AND Меню.БЛ = Блюда.БЛ;
имеющую вид
Трапеза | Блюдо | Вид | Основа | Выход | Труд |
Завтрак | Салат витаминный | Закуска | Овощи | 200. | 4 |
Завтрак | Мясо с гарниром | Закуска | Мясо | 250. | 3 |
Завтрак | Омлет с луком | Горячее | Яйца | 200. | 5 |
. . . |
Ужин | Драчена | Горячее | Яйца | 180. | 4 |
Ужин | Компот | Напиток | Фрукты | 200. | 2 |
Ужин | Молочный напиток | Напиток | Молоко | 200. | 2 |
Тета-соединение таблиц
В базе данных ПАНСИОН трудно подобрать несложный пример, иллюстрирующий тета-соединение таблиц. Поэтому сконструируем такой надуманный запрос:
SELECT Вид_блюд.*, Трапезы.*
FROM Вид_блюд, Трапезы
WHERE Вид > Трапеза;
позволяющий выбрать из полученного в п.3.2.1 декартова произведения таблиц Вид_блюд и Трапезы лишь те строки, в которых значение трапезы "меньше" (по алфавиту) значения вида блюда:
В | Вид | Т | Трапеза |
З | Закуска | 1 | Завтрак |
С | Суп | 1 | Завтрак |
С | Суп | 2 | Обед |
Н | Напиток | 1 | Завтрак |
Соединение таблиц с дополнительным условием
При формировании соединения создается рабочая таблица, к которой применимы все операции, рассмотренные в главе 2: отбор нужных строк соединения (WHERE фраза), упорядочение получаемого результата (ORDER BY фраза) и агрегатирование данных (SQL-функции и GROUP BY фраза).
Например, для получения перечня блюд, предлагаемых в меню на завтрак, можно сформировать запрос на основе композиции (п. 3.2.4):
SELECT Вид, Блюдо, Основа, Выход, 'Номер -', БЛ
FROM Меню, Трапезы, Вид_блюд, Блюда
WHERE Меню.Т = Трапезы.Т
AND Меню.В = Вид_блюд.В
AND Меню.БЛ = Блюда.БЛ
AND Трапеза = ’Завтрак’;
Получим
Вид | Блюдо | Основа | Выход | 'Номер -' | БЛ |
Закуска | Салат витаминный | Овощи | 200. | Номер - | 3 |
Закуска | Мясо с гарниром | Мясо | 250. | Номер - | 6 |
Горячее | Омлет с луком | Яйца | 200. | Номер - | 19 |
Горячее | Пудинг рисовый | Крупа | 160. | Номер - | 21 |
Напиток | Молочный напиток | Молоко | 200. | Номер - | 31 |
Напиток | Кофе черный | Кофе | 100. | Номер - | 32 |
В п.3.6 можно познакомиться с достаточно полным примером соединения таблиц с различными дополнительными фразами.
Соединение таблицы со своей копией
В ряде приложений возникает необходимость одновременной обработки данных какой-либо таблицы и одной или нескольких ее копий, создаваемых на время выполнения запроса.
Например, при создании списков студентов (таблица Студенты) возможен повторный ввод данных о каком-либо студенте с присвоением ему второго номера зачетной книжки. Для выявления таких ошибок можно соединить таблицу Студенты с ее временной копией, установив в WHERE фразе равенство значений всех одноименных столбцов этих таблиц кроме столбцов с номером зачетной книжки (для последних надо установить условие неравенства значений).
Временную копию таблицы можно сформировать, указав имя псевдонима за именем таблицы во фразе FROM. Так, с помощью фразы
FROM Блюда X, Блюда Y, Блюда Z
будут сформированы три копии таблицы Блюда с именами X, Y и Z.
В качестве примера соединения таблицы с ней самой сформируем запрос на вывод таких пар блюд таблицы Блюда, в которых совпадает основа, а название первого блюда пары меньше (по алфавиту), чем номер второго блюда пары. Для этого можно создать запрос с одной копией таблицы Блюда (Копия):
SELECT Блюдо, Копия.Блюдо, Основа
FROM Блюда, Блюда Копия
WHERE Основа = Копия.Основа
AND Блюдо < Копия.Блюдо;
или двумя ее копиями (Первая и Вторая):
SELECT Первая.Блюдо, Вторая.Блюдо, Основа
FROM Блюда Первая, Блюда Вторая
WHERE Первая.Основа = Вторая.Основа
AND Первая.Блюдо < Вторая.Блюдо;
Получим результат вида
Первая.Блюдо | Вторая.Блюдо | Основа |
Морковь с рисом | Помидоры с луком | Овощи |
Морковь с рисом | Салат летний | Овощи |
Морковь с рисом | Салат витаминный | Овощи |
Помидоры с луком | Салат витаминный | Овощи |
Помидоры с луком | Салат летний | Овощи |
Салат витаминный | Салат летний | Овощи |
Бастурма | Бефстроганов | Мясо |
Бастурма | Мясо с гарниром | Мясо |
Бефстроганов | Мясо с гарниром | Мясо |
7.3 Вложенные подзапросы
Виды вложенных подзапросов
Вложенный подзапрос - это подзапрос, заключенный в круглые скобки и вложенный в WHERE (HAVING) фразу предложения SELECT или других предложений, использующих WHERE фразу. Вложенный подзапрос может содержать в своей WHERE (HAVING) фразе другой вложенный подзапрос и т.д. Нетрудно догадаться, что вложенный подзапрос создан для того, чтобы при отборе строк таблицы, сформированной основным запросом, можно было использовать данные из других таблиц (например, при отборе блюд для меню использовать данные о наличии продуктов в кладовой пансионата).
Существуют простые и коррелированные вложенные подзапросы.
Они включаются в WHERE (HAVING) фразу с помощью условий IN, EXISTS или одного из условий сравнения ( = | <> | < | <= | > | >= ).
Простые вложенные подзапросы обрабатываютя системой "снизу вверх". Первым обрабатывается вложенный подзапрос самого нижнего уровня. Множество значений, полученное в результате его выполнения, используется при реализации подзапроса более высокого уровня и т.д.
Запросы с коррелированными вложенными подзапросами обрабатываются системой в обратном порядке. Сначала выбирается первая строка рабочей таблицы, сформированной основным запросом, и из нее выбираются значения тех столбцов, которые используются во вложенном подзапросе (вложенных подзапросах). Если эти значения удовлетворяют условиям вложенного подзапроса, то выбранная строка включается в результат. Затем выбирается вторая строка и т.д., пока в результат не будут включены все строки, удовлетворяющие вложенному подзапросу (последовательности вложенных подзапросов).
Следует отметить, что SQL обладает большой избыточностью в том смысле, что он часто предоставляет несколько различных способов формулировки одного и того же запроса. Поэтому во многих примерах данной главы будут использованы уже знакомые нам по предыдущей главе концептуальные формулировки запросов. И несмотря на то, что часть из них успешнее реализуется с помощью соединений, здесь все же будут приведены их варианты с использованием вложенных подзапросов. Это связано с необходимостью детального знакомства с созданием и принципом выполнения вложенных подзапросов, так как существует немало задач (особенно на удаление и изменение данных), которые не могут быть реализованы другим способом. Кроме того, разные формулировки одного и того же запроса требуют для своего выполнения различных ресурсов памяти и могут значительно отличаться по времени реализации в разных СУБД.
Простые вложенные подзапросы
Простые вложенные подзапросы используются для представления множества значений, исследование которых должно осуществляться в каком-либо предикате IN, что иллюстрируется в следующем примере: выдать название и статус поставщиков продукта с номером 11, т.е. помидоров.
| Результат: |
SELECT Название, Статус
FROM Поставщики
WHERE ПС IN
( SELECT ПС
FROM Поставки
WHERE ПР = 11 ); |
Название | Статус |
СЫТНЫЙ | рынок |
УРОЖАЙ | коопторг |
ЛЕТО | агрофирма |
КОРЮШКА | кооператив |
|
Как уже отмечалось в п.3.3.1, при обработке полного запроса система выполняет прежде всего вложенный подзапрос. Этот подзапрос выдает множество номеров поставщиков, которые поставляют продукт с кодом ПР = 11, а именно множество (1, 5, 6, 8). Поэтому первоначальный запрос эквивалентен такому простому запросу:
SELECT Название, Статус
FROM Поставщики
WHERE ПС IN (1, 5, 6, 8);
Подзапрос с несколькими уровнями вложенности можно проиллюстрировать на том же примере. Пусть требуется узнать не поставщиков продукта 11, как это делалось в предыдущих запросах, а поставщиков помидоров, являющихся продуктом с номером 11. Для этого можно дать запрос
SELECT Название, Статус
FROM Поставщики
WHERE ПС IN
( SELECT ПС
FROM Поставки
WHERE ПР IN
( SELECT ПР
FROM Продукты
WHERE Продукт = 'Помидоры' ));
В данном случае результатом самого внутреннего подзапроса является только одно значение (11). Как уже было показано выше, подзапрос следующего уровня в свою очередь дает в результате множество (1, 5, 6, 8). Последний, самый внешний SELECT, вычисляет приведенный выше окончательный результат. Вообще допускается любая глубина вложенности подзапросов.
Тот же результат можно получить с помощью соединения
SELECT Название, Статус
FROM Поставщики, Поставки, Продукты
WHERE Поставщики.ПС = Поставки.ПС
AND Поставки.ПР = Продукты.ПР
AND Продукт = 'Помидоры';
При выполнении этого компактного запроса система должна одновременно обрабатывать данные из трех таблиц, тогда как в предыдущем примере эти таблицы обрабатываются поочередно. Естественно, что для их реализации тебуются различные ресурсы памяти и времени, однако этого невозможно ощутить при работе с ограниченным объемом данных в иллюстративной базе ПАНСИОН.
Использование одной и той же таблицы во внешнем и вложенном подзапросе
Выдать номера поставщиков, которые поставляют хотя бы один продукт, поставляемый поставщиком 6.
| Результат: |
SELECT DISTINCT ПС
FROM Поставки
WHERE ПР IN
( SELECT ПР
FROM Поставки
WHERE ПС = 6); |
|
Отметим, что ссылка на Поставки во вложенном подзапросе означает не то же самое, что ссылка на Поставки во внешнем запросе. В действительности, два имени Поставки обозначают различные значения. Чтобы этот факт стал явным, полезно использовать псевдонимы, например, X и Y:
SELECT DISTINCT X.ПС
FROM Поставки X
WHERE X.ПР IN
( SELECT Y.ПР
FROM Поставки Y
WHERE Y.ПС = 6 );
Здесь X и Y – произвольные псевдонимы таблицы Поставки, определяемые во фразе FROM и используемые как явные уточнители во фразах SELECT и WHERE. Напомним, что псевдонимы определены лишь в пределах одного запроса.
Вложенный подзапрос с оператором сравнения, отличным от IN
Выдать номера поставщиков, находящихся в том же городе, что и поставщик с номером 6.
| Результат: |
SELECT ПС
FROM Поставщики
WHERE Город =
( SELECT Город
FROM Поставщики
WHERE ПС = 6 ); |
|
В подобных запросах можно использовать и другие операторы сравнения (<>, <=, <, >= или >), однако, если вложенный подзапрос возвращает более одного значения и не используется оператор IN, будет возникать ошибка.
Коррелированные вложенные подзапросы
Выдать название и статус поставщиков продукта с номером 11.
SELECT Название, Статус
FROM Поставщики
WHERE 11 IN
( SELECT ПР
FROM Поставки
WHERE ПС = Поставщики.ПС );
Такой подзапрос отличается от рассмотренного в п.3.3.2 тем, что вложенный подзапрос не может быть обработан прежде, чем будет обрабатываться внешний подзапрос. Это связано с тем, что вложенный подзапрос зависит от значения Поставщики.ПС а оно изменяется по мере того, как система проверяет различные строки таблицы Поставщики. Следовательно, с концептуальной точки зрения обработка осуществляется следующим образом:
- Система проверяет первую строку таблицы Поставщики. Предположим, что это строка поставщика с номером 1. Тогда значение Поставщики.ПС будет в данный момент имеет значение, равное 1, и система обрабатывает внутренний запрос
( SELECT ПР
FROM Поставки
WHERE ПС = 1 );
получая в результате множество (9, 11, 12, 15). Теперь система может завершить обработку для поставщика с номером 1. Выборка значений Название и Статус для ПС=1 (СЫТНЫЙ и рынок) будет проведена тогда и только тогда, когда ПР=11 будет принадлежать этому множеству, что, очевидно, справедливо.
- Далее система будет повторять обработку такого рода для следующего поставщика и т.д. до тех пор, пока не будут рассмотрены все строки таблицы Поставщики.
Подобные подзапросы называются коррелированными, так как их результат зависит от значений, определенных во внешнем подзапросе. Обработка коррелированного подзапроса, следовательно, должна повторяться для каждого значения извлекаемого из внешнего подзапроса, а не выполняться раз и навсегда.
Рассмотрим пример использования одной и той же таблицы во внешнем подзапросе и коррелированном вложенном подзапросе.
Выдать номера всех продуктов, поставляемых только одним по-ставщиком.
| Результат: |
SELECT DISTINCT X.ПР
FROM Поставки X
WHERE X.ПР NOT IN
( SELECT Y.ПР
FROM Поставки Y
WHERE Y.ПС <> X.ПС ); |
|
Действие этого запроса можно пояснить следующим образом: "Поочередно для каждой строки таблицы Поставки, скажем X, выделить значение номера продукта (ПР), если и только если это значение не входит в некоторую строку, скажем, Y, той же таблицы, а значение столбца номер поставщика (ПС) в строке Y не равно его значению в строке X".
Запросы, использующие EXISTS
Квантор EXISTS (существует) - понятие, заимствованное из формальной логики. В языке SQL предикат с квантором существования представляется выражением EXISTS (SELECT * FROM ...).
Такое выражение считается истинным только тогда, когда результат вычисления "SELECT * FROM ..." является непустым множеством, т.е. когда существует какая-либо запись в таблице, указанной во фразе FROM подзапроса, которая удовлетворяет условию WHERE подзапроса. (Практически этот подзапрос всегда будет коррелированным множеством.)
Рассмотрим примеры. Выдать названия поставщиков, поставляющих продукт с номером 11.
| Результат: |
SELECT Название
FROM Поставщики
WHERE EXISTS
( SELECT *
FROM Поставки
WHERE ПС = Поставщики.ПС
AND ПР = 11 ); |
Название |
СЫТНЫЙ |
УРОЖАЙ |
КОРЮШКА |
ЛЕТО |
|
Система последовательно выбирает строки таблицы Поставщики, выделяет из них значения столбцов Название и ПС, а затем проверяет, является ли истинным условие существования, т.е. су-ществует ли в таблице Поставки хотя бы одна строка со значением ПР=11 и значением ПС, равным значению ПС, выбранному из таблицы Поставщики. Если условие выполняется, то полученное значение столбца Название включается в результат.
Предположим, что первые значения полей Название и ПС равны, соответственно, 'СЫТНЫЙ' и 1. Так как в таблице Поставки есть строка с ПР=11 и ПС=1, то значение 'СЫТНЫЙ' должно быть включено в результат.
Хотя этот первый пример только показывает иной способ формулировки запроса для задачи, решаемой и другими путями (с помощью оператора IN или соединения), EXISTS представляет собой одну из наиболее важных возможностей SQL. Фактически любой запрос, который выражается через IN, может быть альтернативным образом сформулирован также с помощью EXISTS. Однако обратное высказывание несправедливо.
Выдать название и статус поставщиков, не поставляющих продукт с номером 11.
| Результат: |
SELECT Название, Статус
FROM Поставщики
WHERE NOT EXISTS
( SELECT *
FROM Поставки
WHERE ПС = Поставщики.ПС
AND ПР = 11 ); |
Название | Статус |
ПОРТОС | кооператив |
ШУШАРЫ | совхоз |
ТУЛЬСКИЙ | универсам |
ОГУРЕЧИК | ферма |
|
Функции в подзапросе
Теперь, после знакомства с различными формулировками вложенных подзапросов и псевдонимами легче понять текст и алгоритм реализации запроса (п. 3.1) на получение тех поставщиков продуктов для Сырников, которые поставляют эти продукты за минимальную цену:
SELECT Продукт, Цена, Название, Статус
FROM Продукты, Состав, Блюда, Поставки, Поставщики
WHERE Продукты.ПР = Состав.ПР
AND Состав.БЛ = Блюда.БЛ
AND Поставки.ПР = Состав.ПР
AND Поставки.ПС = Поставщики.ПС
AND Блюдо = 'Сырники'
AND Цена = ( SELECT MIN(Цена)
FROM Поставки X
WHERE X.ПР = Поставки.ПР );
Естественно, что это коррелированный подзапрос: здесь сначала определяется минимальная цена продукта, входящего в состав Сырников, и только затем выясняется его поставщик.
На этом примере мы закончим знакомство с вложенными подзапросами, предложив попробовать свои силы в составлении ряда запросов, с помощью механизма таких подзапросов:
- Выдать названия всех мясных блюд.
- Выдать количество всех блюд, в состав которых входят помидоры.
- Выдать блюда, продукты для которых поставляются агрофирмой ЛЕТО.
7.4 Объединение (UNION)
В литературе [2] рассматривалась реляционная операция "Объединение", позволяющая получить отношение, состоящее из всех строк, входящих в одно или оба объединяемых отношения. Но при этом исходные отношения или их объединяемые проекции должны быть совместимыми по объединению. Для SQL это означает, что две таблицы можно объединять тогда и только тогда, когда:
- они имеют одинаковое число столбцов, например, m;
- для всех i (i = 1, 2, ..., m) i-й столбец первой таблицы и i-й столбец второй таблицы имеют в точности одинаковый тип данных.
Например, выдать названия продуктов, в которых нет жиров, либо входящих в состав блюда с кодом БЛ = 1:
| Результат: | Продукт |
SELECT Продукт
FROM Продукты
WHERE Жиры = 0
UNION
SELECT Продукт
FROM Соста
WHERE БЛ = 1 |
Майонез |
Лук |
Помидоры |
Зелень |
Яблоки |
Сахар |
|
Из этого простого примера видно, что избыточные дубликаты всегда исключаются из результата UNION. Поэтому, хотя в рассматриваемом примере Помидоры, Зелень и Яблоки выбираются обеими из двух составляющих предложения SELECT, в окончательном результате они появляются только один раз.
Предложением с UNION можно объединить любое число таблиц (проекций таблиц). Так, к предыдущему запросу можно добавить (перед точкой с запятой) конструкцию
UNION
SELECT Продукт
FROM Продукты
WHERE Ca < 250
позволяющую добавить к списку продуктов Масло, Рис, Мука и Кофе. Однако тот же результат можно получить простым изменением фразы WHERE первой части исходного запроса
WHERE Жиры = 0 OR Ca < 250
7.5 Реализация операций реляционной алгебры предложением SELECT
С помощью предложения SELECT можно реализовать любую операцию реляционной алгебры [2].
Селекция (горизонтальное подмножество) таблицы создается из тех ее строк, которые удовлетворяют заданным условиям. Пример:
SELECT *
FROM Блюда
WHER Основа = 'Молоко'
AND Выход > 200;
Проекция (вертикальное подмножество) таблицы создается из указанных ее столбцов (в заданном порядке) с последующим исключением избыточных дубликатов строк. Пример:
SELECT DISTINCT Блюдо, Выход, Основа
FROM Блюда;
Объединение двух таблиц содержит те строки, которые есть либо в первой, либо во второй, либо в обеих таблицах. Пример:
SELECT Блюдо, Основа, Выход
FROM Блюда
WHER Основа = 'Овощи'
UNION
SELECT Блюдо, Основа, Выход
FROM Блюда
WHER В = 'Г';
Пересечение двух таблиц содержит только те строки, которые есть и в первой, и во второй. Пример:
SELECT БЛ
FROM Состав
WHERE БЛ IN
( SELECT БЛ
FROM Меню);
Разность двух таблиц содержит только те строки, которые есть в первой, но отсутствуют во второй. Пример:
SELECT БЛ
FROM Состав
WHERE БЛ NOT IN
( SELECT БЛ
FROM Меню);
Декартово произведение таблиц и различные виды соединений были подробно рассмотрены в п. 3.2.1-3.2.6.
Здесь опущено лишь достаточно нудное описание редко встречаемой операция деления, которая также может быть реализована предложением SELECT с коррелированными вложенными подзапросами.
7.6 Резюме
Краткое знакомство с возможностями предложения SELECT показало, что с его помощью можно реализовать все реляционные операции. Кроме того, в предложении SELECT выполняются разнообразные вычисления, агрегирование данных, их упорядочение и ряд других операций, позволяющих описать в одном предложении ту работу, для выполнения которой потребовалось бы написать несколько страниц программы на алгоритмических языках Си, Паскаль или на внутренних языках ряда распространенных СУБД.
Например, пусть требуется получить калорийность и стоимость тех блюд, для которых:
- есть все составляющие их продукты;
- калорийность не превышает 400 ккал;
- стоимость не превышает 1.5 рубля,
а результат надо упорядочить по возрастанию калорийности блюд в рамках их видов.
Для этого можно дать запрос, показанный на рис. 3.2, позволяющий получить искомый результат в виде таблицы
Вид | Блюдо |
Горячее | Помидоры с луком | калорий - | 244.6 | 0.44 | руб |
Горячее | Бефстроганов | калорий - | 321.3 | 0.53 | руб |
Горячее | Драчена | калорий - | 333.9 | 0.33 | руб |
Горячее | Каша рисовая | калорий - | 339.2 | 0.27 | руб |
Горячее | Омлет с луком | калорий - | 354.9 | 0.36 | руб |
Десерт | Яблоки печеные | калорий - | 170.2 | 0.30 | руб |
Десерт | Крем творожный | калорий - | 394.3 | 0.27 | руб |
Закуска | Салат летний | калорий - | 155.5 | 0.32 | руб |
Закуска | Салат витаминный | калорий - | 217.4 | 0.37 | руб |
Закуска | Творог | калорий - | 330.0 | 0.22 | руб |
Закуска | Мясо с гарниром | калорий - | 378.7 | 0.62 | руб |
Напиток | Кофе черный | калорий - | 7.1 | 0.05 | руб |
Напиток | Компот | калорий - | 74.4 | 0.14 | руб |
Напиток | Кофе на молоке | калорий - | 154.8 | 0.11 | руб |
Напиток | Молочный напиток | калорий - | 264.9 | 0.34 | руб |
Суп | Суп молочный | калорий - | 396.6 | 0.22 | руб |
SELECT Вид, Блюдо, 'калорий -',
(SUM(INT((Белки+Углев)*4.1+Жиры*9.3)*Вес/1000)),
(SUM(Стоимость/К_во*Вес/1000)+MIN(Труд/100)),’руб’
FROM Блюда, Вид_блюд, Состав, Продукты, Наличие
WHERE Блюда.БЛ = Состав.БЛ
AND Состав.ПР = Продукты.ПР
AND Состав.ПР = Наличие.ПР
AND Блюда.В = Вид_блюд.В
AND БЛ NOT IN
( SELECT БЛ
FROM Состав
WHERE ПР IN
( SELECT ПР
FROM Наличие
WHERE К_во = 0))
GROUP BY Вид, Блюдо
HAVING SUM(Стоимость/К_во*Вес/1000+MIN(Труд/100)) < 1.5
AND SUM(((Белки+Углев)*4.1+Жиры*9.3)*Вес/1000) < 400
ORDER BY Вид, 4;
Рис. 3.2. Пример сложного запроса
Такой результат, нестрого говоря, строился следующим образом.
- FROM. Эта фраза инициирует создание в рабочей памяти таблицы, являющейся декартовым произведением таблиц Блюда, Вид_блюд, Состав, Продукты и Наличие.
- WHERE. Эта фраза нужна для преобразования полученного декартова произведения в естественное соединение и удаления из последнего строк с кодами блюд, не обеспеченных продуктами. Естественное соединение образуется путем вычеркивания строк, где не совпадают: код блюда из таблицы Блюда с кодом блюда из таблицы Состав, код продукта из таблицы Состав с кодом продукта из таблицы Продукты и т.д. Обеспеченность блюда всеми продуктами проверяется с помощью последовательности подзапросов. Внутренний подзапрос выдает перечень кодов продуктов, которых нет в кладовой пансионата. Следующий подзапрос выдает коды тех блюд, в состав которых должны входить "отсутствующие" продукты. И, наконец, из естественного соединения вычеркиваются строки с кодами полученных блюд (точнее оставляются строки "Где код блюда не принадлежит перечню кодов блюд, полученному в подзапросе".
- SELECT. Из полученного соединения удаляются столбцы, не используемые в выражениях SELECT или других фразах. Если в списке SELECT есть выражения (константы), то для хранения их значений формируются дополнительные столбцы и инициируются операции по их заполнению. В рассматриваемом примере будут сохранены столбцы Вид, Блюдо, Белки, Углев, Жиры, Вес, Стоимость, К_во и созданы дополнительные столбцы для формирования и хранения значений стоимости и калорийности составляющих каждого блюда, а также для хранения текстовых констант 'калорий -' и 'руб'. Обратите внимание на прием, использованный при суммировании стоимостей продуктов, входящих в состав блюда, и стоимости его приготовления (Труд): можно ли заменить MIN на MAX или AVG?
- GROUP BY. Отредактированное естественное соединение группируется по видам блюд и их названиям. Создаются группы горячих блюд, десертов и т.д., а внутри каждой группы создаются подгруппы строк со сведениями о продуктах, относящихся к конкретному блюду группы.
- SELECT. Каждая подгруппа строк, полученная на предыдущем шаге, преобразуется в единственную строку для результата. В нее заносится вид блюда (общий для всех подгрупп группы), название блюда (общее для всех строк подгруппы), две текстовых константы ('калорий -' и 'руб') и две суммы. Последние формируются путем суммирования тех значений дополнительных столбцов, которые принадлежат подгруппе.
- HAVING. Сформированные строки, не удовлетворяющие условиям фразы HAVING
SUM(Стоимость/К_во*Вес/1000+MIN(Труд/100)) < 1.5 и
SUM(((Белки+Углев)*4.1+Жиры*9.3)*Вес/1000) < 400
исключаются из результата предыдущего шага.
- ORDER BY. Результат шага 6 упорядочивается в соответствии со списком фразы ORDER BY для получения окончательного результата. Сначала строки группируются по видам блюд (в алфавитном порядке), а затем – по значению элемента данных, указанного на четвертом месте фразы SELECT, т.е. по калорийности.
Конечно, рассмотренный запрос весьма сложен, но попробуйте написать на любом знакомом вам языке программу, реализующую те же действия, и оцените сложность ее написания и отладки.
|