НОВЫЙ КУРС БД 2013
.pdf
ID |
|
NAME |
SALARY |
1 |
|
Иван |
10000 |
2 |
|
Сергей |
14000 |
3 |
|
Илья |
15000 |
Следующий запрос с подзапросом вернет имя человека, у которого значение SALARY |
|||
(зарплаты) максимально. |
|
|
|
SELECT id, Name, Salary FROM Mans |
|
||
WHERE salary = (SELECT MAX(salary) FROM MANS) |
|
||
Результатом будет одна строка: |
|
|
|
3 |
|
Илья |
15000 |
Если бы мы просто написали:
Select Name, Max(Salary) From Mans GROUP BY Name
То получили бы не то, что хотели:
1 |
Иван |
10000 |
2 |
Сергей |
14000 |
3 |
Илья |
15000 |
Запрос вернул бы все три строки, поскольку напомню, функция агрегирования Max применяется к выбранной группе строк, а поскольку мы выбрали еще и столбец имя, то его нам нужно включить в столбец группировки. И функция Max будет искать максимум в каждой из групп. Т.е. у нас сформируются три группы Иван, Сергей, Илья и в каждой из групп будет найден максимум зарплаты. Т.е. вернутся все три строки.
Если же мы напишем:
Select Max(Salary) as MS From Mans
То получим верные данные
MS
15000
Однако, выполнение этого запроса не выдает информации о владельце максимальной зарплаты. Вернемся к подзапросам и отметим тот факт, что подзапрос (SELECT MAX(salary) FROM MANS) возвращает единственное значение, которое предназначено для использования во внешнем запросе SELECT id, Name, Salary FROM Mans WHERE salary =…
Особенностью запросов этого типа является то, что внутренний запрос может возвращать только одно значение. Если мы напишем SELECT id, Name, Salary FROM Mans WHERE salary = (SELECT salary FROM MANS), то С БД сообщит об ошибке.
Второй тип запросов позволяет работать не с единственным значением, а с группами записей. Этот тип запроса наиболее употребим, и использует специальную конструкцию IN. Действие конструкция IN аналогично применению оператора or при задании условия в предикате where. Т.е. выбираются строки, в которых есть хотя бы одно совпадение со списком значений перечисленных через or.
Например, пусть есть две таблицы Mans и Jobs, они связаны по полю Job через первичный ключ ID таблицы Jobs :
51
Таблица Mans
ID |
Surname |
Job |
1 |
Иванов |
10 |
2 |
Петров |
20 |
3 |
Сидоров |
30 |
4 |
Виейра |
Null |
|
Таблица Jobs |
|
ID |
|
JobName |
10 |
|
Менеджер |
20 |
|
Продавец |
30 |
|
Уборщик |
40 |
|
Программист |
Предположим, что нам нужно выбрать всех людей, у которых заполнено поле должность. Для решения этой задачи есть несколько способов. Я специально воспользуюсь неправильным методом, для того чтобы показать возможность использования or:
Итак, чтобы выбрать все строки таблицы Mans, в которых поле Job не пусто, я запомню ID всех должностей, в таблице Jobs и буду просто сравнивать их с полем Job:
Select name from Mans where (job=10) or (job=20) or (job=30) or (job=40)
В результате СУБД вернет три строки с фамилиями Иванов, Петров, Сидоров
А теперь покажем, как можно решить ту же задачу с использованием подзапроса:
SELECT Name FROM Mans
WHERE Job IN (SELECT ID FROM Jobs)
В результате вернуться все те же три строки - Иванов, Петров, Сидоров. При этом нам не нужно знать id этих должностей, СУБД делает это динамически в процессе выполнения подзапроса SELECT ID FROM Jobs.
Поясним принцип обработки этого запроса SQL Server’ом:
При выполнении запроса (SELECT Name FROM Mans) берется строка из целевой таблицы (т.е. из таблицы Mans), и значение сравниваемого столбца (т.е. столбца Job) передается во внутренний запрос (SELECT ID FROM Jobs), затем выполняется внутренний запрос, который получает список (список ID) с номерами всех должностей, а затем выполняется проверка: находится ли значение сравниваемого столбца (Job) в полученном списке.
Если внимательно посмотреть на принцип работы этого запроса, обнаружиться, что он выполняет те же действия, как если бы мы использовали операцию внутреннего соединения. И действительно, следующий запрос вернет те же три строки: Иванов, Петров, Сидоров:
SELECT Name FROM Mans
INNER JOIN Jobs ON (Mans.Job = Jobs.id)
Если мы копнем еще глубже, то получается, что внутренний запрос, возвращающий несколько строк может выполнять роль любого вида соединений!
Например, чтобы получить левое соединение по таблице Mans (т.е. выбрать все данные из таблицы Mans) достаточно написать такой код:
SELECT Name FROM Mans
LEFT OUTER JOIN Jobs ON (Mans.Job = Jobs.id)
В результате мы получим все четыре строки таблицы Mans: Иванов, Петров, Сидоров, Виейра. Такой же результат можно получить, если использовать вложенный подзапрос:
SELECT Name FROM Mans
WHERE Job IN (SELECT ID FROM Jobs) or (Job=Null)
52
Вложенные запросы можно использовать для поиска так называемых «висячих» строк, т.е. строк которые имеют значение не из перечисления внешнего ключа. В таблицах ниже, висячая строка это строка из таблицы Mans с ID = 4, поскольку Job c ID = 70 отсутствует в таблице Jobs.
Таблица Mans Таблица Jobs
ID |
Surname |
Job |
1 |
Иванов |
10 |
2 |
Петров |
20 |
3 |
Сидоров |
30 |
4 |
Виейра |
70 |
ID |
JobName |
10 |
Менеджер |
20 |
Продавец |
30 |
Уборщик |
40 |
Программист |
Для поиска таких строк можно использовать довольно простую конструкцию:
SELECT Name FROM Mans
WHERE Job NOT IN (SELECT ID FROM Jobs) and (Job IS NOT Null)
3.6.4 Производные таблицы.
Иногда требуется получить очень «экзотические» данные, а в силу того, что нормализованные базы содержат, как правило, большое количество таблиц, выборка таких данных бывает очень затруднительна. Несомненно, описанные выше инструменты, такие как подзапросы, вычисляемые поля и прочие очень помогают в решении данных задач, однако бывает так, что задачу можно решить, лишь последовательно выполнив несколько запросов. Однако SQL предлагает еще один очень мощный инструмент – производные таблицы. По сути, производная таблица ничем не отличается от обычной таблицы, у нее есть и столбцы и строки и типы данных и т.д. Однако основное ее отличие состоит в том, что она создается автоматически на основе результатов какого-либо запроса и разработчику нет необходимости прописывать ее структуру.
Рассмотрим применение производной таблицы на конкретном примере: вернемся к нашим двум таблицам Mans и Jobs, и дополним нашу базу данных еще двумя таблицами – Sales (Продажи) и Goods (Товары).
Таблица Sales
Id |
Int |
NOT NULL |
PK |
Man |
Int |
NOT NULL |
FK |
Good |
Int |
NOT NULL |
FK |
DateSale |
Datetime |
NOT NULL |
- |
Quantity |
Int |
NOT NULL |
- |
Таблица Goods
Id |
Int |
NOT NULL |
PK |
Name |
Varchar(100) |
NOT NULL |
- |
Price |
Float |
NOT NULL |
- |
Таким образом, наша база содержит теперь 4 таблицы. Заполним все таблицы данными.
Таблица Mans
Id |
Surname |
Job |
1 |
Иванов |
20 |
2 |
Петров |
20 |
3 |
Сидоров |
20 |
Таблица Sales
Id |
Man |
Good |
Quantity |
1 |
1 |
1 |
1 |
2 |
2 |
1 |
3 |
3 |
2 |
2 |
2 |
4 |
2 |
3 |
1 |
5 |
3 |
2 |
2 |
Таблица Jobs
Id |
|
|
|
JobName |
10 |
|
|
Менеджер |
|
20 |
|
|
|
Продавец |
|
Таблица Goods |
|
|
|
Id |
Name |
|
Price |
|
1 |
Телевизор |
|
14000 |
|
2 |
Плеер |
|
1200 |
|
3 |
Компьютер |
|
25000 |
|
4 |
Колонки |
|
2300 |
|
Предположим теперь, что нужно получить список продавцов, которые продали телевизоры. С этой задачей мы легко справимся, написав примерно такой запрос:
Select Mans.Surname from Mans
Inner Join Sales On Man.Id = Sales.Man Inner Join Goods On Sales.Good = Goods.Id Where Good.Name = ‘Телевизор’
53
Результат выполнения данного запроса будет таким:
Surname
Иванов
Петров
А теперь рассмотрим более сложную задачу. Пусть нам требуется найти только тех продавцов, которые продали и телевизор и компьютер. На первый взгляд кажется, что это простая задача, однако «решение в лоб» не даст успешных результатов. Задача состоит в том, чтобы найти продавцов, которые продали оба этих товара и первое, что приходит в голову просто добавить еще одно условие в написанный выше запрос:
Select Mans.Surname From Mans
Inner Join Sales On Man.Id = Sales.Man
Inner Join Goods On Sales.Good = Goods.Id
Where
Good.Name = ‘Телевизор’
AND
Good.Name = ‘Компьютер’
Однако, если вдуматься, то поле может принимать одновременно только одно значение, либо телевизор, либо компьютер. И естественно результатом этого запроса будет 0 строк. Как же поступить? Если мы изменим оператор, с AND на OR, то получим продавцов, которые продали либо телевизоры, либо компьютеры, либо и то и другое вместе. Но нам нужно только последнее – оба товара проданных вместе одним продавцом. Т.е. фактически нам нужно соединить результаты запроса по поиску продаж телевизоров с результатом по поиску продаж компьютеров. Давайте, наконец, воспользуемся тем, чему посвящен данный раздел – производной таблицей.
Итак, для создания производной таблицы нужно сделать 2 шага:
1.Заключить в круглые скобки запрос, который генерирует необходимый результирующий набор;
2.Дать в запросе псевдоним для полученных результатов
Вобщем виде запрос выглядит так:
Select <columns> From
(
<запрос, возвращающий необходимые данные>
)
as <псевдоним для производной таблицы>
Join
<другие обычные таблицы из базы данных>
Таким образом, для решения нашей задачи нужно написать следующий, довольно большой, запрос:
54
Select Mans.Surname From Mans Inner Join
(
Select Mans.Id From Mans
Inner Join Sales On Man.Id = Sales.Man Inner Join Goods On Sales.Good = Goods.Id Where Good.Name = ‘Телевизор’
)
As SalesTV
ON Mans.Id = SalesTV.Id Inner Join
(
Select Mans.Id From Mans
Inner Join Sales On Man.Id = Sales.Man Inner Join Goods On Sales.Good = Goods.Id Where Good.Name = ‘Компьютер’
)
As SalesPC
ON Mans.Id = SalesPC.Id
В результате мы получим только одну строку:
Surname
Петров
Если бы у данного продавца было несколько продаж товаров телевизор и компьютер, то запрос вернул бы несколько строк Петров. Поэтому нужно было бы использовать ключевое слово Distinct. Безусловно, производные таблицы очень мощный инструмент, который позволяет решать большой спектр задач. Однако на реальных базах данных, в которых число строк превышает несколько тысяч, а иногда и миллионов строк, выполнение запросов с производными таблицами занимает довольно большое количество времени, а иногда даже зависание. Причиной этому служит то, что производные таблицы не индексируются, что влечет за собой большие потери по времени при создании соединений (СУБД каждый раз приходится пробегать все строки). Выходом из сложившейся ситуации служит создание временных таблиц и формирование на них индексов. Но, безусловно, для определенных видов задач производные таблицы – незаменимый инструмент.
55
ГЛАВА 4. АСПЕКТ ЦЕЛОСТНОСТИ РМД
Аспект целостности РМД отвечает за корректность и актуальность хранящихся данных, а также за их полноту. В данной главе мы рассмотрим ограничения, накладываемые на таблицы, а также специальные инструменты, применяемые в MS SQL Server для реализации этого аспекта.
4.1Ограничения.
4.1.1Общие сведения об ограничениях.
Ограничения это одна из самых важных тем при изучении баз данных, поэтому эта глава обязательна для изучения. Без ограничений невозможно построить полноценную БД. Начнем с определения понятия ограничения.
Ограничение – это требование, предъявляемое к данным. Ограничения устанавливаются на уровне столбца или всей таблицы и гарантируют соответствие данных определенным правилам обеспечения целостности данных.
Мы рассмотрим три основных вида ограничений:
Ограничения сущностей
Ограничения домена
Ограничения ссылочной целостности
На рисунке 1 показан каждый из видов ограничений на примере таблицы Orders в базе данных Northwind из стандартной поставки SQL Server’a 2005
Рисунок 1. Виды ограничений. Далее рассмотрим каждый вид ограничения более подробно:
Ограничения сущностей
Ограничения сущности распространяются на строки таблицы. Т.е. совокупность отдельных значений столбцов. Единственное назначение ограничения сущности является требования РБД об уникальности каждой строки таблицы. Т.е. в рамках одной таблицы строка должна быть уникальна. К ограничениям сущности относятся ограничения PRIMARY KEY и UNIQUE.
Ограничения домена
Ограничения домена распространяются на один или несколько столбцов таблицы. Такие ограничения проверяют вводимые в таблицу данные на определенное условие при событиях вставки и обновлении. Например, пусть у нас имеется таблица с ценой товаров, чтобы защитить себя от ошибок можно поставить ограничение на столбец цена больше нуля и тогда при вставке и обновлении данных в эту таблицу попадут только положительные значения. К ограничениям домена относятся ограничения CHEK и DEFAULT, они будут рассмотрены позже.
Ограничения ссылочной целостности
Данный тип ограничений предназначен для согласования значений в различных столбцах одной или, чаще всего, в нескольких таблиц. Например, компания может принимать электронные карты только определенных банков, для того чтобы обеспечить такую возможность необходимо создать таблицу, в которой указать весь перечень принимаемых карт и связать ее с таблицей, в которой происходит сделка. Тогда при создании сделки, нужно будет явно указать какой картой будет происходить оплата, если клиент попытается оплатить картой, которая отсутствует в таблице, СУБД выдаст ошибку. Другими словами ограничения ссылочной целостности необходимы тогда, когда нужно согласовать одно множество значений с другим. К такому типу ограничений относится внешний ключ FOREIGN KEY.
Прежде чем рассмотреть каждый из видов ограничений, обозначенных выше, посмотрим на правила именования ограничений. При создании ограничений в визуальном режиме СУБД сама предлагает и задает имена для ограничений. Типичный пример ограничений первичного ключа:
PK_Mans_120ACF20
В данном случае создается первичный ключ для таблицы Mans. Как понятно из названия префикс PK_ означает вид создаваемого ограничения, т.е. PRIMARY KEY. Вторая часть тоже вполне прозрачна - _Mans_ означает таблицу, на которую это ограничение накладывается, а вот с третей частью _120ACF20 возникают трудности. Третья часть это сформированный случайным образом идентификатор. Таким образом формируются все имена для ограничений и в связи с этим возникает вопрос о понятности такого названия для разработчика. Никакой информационной нагрузки этот идентификатор не несет, и разработчик по названию даже не сможет понять, к какому столбцу применено ограничение PRIMARY KEY. Поэтому чтобы избежать подобных ситуаций рекомендуется придерживаться следующих правил:
В именах должно прослеживаться единообразие, т.е. если вы выбрали в качестве разделителя символ подчеркивания и порядок описания (как здесь PK_Mans_120ACF20, ограничения_имя таблицы_идентификатор) то так надо поступать со всеми именами, т.е. следующие имена должны выглядеть например, так: FK_Mans_1298343545, PK_Role_12F3434A и т.д.
Для того чтобы внести дополнительную информацию в название ограничений (и не только) вместо идентификатора лучше указывать имя столбца, на которое это ограничение накладывается, т..е. для предыдущего примера PK_Mans_120ACF20 если мы, например, создаем первичный ключ id, то название бы выглядело так: PK_Mans_Id (или так PKMansId)
При именовании ограничений использовать наиболее краткие формулировки, но вместе с тем понятные.
Далее мы рассмотрим приведенные виды ограничений и начнем с ограничений сущностей
57
4.1.2 Ограничения сущностей.
Как было сказано выше, к этим ограничениям относятся 2 вида ограничений: PRIMARY KEY и UNIQUE.
PRIMARY KEY
PRIMARY KEY или первичный ключ наиболее часто употребляемый вид ограничений, поскольку он отвечает за уникальность каждой строки таблицы, а это является требованием РБД.
На практике встречаются таблицы, не имеющие первичного ключа, однако это скорее исключение, чем правило. И в большинстве своем такие таблицы используются как временное хранилище, чего-то заведомо уникального.
Первичный ключ чаще всего обозначают как PK. Столбец первичного ключа не должен содержать NULL-значений. Далее мы рассмотрим, как создать ограничение PK двумя способами: первый при создании таблицы и второй на существующей таблице.
Создание PK при создании таблицы
.Это самый простой способ создания ограничения первичного ключа (и ограничений в целом). Для того чтобы добавить это ограничение достаточно на требуемом столбце написать PRIMARY KEY. Например, создадим таблицу Man и установим столбец id в качестве первичного ключа:
Create Table Mans
(
id int identity not null PRIMARY KEY, Name varchar(10) not null
)
Создание PK на существующей таблице
Этот способ создания ограничений чуть сложней, но тоже не требует никаких особых «телодвижений». Создадим все тоже ограничение PK на столбец id, однако предположим теперь, что таблица Mans у нас уже создана, а как мы знаем, для изменения существующих объектов применяется оператор ALTER. Итак, код на создания ограничения будет таков:
USE DataBase (в которой создавали таблицу)
Alter Table Mans
ADD CONSTRAINT PK_Mans_Id PRIMARY KEY (Id)
Давайте разберемся, что здесь было сделано: первая строка указывает СУБД, какую базу использовать. Это написано здесь не случайно, поскольку если так не сделать, изменения будут применены к базе данных выбранной по умолчанию, и если окажется, что это не ваше база, последствия могут быть печальны, поэтому первым делом, при создании и изменении объектов базы, указываете какую базу использовать. Вторая строка указывает СУБД, что следует изменить объект таблица с именем Mans. И, наконец, третья строчка словами ADD CONSTRAINT (пер. Добавить ограничение) добавляет ограничение с именем PK_Mans_Id и типом PRIMARY KEY на столбец Id. Если требуется создать составной ключ, то столбцы в скобках указываются через запятую.
Важное замечание: первичный ключ на таблице может быть только один.
UNIQUE
Ограничения уникальности довольно простой тип ограничений, поскольку практически полностью повторяет ограничение первичного ключа, с той лишь разницей, что Unique ограничений можно задавать сколь угодно много (на самом деле для SQL 2005 255 ограничений). Для обозначения ограничений Unique часто используется аббревиатура AK, расшифровывается как Alternative Key, т.е. по сути Unique является альтернативным первичным ключом. Также часто используются сокращения вида U или UQ (UN).
Для данного типа ограничений важно отметить тот факт, что в отличии от ограничений первичного ключа, Unique не исключает возможности использования Null-значений. Однако такое значение в столбце может быть только одно! (Здесь следует пояснить, что хотя одно
58
Null-значение не равно другому Null-значению, с точки зрения ограничения уникальности они все равно рассматриваются как дубликаты. Данный факт противоречит общей картине троичной логики работы С БД, однако, в версии 2005 это так и об этом следует помнить)
Пример создания Unique ограничения также рассмотрим в двух вариантах:
Создание Unique при создании таблицы
Create Table Mans
(
id int identity not null primary key, Name varchar(10) not null UNIQUE
)
Создание Unique на существующей таблице
Alter Table Mans
ADD CONSTRAINT UQ_Mans_Name UNIQUE (Name)
4.1.3Ограничения домена.
Кограничениям домена относятся два вида ограничений: CHECK и DEFAULT.
CHECK
Ограничения проверки (CHECK) подобны проверкам, используемым в предикате WHERE. Т.е. все конструкции, которые можно использовать в WHERE можно применять и здесь, например between, like знаки сравнения и прочее.
Забегая вперед нужно сказать, что ограничения CHECK работают гораздо быстрее правил и триггеров и поэтому их применение очень выгодно. Обозначаются CHECK ограничения как правило CN или CH.
Создание CHECK при создании таблицы
Create Table Mans
(
id int identity not null primary key, Name varchar(10) not null
Data datetime CHECK (data <= GETDATE())
)
Создание CHECK на существующей таблице
Alter Table Mans
ADD CONSTRAINT CH_Mans_Data CHECK (data <= GETDATE())
DEFAULT
Ограничения по умолчанию (Default) указывают какие значения столбцов нужно вставить при выполнении операции INSERT, если не для всех столбцов конструкции INSERT указаны вставляемые значения. Иными словами, это ограничение вставит значения по умолчанию для столбцов, которые не указаны в INSERT.
Например, пусть у нас есть все та же таблица Mans с 3 столбцами: id, name, data
Create Table Mans
(
id int identity not null primary key, Name varchar(10) not null
Data datetime null check (data <= GETDATE())
)
и мы выполняем операцию вставки в эту таблицу следующих данных:
59
INSERT INTO Mans (Data) Values(12.01.2012)
Поскольку столбец Name не может принимать null-значение, СУБД сгенерирует сообщение об ошибке и невозможности вставить данные.
Для выхода из такой ситуации как раз и разработано ограничение по умолчанию. Во время вставки оно вставит указанные в ограничении данные для столбцов, которые не были указаны в операторе вставки. Стоит отметить тот факт, что ограничение по умолчанию срабатывает только при вставке данных в таблицу и не срабатывает при обновлении или удалении строки. Если для столбца, которому задано значение по умолчанию, во время вставки задать какое либо значение, то значение по умолчанию игнорируется и будет вставлено передаваемое значение.
Создание DEFAULT при создании таблицы
Для создания ограничения достаточно написать ключевое слово Default и значение, которое будет задаваться по умолчанию, например 0, ‘Неизвестно’ и пр. Также можно использовать функции например GETDATE для возвращения значения текущей даты.
Create Table Mans
(
id int identity not null primary key, Name varchar(10) not null,
Data datetime null DEFAULT GETDATE()
)
Создание DEFAULT на существующей таблице
Данный способ задания ограничений несколько отличается от приведенных ранее слова ADD CONSTRAINT остаются, но дополнительно необходимо указать ключевое слово FOR, которое указывает СУБД какой столбец является целевым для наложения ограничения Default.
Alter Table Mans
ADD CONSTRAINT DF_Mans_Data DEFAULT GETDATE FOR data
В ранних версиях MS SQL Server применялись конструкции похожие на ограничения по умолчанию. Они имеют такое же название, однако отличаются способом создания. В результате чего может возникнуть путаница между заданным по умолчанию значением и ограничением DEFAULT. Задаются они так:
CREATE DEFAULT UnknowDef AS ‘Неизвестный’
Созданное значение по умолчанию пока не привязано ни к одному столбцу и ни к одной таблице. Для того чтобы привязать его к столбцу таблицы необходимо вызвать специальную процедуру sp_bindefault
Вызов процедуры выглядит так:
Exec sp_bindefault ‘UnknowDef’, Mans.Name
Хранимые процедуры будут рассмотрены позже, а пока необходимо запомнить разницу между ограничениями по умолчанию и значениями по умолчанию. Важно также сказать, что в любых новых базах данных нужно использовать именно ограничения, а не значения по умолчанию.
Еще одним устаревшим инструментом являются правила, они аналогичны ограничениям CHECK. Разница состоит в следующем:
Правило сначала создается в общем виде:
CREATE RULE <Имя правила> AS <условие проверки>
Например, код Create Rule MoreThenZero As @var > 0 создаст правило на проверку положительности. Однако данное правило не привязано ни к одной таблице. Для того чтобы это сделать необходимо выполнить процедуры sp_bindrule (процедуры привязки находятся в БД master)
Exec sp_ bindrule MoreThenZero, Mans.Id
Следует сказать, что поддержка этих двух устаревших инструментов ведется только для совместимости с предыдущими версиями СУБД. В создаваемых базах данных не просто не
желательно их использование, - оно категорически противопоказано!
60
