Professional
SQL Server 2000 Database Design
Louis Davidson
wrox
Wrox Press Ltd.
Луис Дэвидсон
Проектирование баз данных на
SQL Server 2000 Перевод с английского
Москва БИНОМ. Лаборатория знаний 2003
УДК 004.65 ББК 32.973 ДП Переводчик В. А. Епанешников
Д11
Дэвидсон Л. Проектирование баз данных на SQL Server 2000 / Л. Дэвидсон; Пер. с англ. — М.: БИНОМ. Лаборатория знаний, 2003. — 680 с , ил. ISBN 5-94774-074-5 Книга посвящена вопросам проектирования баз данных (БД) с использованием пакета профессионального проектирования SQL Server 2000 фирмы Microsoft. Здесь приведен методический подход ко всем стадиям проектирования БД па основе опыта автора. Рассмотрены как вопросы логического проектирования, когда выбирается структура и основные характеристики базы данных, так и вопросы физического проектирования, включая реализацию, когда выбираются технические средства, программное обеспечение, пишутся программы, проводится необходимая отладка. Автор глубоко не вдавался в теоретические вопросы и использовал только необходимые с его точки зрения результаты. Изложенный материал иллюстрируется лишь небольшими примерами, позволяющими лучше понять те или иные рассматриваемые вопросы. Книга будет полезна как специалистам по проектированию БД, так и инженерам, обслуживающим ту или иную базу данных, созданную на основе SQL Server 2000. Отдельные части могут быть использованы преподавателями вузов для подготовки курсов по проектированию баз данных. УДК 004.65 ББК 32.973
По вопросам приобретения обращаться: в Москве «БИНОМ. Лаборатория знаний» (095)955-03-98, e-mail:
[email protected] в Санкт-Петербурге
«Диалект» (812)247-93-01, e-mail:
[email protected]
Copyright © 2001, Wrox Press, Ltd © Перевод на русский язык, БИНОМ. Лаборатория знаний, 2003
ISBN 5-94774-074-5 (русск.) ISBN 1-861004-76-1 (англ.)
II
Подтверждения торговых марок Wrox старался обеспечить информацию о торговых марках всех компаний и продукции, упомянутых в этой книге соответствующим использованием заголовков. Однако Wrox не может гарантировать точность этой информации.
Список участников издания Автор Луис Дэвидсон
Менеджер серии Брюс Лоусон
Дополнительный материал Гэри Эванс Чарлз Хокинс Крис Ульман Адриан Янг
Технические редакторы Ховард Дэвис Пол Джефкот Гарет Окли Дуглас Патерсон
Технические рецензенты Мартен Больё Майкл Бенкович Максим Бомбардьер Роберт Чанг Джемс Р. Де-Карли Робин Дьюсон Майк Эриксон Эдвард Д. Феррон Скотт Ханзельман Марк Хорнер Симон Робинсон Марк X. Симкин Ричард Уорд Хельмут Уотсон Захр Юнее
Агент автора Тони Берри
Технические конструкторы Кэтрин Александер Кэт Холл Корректор Фиона Берриман
Администраторы проекта Аврил Корбин Силмара Лайон Координатор производства Пип Вонсон Индексация Билл Джонкокс Рисунки Шабнам Хуссейн Дополнительные рисунки Пип Вонсон Симон Хардвеа Обложка Шелли Фразьер Менеджер производства Симон Хардвеа
Сведения об авторе Луис — общепризнанный знаток баз данных. В настоящее время он трудится в качестве разработчика структуры базы данных для Сетей образования Америки в Нэшвилле, штат Теннесси, не говоря уже о написании этой книги и, возможно, еще одной. Он был проектировщиком и разработчиком баз данных на Microsoft SQL Server в течение девяти лет, начиная с его знаменательной неудачи как администратора локальной сети для Международной церковной штаб-квартиры в Кливленде, штат Теннесси (да, имеется такая в Кливленде, штат Теннесси). Начиная с объединения SQL Server с Visual Basic 1.0, он занимался этим продуктом в течение девяти лет, хотя его навыки в Visual Basic прогрессивно ухудшались с версии 4.0, поскольку он сосредоточился прежде всего на SQL Server. Луис выступал на ряде конференций, таких как CA-World (конференция Всемирной компьютерной ассоциации) в 1999 и 2000 гг., относительно решений с ERwin (пакет для проектирования структур баз данных); и PASS 2000 Europe, где он сделал две презентации: одну — по нормализации и другую — по оптимизации запросов. В семье у него жена Валери и дочь Крисси, с которыми он мечтает провести хотя бы один свободный день с тех пор, как установил на своем столе SQL Server и Visual Basic девять лет тому назад...
Благодарности Эта книга посвящена памяти моего отца
Я хотел бы также поблагодарить следующих лиц, которые непосредственно или косвенно влияли на появление этой книги: •
Господа нашего Иисуса Христа, кто создал меня со способностями проектировать и создавать компьютерное программное обеспечение.
•
Мою жену Валери и дочь Крисси, которые не видели меня в течение шести месяцев без переносного компьютера, даже во время отпуска; благодарю их за любовь и терпение.
•
Мою маму, без которой ...
•
Чака Хокинса за помощь в преодолении моих затруднений по материалам главы 13.
•
Чипа Брокера, Доналда Пластера, Майкла Фармера, которые были моими наставниками в течение последних лет.
•
Всех влиятельных людей, с которыми я в настоящее время работаю, и которые терпели меня в течение ряда месяцев при условии ограниченного сна. > ;
•
Всех профессоров, академиков, авторов и т. д., чьи курсы, книги и Web-страницы я читал в течение ряда лет, и которые прямо или косвенно заронили семена знаний в мою память.
•
Штат издательства WROX, кто (за исключением долгих ночей!) общался со мной очень хорошо и сделал все, что произошло.
•
Редакторов книги, которые помогли сформировать и объединить совокупность хаотичных идей в книгу, которую вы держите в своих руках.
Содержание Введение Что охватывает эта книга Кто должен читать эту книгу? Что вам нужно, чтобы использовать эту книгу Используемые соглашения по шрифтам Поддержка клиента
\ 2 2 2 3 4
Часть 1 — Л о г и ч е с к о е п р о е к т и р о в а н и е
5
Глава 1 : В в е д е н и е в м е т о д о л о г и ю Б Д
7
Введение История структур БД OLTP и организация хранилища данных Четыре модуля современной СУБД OLTP Оперативное хранилище данных (ODS) Хранилище данных Витрины данных Структура
Учебный пример Реляционное программирование по сравнению с процедурным программированием Организация циклов Доступ к сети
Краткий обзор процесса проектирования БД на основе SQL Server Резюме
7 8 9 10 11 12 13 14 16
16 17 17 18
19 20
Глава 2 : С б о р и н ф о р м а ц и и д л я п р о е к т а Б Д
21^
Команда проектировщиков БД Документация и связь Одобрение клиента Минимальные информационные требования Опытные образцы БД
22 23 23 24 24
Содержание Интервью с клиентами О чем следует спрашивать Кто будет использовать данные? Как данные будут использоваться? Что вы хотите видеть в отчетах? Где сейчас находятся ваши данные? Сколько эти данные стоят? Как будут данные в новой БД сочетаться с другими данными? Имеются ли какие-то правила, которые управляют использованием данных?
Другие источники для определения правил данных Запрос о цене или запрос предложений Контракты или наряды на работу клиента Соглашения об уровне обслуживания Не забудьте о ревизиях Старые системы Отчеты, формы, электронные таблицы
Схема учебного примера Интервью клиента Предварительная документация
Резюме Глава 3 : Фундаментальные концепции БД Введение Реляционная модель База данных Таблица Строки и столбцы Ограничение значений столбца только допустимыми значениями Идентификаторы строк Необязательные значения столбца Число строк Дополнительные соображения насчет таблиц SQL-операции Ограничение Набор Соединение Произведение Объединение Пересечение Разность Деление Другие операторы Отношения Бинарные отношения Небинарные отношения Последнее замечание относительно реляционной/SQL терминологии
ii
25 27 27 27 27 28 28 29 29 29 29 30 30 30 30 31 31 32 33 36 37
37 38 38 39 41 43 47 49 50 51 54 55 55 55 56 57 57 58 58 58 59 59 62 63
Содержание Определения существенных понятий Функциональная зависимость Зависимость с несколькими значениями
Резюме Глава 4 : Сущности, атрибуты, отношения и бизнес-правила Введение Отказ от выбора окончательной структуры на ранних стадиях Простой пример
Определение сущностей Люди Места Объекты Идеи Документы Другие сущности
Список сущностей
63 63 64
64 65 65 66 67
67 68 68 69 69 69 69
70
Определение атрибутов и доменов
72
Идентификаторы Описательная информация Указатели на расположение Связанная информация Список сущностей, атрибутов и доменов
72 73 74 75 75
Отношения между таблицами Отношения "один к п" Отношение "имеет" Отношение "является" Отношения "многие ко многим" Список отношений
Определение бизнес-правил Идентификация основных процессов Где мы сейчас? Определение необходимых дополнительных данных Повторное рассмотрение с клиентом Повторяйте ваши действия, пока не получите одобрение заказчика
Учебный пример книги Интервью клиента Документы Перечень объектов банковского счета Бизнес-правила и процессы
Метод водопада
77 77 78 78 79 79
81 83 84 85 86 86
87 87 90 93 98
100 iii
Содержание Резюме
101
Глава 5: Моделирование данных Введение Методологии моделирования UML IDEF1X Модели действий Модели данных
103 103 104 104 105 105 108
Сущности Тип сущности Обозначение Атрибуты Первичный ключ Вторичные ключи Внешние ключи Домены Обозначение Отношения Один ко многим Идентифицирующие отношения Неидентифицирующие отношения Обязательное отношение Необязательное отношение Мощность отношения Функциональные имена Другие типы отношений Рекурсия Отношения классификации Многие ко многим Глагольные конструкции (обозначения) Другие методы изображения отношений
109 109 111 112 112 113 114 115 117 118 119 119 120 121 122 123 124 125 125 127 129 130 132
Описательная информация
135
Учебный пример
137
Модель действий М одел ь дан н ых
137 140
Резюме
147
Глава 6 : Методы нормализации
149
Почему нужна нормализация?
150
Уменьшение значений NULL Устранение избыточных данных Устранение ненужного кодирования Максимизация кластерных индексов Уменьшение числа индексов на таблицу Хранение "тонких" таблиц
150 150 151 151 151 151
iv I
I
I!
Содержание Процесс нормализации
152
Первая нормальная форма Все атрибуты должны быть элементарными Экземпляры сущности должны содержать одно и то же число значений Все экземпляры записей сущности должны быть различны Программирование аномалий, которых следует избегать в 1НФ Признаки, что существующие данные не удовлетворяют 1НФ
152 153 155 155 155 158
Вторая нормальная форма
158
Сущность должна соответствовать 1НФ Каждый неключевой атрибут должен описывать весь ключ Проблемы программирования, которых можно избежать с помощью 2НФ Признаки, что ваши сущности не удовлетворяют 2НФ Кодирование при наличии этой проблемы
159 159 161 162 163
Третья нормальная форма
163
Сущность должна быть в 2НФ Неключевые атрибуты не могут описывать неключевые атрибуты Проблемы программирования, которые можно избежать с помощью ЗНФ Признаки, что ваши сущности не удовлетворяют ЗНФ Нормальная форма Бойса-Кодда Признаки программных аномалий
164 164 165 167 168 172
Учебный пример
172
Первая нормальная форма
172
Атрибуты должны быть элементарными Все экземпляры записей сущности должны содержать одно и то же число значений Все экземпляры записей сущности должны быть различными Нормальная форма Бойса-Кодда Итоговые данные Несколько полей с одинаковыми префиксами . Каждый детерминант должен быть ключом Модель
172 174 174 174 175 175 176 179
Резюме Глава 7 : Расширенные темы нормализации Введение Четвертая нормальная форма Тройные отношения Скрытые атрибуты с несколькими величинами Предыстория атрибутов
Дополнительные нормальные формы Денормализация Учебный пример Резюме Глава 8 : Завершение фазы логического проектирования Введение
179 181 181 182 183 189 192
195 196 197 203 205 205
Содержание Использование данных
206
Создание отчетов Стратегия получения отчетов Прототипы отчетов Определение использования данных и права доступа Безопасность Известные ограничения структуры Взаимодействие с внешними системами Пример 1 — Проблематичная система планирования ресурса предприятия Пример 2 — Другой пример "Как не надо делать" Пример 3 — Системы, страдающие от денормализации Дополнительные проблемы, возникающие при взаимодействии с системами третьих лиц Планы преобразования данных
207 208 210 210 211 212 213 213 214 215 216 217
Планирование требуемых объемов План проекта Окончательный обзор документации
217 218 218
Дальнейшие требования
220
Учебный пример
220
Использование данных Отчеты Взаимодействие с внешними системами План преобразования данных Требуемые объемы План проектирования Безопасность Окончательный обзор документации
220 222 224 225 225 227 228 229
Резюме
229
Часть II — Физическое проектирование и реализация
231
Глава 9 : Планирование физической структуры
233
Введение
233
Проблемы формирования отчетов
235
Размер данных Сложность Требования поиска Конкуренция доступа пользователей Своевременность Частота
Влияние характеристик Скорость соединения Количество данных Бюджет Число пользователей vi
•
235 237 237 238 238 239
239 240 242 243 243
Содержание Возможности SQL Server Тиражирование Связанные серверы Сервисы преобразования данных Контроллер распределенных транзакций Объекты управления данными SQL Реализация СОМ-объектов Почта SQL Полнотекстовый поиск Агент SQL Server
Примеры основных топологий "Тонкий" клиент в сравнении с "толстым" клиентом "Толстый" клиент "Тонкий" клиент То, что в промежутке Клиент и конфигурация данных Классический клиент-сервер Трехзвенная конфигурация Сложное использование Web-серверов Глобальная сеть
Учебный пример Резюме Глава 1 0 : Планирование и реализация основной физической структуры Введение Средства генерации БД Схема физического проектирования Преобразование нашего логического проекта Подтипы Другие причины отхода от логического проекта Таблицы Обозначение Владелец Ограничения Столбцы Обозначение Выбор типов данных Точные числовые данные Приближенные числовые данные Дата и время-дата Двоичные данные Строки символов Вариантные данные Другие типы данных Типы данных, определяемые пользователем Необязательные данные
245 245 246 246 246 247 248 248 249 249
250 250 250 250 252 253 253 254 255 256
256 257 259 259 260 261 261 262 266 266 267 268 269 270 270 272 272 277 279 282 283 285 288 292 296 vii
Содержание Вычисляемые столбцы Служебные столбцы Управление параллелизмом
.
Сопоставление (порядок сортировки) Ключи
304 307
Индексы Первичные ключи Вторичные ключи Обозначение Другие индексы Отношения Внешние ключи Обозначение Каскадирование удалений и обновлений Взаимные отношения БД Распределение деталей вашей БД между разработчиками Информационная схема и системные хранимые процедуры Новые описательные свойства
307 315 317 318 319 319 319 320 322 324 325 325 330
Учебный пример Очистка модели Подтипы Упрощение сложных отношений Таблицы доменов Удаление ненужных сущностей Коррекция ошибок в модели
Подготовка модели для реализации Служебные столбцы Первичные ключи Типы данных
Физическая модель
Резюме
Глава 1 1 : Обеспечение целостности данных Введение Примеры таблиц Определяемые пользователем функции Скалярные функции Встроенные функции формирования таблицы Связывание схемы Функции со многими операторами, формирующие таблицы Что определяемые пользователем функции не могут делать
VIII
298 300 300
.
335 335 336 338 339 340 340
342 342 342 346
353
355
357 357 359 360 361 365 366 369 369
Ограничения
371
Ограничения, связанные со значениями по умолчанию Функции Ограничения-проверки Обработка ошибок, вызванных ограничениями Триггеры Кодирование триггеров Обработка ошибок
372 373 375 383 386 387 397
Содержание Т р и г г е р ы AFTER
400
Триггеры INSTEAD OF Использования триггеров INSTEAD OF
407 411
Код клиента и хранимые процедуры Изменчивые правила
417 419
Учебный пример
423
Ограничения по умолчанию Ограничения-проверки Триггеры Удаление времени из дат сделок Проверка согласования города и почтового индекса Распределения сделок не могут превышать размещенную сумму
423 424 431 431 432 433
Необязательные правила
435
Резюме
Глава 1 2 : Расширенный доступ к данным и методы корректировки Введение Рассмотрение запросов
436
437 437 438
Транзакции Блокировка Основные рекомендации
438 443 451
Основные проблемы кодирования
454
Временные таблицы Курсоры Обработка значений NULL Представления Что нового в представлениях? Хранимые процедуры Возвращаемые хранимыми процедурами значения Обработка ошибок Инкапсуляция ' Безопасность Счетчик транзакций
454 459 468 474 478 481 482 485 492 494 495
Общие способы работы с хранимыми процедурами
500
Восстановление данных Создание строк Корректировка строк Удаление строк Пакеты SQL-кода . Пакеты с одним оператором Пакеты с несколькими операторами Компилируемый SQL-код по сравнению с нерегламентированным SQL-кодом при создании приложений Нерегламентированный SQL-код Компилируемый SQL-код Советы
500 506 508 511 513 514 514 517 517 520 523
IX
Содержание
Рассмотрение безопасности Безопасность на уровне строк Безопасность на уровне столбцов Р а с с м о т р е н и е взаимодействия м е ж д у БД Тот же самый сервер Обозначение при использовании того же самого сервера Безопасность на том же самом сервере Корректировка данных на том же самом сервере Различные серверы (распределенные запросы) Учебный пример Основные хранимые процедуры Процедура размещения сделки Процедура обновления таблицы bank Процедура удаления получателя платежа Процедура формирования списка счетов Процедура заполнения типов доменов сделок Специальные хранимые процедуры Действие по выполнению баланса счета Действие по получению информации о счете Безопасность в учебном примере Резюме
Глава 1 3 :О п р е д е л е н и е т р е б о в а н и й к а п п а р а т н ы м с р е д с т в а м
523 527 529 530 530 530 530 531 532 533 534 534 535 537 538 539 539 540 541 542 543
545
Введение
545
Типы БД
546
OLTP OLAP
546 546
Рост OLTP-таблиц
547
Быстрый рост Медленный рост Отсутствие роста
547 547 548
Рост OLAP-таблиц
548
Рост пакетов Рост в связи с ростом компании "Нам нужно больше отчетов!" Не забывайте об индексах
548 549 549 549
Вычисление полного размера таблицы
549
Вычисление размера данных
550
Повторное рассмотрение индексируемых структур в виде В-дерева
553
Вычисление размера индекса
554
Размер загрузки транзакции
557
Содержание
Архивирование данных в случае необходимости Стоимость архивирования Детали архивирования Период архивирования Архивирование таблиц фактов с разделением по датам Архивирование таблиц фактов в соответствии с характеристиками Доступ к архивированным данным
Характеристики сервера Подсистемы памяти Подсистемы памяти на сервере Windows NT Подсистемы памяти на сервере Windows 2000
Управление работой памяти Искусство управления работой Управление работой SQL и узкие места Настройка памяти: ОС и SQL Server Динамическая настройка памяти в SQL Server 2000 Целевая свободная память Настройка памяти для нескольких экземпляров Настройка памяти процесса в SQL Server
Настройка работы сервера Подсистемы ЦП Контроль работы ЦП Волоконное управление
Дисковые подсистемы Основная дисковая подсистема Кэширование записи и SQL Server
Подсистема RAID RAID 0 RAID 1 RAID 5 RAID 10 — где 0 + 1 не равняется 1 Решения для нескольких контроллеров/каналов Настройка диска и контроль работы
Соединения пользователя Запирание и блокировка Демонстрация блокировки Контроль блокировок
Учебный пример Резюме Глава 14: Завершение проектирования Введение
559 560 560 561 561 562 563
563 563 564 564
565 566 567 568 569 570 571 572
573 574 575 576
576 577 577
578 578 579 580 581 581 582
583 585 585 586
588 594 595 595 XI
Содержание Настройка функционирования Поддержка Б Д только для чтения Моделирование изменений
596 598 600
Метод, ориентируемый на таблицы Метод, ориентируемый на решение Реализация Время ожидания Реализация Использование Задачи простого формирования отчетов Оперативное хранилище данных
601 , 605 607 609 610 613 614 615
Модели данных предприятия
618
Переход от опытного варианта Разработка
619 621
Аппаратные средства разработки Проверка характеристик Аппаратные средства проверки характеристик Реализация Обслуживание и планы восстановления поломок
Учебный пример Резюме П р и л о ж е н и е А: 1 2 правил К о д д а для С У Р Б Д Правило Правило Правило Правило
1 : Информационное правило 2: Правило гарантированного доступа 3: Систематическая обработка значений NULL 4: Д и н а м и ч е с к и й оперативный каталог, основанный на реляционной модели Правило 5: Правило общего языка манипулирования данными (ЯМД) Правило 6: Правило к о р р е к т и р о в к и представления Правило 7 : Операции добавления, к о р р е к т и р о в к и и удаления высокого уровня Правило 8: Ф и з и ч е с к а я независимость данных Правило 9 : Л о г и ч е с к а я независимость данных Правило 1 0 : Независимость целостности данных Правило 1 1 : Независимость распределения Правило 1 2 : Правило единственности Заключение
Предметный указатель
XII
621 621 622 622 622
624 628 629 629 630 630 630 631 631 632 632 632 634 634 634 635 637
Введение Если вы, находясь в вашем любимом книжном магазине, внимательно листаете эту книгу, потому что это — книга издательства WROX, я знаю, что вы, вероятно, думаете: "А где же здесь все про коды, настройки и всякое подобное?" Да, это книга не совсем такого рода. (Нет ничего предосудительного в этом виде книг; у меня на моем рабочем столе их тоже очень много.) Что я хотел собрать воедино в этом случае — это создать книгу по проектированию баз данных (БД), которая тонко балансирует между ориентированными на реализацию книгами по SQL Server, сосредоточенными на настройках DBCC (database consistency checker — модуль контроля непротиворечивости БД), выборе индексов и использовании всех кнопок и средств управления SQL Server, с одной стороны, и в полном смысле слова академическими томами, которые уходят глубоко в теорию БД, но дают немного или совсем никакой практической информации, с другой стороны. Эта книга охватывает процесс реализации БД с момента, когда кто-то говорит вам, что он хочет иметь базу данных, всю процедуру формирования таблиц и осуществление доступа к этим таблицам. Она включает также нормализацию таблиц вплоть до пятой нормальной формы. Цель этой книги проста; создать лучшую в мире книгу, такую, что если вы купите десять ее экземпляров, пошлете их десяти вашим лучшим друзьям, и попросите их сделать то же самое, я смогу купить прелестный компьютер с DVD-плеером. Реальная цель, однако, не столь очевидна. Я рассмотрел в течение ряда лет большое количество созданных программ, и независимо от того, насколько хороши были модели объекта, сопровождающий код, или даже сопутствующая документация, сама БД обычно находилась в диапазоне от плохого до ужасного. Мое желание состоит в том, чтобы обеспечить информацией конструкторов БД, что поможет им создавать надлежащие БД. Для
Введение любого, кто имел удовольствие читать учебники по БД для колледжей, ясно, что они несколько суховаты, — что-то вроде пустыни Мохаве в засуху. Это довольно плохо, так как большая часть пропущенной в них информации полезна и уместна. Так что моя переформулированная цель состоит в том, чтобы устранить разрыв между интеллектуальными гениями и нами, рабочими-программистами, и дать необходимые знания, подходящие для реального мира.
Что охватывает эта книга Эта книга состоит из двух частей. Первая охватывает логическое проектирование БД, а вторая рассматривает физическое проектирование и реализацию. Каждая из этих частей начинается с введения, содержащего общий краткий обзор, затем резюме тем, охватываемых каждой из глав. Мы не будем воспроизводить данный материал здесь — обратитесь, пожалуйста, непосредственно к соответствующим страницам.
Кто должен читать эту книгу? Не любая глава этой книги обязательно потребуется каждому читателю, и части ее также не предназначены для каждого программиста. Мне жаль, что это так, потому что в каждой главе имеется материал, который обогатит их способность проектировать и реализовывать БД. Однако это неизбежное зло — каждый знакомится с тем, что ему нужно. Системный архитектор БД Если вы уже системный архитектор БД, кто отвечает за сбор требований и проектирование БД (с причастностью/ответственностью за реализацию), то, пожалуйста, читайте всю книгу, — возможно, пропустив третью главу при первом чтении. Конструктор БД Если вы занимаетесь только реализацией БД, большая часть этой книги должна интересовать вас так, чтобы вы поняли причины, почему "безумный" проектировщик структуры данных хочет создать БД с пятьюдесятью таблицами, когда вы думаете, что требуется только три. Первое чтение могло бы включить главу 5 по моделированию данных, главы 6 и 7 по нормализации и полностью часть II книги. Эта часть описывает все методы реализации БД. Программист БД Если вы прежде всего пишете код для SQL Server, то при первом чтении вам будет интересна весьма небольшая часть этой книги. Хорошо в этом случае включить в чтение главу 5 по моделированию данных, главы 6 и 7 по нормализации, далее главы 9 и 10 по реализации БД, совместно с главами 11 и 12 по обращению к данным.
Что вам нужно, чтобы использовать эту книгу В первой половине книги мы будем обсуждать логическое моделирование данных. Нет никаких требований к программному обеспечению для работы с этой частью книги.
Введение Во второй части, связанной с физическим проектированием, единственное требование для работы с примерами — установленная копия пакета SQL Server 2000 и Query Analyzer (анализатор запроса), который поставляется вместе с пакетом. Это может быть любое издание SQL Server (версия Personal Edition — версия, которую можно поставить на вашем персональном компьютере), если вы сможете соединить ее с Query Analyzer. Вам потребуется база данных, которую вы можете создать с пользователем, выступающим в роли собственника БД, поскольку мы будем создавать все объекты с точки зрения собственника БД. Если вы не имеете копии SQL Server, оценку примера можно выполнить на SQL Server фирмы Microsoft, находящемся на Web-сайте www.microsoft.com/sql.
Используемые соглашения по шрифтам При пролистывании книги вы сталкиваетесь с различными стилями. Это было сделано, чтобы помочь вам легко идентифицировать различные типы информации и не пропустить каких-либо ключевых моментов. Эти стили следующие:
Важная информация, ключевые моменты и дополнительные объяснения показаны выделением подобно этому абзацу. Убедитесь, что вы обратили внимание на них, когда их встречаете.
Общие примечания, вспомогательная информация и краткие замечания выглядят так, как этот абзац. •
Кнопки, которые вы нажимаете на клавиатуре, подобно Ctrl и Delete, показаны курсивом.
•
Если вы видите что-то вроде BackupDB, это будет обозначать имя файла, название объекта или заголовок функции.
•
Первый раз, когда вы сталкиваетесь с важным термином, он изображается полужирным шрифтом.
•
Слова, которые появляются на экране, типа выбора меню, изображаются шрифтом, подобным тому, который используется на экране, например, меню File.
Когда образцы кода изображаются первый раз, они оформляются следующим образом:
I
Private Sub Command_Click MsgBox "Don't touch me" End Sub
С другой стороны, если вы уже видели код, или он не относится непосредственно к сути вопроса, то он изображается следующим образом: Private Sub Command_Click MsgBox "Don't touch me" End Sub
Введение
Поддержка клиента Мы хотим знать, что вы думаете об этой книге: что вам понравилось, что не понравилось, и что, по вашему мнению, мы можем сделать лучше в следующий раз. Вы можете послать ваши комментарии по электронной почте (по адресу feedback@wrox. com). Пожалуйста, не забудьте упомянуть название книги в вашем сообщении. Wrox имеет группу поддержки, так что если у вас появятся любые вопросы, пожалуйста, пошлите их также по адресу вышеупомянутой электронной почты.
Исходный код Полный исходный код для примеров, используемых в этой книге, может быть получен с Web-сайта фирмы по адресу: http://www.wrox.com.
Опечатки Мы приложили все усилия, чтобы удостовериться, что нет никаких ошибок в тексте или коде. Однако человеку присуще ошибаться, и поэтому мы считаем нужным информировать вас относительно любых ошибок: как их определить и исправить. Списки опечаток для всех наших книг находятся по адресу www.wrox.com. Если вы найдете ошибку, которая не была еще выявлена, пожалуйста, сообщите нам. p 2 p . w r o x . c o m Для поддержки авторов и подобных им лиц служит список адресатов SQL Server. Наша уникальная система обеспечивает взаимопомощь программистов (торговая марка programmer to programmer) с помощью списка адресатов, форумов и сетевых конференций в дополнение к системе нашей непосредственной электронной почты. Можете быть уверены, что ваш вопрос исследуется не только профессионалом поддержки, но многими Wrox-авторами и другими экспертами промышленности из наших списков адресатов. В p2p.wrox.com вы найдете список, специально нацеленный на разработчиков, использующих-SQL Server, которые поддержат вас не только в то время, когда вы читаете эту книгу, но также и тогда, когда начнете разрабатывать ваши собственные приложения. Зарегистрироваться на такую поддержку следует с помощью четырехступенчатой системы: 1.
Выйти на p2p.wrox.com.
2.
Щелкнуть на sql_server как на тип списка адресатов, к которому вы желаете присоединиться, затем щелкнуть Subscribe (присоединиться).
3.
Заполнить ваши данные и снова щелкнуть Subscribe.
4.
Дать подтверждение электронной почты.
Почему эта система дает лучшую поддержку Вы можете пожелать присоединиться к спискам адресатов или можете получать их как еженедельный обзор. Если вы не имеете времени или средств, чтобы получить список адресатов, то можете поискать наш диалоговый архив. Барахло и бесполезные сообщения удаляются, и адрес вашей электронной почты защищен уникальной системой Lyris. Любые вопросы относительно присоединения или отключения списков или любые другие вопросы относительно списка следует посылать по адресу
[email protected].
Часть 1 — Логическое проектирование Логическое проектирование БД — одна из наиболее важных задач в любом проектировании БД, и все же, вероятно, также и наименее понятная. Данная часть книги определенно нацелена изменить это, объединяя небольшую теорию БД с некоторыми практическими советами, рассматривая также ряд общих методов, которые часто пропускаются. В этой части мы закладываем основы для физической реализации нашего проекта, которой посвящена вторая половина книги, и при этом охватываем следующий материал: •
Глава 1 Введение в методологию БД — Как следует из названия, приведено краткое представление различных методологий БД, которые обычно используются для реализации полномасштабных систем БД (СУБД — систем управления БД), типа OLTP-БД (On-Line Transaction Processing — обработка транзакций в реальном масштабе времени), хранилищ данных, оперативных хранилищ данных и витрин данных.
•
Глава 2 Сбор информации для проекта БД — В этой главе мы даем краткий обзор процесса определения требований, которые нужны пользователям СУБД, рассматривая некоторые неясные моменты, где скрываются важные данные.
•
Глава 3 Фундаментальные концепции БД — Основы понимания концепций реляционной теории фундаментальны для процесса проектирования БД и рассматриваются здесь. Это обеспечит основу для разработки нашего проекта.
•
Глава 4 Сущности, атрибуты, отношения и бизнес-правила — В этой главе мы начнем процесс превращения информации, собранной в главе 2, в логический проект нашей реляционной БД, в частности, продумывая сущности, которые нам потребуются.
Часть 1 — Логическое проектирование •
Глава 5 Моделирование данных — Как только мы выделили объекты, нам нужен способ показать и распределить информацию между программистами и пользователями. Модель данных — наиболее эффективное средство для изображения информации проектируемой БД.
•
Глава 6 Методы нормализации — Нормализация — процесс использования информации, которую мы собрали в главе 2 и разработали в главе 4 и главе 5, и превращения ее в хорошо структурированный проект модели данных. В этой главе мы рассматриваем правила нормализации, которым должны следовать при проектировании хорошо структурированной модели нашей системы.
•
Глава 7 Расширенные темы нормализации — Эта глава пользуется предыдущей главой, расширяя основные методы нормализации за пределы приемов, знакомых большинству программистов. Таким образом, мы сможем точно отрегулировать наш логический проект, чтобы избежать, насколько возможно, любых аномалий данных.
•
Глава 8 Завершение фазы логического проектирования — Как только мы разработали "совершенную" БД, нам нужно вернуться к первоначальным описаниям, чтобы гарантировать, что данные, которые мы собираемся хранить, будут обеспечивать потребности пользователей. В этот момент многие программисты готовы приступить к кодированию, но важно завершить логическую фазу проектирования двойной проверкой нашей модели и ее документации, чтобы попробовать минимизировать уровень изменений, которые потребуются, когда мы перейдем к ее физической реализации.
Введение в методологию БД Введение Проектирование БД можно рассматривать как интересную смесь искусства и науки. Наука проектирования БД хорошо проработана, с рядом зрелых методов проектирования структур БД (типа нормализации), которые использовались в течение долгого времени. Фактически они почти древние в смысле компьютерной терминологии, и при проектировании вашей БД этим правилам не так уж трудно следовать. Как мы увидим, сравнительно просто перевести такое проектирование на создание таблиц SQL Server. Однако, если вы, конечно, не разрабатываете очень маленькую СУБД, это не завершение задачи. Искусство создания структуры БД проявляется тогда, когда мы должны обеспечить в наших решениях распределение, кластеризацию, избыточность, поддержку круглосуточного и ежедневного функционирования, использование процедур, триггеров, ограничений, целостности и т. д. Для физической реализации БД не существует специальных методов, и порой мудрено определить, какой метод является лучшим. В этой книге будут представлены хорошие методы, которые, наверное, помогут вам проектировать лучшие БД. Я стараюсь представить информацию таким образом, чтобы быть достаточно ясным для новичков, но в то же самое время полезным даже для наиболее закаленных профессионалов. Одну вещь нужно твердо понять, прежде чем мы пойдем далее. Проектирование и структура БД по-разному выглядят для тех, кто занимается настройкой и администрированием БД. Например, как системный архитектор, я редко формирую учетные записи пользователей, обеспечиваю резервирование, или даже формирую средства копирования или кластеризации и т. д. Это — работа администратора БД. Когда я работал для маленьких магазинчиков, эти типы задач были в моей области, но я никогда не чувствовал, что делал эту работу хорошо.
Глава 1 В данной книге мы глубоко рассмотрим вопросы, как проектировать, разрабатывать и реализовывать таблицы БД на SQL Server и сопутствующие им методы доступа; но мы не будем рассматривать детали физических аппаратных средств, системного программного обеспечения и другие аспекты СУБД.
История структур БД Структура БД значительно изменилась за последние несколько десятилетий. Первоначально не было никакого выбора структуры БД способами оптимизации для ограниченных аппаратных средств ЭВМ, на которых они размещались. В начале 1970-х гг. Е. Ф. Кодд, преподаватель математики, в то же время являясь исследователем фирмы IBM, представил новую концепцию, которая была предназначена навсегда изменить путь, которым будут размещаться данные. Его принципы, от которых пошла реляционная модель, опередили свое время. Многим программистам понравились его идеи, но они не могли реализовать их из-за ограничений аппаратных средств ЭВМ. Исследование Кодда реализуется в его трех правилах нормализации. Эти правила развивались, начиная с первоначальной работы Кодда, и мы объясним все общепринятые правила нормализации позже в главах 6 и 7. Хотя я и не буду непосредственно ссылаться на его работы, почти каждая частица теории реляционных БД построена на его классической статье "A Relational Model of Data for Large Shared Data Banks" (Реляционная модель данных для больших распределенных банков данных). Если вы могли прочитать эту статью или даже книгу К. Дж. Дейта "An Introduction to Database Systems" (Введение в СУБД), широко известную как техническая "библия", то, наверное, заметили, что они довольно сухие и академические. Я собираюсь представить важные идеи обычным способом, который является уместным для проектировщика БД и легким для его понимания. "A Relational Model of Data for Large Shared Data Banks" может быть найдена no адресу http://www1 .acm.org/classics/nov95/toc.html. Реляционная теория БД продолжала развиваться, и Е. Ф. Коддом и др. формулировались все более и более строгие правила относительно структуры БД. Эти правила очень ценны и сегодня широко используются, но реляционная модель практически не использовалась, пока не появилась теория. Мы подробно обсудим модель и ее преимущества позже, а пока только учтем, что нормализация требует увеличения числа таблиц БД, и любые совершенствования теории Кодда приводят к дальнейшему увеличению числа необходимых таблиц. Из-за того, что это предъявляет дополнительные требования к аппаратным средствам ЭВМ, обычным программистам БД было нелегко прочувствовать выгоды от более глубокой нормализации. Если добавить к этому факт, что большое число таблиц с большим числом связанных с этим соединений вызывают трудности с аппаратными средствами серверов и программным обеспечением 1980-х и 1990-х гг., то нетрудно понять, почему разработчики БД не могли должным образом структурировать свои БД как из-за технологических ограничений, так и от недостатка понимания. К счастью, последние достижения в технологии позволяют это пересмотреть. За последние десять лет аппаратные средства ЭВМ стремительно улучшались, улучшено и программное обеспечение серверов БД, используя алгоритмы, которые фактически были осязаемы в течение двадцати лет, но не использовались из-за ранее рассмотренных ограничений аппаратных средств. Оптимизация серверов БД для конкретного использования типа OLTP и OLAP (on-line analytical processing — аналитическая обработка в реальном масштабе времени), плюс тот факт, что Microsoft полностью переписал SQL Server, чтобы работать
8
Введение в методологию БД оптимально как с платформой Windows, так и с платформой Intel, в последние несколько лет вызвали исключительное улучшение характеристик и увеличение надежности. Эти факторы, наряду с усовершенствованием операционных систем — ОС (Operating System — OS) и развитием концепций типа организации хранилищ данных (рассмотренных позже в этой главе), позволили создать серверы БД, которые могут работать со структурированными данными надлежащим образом, как было определено тридцать лет назад. БД, построенные сегодня, разработаны с использованием лучших структур, но мы все еще плохо разрабатываем БД предыдущих лет. Даже с хорошим основным проектом, программирование БД может поставить под вопрос базу традиционного программирования типа языков С, C++ или Visual Basic. Это потому, что язык реляционного программирования SQL требует, чтобы программист заново продумал всю систему понятий: множества и отношения вместо операторов IF и циклов. Один оператор SQL при традиционном программировании может расшириться до сотен, если не тысяч, строк процедурного кода, и в некоторых случаях это так и есть. Этого не скажешь о Transact-SQL (T-SQL), языке SQL Server; он не поддерживает циклы и всякие другие основные конструкции языка программирования, но даже в T-SQL, чем большее количество работы отдается реляционному механизму, тем лучше. Другая особенность SQL, которую стоит упомянуть, состоит в том, что он является относительно легким при изучении основ, и как следствие этого, обычно не воспринимается так серьезно, как другие "настоящие" языки программирования. Однако программировать сложные вещи на SQL нелегко, и к нему не следует подходить несерьезно. В этой главе читателю представлены концепции, связанные с основными понятиями БД, в частности, различия между OLTP- и OLAP-БД; здесь также рассмотрены различия между SQL и T-SQL с одной стороны и процедурными языками программирования с другой, потому что в дальнейшем мы будем заниматься только разработкой БД.
OLTP и организация хранилища данных Об обработке транзакций в реальном масштабе времени (Online Transaction Processing — OLTP) в БД большинство людей, вероятно, думает, когда говорят о БД, что она имеет дело с текущими данными, необходимыми для бизнес-операций. Организация хранилища данных — интересное, относительно новое общее понятие БД, которое позволяет для чрезвычайно сложных отчетов строить систему исторических данных, выделенных из транзакций. Позволяя нам хранить большие количества исторических данных, оно дает возможность пользователям исследовать долговременные тенденции в истории бизнеса, в то же время избегая любых воздействий на текущую деятельность. Имеется огромное различие между этими двумя понятиями, которые могут быть получены в итоге следующим образом: •
Системы обработки транзакций в реальном масштабе времени (OLTP) работают с данными, которые используются в транзакциях. БД оптимизируется, чтобы быстро реагировать на добавление новых данных или корректировку/удаление существующих данных. Это делается так, чтобы реагировать даже тогда, когда количество данных очень большое, и имеется большое количество транзакций.
•
Хранилище данных получает результаты, которые OLTP использует при формировании отчетов. Системы OLTP всегда были вялы при формировании отчетов, так как они оптимизированы, чтобы иметь дело с постоянной корректировкой размещенных данных. Когда при формировании отчета мы хотим проверить данные
Глава 1 и произвести вычисления большого количества данных, это может серьезно ухудшить реакцию нашей БД. Кроме того, корректировка данных во время формирования отчета может привести к различным последствиям. Хранилища данных устраняют это, храня данные в форме, которая является эффективной для сложных запросов, — данные оптимизированы для определенного набора вопросов. Кроме того (это недавняя история размещения данных), вы используете для запроса не фактическую, текущую версию данных, а копию, используемую только для чтения. Это немного упрощено, но должно позволить вам эффективно представить различия между OLTP и организацией хранилища данных. В следующем разделе мы проанализируем различные модули, которые составляют каждую из этих технологий.
Четыре модуля современной СУБД За прошлые десять лет (или около того) было много обсуждений относительно проблемы, как общие данные должны быть структурированы. Мы рассмотрим один из основных вариантов, который был согласован со многими экспертами БД. Подход состоит в том, чтобы разбить всю систему на функциональные модули, которые обслуживают различные потребности, вместо использования единой технологии, чтобы реализовать все задачи. Эти различные модули следующие: •
OLTP — OLTP-БД хранит текущие данные — то есть, данные, с помощью которых БД должна управлять функционированием; здесь необходимо держать лишь небольшое количество предыстории.
•
Оперативное хранилище данных (Operational Data Store — ODS) — объединенные данные, используемые для ежедневных отчетов. Такие данные часто объединяются из нескольких несоизмеримых источников с некоторой степенью предварительной обработки, чтобы сэкономить время запроса.
•
Хранилище данных — хранилище большого объема данных для размещения почти всех структур данных и их предыстории.
•
Витрина данных (Data Mart) — специализированное хранилище данных, оптимизированное для объединений данных, используемых для определенных ситуаций, и представляющее собой подмножество хранилища данных. Витрины данных обычно обрабатываются, используя технологию, известную как аналитическая обработка в реальном масштабе времени (Online Analytical Processing — OLAP).
Рассмотрение их как модулей может показаться некорректным, но термин "модуль" используется здесь, чтобы указать, что они — отдельные части объединенной СУБД. Каждый модуль играет свою специфическую роль. По той или иной причине не всякая СУБД будет требовать всех модулей. Два основных фактора выбора необходимых модулей — количество данных, которые должны быть размещены, и параллелизм (который определяет, сколько пользователей будет обращаться к данным в любое конкретное время). Меньшие системы могут управлять формированием отчетов без создания дорогостоящих дополнительных модулей. Единственная проблема состоит в том, чтобы определить, что составляет меньшую систему? Глава 9 будет детально разбираться с этими проблемами. Посмотрим теперь несколько более пристально на эти четыре модуля, которые мы ввели выше.
10
Введение в методологию БД OLTP OLTP-БД содержит данные, используемые в процессе функционирования повседневных транзакций. Она описывается временными характеристиками, в которых отражается обработка потока, и служит источником, в котором находятся данные относительно предметной области. БД характеризуется наличием любого числа параллельных пользователей, создающих, обновляющих и удаляющих данные. Все общие данные должны быть размещены или уже были размещены (в случае исторических данных) в OLTP-БД. Структура размещения данных в OLTP строится, используя нормализацию, — специальный метод структурирования, упомянутый ранее. Нормализация уменьшает количество избыточных данных, помогая предотвратить неприятности при корректировке — наподобие того, что произошло бы, если бы вы хранили адрес заказчика в двух местах БД, а изменили бы его только в одном месте. Нормализация подробно рассмотрена в главах 6 и 7. Главная цель OLTP-БД — целостность текущих общих данных. Это достигается следующими двумя важными принципами: •
Хранить каждую текущую часть данных в единственном месте, где ее можно отредактировать так, чтобы любое изменение отражалось всюду, где она используется.
•
Обеспечение транзакционной поддержки так, чтобы многочисленные изменения БД происходили совместно. Если одно из изменений в транзакции не удается, ни одно из других изменений не должно произойти. Оставшаяся часть транзакции после этой ситуации должна реализовать откат.
Побочный эффект этого, однако, тот, что получение полезной информации может быть затруднено из-за строгой структуры данных. Поскольку OLTP-БД разработана с учетом характеристик транзакций и целостности, данные размещаются так, чтобы можно было их эффективно записывать, но не обязательно эффективно читать. Пользователь будет часто запрашивать многие таблицы, чтобы получить нужный набор информации. Эта книга нацелена на структуру и реализацию OLTP-БД. Для меньших (снова тот же термин!) систем вы можете использовать только этот тип БД. Как пример использования БД типа OLTP, рассмотрим банк, который имеет клиентов с различным количеством денег, размещенных на их счетах. Банк будет обычно иметь БД, которая содержит фамилии и контактную информацию всех клиентов. Банк также, вероятно, будет иметь много распределенных БД, чтобы обращаться ко всем различным типам счетов и денежно-кредитным сделкам (транзакциям), которые совершают его клиенты. Кажется, что лучше все эти данные разместить в той же самой БД, но так как банк имеет огромные количества данных относительно денежно-кредитных сделок, охватывающих различные географические пункты, штаты и даже различные страны наряду с большим количеством контактной информации относительно клиентов, более вероятно, что эти части будут размещены в отдельных БД. Банк может также иметь БД (или ту же самую БД, если она так организована) для будущей контактной информации о клиенте и т. д. Каждая из этих БД будет типа OLTP. Банковские системы находятся среди наиболее сложных OLTP-БД в сегодняшней глобальной экономике. Чтобы вам было легко использовать кредитную карточку где-нибудь в мире, без (по всей вероятности) возможности забрать большее количество денег, чем вы фактически имеете на счете, требуется массивная распределенная OLTP-СУБД, чтобы это реализовать.
11
Глава 1 Я должен разъяснить то, что я подразумеваю под термином "транзакция", прежде чем двинусь дальше. OLTP-БД имеют дело не только с денежно-кредитными или числовыми сделками. Термин "транзакция" означает механизм, с помощью которого вы можете гарантировать ссылочную целостность (другими словами, сохраняя определенные отношения между таблицами при вводе и удалении записи) и атомарность (концепция, означающая, что какая-то информация должна выступать как отдельная единица). Для БД это означает, что вы имеете известный набор результатов в зависимости от того, удается ли или нет какое-либо действие (или группа действий). Мы рассмотрим в главе 12 транзакции и то, как вам их использовать в коде БД. Наиболее важная вещь, которую следует понять, состоит в том, что одной из главных характеристик OLTP-БД является использование механизмов, предохраняющих данные от разрушения при любых действиях пользователя.
Оперативное хранилище данных (ODS) Идея ODS состоит в том, чтобы иметь БД, где все данные, которые вы используете в вашей деятельности, размещаются ежедневно. В зависимости от ваших требований здесь может быть также размещено ограниченное количество исторических данных. ODS разрабатывается так, чтобы разобраться и заняться некоторыми из проблем, связанными с концепцией OLTP, и которые могут быть подытожены следующим образом: •
OLTP-БД обычно имеют сложную структуру со многими таблицами. Структуры данных могут быть весьма тяжелы для понимания, и запрос информации может потребовать творческого использования SQL-языка. Исходя из моего опыта, новичок при написании запросов может загубить систему OLTP, написав неэффективные запросы из-за недостатка понимания внутренней работы SQL Server.
•
Многие OLTP-БД имеют большое количество детализирующих записей. Ежедневные действия, вероятно, не потребуют доступа к каждой сделке, выполненной в течение дня, но будет нужно, вероятно, иметь возможность получить итоговые данные. Если бы вы использовали многократные запросы в системе OLTP, все сделки должны были бы быть заново рассчитаны каждый раз, когда будет сделан запрос.
•
Не все данные размещаются в единственном источнике. Типичная организация может иметь данные, необходимые для обеспечения деятельности, размещенные во многих источниках данных. Многие, как нам бы и хотелось видеть, уже изменены, многие из общих данных все еще размещаются на универсальных ЭВМ в нереляционных БД, написанных на языке КОБОЛ.
В ODS данные объединены из всех несоизмеримых источников в общую структуру и суммированы в соответствии с предъявляемыми требованиями. Они могут обновляться так часто или нечасто, как требуется. Данные характеризуются небольшим количеством, если таковые вообще имеются, допустимых корректировок пользователем, с умеренным количеством поддерживаемой предыстории, чтобы формировать ежедневные запросы и показывать тенденции за короткие периоды. В сценарии банка, ODS вероятно имела бы все сделки в течение прошлого дня и, возможно, за прошлую неделю или месяц, размещенные таким образом, когда простой запрос выдаст любые ответы, которые вам понадобятся. Некоторые объединенные данные, если они часто требуются, также могут быть размещены. Например, можно было бы сохранять список счетов клиентов, где можно просмотреть текущий баланс в течение предыдущего дня. Этот вид данных тогда мог бы быть запрошен клиентом, чтобы посмотреть баланс в течение предыдущего дня, а также любые удаленные сделки. Следовательно, итоги всего счета не нужно получать каждый раз, когда клиент хочет видеть информацию о счете.
12
Введение в методологию БД Дополнительно могли бы быть посланы уведомления, основанные на данных, полученных в результате прокручивания балансов счетов. Хранение полученных итоговых данных не является требованием для ODS. Необходимым может быть только формирование набора таблиц, которые являются более простыми для запросов, так, чтобы пользователи могли выполнять нерегламентированные запросы. Отличный пример ODS — БД, помещенная в переносные компьютеры коммерческого штата, который находится в пути, или в карманных компьютерах для людей, которые находятся/работают вне офиса и не имеют постоянного рабочего стола. Следовательно, цель ODS может быть выполнена, обеспечивая пользователей рабочими данными, в которых они нуждаются для принятия ежедневных решений, и храня их актуальными за короткий период.
Хранилище данных Основное использование хранилища данных (Data Warehouse — DW) — поддержать принятие решения, храня столько исторической информации о структуре, сколько необходимо. Поддержка решения — довольно общий термин, который означает возможность ответить на сложные вопросы относительно того, как функционирует предметная область. Лучшие решения могут быть сделаны, когда доступно большее количество данных, нужных пользователю, которое можно просмотреть, получить их итог и уточнить. Надлежащая система поддержки решения может формировать "центр сведений" для организации. Например, если коммерческая группа в вашей компании была способна видеть коммерческие тенденции в течение десятилетнего периода, коррелированные с использованием потока рекламы в любое конкретное время, это будет конечно лучше, чем наличие тех же самых данных за один год. Или даже за месяц. Другая цель хранилища данных, как и в ODS, состоит в том, чтобы отделить активную транзакционную обработку от аспектов формирования отчетов системы БД так, чтобы мы могли делать более интенсивные запросы, которые не будут затрагивать возможность наших пользователей создавать данные и иметь к ним доступ в наших системах OLTP. Более старая копия данных из OLTP-БД размещается в хранилище данных. Частота обновления информации определяется количеством данных, потребностью пользователей, и временем, необходимым для выполнения копирования. Эти данные размещаются в виде, удобном для организации запросов, такая структура принципиально отличается от той, которая является удобной для корректировок. Никакая корректировка данных не должна никогда выполняться в хранилище данных; любые изменения должны быть сделаны в оперативной БД. Только во время, когда хранилище данных меняет свои данные, оно содержит наиболее современный набор информации, получаемой от оперативной БД. Следовательно, вы никогда не должны использовать эти данные в запросах, которые требуют современного точного ответа. Хранилище данных используется исключительно для ретроспективного анализа данных. Полное хранилище данных могло бы содержать данные за несколько лет от всех неоднородных источников в пределах организации, таких как наследство систем универсальных ЭВМ, БД на SQL Server и Oracle, и преобразовывать их в единственную БД, использующую общие структуры. Таким образом, ценные данные от наследуемых БД в организации могут быть объединены со всеми более новыми, хорошо структурированными БД (так же, как БД третьей стороны, которые используются для различных задач) в один общий набор структур, которые могут использоваться для получения информации, необходимой для принятия решений. Например, данные о людских ресурсах могли бы быть получены от одной сторонней системы, главная бухгалтерская книга — от другой, и маршрутизатор IP-адресов — от третьей. Все эти БД могут дать часть задачи, которая обеспечит ваших пользователей полной картиной, по которой они должны принять решение. 13
Глава 1 Как будет обсуждено позже, одним из наиболее важных вопросов, который вы должны рассмотреть, когда разрабатываете полную СУБД, является потенциальный диапазон запросов, которые могут потребоваться. В то время как на многие запросы можно отвечать непосредственно из OLTP-БД ("Каков текущий баланс счета клиента X?"), ряд из них придется направить в хранилище данных ("Какова средняя сумма дебета и кредита клиентов по банкоматам в течение прошлого года в каждом регионе страны?"). Хранилище данных — идеальное место для объединения данных, которые трудно объединить таким образом, как нужно вашим пользователям. Хранилище данных — огромное достижение — формируется не просто. Его не нужно рассматривать лишь как место быстрого формирования ответов на запросы. Вы будете, вероятно, желать вводить данные из многих несоизмеримых источников, некоторые из них могут изменяться, поскольку проходит время, особенно, когда данные поступают от третьих лиц (новый продавец для кадровой БД, новая версия другой системы и, что я предпочитаю, системы от компаний, которые ваша собственная компания только что приобрела — вот всего лишь несколько примеров). Так как хранилище данных банка, по-видимому, будет огромно, его, вероятно, будет тяжело использовать персоналом, осуществляющим банковские реализации и маркетинг. Им понадобились бы данные, чтобы ответить на вопросы типа "Какие из их программ работают лучше всего, когда ..." и "Какой вид программ выбрать, когда...". Страховой штат, вероятно, смотрел бы на данные с точки зрения тенденций скоростей работы с заложенным имуществом. Наличие такого обширного набора информации в БД делает новые технологии, заложенные в SQL Server, очень важными для организации хранилищ данных.
Витрины данных Витрина данных — характерная часть хранилища данных и обычно предназначена для определенных потребностей подразделения или отдела, или для определенной цели бизнеса в пределах организации. Она строится, используя специальные структуры БД, известные как схемы "Звезда" или "Снежинка". Схемы "Звезда" — фактически простые БД, с единственной таблицей фактов (таблица, содержащая информацию, которая может быть подытожена, чтобы получить детали относительно истории действий организации), связанной с набором таблиц измерений, которые классифицируют факты в таблице фактов. Следует заметить, что данные в таблице фактов, прежде всего, числовые. Схемы "Снежинка" — просто расширение схем "Звезда", где таблицы фактов могут также быть и таблицами измерений. Другой важный термин, используемый для витрин данных, который мы должны ввести, — кубы. Куб — еще один довольно странный термин, но он описывает, как OLAP-технология организует таблицы измерений нашей схемы "Звезда" или "Снежинка". Измерения таблицы фактов описываются измерениями куба, в то время как каждый элемент данных в кубе представляет факт, содержащий конкретный уровень для различных измерений куба. Рассмотрим следующие три запроса: •
Сколько всего посетителей было на нашем Web-сайте в 2000 г.?
•
Сколько всего посетителей было на нашем Web-сайте в 2000 г. и с какими разделами они ознакомились?
•
Какие категории посетителей были на нашем Web-сайте в 2000 г. и с какими разделами они ознакомились?
14
и
Введение в методологию БД Каждый из этих запросов будет использовать различное число измерений, чтобы решить проблему. Наша таблица фактов тогда содержала бы числовые индексы пользователей на нижнем уровне, а таблицы измерений будут использоваться, чтобы группировать слой данных в соответствии с потребностями пользователя. Механизмы организации куба могут использоваться, чтобы предварительно объединить ответы на части или все запросы, которые может сформировать пользователь, в группы, чтобы сделать ответ на запрос пользователя более быстрым. Более глубокое объяснение этих структур данных выходит за границы возможностей этой книги. Подробнее все рассмотрено в "Professional SQL Server 2000 " (Wrox Press, ISBN 1861004486) и "Professional Data Warehousing with SQL-Server 7.0 and OLAP Services" (Wrox Press, ISBN 1861002815). Витрины данных могут использовать SQL Server (хотя и не обязаны), чтобы размещать свои данные, но к данным не обязательно обращаться, используя классические команды SQL (хотя, когда SQL Server используется, чтобы размещать многомерные данные, это можно использовать). Витрины данных обычно используют OLAP-серверы, чтобы выполнить большую часть агрегатирования до того, как пользователь запросит данные. Существует SQL-подобный язык доступа к данным в витрине данных (Multidimensional Expressions — многомерные выражения — или MDX), хотя технически он непосредственно и не работает с данными из витрины данных. Однако обычно к данным обращаются, используя средства без использования кодирования. Подходящими средствами являются Microsoft Excel, Microsoft Access, Cognos Impromptu, а также Lotus — обращение по имени, которое используется редко. Данные в витринах данных — только для чтения по определению (конечно, исключая процессы, которые периодически освежают их). Главная цель витрины данных состоит в том, чтобы показать тенденции, региональные или сгруппированные по какому-то другому принципу данные, которые пользователь может пожелать, выделяя содержание массивного хранилища данных в небольшие сегменты, к которым проще обращаться и управлять ими. Каждая витрина данных будет структурирована таким образом, чтобы решать довольно специфический ряд задач. Рассмотрим пример электронного продавца книг. Витрина данных, которую его отдел маркетинга без сомнения хотел бы иметь, будет содержать информацию о проданных произведениях. Главная таблица фактов содержала бы числовые коммерческие показатели, типа количества проданных изданий и их цены. Тогда измерения были бы смоделированы для дат продаж; каким образом пользователи на абонентском пункте получают информацию (или они ищут непосредственно издание, или осуществляют общий поиск автора, или мы побуждаем их сделать покупку, высвечивая информацию на экране); информацию о клиентах (откуда они, какими видами личной информации они обладают) и, наконец, произведений, которые были куплены. Из этой информации мы можем, например, увидеть, что около четверти продаж были сделаны людьми в Топека, или получить любую другую комбинацию измерений; таким образом строятся неопределенные или, наоборот, очень конкретные запросы, чтобы получить ответ. В случае наших OLTP-БД, это потребовало бы сотен строк кода SQL (и я могу подтвердить это по болезненным личным воспоминаниям). В банковском примере архитекторы данных (или, естественно, конечные пользователи) могут создавать определенные витрины данных для различных пользователей хранилища данных. Например, пользователь маркетинга может иметь витрину данных с банковскими клиентами, определяемыми уровнем доходов, регионом страны, количеством оплат, величиной капитала и текущей информацией. Исходя из этого могут быть сформулированы запросы: показать клиентов для каждого региона, с каждым уровнем дохода, кто выполнил свои платежи своевременно, с каким уровнем доходов. Это могла бы быть очень мощная информация при определении новых условий специальных тарифов в различных частях страны. 2 1868
15
Глава 1
Структура Полная структура окончательного выбора БД зависит от масштаба проблемы, которую вы пробуете решать. В следующей диаграмме мы берем типичную большую систему и делим ее на части, чтобы видеть поток данных через всю БД.
Все еще используемая наследуемая БД1
Все еще используемая наследуемая ^ БД2
Витрина данных
Все еще используемая наследуемая БДЗ
/
/ Оперативное хранилище данных
Витрина данных
Витрина данных
Вы можете заметить, что имеется много витрин данных, рассеянных по всей диаграмме. Витрины данных проще всего строить на основе хранилища данных, так как хранилище данных уже имеет историческую информацию, но они могут быть построены в любом месте по пути следования информации. Обратите внимание, что мы берем наши исходные данные в наследуемых БД и OLTP-БД и преобразовываем их для использования в ODS. На самом деле, мы делаем примерно ту же самую вещь на каждой ступеньке процесса, от ODS до хранилища данных, и на витринах данных. Вообще-то, как только вы преобразовали данные в ODS-подобный формат, вам не нужно будет делать слишком много изменений, чтобы получить данные в формате хранилища данных. Фактические сценарии могут изменяться от ситуации к ситуации.
Учебный пример На протяжении книги мы будем приводить пример процесса разработки БД. Он охватит все аспекты проектирования, от начальных его идей, через интервьюирование клиентов, моделирование БД, создание ее, используя SQL Server, выбор аппаратных средств и организацию доступа к ней. Темой примера будет персональный реестр текущего счета. В то время как фактическое количество данных, которое будет размещено в окончательных БД (которые расположены на Web-сайте фирмы Wrox по адресу www.wrox.com), будет намного меньше, чем могло бы обычно требоваться для развитого хранилища данных, это — просто пример с легко управляемым размером.
16
Введение в методологию БД Выбор текущего реестра дает достаточно свободы действий, чтобы показать многие из различных типов отношений, структурных проблем, и т. д., с которыми приходится сталкиваться при создании реальной БД. В проект может быть включен некоторый уровень многопользовательского функционирования при наличии интерфейса для многих пользователей текущего счета, чтобы войти в дебеты и кредиты в одно и то же время. Это позволит нам рассмотреть проектирование и реализацию ODS БД, использование утилит SQL Server, чтобы выполнить задачи извлечения данных из OLTP-БД, преобразование данных в различные формы, и загрузку их в витрину данных (обычно называемые какЕТЪ — Extract, Transform, and Load — извлечение, преобразование и загрузка).
Реляционное программирование по сравнению с процедурным программированием Система понятий реляционного программирования настолько отличается от процедурного программирования, что может вызвать много огорчений, если эти различия будут неправильно истолкованы. Реляционное программирование — это все, что связано с определением того, что вы хотите, и затем запуск реляционного механизма для выполнения всей работы, а процедурное программирование — все, что связано с эффективным выполнением циклов. Первичный реляционный язык (и единственный, который мы будем рассматривать с этого времени) — SQL. Предполагается, что вы, читатель, имеете опыт в использовании SQL, поскольку мы будем представлять все наши примеры в T-SQL — языке Microsoft SQL. Пока существуют различные мнения относительно глубины различий между этими двумя принципами, различные методологии, которые каждый из них влечет за собой, означают, что нужно быть осторожными при определении того, что наиболее соответствует имеющейся задаче. В главе 3 мы рассмотрим основы реляционной теории более глубоко и выясним, как она связана с SQL.
Организация циклов При процедурном программировании основной путь доступа к данным — организация циклов с массивами данных. Мы можем представить программу, написанную так, что программист выполняет оператор SELECT, чтобы заполнить массив данных из одной таблицы (назовем ее outerTable), и затем выполняет оператор SELECT, чтобы получить массив данных из другой таблицы (назовем ее innerTable). Тогда он может найти нужную информацию, используя два вложенных цикла следующим образом:
I
For Each row In outerTable For Each row In innerTable <... проверить некоторое равенство, затем выполнить некоторое действие > Next row In innerTable / Next row In outerTable
Этот код — то, что SQL Server делает за сценой, когда он запрашивает две таблицы. Но это чрезвычайно плохая практика программирования — писать программу подобно этому, поскольку следующий код SQL исполняет тот же самый запрос.
I
SELECT * FROM innerTable JOIN outerTable ON innerTable.key = outerTable.key
17
Глава 1 SQL Server оптимизирован на выполнение этого действия, и сервер имеет более высокую производительность, чем клиент. Пока это — простой пример кода, те же самые принципы используются при выполнении сложных запросов — всегда лучше передать серверу столько работы, сколько возможно. Это называется соединением таблиц. Результирующий набор данных, возвращенный запросом, может быть далее отфильтрован и очищен путем изменения параметров выборки. Важно учиться передавать в SQL Server работу в максимально возможной степени, так как он непосредственно предназначен для целей управления данными. Это особенно важно, потому что некоторые запросы в хорошо структурированной базе данных могут включать десять, двадцать, или даже большее количество таблиц. Чем больше количество используемых таблиц, тем труднее это осуществить с помощью SQL без использования соединений (поскольку соединения представляют отношение таблиц). В главах 10, 11 и 12 мы будем использовать SQL несколько глубже, поскольку рассмотрим, как формировать и организовывать доступ к нашим данным в БД, которые мы создадим. Эта книга, конечно, не руководство по SQL; она в первую очередь рассматривает, как использовать операторы SQL в проекте базы данных. Хорошее понимание SQL-языка существенно, чтобы правильно управлять данными в реляционной БД, и это позволит разработать программное обеспечение для доступа к таким данным. Процедурные программисты, которые игнорируют это, просто не смогут разрабатывать хорошо работающие приложения. Так же, как вы используете молоток, чтобы забить гвоздь, и пилу, чтобы распилить древесину, вы должны использовать все инструменты, имеющиеся в вашем распоряжении (включая и процедурные, и реляционные языки), чтобы хорошо выполнить работу. Для дальнейшего чтения на эту тему используйте следующие книги: •
Instant SQL Programming (Wrox Press ISBN
•
Professional SQL Server 2000 Programming (Wrox Press ISBN
•
The Essence of SQL: a Guide to Learning Most of SQL in the Least Amount of Time, (Peer to Peer Communications, ISBN 0964981211)
1874416508) 1861004486)
Доступ к сети Одна из первопричин, по которой вы должны создать надлежащий код SQL, — доступ к сети. Приложения SQL БД, как правило, распределены между двумя или большим числом компьютеров. В настоящее время довольно редко бывает единственный пользователь SQL БД, который работает на том же самом компьютере, на котором расположены и данные. Хотя это не всегда так (с SQL Server 2000, обладающим Microsoft Data Engine и версией СЕ, позволяющей создавать системы с единственным пользователем), наша книга будет сконцентрирована на многопользовательских системах, которые включают два или больше компьютеров. Однако все примеры в книге могут быть реализованы и проверены на любой версии SQL Server. Чтобы реализовать пример организации цикла из предыдущего раздела, каждая строка в i n n e r T a b l e и o u t e r T a b l e должна быть восстановлена через сетевую связь с клиентом. Если таблица большая, это было бы* вероятно, очень медленное действие, даже на относительно быстрой сети. Эффективность, полученная за счет использования ресурсов процессора сервера, позволяет многим пользователям осуществлять доступ к данным более быстро, чем в случае, если работа выполнялась бы на машинах клиента, не говоря уж о том, что количество данных, которые должны быть переданы по сети, будет меньше, минимизируя величину скорости доступа сети. Хотя хорошо разработанные системы централизуют обработку, вы часто видите, что плохо реализованные приложения пытаются делать работу, которую должен делать механизм БД. 18
Введение в методологию БД
Краткий обзор процесса проектирования БД на основе SQL Server Процесс проектирования надлежащей СУБД имеет несколько моментов, общих с проектированием эффективных компьютерных систем любого назначения. • • •
Это — относительно прямая задача. Она может потребовать очень больших временных затрат. Она редко получается той мощности, которая требуется.
Как мы будем много раз повторять на протяжении всей книги, БД занимают центральное место среди компьютерных систем, которые создаются сегодня. Даже в системах, которые не предназначены для БД, обычно требуется какая-либо форма хранения данных. В этой книге мы концентрируемся прежде всего на больших системах, которые имеют БД в качестве центральной системы. Примеры таких БД — вокруг нас: в Интернете, в наших банках, правительстве, компаниях, магазинах бакалеи, аптеках, и т. д. Процесс проектирования таких БД можно разбить на несколько шагов, которым мы будем следовать в этой книге: •
•
•
•
•
Определение цели — не смейтесь над этим, считая все слишком очевидным: большинство проектов портятся разработчиками, не имеющими никакой реальной идеи того, что пользователь фактически хочет или в чем нуждается, потому что они перескакивают с одного заключения на другое или не в состоянии слушать правильных доводов пользователей, или в некоторых случаях любых пользователей вообще. В течение этой стадии мы будем определять функциональные требования, требования к рабочим характеристикам и отчетам для окончательной системы, которую мы будем создавать. Логический проект — процесс проектирования логического пути достижения целей, которые формируют в реализации независимый путь. Одна из главных причин того, что не придется часто ссылаться на SQL Server в первой половине этой книги, связана с тем, что логический проект БД независим от физического проекта БД и ее реализации. Физический проект — на этом этапе берется логический проект и приспосабливается к конкретной реализации. Эта стадия проекта связана с определением того, как СУБД будет физически реализована с использованием любых доступных аппаратных средств и программного обеспечения ЭВМ. Физическая реализация — стадия реализации проекта связана с фактическим размещением физических данных на серверах БД и разработкой кода для доступа к данным. Обзор — процесс оценки, были ли достигнуты цели. Печально, но это наиболее часто пропускаемая стадия проектирования, потому что она требует так досадно много времени, и это никакая не забава вообще: тестирование, документация и все другие вещи, которые вызывают в нас крайне неприятные чувства при их выполнении, но мы должны это делать. Это должно включать механизм использования обратной связи от пользователя и рассмотрение плана обслуживания, связанного с исправлением любых идентифицированных проблем.
В этой книге первые два шага рассмотрены в части "Логическое проектирование", а остальные — в части "Физическое проектирование и реализация". 19
Глава 1
Резюме В этой главе мы представили основы современных СУБД, особенно OLTP и методологию организации хранилища данных. Мы установили, что имеются четыре основных части в СУБД предприятия. Они могут быть представлены как: •
OLTP (Система обработки транзакций в реальном масштабе времени) для размещения наших текущих данных лишь с ограниченным количеством исторических данных, сохраненных, чтобы принимать самые последние решения.
•
ODS (Оперативное хранилище данных) для создания механизма хранения данных, необходимых для принятия ежедневных бизнес-решений, не прерывая пользователей в OLTP-БД.
Q
DW (Хранилище данных) — не следует путать с общим термином организации хранилища данных; DW используется, чтобы хранить массивные исторические величины, что позволит нам обслуживать объединенную БД, включающую многие корпоративные БД и видеть тенденции за длительные промежутки времени.
•
Витрины данных — часто путаются с OLAP или кубами (которые являются технологиями, используемыми в запросах на поиск данных в витрине данных); витрина данных используется, чтобы получить срез хранилища данных или, в его отсутствии, OLTP или ODS данных, и позволяет пользователю рассматривать объединенные данные в гибкой форме.
Наша книга будет нацелена на рассмотрение данных, особенно данных в OLTP-БД. Данные и, следовательно, OLTP-БД, являются единственной, наиболее важной частью любого проекта. Некоторые могли бы аргументировать другое: что, например, более важен надлежащий проект или даже логическая природа информации (потому что она моделирует сферу деятельности предприятия). Однако очень немного пользователей интересуется природой данных, или же интерфейсом, который они используют для доступа к ним, если они могут просматривать и управлять данными, как им нужно. Имеется так много устаревших систем, неуклюжих в работе, плохо продуманных, и все же они продолжают и продолжают использоваться. Процессы изменяются, так же, как и идеи, но запасенные данные будут жить намного дольше, чем любой интерфейс или даже процесс. Одна из главных задач администраторов БД — всюду, где это требуется, преобразовывать данные из одной системы в другую. Независимо от того, как меняется структура БД от версии к версии, данные будут все же использоваться снова и снова. Если вы когда-либо что-нибудь программировали, вы, несомненно, не согласитесь с некоторыми из мнений/идей в этой книге. Я полностью принимаю, что эта книга — не евангелие от святого Луки для БД. Мои идеи и мнения сформировались в течение десяти лет работы и изучения БД в дополнение к точкам зрения многих совершенно разных людей, книг, курсов колледжа, и семинаров. Методология проектирования, представленная в этой книге — сумма этих идей, и я очень благодарен другим людям, которых я могу вспомнить. Я надеюсь, что это доказывает пользу обучения, и что через чтение работ других людей и испытания ваших собственных идей вы сформируете собственную методологию, которая удовлетворит вас и сделает вас успешным проектировщиком БД.
20
Сбор информации для проекта БД В этой главе мы вернемся к исходным принципам и обсудим самые первые шаги, которые нужно предпринять, начиная проектирование БД. Получение предварительной информации для нового проекта БД — типа решения, что мы собираемся хранить, — является одной из наиболее важных задач, которую вы должны будете решить, хотя могут быть ограничения во времени и материальных ресурсах, которые подразумевают, что этот процесс не будет выполнен настолько полно, насколько это нужно. Построение вашей БД без адекватного предварительного анализа может быть уподоблено строительству дома на песке, где после того, как будет построен нулевой цикл, пески сдвинутся, и вы должны будете все начать снова. Такие ситуации часто возникают, когда после того, как БД создана и развернута, оказывается, что необходимо включить в БД пропущенную особенность, что потребует перепроектирования ее с самого начала. Надлежащий анализ — твердая основа того, что будет создан удачный проект без шероховатостей. При сборе информации для проекта БД вы должны избежать искушения начать выбирать в это время какую-либо структуру. Не определяйте на этой стадии таблицы, поля и т. д., даже если вы искушенный проектировщик БД. Наивно пытаться моделировать процесс, выбирая единственный путь, пока вы не проконсультировались со всеми сторонами, вовлеченными в проект, выслушали их идеи и потребности. Слишком часто мы начинаем формировать структуру БД прежде, чем получим достаточные знания о задаче, и это не помогает ни нашим заказчикам, ни нам самим. Эта глава помещена до рассмотрения моделирования данных, чтобы подчеркнуть данный момент.
Глава 2 В процессе проектирования БД обрабатывается много важной информации. Эта информация будет полезна для других членов команды проектировщиков теперь и в будущем, чтобы понимать и поддерживать систему. При этом должна быть рассмотрена формальная стратегия ведения документации.
Команда проектировщиков БД Чтобы начать процесс сбора информации, должна быть организована команда проектировщиков БД. Хотя команда и может состоять только из единственного человека (что, вероятно, будет в маленьких организациях или проектах), имеются, по крайней мере, четыре роли, которые должны быть заполнены, потому что каждая предназначена для достижения специфичной, существенной цели: •
Деловой аналитик излагает в деталях требования бизнеса пользователей и обеспечивает команду проектировщиков функциональным описанием, которое они перерабатывают в техническое описание. Он также действует как защитник интересов пользователя, убеждаясь, что окончательное проектное решение удовлетворяет указанным деловым потребностям клиента. Деловой аналитик также убеждается, что все контракты, соглашения, и т. д. выполнены.
•
Архитектор данных берет функциональное описание и разрабатывает техническую реализацию описания. (Системный архитектор также помогает в этой задаче.) Архитектор данных проектирует все размещение данных и структуру доступа к ним, а также выбирает надлежащие технологии хранения, основанные на том, что необходимо пользователю. В книге рассматриваются только обязанности этой роли. Обратите внимание, что архитектор данных — не то же самое, что администратор БД (Database administrator — DBA), который связан с реализацией и имеет дело с аппаратными средствами ЭВМ, программным обеспечением и моментами, обеспечивающими гладкую работу. Архитектор данных занимается вопросом, каким образом СУБД, реляционные или другие, объединить вместе и структурировать.
•
Системный архитектор отвечает за проектирование полного интерфейса пользователя и промежуточных объектов бизнеса, которые требуются в проекте, и отвечает за выбор внешнего интерфейса и промежуточных технологий. Есть определенное различие между этой ролью и архитектором данных — последний является ответственным исключительно за БД проекта, в то время как системный архитектор имеет дело со всем остальным, хотя, вероятно, могут быть и некоторые накладки между этими ролями.
•
Менеджер проекта — "босс", кто прежде всего отвечает за то, как каждый другой член команды делает свою работу, и занимается планированием.
Каждая из этих ролей будет существовать и в процессе проектирования, и в процессе реализации. Имеются другие важные роли, которые также внесут свой вклад, например, спонсор проекта или представитель клиента, кто заинтересован в окончательном проекте и обеспечивает финансы для развития, испытаний, документации и т. д. Однако четыре роли, внесенные в список выше, — основная группа, а определять другие или рассматривать большее количество деталей — вне возможностей этой книги. Причина определения этих ролей вообще состоит в том, чтобы показать, что если различные люди исполняют каждый свою работу, то архитектор данных может сосредоточиться почти исключительно на том, как размещать данные.
22
Сбор информации для проекта БД
Документация и связь Во время процесса анализа имеется одна хорошая привычка, которую вы должны принять — документировать всю информацию, которую вы получаете. Предположим, что вы можете завтра попасть под автобус. Мягче эту мысль можно выразить так: "Если со мной что-то случится, кто-то другой будет должен заняться моей работой". Другой пример— в нашей промышленности становится все более трудно ориентироваться только на лучших служащих. Если кто-то оставляет проект, потому что конкурент предлагает ему необоснованно огромное жалованье, замена будет связана с крутой кривой обучения, чтобы набрать темп, и единственный способ помочь этому — документировать всю информацию. Таким образом, вы должны документировать, документировать, документировать! Ни в коем случае нельзя все держать в вашей голове. Поскольку вы начинаете собирать замечания относительно потребностей пользователей, полезны следующие руководящие принципы: •
Поддерживать набор документов, включающих информацию о проекте системы и спецификации. Важные документы, которые следует иметь, включают: выявленные замечания к проекту, документы, описывающие устные запросы по изменениям, и записи по всем спецификациям, таким как функциональная, техническая, испытаний и т. д.
•
Помимо формальной документации, важно сообщать членам вашего коллектива проектировщиков новейшую и полную информацию. Разработайте и поддерживайте общее хранилище для всей информации.
•
Используйте минуты встреч и храните записи каждого предложения, запроса, или идеи, о которых говорили ваши заказчики.
•
Записывайте все, что пользователям не нравится.
•
Составьте перечень исходных границ проекта и постоянно помните о них. Это предотвратит от создания чрезмерно большого проекта или не того, который нужен.
Одной из первых работ команды проектировщиков должно быть определение потребностей (формулировка цели или характеристики цели), которые описывают параметры проекта. Они будут учитываться и с ними будет происходить сравнение в течение проектирования и реализации, а также после завершения работы. Если, однако, характеристики проекта и цели четко не заданы на стадии определения потребностей, или ничто не записано, то имеется большой шанс, что будут конфликты между вами и вашими клиентами, когда ваши идеи их не устраивают. Такая неопределенность или нерешительность могла бы вызвать в процессе проектирования ненужные обсуждения, стычки, или даже судебные процессы. Так удостоверьтесь, что ваши клиенты понимают то, что вы собираетесь сделать для них, и используйте язык, который будет правильно понят, но достаточно определенный, чтобы описать то, что вы изучите в процессе сбора информации.
Одобрение клиента Поскольку вы выполняете полный процесс проектирования БД, клиент, без сомнения, изменит свое мнение об именах полей, определениях полей, бизнес-правилах, интерфейсе пользователя, цветах — почти обо всем, о чем сможет, — и вы должны быть готовы к этому. Независимо от того, что клиент захочет или потребует сделать, вы будете должны пытаться это выполнить. Клиент является окончательным контролером проекта, и вы должны быть достаточно гибки при обращении с любыми предложенными изменениями, как незначительными, так и существенными. 23
Глава 2 После каждой встречи суммируйте ваши замечания на языке, который все участники могут понимать, и посылайте им копии. Держите папку со всеми ответами, которые вы получаете, и пересылайте их менеджеру проекта. В любой момент клиент может сказать вам "Я никогда не говорил это". Если нет никакого документа, подтверждающего то, что вы говорите, то это может привести к неприятностям. Так что я снова повторю — храните документацию, и если вы должны принять решение, которое не понравится клиенту, то должны будете иметь документ, дублирующий ваше решение. Лучший способ избежать конфликта, если клиенты будут изменять свое мнение и "характеристики поползут", — удостовериться, что вы получаете одобрение вашего клиента на всех стадиях процесса проектирования.
Минимальные информационные требования Независимо от того, являетесь ли вы командой из одного человека или винтиком в ударной силе проектировщиков из пятидесяти человек, имеется набор основной информации, которую команда проектировщиков должна собрать в течение ранних стадий, если они собираются продолжить процесс проектирования. Для начала вам потребуется груда записей, распечаток, изображений экрана, CD-ROM, загруженных крупноформатными таблицами, копий БД, документов в редакторе Word, документов электронной почты, рукописных материалов и т. д. Никакая структура пока не нужна для этой начальной информации, и было бы, вероятно, лучше в этот момент вообще не иметь никакой структуры, хотя это, конечно, вопрос вкуса. Некоторые утверждают, что вы не должны только собирать информацию, чтобы идти дальше, а лучше быть организованным с самого начала, чтобы после завершения информационного поиска вы могли бы задавать все более и более интеллектуальные вопросы. Причина того, что я не верю в эффективность слишком большого количества структур, заключается в том, что в этом случае я могу придавать вес собранной информации. Реальное упорядочивание должно произойти, когда вы начнете анализировать собранную информацию, что мы обсудим позже. Взглянем на начальные стадии процесса проектирования и источники этой исходной информации.
Опытные образцы БД Опытные образцы полезны при разработке крупномасштабных систем, которые оправдывают этот начальный шаг. Их целью должно быть "доказательство концепции" — возможность команды проектировщиков и пользователей конкретизировать критические элементы проекта, от которых будет зависеть успех или неудача. Иногда в качестве архитектора данных вы будете пытаться использовать опытный образец БД, который был торопливо разработан, и "заставить его работать" или, еще хуже, "отполировать БД". Действительно, вы можете унаследовать неструктурированный, неорганизованный опытный образец, и ваша задача будет состоять в том, чтобы на его основе создать промышленную БД. Действительно, ужасные слова!
24 иi
Сбор информации для проекта БД Имейте в виду, что опытные образцы должны рассматриваться лишь как диалоговые картинки, чтобы заставить заказчика подписать контракт с вашей компанией. Снова и снова нанимаются консультанты, чтобы разработать опытный образец, который выглядит настолько хорошим, что кажется, что он готов для развертывания на предприятии. Многие скажут вам "Это напоминает то, что нужно, так почему его выбрасывать?" Главная причина в том, что при создании опытного образца вы комбинировали, быстро стряпали и слепили код, разрабатывая идеи неорганизованным образом. Надлежащая структура и предусмотрительность при этом проигнорированы. Как только молниеносная разработка опытного образца выполнена, вы имеете пользовательский интерфейс и функциональную БД, но очень мало мысли, или просто никакой, которая была вложена в его структуру, хотя все и выглядит симпатично. Лучше, как только заказчик распишется, начать на пустом месте; разрабатывая окончательное приложение, используя структурированные и поддерживаемые стандарты кодирования. Очень важно, чтобы как архитектор данных вы работали настолько твердо, насколько возможно, чтобы использовать код опытного образца только как рабочую модель, как часть документации, которую вы используете, чтобы улучшить ваш собственный проект. Это поможет убедиться, что вы не пропустите какую-либо критическую часть информации, которая нужна пользователям, типа названия поля, операции поиска или даже кнопки (которая вызывает элемент данных), но не говорит вам ничего относительно структурных проблем. Работайте с этими проблемами сами. При проектировании БД предприятия не может быть никаких коротких путей.
Интервью с клиентами В большом бизнесе, корпоративном мире маловероятно, что архитектор данных будет встречаться с пользователем, не говоря уж о взятии у него интервью. Менеджер проекта, деловой аналитик и системный архитектор могли бы дать всю информацию, которая ему/ей потребуется. Однако бывают времена, когда архитектор данных фактически вовлекается в процесс интервью в зависимости от структуры команды проектировщиков. В моем опыте консультанта я был в роли фактического участника интервью с клиентом (совещания по проекту). При случае, я был должен брать интервью у интервьюера, чтобы выяснить противоречия! Клиента интервьюируют, когда проектирование БД действительно начинается. Однако многие клиенты обычно мыслят визуально; они думают, в частности, в терминах форм, Web-страниц и простых пользовательских интерфейсов. Во многих случаях ваши клиенты не будут иметь абсолютно никакого понимания относительно того, как система создана. Ваша работа в качестве архитектора данных, связанная с интервьюированием клиентов, должна сводиться к сопоставлению их действительных потребностей с тем, как они сами их трактуют - это касается должным образом структурированной БД, скрытой за пользовательским интерфейсом. Изменение всех форм при включении нового текстового окна, метки или чего-нибудь еще является относительно простой задачей, давая пользователю ложное впечатление, что создание приложения БД является легким процессом. Если вы хотите подтверждения, покажите пользователю почти законченный опытный образец приложения без поддержки БД. Клиенты могут быть поражены, что вы собрали кое-что так быстро, но когда запустят его и будут наблюдать, то споткнутся на этом. Редко кому будет понятно, что за фасадом существует что-то еще, а именно, БД и объекты бизнеса промежуточного уровня — то, где вся главная работа и выполняется. Исчерпывающее рассмотрение методов интервью — вне возможностей этой книги, но имеется несколько ключевых моментов, которые должны быть упомянуты. Во-первых, слово "интервью" используется вместо слова "опрос". Первое предполагает обмен идеями один на 25
Глава 2
один, в то время как второе подразумевает навязывание ваших идей оплачивающему заказчику. Если вы произведете впечатление властного человека и попытаетесь внушить заказчику, что он/она хочет, вы испортите ваши отношения прямо с самого начала. Будьте тактичны и не создавайте представление, что вы знаете больше, чем заказчик. Пробуйте установить основную тему для интервью так, чтобы были охвачены важные области, по которым вы имеете открытое мнение. В качестве затравки вам будет полезен один простой вопрос: "Что вы хотите от этого проекта?" Это позволит вашим интервьюируемым рассказать, что они хотят от проекта. Попросите их объяснить свои идеи относительно того, как они достигли бы этого. Это даст вам ясное представление того, что они хотят и почему, предотвращая ваши исходные идеи от изменения, когда начнете заниматься проблемами, которые будете решать для клиента. Будьте готовы слушать, делать записи, задавать вопросы, получать разъяснения — и делайте как можно больше записей. Удостоверьтесь, что вы обращаетесь с каждым человеком, у которого берете интервью относительно проекта, как с индивидуумом. Каждый человек будет, вероятно, иметь свою, отличную от других точку зрения. Не думайте, что первый человек, с которым вы говорите, может сказать за остальных, даже если все они занимаются одним и тем же, или если этот индивидуум — менеджер. Разговоры "один на один" позволяют клиентам высказывать мнение без неуклюжих прерываний со стороны коллег. Помните тот факт, что самые громкие и самые смелые люди могут и не иметь лучших идей, чем у тихого человека, который сидит сзади и ничего не говорит такого, что может быть ключом ко всему проекту. Удостоверьтесь, что вы получаете общие мнения. Полезна техника, когда вы говорите клиенту, что понимаете то, что он сообщил, повторяя во время встречи наиболее важные пункты. Это также полезно для разъяснения неясных моментов и помогает в процессе формирования заметок. Аудио- или видеозаписи встреч иногда используются, но это может показаться агрессивным и создать дискомфорт клиентам, так что они должны использоваться только в ситуациях, где это действительно необходимо из-за предыдущих проблем обмена информацией. Никогда не делайте запись беседы без согласия интервьюируемых. Если можно, возьмите на встречи человека, который не имеет никаких обязанностей, кроме ведения записей. Как архитектор данных, вы должны будете вспомнить многое из того, что было сказано в течение этих начальных встреч, и это жизненно важно в представлении того, для чего вы делаете свою работу. Это объясняет важность документации. Если все записывается и регистрируется, а не только запоминается, клиенты могут регулярно просматривать записи. Это означает, что вы не только можете улучшить отношения с вашими клиентами, но и увеличите ваши возможности в идентификации данных, которые они хотят видеть, а также обеспечит команду проектировщиков информацией, требуемой при проектировании конечного продукта. Эта книга возникла по итогам восьми лет создания ошибок в процессе проектирования БД, и интервью клиента — одна из наиболее критических частей процесса, с которым я сталкивался. Это может показаться неподходящей темой для опытных программистов, но даже лучшим из нас следует напомнить, что бряцание оружием, запугивание клиентов, сообщение им, что они хотят, до того, как они сами сообщат вам об этом, и даже попытки управлять ожиданиями пользователя, могут привести к крушению даже хорошо развитой системы. Хорошая техника интервьюирования клиента необходима нам, чтобы получить твердую основу для процесса проектирования. Если вы имеете шаткую основу, конечный продукт будет, вероятно, также шатким.
26
Сбор информации для проекта БД
О чем следует спрашивать Очень важно быть уверенным, что, независимо от того, кто бы ни интервьюировался, были выяснены ответы на следующие вопросы:
Кто будет использовать данные? Ответ на этот вопрос может указать на другой персонал, нежели тот, у которого вы хотели бы взять интервью, и будет, вероятно, иметь значение, когда вы будете определять безопасность системы. •
Как данные будут использоваться? Представьте, что вас просят создать БД учета партнеров. Вам нужно будет знать: •
Будут ли использоваться имена партнеров лишь для обращений по телефону, подобно быстрой телефонной книге?
•
Будем ли мы делать массовую рассылку электронной почты или отправлять по почте сообщения персонально членам списков партнеров? Должны ли для этой цели имена быть подразделены на группы?
•
Будем ли мы использовать имена, чтобы запрашивать ответ по почте, вроде пожертвований, просроченных векселей или новых инвесторов?
•
Насколько важно иметь правильные титулы для каждого партнера (например, г-н, а не гр.)? Нужна нам любая фонетическая информация — потеряем ли мы продажу, если пользователь неправильно произносит имя заказчика?
Знание, для чего ваш клиент планирует использовать данные в системе, действительно, очень важная часть информации. Вы не только поймете процесс, но и можете также получить хорошее представление о типах данных, которые следует разместить.
Что вы хотите видеть в отчетах? Отчеты — часто одна из наиболее забываемых частей процесса проектирования. Многие разработчики-новички оставляют их разработку на самую последнюю минуту. Однако пользователи, вероятно, больше заинтересованы в отчетах, которые формируют данные, чем в чем-либо еще, что вы будете делать. Отчеты используются как основа для принятия жизненно важных решений, и могут либо поддержать, либо разрушить компанию. Возвращаясь к примеру с партнерами, следует задать вопрос, какое имя клиент хочет видеть в отчетах? •
Имя, фамилия
•
Имя, отчество, фамилия
•
Фамилия, имя
•
Прозвище
Очень важно "попробовать на зуб" эти проблемы пораньше, независимо от того, сколь малыми или глупыми они ни показались в этот момент. Мы рассмотрим формирование инфраструктуры отчетов позже в нашем проекте, и этот вид информации может быть необходим.
27
Глава 2 Одно замечание-предупреждение: в то время как очень важно получить некоторые идеи относительно того, какие данные необходимы в отчетах, вы должны быть осторожны, чтобы избежать любых обсуждений относительно их фактического представления. Это — не задача архитектора данных заниматься чем-либо, кроме требований к данным отчетов.
Где сейчас находятся ваши данные? Было бы замечательно иметь полностью новую БД, абсолютно не имеющую никаких данных, существовавших ранее. Это бы сделало жизнь такой легкой! К сожалению, такого почти никогда не бывает, кроме, возможно, новых компаний, но даже они будут иметь некоторые данные, которые они хранили, когда начали работать. Каждая организация своеобразна. Некоторые имеют данные в одном единственном месте, в то время как другие рассеивают их по многим местам. Редко, если вообще когда-либо, данные находятся уже в хорошо структурированных БД, так чтобы вы легко могли осуществить доступ. Если бы это имело место, вот бы была забава! Действительно, почему тогда клиент вообще к вам пришел? Клиенты обычно имеют данные в следующих разных местах: •
Универсальная ЭВМ или наследуемые данные Миллионы строк кода, написанного на КОБОЛЕ, все еще используются многими корпорациями.
LJ
Электронные таблицы Электронные таблицы — замечательный инструмент, чтобы рассматривать, вырезать и нарезать данные, но являются неподходящим местом, чтобы обслуживать сложные БД. Большинство пользователей знает, как использовать электронные таблицы в качестве БД, но, к сожалению, не так хорошо рассмотрено обеспечение целостности их данных.
•
Настольные БД типа Access Настольные БД — мощные инструменты, их легко развертывать и использовать. Однако это часто означает, что такие БД построены и поддерживаются не техническим персоналом и плохо разработаны, потенциально вызывая большое число проблем, когда они должны быть увеличены или изменены.
•
Картотека Да, все еще имеется много компаний, которые в настоящее время не имеют никаких компьютеров и поддерживают обширные запасы бумажных документов. Ваш проект мог бы просто заменить картотеку системой, основанной на компьютере, или поставить простую БД, которая регистрирует физическое местонахождение существующих бумажных документов.
Данные, которые вы должны включить в разрабатываемую вами БД на основе SQL Server, будут поступать из этих и других причудливых и замечательных источников, которые вы найдете у клиента.
Сколько эти данные стоят? Важно также иметь суждения о стоимости данных. То, что данные доступны, не обязательно подразумевает, что они должны быть включены в новую БД. Клиент должен быть информирован относительно всех данных, которые являются доступными, и обеспечен оценкой стоимости перевода их в новую БД. Стоимость передачи наследуемых данных может быть высока. Таким образом, клиенту дают возможность принять решение, что можно сохранить фонды для более важных целей.
28
Сбор информации для проекта БД Как будут данные в новой БД сочетаться с другими данными? Как только вы хорошо проработаете, где располагать все важные данные клиента, можно начать определять, как данные в вашем новом решении на SQL Server будут взаимодействовать с данными, которые останутся в своем первоначальном формате. Это может включать создание связей с универсальными ЭВМ с помощью сложных шлюзов, связи сервера с другими SQL Server и Oracle или даже соединение с электронными таблицами. Мы не можем делать слишком много предположений относительно этой темы в данный момент в нашем проекте. Только зная основную структуру, вы должны этим заняться, что может быть очень полезно позже.
Имеются ли какие-то правила, которые управляют использованием данных? Взяв наш предыдущий пример с партнерами, мы могли бы обнаружить что: •
Каждый партнер должен иметь соответствующий адрес электронной почты.
•
Каждый партнер должен иметь соответствующий почтовый адрес.
•
Клиент проверяет каждый адрес электронной почты, используя почтовую программу, и партнер не будет соответствующим партнером, пока эта проверка не будет успешно выполнена.
•
Партнеры должны быть подразделены на типы.
Будьте осторожны при использовании каких-либо правил, подобных этим. Согласуйте их с клиентом. Ваш конечный продукт может стать недопустимым из-за того, что вы ввели в данные правило, которое клиент не хочет иметь.
Другие источники для определения правил данных Кроме интервью, имеются и другие источники, которые вы можете использовать, чтобы найти правила для данных и другие виды информации, важные для проекта. Часто менеджер проекта получает эти документы.
Запрос о цене или запрос предложений Имеются два первичных документа: •
Запрос о цене (The Request for Quote — RFQ) — документ с должным образом сформированной спецификацией, который организация высылает фирмам, чтобы определить, сколько что-то будет стоить.
U
Запрос предложений (The Request for Proposal — RFP) — для меньшего количества сформированных идей, которые организация желает распространить, используя свободные консультационные услуги.
29
Глава 2 Копии этих документов должны быть добавлены к груде информации, которая вам понадобится позже в процессе проектирования. В то время как эти документы вообще состоят из отрывочной информации относительно проблемы и желательного решения, вы можете использовать их, чтобы подтвердить первоначальную причину необходимости создания СУБД, и для получения более твердого выбора, какие типы данных должны быть помещены в ней.
Контракты или наряды на работу клиента Получение копий контрактов — довольно радикальный подход к сбору информации для проекта. Откровенно говоря, в корпоративной структуре вы, вероятно, должны будете бороться с руководством, чтобы заставить его понять, почему вы вообще должны видеть контракт. Контракты часто трудно читать из-за языка, на котором они написаны. Однако будьте прилежными в фильтровании информации, и вы раскроете основной набор требований к СУБД — требования, которые должны точно выполнить, или в противном случае вам могут не заплатить за работу. Обратите внимание, что важен не только контракт на создание системы, но должны быть также учтены любые-контракты, которые создаваемая вами система должна обеспечивать.
Соглашения об уровне обслуживания Серьезной частью контрактов, которая является очень важной для процесса проектирования — требуемый уровень обслуживания. Она может определять число формируемых страниц в минуту, число записей в БД и т. д.
Не забудьте о ревизиях Когда вы строите систему, то должны учесть, будет ли система ревизоваться в будущем и кем. Правительство, клиенты ISO-9000, и другие клиенты, которые проверяются организациями по стандартизации, вероятно, будут иметь строгие контрольные требования. У других клиентов также будут финансовые контрольные процессы. Эти контрольные планы могут содержать ценную информацию, которая может использоваться в процессе проектирования.
Старые системы Если вы пишете новую версию работающей в настоящее время СУБД, то доступ к существующей системе может быть и благословением, и проклятием. Очевидно, что большая часть информации, которую вы можете собрать об исходной системе, очень важна. Все экраны, модели данных, модели объектов, документы пользователя и т. д. являются чрезвычайно важными для процесса проектирования. Однако если вы не просто пересматриваете существующую систему, очень важно использовать старую систему БД только как отправную точку. Очень легко думать в терминах доводки существующего кода, использующего все особенности и данные существующей системы как основание для обновления системы. Иногда это могло бы быть правильным направлением работы, но, вообще говоря, это не так. В большинстве случаев, существующая система, которую вы заменяете, будет иметь много проблем, которые должны быть установлены, но не повторены.
30
Сбор информации для проекта БД Отчеты, формы, электронные таблицы Весьма большой процент компьютерных систем построен для заполнения форм государственных форм, форм компании, всех иных видов форм. Вы должны обеспечить сбор всех этих данных, которые рассеяны по всей компании, и обязательно найти их все. Фактически ясно, что эти источники будут содержать данные, которые потребуются для вашего проекта, так что удостоверьтесь, что клиент дает вам все такие данные.
Схема учебного примера Главная цель примера в этой главе не в том, чтобы проиллюстрировать часть процесса сбора информации, — маловероятно, что архитектор данных будет делать это в большинстве команд проектировщиков — а скорее легко управляемый пример, которому мы можем следовать на протяжении книги. Для удобства вообразим, что мы — команда проектировщиков из одного человека (за исключением некоторой информации, поступающей от делового аналитика) и мы также полностью игнорируем любой вид пользовательского интерфейса. Вы получаете электронную почту от менеджера по программированию, заявляющего, что отдел бухгалтерского учета нуждается в БД для управления обслуживанием текущих счетов. После того, как вы встретились со своим менеджером по информационным технологиям, вы обращаетесь к менеджеру бухгалтерского учета Сэму Смиту. Необходимо организовать встречу (интервью) с Сэмом, чтобы понять, что требуется.
31
Глава 2
Интервью клиента Вот тип записей, которые бы вы делали при этой встрече:
(Bcmpeia с Сэмом Смитом, бухгалтерское дела, 24 ноября 2000, W 1асов утра, ЪотшЖ конференц-Зал 'Постишвян: Луис Юэвидсон, архитектор данных; Сэм Смит, менедэ/сер бухгалтерском yiema; Юфо Юфонс, экономист ЧПервая всмреЫ (ИспольЗуемме дополнительные документы: регистр банка, типовой iek, типовой лицевой film, а такфе электронный формат лицевого dema. 33 настоящее время, испольЗуется (м/мафни4 регистр iekoS, а такфе основной внутренний регистр. (Рассматривали возможность использования подготовленных материалов, но не нашли ни одного, который бы ойспеАивал все, imo xoiem 3aka3iuk. M. у большинства предложенных путей слишком много особенностей. ^lip/mo иметь многопользовательские воЗмофности (ввод и просмотр). (Распределение интЬант, Щоцесс: иметь последнюю информацию относительно dema. (В настоящее время баланс dema проводится один раЗ в месяц. МспольЗуется omiem иЗ банка. Юля nmyienus баланса жтлъЗуетсл по крайней мере полный день. Хотелось бы, полукть баланс по demy еженедельно, испольЗуя данные нЗ 'Интернета ^Интересует только ведение clemoS, но не сберефения, акции, обяЛаттьства, и, т. д. Юля этого уо/се имеется система. Ъудет ревиЗоватъсл ефегодно главным аудитором. Однако контрольною плана нет. (Когда Сэм Закошил,, был Задан вопрос об отслефивании проо Ъыло бы хорошо иметь информацию о полуЫтет тмттфа для большинства выписываемых нами 'кков. УЛакфе относительно категориЗации 1вков Сэм скаЗал, imo это было бы хорошо, но не является необходимым в данный момент времени.
32
Сбор информации для проекта БД Просматривая записи, можно заметить, что имеется несколько документов, которые мы должны собрать в отделе бухгалтерского учета, если это доступно: регистр чеков, отчет банка, план ревизий и пустой чек.
Предварительная документация Типовой регистр чеков
Number
Date
{Description
Category
12390
112/15/00
{Pizza Hut
Employee Appreciation
12391
U2/15/00
{Allied Mortgage
Building payment
12392
112/16/00
iTN Electric
Utilities
12393
j 12/16/00
{Deposit
N/A
Amount
Balance
Account.Running Total Здесь Number — номер; Date — дата; Description — описание; Category — категория; Amount — сумма; Balance — баланс; Account — счет; Running Total — текущий итог. Здесь и далее даты будут приводиться в американском формате: сначала идет номер месяца, затем число и, наконец, две последние цифры года. Прим. перев.
33
Глава 2
Типовой лицевой счет банка
Bank of the Account Statement Account Statement Previous Balance Current Balance Total Debits Total Credits 11/5/00
—I Account. Account Number
Account number
Last Balanced: Statement Date:
11/5/00 12/5/00
Check
12200
11/6/00
12201
Check
11/8/00
X48393
Deposit
11/28/00
X99778
Direct Withdrawal
Строки удалены
Строки удалены
Строки удалены
12/01/00
Check
12/02/00
' Check
12/03/00
; Check
Строки удалены
Строки удалены :
Checks: 12200
|
\
| 12205
| 12212
Строки удалены . Строки удалены
| Строки удалены | Строки удалены | Строки удалены | Строки удалены
12204*
\ 12211
•
•
\
12217;
Означает разрыв в последовательности чеков
Statement. Balancing Items (the s t a t e m e n t contains copies of all t h e i t e m s t h e register s h o u l d have)
Deposit Direct Withdrawal p
Statement. Previous B a l a n c e Current Balance Total D e b i t s Total C r e d i t s Previous Balance Date Statement Date
Здесь Check — чек; Deposit — депозит; Direct Withdrawal — прямое изъятие. Заметьте, что я вычеркнул все числа в вышеупомянутых отчетах. Если не очень важно иметь точные числа по документам, удаляйте любую важную информацию, которая могла бы вызвать проблемы, если бы попала не в те руки. 34
Сбор информации для проекта БД Формат потока данных типового регистра банка Столбец
Тип данных
Требуется
Дата сделки
Date только
Да
Номер сделки
String(20)
Да
Описание
String(lOO)
Да
Количество
Money
Да
(String
(N) означает строку в N символов, Date — тип-дату, Money — денежный тип.)
Обратите внимание, что этот поток данных не из нашего анализа. Скорее всего, это — бумажный документ, объясняющий, что банк будет давать клиенту, чтобы обеспечить электронный баланс счетов. ТИПОВОЙ
чек
Junel2,2000
13403 WROX Subscriptions dollars
Magazine Subscription
Это — то, что мы взяли из примера в данной главе. Мы сделали хорошее начало — возможности нашего проекта чрезвычайно маленькие, а мы собрали так много информации, насколько могли в данный момент, но придет время, когда нам потребуется большее количество информации. В последующих главах мы вернемся к этим записям и документации, когда будем формировать непосредственно саму БД.
35
Глава 2
Резюме В этой главе мы сконцентрировали внимание на существенном предварительном шаге проектирования БД — процессе сбора информации. Практический вывод из всего — получите настолько много данных, насколько можете, включая и документы. Посмотрите везде — не оставляйте ни одного уголка, который бы вы не обшарили, чтобы получить столько информации, сколько клиент готов и в состоянии вам сообщить относительно решаемой задачи. Иногда вы будете знать точно, как решить задачу даже после первого сообщения по электронной почте. В другой раз потребуется месяц копания вокруг, чтобы получить достаточно информации, чтобы начать заниматься этой задачей. Проектирование хорошо структурированных систем и БД — трудная работа, которая занимает относительно долгое время, чтобы выполнить все правильно. Первый шаг — понять задачу прежде, чем ее решать. Всегда имейте в виду, что требуется большее количество времени, чтобы переделать задачу, чем в случае, когда все делается правильно в первый раз. Мы посмотрим, что означает "правильно" на стадии физического моделирования данных при проектировании БД. Это будет намного позже.
36
Фундаментальные концепции БД Введение В этой главе я стремлюсь снять завесу мистики над теорией и терминологией, используемой многими академиками БД. Группа, которой я принадлежу — наиболее активная группа "бойцов-фронтовиков", кто фактически проектирует хорошие пользовательские БД, чтобы заработать на жизнь, и вся эта теория кажется очень удаленной от мира, в котором мы живем и работаем. Это может вызвать у вас вопрос, является ли теория проектирования БД заслуживающей внимания. Конечно, понимание принципов и теории, на которых построена технология, несомненно, очень важно. Например, предположим, что вы отдаете свой автомобиль механику, который не понимает, как работает зажигание, или летите в самолете с пилотом, который не понимает теорию полета? Имея это в виду, почему вы должны ожидать, что заказчики придут к вам для экспертизы БД, если вы не понимаете теорию, которая поддерживает проектирование БД? Вышеупомянутое означает, что некоторые из терминов и концепций, используемые академиками, могли бы запутать и расстроить лучших из нас, частично из-за используемой математики. По этой причине имеются некоторые, известные только посвященным термины, которые, казалось бы, должны для большинства программистов означать одну и ту же вещь, но фактически означают кое-что совершенно разное. Однако важно получить некоторое понимание реляционной теории БД, чтобы можно было разрабатывать соответствующие проекты для таких систем.
Глава 3 Итак, чтобы понять, о чем бредит академическое сообщество, мы хотим посмотреть на некоторые простые аспекты реляционной теории, сравнивая их с SQL-концепциями и методами, которые нам уже известны. Поскольку имеется много других книг по данному предмету, мы не собираемся зарываться слишком глубоко на территорию академиков; скорее мы собираемся попробовать представить основу полезных концепций БД. Эта глава представит реляционные концепции в значительной степени с SQL-ориентированной точки зрения. Мы будем часто касаться реализации вещей (хотя эта часть книги не связана с техническими вопросами), которых нельзя избежать при предоставлении нашего теоретического объяснения в терминах концепций, с которыми вы будете знакомиться. Однако мы не увязнем в вопросах реализации; это будет только краткий обзор. Для большей информированности не стесняйтесь рыться в других источниках, предложенных в главе 1. На протяжении этой главы мы рассмотрим: •
Реляционную модель — исследование терминологии, используемой для наиболее значимых блоков; мы будем их строить в более поздних главах.
•
Определения дополнительных концепций БД.
Реляционная модель Как упоминалось в главе 1, Е. Ф. Кодд изобрел реляционную модель в начале 1970-х гг. В этом разделе мы рассмотрим ее наиболее важные части с точки зрения Microsoft SQL Server. Как мы увидим, реализация с помощью SQL Server имеет много общего с этой моделью, но не совсем то же самое. Многие реляционные пуристы съеживаются, когда реализуются все разновидности SQL (не только Microsoft SQL Server). Эта книга — не место для обсуждения их возражений, и мы намереваемся избежать любого противоречия в отношении этого, поскольку рассматриваем физический проект и части реализации на SQL Server.
База данных Первый термин, который мы должны определить, — база данных (БД).
Просто говоря, база данных — собрание данных, организованных для легкого и быстрого поиска и восстановления.
Это может быть карточный каталог в библиотеке, БД на SQL Server или текстовый файл. Технически, нет никакой соответствующей концепции в реляционной теории, но поскольку мы используем этот термин часто, важно, чтобы мы представляли его.
В SQL Server база данных — собрание объектов и данных, которые логически сгруппированы вместе.
38
Фундаментальные концепции БД
Таблица Одно из наиболее частых неправильных представлений относительно реляционной модели — то, что термин реляционный связан с отношениями между таблицами. Фактически он связан с термином relation (математическое название таблицы), который оказывается (почти) синонимом термину "таблица". Как мы увидим, реляционная модель использует различные имена не только для того, чтобы большинство программистов знало о таблицах, но также и об элементах, содержащихся в них. Центральный, наиболее важный элемент, с которым мы имеем дело — сама таблица. Таблица — физическое представление некоторого объекта, или реального, или мнимого. Когда вы проектируете таблицы, то будете рассматривать людей, места, вещи (существительные), о которых вы хотите хранить информацию. Понимание концепции рассмотрения таблиц как существительных, а существительных как таблиц, является первым шагом к проектированию соответствующих таблиц. Позже мы исследуем более подробно, что мы должны хранить в таблицах и других структурах, рассмотренных в этом же разделе. Затем в следующей главе мы начнем рассмотрение, как идентифицировать таблицы и другие объекты. Термин таблица — существенно ориентируемый на реализацию термин и имеет следующий смысл:
Упорядоченное расположение данных. Особенно такое, в котором данные размещены в столбцах и строках в существенно прямоугольной форме.
Две общих версии таблиц: электронная таблица Excel и результат простого SQL-запроса в БД Northwind (русская локализация этой БД называется Борей). Последняя таблица показана ниже: Щ SQL Queiy Analyzer - (Quew - NASH200.Noithwind.£NA\ldavidson - Untitledi'] File Editfluerytools Wn i dow Help • * 10 Northwind Щ$ ^•еИП Ш t i (ft «о П pelect
B
from region
-J-if RegionID
RegionDescription Eastern Western Northern Southern
(4 rou(s) affected)
Query bach completed
NASH200130) ENAMdavidson 1511 NuilUw.d 0 00 00
4 fows Lni.CoH
39
Глава 3 Даже до компьютерных электронных таблиц люди работали на бумаге с таблицами, содержащими строки и столбцы (хотя они и не назывались электронными таблицами). Мы взглянем глубже на строки и столбцы позже в этом разделе. Как мы упомянули ранее, таблица известна как отношение, или, более определенно, именованное отношение. Как мы рассмотрим позже, таблицы и представления — оба рассматриваются как именованные отношения, а результирующие наборы — неименованные отношения. Понимание этого поможет вам лучше писать SQL-запросы. Следующая таблица — очень характерная таблица, составленная для ресторанов с несколькими определенными столбцами. Каждый из терминов таблицы будет определен полностью в этой главе. Вторичные ключи
Первичный ключ
Table ID • (табличный • идентификатор)
Таблица Отношение
:
Name (имя)
Code (код)
Date (дата)
1
Big Momma's V ittles
BMV
10/22/00
2
Next Step Eatery
NSE
10/22/00
3
Bob's Dog Hut
BDH
11/13/00
4
Earl's Restaurant
ER
8/12/00
5
Roger's Restaurant
RR
7/4/00
Строки
Число строк
Тип данных, определенный пользователем тип данных и/или проверкиограничения
Это не означает, что таблица и отношение — одно и то же. Определение отношения следующее:
Отношение: структура, составленная из атрибутов (индивидуальные характеристики, типа имени или адреса, соответствующие столбцам в таблице) и кортежей (наборы значений атрибутов, описывающих индивидуальные характеристики отдельных экземпляров, например, заказчиков, соответствующих строкам таблицы). В пределах отношения экземпляры не могут повторяться; каждый должен быть уникален. Кроме того, экземпляры в пределах отношения считаются неупорядоченными; если два экземпляра поменять местами, отношение не изменяется.
Это определение может сначала показаться немного запутанным. Не волнуйтесь; все станет более ясным, когда мы пройдем через всю эту главу.
40
Фундаментальные концепции БД Отношение — очень строгая математическая концепция, основанная на теории множеств, в то время как таблица — физическая реализация отношения со специальными свойствами. То есть, можно сказать, что отношение может использоваться, чтобы определить все таблицы, но не все отношения могут быть представлены как таблицы. Однако разумно предположить, что различия между этими двумя понятиями являются столь тонкими, что могут показаться среднему программисту БД почти бессмысленными. Однако мы будем использовать другой термин для обозначения таблицы —сущность (entity). Термин "сущность" часто используется в логическом моделировании, чтобы обозначить концептуальную версию таблицы. Я лично люблю этот термин, поскольку он действительно подчеркивает, что таблица — фактическое представление чего-то. "Таблица" — очень уж абсолютный термин, который имеет совершенно другое значение вне области информатики. Первый раз, когда я сказал моей жене, что построил таблицу, она подумала, что я ничего не делаю по своей профессии, "А я думала, что ты был с компьютерами" Термин "сущность" имеет меньшее количество издержек и меньше привязан к реализации. В следующей главе, когда мы начнем идентифицировать таблицы, то сначала идентифицируем их концептуально как сущности, чтобы избежать втискивания в структуру, которую таблицы непременно имеют.
Строки и столбцы Строка (или запись) задает экземпляр какого-то объекта, который физически представляется таблицей. Концепция экземпляра связана с объектно-ориентированным программированием, но интерпретируется по-другому в реляционном программировании. В объектно-ориентированном программировании (ООП) (и даже в объектно-ориентированных базах данных) класс — общее определение, содержащее атрибуты и методы, которые могут работать с конкретными экземплярами класса, обычно упоминаемыми как объекты. В SQL Server мы имеем дело с таблицами, содержащими столбцы и строки. Определение таблицы во многом подобно классу в ООП, а конкретный экземпляр таблицы — строка, которая аналогична объекту. Поскольку мы не имеем никаких методов для управления данными, специально введенных в определение таблицы, мы должны иметь определенные SQL-операции, которые можно использовать для взаимодействия с данными в таблицах. Главная причина различий, вообще говоря, — проблема реализации. Поскольку SQL в большей степени создан для организации доступа к данным, очень легко в любой момент видеть многочисленные экземпляры SQL-"объектов", так что многие программисты не замечают параллели между объектом ООП и строкой SQL Server. При моделировании данных и объектов в последующих главах мы подчеркнем некоторые из параллелей между методологиями ООП и реляционным программированием БД. Как мы можем видеть на рисунке ниже, реляционная модель существенно отличается названиями строк и столбцов. Строки — кортежи (tuples — по-английски, произносится как "купле"), а столбцы — атрибуты. Термин "кортеж" — один из тех забавных технических терминов, который, кажется, или заставит вас сделать паузу и задуматься, или заставить вас хихикать (что и случилось со мной). Согласно своей легенде, слово "кортеж" не имеет никакого другого значения, и вы можете найти его не в любом словаре. Возможно, оно было выдумано в течение формирования реляционной модели Кодда, в частности, как слово без предвзятых понятий, приложенных к нему. Это устраняет беспорядок, возникающий при использовании терминов типа "таблица".
41
Глава 3
Вторичные ключи
Первичный ключ
Name (имя)
Code (код)-
Date (дата)
1
Big Momma's V ittles
BMV
10/22/00
2
Next Step Eatery
NSE
10/22/00
3
Bob's Dog Hut
BDH
11/13/00
4
Eari's Restaurant
ER
8/12/00
5
Roger's Restaurant
RR
7/4/00
Taole Ю
(тай.шчный иден!ифйквн1р)
Таблица (сущность) Отношение
Строки Число строк Кортежи
Тип данных, определенный пользователем тип данных и/или проверкиограничения
В то время как термин "кортеж", кажется, довольно неочевиден многим программистам, атрибут — название намного лучшее для представления столбца. Каждый столбец должен содержать характеристику или атрибут экземпляра строки или кортежа, или более просто сказать — каждый столбец содержит атрибут, который описывает строку. Если думать в такой манере, использование столбца становится более осмысленным. Аналогия со свойствами ООП должна быть разумно очевидна для любого, кто использовал ООП. Столбец — свойство экземпляра (или строки) таблицы (или отношения/сущности). Один из принципов реляционной теории — то, что атрибуты не имеют никакого свойственного им упорядочения. Вы, вероятно, поняли из опыта, что столбцы в таблице имеют свойственное им упорядочение, поскольку SQL-пользователи хотят иметь постоянный набор столбцов, которые будут возвращены независимо от того, как столбцы были эффективно размещены в физической среде. Главный момент здесь заключается в том, что вы никогда не должны задумываться о физическом упорядочении столбцов. Имеется несколько общих правил сортировки столбцов, которым архитекторы данных обычно следуют при проектировании таблиц, типа того, что первичные ключи размещаются слева в таблице, за которыми следуют более важные столбцы, а менее читаемые столбцы размещаются в правой части, но это — только конкретно для удобства программиста или пользователя. Мы рассмотрим это в дальнейшем, когда фактически начнем моделировать таблицы в главе 5. Каждая строка в таблице должна содержать один и тот же набор столбцов. Каждый столбец должен иметь название, которое является уникальным среди названий столбцов в той же конкретной таблице. Это может казаться очевидным, но все же стоит упомянуть. Последний момент, который мы должны обсудить, — степень отношения, которая соответствует числу столбцов в таблице. Этот термин не используется так часто, но я видел, что он упоминался в некоторых документах, и его стоит отметить для завершенности.
42
Фундаментальные концепции БД Атрибуты атрибута Я только что дал такое название этому разделу, потому что оно заставило меня улыбнуться. Однако важно понять, что каждый столбец в таблице имеет собственные атрибуты. Наиболее важными характеристиками (помимо названия столбца, которое уже рассмотрено) являются следующие: •
допустимые значения столбца;
•
является ли столбец частью идентификатора строки;
•
обязательны ли значения столбцов.
В ряде следующих разделов мы будем говорить об этом более подробно.
Ограничение значений столбца только допустимыми значениями Таблицы SQL Server имеют несколько средств для ограничения значений, которые могут быть введены в столбец: •
типы данных (основные);
•
определенные пользователем типы данных;
•
ограничения;
Q
триггеры и хранимые процедуры.
Каждое из них служит четко своей цели и должно обычно использоваться в перечисленном порядке. В реляционной терминологии, как показано на рисунке ниже, все они рассматриваются под заголовком домены. Домен определяется как множество допустимых значений для данного атрибута. Мы кратко рассмотрим эти механизмы в следующих разделах. Первичный ключ
Вторичные ключи
Число строк
Возможные значения
Тип данных, определенный пользователем ] 4 — т и п данных / и/или проверки-ограничения домены
4 3
Глава 3 Основные типы данных Основной тип данных — самая простая форма домена в SQL Server. Тип значения определен так же, как характерный диапазон входной величины. Одна дополнительная деталь, которую следует упомянуть, — то, что каждый тип данных также поддерживает значение NULL, если мы не определяем иначе. Например: •
Переменная типа i n t e g e r требует ввода целой числовой величины и может быть между - 2 3 1 (-2 147 483 648) и +2 3 1 (2 147 483 647).
•
Тип данных t i n y i n t также требует ввода целой числовой величины, но имеет диапазон неотрицательных целых чисел между 0 и 255.
•
Тип данных v a r c h a r (20) требует данные в виде ASCII-символов в диапазоне от 0 до 20 символов.
Базовые типы данных — набор "примитивных" типов данных, которые определены в SQL Server. SQL Server имеет следующие базовые типы данных, которые могут использоваться как типы данных столбцов: binary big int bit char datetime decimal float image int money nchar ntext nvarchar
numeric real smalldatetime smallint smallmoney sql variant sysname text timestamp tinyint uniqueidentifier varbinary varchar
Есть еще два типа данных: c u r s o r и t a b l e , хотя они никогда не используются в качестве типов данных столбцов. Выбор точного типа данных столбца — один из наиболее важных выборов, которые вы будете выполнять на стадии физического проектирования. Во время логической стадии проектирования, как минимум, нужно определить общий тип данных, которые должны быть размещены. Общие классы типов данных могут включать: Binary (двоичный) Blob (BLOB-тип) Character (символ) Date (дата) Logical (логический) Numeric (числовой) Time (время) Мы обсудим типы данных более детально в главе 10.
44
Фундаментальные концепции БД Определяемые пользователем типы данных (с правилами) SQL Server позволяет создать настраиваемые типы данных, основанные на существующих типах. Такой тип определяется только в терминах единственного базового типа данных. Далее можно создать правило и связать его с новым типом данных, которое ограничивает размещаемые значения, допустимые в столбце, по своему желанию. Это позволяет вам создать много столбцов из одного и того же шаблона и обеспечить контроль вводимых значений. Если вы не знакомы с определяемыми пользователем типами данных, для получения дополнительной информации проконсультируйтесь в SQL Server Books Online (электронное руководство по SQL Server). В качестве простого примера вы могли бы пожелать задать следующие определяемые пользователем типы: •
IntegerFromOneToTen — определяемый как целое число, с правилом, которое ограничивает вводимые значения в диапазоне от единицы до десяти включительно.
•
SocialSecurityNumber — определяемый как строка из девяти символов или строка из одиннадцати символов, которая ограничена наличием черточек в четвертой и седьмой позициях, а все другие символы — цифровые.
Как вы видите, многие общие типы данных могут быть созданы, чтобы помочь в реализации домена значений, допустимых для столбца. Нужно также отметить, что определяемые пользователем типы данных не должны быть простыми основными типами данных, как определено в реляционной теории. Допустимо иметь более сложные типы данных, пока в них нет повторяющихся групп. Например, вы могли бы иметь следующий тип данных: •
2DGraphPoint — определенный как X — целое число, Y — целое числа
Но вы не могли бы иметь тип данных таблицы с неограниченным числом атрибутов. Это допустимо только в случаях типа графической точки, где две величины X и Y фактически представляют единственный атрибут при таком расположении. Обратите внимание, что Microsoft SQL Server в настоящее время не поддерживает это.
Ограничения Ограничение — другой механизм, который может использоваться, чтобы ограничить значения, вводимые в столбец. •
•
•
NULL-ограничения — NULL-ограничение определяет, обязательно ли данные должны быть введены в столбец. Значение NULL в реляционной теории указывает, что атрибут имеет неизвестное значение, обычно представляется как может быть. Важно понимать, что когда сравниваются два значения NULL, они никогда не равны. Проверки-ограничения — механизмы, которые позволяют нам определять допустимый диапазон значений для столбца. Например, мы можем создать ограничение на столбец, который хранит месяц года и допускает только значения от 1 до 12. Ограничение внешнего ключа — средства, которые используются, чтобы быть уверенным, что значение в одной таблице соответствует ключу другой таблицы. Они используются, чтобы обеспечить целостность данных и поддерживать отношения между таблицами. Хотя мы и не будем их в данной главе рассматривать более глубоко, важно просто упомянуть их существование в роли домена для столбца. 45
Глава 3 Триггеры и хранимые процедуры Вы можете использовать триггеры и хранимые процедуры, чтобы реализовать домен столбца, хотя они вообще-то используются для намного более мощных целей. В этом разделе мы рассмотрим только использование каждого из них в качестве домена. Триггеры — механизмы, которые позволяют коду запускаться всякий раз, когда данные в таблице вводятся или изменяются. В то время как они позволяют нам закодировать сложные правила домена, которые не могут быть записаны в ограничения, они не могут регулировать значения, вводимые в отдельный столбец. Последнее имело бы место только в том случае, если бы правила были слишком сложные для простого ограничения, например при требовании автоматической зачистки для ввода новых значений от удаленного супервизора. Мы исследуем триггеры более подробно в последующих главах. Другой механизм в SQL Server, который формирует домен столбца — хранимая процедура. Хранимые процедуры — не лучший способ делать это, но, опять-таки, могут иметься серьезные основания для их использования (например, запрещение прямого доступа к определенным строкам и/или столбцам данных). Основная задача состоит в том, что для этого случая мы должны учесть все корректировки для столбца, чтобы фильтровать с помощью единственной хранимой процедуры или повторяемого для каждого случая количества кода, когда пользователь должен обновлять данные. Единственный случай, когда мы должны обратиться к использованию хранимых процедур для формирования домена — это когда выполнение требований необязательно или они изменяются. Мы должны также кратко упомянуть, что в то время как хранимые процедуры могут использоваться, чтобы реализовать домены, то же самое можно сказать относительно любого кода, который написан, чтобы вставить данные в столбец. В главе 11 мы рассмотрим триггеры и хранимые процедуры как механизм обеспечения целостности данных более детально.
Замечание относительно терминологии В этой книге мы обычно будем использовать термин "домен" на протяжении логического проектирования, чтобы указать описание типов данных, таких как номер (number), целое число (integer), строка (string) и т. д. вместе с общим описанием допустимых значений для атрибута. Мы будем стараться использовать тот же самый домен для всех столбцов, которые имеют одни и те же атрибуты. Например, мы могли бы определить следующие домены в типичной БД: •
Amount (сумма) — денежная величина, с доменом, который всегда требует значения;
•
String (строка) — характерное значение строки, которая содержит алфавитно-цифровые величины и которая всегда требует значения; Description (описание) — значение строки, которая используется для дополнительного описания экземпляра таблицы и которая всегда требует значения;
•
• •
46
FutureDate (будущая дата) — значение даты, которая при вводе должна быть больше, чем текущая дата, и которая всегда требует значения; Social Security Number (номер социального страхования) — строка из одиннадцати символов формата ###-##-####, которая всегда требует значения.
Фундаментальные концепции БД Когда мы идентифицируем новые атрибуты, то будем смотреть, подходит ли новый атрибут какому-либо существующему домену, и если это так, то мы назначаем соответственно этот домен. Если же это не так, то мы создаем новый домен. Это даст нам атрибуты, которые позволяют строить БД более быстро, более легко, и более последовательно. Мы будем использовать термин бизнес-правила, чтобы указать предикат для таблицы или БД в целом. Предикат — просто правило, которое управляет значениями в БД. Как пример этого, рассмотрим домен SocialSecurityNumber. Этот домен пришел с предикатом, размещающим значение в атрибут, который использует его всегда в формате "###-##-####". В то время как мне не очень-то нравится термин "бизнес-правила", это довольно распространенный сегодня термин. Заметим здесь, что мы продолжим далее определять бизнес-правила на протяжении всей книги, так как имеется много различных проблем, связанных с этим очень общим термином. Очень часто мы не будем идентифицировать наши бизнес-правила как часть определений таблицы (или сущности); скорее, они будут помещаться в нашу документацию и реализацию.
Идентификаторы строк В реляционной теории отношение не может иметь повторяющиеся кортежи. Те, кто имеет опыт работы с SQL Server, конечно, хотели бы перейти к построению индекса, чтобы иметь дело с этой ситуацией. Однако в реляционной теории не имеется также и никакой концепции индекса, так как индекс — физическое устройство, а, строго говоря, реляционная теория не имеет дело с проблемами реализации. В физической таблице нет никакого ограничения, которое говорит, что не должны быть повторяющиеся строки. Однако, в практическом смысле, никакие две строки не могут быть идентичными, потому что в деталях реализации скрыты атрибуты, которые предотвращают появление этой ситуации (типа номера строки или точного местоположения в физическом носителе данных, которое вы не можете фактически видеть). Однако это всегда плохо, когда Вы можете иметь повторяющиеся строки по ряду причин: •
Невозможно определить, какая строка является какой; это означает, что мы не имеем никакого логического метода изменения или удаления отдельной строки.
•
Если больше чем один объект имеет точно те же самые атрибуты, это, вероятно, опишет тот же самый объект так, что если мы пробуем изменить одну строку, то другая строка тоже должна измениться; это означает, что мы имеем зависимые строки.
Чтобы бороться с этим, мы используем концепцию потенциального ключа. Ключ используется, чтобы обеспечить уникальность атрибута или совокупности атрибутов. Сущность может иметь столько потенциальных ключей, сколько потребуется, чтобы обеспечить уникальность ее столбцов. Так же, как в SQL Server, мы имеем концепцию первичных ключей и вторичных ключей. Первичный ключ используется как первичный идентификатор для таблицы, а вторичные ключи (реализуемые как уникальные ограничения) — другие поля, по которым должна поддерживаться уникальность, как видно из следующей таблицы:
3 1868
47
Глава 3
Первичный ключ Потенциальные Вторичные ключи
Число строк
Тип данных, определенный V\ пользователем Возможные j 4 — тип данных значения / и/или ^ проверки-ограничения домены
Выбор потенциальных ключей — очень важный вопрос. Большой процент ошибок БД возникает оттого, что не определены все возможные потенциальные ключи. Когда мы будем получать правила нормализации в главе 6, то обсудим специфические особенности выбора потенциальных ключей. Составной ключ Ключи, по определению, могут быть составлены из любого числа столбцов, хотя лучше стараться ограничивать число используемых столбцов настолько, насколько возможно. Старайтесь иметь определенное число столбцов, насколько это возможно, но если необходимо, может быть столько столбцов, сколько вы пожелаете. Например, вы можете иметь таблицу Book (книга) со столбцами Publisher_Name (название издателя), P u b l i s h e r _ C i t y (город издания), ISBN_Number (ISBN-номер), и Book_Name (название книги). Для тех, кто не знает, ISBN — уникальный идентификационный номер, назначаемый книге при ее издании. Из этих атрибутов мы решаем, что можем определить три ключа: •
Publisher_Name, Book_Name — очевидно, что издатель, вероятно, выпустит больше чем одну книгу. Также можно осторожно предположить, что названия книг не уникальны в множестве всех книг. Однако, вероятно, истинно то, что один и тот же издатель не будет издавать две книги с одним и тем же названием (по крайней мере, мы предположим, что это так).
• •
ISBN_Number — мы уже говорили, что ISBN_Number уникален. P u b l i s h e r _ C i t y , ISBN_Number — так как ISBN_Number уникален, то комбинация P u b l i s h e r _ C i t y и ISBN_Number также уникальна.
Первый и третий ключи, которые мы определили, — сложные ключи. О третьем следует сказать несколько больше. Значение этого ключа — то, что в каждом городе вы можете повторно использовать ISBN_Number; факт, который является, очевидно, не истинным. Это — очень частая проблема со сложными ключами, когда они не обдуманы должным образом. 48
Фундаментальные концепции БД Хотя это немного в стороне от темы, важно не путать уникальные индексы с ключами. При работе с индексами могут быть причины выбрать ключ P u b l i s h e r _ C i t y , ISBN_Number в нашей БД на SQL Server. Однако мы не должны идентифицировать его как первичный ключ таблицы. В главе 10 мы обсудим реализацию ключей, а в главе 14 рассмотрим реализацию индексов для улучшения доступа к данным. Первичный Ключ Первичный ключ — это ключ, который используется, чтобы дать определенный интерфейс доступа к отдельной строке таблицы. Обычно, это — просто указатель (термин, заимствованный из функционального программирования), используемый для доступа к строке, и он, вероятно, не должен изменяться или, по крайней мере, должен изменяться очень нечасто. В следующем разделе будет кратко введено понятие типа ключа, называемого искусственным ключом, который мы будем использовать на протяжении этой книги, и который будет спасать вас от многих неприятностей во время реализации. Мы обсудим точную логику, определяющую этот выбор, когда будем рассматривать физическое проектирование, но пока достаточно рассмотреть только смысл терминов. Искусственные ключи Искусственный ключ — изобретение, единственной причиной существования которого является возможность идентифицировать строку. Это обычно автоматически формируемый номер (столбец идентификации), или GUID (Globally Unique IDentifier — глобальный уникальный идентификатор), который является очень большим идентификатором, уникальным на всех машинах в мире. Реализация GUID — вне возможностей этой книги; однако полная документация доступна в www.microsoft.com. Главная причина использования искусственного ключа состоит в том, чтобы обеспечить ключ, который конечный пользователь никогда не должен видеть и никогда не должен с ним взаимодействовать. Он служит просто как указатель, и ничего больше. Для тех, кто не понимает, что такое указатели, они являются общими конструкциями, пришедшими из языка С или любого языка программирования низкого уровня, используемыми для указания на местоположение в памяти. Искусственные ключи указывают на положение в таблице. Для таблицы должны быть также определены и другие ключи, иначе это некорректная таблица. Концепция искусственного ключа — своего рода беспокойство для тех, кто в перспективе является пуристом. Так как он не описывает запись вообще, то может ли он на самом деле быть атрибутом записи? Искусственные ключи, вероятно, даже не должны быть упомянуты в части, посвященной логическому проектированию, но важно знать об их существовании, так как они будут, несомненно, все еще неожиданно возникать в некоторых логических проектах. В главе 6 мы обсудим все "за" и "против" такого подхода. Вторичные ключи Любой потенциальный ключ, который не выбран как первичный ключ, упоминается как вторичный ключ. Вторичные ключи очень важны для успешной работы БД. По той или иной причине, большинство таблиц имеет больше чем один способ идентификации. Это особенно важно, когда используются искусственные ключи в качестве первичных ключей.
Необязательные значения столбца Это — куча проблем, которые я предпочел бы не раскрывать, но должен. Таблицы могут иметь столбцы, для которых содержание еще неизвестно или ненужно в данном контексте. К сожалению, в SQL имеется только один механизм, чтобы обозначить, что это факт. Это — значение NULL. Из перечня различных целей использования NULL очевидно, что это — не оптимальный путь обработки таких ситуаций. Помещение неправильных данных в столбец 49
Глава 3 (типа даты 31 декабря 9999) даже более сомнительно, чем NULL, по двум причинам. Во-первых, вы можете впоследствии обнаружить, что хотите хранить данные с точно тем же самым значением, которое вы только что ввели. Во-вторых, вы можете увидеть, что ввод значения, подобного этому, может оказаться непонятным пользователю. Закон независимости целостности (номер 10 из 12 законов Кодда — см. приложение А) требует, чтобы никакой столбец в первичном ключе не был необязательным столбцом. Сведенный к единственному предложению он означает: NULL — плохо. Многие теоретики предпочли бы, чтобы с NULL было покончено раз и навсегда. Имеется много причин для этого, как мы увидим в более поздних главах, но в этот момент мы будем придерживаться очень простого взгляда. NULL определен как неизвестное значение так, что никакие два значения NULL не эквивалентны. Аналогично при реализации NULL рассмотрение значения NULL может иметь несколько возможностей: •
Я не знаю значение.
•
Я никогда не буду получать значение.
•
Значение в этом столбце не имеет смысла в данной ситуации.
Я предпочел бы видеть набор реализованных значений типа NULL, чтобы знать, почему столбец имеет значение NULL. Имеется ряд методов работы с необязательными данными без NULL, но реализация может требовать использования весьма небольшого числа таблиц. Мы обсудим, как работать со значениями NULL, несколько глубже в главе 12.
Число строк Число строк в таблице имеет специальное название в реляционной теории. Как показано на рисунке, приведенном ниже, это называется мощностью (cardinality). Этот термин редко используется, чтобы указать число строк в таблице; однако он используется, чтобы указать число строк, которые удовлетворяют некоторому условию. Мы рассмотрим другую форму мощности позже в этой главе, когда будем обсуждать отношения. Первичный ключ Потенциальные Вторичные ключи ключи
Строки Число строк Кортежи
Мощность
Возможные значения
5 0
Тип данных, определенный пользователем ] 4 — тип данных / и/или проверки-ограничения домены
Фундаментальные концепции БД Дополнительные соображения насчет таблиц В этом разделе мы рассмотрим некоторые дополнительные соображения, относящиеся к таблицам в реляционной теории. Внешние ключи Когда ключ — не ключ? Когда это — внешний ключ. Не шутка, но истина. Внешний ключ — фактически столбец (или комбинация столбцов), чьи значения соответствуют первичному ключу или уникальному ключу в той же самой или другой таблице. Существование внешнего ключа в таблице представляет реализацию отношений между таблицами. Мы обсудим отношения более подробно позже в этой главе. Внешний ключ не обязательно должен иметь уникальные значения. Закон независимости целостности (номер 10 из 12 законов Кодда — см. приложение А) требует, чтобы для всех величин, не являющихся необязательным внешним ключом БД, имелось соответствующее значение первичного ключа в связанной таблице. Представления Представления — это таблицы, которые не существуют как базовые (фактические) таблицы; фактически, они — виртуальные. Представление — реализация именованного отношения, как мы обсуждали ранее в разделе Таблица этой главы. Представления появляются как таблицы для пользователя и должны осторожно рассматриваться в этом качестве. Основанные на законах Кодда (см. приложение А), представления, как предполагается, являются корректируемыми, что сильно увеличивает их способности выполнять свои задачи. Фактически, в SQL Server 2000 мы можем реализовать любое представление как корректируемое, используя несколько новых особенностей. Это будут рассмотрено в деталях в части Физическое проектирование книги, когда мы будем выяснять, какие проблемы можно, а какие нельзя решить, используя представления. Представления имеют ряд полезных применений и должны рассматриваться как часть логического проекта, поскольку они могут использоваться, чтобы решать некоторые очень важные проблемы. Они могут использоваться для следующих целей: Q
Обеспечение безопасности. Включением или исключением определенных столбцов из определения представления, конкретной отменой прав доступа к базовым таблицам и выдачей прав на представление можно предусмотреть безопасность исключенных столбцов.
•
Возможность рассматривать различными пользователями одни и те же данные различными способами. Строя различные представления данных, пользователи могут получить представление, которое удовлетворяет их потребностям. Таким образом, данные, которые являются несущественными для пользователя, могут быть скрыты от него, чтобы улучшить читаемость. В дополнение к этому, могут быть установлены отношения с помощью внешних ключей для отображения пользователю данных из связанных таблиц.
•
Обеспечение логической независимости данных. Один из основных принципов реляционной теории заключается в том, что реализация БД не должна давать никаких различий для пользователей. Если что-то изменяется в реализации БД, пользователь не должен это заметить. Представления 51
Глава 3 могут использоваться почти таким же образом, чтобы изолировать пользователя от изменений базовой таблицы. Очевидно, что представление зависит от структуры основной таблицы, но если вы добавите столбец в основную таблицу, представление не должно добавлять его, если в этом нет необходимости. Если вы удаляете столбец или перемещаете его в другую таблицу, представление можно так изменить, чтобы вернуть отсутствующее значение или получить значение из новой таблицы. Представления — очень важная часть любой реализации реляционной БД. Однако они рассматриваются среди физических проблем реализации, так что следующий раз мы обсудим представления именно в этой части книги. Отношения Законы Кодда (см. приложение А) заявляют, что каждая БД должна быть описана на логическом уровне таким же образом, как и данные пользователя, чтобы пользователи с надлежащей безопасностью могли использовать один и тот же реляционный язык для рассмотрения пользовательских данных. Реляционная теория рассматривает структуру отношения таким образом, чтобы обеспечить два момента: •
Заголовок — набор пар — имя столбца и имя типа данных, — которые определяют столбцы таблицы. Это служит прослойкой между определениями, которые видит SQL-программист, и фактической реализацией, которую программист СУРБД (системы управления реляционной базы данных, Relational DataBase Management System — RDBMS) обычно реализует в структуре сервера БД.
•
Тело — строки, которые составляют таблицу.
В SQL Server и большинстве БД мы чаще рассматриваем каталог или, в терминах SQL Server, системные таблицы как групповое описание таблиц и других структур в БД. SQL Server представляет заголовок или каталог набора таблиц, называемый информационной схемой (Information Schema). Мы рассмотрим информационную схему в главе 10. Ограничение значений, которые помещаются в таблицы До сих пор мы имели дело со значениями ограничений, накладываемых на столбцы, и которые технически накладываются на них. Однако поскольку мы уже рассмотрели концепцию домена столбца, нам теперь просто нужно кратко перечислить ситуации, когда нужно ограничить данные в зависимости от других данных. Мы можем разделить их на три типичных сценария: •
зависимости между столбцами;
•
зависимости между строками;
•
зависимости между таблицами.
Каждый из них налагает на реализацию весьма специфические проблемы: •
Зависимости между столбцами. Зависимости между столбцами имеют дело с ситуацией, когда значение, размещенное в одном столбце, требует некоторого домена значений в другом столбце.
52
Фундаментальные концепции БД Например, пусть имеются два поля: hasDriversLicence (наличие водительских прав) и driversLicenseNumber (номер водительских прав). Если значение hasDriversLicense равно False, то не нужно вводить значение DriversLicenseNumber. Обычно вы решаете эту задачу для таблицы с помощью простого ограничения CHECK (проверка). •
Зависимости между строками. Зависимости между строками описывают ситуацию, где каждая строка той же самой таблицы диктует значения в других строках. Примером этого могла бы быть банковская система. Когда пользователь собирается произвести изъятие со своего счета, предварительно должны быть выполнены все бизнес-операции, чтобы увидеть, имеется ли достаточное количество денег, чтобы сделать изъятие. Фактически это могло бы быть выполнено и иначе, но логически оно должно выглядеть именно так. В общем случае эта ситуация должна быть решена, используя триггер, или, что менее желательно, хранимую процедуру. Одной из реальных проблем, связанных с зависимостями между строками в SQL Server, является то, что достаточно легко предотвратить чрезмерные величины (слишком много записей со слишком большими значениями), но очень трудно предотвратить слишком малые вводимые значения. Мы вернемся к этой теме в главе 12, когда начнем реализовывать этот вид проблем.
U
Зависимость между таблицами. Другая ситуация могла бы состоять в том, что если в одной таблице находится какое-то конкретное значение, то вы не хотите помещать соответствующие данные в связанную таблицу. Примером такого ограничения могла бы быть персональная информация о пользователе. Одна таблица будет содержать запись имени человека и его возраста, а другая таблица может быть предназначена для хранения его адреса и телефонного номера. Если человек находится в определенном возрасте, может быть фактически незаконным хранить его адрес и телефонный номер. Как и с зависимостью между строками, это также должно быть решено, используя триггер или, что менее желательно, хранимую процедуру. В зависимостях между таблицами существуют те же самые трудности, что и с зависимостями между строками.
Предикаты Мы обсудили предикаты для атрибутов, но теперь нам нужно рассмотреть предикаты, которые управляют использованием таблицы. Напомним, что термин предикат — причудливый путь задания правила, которое управляет значениями в БД. Предикат для каждой таблицы должен быть сформирован во время логического проектирования настолько полно, насколько возможно. Говоря прозаически, составление списка предикатов для таблицы нужно не только разработчику, но и пользователю для утверждения этих правил. Предикат должен содержать любое правило, управляющее вводом данных. Довольно трудно этому следовать, но овчинка стоит выделки. В следующей главе мы рассмотрим разработку предикатов для таблиц, с которыми будем иметь дело. 53
Глава 3
SQL-операции Важный момент, который нужно отметить при обсуждении реляционной технологии — реляционные операторы в T-SQL. Мы не собираемся слишком углубляться в это обсуждение, но некоторое упоминание об этих операторах должно быть включено в любое обсуждение реляционной модели просто для законченности. В этом разделе мы кратко рассмотрим операторы отношения, составляющие реляционную алгебру, которая является основой для всех SQL-реализаций. Как мы намекнули ранее, все наборы результатов, которые возвращаются запросом SELECT (запросом на выборку) языка SQL, можно рассматривать как таблицы (хотя они физически и не размещены соответствующим образом). Обратите внимание, что таблица должна содержать уникальные названия для каждого возвращаемого столбца, но это не всегда выполняется для набора результатов SQL (даже при условии, что должно быть именно так). Важно помнить этот основной принцип, поскольку вы пишете ваши собственные операторы SELECT, и это причина того, что использование SELECT * является особенно неприятным, что будет подтверждено в главе 12. Таблицы, которые сформированы в результате любого SQL-действия, считаются неименованными таблицами в противоположность именованным таблицам и представлениям. Первоначально Кодд сформулировал восемь реляционных операторов. Большинство их имеет форму одного отношения в виде оператора с одним или двумя отношениями. Только один из них не соответствовал этой форме и не реализуется в SQL. Следующие реляционные операторы — список операторов, которые первоначально были описаны Коддом в 1972 г. в Relational Completeness of Data Base Sublanguages (Реляционная реализация диалектов базы данных). Они приведены в списке в том порядке, в котором представлены в тексте, поскольку каждый логически добавляется к предыдущему. •
Ограничение (Restrict)
•
Набор (Project)
•
Соединение (Join)
•
Произведение (Product)
•
Объединение (Union)
•
Пересечение (Intersect)
•
Разность (Difference)
•
Деление (Divide)
Есть весьма много исследований в этой области, и совсем немного операторов было добавлено за эти годы. Исходные восемь используются, чтобы дать краткое представление реляционных операторов. Мы кратко рассмотрим каждый из исходных операторов и оставляем читателю право выбора их чтения по своему усмотрению. Важно не путать эти операторы с любыми ключевыми словами, используемыми в конкретном SQL-диалекте. Конкретный набор SQL-операторов является результатом двадцатилетних попыток создать реляционный язык, который конечные пользователи могли бы легко использовать, в то же время реализуя громадное число операций.
54
Фундаментальные концепции БД Ограничение Оператор ограничения используется, чтобы ограничить строки таблицы в соответствии с данным критерием. Это прямо аналогично простому выражению WHERE запроса SELECT. Например: SELECT FROM Щ WHERE
fieldname, fieldname2, tablename fieldname > value
...
,
fieldnameN
Выражение WHERE — реализация оператора ограничения, и можно рассматривать результат этой операции как неименованную таблицу с единственным полем fieldname и содержащую все строки, где fieldname больше, чем заданное значение v a l u e . В выражении WHERE мы можем использовать любой из обычных операторов сравнения (=, >, <, >=, LIKE и т. д.).
Набор Оператор набора используется, чтобы ограничить число столбцов в результирующей выходной таблице. В следующем примере мы предполагаем, что имеется более одного столбца в таблице: Щ SELECT FROM
DISTINCT fieldname, tablename
fieldname2
Таким образом вы можете последовательно подразделить результирующую таблицу на отдельные столбцы или комбинации столбцов. В реляционной теории любые повторяющиеся значения могут быть удалены, если мы добавим ключевое слово DISTINCT языка SQL. Обычно возникает вопрос: "Почему в реляционной теории нет никакого оператора DISTINCT?". Каждый оператор в нашем обсуждении здесь состоит из одного или двух отношений и возвращает отношение. По определению, отношение не имеет никаких двойных строк. Так как результатом нашего запроса должно быть отношение, он не будет иметь никаких дубликатов, и оператор DISTINCT принципиально избыточен. В сущности, поэтому оператор DISTINCT является встроенным. Опять-таки, это — одно из наиболее важных различий между определениями таблицы и отношения.
Соединение Оператор соединения используется, чтобы связать одну таблицу с другой на основе соответствия значений столбца, обычно первичного ключа одной таблицы с внешним ключом другой. SELECT FROM
fieldname tablenamel JOIN tablename2 ON tablenamel.keyl
= tablename2.keyl
Мне кажется, что эта операция также должна быть объявлена, используя более реляционный алгебраический стиль. Реляционный алгебраический стиль показывает, как ограничены возможности реляционных операторов по сравнению с гораздо более функциональным SQL-языком.
55
Глава 3
I
JOIN WITH OVER
tablenamel tablename2 keyl
Это обозначение на самом деле намного более ясное, но делает точно ту же самую вещь. Оно, конечно, работает только тогда, когда названия ключей одинаковы. Причина, по которой я показываю это обозначение, заключается в том, что я хочу ясно дать понять, что JOIN в первом отрывке кода можно представить как отдельную таблицу. Далее рассмотрим влияние объединения двух операций JOIN, как в следующем запросе: SELECT FROM
fieldname table1 JOIN table2
ON tablel.keyl = table2.keyl JOIN table3 ON table2.key3 = table3.key2 Первую операцию соединения можно представить следующим образом: (SELECT * FROM Tablel JOIN table2 ON tablel.keyl = table2.keyl) = derivedTablel
Тогда мы можем переписать наш запрос следующим образом: SELECT FROM
I
fieldname derivedTablel JOIN table3 ON derivedTablel.key2 = table3.key2
Это все, насколько глубоко мы будем вникать в операторы соединения в этом разделе. Цель этого раздела просто ввести в курс работы оператора соединения и связи его с технологией соединения в SQL Server. В главах 11 и 12 мы снова рассмотрим эту концепцию.
Произведение Одной из наименее полезных и наиболее опасных операций является операция произведения. Оператор произведения берет две таблицы и строит таблицу, состоящую из всех возможных комбинаций строк по одной из каждой таблицы. В SQL-языке он обозначается выражением CROSS JOIN. SELECT FROM
I
fieldname tablenamel CROSS JOIN tablename2
Для лучшего объяснения этой операции рассмотрим следующее:
56
Фундаментальные концепции БД
Tablel:
Fred
Table2:
Sam
_>
Bugs
Wilbur
Tablel product Table2
Fred
Sam
(умноженная на)
Fred
Bugs
Ed
Fred
Ed
Wilbur
Sam
Wilbur
Bugs
Wilbur
Ed
Вы можете видеть, что число строк, которое будет возвращено оператором произведения — число строк в Tablel, умноженное на число строк в ТаЫе2. Это число может стать весьма большим, содержать двойные и бесполезные строки и может быстро задушить ваш сервер, так что оператор произведения используется не так часто.
Объединение Оператор объединения берет в качестве входных две таблицы одной и той же структуры и строит другую таблицу со всеми строками из обеих таблиц, но удаляя дубликаты: SELECT FROM Щ UNION SELECT
FROM Tablel:
fieldname tablenamel fieldname
tablename2 Fred
Table2:
Wilbur
Fred
-3
Wilbur
Tablel product Table2
Fred
(объединение с)
Wilbur Babe
Babe
Самое время напомнить вам, что две входные таблицы оператора объединения должны иметь один и тот же тип, но не обязательно они должны быть базисными таблицами. Таблица может также рассматриваться и как вторичная таблица, сформированная операцией набора, рассмотренного ранее. •
Пересечение Оператор пересечения очень близок к операторам объединения и присоединения. Оператор пересечения в значительной степени противоположен оператору объединения. Он использует две таблицы одной и той же структуры, удаляет значения, которые не присутствуют в обеих таблицах одновременно, и возвращает результирующую таблицу. Tablel:
Jeb
Table2:
Jeb
Silver
Howie
Carney
Billy
_»
Tablel intersect Table2
Jeb Carney
Carney Bob
57
Глава 3 Пересечение непосредственно не реализовано в SQL-синтаксисе. Следующий фрагмент кода показывает основной способ реализации пересечения в SQL. SELECT FROM I
fieldname tablenamel JOIN tablename2 ON tablenamel.fieldname = tablename2.fieldname
Разность Разность — операция, которая используется весьма часто при реализации системы. Точно так же, как объединение и пересечение, разность снова требует таблиц одной и той же структуры. Суть оператора разности заключается в том, что мы хотим получить каждое значение из одной таблицы, которого нет в другой таблице. Это — в противоположность оператору пересечения, который удаляет значение, если оно не находится в обеих таблицах. Разность снова не имеет соответствующего ключевого слова в SQL. Она может быть реализована наиболее простым способом, используя оператор NOT IN, как показано ниже: SELECT FROM
fieldname tablenamel
WHERE
f i e l d n a m e NOT IN (SELECT f i e l d n a m e FROM t a b l e n a m e 2 )
I
Если вы программировали на SQL, то, вероятно, заметили, что эта реализация, возможно, не оптимальна, хотя в логическом смысле это — лучшая реализация. Хотя мы обычно не должны формулировать запросы на стадии логического моделирования проекта БД, однако, если это потребуется, мы должны всегда использовать наиболее прямое представление запроса, который мы хотим сформировать.
Деление Оператор деления — один из странных операторов, который вы, вероятно, никогда не будете использовать. Он использует два отношения, одно — с единственным столбцом, другое — с двумя столбцами, и возвращает таблицу значений, которые соответствуют (в столбце, который определен) всем значениями в отношении с единственным столбцом. Нет никакого определенного способа осуществить это способом, который будет ясен в SQL, так что мы опустим это здесь из нашего обсуждения, поскольку оператор не является жизненно важным для большинства архитекторов БД и SQL-программистов.
Другие операторы Дополнительные операторы могут быть разделены на две категории — типы корректировки (INSERT, UPDATE, DELETE) и дополнительные поисковые типы. Мы не будем в этой главе обсуждать операторы корректировки, поскольку они очень близки к тому, как SQL реализует их, а также имеют очень небольшую возможность логического варьирования. Мы будем обсуждать их в некоторых деталях в главе 12. Так же как и поисковые операторы, есть другие реляционные операторы, определенные в течение почти тридцати последних лет, начиная с формулировки рассмотренных выше восьми основных операторов. Фактически все команды DML-языка (Data Manipulation
Language — язык манипулирования данными — ЯМД) в SQL Server являются или 58
i
I!
Ill
II
I
II! I
Фундаментальные концепции БД результатом, или расширением этих основных операторов. Мы должны также обратить внимание, что DDL-язык (Data Definition Language — язык определения данных — ЯОД) в основном используется для определения наших объектов, и в то время как может и не быть никакого эквивалента реляционного оператора, любой реляционный язык должен осуществить некоторую форму DDL, чтобы строить объекты для DML. Я ограничил пределы возможностей книги этими восемью исходными операторами. Цель данного раздела, посвященного реляционным операторам, состояла не в том, чтобы заменить или даже дополнить различные учебники по этой теме, а, скорее, дать представление, которое поможет нам разобраться с основами того, откуда произошли SQL-операции. Понимание основных принципов SQL поможет нам при логическом проектировании наших БД и написании кода.
Отношения Мы уже упомянули концепцию первичного ключа и внешнего ключа. Теперь поместим их рядом и посмотрим на объект, который они формируют, —отношение. Отношения — это то, что делают таблицы, которые вы успешно создали, заменяя повторяющиеся группы связью таблиц. Однако имеется один момент, который мы должны учесть при этом. Типы отношений, которые мы будем обсуждать в этом разделе, не сводятся только к форме, когда на первичный ключ одной таблицы ссылается другая таблица через внешний ключ. Если это так, то независимо от того, как все отношения будут в конечном счете физически выполнены при реализации БД, логически может потребоваться несколько отношений "первичный ключ — внешний ключ", чтобы обеспечить простые логические отношения. В этом разделе мы обсудим типы логических отношений и оставим обсуждение физической реализации на более позднее время. Отношения на этом этапе могут быть разделены на два основных типа, смысл которых нам нужно понять: Q
бинарные отношения;
Q
небинарные отношения.
Различие между этими двумя типами заключается в числе таблиц, обеспечивающих отношение. Бинарные отношения включают две таблицы, в то время как небинарные отношения включают более чем две таблицы. Это может показаться маленьким различием, но на самом деле это не так. SQL и связанные с этим отношения ограничены бинарными отношениями, в то время как в реальном мире нет никаких таких ограничений. Когда вы проектируете БД, то должны помнить и уметь представить каждую из возможных ситуаций. Когда мы будем рассматривать моделирование данных в главе 5, то обсудим, как представить каждую из них в модели данных.
Бинарные отношения Бинарные отношения — отношения между двумя таблицами. Большинство отношений, с которыми мы обычно имеем дело, подойдет под эту категорию. Число порожденных строк, которые могут участвовать с каждой из сторон отношения, называется мощностью. В этом отношении мы рассмотрим различное число элементов бинарных отношений.
59
Глава 3 Отношения "один к п" Отношения "один к п" представляют собой класс отношений, где одна таблица передает свой первичный ключ другой таблице в качестве внешнего ключа. Таблица, которая содержит первичный ключ, известна как таблица-предок, а таблица, которая получает и использует его как внешний ключ, — как таблица-потомок. Эта терминология предков-потомков рассматривается только в отношении между двумя конкретными таблицами. Потомок может иметь много предков, а предок может иметь много потомков. Отношение "один к п" — наиболее общий тип отношения, с которым вы будете иметь дело. Однако имеется несколько различных возможностей для мощности этого отношения. Можно выделить следующие варианты: •
Отношения "один к одному": мощность отношения "один к одному" означает, что для любого конкретного предка может существовать только единственный потомок. Примером этого типа отношений могло бы быть отношение House (дом) — Location (расположение), как изображено на рисунке ниже. Очевидно, что дом может находиться только в одном месте. Обсуждение того, почему все это не расположено в одной таблице, отложено на более позднее время.
Houseld (идентификатор дома) House Type (тип дома) 1001
1
1002
2
1003
1
1004
3
1005
4
Houseld (идентификатор дома) Location (расположение)
•
60
1001
Database Drive
1002
Administration Avenue
1003
SQL Street
1004
Primary Key Place
1005
Business Rules Boulevard
Отношения "один ко многим": отношение "один ко многим" является наиболее важным типом отношения. Для каждого предка может быть неограниченное число записей-потомков. Примером отношения "один ко многим" могло бы быть отношение "State (государство) — Address (адрес)", как показано на рисунке ниже. Очевидно, имеется много адресов для одного государства, хотя конкретный адрес может быть только в одном государстве.
Фундаментальные концепции БД
State Id No.(идентификатор штата)
Amabala
2
Aksala
3
Sasnakra
State Id Number
(идентификатор штата)
в.
State (штат)
1
Number of bedrooms
Address (адрес)
(число спален)
Number of bathrooms (число санных комнат)
1
402 Database Drive
1
1
1222 Administrator Avenue
2
1
2
23 Login Lane
2
1
2
246 Cardinality Close
1
1
2
893 Server Street
3
2
3
107 Query Quay
2
1
3
293 Primary Key Place
4
2
1
Отношения "один точно к п": фактически отношение "один к одному" — просто частный случай этого типа отношений. Это редко используемый тип отношений, но в подходящих случаях он может использоваться. Например, он мог бы быть использован, если пользователь может иметь только два адреса электронной почты. Следующий рисунок показывает, как отношение мощности "один к двум" могло бы использоваться, чтобы обеспечить отношение "User (пользователь) — E-mail (электронная почта)".
Employee Reference Number (номер сотрудника)
5001
•
Bob
5002
Red
5003
Jean
Employee Reference Number (номер сотрудника)
—• —•
Name (имя)
EMail Address (адрес электронной почты)
5001
[email protected]
5001
[email protected]
5002
[email protected]
5002
[email protected]
5003
[email protected]
5003
[email protected]
Рекурсивные отношения: рекурсивные отношения — это те отношения, когда предок и потомок — одна и та же таблица. Этот вид отношений — то, как можно реализовать дерево, используя SQL-конструкции. Если вы реализуете рекурсивное отношение мощности "один к двум", то можете создать древоподобную структуру данных. Классический пример рекурсивных отношений — спецификация материалов. Возьмем автомобиль. Составные части и он сам могут рассматриваться как части стоимости для изготовителя, и каждая из его компонент, которая также имеет свои части, представляет часть, которая входит в целое. Некоторые из этих компонент также составлены из частей. В рассматриваемом примере автомобиль можно представить составленным рекурсивно из частей автомобиля и всех их составных частей. 61
Глава 3 Отношения "многие ко многим" Другой тип бинарных отношений (фактически второй наиболее общий тип отношений) — отношение "многие ко многим". В отличие от случая, когда имеется единственный предок и один или большее количество потомков, здесь может быть больше чем один предок и потомок. Это невозможно осуществить, используя только две таблицы, но мы оставим реализацию отношений "многие ко многим" до части книги, связанной с физическим моделированием, и главы 10. Примером отношения "многие ко многим" может быть автомобильный дилер. Возьмем любую конкретную модель автомобиля, которая продается многими различными автомобильными дилерами. Далее возьмем одного конкретного автомобильного дилера. Он, в свою очередь, продает много различных моделей автомобилей.
Небинарные отношения Небинарные отношения включают в отношение больше двух таблиц. Это бывает гораздо чаще, чем можно было бы ожидать. Например: Wrox снабжает книгами продавцов книг, которые обеспечивают их продажу. Оно называется тройным отношением, потому что включает три таблицы. Когда мы имеем дело с такими отношениями, то разбиваем их на три таблицы с двумя или большим количеством отношений. •
Wrox снабжает книгами.
•
Продавцы книг продают книги.
•
Продавцы книг снабжаются издательством Wrox.
Однако из этих трех отношений вы не можете точно вывести исходное тройное отношение. Все, что вы можете вывести, это: •
Wrox снабжает книгами кого-то.
•
Продавцы книг продают книги, которыми их снабжает какое-то издательство.
•
Wrox обеспечивает чем-то продавцов книг.
Из этих отношений вы не можете точно вывести исходное утверждение, что Wrox снабжает книгами продавцов книг. Это — обычная ошибка, которую делают многие архитекторы данных. Исходное отношение будет, вероятно, идентифицировано на стадии проектирования, а затем не будет реализовано должным образом, когда вы начнете разрабатывать отношения. Надлежащее решение этого вида задачи состояло бы в том, чтобы иметь таблицу для издателей (Wrox), таблицу для изделий (книги) и таблицу для магазинов (продавцы книг). Тогда вы имели бы таблицу с тремя бинарными отношениями, по одному на каждую из этих таблиц. Это решает проблему (пока). В главе 7 на основе расширенных правил нормализации мы будем иметь дело с проблемами, которые возникают, когда вы должны хранить отношения с более чем двумя таблицами. 62
Фундаментальные концепции БД
Последнее замечание относительно реляционной/SQL терминологии В этой книге мы будем придерживаться терминологии SQL-стиля кроме случаев, где это не имеет смысла. Важно понять основные принципы реляционной теории, однако не нужно смущаться и тому, кто не прочитал эту главу или серьезно не изучал БД. SQL-терминология может и не быть совершенной (и действительно, это так), и так думает большинство программистов, даже те, кто являются приверженцами реляционной теории.
Определения существенных понятий Прежде чем завершить эту главу, описывающую фундаментальные понятия БД, следует рассмотреть пару дополнительных понятий. Они будут очень важны позже в процессе проектирования (и, возможно, более ясными), но представление о них важно дать в этой главе.
Функциональная зависимость Термин функциональная зависимость является одним из тех терминов, которые выглядят более сложными, чем есть на самом деле. Это — фактически очень простое понятие. Если мы определяем функцию для какого-то одного значения (назовем eroValuel), а результат этой функции всегда одно и то же значение (скажем, Value2), то Value2 функционально зависит от Value 1. Это довольно простое, но все же важное понятие, которое нужно представлять. Правильно реализованные функциональные зависимости — очень полезные вещи, в то время как неподходящие функциональные зависимости являются основой многих проблем БД. Существует один дополнительный запутывающий термин, который связан с функциональной зависимостью. Этот термин —детерминант, который может быть определен как "любой атрибут или комбинация атрибутов, от которых функционально зависит любой другой атрибут или комбинация атрибутов". Так, в нашем предыдущем примере, Value 1 можно было бы рассматривать как детерминант. На ум приходят два примера: •
Рассмотрим математическую функцию типа 2 * X. Для каждого значения X будет получено конкретное значение. Для 2 мы получим 4, для 4 мы получим 8. Каждый раз, когда мы помещаем в функцию значение 2, мы всегда будем получать 4, так что в этом случае 2 функционально определяет 4 для функции 2 * X.
•
В более связанном с БД примере рассмотрим серийный номер изделия. Из серийного номера может быть получена дополнительная информация типа номера модели и других конкретных характеристик изделия. В этом случае серийный номер функционально определяет ряд конкретных характеристик.
Если это вам кажется знакомым, то потому, что любой ключ таблицы функционально определяет другие ее атрибуты, и если вы имеете два ключа, один из которых — первичный ключ, а другой — вторичный ключ, то первичный ключ функционально определяет вторичный ключ и наоборот. Это понятие будет самым важным при обсуждении нормализации в главе 6. 63
Глава 3
Зависимость с несколькими значениями Интуитивно менее понятной является зависимость с несколькими значениями. В нашем предыдущем разделе мы обсуждали функциональную зависимость в случае единственного значения. В качестве иллюстрации зависимости с несколькими значениями рассмотрим факт, что эта книга была написана одним автором, так что, если бы мы определяли функцию автора этой книги, мы каждый раз должны были бы иметь единственное значение. Однако если бы существовала функция технического редактора, она возвратила бы много значений, так как у этой книги несколько технических редакторов. Следовательно, технический редактор для книги имеет несколько значений. Мы снова столкнемся с понятием функциональной зависимости позже, в главе 6.
Резюме В этой главе мы проскочили тридцать лет исследований всего с помощью нескольких страниц. Мы определили многие из терминов и понятий, которые являются очень распространенными как в нашей нынешней корпоративной прагматической культуре, так и в кругах "чистых" исследователей. Мир нуждается в обеих из этих групп, чтобы работать вместе. Мы также посмотрели на некоторые основы построения реляционного языка программирования SQL. В следующей таблице мы установим соответствие между общими терминами БД SQL Server и их синонимами из реляционной теории и, в меньшей степени, терминологией логического проектирования: Общий термин
Синоним
Таблица
Столбец
Сущность (конкретно для логического проектирования), отношение (чаще в теоретических дискуссиях) Атрибут
Строка
Кортеж
Допустимые значения, которые могут быть размещены в столбце, учитывая тип данных, ограничения и т. д. Число строк
Домен
Число столбцов
Степень
Мощность
Моя искренняя надежда — что данная глава представит эти темы на уровне технической конференции, что, по крайней мере, даст вам достаточное теоретическое понимание, чтобы сделать проектирование БД понятным, или, что лучше, стимулирует вас пойти и прочитать больше о предмете, чтобы вы полностью поняли всю ту информацию, которую я пробежал слишком быстро. Конечно, мое реальное опасение — что сам Е. Ф. Кодд прочитает мою книгу и придет в мой дом, чтобы отругать меня за то, что, потратив годы и проштудировав большое количество страниц хорошо обдуманных записей и исследований, я превратил их в двадцать страниц или около этого. В этом русле я надеюсь, что вы будете эту тему изучать и далее, так как она позволит вам стать хорошим архитектором/программистом БД. Сделав краткую теоретическую остановку во время путешествия в страну БД, мы возвращаемся к изучению нашей темы, поскольку действительно начинаем сам процесс проектирования. 64
.4 ^ n
Сущности, атрибуты, отношения и бизнес-правила Введение Теперь, посмотрев на основные блоки здания БД, мы должны начать идентифицировать эти блоки здания в рамках предварительной информации, которую мы собрали в главе 2. Я допускаю, что такой подход выглядит излишне осторожным, но это — "лучшая практика", однако многие квалифицированные программисты, вероятно, будут иметь свой план. Я так же на это смотрю; однако результаты, которые я получаю, всегда гораздо лучше, когда я выполняю эти предварительные шаги перед началом проектирования новой БД. Чем больше мы будем следовать этому пути при проектировании, тем лучшее будет результат. Методы, описанные в этой главе, помогут вам избежать опасности увеличения затрат на обслуживание проекта, уменьшить частоту, с которой вы должны будете потом возвращаться, чтобы изменить структуры и код, и вообще уменьшать риск неудачи. В главе 2, где описывается стадия сбора информации, мы собрали массу информации, которая в настоящий момент загромождает ваш стол. В этой главе добавим к этой информации еще структуру и затем взглянем на все интегрально. Мы пройдем все — идентификацию и назначение начальных названий таблицам, отношениям, столбцам и бизнес-правилам — конечно же, при этом мы консультируемся с нашими пользователями для получения любой дополнительной информации. Закончив этот процесс, мы будем иметь основную документацию, которая представлена все еще в формате, когда можно делиться информацией с пользователем без бремени технических терминов и диаграмм (которые мы начнем разрабатывать позже).
Глава 4
Отказ от выбора окончательной структуры на ранних стадиях Для тех читателей, кто плохо знаком с проектированием БД, или кто даже не создал ни одной таблицы, принципы, рассмотренные в этой главе, должны выполняться, чтобы гарантировать лучший результат для полученного проекта. Если вы уже знаете, как создать таблицу, то могли бы подвергнуть сомнению упрощенный характер того, как мы рассуждаем в этой главе. Вы уже можете иметь некоторые идеи того, какие таблицы следует разбить и на какие составные части. Однако при выполнении таких действий на ранних этапах процесса проектирования, что может казаться выгодным для сокращения времени разработки, вы заплатите за это в полной мере, когда должны будете возвратиться для корректировки БД, вставляя, например, столбцы, потому что вы пренебрегли необходимостью потратить время для рассмотрения вашей стратегии с клиентом. Не забудьте также продолжать корректировать вашу документацию, так как лучшая в мире документация бесполезна, если она устаревшая. Как только вы создадите одну БД или две (или две сотни таких БД), то, вероятно, поймете, что очень трудно отказаться от досрочного выбора окончательной структуры. Когда вы слушаете клиента, объясняющего, что он хочет, то, вероятнее всего, представляете набросок структуры даже до того, как он выскажет все, что хочет; это будет почти вашей второй натурой. К сожалению, это — не лучшее, что можно сделать на этой стадии развития/организации процесса проектирования БД. Вы должны до поры, до времени игнорировать эти организационные поползновения. Независимо от того, с помощью каких средств вы фактически объединяете информацию (средств создания ER-диаграмм (ER-диаграммы — диаграммы сущностей и отношений), средств объектно-ориентированного представления структур, текстового процессора или даже карандаша и бумаги), — со стремлением построить БД на этом этапе нужно бороться. Почему же? Ответ очень прост. В вашем рвении собрать воедино создаваемую БД, вероятно, будет пропущена ценная информация, не будут выполнены, по-видимому, и некоторые важные шаги, которые мы обсудим в следующих главах. На этом этапе вы должны ограничить себя только рассмотрением собранной документации для определения следующих моментов: •
сущности и отношения;
•
атрибуты и домены;
•
бизнес-правила.
Делая это, вы, вероятно, завершите работу с намного меньшей грудой документации, возможно, представляющей десять процентов от первоначального объема, но будете все еще уверены, что не пропустили никакие важные данные. В этот момент вы можете гарантировать получение информации от пользователя до того, как начнете применять очень жесткую последовательность операций, рассмотренную в следующих трех главах, чтобы выполнить процесс разработки. Мы упомянули в главе 3, что с этой главы и далее будем использовать ориентируемые на реализацию термины, чтобы описать объект, когда это имеет смысл. В этой главе мы используем термины "Сущности" и "Атрибуты", поскольку будем искать объекты, которые никогда не могут появиться как физические структуры. Мы начнем использовать ориентируемые на реализацию термины, как только станем переходить к структурам, напоминающим то, что обычно называется таблицами и столбцами. 66
Сущности, атрибуты, отношения и бизнес-правила Простой пример Проиллюстрируем то, что вы только что прочитали, следующим примером, который мог легко оказаться фрагментом записей реального процесса проектирования.
'Имеются ЗакаЗШки, котфым они продают подфэ/санные иЗделия, обы1но офисное оборудование, но такэ/се и мебель. Они идмжифиуи^/ют ЗакаЗЬков номфом, ффми^/еммм cyijjecm6ymije4 системой. Они makojce, продают новые вепрь, но подффшние ~ это то, на^ км они Зарабатывают. Скта-фактфи iwiarmwmm в^се-тделънл, но поставки делаются немедленно, как только ЗакаЗЫк делает ЗакаЗ, если ему довфяют (нет никаких кредитных тфоблем). У1юбш иЗмтения скта-фактфм долфнм быть Задокумштм^юванм. Здсе изделия отправляются со склада в i/етфе города. 'Нуфно поместить инффмаз/ию о контактах для kaojakm ЗакаЗЫка — телефонные тмфа (факс и голосоЫь), adftec п адреса элёятфтнои* тюЬпм. ЯфтсоофаЗно иажъЗоватъ факс или электронную noimy для фшниЗауии nodmflefo/cdmuii. 'Желательно такфе иметь с£о(юдную область для контактов, т. к. они могут бести ффшш контактов с ЗакаЗЫкамн.
В каждом из последующих разделов, мы будем просматривать эту документацию, чтобы выявить нужную информацию. Звучит достаточно просто, а? Это, действительно, намного легче, чем могло бы показаться.
Определение сущностей Сущности, действительно, одни из наиболее простых для выявления объектов, когда вы просматриваете документацию. Они обычно представляют людей, места, объекты, идеи или вещи, которые грамматически выступают как существительные. Это не означает, что сущности отражают только реальные вещи. Ими могут быть также абстрактные концепции, например, "Новые изделия" является абстрактной концепцией, которая не является физической, но представляет удобный способ группировать диапазон изделий. Фактически, более разумным определением того, что такое сущность, может быть то, что она используется для размещения всей наглядной информации, необходимой для полного описания отдельного человека, места, объекта или идеи. Например: Q
Человек: может представлять студента, служащего, водителя и т. д.
•
Место: город, здание, дорога и т. д.
•
Объект: деталь, инструмент, электронный блок и т. д.
•
Идея: документ с требованиями, группа (подобно группе безопасности для приложения), журнал действий пользователя и т. д.
Обратите внимание, что имеется наложение нескольких из категорий (например, здание — "место" или "объект"). Вам редко будет требоваться дискретно идентифицировать, что 67
Глава 4 представляет собой сущность — человека, место, объект или идею. Однако если вы можете разместить ее в пределах конкретной группы, это может помочь назначить некоторые атрибуты, например "Имя" для человека или "Адрес" для здания. Для тех, кто имеет опыт работы с реляционными БД, будет ясно, что когда сущность реализуется в фактическую таблицу БД, она будет, вероятно, очень отличаться от начальных сущностей, которые вы определили. Важно не волноваться относительно этого на данной стадии процесса проектирования — мы должны постараться не заниматься серьезно в данный момент возможной реализацией БД. Причина, почему мы заинтересованы различать людей, места и объекты — то, что это поможет определить некоторую основную информацию, которая потребуется позже, когда начнем заниматься в данной книге реализацией. Это также поможет нам быть уверенными, что мы имеем всю документацию, необходимую для описания каждой сущности.
Люди Почти каждая БД должна будет хранить информацию относительно некоторого вида сущностей, представляющих людей. Большинство БД будет иметь, по крайней мере, какое-то упоминание пользователя. В то время как не все пользователи — люди, о пользователе почти всегда думают как о человеке, хотя пользователем могла бы быть корпорация или даже машина, наподобие сетевого принтера. Что же касается реальных людей, в отдельной БД может потребоваться хранить информацию о многих людях. Например, БД школы может иметь в качестве сущностей учащихся, учителей и администрацию. Может также потребоваться, чтобы только одна таблица представляла все три категории. Это — вопрос, который мы рассмотрим поподробнее в более поздних примерах. В нашем примере можно выделить две сущности людей — "Контакты" и "Заказчики":
нуфно поместить инффмаумю о контактах для kaojaim ЗакаМнка ... ... ojcyfmtu контактов ... Информация о партнерах и заказчиках, которую нам нужно хранить, должна, однако, быть описана, и мы на этой стадии процесса документирования не можем знать, действительно ли они будут рассматриваться как отдельные сущности. Мы рассмотрим различные атрибуты партнеров и заказчиков а разделе этой главы, касающемся атрибутов.
Места Существует много различных типов мест, относительно которых пользователи пожелают хранить информацию. В нашем примере записей имеется одна очевидная сущность, связанная с местом:
иЗдвлт со склада 6 уенпфе города. Обратите внимание, что партнеры и заказчики, очевидно, имеют почтовые адреса, определяющие их местоположение. 68
Сущности, атрибуты, отношения и бизнес-правила
Объекты Объекты относятся, прежде всего, к физическим предметам. В нашем примере мы имеем три различных объекта:
к)м(фым они продают подфо/сатыв нЗделил, otmluo офисное оборудование, но такфе и мебелг Изделия, офисное оборудование и мебель — объекты, но заметим, что мебель и офисное оборудование — также изделия. Однако вас не должно шокировать, что изделия, офисное оборудование и мебель будут иметь очень разные атрибуты. Можно ли объединить эти три объекта в одну абстрактную сущность, называемую изделиями, или будем рассматривать их как три отдельные сущности, будет полностью зависеть от того, с какой целью вы разрабатываете конкретную БД.
Идеи Нет никакого правила, требующего, чтобы сущности были реальными объектами или даже физически существовали. На этой стадии разработки мы должны будем рассмотреть информацию, которую пользователь хочет хранить об объектах, которые не соответствуют уже установленным категориям "людей", "мест" и "объектов" и которые могут быть или не быть физическими объектами.
Документы Для многих термин "документы" обычно ассоциируется с осязаемыми листами бумаги, содержащей информацию, которую клиенты должны отслеживать. Это может выглядеть как спор о мелочах, но это не то же самое. Что, если мы сделаем копии листов бумаги? Означает ли это, что имеются два документа, или оба они — тот же самый документ? В нашем примере мы действительно просто ссылаемся на большое число листов бумаги:
. Clem-cfia'kmyfm mlamawmcu ... Счета-фактуры — листы бумаги, которые отправляются заказчику после поставки изделий. Однако мы только знаем, что счета-фактуры напечатаны физически на бумаге, нокогда они отправлены или даже как они посланы, на этой стадии неизвестно. На этой стадии мы только идентифицируем сущности и двигаемся дальше; опять мы выдвигаем предположения относительно того, как данные будут использоваться позже в этом процессе.
электронную mimy для о^шннЗауш тдтв(фф)внн4 ... Здесь мы имеем тип документа, который не написан на бумаге — сообщение электронной почты.
Другие сущности На этой стадии разработки мы должны будем рассмотреть информацию относительно сущностей, которые пользователи хотят хранить и которые не соответствуют уже установленным категориям "людей", "мест", "объектов" и "документов". 69
Глава 4 Протоколы ревизий и журналы БД Типичная сущность, которую вам нужно будет определить — протокол ревизии или журнал БД. Это — не обычная сущность, в которой не хранятся никакие данные пользователя и, по существу, должна вообще рассматриваться на физической стадии проектирования. Единственные виды сущностей, которые мы рассматриваем на этой стадии — это те, о которых пользователь желает хранить данные. Аналогично и операторы, наподобие следующего, не нужно рассматривать на этой стадии, а следует оставить до реализации:
Любые иЗменения dema-фактфп долфны быть Задокументированы
р
События Сущности-события обычно представляют собой глаголы или действия.
... поставки делаются немедленно, как только ... Записываются ли поставки в какой-либо физический документ, на данном этапе неясно: выражение лишь указывает, что событие имеет место. Эти события важны, и пользователи захотят вести их список или формировать данные, связанные с возникновением событий. В нашем примере, может быть, мы будем документировать события после того, как они произойдут, так что это может вылиться в протокол сделанных поставок. Другим типом событий для вас могут быть показания счетчика, погодные данные, измерения приборов и т. д. Записи и журналы Последний из типов сущностей, которые мы исследуем на этой стадии — запись или журнал действий. Заметьте, что я имею в виду записи, не находящиеся в БД. Это может быть любой вид деятельности, при котором пользователь мог бы предварительно сделать записи на бумаге. В нашем примере пользователь хочет хранить записи о каждом сделанном контакте с заказчиком.
... могут 9Шт ффнал контактов о Зака31пкамн. Это другой тип сущностей, который подобен контрольному протоколу, но потенциально он будет содержать больше информации, — такой как замечания относительно контакта, а не только то, что контакт имел место.
Список сущностей Пока мы определили следующий список предварительных сущностей.
70
Сущности, атрибуты, отношения и бизнес-правила
Сущность
Описание
Контакт
Люди, выступающие в качестве заказчиков, и с которыми следует установить контакт
Заказчик
Организация или конкретное лицо, которым продаются изделия, офисное оборудование и/или мебель
Склад
Место, где размещаются товары для продажи
Поставки
События, когда проданные товары поставляются заказчику
Изделия
Товары для продажи
Офисное оборудование
Тип изделия
Мебель
Тип изделия
Счет-фактура
Физический документ, который отправлен заказчику, чтобы запросить оплату за проданные изделия
Подтверждение размещения изделия
Электронный документ (возможно, это единственный формат), который позволяет сообщить заказчику, что заказ размещен
Журнал
Распечатка всех контактов
Бухгалтерский журнал
Фактическая запись контакта с заказчиком
Замечание для физического моделирования: регистрировать любые изменения счетов-фактур Описания основаны на фактах, которые мы тщательно выбрали из исходной документации. Обратите внимание, что в описании контакта мы не стали делать никаких предположений относительно того, как используются данные, в то время как полностью описаны главные моменты, основанные на том, что было предварительно согласовано. Теперь мы имеем список всех сущностей, которые были определены в нашей предыдущей документации. На этой стадии мы постоянно просматриваем то, что сказал нам клиент. Когда же мы начинаем анализировать то, что было определено, то можем заняться деталями, которые клиент нам не дал. Вместо того чтобы заполнять бреши предположениями и догадками, нам нужно (в идеале) подготовить список вопросов и передать их деловому аналитику, который повторно посетит клиента, чтобы извлечь большее количество информации: •
У вас имеется только один склад?
•
Ваши заказчики — отдельные лица или большие сообщества?
•
Имеются другие типы изделий, которые вы продаете? Или мебель и офисное оборудование настолько важны, что вы должны хранить специальную информацию относительно них?
71
Глава 4
Определение атрибутов и доменов Основная причина использования здесь термина "атрибуты" вместо термина "столбцы" заключается в том, что мы будем искать элементы, которые идентифицируют, составляют, и описывают сущность, которую вы пытаетесь представить; либо, выражая это в компьютерных терминах — свойства сущности. Например, если сущность — "Человек", атрибуты могли бы включать "Номер водительских прав", "Номер социального страхования", "Цвет волос", "Цвет глаз", "Вес", "Супругу" ("Супруга"), "Детей", "Почтовый адрес" и "Адрес электронной почты". Каждая из этих характеристик служит, чтобы представить какую-то сторону сущности. Однако из нашего определения того, что должен хранить столбец, мы знаем, что это должна быть единая часть информации, хотя для атрибута такого требования нет. Идентификация, которая может связать атрибуты с сущностью, требует различного подхода, исходя из определений самих сущностей. Атрибуты часто находятся как прилагательные, используемые для описания сущности, которую вы предварительно нашли. Поскольку информация о домене данных обычно определяется в то же самое время, когда и атрибуты, мы также посмотрим и на них.
Идентификаторы Причина, почему мы начинаем с идентификаторов, заключается в том, что они являются наиболее важными. Каждая сущность должна иметь по крайней мере один идентифицирующий атрибут или набор атрибутов. Без атрибутов нет никакой возможности далее в процессе определять различные объекты. Этими идентификаторами является то, что мы определили как ключи в главе 3. В вышеупомянутом примере один такой идентификатор показан следующим образом:
Они идттифпурфуют ЗакаЖнков номером, формируемым существующей системой1. Почти для каждой сущности, которую вы разрабатываете на этой стадии проектирования, легко найти какой-либо идентификатор. Причина этого кроется в том, что на этой стадии вы имеете дело с сущностями, которые легко выбрать, потому что они имеют естественные идентификаторы. Важно также быть уверенным, что то, о чем вы думаете как об уникальном элементе, действительно является уникальным. Взгляните на имена людей. Для нас они почти уникальны, но на самом деле в Соединенных Штатах имеются сотни Луисов Дэвидсонов, а это не самое частое имя. Имеются тысячи, если не миллионы, Джонов Смитов! Вот некоторые общие примеры хороших идентификаторов:
72
•
Для людей — номера социального страхования (в США); полные имена (не всегда совершенный идентификатор); или другие числовые идентификаторы (например, номера заказчиков, табельные номера служащих и т. д.).
•
Для деловых документов (счета-фактуры, счета, формируемые компьютером уведомления) — они обычно определяются некоторым назначенным числом, когда посылаются другим организациям.
Сущности, атрибуты, отношения и бизнес-правила
Q
Для книг — ISBN-номера (сами названия не уникальны).
•
Для изделий на продажу — номера изделий (сами названия изделий не уникальны).
•
Для компаний, с которыми клиенты имеют дело — они обычно задаются номером заказчика/клиента для отслеживания.
•
Для зданий — полный адрес, включая индекс/почтовый код.
•
Для почтовых корреспонденции — имя и адрес адресата и дата отправки.
Имеется много других примеров, но в данный момент вы должны понять, что мы подразумеваем, когда говорим об идентификаторах. Вспомнив реляционную модель, которую мы обсуждали в главе 3, каждый экземпляр отношения (или строка, если вы желаете) должен быть уникальным. Определение уникальных естественных ключей для данных — самый первый шаг в реализации проекта. Некоторые из идентификаторов, перечисленных выше, не могут гарантировать, что будут уникальными; например, в случае номеров компаний и деловых документов идентификаторами могли бы быть последовательные числа, которые не являются естественными ключами. Однако во многих случаях эти значения — разумные ключи: это означает, что в них заключена и другая важная информация. В большинстве случаев разумные ключи могут быть разбиты на части. В некоторых случаях, однако, данные, вероятно, ничего не будут вам означать. Возьмем следующий пример серийного номера изделия: XJV 10229392: •
X — тип изделия — телевизионная трубка;
Q
JV — подтип изделия — 32-дюймовая консоль;
•
102 — номер партии, означающий, что изделие было произведено в 102-ой партии;
•
293 — день года;
•
9 — последняя цифра года;
CJ
2 — цвет.
Разумные ключи имеют свою цель, но при логическом проектировании мы должны найти все части информации, которые в них входят.
Описательная информация Описательная информация — один из самых легких типов для определения. Прилагательные, обычно описывающие предметы, которые были предварительно идентифицированы как сущности, довольно распространенны и обычно указывают непосредственно на атрибут. В нашем примере мы имеем различные типы изделий: новые и подержанные.
... nftofawm подерфанние иЗдвлпя ... Они mafa/ce продают новые вещи Главное здесь заключается в том, что мы теперь идентифицировали возможный домен атрибута. В этом случае атрибут — "Тип изделия", а домен, вероятно, будет включать "Новый" и "Подержанный". Конечно, возможно, впоследствии это может быть преобразовано в два атрибута — "Является новым" и "Возраст изделия" или, может быть, единственный атрибут — "Год изготовления".
73
Глава 4 В следующем примере мы видим, что должны будем иметь атрибут, который определяет кредитные способности заказчика:
... как только SakaSiuk делает ЗакйЗ, если ему доверяют (нет никаких кредитных проблем)^ Как только будет выяснено, есть ли у заказчика проблемы с кредитом, мы определенно будем хотеть в этот момент задать вопрос, что нам нужно знать еще относительно того, как БД может облегчить процесс управления кредитоспособностью. С другой стороны, следующий пример:
... телефонные номера (факс и голосовой), адрес ..^ дает впечатление, что мы имеем сущность "Контакт", которая имеет атрибут "Номер телефона" с доменом, состоящим из значений "факс" и "голосовой". Здесь уместно вставить небольшое напоминание, что одним из главных различий между столбцом и атрибутом является то, что столбец является физическим представлением единственного элемента информации, размещенной в той же самой физической сущности или таблице, в то время как атрибут может быть физически размещен и не в той же самой таблице.
Указатели на расположение Указатели на расположение используются как способ разместить что-либо, начиная от физического ресурса и кончая классификацией, а также чтобы дифференцировать и размещать значения в таблице. Пример указателя на расположение приведен ниже:
(Все изделия отправляются со склада в центре Здесь мы имеем местоположение склада. Склад может иметь только один адрес, так что это определенный указатель на расположение. Обратите внимание, что из этого фрагмента фразы не ясно, имеются ли другие склады. Одно очевидно, что мы имеем по крайней мере один склад, который находится в центре города. В следующем примере мы имеем четыре очень типичных указателя на расположение:
... поместить информацию о контактах для кафдого ЗакаЛ1ика — телефонные номера (факс и голосовой), адрес и адреса электронной noimu. Большинство сущностей, которые связаны с организациями, будет иметь много атрибутов телефонных номеров, адресов и адресов электронной почты. В нашем примере мы имеем четыре атрибута для сущности "Контакт". Дальнейший обзор позволит лучше определить все фактические потребности, когда дело дойдет до этой информации, но это — довольно хороший перечень того, что важно для пользователя.
74
Сущности, атрибуты, отношения и бизнес-правила
Вы можете задаваться вопросом, почему мы думаем об адресе как единственном атрибуте, когда вы хорошо знаете, что "адрес" состоит из номера квартиры, улицы, города, штата, почтового индекса — нескольких атрибутов. Ответом будет то, что все они составляют адрес, что мы мыслим обо всех них как об единственном атрибуте, и этого нам достаточно. В это будет вложено больше смысла позже, когда мы начнем процесс структуризации, но пока будет достаточно представлять, что когда пользователь видит слово "адрес" в контексте нашего примера, он думает о понятии адреса, используемого для задания физического местоположения. Таким образом вы можете избежать любого обсуждения того, как адрес фактически сформирован, не говоря уже обо всех различных форматах адреса, с которыми мы, вероятно, должны будем иметь дело, когда займемся физической реализаций атрибута "Адрес" позже в этой книге. Последний атрибут, который мы рассмотрим, вытекает из следующего предложения:
ЩелесообраЗно исполъЗоватъ факс нлн электронную noimy для оршниЗауш подтвфусдений^ Атрибут "Факс или электронная почта" относится к механизму поставки, для того чтобы определить, что мы будем использовать для передачи подтверждения заказа изделия. Мы будем, конечно, хотеть иметь записи того, как и когда послали изделия заказчику. Следовательно, нам будет нужен атрибут, описывающий механизм поставки, в таблице подтверждения заказа изделия.
Связанная информация В некоторых случаях (хотя и не в нашем примере) мы будем иметь связанную информацию, которая потребуется в виде атрибутов для наших сущностей. Некоторые примеры этого следующие: •
Дополнительные материалы — пусть где-нибудь вы имеете сущность, которая описывает книгу или какой-нибудь другой тип средств информации (предположим, Amazon.com); вы, вероятно, захотите внести в список дополнительные ресурсы, которые пользователь может также посмотреть.
•
Контакты — мы уже рассматривали таблицу контакта, но контакт также представляет собой технически и атрибут сущности. Независимо от тогб, захотите вы или нет, чтобы контакт был отдельной сущностью на данном или более поздних этапах, окончательная БД будет выглядеть одинаково, потому что процесс нормализации (главы 6 и 7) гарантирует, что атрибуты, которые на самом деле являются сущностями, будут соответственно реструктурированы.
•
Web-сайты, FTP-сайты (FTP (File Transfer Protocol) — протокол передачи файлов) или другие различные Web-ресурсы — вы должны будете часто идентифицировать сущности "Web-сайт" или "URL-ресурс" (URL (Uniform Resource Locator) — унифицированный указатель информационного ресурса, фактически адрес в Интернете), которые определяются как сущности; такая информация могла бы быть определена и как атрибуты.
Список сущностей, атрибутов и доменов Следующая таблица показывает сущности совместно с описаниями и доменами столбцов. Атрибуты сущности помещены с отступом в пределах столбца сущностей. 75
Глава 4
Сущность
Описание
Контакт
Люди, выступающие в качестве заказчиков, и с которыми следует установить контакт
Номер факса
Номер телефона, чтобы посылать факсимильные сообщения заказчику
Любой подходящий номер телефона
Номер голосового телефона
Номер телефона для голосовой связи
Любой подходящий номер телефона
Адрес
Почтовый адрес контактного лица
Любой подходящий адрес
Адрес электронной почты
Адрес электронной почты контактного лица
Любой подходящий адрес электронной почты
Заказчик
Организация или конкретное лицо, которым продаются изделия, офисное оборудование и/или мебель
Номер заказчика
Ключевое значение, которое используется для идентификации заказчика
Неизвестный
Надежность кредита?
Сообщает нам, можно ли отправить заказчику изделия немедленно
True, False
Склад
Место, где размещаются товары для продажи Нахождение •
Определяет, какой склад имеется в виду, — только один может быть в этот момент, так как определено только одно значение домена
Поставки
События, когда проданные товары поставляются заказчику
Изделия
Товары для продажи
Тип изделия
76
Домен столбца
Определяет различные типы изделий для продажи
Офисное оборудование
Специальный тип изделия на продажу
Мебель
Специальный тип изделия на продажу
"В центре города"
" Подержанное ", "Новое"
Сущности, атрибуты, отношения и бизнес-правила
Сущность
Описание
Счет-фактура
Физический документ, который отправлен заказчику, чтобы запросить оплату за проданные изделия
Подтверждение размещения изделия
Электронный документ (возможно, это единственный формат), который позволяет сообщить заказчику, что заказ размещен
Тип механизма передачи
Определяет, как подтверждение заказа изделия передается заказчику
Журнал
Распечатка всех заказчиков
Бухгалтерский журнал
Фактическая запись контакта с заказчиком
Домен столбца
"Факс", "Электронная почта"
Замечание для физического моделирования: регистрировать любые изменения счетов-фактур Обратите внимание на использование термина "Любой подходящий". Возможности этих формулировок должны быть уменьшены до разумной величины. Многие БД, которые хранят телефонные номера и адреса, не могут справляться со всеми возможными форматами, используемыми в разных регионах мира, главным образом, потому что это не такая простая задача. На этой стадии мы определили список сущностей и атрибутов, которые нам удалось выделить. Заметьте, что при этом процессе мы все еще не начали добавлять что-нибудь к проекту. Интересно было бы также обратить внимание, что мы имеем документ почти в страницу, а всего-навсего проанализировали два маленьких параграфа текста. Когда вы это сделаете в реальном проекте, окончательный документ будет намного больше, и в документации будет, вероятно, весьма немного избыточности. Вы будете без сомнения определять нужные данные лучше, чем мы делали на предыдущих немногих страницах!
Отношения между таблицами Наиболее важные решения, которые вы будете принимать относительно структуры БД, будут обычно связаны с отношениями.
Отношения "один к п" В любом отношении "один к п" (то есть, "один к одному" или "один ко многим") таблица, которая является таблицей "один", рассматривается как предок, а "п" является потомком или потомками.
77
Глава 4
Отношение "один к п" часто используется при реализации, но редко встречается на ранних этапах проектирования БД. Причина этого заключается в том, что большинство естественных отношений, о которых сообщит вам пользователь, оказывается отношением "многие ко многим". Имеются два известных типа отношений "один к п", которые мы должны обсудить. Они на самом деле весьма просты, и опытный проектировщик БД выявит их немедленно.
Отношение "имеет" Основной специальный тип отношения — отношение "имеет". Оно названо так, потому что таблица-предок в отношении имеет одну или большее количество сущностей-потомков, используемых как атрибуты родителя. Фактически, отношение "имеет" — способ задания атрибута, который часто используется неоднократно. Некоторые примеры: •
адреса;
•
номера телефонов;
G
дети.
В разделе с нашим примером мы имели:
... поместить информацию о контактах для кяа/сдж ЗакаЗЫка ... В этом случае мы имеем сущность "Заказчик", которая имеет одного или большее количество контактов. Вы будете встречать этот тип отношения неоднократно; обратите также внимание, что это является основой для решения, получаемого при использовании четвертой нормальной формы, которую мы рассмотрим в главе 6. Другой пример отношения "имеет" приведен в следующем примере:
... для контактов, т. к. они могут вести фурнал контактов с ЗакаЗШкамг В этом случае партнер имеет несколько вхождений в журнал. С другой стороны невероятно, что вход журнала будет связан со многими партнерами.
Отношение "является" Другой специальный случай отношения "один к п" — отношения "является". Суть отношения "является" заключается в том, что сущность-потомок в отношении расширяет сущность-предка. Например, автомобили, грузовики, железнодорожные средства — это все типы транспортных средств, так что автомобиль является транспортным средством. Мощность такого отношения всегда "один к одному", поскольку сущность-потомок просто содержит более определенную информацию, которая определяет это расширенное отношение. Причина для этого вида отношений концептуальна. Может быть некоторая информация, которая является общей для каждой сущности-потомка (размещенная как атрибуты сущности-предка), а другая информация является специфичной для каждой конкретной сущности-потомка (размещенная как атрибуты сущности-потомка).
78
Сущности, атрибуты, отношения и бизнес-правила
В нашем примере, мы имеем следующую ситуацию:
... продают подффмпш иЗделнл, ofhaiuo офисное околдование, но mfo/ce и мебель, В этом примере мы имеем сущность "Изделие", которая является достаточно общей. Сущность "Изделие" вряд ли будет содержать много информации относительно того, что представляет собой офисное оборудование, или из чего сделан предмет мебели. Это приближается к концепции наследования в объектно-ориентированной БД. Обратите внимание: отношение "является" несколько похоже на подкласс в объектно-ориентированном программировании. В реляционных БД нет никакого понятия наподобие подкласса, хотя это может измениться в следующих SQL-спецификациях. Мы обсудим эту тему дальше, когда приступим к моделированию БД.
Отношения "многие ко многим" Отношение "многие ко многим" используется гораздо более часто, чем вы могли бы представить. Фактически, поскольку мы начинаем детализировать наш проект, то будем использовать его для большей части наших отношений. Однако на данной стадии будет распознано только очень немного отношений "многие ко многим". В нашем примере имеется одно такое очевидное отношение:
, 'которым они продают подфо/саннш нЗделия ... Если мы используем простое отношение "один ко многим", то были бы привязаны либо к одному заказчику, который приобретает разные изделия, или к тому, что каждое изделие может быть приобретено единственным заказчиком. С другой стороны, при таком отношении одно изделие может быть продано многим заказчикам, но каждый заказчик может приобрести только единственное изделие. Очевидно, что ни один из этих вариантов не реалистичен. Многие заказчики могут приобрести многие изделия, и многие изделия могут быть проданы многим заказчикам.
Список отношений Давайте посмотрим на документ, с которым мы работаем, еще раз, на сей раз удалив атрибуты, добавленные в предыдущем разделе, чтобы сэкономить место и сделать более ясным то, что мы сделали.
4
1868
7 9
Глава 4
Сущность
Описание
Контакты
Люди, выступающие в качестве заказчиков, и с которыми следует установить контакт
Имеют бухгалтерский журнал Заказчики
Для определения, когда они контактировали по адресу заказчика Организация или конкретное лицо, которым продаются изделия, офисное оборудование и/или мебель
Приобретают изделия
Заказчики приобретают изделия у клиентов
Имеют контакты
Разместить имена, адреса, номера телефонов и т. д. людей по адресу заказчика
Склад
Место, где размещаются товары для продажи Размещают товары для отправки
Товары размещаются на складе для продажи заказчикам
Поставки
События, когда проданные товары поставляются заказчику
Изделия
Товары для продажи
Могут быть проданы заказчикам Офисное оборудование Является товаром
Мебель
Заказчики могут купить любые из продаваемых товаров Тип товара Офисное оборудование просто является расширением понятия изделия, возможно несущим большее количество информации Тип товара
Является товаром
Мебель просто является расширением понятия изделия, возможно несущим большее количество информации
Счет-фактура
Физический документ, который отправлен заказчику, чтобы запросить оплату за проданные изделия
Подтверждение размещения изделия
Электронный документ (возможно, это единственный формат), который позволяет сообщить заказчику, что заказ размещен
Журнал
Распечатка всех контактов
Бухгалтерский журнал
Фактическая запись контакта с заказчиком
Замечание для физического моделирования: регистрировать любые изменения счетов-фактур
80
Сущности, атрибуты, отношения и бизнес-правила
Определение бизнес-правил Бизнес-правила могут быть определены как совокупность операторов, которые управляют и формируют деловое поведение. В зависимости от методологии организации данных, эти правила могут быть в форме помеченных списков, простых текстовых диаграмм или других форматов. Для наших целей мы прежде всего будем рассматривать правила, которые являются в основном теми же самыми, что и утверждения, которые мы обсуждали в предыдущей главе, и охватим любые критерии, которые точно не укладываются в нашу конструкцию таблицы, столбца и домена. Реализация не подразумевает существования бизнес-правил на данной стадии процесса проектирования. Все, что мы хотим сделать — собрать бизнес-правила, связанные с данными, для последующего рассмотрения. При определении бизнес-правил вы можете получить некоторое дублирование правил и доменов для атрибутов, но в данный момент это не является проблемой. Важно задокументировать столько правил, сколько возможно, так как пропущенные бизнес-правила будут иметь для вас большие последствия, нежели пропущенные атрибуты, отношения или даже таблицы. Новые таблицы и атрибуты будут часто найдены, когда вы будете реализовывать систему, обычно по необходимости, но выяснение, что нужны новые бизнес-правила на последней стадии разработки, может разрушить весь проект, вызывая дорогое переосмысление или опрометчивое "залатывание дыр" в них. Разработка бизнес-правил — не очень трудный процесс, но это — потраченное время и довольно утомительно. В отличие от сущностей, атрибутов и отношений нет никакого прямого определенного алгоритма для выявления всех бизнес-правил. Однако моя общая практика, когда я рассматривал бизнес-правила, дает вывод, что нужно читать документы строка за строкой, отыскивая предложения, включающие слова, наподобие "как только ... происходит", "... надлежит ...", "... должен ...", "... будет ...", и т. д. Но документы не всегда включают каждое бизнес-правило. Вы могли бы просмотреть сотню или тысячу счетов, и не увидеть ни одного случая, где клиент отдает в кредит деньги, но это не означает, что это никогда не бывает. Во многих случаях бизнес-правила могут быть добыты из двух мест: • Q
старый код; клиенты.
Получение бизнес-правил с помощью любого из этих источников никогда не является приятным делом. Не каждый программист пишет удобочитаемый код. Если вам повезет, ваш деловой аналитик возьмет интервью у клиентов относительно этих правил. В нашем примере "фрагмента записей о встрече" есть несколько бизнес-правил, которые мы можем определить. Например, мы уже обсудили потребность в атрибуте, представляющем собой номер заказчика, но не могли определить домен для номера заказчика, поэтому возьмем следующее предложение:
Они идентифицируют SakaSlukoS номером, формируемым существующей системой. ф> и получим бизнес-правило, наподобие следующего: Номера заказчиков формируются на основе алгоритма работы существующей системы. (Проверить значения их текущих номеров.)
81
Глава 4 Другое предложение в нашем примере дает еще одно возможное бизнес-правило: пеЫтаютсп еженедельно Для него мы можем получить следующее правило:
Счета могут быть напечатаны только в определенный день недели. Однако действительно ли это фактическое бизнес-правило? Маловероятно, что клиент хотел бы связать себя системой, в которой можно печатать счета только в некоторый день недели. Однако вы могли бы заявить, что это точно то, что клиент указал в первоначальной документации. Это, вероятно, будет одной из ситуаций, где английский язык (и русский тоже. Прим. перев.) недостаточно технически точен, чтобы определить важные детали в такой короткой форме. Вышеупомянутое извлечение из примера документации могло бы подразумевать: клиент ожидает, что будет столько документов, которые достаточно печатать еженедельно, или что документы удобно печатать еженедельно. Мы должны будем выяснить, что предполагалось в этом утверждении, когда созданный нами документ будет рассматриваться клиентом. Последнее бизнес-правило мы можем получить из следующей части документа: ... но поставки делаются немедленно^ как только ЗйкаЗШ деллем ЗакаЗ, если ему довфлют (нет никаких кредитных пробл Мы имели с ней дело раньше, когда рассматривали атрибуты, но она заключает также очень важное бизнес-правило:
Поставки не должны выполняться немедленно, если заказчики некредитоспособны.
Конечно, мы должны определить: • •
Что здесь означает термин "немедленно"? Как мы оцениваем кредитоспособность клиента?
•
Когда мы в конце концов можем отправить изделия заказчикам, как будут осуществляться платежи и как будут сделаны поставки?
Эти проблемы также должны быть выяснены при дальнейших консультациях с клиентами. Данный раздел, посвященный бизнес-правилам, вряд ли полон. Легко привести примеры таблиц, атрибутов и отношений, но бизнес-правила имеют гораздо более неясную природу. Чтобы сделать наш пример таким, чтобы можно было получить большую часть 82
Сущности, атрибуты, отношения и бизнес-правила
бизнес-правил, мы должны будем существенно увеличить количество информации в документации примера, что сделает идентификацию сущностей и атрибутов излишне сложной. Просто очень важно отметить каждое возможное место, где может быть получено то или иное бизнес-правило, которое определяет, как используются данные и которое не должно конкретно управлять доменами столбцов. Установление бизнес-правил — потенциально протяженный процесс, который нельзя обойти или сократить в реальном проекте. Отсутствие или неполные бизнес-правила вызовут большие проблемы позже.
Идентификация основных процессов Процесс обычно представляет собой логически связанную последовательность шагов, предпринимаемых программой, которая использует выделенные нами данные, чтобы что-то с ними сделать. Как пример, рассмотрим процесс получения водительских прав (по крайней мере, здесь, в штате Теннесси): Q
Заполнить разрешающие формы ученика.
•
Получить разрешение на обучение.
•
Практика.
•
Заполнить форму водительских прав.
•
Пройти экзамен на вождение.
•
Получить фотографию.
•
Получить права.
Каждый из этих шагов должен быть завершен до перехода к следующему. Процессы могут включать все или не все шаги, перечисленные на логической стадии, и конечно будут иметь бизнес-правила, управляющие ими. Процесс получения водительских прав в штате Теннесси требует, чтобы вам было пятнадцать лет для получения разрешения на обучение, было шестнадцать лет для получения водительских прав, чтобы вы сдали экзамен, практика должна быть с лицензированным водителем и т. д. Если вы являетесь деловым аналитиком, помогающим проектировать систему получения водительских прав, то были бы должны в какой-то момент задокументировать этот процесс. Идентификация процессов очень важна для задачи моделирования данных. Многие процедуры в СУБД требуют работы с данными, а процессы являются решающими в этих задачах. Каждый процесс будет обычно переводиться в один или большее количество запросов или хранимых процедур, которые могут потребовать большего количества данных, чем мы определили. В нашем случае мы имеем несколько примеров таких процессов:
Ммеютм ЗакаМнки, кппафмм они продают подфэ/стнме нШякя Эта запись говорит нам, что есть средства создания и работы с заказами изделий, купленных клиентом. Внешне это не похоже на кардинальное открытие, но, вероятно, будут иметься записи в нескольких таблицах, чтобы облегчить надлежащее создание заказа. Мы еще не определили таблицу заказа или заказываемых изделий, или даже цены изделий, хотя нам, несомненно, нужно это сделать. 83
Глава 4
Clem-(f)afawyjm пеАатютоя ... ф> Мы сделали запись о счетах, но этот процесс "печатания счета" может потребовать дополнительные атрибуты для определения, что счет-фактура был напечатан или перепечатан, для кого он был напечатан и может ли он быть перепечатан. Управление документами — очень важная часть многих процессов, которая может помочь в модернизации бумажной системы. Обратите внимание, что печать счета-фактуры может походить на довольно глупую процедуру — нажимай на кнопку на экране, и принтер выдаст бумагу. Все, что мы должны сделать, — выбрать некоторые данные из таблицы, не так уж и много? Однако когда мы печатаем документ, нам, вероятно, придется делать запись факта, что документ был напечатан, кто напечатал его, и каково использование документа. Нам бы также могло потребоваться указать, что документы напечатаны в течение процесса, который включает операции включения и суммирования продаваемых единиц в счете-фактуре. Здесь наиболее важный момент заключается в том, что мы не можем делать любые предположения. Другие основные процессы, которые были выявлены, следующие: • • • Q •
Организация поставок — из "... поставки делаются немедленно, как только заказчик делает заказ, если ему доверяют ...". Изменение счета-фактуры — из "Любые изменения счета-фактуры должны быть задокументированы." Поставки изделий — из "Все изделия отправляются со склада в центре города." Организация подтверждения заказа изделия — из "... факс или электронную почту для организации подтверждений." Контакт с заказчиком — из "Желательно также иметь свободную область для контактов, т. к. они могут вести журнал контактов с заказчиками."
Каждый из этих процессов определяет единицу работы, которую нужно выполнить на стадии реализации проекта БД.
Где мы сейчас? Должно быть совершенно очевидно, что не все сущности, атрибуты, отношения, бизнес-правила и процессы даже для нашего простого примера были идентифицированы на данный момент. В этом разделе мы кратко рассмотрим шаги, которые нужно выполнить для завершения задачи формирования набора рабочей документации. Каждый из следующих шагов может показаться довольно очевидным, но их можно легко пропустить или проскочить в реальной жизненной ситуации. Реальная проблема состоит в том, что если вы не выполняете весь процесс разработки, то можете пропустить огромное количество информации, часть которой может быть жизненно важной для успешного проектирования. Часто, когда архитектор достигает этого конкретного момента, он/она будет пытаться перейти к реализации проекта. Однако имеются, по крайней мере, еще три шага, которые мы должны выполнить, прежде чем сможем начать следующую стадию проектирования.
84
•
Определение необходимых дополнительных данных.
• •
Рассмотрение проекта с клиентом. Повторение процесса, пока и мы не будем удовлетворены, и клиент не будет удовлетворен и не скажет, что можно остановиться на том, что мы разработали.
Сущности, атрибуты, отношения и бизнес-правила Обратите внимание, что эти процедуры должны выполняться в течение всего процесса проектирования, а не только в той части, где определяются данные.
Определение необходимых дополнительных данных Вплоть до этого момента мы старались не расширять информацию, которая была получена на стадии ее сбора. Целью было получение основы нашей документации, так что мы могли довольствоваться грудами документов, которые первоначально собрали. Добавление сюда наших новых мыслей до выяснения того, что было в исходной документации, может запутать как клиента, так и нас самих. Однако на данном этапе нам нужно изменить стратегию и начинать добавлять атрибуты, которые мы считаем нужными. Обычно имеется довольно большой набор очевидных атрибутов и, в меньшей степени, бизнес-правил, которые не были определены ни одним из пользователей или на основе начального анализа. Для каждой из идей и элементов, выделенных к настоящему моменту, нам нужно тщательно рассмотреть и определить дополнительные атрибуты, которые, как мы думаем, будут необходимы. Например, возьмем сущность "Контакт", которую мы определили ранее: Сущность
Описание
Контакт
Люди, выступающие в качестве заказчиков, и с которыми следует установить контакт
Атрибуты Номер факса
Номер телефона, чтобы посылать факсимильные сообщения заказчику
Любой подходящий номер телефона
Номер голосового телефона
Номер телефона для голосовой связи
Любой подходящий номер телефона
Адрес
Почтовый адрес контактного лица
Любой подходящий адрес
Адрес электронной почты
Адрес электронной почты контактного лица
Любой подходящий адрес электронной почты
Отношения Имеют бухгалтерский журнал
Для определения, когда они контактировали по адресу заказчика
Мы вероятно хотели бы отметить, что контактное лицо будет нуждаться в следующих дополнительных атрибутах: •
Имя — полное имя заказчика, вероятно, наиболее важный из всех атрибутов.
•
Имя супруга (супруги) — имя мужа или жены заказчика. Этот вид информации бесценен при создании контактов, если вы хотите персонифицировать ваше сообщение или спросить относительно семейства заказчика. 85
Глава 4 • •
Дети. Дата рождения — если день рождения человека известен, можно послать к этой дате открытку.
Имеется, конечно, большее количество атрибутов, которые мы могли бы добавить к сущности "Контакт", но этот набор должен сделать сущность достаточно ясной. Могут также иметься дополнительные таблицы, бизнес-правила и т. д., чтобы рекомендовать клиенту на этой стадии проектирования задокументировать их и добавить к вашим спискам. Одна из главных вещей — удостовериться, что ваши новые элементы данных отличаются от тех, которые уже согласованы с клиентом на предыдущих консультациях, и от предварительной документации.
Повторное рассмотрение с клиентом Как только вы закончили собирать эту первую подборку документов, пришло время встречаться с клиентом. Важно показать ему все, что у вас есть, и ничего не пропустить. Совершенно необходимо сделать так, чтобы клиент рассмотрел каждую часть этого документа и понял то решение, которое вы начинаете разрабатывать для него. Конечно, клиент может иметь очень маленький или никакой опыт в вопросах проектирования БД и может требовать помощи и объяснений от вас, чтобы лучше понять ваш проект. Стоило бы даже сделать два документа, один для клиента в терминах обывателя и другой внутренний, более детальный для использования вашей командой проектировщиков. Заслуживает внимания разработка некоторой формы документа отказа от претензий, который клиент подпишет до того, как вы продолжите свою работу. Это могло бы быть хорошим юридически обязательным документом.
Повторяйте ваши действия, пока не получите одобрение заказчика Конечно, маловероятно, что клиент немедленно согласится со всем, что вы ему говорите, даже если вы — величайший архитектор данных в мире. Обычно требуется несколько попыток, чтобы получить его согласие, и каждое повторение должно продвигать вас и клиента ближе к цели. Поскольку вы выполняете все больше повторений проекта, становится все более и более важным удостовериться, что на основании имеющейся у вас информации клиент каждый раз отказывается от своих претензий; вы сможете указать на эти документы, когда клиент изменит свое мнение позже. Плохо, когда вы не выполняете соответствующей работы по управлению повторным рассмотрением и процессом документирования. Я занимался консультированием проектов, когда проект был хорошо разработан и согласован, но документация о том, что было согласовано, была сделана не слишком хорошо (уйма подтверждений на высоком уровне, чтобы сэкономить деньги!). Когда прошло время, и много тысяч долларов было потрачено, клиент пересмотрел документ соглашения, и стало очевидно, что мы вообще о многом не договорились. Само собой разумеется, что работа была выполнена не слишком хорошо.
86
Сущности, атрибуты, отношения и бизнес-правила
Учебный пример книги Перейдем теперь к нашему реальному примеру проектирования БД. Вернувшись к учебному примеру, начатому в главе 2, мы должны заняться разработкой сущностей на основе принципов, уже представленных в этой главе.
Интервью клиента Всегда лучше начать с записей интервью с клиентом. Сначала мы просматриваем их и отмечаем все пункты, которые собираемся представить сущностями. В нашем примере мы будем помечать полужирным шрифтом эти пункты.
З&сифеЫ с Сэмом Смитом, бухштфскм дело, 2Л ноября 2006\ -W ia-сов утра, Ъольшой, конференц-Зал посетители: Луш Юэбидсщ архитектор данных; Сэм Смит,, менедфер бухгалтерского укта; Юфо Юфонс, экономист
91ервая встрек МспольЗуемые дополнительные документы: регистр банка, типовой iek, типовой лицевой clem, a такз/св электронный формат лицевого clem. $ настоящее время испольЗуется бумафный регистр iekoS (check register), а такфе основной вм/тренний регистр, Рассматривали воЗмоо/сность использования подготовлентлх материалов, но не нашян ни одного, который бы обетеАивал все, imo xoiem 3aka3(uk. 94 у большинства предлофенных путей слиткам много особенностей. 91р1сно иметь многопользовательские воЗмофности (ввод и просмотр). Распределение iepe3 интранет. Щоуесс: Муфио иметь последнюю информацию относительно ciema (account). (В настоящее время баланс ciema проводится один раЗ в месяц. МслолъЗуется omiem (statement) иЗ банка (hank). Юля полфеиил баланса испольЗуется по крайней мере полный день. Хотелось бы mлyiamь баланс по demy ефенеделъно, испольЗуя данные иЗ Мнтернетл. Мнтересует только ведение скмов, но не сберефения, акции, обязательства, и т. д. Юля этого уфе имеется система. Щцдет ревизоваться ефегодно главным аудитором. Однако контрольного плана нем. 'Когда Сэм Закончил, был Задан вопрос об отшфиванми продавцов: Ъыло бы хорошо иметь информацию о полу1ателе платефа (payee) для большинствавыписываемых нами iekoe. Шакфе относительно категориЗации iekoS (check) Сэм скаЗал, imo это было бы, хорошо,-но не яёляется необходимым в данный момент времени.
87
Глава 4 Мы нашли следующий исходный ряд сущностей, с которыми будем иметь дело:
88
Сущность
Описание
Check (чек)
Бумажный документ, который используется, чтобы передать фонды кому-нибудь
Account (счет)
Банковские отношения, установленные, чтобы обеспечить финансовые сделки
Check register (регистр чеков)
Место, где зарегистрировано использование счета
Bank (банк)
Организация, которая имеет текущий счет, вместо которого выписываются чеки
Statement (отчет)
Документ (бумажный или электронный), который поступает от банка раз в календарный месяц и который сообщает нам соображения банка о том, что мы потратили
Payee (получатель платежа)
Личность или компания, которой мы посылаем чеки
Сущности, атрибуты, отношения и бизнес-правила Следующим шагом мы просматриваем документ и ищем атрибуты (которые помечаем полужирным подчеркнутым шрифтом) и отношения, которые мы выделяем серым фоном.
3dcm,peia с Сэмом Смитом, бухгалтерское дело, 24 ноября 2000,
Ю ксов утра, большой
конферепц-Зал (Посетители: Луис Юэвидсон, архитектор данных; Сэм Смит, менедфер бухмлтерского yiema; Юфо Юфонс, экономист Первая встреЛа МспольЗуемые дополнительным документы,: регистр банка-, типовой iek, типовой лицевой dem, а такфе электронный формат лицевого dema. 3d настоящее время испольЗуется бумафный
(paper)
регистр lekoS, а такфе основной
внутренний регистр. Рассматривали воЗмофность использования подготовленных материалов, но не нашли ни одного, который бы обеспеАивал все, imo xoiem ЗакаЗЫк. My большинства предлофенных путей слишком много особенностей. HipjcHo иметь мнтопольЗовательские воЗмофности (ввод и просмотр). Распределение iepe3 интранет. Процесс: 'Нуфно иметь последнюю информацию относительно dema
(account).
3d настоящее время баланс dema проводимся один раЗ в месяц. МспольЗуется omiem и 3 банка. Юля полукнт баланса испольЗуется по крайней мере полный день. Хотелось бы полукть бала-ис по demy ео/сенеделъно (balanced weekly), испольЗуя данные нЗ с/
с/ — '
*——
J /
с/
Мнтернета (internet data). Мнтересует только ведение demoe, но не сберефения, акции, обязательства, и т. д. Юля этого уфе имеется система,. Ъудет ревиЗоват,ься ефегодно главным аудитором. Однако контрольного плана нет. Когда Сэм Закткщ
был Задан вопрос об отслефивании продавцов:
Ъьию бы хорошо иметь информацию о иолуЫтеле платефа для большинства выписываемых нами hhiC> . Шакфе относительно категорпЗашп
(categorizing)
tekoS Сэм скаЗал, imo это било бы
хорошо, но не .является необходимым в данный момент времени.
В настоящее время фактически нет полного набора информации, который нужно подобрать из этих заметок. Вы можете заметить, что интервью с клиентом раскрывает очень небольшую информацию; в этом случае мы можем захотеть понять, почему мы не получили информацию, которую искали. Говорили не с теми людьми в организации, или задавали неправильные вопросы? Неприятно отвечать на вопрос "Почему я потерпел неудачу? " — но это сделает вас более хорошим архитектором данных в будущем.
89
Глава 4
Документы Наша документация учебного примера справедливо хромая (чтобы пример был коротким и приятным), но, к счастью, мы имеем несколько документов, которые прольют немного света на наше решение. Мы сейчас рассмотрим эти документы и отметим атрибуты (и возможно, сущности), которые там найдем. Первый документ, который мы рассмотрим — типовой чек. Этот вид документа — мечта проектировщика. Так как мы уже определили сущность Check (чек), каждое из полей в этом документе, вероятно, будет атрибутом. Check Number June 12, 2000
13403 WROX Subscriptions
Magazine Subscription Memo
Amount
Здесь CheckNumber — номер чека; Payee — получатель платежа; Date Written — дата заполнения; Memo — служебная запись; Amount — сумма; Signature — подпись. В нашем чеке можно отметить два момента: • •
Термин Amount (сумма) определен на чеке дважды, обеспечивая меру избыточности и защиты от мошенничества, которые несущественны на этом уровне проектирования. Payee (получатель платежа) уже был определен как отношение типа "имеет", который, в сущности, является атрибутом таблицы, так что мы не должны его снова определять как атрибут.
Следующее, что мы рассмотрим, — типовой регистр чеков. Это — просто запись каждого чека, как только он будет введен.
Numb ег
Date
Description
iCategcry
12390
12/15/00
Pizia Hut
Employee Appreciation
12391
112/15/00
Allied Mortgage
Building payment
12392
12/16/00
TN Electric
Utilities
12393
12/16/00
Deposit
N/A
Amount
Balance
шшт шшш
| Accountflunnino Total
Здесь Number — номер; Date — дата; Description — описание; Category — категория; Amount — сумма; Balance — баланс; Account — счет; Running Total — текущий итог.
Сущности, атрибуты, отношения и бизнес-правила Заметим, что номера, даты, описания (получатели платежей), категории и суммы уже получены непосредственно из чеков, но баланс (Balance) — новый параметр. Атрибут Balance (баланс) не соответствует в полной мере сущности Check (чек), но имеет смысл для сущности Account (счет). Следовательно, мы добавим атрибут Balance сущности Account. Затем мы должны рассмотреть отчет банка. Отметим все атрибуты так же, как мы делали в чеке. Отчет банка обеспечит атрибуты для нескольких сущностей. BankBank . Name iI
Bank of the I Account Statement Account Statement
Account. Account Number
Account number I
Previous Balance Current Balance Total Debits Total Credits
Last Balanced: Statement Date:
11/5/00 12/5/00
11/5/00 11/6/00 11/8/00 11/28/00 ... строки удален
. строки удален
12/01/00 12/02/00 12/03/00
Checks: 12200 строки удалень
... строки удален 12204*
'Означает разрыв в последовательности чеков
Statement. Balancing Items (the statement contains copies of all the items the register should have)
Statement. Pre/ious Balance Current Balance Total Credi Previous Balance Date Statement Date
Здесь Bank — банк; Bank Name — название банка; Account — счет; Account Statement — отчет no счету; Account Number — номер счета; Previous Balance — предыдущий баланс; Current Balance — текущий баланс; Total Debits — общий дебет; Total Credits — общий кредит; Last Balanced — дата последнего баланса; Statement Date — дата отчета; Check — чек; Deposit — депозит; Direct Withdrawal — прямое изъятие; Balancing Items — элементы баланса; Previous Balance Date — дата предыдущего баланса. 91
Глава 4 Отчет банка дал нам несколько атрибутов для сущностей — Bank (банк), Account (счет) и Statement (отчет). Здесь мы определяем две новых сущности: Q
Deposit (депозит) — используется для добавления денег к счету;
•
Direct Withdrawal (прямое изъятие) — деньги, изъятые со счета без заполнения бумажного чека.
Снова мы имеем дату чека, номер чека, сумму чека и текущий итог (все вычеркнуто по причинам секретности и безопасности, хотя всегда хорошей идеей является изображение вида некоторых документов, содержащих конфиденциальную информацию, с помощью отображения диапазона цифр и их формата — например, номера чеков дополнить ведущими нулями). Мы также обращаем внимание, что сущности Deposit (депозит) и Direct Withdrawal (прямое изъятие) имеют, очевидно, одни и те же атрибуты, и они — все в той же самой таблице с текущим итогом. Так что сущности Deposit и Direct Withdrawal будут иметь атрибуты Data (дата), Number (номер) и Amount (сумма). Текущий итог снова является общим для счета. Заключительная часть документации, которую мы должны рассмотреть — электронный формат регистра банка. Электронная версия более редкая, чем бумажная версия. В электронной версии может быть и больше информации, но это — все, что нам в настоящее время дают. Она также может быть единственной, что нам дают, потому что это действительно все, что нам нужно для балансирования счета.
Column
Data type
Required
Transaction Date
Date Only
Yes
Transaction Number
String(20)
Yes
Description
String(lOO)
Yes
Item Amount
Money
Yes
Transaction. Transaction Date Transaction Number Description Item Account
Здесь Column — столбец; Data type — тип данных; Required — требуется; Transaction— сделка; Transaction Date — дата сделки; Transaction Number — номер сделки; Description — описание; Item Amount — сумма; Date Only — только дата; String (N) — строка из N символов; Money — денежный тип. В то время как здесь нет никаких новых атрибутов для уже имеющихся сущностей, имеется упоминание о новой сущности — Transaction (сделка). Интересно, что банк использует эту сущность с соответствующими атрибутами, чтобы сообщить о значениях некоторых других сущностей. Все наводит меня на мысль, что, вероятно, имеется единый объект, который одновременно представляет и чеки, и депозиты, и прямые изъятия. Это указывает на то, что мы будем иметь сущность Transaction с отношениями "является" к сущностям Deposit, Direct Withdrawal и Check. 92
Сущности, атрибуты, отношения и бизнес-правила Проработав все наши документы, мы должны закончить наш документ сущностей. Здесь мы отметим все дополнительные атрибуты, которые добавим в самый левый столбец.
Перечень объектов банковского счета Вот этот набросок документа, который мы формируем на основе нашего первоначального анализа: Сущность
Описание
Домен
Transaction (сделка)
Логическая операция, которая добавляет или изымает деньги из нашего счета
Атрибуты Transaction Date (дата сделки)
Дата, когда деньги добавлены или удалены со счета
Подходящие даты
Transaction Number (номер сделки)
Уникальный номер, используемый для идентификации сделки
Строка
Description (описание)
Строка описания сделки
Строка
Item Amount (сумма)
Количество денег, добавленных или удаленных со счета
Вещественное число с двумя десятичными цифрами
Check (чек)
Бумажный документ, который используется, чтобы передать фонды кому-нибудь
Атрибуты Check Usage Type (тип использования чека)
Определяет категорию использования чека
Неизвестный
Check Type (тип чека)
Определяет категорию чека
Неизвестный
Check Number (номер чека)
Номер для однозначного определения чека
Целое число
Date Written (дата заполнения)
Дата, когда чек был написан
Подходящие даты, возможно, не будущие даты
Amount (сумма)
Количество денег, снятых со счета и переданных получателю платежа
Вещественное положительное число с двумя десятичными цифрами
Продолжение таблицы, на следующей странице 93
Глава 4
Описание
Домен
Memo (служебная запись)
Краткое описание, для чего используется чек
Текст
Signature (подпись)
Подпись лица, выписавшего чек — мы, возможно, просто храним текстовую версию имени подписавшего
Текст
Сущность
Отношения Имеет получателя платежа
Используется для идентификации получателя платежа, которому послан чек
Является сделкой
Определяет, что чек является подклассом сделок
Payee (получатель платежа)
Личность или компания, которой мы посылаем чеки
Атрибуты Новый
Name (имя)
Имя любого, кому заплачены деньги
Строка
Новый
Address (адрес)
Адрес получателя платежа
Любой подходящий адрес
Новый
Phone Number (номер телефона)
Номер телефона получателя платежа
Любой подходящий номер телефона
Deposit (депозит)
Используется для добавления денег на счет
Атрибуты Date Written (дата написания)
Дата написания чека
Подходящие даты, возможно, не будущие даты
Deposit Number (номер депозита)
Номер для однозначного определения депозита
Целое число
Amount (сумма)
Количество денег, добавляемых к счету
Вещественное положительное число с двумя десятичными цифрами
Description (описание)
Описание из электронного источника
Строка
Отношения Является сделкой
94
Определяет, что депозит является подклассом сделок
Сущности, атрибуты, отношения и бизнес-правила
Сущность
Описание
Direct Withdrawal (прямое изъятие)
Используется для изъятия денег ее) счета без какого-либо бумажного документа
Домен
Атрибуты Date Withdrawn (дата изъятия)
Дата, когда был написан чек
Подходящие даты, возможно, не будущие даты
Number (номер)
Номер для однозначного определения чека
Целое число
Amount (сумма)
Количество денег, снятых со счета
Вещественное положительное число с двумя десятичными цифрами
Description (описание)
Описание из электронного источника
Строка
Отношения Является сделкой
Account (счет)
Определяет, что прямое изъятие является подклассом сделок Банковские отношения, установленные для организации финансовых сделок — мы, вероятно, будем иметь дело со многими банками
Атрибуты Account Number (номер счета)
Число, однозначно определяющее счет
Неизвестный, устанавливается, банком, возможно, строка
Balance Date (дата баланса)
Дата и время, когда произведен баланс счета
Подходящие даты
Running Total (текущий итог)
Текущее количество денег на счете
Вещественное положительное число с двумя десятичными числами
Отношения Проведение баланса, используя отчет
Определяет, получаются ли данные из баланса счета
Использует сделки
Для счета используются сделки, чтобы добавлять или изымать деньги Продолжение таблицы на следующей странице 95
Глава 4
Сущность
Описание
Домен
Check Register (регистр чеков)
Место, где фиксируется использование счета
Атрибуты Register Type
Bank (банк)
Описывает тип регистра, который мы используем для хранения записей счетов
"Документ"
Организация, которая использует чековые счета и которая выписывает чеки — мы, вероятно, будем иметь дело со многими банками
Атрибуты Bank Name (название банка)
Название банка, с которым мы имеем дело
Строка
Отношения Рассылает отчеты
Банк рассылает отчеты, чтобы мы могли сбалансировать счет
Имеет счета
Идентифицирует счета банка
Statement (отчет)
Документ (бумажный или электронный), который поступает из банка раз в календарный месяц и сообщает нам все, что думает банк о наших тратах
Атрибуты
96
Statement Type (тип отчета)
Определяет тип отчета, полученного из банка
"Интернет-данные", "Документ"
Previous Balance (предыдущий баланс)
Определяет, какой предполагался баланс счета после последнего баланса отчета
Вещественное число с двумя десятичными цифрами
Previous Balance Date (дата предыдущего баланса)
Определяет дату последнего баланса счета
Дата
Сущности, атрибуты, отношения и бизнес-правила
Описание
Домен
Current Balance (текущий баланс)
Определяет баланс счета после того, как все элементы отчета были сформированы
Вещественное число с двумя десятичными цифрами
Statement Date (дата отчета)
Определяет дату, когда сформирован отчет — это, вероятно, тот день, когда отчет создан банком
Total Credits (общий кредит)
Сумма всех элементов, которые добавляют деньги на счет в период, охватываемый отчетом
Вещественное положительное число с двумя десятичными цифрами
Total Debits (общий дебет)
Сумма всех элементов, которые изымают деньги со счета в период, охватываемый отчетом
Вещественное отрицательное число с двумя десятичными цифрами
Balancing Items (элементы баланса)
Все операции, которые выполнил банк, и о которых он теперь оповещает нас
Массив сделок (чеков, депозитов, прямых изъятий)
Сущность
Вы можете, вероятно, заметить, что мы достигаем такого состояния, когда даже для такого маленького примера этот формат документации становится неуправляемым. В следующей главе мы обсудим моделирование данных, которое обеспечивает метод сбора и представления этих данных, являющийся намного более легким для работы. Это не означает, что данный вид описания не имеет никакого смысла. По крайней мере, вы должны сформировать этот документ, чтобы представить клиентам. По этой причине мы выполнили это упражнение, а не перешли непосредственно к моделированию данных. Документ, который мы сформировали, дает хорошее описание того, что мы хотим реализовать либо техническим, либо нетехническим способом.
97
Глава 4
Бизнес-правила и процессы Для рассмотрения бизнес-правил и процессов мы вернемся к записям нашего интервью с клиентом. Процессы помечены серым фоном, а бизнес-правила обведены рамкой!
Здстрек с Сэмом Смитом, бухгалтерское дело, 24 ноября 2000,
JO iaco6 утра, Ъолыиой
конффтц-Зал посетителя: Луис Юэвидсон, фштеЬаф
данных; Сэм Смит, менедо/сер бух/гашерского
yiema; Юфо Юфонс, экономист 'Первая (kmpela 14спольЗуемме дополнительные документы: регистр банка, типовой 1вк, шиповой лицевой clem, а тлкфе электронный формат лицевою
фш.
(В настоящее время используется бумафный
(f>af>er) fjeiucmft кков, а тлкфе основной
внутренний fretucmf). (Рассматривали воЗмофносмъ использования подготовленных материалов, но не нашли ни одного, komojmu бы обвсшАивал, все, im,o xoiem SakaSiuk. My большинства п^едлофенных путей слишком много особенностей. уНуо/сно иметь многопользовательские воЗмофносмщ (ввод и nftocMomj?). Распределение iefie инт^анем, Щ>оцесс: 9~1уфно иметь последнюю информацию относительно clema
(account).
1д настоящее время баланс ШетЛ (halanmifj account) проводится одни j?a3 в месяц. МсполъЗуетсп omiem иЗ банка. Юля полу1ения баланса, испольЗуется по крайней мере полный день] Хотелось бы, полукть баланс по demy ефенеделыш (balanced weekly), испольЗуя данные иЗ ^Интернета (internet data). Мнтересует только ведшим сктов, но не сбережения, акции, обязательства, и т. д. Юля этого уу/се имеется система. Шудет ревйЗоватьоя (audited) ежегодно главным аудитором. Юдилко контрольного плаял нет. %огда Сэм Закон1ил, был Задан Запрос об отсле/фивании продавцов: Ъыло бы хорошо иметь информацию о полугателе платефа для большинства выписываемых нами 1ек.ов. 9JIah/ce относительно категоризации
(categorizing)
iekoS Сэм скйЗал,, 1то это было бы,
хорошо, но не является необходимым в данный момент времени.
98
Сущности, атрибуты, отношения и бизнес-правила
Эта документация дает нам следующее Бизнес-правило
Описание
Должны быть многопользовательские возможности
Возможно, что одновременно потребуется вводить сделки более чем одному человеку
Составлять баланс по счету нужно еженедельно
Нам нужно иметь возможность балансировать счет более часто, чем обычно один раз в месяц; возможно, чтобы уменьшить время между моментами баланса, нужно будет находить пропущенные сделки более часто
Будет ревизоваться ежегодно
Некая внешняя фирма будет проверять наши числа ежегодно, чтобы удостовериться, что все находится в хорошей форме; вероятно, мы сформируем некоторую документацию, чтобы облегчить этот процесс
Новое
Пользователь должен получить предупреждение, если попытается забрать слишком много денег
Пока это только идея, что пользователь может пожелать иметь помощь, чтобы избежать превышения счета
Новое
Не следует вводить сделки, которые произойдут в будущем
Можно избежать пост-датирования чека
Описание
Процесс
Новый
Составление баланса счета
Урегулировать все сделки с точки зрения банка, чтобы мы должны были удостовериться, что все из них зарегистрированы
Ревизия счета
Процесс, чтобы удостовериться, что все именно так, как зарегистрировано
Ввод сделки
Ввод нового депозита, чека или прямого изъятия
Имеется, вероятно, большее количество бизнес-правил и процессов, которые могут быть предложены, но то, что мы имеем — хорошее начало. К этому моменту вы сформировали проект документа для одобрения клиентом.
99
Глава 4
Метод водопада Заключительный момент, на который мы должны обратить внимание, состоит в том, что вам, возможно, потребуется выполнить весь этот процесс много раз, в зависимости от того, как хорошо вы выполнили процесс, описанный в этой главе. Те, кто имеет опыт руководства проектом, могут заметить, что процесс, отраженный в этой книге — точный метод водопада, который описывает процесс проектирования программного обеспечения подобно следующей схеме:
Описание
Проектирование
Кодирование
Проектирование
Тестирование
Она описывает некоторый путь, который мы выполняем здесь в процессе проектирования БД. Однако на практике этот процесс был бы более сложен, чем изображенный на предыдущем рисунке. Фактически он представлял бы цикл, а не линейно следующие друг за другом блоки. Как результат стадии тестирования, может потребоваться включить в проект новые определенные описания; это подразумевает, что весь процесс должен начаться заново. Каждый новый запуск цикла называется итерацией. Любой программный продукт или приложение выполняет много итераций, прежде чем завершит работу. Проблема добавления циклической информации состоит в том, что мы можем вернуться от кодирования назад к проектированию или от проектирования назад к описанию. Если все идет точно согласно плану, то вышеупомянутая диаграмма иллюстрирует полный процесс проектирования. Разумеется, решение в конечном счете должно быть получено, и ключ к этому — попытаться ограничить число итераций всякий раз, когда это возможно. Несколько больших изменений намного лучше, чем много небольших изменений.
100
Сущности, атрибуты, отношения и бизнес-правила
Резюме В этой главе мы рассмотрели процесс нахождения сущностей, которые, в конечном счете, составят основу решения вашей БД. Мы пропахали всю документацию, которая была получена на стадии сбора информации. При этом старались не добавлять наши собственные идеи в решение, пока не обработали всю исходную документацию. Следует заметить, что это не такая уж маленькая задача; в нашем исходном примере мы имели для работы только два абзаца, а завершили почти полутора страницами документации. Мы также провели интервью с клиентом и получили его согласие. Следует отметить, что запись всей этой информации в простой текстовый документ — не идеал. Мы выбрали этот способ размещения просто для целей этой книги, но обычно мы используем специализированные средства для формирования такого вида документации. Процесс моделирования данных, к которому мы перейдем в следующей главе, сформирует скелет документации с незаполненными местами, в которые вы будете вводить вашу информацию. До сих пор мы занимались только той частью процесса проектирования, которая связана с описанием. И этого действительно нельзя избежать. Любой процесс разработки, который требует хранения данных, должен полностью определить эти данные до перехода на стадию проектирования. Стадия реализации будет рассмотрена во второй половине этой книги, когда мы перейдем к физическому проектированию. Когда процесс, рассмотренный в этой главе, будет завершен, мы будем иметь почти все, что должны взять от клиента, прежде чем создадвать точную спецификацию данных.
101
*
Г
ч
'
.
Моделирование данных Введение Термин "моделирование данных" часто используется в очень широком смысле для описания всего процесса проектирования БД. Такая формулировка не так уж и плоха, но здесь мы будем понимать под данным термином не это. В этой главе мы будем использовать его для обозначения процесса разработки графических представлений структур данных. Документ, который мы начали разрабатывать в предыдущей главе, стал неуправляемым за короткое время, и идентификация отношений между различными сущностями стала очень трудной. Следовательно, мы нуждаемся в некотором способе представить информацию, которую представили как таблицы в документе, в более понятном формате. Чтобы сделать это, мы изображаем графические модели (картинки), чтобы преобразовать документ в форму картинки. Имеется много типов моделей или диаграмм: модели процесса, диаграммы потока данных, модели данных, диаграммы последовательностей; список мог бы продолжаться в течение весьма долгого времени. Мы, однако, сосредоточимся на двух специфических моделях: •
Модель действий (Use Case) — модель, которая входит в более общее описание, известное как UML (см. ниже), используемое для моделирования нужд и требований пользователей. Модель действий предназначена для задания всей известной о проекте информации, чтобы она была удобочитаемой как для пользователей, так и для проектировщиков.
•
Модели данных (Data Models) — модели, которые полностью связаны с представлением структур данных в реляционной БД.
Прежде чем мы посмотрим на вышеупомянутые модели более внимательно, обсудим кратко методологии моделирования, которые будем использовать в каждом случае.
Глава 5
Методологии моделирования Модель действий фактически является частью более общего описания, называемогоUML (Unified Modeling Language — унифицированный язык моделирования). UML-описание не имеет средств моделирования данных для реляционных БД (это — объектно-ориентированная технология), так что мы должны будем использовать другую методологию моделирования данных. Мы остановим наш выбор методологии моделирования данных, взяв ту, которая является самой легкой для чтения, а также позволяющей отображать и размещать все, что нам потребуется. В этой книге мы сосредоточимся на одном из наиболее популярных форматов моделирования для реляционных БД — IDEF1X (Integration Definition For Information Modeling — интегральное определение для информационного моделирования), и затем кратко коснемся другого формата — IE (Information Engineering — информационная разработка). В то время как методология моделирования данных может выбираться исходя из личных предпочтений, экономика, стандарты или возможности компании обычно влияют на выбор средств. В этой главе мы попытаемся непредвзято посмотреть на некоторые особенности средств, которые требуются или желательны, если нужно расширить основы графического моделирования. Вся информация, найденная в документе, который мы создавали в предыдущей главе, должна быть представлена и размещена в модели данных, и мы должны иметь возможность получать эти данные, чтобы поделиться с клиентом и программистами. U M L UML — де-факто стандартная методология для определения и документирования компонент объектно-ориентированных программных систем, созданная в значительной степени благодаря работе трех людей: Грэди Буча, Ивара Якобсона и Джима Рамбо. Каждый из этих людей имел свои собственные (или частично собственные) методологии, но они были объединены вместе, поскольку все эти люди начали работать вместе над продуктом Rational Rose на фирме Rational Software. Однако это не закрытая методология, и она сегодня используется и стандартизирована в программных средствах, предлагаемых многими большими корпорациями. В настоящее время полную документацию версии 1.3 можно найти в http://www.rational.com/uml. Корпорации, которые внесены в список использующих UML-описание: Rational Software, Microsoft, Hewlett-Packard, Oracle, Sterling Software, MCI Systemhouse, Unisys, ICON Computing, IntelliCorp, i-Logix, IBM, Object Time, Platinum Technology (CA), Ptech, Taskon, Reich Technologies, Softeam. По-видимому, важно упомянуть здесь, что UML не только методология моделирования, но и законченный метод объектно-ориентированного проектирования. В то время как методология моделирования — просто метод графического отображения компонент компьютерной системы, метод проектирования влечет за собой проектирование системы "от колыбели до могилы". UML предъявляет невысокие требования, когда мы хотим моделировать процессы типа тех, которые обнаружили при рассмотрении прошлой главы. Другая цель состоит в том, чтобы дать прямое объяснение того, почему при моделировании данных следует использовать UML, хотя мы не будем использовать его непосредственно для построения или моделирования наших реляционных данных, так как (об этом было сказано ранее) он предназначен не для реляционных технологий БД. 104
Моделирование данных UML составлен из весьма небольшого числа различных моделей, каждая из которых используется, чтобы представить компоненты системы, но мы используем только одну из них. Для чистого архитектора данных, имеющего дело только с построением реляционной БД, большинство моделей, вероятно, не так интересны, за исключением диаграмм действий (use case diagram). Позже мы дадим краткий обзор методики моделей действий на таком уровне, который позволит использовать ее в нашей задаче проектирования СУБД. I D E F 1 X Рассмотреть все существующие методологии моделирования было бы почти невозможно. Все они служат одной и той же цели и все имеют почти одни и те же объекты, отображаемые слегка различающимися способами. В этой главе мы остановимся на одной из наиболее популярной из них — IDEF1X, которая основана на сообщении FIPS (Federal Information Processing Standard — федеральный стандарт по обработке информации США) №184 от 21 сентября 1993 г. Чтобы быть справедливыми, имеется несколько методологий моделирования, которые являются в значительной степени эквивалентными для моделирования данных, типа IE (см. выше) и Chen ERD (диаграмма Чена отношений между сущностями), и если вы использовали одну из этих методологий, то вы, вероятно, привыкли к ней и не пожелаете изменить технологию на основе этой главы. Однако все примеры в книге будут в IDEFlX-формате. IDEF1X была первоначально разработана в Военно-воздушных силах США в 1985 г., чтобы обеспечить следующие требования: •
поддержка разработки моделей данных;
•
быть языком, который одновременно и легок в изучении, и является мощным;
•
быть доступным;
•
быть хорошо протестированным и проверенным;
•
быть подходящим для автоматизации.
Для получения полной копии IDEFlX-описания обращайтесь к http://www.qd.cl/webpages/idef/idef1/idef1x.html. IDEF1X очень хорошо работает в случае предъявления вышеупомянутых требований и используется во многих популярных средствах проектирования, типа СА ERwin, Embarcedero ERStudio и Microsoft Visio Enterprise Edition. Она также поддерживается другими распространенными средствами, которые можно найти в Интернете. Теперь, когда мы обсудили методологии, которые будем использовать, посмотрим, как их применять к нашим моделям.
Модели действий Как описано в Руководстве по UML, версия 1.1, изданная 1 сентября 1997 г. (полный документ находится в http://www.rational.com/uml), модели действий (Use Case) представляют внутри системы функциональные возможности этой системы или класса так, как это видят внешние пользователи.
105
Глава 5 Модель действий описывает: •
компоненты системы, смоделированные как действия (use cases);
Q
пользователей системы, смоделированных как исполнители (actors);
•
отношения между исполнителями и действиями.
Очень упрощенно модели действий представляют абстракцию диалогов между исполнителями и системой. Они описывают потенциальные взаимодействия, не вдаваясь в детали. Этот тип информации существенен для архитектора БД, так как нам всегда нужно идентифицировать пользователей, которые будут обращаться к БД, и то, что они хотят делать. Имеется несколько очень важных причин для этого: •
безопасность;
•
обеспечение данных, необходимых для поддержания существующего процесса;
•
создание хранимых процедур и триггеров для поддержания процесса.
Как мы увидим в следующем разделе, модели данных вообще не имеют отношения к процессам или пользователям. Так как модели действий являются частью UML, они часто используются не только строго объектно-ориентированными проектировщиками, но также и архитекторами систем всех типов, чтобы получить чертеж действий, которые должны дать желаемый результат. Различным категориям проектировщиков/архитекторов при использовании различных технологий моделирования понадобится использовать различные части диаграмм действий (как и нам с нашими моделями данных). Нет никакого набора приемов описания действий, но имеется определенный тип модели. Диаграммы действий очень просты, и имеются только два символа, используемые в них. Первый символ — исполнитель (actor), который является фигуркой человечка:
Исполнитель представляет пользователя, систему или часть аппаратных средств, которые выполняют действия в системе. Второй символ — действие (use case) — изображается обычным эллипсом с названием внутри:
106 I
Моделирование данных Чтобы смоделировать факт, что исполнитель использует действие, эти два символа связываются одинарной линией, именуемой отношением коммуникаций (communicates relationship):
Элементы действий могут быть также связаны друг с другом двумя способами: Q
с помощью другого действия для завершения цели — отношение "использует" ("uses");
•
расширяя другое действие, чтобы уточнить его поведение — отношение "расширяет" ("extends").
Они моделируются очевидным образом: «использует» или «расширяет Действие X )
( Действие Y
Пусть, например, мы хотим смоделировать процесс создания книги. Мы имеем, по крайней мере, следующих исполнителей: издатель, принтер, редактор и (надо надеяться) автор. Тогда нам понадобятся следующие элементы действий: предложение книги, прием книги, написание книги, редактирование книги, печатание книги и (мое любимое) плата за книгу. Это, вероятно, не включает всех исполнителей и действий, нужных для моделирования процесса публикации книги, но вполне достаточно для нашего примера. Отсюда мы получаем следующую модель:
Издатель
Редактор
Принтер
107
Глава 5 Один момент может показаться странным в этой диаграмме — прием книги находится до предложения книги. Однако в диаграмме действий нет никакого упорядочения. Упорядочение будет позже при рассмотрении процесса. Фактически, в очень большой диаграмме действий могут быть сотни элементов действий, связанных со многими исполнителями. Каждый элемент действия будет обычно иметь информацию, объясняющую его назначение. Следующая таблица показывает возможное описание для одного из наших действий: Название
Написание книги
Описание
Процесс выработки идеи и превращения ее в несколько сотен страниц
Предпосылки
Книга должна быть предложена и одобрена Схема должно быть полностью изложена в деталях и готова для написания книги
Постусловия
Рукопись произведения будет завершена и готова для издания
Этапы работы
Разработать план Написание введения Написание всех глав Написание резюме
Из модели и описаний сразу же можно видеть не только то, кто является пользователями, но также и каковы их взаимодействия с нашей системой написания книги. Обратите внимание, что и издатель, и принтер включены в действие "Печатание книги". Издатель заказывает печатание, а принтер фактически выполняет печатание. Какое бы средство ни использовалось для построения диаграммы действий, важно, чтобы оно могло хранить детальную информацию относительно каждого действия или исполнителя в модели. Хорошие средства от продавцов, перечисленных ранее, позволят нам формировать чрезвычайно полезные сообщения о данных, которые мы помещаем в модель. Я оставляю вас, читатель, далее копаться в UML, по крайней мере, до такой степени, чтобы вы были способны понять модели, которые создадут ваши деловой аналитик и системные архитекторы. Одна из лучших книг по этой теме — "Instant UML" (Wrox Press, ISBN 1861000871).
Модели данных Имеются два основных типа моделей данных — логическая и физическая. В течение процесса проектирования СУБД мы сначала строим логическую модель и затем на ее основе — одну или более физических моделей. Если процесс проектирования идет успешно, полная логическая модель должна быть функционально эквивалентна физической модели(ям), как только будет реализована полная логическая модель. Никакие данные, которые пользователь хочет видеть, не должны появиться в физической модели без того, чтобы не быть в логической модели.
108
Моделирование данных Логическая модель представляет основную неизменяемую сторону информации, которую клиент должен поддерживать. Логическая модель может быть реализована разными способами в зависимости от потребностей физической реализации. Независимо от того, используем ли мы SQL Server 2000, Oracle, Access или даже Excel для размещения данных, логическая модель должна остаться неизменной. В процессе логического моделирования мы в первую очередь должны быть убеждены, что каждая часть информации зарегистрирована, так что ее можно будет где-нибудь разместить. Поэтому, когда мы будем в следующих двух главах выполнять процесс нормализации, наша модель будет превращаться из дезорганизованной в значительной степени массы сущностей, с которыми мы начали работать в предыдущей главе, в достаточно организованный набор сущностей. К концу этого процесса мы должны идентифицировать каждую отдельную часть информации, которую мы, возможно, пожелаем физически разместить. Главное различие между логическими и физическими моделями — реализация. Так как мы используем тот же самый язык моделирования, логическая модель будет всегда в значительной степени походить на физическую модель, но в течение стадии логического моделирования лучше всего заставить себя игнорировать слово КАК и полностью сосредоточиться на слове ЧТО войдет в БД. Физическая модель, которую мы рассмотрим в главе 10, обеспечивает детальный план действий для реализации БД. Эта модель — то, где мы будем брать сущности, которые обнаружили в предыдущей главе, и превращать их в таблицы. Возможны различные процессы в зависимости от цели и использования данных в БД, и они будут обсуждены во второй половине книги. В то время как логическая модель должна быть достаточно полна, чтобы описать коммерческую деятельность, физическая модель данных, которую мы создадим позже, может быть подвержена компромиссам между характеристиками и эффективностью. Если эти две модели так сильно отличаются, то в чем цель логического моделирования? Это — серьезный вопрос. Фиксируя, какое должно быть оптимальное размещение данных с точки зрения логичности, мы увеличиваем нашу способность создать более хорошую физическую БД, даже если должны отклониться немного (или много) от логической модели. Логическая модель остается важным документом для более поздних изменений в системе и не будет чрезмерно извращена деталями размещения данных.
Сущности В стандарте IDEF1X сущности (которые являются синонимом таблиц) моделируются прямоугольниками, что используется в большинстве методологий моделирования данных. Имеются два различных типа сущностей, которые мы моделируем: идентификаторо-независимый и идентификаторо-зависимый, также часто упоминаемые как независимый (independent) и зависимый (dependent) соответственно.
Тип сущности Независимая сущность называется таким образом потому, что она не имеет никаких зависимостей первичного ключа от любой другой сущности или, другими словами, нет никаких внешних или мигрирующих ключей в первичном ключе. В главе 3 мы обсуждали внешние ключи, но IDEF1X вводит дополнительный термин, который является несколько запутывающим, но более иллюстративным, чем внешний ключ, — мигрирующий. Термин "мигрирующий" является обычным и включен в описание, 109
Глава 5 но это может ввести в заблуждение, так как мигрировать означает двигаться. Вместо того чтобы фактически передвигаться, он относится к первичному ключу одной сущности, "мигрируя" (копируется) как атрибут в другой сущности, таким образом, устанавливая отношения между этими двумя сущностями. Следовательно, сущность "независима" от любых других сущностей. Все атрибуты, которые не мигрируют, являются собственными, поскольку они имеют свое происхождение в текущей сущности. Независимая сущность отображается прямоугольником с незакругленными углами следующим образом:
Здесь Independent
— сущность "Независимая".
Зависимая сущность противоположна независимой сущности, поскольку она будет иметь первичный ключ другой сущности, который мигрировал в ее первичный ключ. Мы будем редко завершать работу, используя зависимые сущности на стадии физического моделирования проекта. Это связано с тем, что первичный ключ сущности не должен быть изменяем в БД, и, конечно, не иметь зависимые сущности, на которые он ссылается. Конечно, концепция каскадной коррекции существует, так что это не такая уж серьезная проблема, но мы увидим позже в главе 10 трудности, характерные для формирования сущностей таким образом. Мы рассмотрим более глубоко идею независимых и зависимых сущностей в разделе этой главы, посвященном идентифицируемым и неидентифицируемым отношениям. Зависимая сущность будет изображаться прямоугольником с закругленными углами: Dependent
Здесь Dependent — сущность
"Зависимая".
То, что мы здесь видим, немного напоминает ситуацию яйца и цыпленка. Зависимая сущность зависит от некоторого типа отношений. Последующий раздел, посвященный атрибутам, содержит информацию, которая основана на некоторых типах отношений, которые мы еще не рассмотрели. Однако ясно, что мы не можем ждать, пока рассмотрим отношения, чтобы представить материал, необходимый для завершения рассмотрения сущностей. Если Вы впервые рассматриваете модели данных, эту главу может потребоваться перечитать, чтобы получить полную картину, поскольку концепция независимых и зависимых объектов связана с отношениями.
110
Моделирование данных Обозначение Х о т я описание I D E F 1 X не дает п р а в и л а , к а к н а з ы в а т ь с у щ н о с т и , п р и ф о р м и р о в а н и и сущностей м ы д о л ж н ы у п о м я н у т ь этот момент. Один и з наиболее в а ж н ы х а с п е к т о в п р о е к т и р о в а н и я и л и р е а л и з а ц и и любой с и с т е м ы — то, к а к в ы н а з ы в а е т е в а ш и о б ъ е к т ы , п е р е м е н н ы е , и т. д. Обозначение объектов Б Д в этом не о т л и ч а е т с я , и в о з м о ж н о более в а ж н о ясно н а з в а т ь о б ъ е к т ы Б Д , чем другие о б ъ е к т ы п р о г р а м м и р о в а н и я . Н а з в а н и я , к о т о р ы е в ы даете в а ш и м с у щ н о с т я м (и а т р и б у т а м т а к ж е ) , будут переведены в н а з в а н и я с у щ н о с т е й , к о т о р ы е т о ж д е с т в е н н о будут и с п о л ь з о в а т ь с я п р о г р а м м и с т а м и и п о л ь з о в а т е л я м и . Л о г и ч е с к а я модель будет р а с с м а т р и в а т ь с я в а м и к а к п е р в и ч н о е схемное р е ш е н и е того, к а к Б Д б ы л а з а д у м а н а , и д о л ж н а б ы т ь ж и в ы м д о к у м е н т о м , к о т о р ы й в ы и з м е н я е т е перед и з м е н е н и е м любых физических структур. Несколько основных руководящих принципов для обозначения сущностей: •
Н а з в а н и я с у щ н о с т е й н и к о г д а не д о л ж н ы быть во м н о ж е с т в е н н о м ч и с л е . П е р в о п р и ч и н а этого — то, что н а з в а н и е д о л ж н о о б р а щ а т ь с я к э к з е м п л я р у с м о д е л и р о в а н н о г о о б ъ е к т а , а не к совокупности объектов. К р о м е того, звучит глупо, если с к а з а т ь , ч т о в ы и м е е т е " з а п и с ь а в т о м о б и л е й " ( з а п и с ь (record) — конструкция, используемая в программировании. Прим. перев.). Н е т , в ы имеете з а п и с ь а в т о м о б и л я . Д л я д в у х автомобилей в ы и м е л и бы две з а п и с и а в т о м о б и л я . (Однако, обратите в н и м а н и е , что с и с т е м н ы е т а б л и ц ы в SQL S e r v e r все во м н о ж е с т в е н н о м ч и с л е , к о т о р ы е я л и ч н о и м и т и р о в а л в период моего с т а н о в л е н и я . )
•
Д а в а е м о е н а з в а н и е д о л ж н о непосредственно соответствовать с о д е р ж а н и ю с у щ н о с т и . Н а п р и м е р , если в ы моделируете з а п и с ь , к о т о р а я п р е д с т а в л я е т ч е л о в е к а , н а з о в и т е ее person (человек). Е с л и в ы моделируете автомобиль, н а з о в и т е это automobile (автомобиль). Обозначение — не всегда урезанное и сухое, но б л а г о р а з у м н о и м е т ь н а з в а н и я п р о с т ы е и по сути.
Ч а с т о н а з в а н и я с у щ н о с т е й д о л ж н ы быть составлены и з н е с к о л ь к и х слов. К о н е ч н о , допустимо в к л ю ч а т ь в н а з в а н и я п р о б е л ы , когда н у ж н о и с п о л ь з о в а т ь н е с к о л ь к о слов, но это не о б я з а т е л ь н о . Н а п р и м е р , с у щ н о с т ь , к о т о р а я х р а н и т адреса л ю д е й , м о г л а бы б ы т ь н а з в а н а : Person Address, Person_Address и л и , и с п о л ь з у я с т и л ь , к которому я недавно п р и в ы к , и тот, которому м ы будем следовать в этой к н и г е , — p e r s o n A d d r e s s . Общее н а з в а н и е этого т и п а обозначения — " н о т а ц и я в е р б л ю д а " и л и с м е ш а н н ы й с л у ч а й . Обратите внимание, что мы находимся на логической стадии моделирования. Мы вообще хотели бы избежать использования пробелов в названиях наших физических структур. Хотя и допустимо иметь названия с пробелами, это не очень хорошо с точки зрения удобства. В SQL Server вам потребовалось бы использовать эти названия, окруженные квадратными скобками, [подобно этому примеру]. Н и к а к и е с о к р а щ е н и я не д о л ж н ы и с п о л ь з о в а т ь с я в л о г и ч е с к и х и м е н а х с у щ н о с т е й . К а ж д о е слово д о л ж н о б ы т ь полностью п о н я т н о . Аббревиатуры имеют т е н д е н ц и ю з а п у т а т ь с м ы с л . Однако с о к р а щ е н и я могут б ы т ь н е о б х о д и м ы в ф и з и ч е с к о й модели из-за некоторого стандарта о б о з н а ч е н и й , к о т о р ы й в ы н у ж д а е т вас это д е л а т ь . Е с л и в ы используете с о к р а щ е н и я , то д о л ж н ы г а р а н т и р о в а т ь , ч т о о п и с а т е л ь к а ж д ы й р а з и с п о л ь з у е т одно и то ж е с о к р а щ е н и е . Это — одна и з о с н о в н ы х п р и ч и н избегать с о к р а щ е н и й , чтобы не п о л у ч и т ь описатели, н а з в а н н ы е " d e s c r i p t i o n " , " d e s c r y " , " d e s c " , " d e s c r i p " и " d e s c r i p t n " , все обозначающие о п и с а т е л ь d e s c r i p t i o n . О ф и з и ч е с к и х о б о з н а ч е н и я х будем д о п о л н и т е л ь н о говорить в главе 10. О д н а к о небольшое п р е д у п р е ж д е н и е : в а ж н о не и с п о л ь з о в а т ь д л я 5—1868
"I "I "I
Глава 5 сущностей длинные описательные названия в виде целых предложений типа leftHandedMittensLostByKittensOnSaturdayAfternoons (рукавицы с левой руки, затерянные котятами в субботу после полудня) за исключением случая, когда эта сущность должна отличаться от leftHandedMittensLostByKittensOnSundayMornings (рукавицы с левой руки, затерянные котятами в воскресенье утром) — усечение на экране затруднит чтение названия. Лучшим названием могло бы быть mittens (рукавицы) или даже lostMittens (потерянные рукавицы). Многое из того, что кодируется в большом названии, будет, вероятно, само по себе сущностями: Mittens (рукавицы), Mitten Status (состояние рукавицы), Mitten Hand (с какой руки рукавица), Mitten Used By (рукавица, используемая для ...), Kittens (котята), и т. д. Однако это больше связано с понятием нормализации, и будет обсуждено далее в главах 6 и 7.
Атрибуты Атрибуты сущности должны иметь уникальные имена внутри нее. Они представляются в виде списка имен внутри прямоугольника сущности: AttributeExample Attribute 1 Attribute 2 Здесь AttributeExample
— сущность "Пример атрибутов"; Attribute
— атрибут.
Замечание: формально, это неправильная сущность, так как не имеет определенного первичного ключа (как требуется в IDEF1X). Мы рассмотрим ключи в следующем разделе. На данном этапе мы просто ввели бы все атрибуты, которые определили на стадии разработки. Практически, было бы удобно объединить процесс выявления сущностей и атрибутов с начальной стадией моделирования. Все будет зависеть от того, сколь хорошие средства вы используете в своей работе. Большинство средств моделирования данных обеспечивает быстрое построение моделей и хранение обширной информации для документирования их сущностей и атрибутов. На ранних стадиях логического моделирования' может быть весьма большое различие между атрибутом и столбцом. Как мы увидим в следующих двух главах, определение, что атрибут может хранить, изменится совсем немного. Например, атрибуты человека могут начинаться просто с адреса и номера телефона. Однако, как только мы это нормализуем, то разобьем их на много столбцов (адрес — на номер квартиры, название улицы, город, штат, почтовый индекс и т. д.) и, возможно, на много других сущностей.
Первичный ключ Как мы отметили в предыдущем разделе, сущность в IDEF1X должна иметь первичный ключ. Это удобно для нас, так как в главе 3 мы определили, что для кортежей или сущности каждая запись должна быть уникальна. Первичный ключ может быть единственным атрибутом, или он может быть составным ключом (определенным ранее как ключ с несколькими полями), и должны быть значения для всех атрибутов ключа (на физическом языке, в первичном ключе не допускаются значения NULL). Обратите внимание, что никакое дополнительное обозначение не требуется, чтобы указать, что величина является первичным ключом. 112
Моделирование данных Первичный ключ обозначается помещением его атрибутов выше горизонтальной линии, проходящей через прямоугольник сущности: Primary Key Example Primary Key Attribute 1 Attribute 2 Здесь Primary Key Example — сущность "Пример первичного ключа"; Primary первичный ключ; Attribute — атрибут.
Key —
На ранней стадии логического моделирования я обычно не люблю назначать какие-либо значащие атрибуты первичного ключа. Главная причина этого состоит в том, чтобы сохранить чистую модель. Я обычно добавляю абстрактный первичный ключ, чтобы перейти к другим сущностям; это позволяет мне видеть, кто чем владеет. Как мы увидим позже в этой главе, абстрактный первичный ключ будет содержать название сущности, так что, когда мы создаем отношения, которые вызывают миграцию ключа, независимо от того, какие первичные ключи мы выберем, они будут мигрировать к сущности-потомку в отношении. Поскольку, вероятно, мы не выберем первичный ключ, который будет в конечном счете реализован, я обычно моделирую все потенциальные ключи (или уникальные идентификаторы) как вторичные ключи (непервичные уникальные ключи). Результатом является то, что в логической модели становится ясно, какие сущности играют роль собственника по отношению к другим сущностям. Это, конечно, не является требованием логического моделирования, а отражает мое собственное предпочтение, которое я счел полезным методом документирования и который оставляет модель чистой и соответствует моему методу реализации, рассмотренному позже. Использование естественного ключа в качестве первичного ключа на логической стадии моделирования не только допустимо, но и предпочитается многими архитекторами. Я попытался приравнять все мои логические объекты объектно-ориентированным классам, которые идентифицированы не первичным ключом, а, скорее, указателем.
Вторичные ключи Наша следующая тема посвящена вторичным ключам. Как было предварительно определено в главе 3, вторичные ключи — набор одного или большего количества полей, чью уникальность мы хотим гарантировать по всей сущности. Вторичные ключи не имеют специальных обозначений, как и первичные ключи, и не мигрируют при любых отношениях. Они изображаются на модели очень простым способом: Alternate Key Example Primary Key Alternate Key (AK1) Alternate Key 2 Att 1 (AK2) Alternate Key 2 Att 2 (Ak2) Здесь Alternate Key Example — сущность "Пример вторичных ключей"; Primary первичный ключ; Alternate Key — вторичный ключ; Att — атрибут.
Key —
113
Глава 5 В этом примере имеются две вторичные ключевые группы: группа АК1 (Alternate Key 1), которая имеет в качестве своего члена один атрибут, и группа АК2 (Alternate Key 2), которая имеет два атрибута. Одно расширение, которое использует ERwin (средство моделирования данных, созданное Computer Associates: http://ca.com/products/alm/erwin.htm), показано ниже: Alternate Key Example Primary Key Alternate Key (AK1.1) Alternate Key 2 Att 1 (AK2.1) Alternate Key 2 Att 2 (Ak2.2) Обратите внимание, что имеется дополнительное обозначение <номер позиции>, прибавляемое к АК1 и АК2, чтобы обозначить положение атрибута в ключе. Формально в логической модели эту информацию не следует показывать, и, конечно, она должна игнорироваться. Логически не имеет значения, который атрибут используется первым в ключе. Когда ключ будет физически реализовываться, это будет интересным только по причинам самой реализации.
Внешние ключи Внешние ключи также называются мигрирующими атрибутами. Они являются первичными ключами сущности, которые служат как указатель на экземпляр информации в другой сущности. Эти ключи снова являются результатом отношений, которые мы рассмотрим в следующем разделе. Внешние ключи обозначаются как вторичные ключи с добавлением символов "FK" (Foreign Key) после внешнего ключа, например, Foreign Key Example Primary Key Foreign Key (FK) Здесь Foreign Key Example — сущность "Пример внешнего ключа"; Primary первичный ключ; Foreign Key — внешний ключ.
Key —
Диаграмма не показывает, из какой сущности мигрирует ключ, и может возникнуть неразбериха в зависимости от того, как вы выберете ваши первичные ключи. Это — ограничение всех методологий моделирования, поскольку было бы излишне запутанным для процесса, если бы мы показали название сущности, откуда берется ключ, по нескольким причинам: •
Нет никакого предела (да и не должно быть) тому, как далеко ключ будет мигрировать от своего первоначального владельца.
•
Это также неблагоразумно потому, что один и тот же атрибут мог бы мигрировать из двух разных сущностей, особенно на ранних стадиях процесса логического проектирования.
Это — одна из причин, чтобы для схемы первичного ключа, которую мы создадим в нашей логической модели, просто выбирать название <entityName>Id (<имя_сущности>1с1) как имя ключа для данной сущности. По моему мнению, такое название сущности легко опознаваемо и более ясное.
114
Моделирование данных Домены Домен (Domain) — термин, который мы, к сожалению, собираемся использовать в двух очень близких контекстах. В главе 3 мы узнали, что домен — набор допустимых значений для атрибута. В IDEF1X домены имеют тонко отличающееся определение, которое охватывает предыдущее определение, но с полезной характерной особенностью. В этом случае домены — механизмы, которые не только позволяют нам определять допустимые значения, которые могут быть размещены в атрибуте, но также и обеспечивают вид наследования в определениях наших типов данных. Вот примеры: •
String (строка) — строка символов.
•
SocialSecurityNumber (номер социального страхования) — набор символов с форматом ###-##-####.
•
Positivelnteger (положительное целое) — целое число, которое предполагает домен от О до max (целое число).
Далее мы можем создать подтипы, которые унаследуют значения исходных доменов. Мы будем строить домены для любых атрибутов, которые регулярно используем, а также домены, которые являются шаблонами для нечасто используемых атрибутов. Например, мы могли бы иметь основной домен типа символа, где определяем, что нужны не все символы. Мы могли бы также определить домены, названные пате (название) и description (описание), для использования во многих сущностях, и определить их, как это нам потребуется. При логическом моделировании требуется только несколько видов информации, которую мы, вероятно, будем хранить, таких как обычные типы атрибутов — символ, числовой, логический, или даже двоичные данные. Мы должны хранить эти домены как реализацию описания независимых типов данных. Например, мы могли бы определить домен: •
GloballyUniqueldentifier (GUID — глобальный уникальный идентификатор) — значение, которое будет уникальным независимо от того, где оно сформировано.
В SQL Server мы могли бы использовать uniqueidentifier (GUID-значение), чтобы реализовать этот домен. В Oracle, где нет точно того же механизма, мы реализовали бы это по-другому; то же самое было бы истинным, если мы используем текстовые файлы для хранения данных. Когда мы начнем физическое моделирование, то будем использовать те же самые домены, чтобы унаследовать также и физические свойства. Это — реальное достоинство использования доменов. Создавая для атрибутов шаблоны многократного применения, которые будут также использоваться, когда начнем создавать столбцы, мы уменьшим количество усилий, которые потребуются, чтобы строить простые сущности, составляющие большую часть нашей работы. Это также позволит создать стандарты компании, многократно используя те же самые домены во всех наших общих моделях. Позже физические детали, такие как тип данных, ограничения и возможность не иметь значения, будут выбираться только из нескольких основных физических свойств, которые можно наследовать. Так как нам нужно иметь намного меньше доменов, чем реализованных атрибутов, мы получим двойную выгоду быстрой и последовательной реализации. Однако мы не можем использовать механизмы наследования, когда строим наши таблицы вручную. Реализация доменов строго основана на используемых программных средствах.
115
Глава 5 Например, мы могли бы определить следующую иерархию:
--—
String
.^
Name [ FileName J
[Description
1 "^^-~ LastName ] f FirstName J
Здесь String (строка) — базовый домен, от которого мы затем получим Name (название) и Description (описание). FileName (имя файла), FirstName (имя) и LastName (фамилия) являются наследниками Name (название). На стадии логического моделирования это может показаться работой впустую, потому что большинство этих доменов будет обладать некоторыми основными деталями типа запрета использования NULL или незаполненных данных. Однако вам не всегда может потребоваться FileName, в то время как LastName — практически всегда. Важно реализовать домены для возможно большего числа различных типов атрибутов в случае, если вы находите правила или типы данных, которые являются общими для любых доменов, которые вы задали. Домены — одна из наиболее захватывающих особенностей IDEF1X. Они обеспечивают легкий метод создания стандартных атрибутов, сокращения как промежутка времени, требуемого для этого создания, так и числа ошибок, которые возникают при этом. Специальные программные средства реализуют домены, обеспечивая определение и наследование большинства свойств цепочки доменов, что делает создание БД более легким. Очевидно, что если вы создаете ваши БД и модели вручную, менее вероятно, что вы реализуете ваши таблицы, используя наследование доменов. Домены или типы данных можно показать у сущности справа от названия атрибута следующим образом: domainExample attributeName: domainName attributeName2: domainName Здесь domainExample — сущность "Пример домена"; attributeName domainName — имя домена.
— имя атрибута;
Так, если мы имели сущность, которая содержит значения доменов для описания типа person (человек), мы могли бы сформировать модель: personType personTypeld: primaryKey name: name description: description Здесь personType — сущность "тип-человек"; personTypeld— типа-человека; name — имя; description — описание. 116
идентификатор
Моделирование данных В этом случае мы имеем три домена: •
•
•
PrimaryKey — является указателем строки для мигрирующего внешнего ключа. Реализация не подразумевает построения домена, так что мы можем выбрать это значение любым образом, как пожелаем. . Name — домен общего вида для хранения имени экземпляра сущности. Используя его как наш стандартный домен имени всюду, где мы хотим поместить название, мы обеспечим согласованность. Если имя данной сущности не соответствует этому образцу, мы можем создать новый домен. Description — тот же самый тип домена, что и домен имени, только хранит описание.
Обозначение Обозначение атрибута несколько более интересно, чем обозначение сущности. Мы сказали, что название сущности никогда не должно быть во множественном числе. Тот же самое формально истинно и для атрибутов. Однако в этом месте процесса моделирования мы будем все же использовать названия атрибутов во множественном числе. Использование названия во множественном числе может быть хорошим напоминанием, что мы ожидаем несколько значений. Например, мы могли бы иметь сущность Person (человек) с идентифицированным атрибутом Children (дети). Сущность Person идентифицировала бы единственного человека, а атрибут Children идентифицировал бы сыновей и дочерей этого человека. Стандарты обозначения атрибутов всегда были весьма горячей темой с несколькими различными схемами обозначений, разработанными за прошлые 30 лет создания БД. Мы рассмотрим это в главе 10, когда начнем реализовывать нашу БД. Стандарт обозначений, которого мы будем придерживаться, очень прост. Q
Обычно не нужно повторять название сущности в названии атрибута, за исключением первичного ключа. Название сущности подразумевается включением атрибута в сущность. Так как первичный ключ будет мигрировать к другим сущностям, эта маленькая уступка делает миграцию более простой, поскольку мы не должны переименовывать каждый атрибут после миграции, не говоря уже о создании присоединения к устройству очистки памяти в SQL.
•
Выбранное название атрибута должно отражать точно, что содержится в атрибуте и как это относится к сущности записи.
•
Как и у сущностей, никакие сокращения не должны использоваться в логическом обозначении атрибутов. Каждое слово должно быть записано полностью. Если какое-то сокращение все же должно использоваться в каком-то месте из-за некоторого стандарта обозначений, то должен быть какой-то способ, чтобы удостовериться, что оно используется последовательно, как было рассмотрено ранее в этой главе.
Последовательность — ключ к надлежащему обозначению атрибутов, так что если у вас или у вашей организации нет стандарта назначения имен, стоило бы его разработать. Мой принцип задания имен — они должны быть простыми и удобочитаемыми, избегая сокращений. Этому стандарту мы будем следовать и при логическом моделировании, и на физической стадии. Независимо от того, каков ваш стандарт, задание образцов обозначения будет делать ваши модели легкими для работы и для вас самих, и для ваших программистов и пользователей. Любой стандарт всегда лучше отсутствия какого-либо стандарта.
117
Глава 5 Другой шаг, который вы должны сделать — это спросить о стандартах обозначений Вашего клиента (или работодателя), чтобы обеспечить будущее взаимопонимание, а не создавать новые стандарты.
Отношения До сих пор мы рассматривали конструкции, в значительной степени одинаковые при всех методологиях моделирования данных. Сущности всегда обозначались прямоугольниками, и атрибуты — словами в этих прямоугольниках. Однако отношения — совсем другой вопрос. Каждая методология дает свои отношения. IDEF1X весьма отличается от всех других прежде всего хорошим способом представления. Причина, по которой я предпочитаю IDEF1X — его способ представления отношений. Чтобы понять это, возьмем термины parent (передок) и child (потомок) и пример отношения между ними. Из глоссария в описании IDEF1X мы находим следующие определения: •
Entity, Child (сущность, потомок) — сущность в определенных отношениях связи, чьи экземпляры могут быть связаны с нулем или одним экземпляром другой сущности (сущность-предок).
•
Entity, Parent (сущность, предок) — сущность в определенных отношениях связи, чьи экземпляры могут быть связаны с множеством экземпляров другой сущности (сущность-потомок).
•
Relationship (отношение) — связь между двумя сущностями или между экземплярами одной и той же сущности.
Это настолько ясные определения, что заслуживают быть принятыми на государственном уровне. Каждое отношение обозначается линией, проведенной между двумя сущностями со сплошным кружком на одном конце этой линии. Parent
Child
Первичный ключ предка мигрирует к потомку. Это представляет то, как мы обозначаем внешний ключ на модели. Мы попытаемся пройти все различные структуры в IDEFlX-методологии, сопровождая их кратким обзором некоторых других методологий, что будет необходимо нам для понимания, так как не каждый будет использовать IDEF1X. Имеется несколько различных типов отношений, которые мы рассмотрим: •
1 1 8
Идентифицирующее
Table A
Table В
L
LJ
Неидентифицирующее
Table A
Q
Необязательное
Table A
J Л
Z
D
Table В
Table В
о-
•
Моделирование данных
•
Рекурсия
•
Классификация или подтипы
•
Многие ко многим
Здесь Table — сущность "таблица"; Tree Entity ОДИН КО
Table A
Table В
— сущность "дерево".
МНОГИМ
Отношение "один ко многим" является на самом деле примером неправильного употребления, и может рассматриваться как отношение "один к любому числу". Оно охватывает отношения "один к нулю", "один к одному", "один ко многим" или, может быть, "один к точно п". Мы увидим, что формально более точно — это "один к (от m до п)"; так мы можем определить многие очень точные (или очень свободные) термины, как диктует ситуация. Однако более привычный термин — "один ко многим", и мы не будем делать уже запутанный термин еще более запутанным. Имеются два основных типа отношения "один ко многим" —идентифицирующее и (неудивительно) неидентифицирующее. Различие, как мы увидим, определяется тем, мигрирует ли первичный ключ. Имеется очень немного разновидностей отношения "один ко многим", и мы рассмотрим все их (и их обозначения) в этом разделе.
Идентифицирующие отношения Идентифицирующее отношение указывает, что атрибут мигрирующего первичного ключа мигрирует в первичный ключ потомка следующим образом: Parent Parent Primaiy Key Attribute 1 Attribute 2
Child 'Child Primaiy Key Parent Primaiy Key (FK) Attribute 1 .Attribute 2
;
Здесь Parent — сущность "предок"; Child — сущность "потомок"; Parent Primary Key— первичный ключ предка; Child Primary Key — первичный ключ потомка; Attribute — атрибут.
119
Глава 5 Обратите внимание, что сущность-потомок в отношении изображена как прямоугольник с закругленными углами, что означает зависимую сущность. Причина, что оно называется идентифицирующим отношением, заключается в том, что мы должны будем иметь экземпляр предка, чтобы иметь возможность определить запись экземпляра-потомка. Существование экземпляра-потомка (определяемого как "характерные или необходимые свойства, которые служат, чтобы определить или идентифицировать кое-что") определяется существованием предка. Другими словами, идентификация и определение записи-потомка основаны на существовании записи-предка. Пример — заказ на поставку и его элементы. Без заказа на поставку элементы не имеют никакого смысла.
Неидентифицирующие отношения Неидентифицирующее отношение означает, что атрибут первичного ключа не мигрирует в первичный ключ потомка. Оно используется более часто, чем идентифицирующее отношение. В то время как идентифицирующие отношения были основаны на необходимости существования предка, что определяет даже само существование потомка, неидентифицирующее отношение — (что не удивительно) полная противоположность. Взяв пример заказа на поставку, рассмотрим продавца изделия. Он не определяет существование элементов заказа. Продавец в этом случае может быть, а может и не быть, так как бизнес-правила могли бы определять, но обычно они это не делают, является ли отношение идентифицирующим или неидентифицирующим; сами данные будут фундаментальными свойствами отношения. Неидентифицирующие отношения моделируются пунктирной линией: Parent
Child
Здесь Parent — сущность "предок"; Child — сущность "потомок". Почему вам следует использовать идентифицирующие вместо неидентифицирующих отношений? Причина фактически довольно проста. Если сущность-предок (как мы отметили в предыдущем разделе) определяет сущность-потомок, то мы будем использовать идентифицирующее отношение. Если же, с другой стороны, отношение определяет один из атрибутов потомка, то мы используем неидентифицирующее отношение. Как другой характерный пример рассмотрим следующее: •
Идентифицирующее — Пусть есть сущность, которая хранит партнеров, и сущность, которая хранит номера телефона партнеров. Партнер (contact) определяет номер телефона, и без партнера не имелось бы никакой необходимости в номере телефона партнера (contactPhoneNumber). Contact
ContactPhoneNumber J
120
Моделирование данных •
Неидентифирующее — Возьмем сущности, которые мы определили для идентифицирующего отношения, вместе с дополнительной сущностью, которую назовем contactPhoneNumberType (тип номера контактного телефона). Эта сущность связана с сущностью contactPhoneNumber, но неидентифицирующим способом, и определяет набор возможных типов номеров телефонов ("Голосовой", "Факс" и т. д.), каким мог бы быть contactPhoneN umber. Тип номера телефона не определяет номер телефона, а просто классифицирует его. Contact
ContactPhoneNumber
С
J
ContactPhoneNumberType Сущность contactPhoneNumberType обычно называется сущностью домена или таблицей домена. Вместо того чтобы иметь фиксированный домен для атрибута, мы проектируем сущности, которые позволяют программно изменять домен без перекодирования ограничений и кода клиента. Как дополнительное преимущество, мы можем добавлять столбцы, чтобы определять, описывать и расширять значения домена для обеспечения бизнес-правил. Это также позволяет пользователю клиента строить для других пользователей списки, чтобы выбирать значения с очень небольшой долей программирования. Имеются два различных класса неидентифицирующих отношений — обязательное и необязательное. Мы поподробнее рассмотрим их в следующих двух разделах.
Обязательное отношение Обязательные неидентифицирующие отношения называются таким образом потому, что требуется мигрирующее поле в экземпляре потомка. Когда реализуется это отношение, мигрирующий ключ должен быть помечен как NOT NULL (не является NULL). D
arent Parent Primary Key Attribute 1 Attribute 2
Child Child Primary Key Parent Primary Key (FK) Attribute 1 Attribute 2
Здесь Parent — сущность "предок"; Child — сущность "потомок"; Parent Primary Key— первичный ключ предка; Child Primary Key — первичный ключ потомка; Attribute — атрибут.
121
Глава 5 Обратите внимание, что сущность-потомок в этом примере не имеет закруглений у прямоугольника. Это потому, что обе сущности независимы, так как ни один из атрибутов первичного ключа потомка не является внешним ключом. Другой пример такого отношения мог бы быть взят из БД проката кинофильмов: genre
-CD
Здесь genre — сущность "жанр"; movie — "кинофильм". Отношение можно было бы представить в виде Genre <классифицирует> Movie, где сущность Genre — сущность "один", a Movie — "многие". Обязательно каждый арендуемый кинофильм должен иметь жанр, чтобы их можно было расставить по полкам.
Необязательное отношение Нам не всегда нужно, чтобы потомок имел значение мигрирующего ключа. В этом случае мы зададим мигрирующий ключ потомка необязательным, для которого в этом случае разрешается значение пустого указателя (NULL). Так как неидентифицирующее отношение означает, что предок — атрибут потомка, то это эквивалентно наличию необязательного атрибута (атрибут, который может иметь значение NULL). Мы показываем это пустым ромбом на противоположном конце линии с черным кружком, как изображено ниже: Parent Parent Primary Key Attribute 1 О Attribute 2
Child Child Primary Key -• Parent Primary Key (FK) Attribute 1 Attribute 2
Здесь Parent — сущность "предок"; Child — сущность "потомок"; Parent Primary Key — первичный ключ предка; Child Primary Key — первичный ключ потомка; Attribute — атрибут. Если вы задаетесь вопросом, почему не может быть необязательного идентифицирующего отношения, то потому, что вы не можете иметь никаких необязательных атрибутов в первичном ключе; это справедливо и для IDEF1X, и для SQL Server 2000. В качестве необязательного отношения "один ко многим" рассмотрим следующую структуру: invoice
invoiceLineltem
С
>—
dsicountType
Здесь invoice — сущность "счет-фактура"; invioceLineltem счета-фактуры"; discountType — сущность "тип скидки". 122
— сущность "элемент
Моделирование данных Сущность invoiceLineltem содержит элементы счета-фактуры, определяющие оплату. Далее рассмотрим ситуацию, когда мы в некоторых случаях применяем стандартные скидки к элементу счета-фактуры. Тогда отношение от invoiceLineltem к сущности discountType — необязательное.
Мощность отношения Мощность отношения означает число экземпляров потомка, которое может быть помещено для каждого предка этого отношения. Следующая таблица показывает шесть возможных мощностей, которые могут быть у отношения.
Один к нулю или большему числу
Один к одному или большему числу
Rarent
Один к нулю или одному (не больше, чем к одному)
Child 7
V
J Один к (от четырех до восьми)
Один к точно N (здесь 5 означает, что предок имеет точно 5 потомков)
Специальная запись, описывающая мощность
Здесь Parent — сущность "предок"; Child — сущность "потомок". Возможное отношение "один к одному или большему числу" могло бы использоваться, чтобы представить отношение между опекуном (guardian) и учащимся (student) в школьной БД: Guardian
Student О
4
Р
Мне нравится этот пример, потому что он выражает потребности чрезвычайно хорошо. Он говорит, что если запись guardian существует, то должен существовать student, но для нас не требуется, чтобы запись student имела поле guardian для размещения данных о нем.
123
Глава 5 Другой пример, который я обычно люблю рассматривать при обсуждении вопроса мощности отношения, следующий. Рассмотрим клуб, который имеет членов (Member) и некоторые положения (Position), которые они должны или могли занять: Member
Position
Member
Position
А
В
z
Member С
Position 0-2*
В примере А мы говорим, что член клуба может занять столько положений, сколько их имеется. Во втором случае мы говорим, что он может занять по крайней мере одно положение, в последнем случае — 0, 1 или 2. Все они выглядят одинаково, но Z или 0-2 используются чаще. Лично я рассмотрел, включая примеры, все из этих типов мощностей, но в большинстве случаев это либо слишком трудно, либо слишком глупо, так что мы просто упомянули несколько более типичных. Однако очень хорошо иметь их под рукой, если они станут необходимы.
Функциональные имена Функциональное имя — альтернативное название, которое мы даем атрибуту, когда используем его как внешний ключ. Цель функционального имени состоит в том, что мы иногда должны разъяснять использование мигрирующего ключа, потому что или сущность-предок очень общая, и мы хотим дать более определенное название, или мы имеем многократные отношения от той же самой сущности. Так как названия атрибутов должны быть уникальны, нам нужно назначить различные названия для записей потомка. В нашем примере: Parent Parent Primary Key
Child • • Child Primary Key Parent Primary Key 2.Parent Primary Key (FK) Parent Primary key 1 .Parent Primary Key (FK) Здесь Parent — сущность "предок"; Parent Primary Key — первичный ключ предка; Child — сущность "потомок"; Child Primary Key — первичный ключ потомка. 124
Моделирование данных
... мы имеем два отношения от сущности-предка к сущности-потомку, и мигрирующие атрибуты были названы как P a r e n t Primary Key 1 (первый первичный ключ предка) и Parent Primary Key 2 (второй первичный ключ предка). В качестве примера пусть у вас есть сущность User (пользователь), и вы хотите разместить name (имя) или Id (идентификатор) пользователя, которые формируют сущность Object (объект). Мы получили бы ситуацию наподобие следующей: User Primary Key
Object • * Primary Key Created For Primary Key.Primary Key (FK) Created By Primary Key.Primary Key (FK) Обратите внимание, что мы имеем два отношения к сущности Object от сущности User. Один назван [Created For Primary Key] (созданный для первичного ключа), и другой [Created By Primary Key] (созданный первичным ключом). Этот пример также показывает, почему вы поместили бы название сущности в атрибут первичного ключа. Когда первичный ключ мигрирует к другой сущности, важно знать, какова первоначальная сущность-предок. Так как атрибут в сущности Object имеет название [Primary Key] (первичный ключ), мы бы имели название первого атрибута таким же, как и второго.
Другие типы отношений Имеется несколько других, менее важных типов отношений, которые не используются так уж часто. Однако они чрезвычайно важны для понимания.
Рекурсия Одно из более сложных для реализации отношений, но одно из наиболее важных — рекурсивное отношение, известное также как самоприсоединяющееся, иерархическое, самоссылающееся или самоотносящееся. Оно моделируется неидентифицирующим отношением не к другой сущности, а к себе самой. Мигрирующему ключу отношения дают функциональное имя (и я обычно использую соглашение по обозначению, добавляя "parent" (предок) в начале названия атрибута, но это не обязательно). Tree Entity -"I ? 1 Здесь Tree Entity
1 1 1
— сущность-дерево. 125
Глава 5 Рекурсивное отношение удобно для создания древовидных структур, как в следующей структуре организации:
A l l « _
—
,
^
—
—
—
-
•
—
I T
H R
—
M a r k e t i n g
**•
Programming
D a t a b a s e M a n a g e m e n t
Здесь All — вся организация; IT—подразделение информационных технологий; HR — отдел кадров; Marketing — подразделение маркетинга; Programming — отдел программирования; Database Management — отдел менеджеров БД. Чтобы объяснить эту концепцию, нам нужно рассмотреть данные, которые размещаются с помощью следующей иерархии:
Здесь organization
— сущность "организация".
Мы используем organization!^rame (название организации) в качестве первичного ключа и parentOrganizationName (название организации — предок) как мигрирующий ключ, атрибут с функциональным именем, который задает само-ссылку соединения: OrganizationName
parentOrganizationName
All IT HR Marketing Programming Database Management
All All All IT IT
Диаграмма организации может теперь быть восстановлена, начиная с "АН" (вся организация), и получая его первого потомка — "IT" (подразделение информационных технологий). Далее мы получаем первого потомка "IT" — "Programming" (отдел программирования). "Programming" не имеет никаких потомков, поэтому мы возвращаемся к "IT" и получаем следующего потомка — "Database Management" (отдел менеджеров БД) и т. д. Рекурсивное отношение так названо потому, что популярный алгоритм для реализации таких структур на функциональных языках программирования использует рекурсию, чтобы организовать процесс, который мы моделируем в нашем примере.
126
Моделирование данных В качестве заключительного примера рассмотрим сущность person (человек), где мы храним имя, адрес и т. д. людей, которых мы включаем. Если мы хотим построить эту сущность с возможностью указать на супруга (spouse), то могли бы спроектировать следующее: person person Id spouseFtersonld (FK)
?1
Гi
является супругом
Здесь personld
— идентификатор человека; spousePersonld
— идентификатор супруга.
Заметьте, что мы задаем его как отношение "один к нулю или одному", так как (для большинства регионов) человек может иметь только единственного супруга (супругу).
Отношения классификации Отношения классификации (также упоминаются как подтипы) — другой специальный тип отношения "один к нулю или одному", используемые, чтобы указать, является ли одна сущность специфическим типом другой общей сущности. Обратите также внимание, что нет никаких черных точек на концах линий; специфические сущности изображены прямоугольниками с закругленными углами; это показывает, что они действительно зависят от общей сущности. При использовании этого типа отношений мы имеем три разные части:
En — itiy "-уJ
Generic 1
<4
ceific Entiy More Sjp С D •
s)icm ri n iator
Г
fiDEntyi с pecc
Other More £
Generic Entity (общая сущность) — это сущность, которая содержит все атрибуты, являющиеся общими для всех сущностей-подтипов. Discriminator (дискриминатор) — это атрибут, который действует как ключ, чтобы определить сущность, где размещена дополнительная, более определенная информация.
•
Specific Entity (специфическая сущность) — это место, где размещена специфическая информация, и которое определяется дискриминатором.
127
Глава 5 Например, рассмотрим библиотеку видеофильмов. Если вы хотите хранить информацию о каждом из видеофильмов, которые у вас имеются, независимо от их формата, то могли бы построить отношение классификации следующим образом:
Эти типы отношений обычно называются отношениями "является" — является VHC-видео (для бытового видеомагнитофона) или является DVD-видео (цифровой диск). Таким образом, вы могли бы хранить название, фамилии актеров, длину и, возможно, описание содержания в сущности Video (видеофильм), а затем на основе дискриминатора format (переключатель форматов) вы могли бы разместить информацию, которая является специфичной для VHS-лент или DVD в своих собственных отдельных сущностях, вроде специальных особенностей и меню для DVD, медленной или быстрой скорости для VHS-лент. Имеются два различных типа категорий — полные и неполные. Полный набор категорий моделируется двойной линией дискриминатора, а неполный — одной.
Полный набор категорий
Неполный набор категорий
Основное различие между полными и неполными категориями заключается в том, что в полных отношениях классификации каждый общий экземпляр должен иметь один специфический экземпляр; в неполном случае это не обязательно. Например, мы могли бы иметь полный набор категорий подобно следующему:
128
Моделирование данных Отношение читается следующим образом "Emloyee (служащий) должен быть или Male (мужчина), или Female (женщина)". Это, конечно, полный набор категорий, так как не существует никакого другого пола (sex). Однако возьмем следующий набор категорий Employee
Salaried
Hourly
В этом случае мы имеем неполный подтип, потому что Employee (служащий) является или salaried (находящимся на ставке), или hourly (почасовиком), но могут быть и другие категории (type), типа работающих по контракту. Нам может не понадобиться хранить какую-либо дополнительную информацию относительно них, так что не потребуется специфическая сущность. Это отношение читается как: "Служащий может быть или находящимся на ставке, или почасовиком, или кем-то другим".
Многие ко многим Отношение "многие ко многим" известно также как неспецифицированное отношение, которое является фактически лучшим названием, но гораздо менее известным. Очень часто мы имеем отношения "многие ко многим" в наших логических моделях. Фактически, чем ближе вы приближаетесь к надлежащей модели БД, тем больше вы видите, что каждое отношение будет иметь тип "многие ко многим". На ранних стадиях моделирования полезно определить отношения как "многие ко многим", даже если они будут изменяться во время реализации. Эти отношения моделируются линией с заполненной черной точкой на каждом конце: Entity Y
Entity X
! Здесь Entity
— сущность.
Обратите внимание, что мы не можем фактически реализовать это отношение в физической модели, так что часто переходим к более реализуемому представлению: Entity_X
Entity_X_Entity Y
Entity_Y
Здесь промежуточная сущность Entity_КJEntity_Y называется разрешающей сущностью (resolution entity). В нашем моделировании мы будем использовать и прежнее представление, когда не определяются никакие дополнительные атрибуты, чтобы описать отношение, и последнее представление, когда мы эти атрибуты используем.
129
Глава 5 Чтобы сделать это несколько более ясным, рассмотрим следующий пример: Customer
Product
Customerld
Productld
Name
Name
Здесь Customer — сущность "заказчик", Product — сущность "изделие"; Customerld, Productld — соответствующие первичные ключи; Name — имя заказчика или название изделия. В этой ситуации мы установили отношение, где многие заказчики (customer) связаны со многими изделиями (product). Это — обычная ситуация, поскольку мы редко создаем определенные изделия для определенных заказчиков; скорее, любой заказчик может купить любое из изделий. В этом месте моделирования мы использовали бы представление "многие ко многим". Однако если мы обнаруживаем, что заказчик может только быть связан с изделием в течение некоторого периода времени, мы могли бы выбрать другое представление: Customer Customerld Name
Customer Product ^Productld Customerld Beg in Date
3
roduct Productld
^
Name
На этом рисунке в дополнение к обозначениям предыдущего рисунка Customer Product — сущность "заказчик-изделие"; Productld, Customerld — первичный ключ промежуточной сущности; BeginDate — начало периода, когда конкретный заказчик связан с конкретным изделием, EndDate — конец этого периода. Фактически в большинстве случаев мы увидим, что отношение "многие ко многим" будет требовать некоторой дополнительной информации для своего завершения.
Глагольные конструкции (обозначения) Отношениям дают имена, которые называются глагольными конструкциями, и которые делают отношение между сущностью-предком и сущностью-потомком удобочитаемым предложением; они включают названия сущностей и мощность отношения. Название обычно формируется от предка к потомку, но может быть выражено и в обратном направлении или даже в обоих направлениях. Глагольная конструкция располагается на модели где-нибудь около линии, которая формирует отношение: Parent
<имя отношения>
Child
-CD
Отношение следует назвать так, чтобы имя удовлетворяло следующей общей структуре для чтения всего отношения: мощность предка — имя сущности-предка — имя отношения сущности-потомка 130
мощность потомка — имя
Моделирование данных Например, следующее отношение: contact
phoneNumber is phoned using
будет прочитано следующим образом: One contact is phoned using zero, one or more phoncNumber(s). (Одному партнеру позвонят, используя ноль, один или большее количество телефонных номеров.) Конечно, предложение может иметь (или не иметь) правильный грамматический вид, поскольку возникает вопрос, как партнеру могут позвонить, используя ноль телефонных номеров. Очевидно, если бы мы попробовали сказать это нетехническому человеку, то было бы больше смысла сказать следующим образом: One contact can either have no phoneNumber or may have one or more phoneNumbers. (Один партнер может или не иметь никакого номера телефона или иметь один или большее количество телефонных номеров.) Очевидно, язык моделирования не учитывает лингвистику при формировании этой спецификации, но с технической точки зрения не имеет значения, что партнеру позвонят, используя ноль телефонных номеров, так как из этого следует, что он не имеет никакого номера телефона. Способность читать отношение помогает нам замечать очевидные проблемы. Например, следующее отношение: contactType ^
classifies
contact Z
на первый взгляд выглядит нормальным, но когда читаешь следующим образом: One contactType classifies zero or one contact (Один тип партнера классифицирует ноль или одного партнера) то это не имеет логического смысла. Вероятно, мы хотели бы исправить эту ситуацию в нашей модели: contactType
classifies
contact
для которой мы теперь читаем: One contactType classifies zero or more contacts. (Один тип партнеров классифицирует ноль или большее количество партнеров.) Обратите внимание, что тип отношения (идентифицирующее, неидентифицирующее, необязательное, обязательное) не делает никаких различий при чтении отношения.
131
Глава 5 Мы можем также включать глагольную конструкцию, которая читается от потомка к предку. Для отношения "один ко многим" это имело бы следующий формат: one child record
exactly one parent record. (одна запись-потомок <отношение> точно одну запись-предка.) Для нашего первого примера нам следует прибавить дополнительную глагольную конструкцию: contact
phoneNumber is phoned using/ Jmay be used to phone | 1
Это будет читаться совершенно противоположно предыдущим примерам, т. е.: One phoneNumber may be used to phone exactly one contact (Один номер телефона может использоваться, чтобы позвонить точно одному партнеру.) Так как мы идем от многих к одному, то будем всегда знать, что предок в отношении будет иметь относительное значение, и так как мы читаем в контексте существования потомка, то можем также предполагать, что имеется единственная запись-потомок, рассматриваемая в предложении.
Другие методы изображения отношений В этом разделе мы постараемся кратко описать две других методологии моделирования, с которыми вы, вероятно, часто будете иметь дело при проектировании БД для SQL Server 2000.
Information Engineering (информационная разработка) Методология IE (Information Engineering) хорошо известна и широко используется. Она действительно весьма популярна и успешно работает при отображении необходимой информации. Эта методология также нежно называется методом "птичьей" лапки. Изменяя основные символы на конце линии, мы можем получить все различные варианты отношений. Следующая таблица показывает различные символы, которые мы можем использовать, чтобы строить представления отношений: Символ
Описание Многие — сущность на стороне с "птичьей лапкой" означает, что может быть более одного значения, которые относятся к экземпляру другого свойства
е—
Необязательная сущность — указывает, что не обязательно будет связанный экземпляр сущности с этой стороны отношения для экземпляра сущности на другой стороне. Это может быть описано как "нуль или большее количество" в противоположность к описанию "одно или большее количество"
1—
Идентифицирующее отношение — ключ сущности с другой стороны отношения мигрирует в эту сущность Необязательное отношение — пунктирная линия с одной стороны отношения указывает, что мигрирующий ключ может быть NULL
132
Моделирование данных Например: Table В
Table A
Один ко многим, обязательное, неидентифицирующее Table A
Table В
ок. Один ко многим, обязательное, идентифицирующее Table A
Table В
Один к одному, необязательное, идентифицирующее
Table A
Table В
Многие ко многим
Атрибуты показываются почти таким же способом, как и при использовании IDEF1X, внутри прямоугольника. Это обозначение не столь ясное, как в IDEF1X, но оно довольно хорошо работает и, вероятно, будет в некоторых документах, с которыми мы встретимся в качестве архитектора данных. Методология IE не всегда в полной мере реализована в каждом программном средстве. Однако круг и "птичьи лапки" обычно реализованы должным образом. Дальнейшие детали этой методологии могут быть найдены в "Information Engineering" (Prentice Hall, ISBN 013464462X (vol. 1), 0134648854 (vol. 2) и 013465501X (vol. 3). Chen ERD (диаграмма Чена отношений между сущностямии) Chen ERD отличается очень немногим, но довольно наглядна. На следующем рисунке мы представляем очень простую диаграмму с показанными основными элементами диаграммы Чена.
Сущность 1
1 и только 1
Сущность 2
1 3 3
Глава 5 Каждая сущность снова представляется прямоугольником; однако атрибуты не показываются на диаграмме, а приложены к сущности в виде кружков. Первичный ключ обозначается двойным подчеркиванием. Мощность отношения обозначена в тексте. В примере это — 1 и только 1 Сущность 1 <название отношения> 0 ко многим Сущность 2. Основная причина использования формата Chen ERD — для разнообразия. По крайней мере, одна методология (Бахмана) реализует атрибуты в этом стиле, когда они не показываются в прямоугольнике. Хотя я и понимаю логику этого (атрибуты являются отдельными объектами), но пришел к выводу, что модели, которые я видел в этом формате, выглядят чрезмерно загроможденными даже для очень маленьких диаграмм. Однако этот формат хорошо работает с логической моделью без использования разных "тайных символов", чтобы описать мощность. Хотя я и не могу сказать, что эта методология не используется в средствах проектирования БД, но я нигде ее не видел, за исключением Microsoft Visio, кроме того, многие из диаграмм, которые вы найдете в Интернете, будут изображены в этом стиле, так что интересно это понять. Дальнейшие детали могут быть найдены BThe Entity Relationship Model — Toward a Unified View of Data, (ACM Transactions on Database Systems, Volume 1, No. 1 (March 1976), pp 9-36). Средства построения диаграмм Microsoft SQL Server В программном средстве Enterprise Manager пакета SQL Server версии 7.0 было добавлено новое довольно изящное средство — средство изображения диаграмм БД. Оно также довольно интересно изображает сущности. Следующий рисунок — пример отношения "один ко многим":
ЕЭ535Я I I Parentld Attribute 1 ____Attribute2
Child
Childld f Attri bute! AtHbute2 Parentld
«Ok:
Первичные ключи идентифицируются небольшим ключиком у атрибута. Отношение обозначается линией между сущностями, на одном конце которой находится ключик, а на другом конце — знак бесконечности. Мы можем показывать сущности в нескольких форматах: так, как только что изображено, или с изображением типов данных всех атрибутов: Parent Column Name V Parentld Attributel Attribute2
j
Data Type int varchar varbinary
^[Leng th 1 Allow Nulls 1 ^ 4 1 50 \/ •V
Ha этом рисунке Column Name — имя столбца, Data Type — тип данных, Length — длина информации в байтах, Allow Nulls — разрешение использования значения NULL. На следующем графике я привел модель всей БД Northwind, используя средство изображения диаграмм, чтобы показать, что оно на самом деле хорошо отображает модель:
134
Моделирование данных
Empolyees ; • __ EmpolyeeD l : : LasN tame FrsitName Ttile Te tliOfCourtesy BrithDate Hre iDate Address Ctiy Regoin Posa tC l ode Country Щ CustornerTypelD •• ; | CustomerDesc
FK_CustomerCustonner...
CustomerCustQre i rDer FjJ CustomerD l CustornerTypeD l
FK_Products_Categoi
Cae tgoryID QuantyiPerUnti UntP i rcie Untsi n IStock UntsiOnOrder ReorderLevel Dsiconntiued FK Order Details Products
Order Detasli OrderD l ProducD tI UntP i rcie Quantyi Dsicount
FK_Order_Detasli_Orders
Customers c i Cu'stomerlD CornpanyName ContactName ContactTitle Address City Region ^ ~ PostalCode Country Phone Fax
3
'f—
Однако, хотя это все и правильно, здесь средство моделирования данных показано не полностью и, вероятно, если будет нужно, не будет использоваться таким образом. Мы включаем методологию моделирования SQL Server потому, что она включена в SQL Server, и понятно, что в некоторых ситуациях это — лучшее средство, которое вы можете использовать. Эта методология — только физический инструмент моделирования, и позволяет использовать все конкретные возможности реализации в SQL Server, включая возможность аннотировать наши таблицы и столбцы описательной информацией. Она является мощным средством для распределения конкретной структуры между разработчиками БД. Так как это довольно специфическая реализация, то если мы решим осуществить отношение с помощью триггера, то в структуре не будет видно, что этот триггер существует. В большинстве случаев это не лучшее средство представления информации конкретного отношения, разработанного для БД, но, если необходимо, дает полезный взгляд на структуру БД.
Описательная информация Мы выделили наши сущности, назначили атрибуты и домены к ним и установили отношения между ними, но сделали не все. Мы обсудили обозначение наших сущностей, атрибутов и даже наших отношений, но даже с правильно построенными названиями все еще, вероятно, будет неясность относительно того, для чего используется каждый атрибут.
135
Глава 5 Нужно добавить комментарии к картинкам в нашей модели. Комментарии позволят потенциальному читателю, и даже вам самим знать, что вы первоначально имели в виду. Помните, что не каждый, кто будет рассматривать модели, которые мы создаем, будет на том же техническом уровне, на котором находятся нереляционные программисты, не говоря уж, конечно, о пользователях или (не технических) менеджерах продукта, которые не имеют никакого опыта моделирования. Мы уже сделали хорошее начало этого процесса в предыдущей главе, где добавляли эту информацию в наш документ, созданный в редакторе Word. Заметим, что как только мы найдем средство моделирования данных, которое будет нам удобно, мы должны иметь возможность обращаться со всеми данными, которые мы вводили в предыдущей главе, и даже больше. Это может оказаться намного быстрее, чем использование документа Word для размещения всей информации и затем передачи ее в модель данных. Для описательной информации не нужен специальный формат; она просто должна быть детализированной, современной и способной ответить на многие вопросы, которые мы, вероятно, будем задавать. Каждый из этих элементов информации должен быть размещен таким образом, чтобы пользователям было легко связать его с той частью модели, где он используется, и должен быть помещен или в документ, или в средство моделирования в виде метаданных. •
Вопросы могут быть следующего типа:
•
Что из себя представляет объект?
•
Как он будет использоваться?
•
Кто может его использовать?
•
Какие дальнейшие планы использования объекта?
Возможности описаний не должны простираться за пределы объектов или сущностей, о которых идет речь. Например, описание сущности должно относиться только к сущности, а не каким-либо связанным сущностям, отношениям или даже атрибутам, если это не является необходимым. Определения атрибута должны говорить только о конкретном атрибуте и о том, каковы могут быть его значения. Поддержание хорошей описательной информации эквивалентно помещению приличных комментариев в коде. Поскольку БД — всегда центральная часть любой компьютерной системы, комментарии на этом уровне более важны, чем в любом другом месте. Например, пусть мы имеем следующие две сущности: contactType contactTypeld name description
contact классифицирует
contactld contactTypeld (FK) name
Здесь contactType — сущность "тип партнера"; contact — сущность "партнер"; contactTypeld — идентификатор типа партнера; contactld — идентификатор партнера; name — имя; description — описание.
136
Моделирование данных Мы могли бы иметь следующий довольно общий набор описательной информации, размещенной, чтобы охарактеризовать атрибуты, которые мы создали: Сущности Contact
Человек, с которым устанавливаются связи в деловых целях Attributes
Описание
Contactld
Первичный ключ, указатель на партнера (contact)
ContactTypeld
Первичный ключ, указатель на тип партнера (contactType)
Name
Полное имя партнера (contact)
ContactType
Домены различных типов партнеров (contact)
Attributes
Описание
ContactTypeld
Первичный ключ, указатель на тип партнера (contactType)
Name
Уникальное имя типа партнера
Description
Описание, как именно можно использовать партнера
Отношения: Имя сущностипредка
Фраза
Имя сущностипотомка
Определение
ContactType
классифицирует
Contact
Классификация типов партнеров. Потребуется при спецификации.
Учебный пример В нашей предыдущей главе мы создали документ, содержащий все объекты, которые нам нужно смоделировать. В этом примере мы просто хотим преобразовать всю информацию, которую помещаем в документ, и поместить ее в набор графических моделей, где это имеет смысл.
Модель действий Начнем с модели действий, потому что это вообще первая модель, которую вы должны создать. В нашем учебном примере интервьюер определил двух исполнителей: основного пользователя счета и клерка бухгалтерского учета. В то время как пользователь счета 137
Глава 5 выполняет действия типа получения информации о счете, взятия денег, вложения депозитов и так далее, клерк бухгалтерского учета должен выполнять задачи типа организации доступа к информации о счете и ввода сделок. Мы можем суммировать исполнителей и действия, которые для нас будут представлять интерес, когда начнем организовывать защиту и писать процедуры, следующим образом: Исполнители: Название
Описание
Основной пользователь счета
Каждый пользователь, который может помещать или брать деньги со счета
Клерк бухгалтерского учета
Пользователь, который может вводить и поддерживать счета
Действия:
138
Название
Описание
Получение информации о счете
Позволяет пользователю получать информацию о счете, такую как название банка, баланс и т. д.
Ввод сделок
Позволяет пользователю записывать произведенные сделки
Ввод чека
Позволяет пользователю оформить чек сделки
Баланс
Позволяет пользователю поддержать баланс прихода и расхода, представляемый банком
Оператор загрузки
Позволяет пользователю загрузить информацию об операциях, совершенных банком
Изъятие денег
Пользователи получают деньги со счета для расходования
Организация депозита
Пользователь помещает деньги на счет
Выписка чека
Пользователь пишет бумажный документ и дает его другому человеку для передачи фондов
Моделирование данных При разработке реальной системы модель действий — очень важная часть проекта. Однако так как наш пример фактически довольно прост, мы не будем слишком тщательно разрабатывать совершенную модель действий. Следующая диаграмма действий — возможный результат анализа, который мы выполнили:
( Информация / \ ^ о счете
Л
/
/
У>-—к-^
/«расширяет»/
/
Ж TV Л\
( Баланс «использует» \ .
)
\
\ \
Основной пользователь счета
\ \
г—л-_^
\
V
\ \
f >/ V
Оператор загрузки
__
^
9
г Л
^
/
X
>
Клерк бухгалтерского учета
\ \
Изъятие денег J
X
\
\
\
(х " " "Ввод чека — " ^ \ Ух.
/
\
\ J\
\
у Организация Л Ч^ депозита J
X
Выписка чека
)
Хотя и далекая от анализа полного процесса, эта модель дает хороший набор действий и исполнителей для нашей небольшой системы текущих счетов. Обратите внимание, что в примере мы вставили «использует» и «расширяет», чтобы сделать его более полным. Баланс использует загрузку сделок (если это возможно), чтобы получить сделки из электронного источника, а ввод чека «расширяет» ввод сделки, потому что чек содержит в значительной степени ту же самую информацию, что и общая сделка, с некоторыми дополнениями. Мы не включили депозиты или прямое изъятие как отдельные действия, чтобы сэкономить место.
139
Глава 5 Это — то, как мы собираемся использовать действия. То, что мы представили здесь — просто задачка, предназначенная специально помочь нам, как архитекторам данных, построить модель, которая объясняет, каковы процессы в нашей системе. Дальнейшие детали могут быть найдены в подробном изложении (полный документ находится в http://www.rational.com/uml) или читая Пьера-Алана Мюллера "Instant UML" (Wrox Press, ISBN 1861000871). Тогда вы можете получить один из нескольких доступных UML-инструментов и начать использовать действия и UML. UML, вероятно, будет использоваться для полного и исчерпывающего понимания и позволит легче взаимодействовать с нереляционными аналитиками, с которыми мы должны иметь дело.
Модель данных Завершив распознавание процессов, мы переходим к основному этапу формирования модели данных — выбору нашей предварительной структуры БД. Первый шаг в моделировании данных — модели сущностей и атрибутов, которые предварительно были выделены. Замечание: В остальной части примера мы обычно предполагаем, что любые определения, которые мы ранее добавили, содержатся в модели. Мы не можем расширить все поля, чтобы показать определения, но будем, однако, пробовать дать определения для всех новых объектов, которые мы создаем. Таким образом, мы просто создаем сущности для каждого из наших объектов, добавляя все атрибуты, выбирая базовые типы доменов следующим образом: \ccount
Bank
Statement
Number: String Balance Date: Datetime Running Total: Number
Name: String
Transaction
DirectWithdrawal
Type: String PreviousBalance: Number PreviousBalanceData: Datet me CurrentBalance: Number StatementDate: Datetime TotalDebits: Number TotalCredits: Number Balancingltems: Number
Date: Datetime Number :String Description: String Amount: Number Type: String
WithdrawalDate: Datetime Number: Number Amount: Money
CheckRegister
Check
Deposit
Payee
Date: Datetime Number: Number Description: String Amount: Money
Name: Number Address: Address PhoneNumber : Phone Number
UsageType: String Number: String DateWritten: Datetime Amount: Money Memo: String Signature: Blob
RegisterType:
Здесь мы используем обозначения, введенные ранее (см., например, главу 4). 140
Моделирование данных Теперь мы имеем все сущности из примера предыдущей главы в гораздо более компактном и легком для сопровождения формате. Следующий шаг — идентифицировать все отношения, где два объекта — по существу одно и то же, и реализовать их как подтипы. Мы имеем один такой вариант в нашем примере: сущности Check (чек), Deposit (депозит) и DirectWithdrawal (прямое изъятие) — все являются только типами сделок (Transaction). Обратите внимание, что мы добавили атрибут типа дискриминатора для подтипов и удалили все атрибуты, которые являются дубликатами тех, которые уже были в сущности Transaction.
Transaction Date: Number Description Amount Type
_Type
Check
Deposit
DirectWithdrawal
UsageType Memo ^Signature
141
Глава 5 Следующим шагом является добавление и задание имен всем отношениям в модели. Мы все еще не добавили первичные ключи к модели, так что на этой стадии не имеем никакой миграции ключей. Это позволяет проще начать реализацию. Фактически мы добавим ключи на следующем шаге. Bank
Statement
Name
Туре PreviousBalance PreviousBalanceData CurrentBalance StatementDate Total Debits TotalCredits \Balancingltems
Account
преобразует фонды вводом
CheckRegister RegisterType
Payee
Type Name Address PhoneNumber
DirectWithdra wal
Deposit
Check /•
1 N
1 1
U s a g e T y p e
получает деньги, переданные спомощью
M e m o
S i g n a t u r e
.
Заметьте, что мы рассматриваем отношение "получатель платежа — чек" как неидентифицирующее отношение. Это связано с тем, что чек не определяется своим получателем платежа, так как получатель платежа — просто атрибут чека. Но получатель, однако, требуется. Заключительный шаг в создании нашей начальной модели должен идентифицировать все естественные уникальные ключи в данных. Например, в сущности account (счета) число, вероятно, будет уникальным значением. В сущности bank (банк) мы имеем название банка, которое должно быть уникально. Transactions (сделки) будут, вероятно, иметь для идентификации уникальные номера. Наши предположения будут иногда правильными, а иногда — не совсем правильными, но они — логические предположения. Как всегда, в любое время вы предполагаете, что должны это проверить, и ваша модель и сопроводительная документация должны всегда иметь признак выполнения проверки.
142
Моделирование данных Например, в сущности transaction (сделка) мы установили атрибут number (номер) как уникальный. На первый взгляд это имеет логический смысл, но поскольку мы продолжаем работать, то (будем надеяться) поймем, что номер сделки уникален только для данного счета. Существует несколько неправильных способов обойти это, например, добавлением номера счета к началу номера каждой сделки (это то, что известно как интеллектуальный, разумный или составной ключ, но его может оказаться трудно быстро расшифровать, поскольку он не выделяется в модели данных). Правильным решением будет добавление первичного ключа сущности account (счет) к вторичному ключу, который мы сформируем. Это будет означать, что наша модель теперь выглядит следующим образом:
Bank
Statement
предлагает Name(AK1.1)
Type PreviousBalance PreviousBalanceData CurrentBalance StatementDate TotalDebits TotalCredits ^Balancing Items
Account
Number (AK1.1) Balance Date Running Total
преобразует фонды вводом
CheckRegister
Transaction Date: Number (AK1.1) Description Amount
RegisterType
Payee Type Name(AK1.1) Address PhoneNumber DirectWithdra wal
Deposit
Check
получает деньги, переданные с помощью
6—1868
1 4 3
Глава 5 В этой модели мы обычно будем использовать в качестве первичного ключа целые числа, причем они не должны быть редактируемые. Это также делает здесь выбор легче. Выбор первичного ключа — в значительной степени вопрос вкуса, но в этой книге мы акцентировали внимание на первичных ключах, ориентированных на указатели. Мы разъясним, почему следует физически реализовывать ключи таким образом, в главе 10. Однако, как мы сказали ранее, не выбрав надлежащих первичных ключей к этому моменту, мы не завершим работу с разрозненной моделью, получаемой от миграции ключей.
Bank
Account Bankld Accountld
Л
предлагает
Statement
Bankld Name(AK1.1)
Number (AK1.1) Balance Date \RunningTotal j
преобразует фонды вводом
/"Bankld (FK) Statements Type PreviousBalance PreviousBalanceData CurrentBalance StatementDate TotalDebits TotalCredits \Balancingltems
Transaction Bankdld (FK) Accountld (FK) Transactionld (FK)
CheckRegister CheckRegisterld RegisterType
Date:
Number (AK1.1) Description Amount Payee Type
Payeeld Name(AK1.1) Address PhoneNumber
DirectWithdra wal
Check Bankdld (FK) Accountld (FK) Transactionld (FK)
'Bankdld (FK) Accountld (FK) Transactionld (FK) Deposit Bankdld (FK) Accountld (FK) Transactionld (FK)
1 4 4
UsageType Memo Signature
получает деньги, переданные с помощью
Моделирование данных Заключительный шаг, который мы должны сделать, — выбрать логические домены для каждого из атрибутов. Идея здесь заключается в том, чтобы попробовать сгруппировать подобные атрибуты в один и тот же домен, как, например, в случае атрибута date (дата) в сущности transaction (сделка), previousBalanceDate (дата предыдущего баланса) в сущности statement (отчет) и balanceDate (дата баланса) в сущности account (счет). Для атрибутов первичного ключа мы зададим домен первичного ключа, и пока оставим их такими. Наша модель теперь выглядит следующим образом: Bank Bankld: PrimaryKey Name: Name (AK1.1)
Bankld: PrimaryKey (FK) Accountld: PrimaryKey Number: AlphaNumericNumber (AK1.1) BalanceDate: Date RunningTotal: Amount преобразует фонды вводом
Bankld: PrimaryKey (FK) Statementld: PrimaryKey Type: Code PreviousBalance: Amount PreviousBalanceDate: Date CurrentBalance: Amount StatementDate: Date TotalDebits: Amount TotalCredits: Amount Balancingltems: Balancingltems
Transaction
Bankdld: PrimaryKey (FK) Accountld: PrimaryKey (FK) Transactionld: PrimaryKey Date: Date Number: AlphaNumericNumber (AK1.1) Description: Description Amount: Amount Type: Code
CheckRegister CheckRegisterld: PrimaryKey RegisterType: Code
Payee Payeeld: PrimaryKey
Type
Name: Name (AK1.1) Address: Address PhoneNumber: PhoneNumber DirectWithdra wal Bankdld: PrimaryKey (FK) Accountld: PrimaryKey (FK) Transactionld: PrimaryKey (FK) Deposit Bankdld: PrimaryKey (FK) Accountld: PrimaryKey (FK) Transactionld: PrimaryKey (FK)
Check Bankdld: PrimaryKey (FK) Accountld: PrimaryKey (FK) Transactionld: PrimaryKey (FK) Payeeld: PrimaryKey (FK) UsageType: Code Signature: Blob
получает деньги, переданные с помощью
Будем дальше назначать домены каждому из атрибутов, которые создадим, продолжая стадию логического моделирования. В данный момент мы определили и использовали следующие домены: Address (адрес)
Включает все адреса
AlphaNumericNumber (алфавитно-цифровой номер)
Величина, на которую обычно ссылаются как на число, но фактически может содержать алфавитные и цифровые данные
Amount (сумма)
Денежная величина
Balancingltems (элементы баланса)
Группа элементов, необходимых для баланса счета Продолжение таблицы на следующей странице 145
Глава 5
Blob
Рисунки, документы в двоичном формате (BLOB — Binary Large Object)
Code (код)
Величина, используемая как вид текстового указателя
Date (дата)
Значение даты
Name (имя)
Удобочитаемый признак, который назначается экземпляру сущности для легкого доступа
PhoneNumber (номер телефона)
Полный номер телефона
PrimaryKey (первичный ключ)
Используется как указатель к экземпляру сущности
На данном этапе мы не будем делать большего в терминах назначения предикатов этим доменам, так как сделаем несколько изменений в модели в ряде следующих глав, и несомненно обнаружим большее количество доменов (а несколько удалим). Мы, однако, не будем отставать с процессом добавления доменов всякий раз, когда создаем новый атрибут, поскольку наша модель становится все более зрелой. Как вы можете видеть, модель становится немного больше, а также начинает проявлять признаки реализуемой структуры. Мы будем делать много изменений в структуре в последующих главах, но заключительная структура будет все еще несколько напоминать то, что мы имеем сейчас. На данном этапе мы будем использовать определения наших атрибутов, которые собрали ранее. В качестве примера возьмем сущность statement (отчет): Statement (отчет)
146
Представляет документ (бумажный или электронный), который поступает из банка раз в календарный месяц, и который сообщает нам все, что думает банк о наших тратах
Атрибуты
Описание
Bankld (код банка)
Идентификатор первичного ключа банка
Statementld (код отчета)
Идентификатор первичного ключа экземпляра отчета
Туре (тип)
Определяет тип отчета, полученного из банка
PreviousBalance (предыдущий баланс)
Определяет, какой предполагался баланс после последнего балансового отчета
PreviousBalanceDate (дата предыдущего баланса)
Определяет дату составления последнего баланса по счету
CurrentBalance (текущий баланс)
Определяет баланс счета после того, как все элементы отчета были сформированы
StatementDate (дата отчета)
Определяет дату, когда сформирован отчет
TotalCredits (общий кредит)
Сумма всех элементов, которые добавляют деньги на счет в период, охватываемый отчетом
TotalDebits (общий дебет)
Сумма всех элементов, которые изымают деньги со счета в период, охватываемый отчетом
Balancingltems (элементы баланса)
Все элементы, которые выполнил банк, и о которых он теперь оповещает нас
Моделирование данных
Резюме В этой главе рассмотрен текстовый проект, который получен в предыдущей главе, и помещен в более краткий графический формат. Здесь представлены два конкретных типа моделирования, а именно, модели данных UML и IDEF1X. Мы кратко рассмотрели UML, и особенно использование действий, которые, прежде всего, позволяют нам применять диаграммы действий для моделирования процессов, которые мы, без сомнения, обнаружим на стадии проектирования БД. При моделировании данных мы прежде всего сосредоточились на методологии моделирования IDEF1X, детально рассмотрев символику, которая нам понадобится в процессе проектирования. Основной набор выделенных символов позволит нам в деталях полностью моделировать БД сначала на логической, а затем и на физической стадии проектирования. Последний момент, который мы отметили, это то, что для каждой сущности и каждого атрибута, которые мы выделили во время процесса моделирования, нам нужно, по крайней мере, идентифицировать следующие детальные свойства: Свойство
Цель
Name (название)
Название, которое полностью определяет цель сущности или атрибута
Description (описание)
Полное контекстное объяснение назначения атрибута для программиста, а также и пользователя
Predicates (Domain) (предикаты (домен))
Обычно предикаты определяют ЛЮБЫЕ правила, которые управляют данными при вводе или выводе их из нашей БД. Обычно достаточно просто установить домен (единственный предикат столбца, основанный на ограничениях и типах данных на стадии физического моделирования) и задокументировать другие предикаты на уровне БД или таблицы
Мы будем использовать эту описательную информацию позже в процессе построения наших проверок ограничений, построения триггеров и т. д., чтобы защитить и использовать данные как в надежном хранилище. Таким образом, пользователь может обеспечить конфиденциальность того, что они представляют, и гарантировать, что они будут действительно тем, что должно быть размещено в БД. Рассмотрев символику, которая нужна для моделирования нашей БД, мы теперь должны рассмотреть нормализацию нашего проекта, что и сделаем в следующих главах.
147
Методы нормализации Независимо от того, какую СУБД вы разрабатываете, нормализация — жизненно важный процесс, который должен быть выполнен, чтобы гарантировать, что БД вашей системы является и точной, и не содержит дублированной информации. Нормализация может быть суммирована в единственном предложении: "Каждая сущность в вашей БД должна иметь единственную тему". (http://www.gslis.utexas.edu/-l384k11w/normstep.htnnL) Различные уровни нормализации БД указывают, как хорошо структура придерживается признанных стандартов проектирования БД, хотя после третьего уровня имеются некоторые разногласия среди разработчиков БД относительно того, насколько полезны дальнейшие уровни. Мы рассмотрим эти аргументы в следующей главе. Как вы могли бы предположить, структура БД — одна из наиболее важных тем для архитектора БД. Разногласия часто возникают между архитекторами БД и разработчиками клиентской части относительно того, как хранить данные. Почему? Потому что никакие два проекта БД не похожи друг на друга, и всегда имеется несколько правильных способов структурировать БД. В этой и следующей главах мы рассмотрим основные принципы, которые помогут вам избежать многих, многих путей, которые приводят к плохим структурам БД. Базу данных для обработки транзакций в реальном масштабе времени (OLTP), которая не была нормализована, вообще-то создать вначале для клиента быстрее, так как в ней намного меньше таблиц. Однако это преимущество скоро исчезнет, когда потребуются незначительные изменения в работающей системе. Опытные проектировщики БД понимают, что изменения в структуре данных требуют больших затрат. Когда нет никаких записей в таблице и никакого кода для доступа к таблице, изменения структуры можно выполнить
Глава 6 просто за минуты. Когда мы добавили сотню записей в таблицу, причем семь из них имеют по десять записей в связанных таблицах, а программисты написали сотни строк кода, мы можем легко потратить час полезного времени, изменяя структуру таблицы. Если имеется миллион записей в таблице, забудьте об этом. Как только мы изменили структуру таблицы, нужно также изменить и весь код для доступа к таблице. Нормализация будет также экономить много места для размещения данных, поскольку главная цель состоит в том, чтобы избежать повторения данных. Процесс нормализации также гарантирует последовательные структуры сущностей, которые помогут нам избежать изменения наших существующих сущностей в будущем. Если мы проделали приличную работу на стадии проектирования, связанной со сбором данных, процесс нормализации должен устранить большинство сомнительных областей в БД. Естественно, я не говорю, что, как только вы создали проект, нельзя изменять его, поскольку расширения и изменения в деловой практике могут вызвать много необходимых поправок. Однако более легко добавить новую информацию и новые сущности в полностью нормализованную БД. Эта и следующая главы посвящены признанным методам нормализации, и вы увидите, что результаты нормализации минимизируют наличие дублированной информации в ваших БД и сделают их более легкими для управления и изменения, а также более безопасными — хотя это создаст намного больше сущностей.
Почему нужна нормализация? Есть много причин для нормализации структур данных.
Уменьшение значений NULL Значения NULL могут быть очень вредны для целостности данных и производительности, они могут приводить к неприятностям при формировании запросов к БД. Однако значения NULL интересны сами по себе; много можно говорить о том, как SQL Server 2000 обращается с ними и как можно избежать их или минимизировать их воздействие в ваших БД. Но чтобы не отвлекаться здесь, правила обработки NULL будут рассмотрены более глубоко в главе 12. Однако процесс нормализации может привести к уменьшению количества значений NULL, содержащихся в вашей БД.
Устранение избыточных данных Любая редактируемая часть данных, которая не является первичным ключом таблицы (или частью ключа), но является внешним ключом (или частью его) и появляется более чем один раз в БД, потенциально может вызвать ошибку. Конечно, мы видели это раньше, если имя человека размещено в двух местах, и одно из них изменяется, а другое — нет, то неожиданно мы имеем два имени там, где прежде было только одно. Проблема с хранением избыточных данных будет очевидна для любого, кто менял свой адрес. Каждая государственная власть требует, чтобы вы лично изменили информацию об адресе в налоговых формах, водительских правах, регистрации автомобиля и т. д., вместо того, чтобы сделать централизованно одно изменение.
150
Методы нормализации Устранение ненужного кодирования Для управления ненормализованными данными может потребоваться дополнительное программирование в триггерах, хранимых процедурах, или даже в логических цепочках бизнеса, и это, в свою очередь, может существенно повредить работе. Не говоря уж о том, что дополнительное кодирование увеличивает шанс возникновения новых ошибок в лабиринте кода, который требуется, чтобы обслуживать избыточные данные. Много проектов БД терпят неудачу из-за чрезмерного требования синхронно хранить избыточные данные.
Максимизация кластерных индексов Кластерные индексы используются для задания исходного порядка таблицы в SQL Server. Это специальные индексы, в которых физическое хранение данных соответствует порядку индексированных полей, который позволяет лучше реализовывать запрос, использующий этот индекс. Как правило, они используются, чтобы организовать таблицу удобным образом для увеличения производительности. Каждая таблица может иметь только один кластерный индекс. Чем больше кластерных индексов в вашей БД, тем меньше сортировки вам, вероятно, придется делать, и более вероятно, что в запросах можно будет использовать MERGE JOIN (соединение слиянием) — специальный тип очень быстрой операции соединения, которая требует отсортированных данных. Сортировка — очень дорогостоящее действие, которого нужно избегать, если возможно. Кластерные индексы и индексы вообще будут детально рассмотрены в главах 10 и 14.
Уменьшение числа индексов на таблицу Чем меньше индексов на таблицу, тем меньше страниц нужно перелистать при корректировке таблицы или добавлении в нее данных. Под страницами мы понимаем не то, что называется Web-страницами, а концепцию страниц в SQL Server. В SQL Server данные и индексы разбиты и размещены на страницах размером в 8К. Конечно, SQL Server не хранит в любой момент всю БД в памяти. Что он делает, так это держит "снимок" того, что используется в настоящее время. Чтобы сохранять иллюзию наличия всей БД в памяти, SQL Server вводит и выводит страницы на высокой скорости, чтобы добраться до нужной области памяти, когда это требуется, однако эта область может содержать только ограниченное число страниц в любой момент. Страницы поэтому вводятся в оперативную память и выводятся из нее по принципу, что те, к которым наиболее часто обращаются, остаются в памяти. Операция перемещения страниц является дорогостоящей с точки зрения производительности. Поэтому, чтобы обеспечить максимальную производительность, вам нужно перемещать как можно меньше страниц. Когда таблица имеет много столбцов, вам может понадобиться несколько индексов для таблицы, чтобы избежать проблем с реализацией поиска. В то время как эти индексы могут дать существенное улучшение поиска, само поддержание индексов может быть очень дорогостоящим. Индексы — очень хитрая тема, потому что они обладают и положительным, и отрицательным воздействием на производительность и требуют четкого баланса для оптимального использования.
Хранение "тонких" таблиц Когда мы называем таблицу "тонкой", то подразумеваем, что в ней имеется относительно небольшое число столбцов. Более "тонкие" таблицы подразумевают, что большее количество данных размещается на данной странице в вашей БД, таким образом позволяя серверу БД восстановить большее количество строк таблицы за одно чтение, чем в противном случае. Все это означает, что в системе будет большее количество таблиц, когда мы закончим 151
Глава 6 нормализацию. Имеется, однако, предел здравого смысла, и я не рекомендую иметь таблицы с одним столбцом. Имейте также в виду, что в типичной OLTP-системе обычно используется немного столбцов при каждом изменении данных, и часто используются запросы, чтобы получить в итоге единственное значение, подобно балансу счета. Для этих видов запросов требуется высокая производительность.
Процесс нормализации Если мы вспомним одно наше описание нормализации из первого раздела данной главы — "Каждая таблица в вашей БД должна иметь единственную тему" — то это может подразумевать, что каждая таблица должна стараться представлять единственную сущность. Данная концепция станет совершенно очевидной к концу следующих двух глав, когда мы просмотрим процесс нормализации. Нормализация — это процесс. Имеются семь следующих широко используемых типов нормализации: •
Первая нормальная форма (1НФ)
•
Вторая нормальная форма (2НФ)
•
Третья нормальная форма (ЗНФ)
•
Нормальная форма Бойса-Кодда (НФБК)
•
Четвертая нормальная форма (4НФ)
•
Пятая нормальная форма (5НФ)
•
Нормальная форма домен-ключ (НФДК)
В 1972 г. Е. Ф. Кодд представил миру Первую нормальную форму (1НФ), основанную на форме данных, и Вторую и Третью нормальные формы (2НФ и ЗНФ), основанные на функциональных зависимостях данных. Они были далее улучшены Кодд ом и Бойсом в Нормальной форме Бойса-Кодда (НФБК). Данная глава детально охватывает эти четыре формы. Во время обсуждения нормализации в этой и следующей главах мы пройдем через каждую из указанных выше форм, рассматривая, как устранить все нарушения, которые мы выявим с помощью соответствующих правил, сформулированных для каждой формы. Мы могли бы решить проигнорировать некоторые из нарушений для целесообразности, и мы рассмотрим это по пути. Важно также не только прочитать о каждой форме в книге, но и рассмотреть каждую форму в контексте логического моделирования. Когда мы дойдем до конца следующей главы, должно быть ясно, что ЗНФ — не волшебная форма, как полагают некоторые архитекторы БД.
Первая нормальная форма Первая нормальная форма (1НФ) связана с формой атрибутов и записей. Она наиболее важная из всех нормальных форм. Эта форма также используется в определении реляционной модели БД, и определение 1НФ — одно из двенадцати правил Кодда, которое фактически связано не с нормализацией, а скорее с набором правил, которые определяют реляционную БД. Эти правила перечислены и обсуждены в приложении А.
152
Методы нормализации Сущности в 1НФ будут иметь следующие характеристики: •
Все атрибуты должны быть элементарными, то есть, иметь только одно единственное значение, представленное в единственном атрибуте единственного экземпляра сущности.
•
Все экземпляры в таблице должны содержать одно и то же число значений.
•
Все экземпляры в таблице должны быть различны.
Нарушения 1НФ проявляются в физических моделях при работе с беспорядочными данными, что мы вкратце рассмотрим.
Все атрибуты должны быть элементарными Атрибут может представлять только одно единственное значение, он не может быть группой. Это означает, что не может быть никаких массивов, никаких разграниченных данных, и никаких атрибутов с несколькими значениями, размещенными в поле. Перефразируя это, значения, помещенные в атрибут, не могут быть раздроблены на меньшие части. В качестве примера мы рассмотрим некоторые общие нарушения этого правила 1НФ. Поля адреса электронной почты В сообщениях электронной почты адрес обычно помещается в следующей форме: [email protected];[email protected];[email protected]. Это явное нарушение 1НФ, так как мы пытаемся разместить более одного адреса в одном поле электронной почты. Каждый адрес электронной почты должен формировать отдельное поле. Имена Рассмотрим полное имя "Джон К Паблик". Это — очевидная проблема, поскольку мы имеем имя, инициал отчества и фамилию в единственном поле. Если разбить полное имя на три части, то получим поля f i r s t _ n a m e (имя), m i d d l e _ i n i t i a l (инициал отчества) (или middle_name (отчество), которое я предпочитаю), и last_name (фамилия). Это обычно хорошо, так как полное имя человека в США обычно имеет три части. В некоторых ситуациях это может быть и недостаточно, и вы можете не знать число частей, пока пользователь не введет данные. И тем не менее вы должны знать требования к данным. Номера телефона Рассмотрим случай номера телефона. Американские номера телефона имеют форму "1-423-555-1212" плюс некоторое возможное число расширения. Из наших предыдущих примеров вы можете видеть, что имеется несколько полей, вложенных в этот номер телефона, не говоря уж о возможных расширениях. Кроме того, часто нужно хранить в БД не только американские номера телефона. Решение относительно того, как обращаться с этой ситуацией, может быть полностью основано на том, как часто вы храните международные телефонные номера, поскольку могло бы быть невозможно (могло бы быть — я не пробовал, так что не могу быть уверенным на все сто процентов) создать таблицу или набор сущностей, чтобы управлять любой ситуацией.
153
Глава 6 Так, для американского стиля номера телефона нам потребовалось бы пять полей для каждой из следующих частей: C-AAA-EEE-NNNN-XXXX •
•
(С) Код страны — это то, что мы набираем для номеров, которые находятся не в пределах кода региона, и обращений к телефону, когда мы набираем не местный номер. (ААА) Код региона — указывает вызов региона, который расположен в пределах штата. (ЕЕЕ) Телефонная станция — указывает набор чисел в пределах кода региона
•
(NNNN) Номер — число, уникальное для индивидуальных телефонных номеров.
•
(ХХХХ) Расширение — число, которое должно быть вызвано однажды, когда вы соединяетесь, используя предыдущие номера.
•
Адреса К данному моменту вы должны знать, что весь адрес должен быть разбит на поля для размещения уличного адреса, города, штата и почтового кода (здесь и далее мы игнорируем для краткости международный фактор). Однако уличный адрес должен также быть разбит в большинстве случаев на номер дома, название улицы, номер квартиры и почтовый ящик. Мы кратко упомянем уличные адреса далее еще раз. /Р-адреса IP адреса — очень интересный случай, потому что они состоят из четырех частей данных, формируемых в виде BY1.BY2.BY3.BY4, где BY типа byte (байт). Он представляет четыре различных атрибута, но фактически является единственным значением целого числа без знака, получаемого по математической формуле (BY1 * 256?) + (BY2 * 2562) + (BY3 * 2561) + + (BY4 * 256°). Следовательно, если вы имеете IP-адрес 24.32.1.128, это могло бы быть размещено как 404750720 - (24 * 2563) + (32 * 2562) + (1 * 2561) + (128). Как вы фактически храните это значение, будет зависеть в большой степени от того, что вы будете делать с данными. Например: •
Если вы имеете дело с IP-адресом как четырьмя различными значениями, вы могли бы хранить это в четырех различных полях, возможно названныхipAddressPartl (1-я часть IP-адреса), ipAddressPart2 (2-я часть IP-адреса), ipAddressPart3 (3-я часть IP-адреса) и ipAddressPart4 (4-я часть IP-адреса). Обратите внимание, что, так как здесь всегда будет ровно четыре части, этот тип хранения не нарушает правила 1НФ, что все экземпляры сущности должны содержать одно и то же число значений. Это рассматривается в следующем разделе.
•
Одна из причин хранить IP-адрес как единственную величину — проверка диапазона. Если вы имеете один IP-адрес и должны выяснить, когда он находится между двумя другими IP-адресами, хранение адреса как целого числа позволяет вам просто искать данные с помощью оператора типа: "where ipAddressCheck is between ipAddressLow and ipAddressHigh", где ipAddressCheck — проверяемый IP-адрес, ipAddressLow — нижняя граница IP-адреса, ipAddressHigh — верхняя граница IP-адреса.
Перейдем теперь ко второму правилу 1НФ.
154
Методы нормализации Экземпляры сущности должны содержать одно и то же число значений Лучше это проиллюстрировать на небольшом примере. Если у вас есть сущность, которая хранит имя человека, то, если одна строка содержит одну составляющую имени, то и все строки должны иметь только одну составляющую. Если они могут иметь две составляющие, то все экземпляры должны иметь возможность хранить две составляющие. Если же они могут иметь различное число составляющих имени, мы должны с ними работать по-разному. Пример нарушения этого правила 1НФ можно найти в сущностях, которые имеют несколько полей с одним и тем же основным именем и суффиксом (или префиксом), представляющим собой число, типа a d d r e s s _ l i n e _ l (адресная шина 1), a d d r e s s _ l i n e _ 2 (адресная шина 2) и т. д. Обычно это — попытка допустить многократные значения для единственного поля сущности. В редких случаях, когда используется всегда одно и то же число значений, формально нет никакого нарушения 1НФ. Но даже в таких случаях это обычно все же не лучшее решение проекта, поскольку пользователи часто могут изменить их назначение. Чтобы преодолеть все это, мы могли бы создать сущность-потомок, чтобы разместить плохо сформированные значения исходной сущности. Это также позволит нам иметь фактически неограниченное число значений там, где предыдущее решение имело конечное (и небольшое) число возможных значений. Одна из проблем, с которой вы при этом столкнетесь, это то, что записи-потомки, которые вы создаете, будут требовать упорядочивающей информации, которая обеспечивает соответствующую организацию для использования фактических строк. Обратите внимание, что фактическая реализация адресов будет, конечно, основана на требованиях создаваемой системы. Мы можем использовать правила мощности, как описано в предыдущей главе, чтобы ограничить число возможных значений. Если мы должны ввести ограничение, потому что наша модель утверждает, что нам нужно максимум два потомка и минимум один, мощность обеспечит механизм для этого.
Все экземпляры записей сущности должны быть различны Это кажется очевидным, но об этом все-таки следует сказать. В основном, это указывает, что каждая сущность в 1НФ должна иметь первичный (или уникальный) ключ. Имейте в виду, однако, что только добавление искусственного ключа к сущности может фактически реализовать предписание этого правила, но, конечно, это не самоцель. В следующей главе мы расширим эту идею.
Программирование аномалий, которых следует избегать в 1НФ Нарушения 1НФ очевидны и часто очень неудобны, если к столбцам, на которые они воздействуют, часто обращаются. Следующие примеры описывают некоторые из ситуаций, которых мы можем избежать, помещая наши сущности в 1НФ. Коррекция списков в отдельном поле Большая проблема, связанная с нарушениями 1НФ, определяется тем, что ни реляционная теория, ни SQL не могут работать с неэлементарными полями. Рассмотрим наш предыдущий пример атрибута адресов электронной почты. Предположим, что мы имеем таблицу, названную person (человек) со следующей схемой: 155
Глава 6 CREATE TABLE person personld int NOT NULL IDENTITY, name varchar (100) NOT NULL, e-mailAddress varchar(1000) NOT NULL
Здесь создается таблица person (человек), содержащая три атрибута: personld — идентификатор записи, целого типа, не может иметь значение NULL, является первичным ключом; name — имя человека, типа строки до 100 символов, не может иметь значение NULL; e-mailAddress — адрес электронной почты, типа строки до 1000 символов, не может иметь значение NULL. Если мы позволим нашим пользователям иметь больше одного адреса электронной почты и хранить это в атрибуте адреса электронной почты, наше поле электронной почты могло бы выглядеть так: "[email protected]; ldavidson@e-mail. com". Рассмотрим также, что многие различные пользователи БД могут использовать адрес электронной почты Davidsons@d. com. Если нам нужно изменить адрес электронной почты с [email protected] на [email protected], нам бы потребовался код, подобный следующему, для каждого человека, который использует адрес этой электронной почты: UPDATE person SET e-maleAddress = replace(e-maleAddresses, '[email protected]') WHERE e-mailAddress l i k e '%[email protected]%'
'[email protected]',
Этот код не выглядит неприятным, но что сказать относительно случая, когда имеется также и адрес электронной почты theDavidsons@d. com? Что, если мы не хотим изменять это значение? Так как код, который вы пишете, должен работать во всех случаях, это было бы недопустимо, потому что предшествующий оператор UPDATE будет иметь нежелательные побочные эффекты в таких случаях. Коррекция полей с несколькими составляющими Логика программирования, которая требуется для изменения части поля с несколькими составляющими, может быть очень запутанной. Возьмем, например, случай телефонных кодов регионов. В Соединенных Штатах мы имеем большее количество телефонов, пейджеров, телефонных ячеек и т. д., чем то, о котором создатели системы кода региона когда-либо думали, и поэтому они часто изменяют их или вводят новые коды регионов. Если все значения номера телефона размещены в элементарных контейнерах, обновление кода региона потребовало бы единственный, легко реализуемый короткий SQL-оператор, наподобие следующего:
I
UPDATE phoneNumber SET areaCode - '423' WHERE areaCode = '615' AND exchange IN ('232','323'
, ..., '989')
В противном случае мы получим следующее:
I
UPDATE phoneNumber SET phoneNumber = REPLACE(phoneNumber, '-615-','-423-') WHERE phoneNumber LIKE '_-615' AND s u b s t r i n g ( p h o n e N u m b e r , 7 , 3 ) I N( ' 2 3 2 ' , ' 3 2 3 ' , ...,
Заметьте, что substring() 156
'989')
— расширение в T-SQL и не входит в стандарт SQL.
Методы нормализации Этот пример требует для правильной работы искусного форматирования телефонных данных, а это редко бывает без увеличенного количества кода. Все это при использовании простого формата удовлетворило бы всем проблемам. Коррекция записей с переменным числом фактов Одна из главных проблем, связанная с разрешением переменного числа фактов в данной записи, связана с различными ситуациями, которые возникнут, когда вы будете работать с одним из полей вместо другого. Пусть мы имеем хорошо структурированную таблицу типа: CREATE TABLE payments ( paymentsld int NOT NULL IDENTITY, accountld int NOT NULL, paymentl money NOT NULL, payment2 money NULL )
Здесь создается таблица с именем payments (оплаты), содержащая четыре атрибута: paymentld — идентификатор записи в таблице, целого типа, не может иметь значение NULL и является первичным ключом; accountld — идентификатор счета, с которым связана запись, также целого типа, не может иметь значение NULL; paymentl — величина оплаты (основная), денежного типа, не может иметь значение NULL; payment2 — величина оплаты (дополнительная), денежного типа, может иметь значение NULL. Когда пользователь должен делать два платежа (по каким-либо причинам), следующий отрывок кода показывает, что потребуется, чтобы ввести оплату: UPDATE payments SET paymentl = case WHEN paymentl IS THEN 1000.00 payment2 - case WHEN paymentl IS THEN 1000.00 WHERE accountld = 1
NULL ELSE paymentl END, NOT NULL AND payment2 IS NULL ELSE payment2 END
Конечно, вряд ли точно так будет решаться задача, но даже если эта логика выполнена на стороне клиента, формируя различные блоки произведенных или предполагаемых платежей, все-таки ясно, что такое использование является проблематичным. Альтернативой могла бы быть таблица следующего вида: CREATE TABLE payment ( paymentld int NOT NULL IDENTITY accountld int NOT NULL, — внешний ключ к таблице account date datetime NOT NULL, amount money )
Здесь создается таблица с именем payment (оплата), содержащая четыре атрибута: paymentld — идентификатор записи в таблице, целого типа, не может иметь значение NULL и является первичным ключом; accountld — идентификатор счета, с которым связана запись, также целого типа, не может иметь значение NULL; date — дата оплаты, не может иметь значение NULL; amount — величина оплаты, денежного типа, не может иметь значение NULL. 157
Глава 6 Тогда добавление платежа было бы просто добавлением новой записи в таблицу payment: I
INSERT payment ( a c c o u n t l d , VALUES ( 1 , ' J u n e 12 2 0 0 0 ' ,
date, value) $300.00)
Это — конечно лучшее решение. Одна из главных проблем с этим примером — это то, что дата не является элементарным типом данных (другими словами, она сама — поле с несколькими величинами). Фактически, SQL Server просто имеет тип данных date time (дата-время), который действительно представляет несколько полей, свернутых в одно поле: день, месяц, год, час, минута, секунды, миллисекунды, и т.д. Тип данных datetime — одна из раздражающих особенностей SQL Server, который мы часто должны использовать по причинам удобства, но так как он хранит несколько значений, то фактически не соответствует 1НФ. Имеется масса функций в SQL Server, разработанных, чтобы восполнить факт, что d a t e t i m e (и smalldatetime) — не реляционные типы данных БД. Есть ситуации, где благоразумно реализовать наши собственные типы даты и времени, определяемые пользователем, и работать с переменными времени в разумной манере.
Признаки, что существующие данные не удовлетворяют 1НФ Далее мы собираемся посмотреть, как можно распознать, являются ли данные в вашей БД соответствующими 1НФ или нет. Данные, которые содержат неалфавитные символы Под неалфавитными символами мы подразумеваем запятые, скобки, круглые скобки, символы конвейеризации и т. д. Они являются предупреждением, что мы имеем дело с полем, содержащим несколько величин. Однако будьте осторожны и не зарывайтесь. Например, если мы решили иметь блок текста, мы, возможно, все хорошо нормализовали, если имеем сущность word (слово), сущность sentence (предложение) и сущность paragraph (абзац). Этот признак более применим к сущностям, которые содержат списки с разделителями.
Имена полей с числами в конце Как мы заметили, очевидным примером этого было бы наличие сущностей с именами атрибутов childl (первый потомок), child2 (второй потомок) и т. д. или, что я предпочитаю, UserDefinedl (первый определенный пользователем атрибут), UserDefined2 (второй определенный пользователем атрибут) и т. д. Очень вероятно, что это неправильно. Это — довольно общий пережиток со времен БД с однородным файлом. Доступ к данным в нескольких таблицах был дорогостоящим, так что много полей помещалось в единственную таблицу. Но для реляционных СУБД это очень вредно.
Вторая нормальная форма Следующие три нормальные формы, которые мы рассмотрим, связаны с отношениями между атрибутами в сущности и ключом в той же сущности, и они минимизируют неподходящие функциональные зависимости. Мы увидим, что некоторые виды отношений между ключом и другими полями нежелательны и должны быть устранены. Как было отмечено в главе 3, функциональная зависимость подразумевает, что если мы используем функцию с одним значением (назовем его значением 1) и результат этой функции всегда точно то же самое значение (назовем его значением 2), тогда значение 2 функционально зависит от значения 1. 158
Методы нормализации Например, рассмотрим следующую ситуацию. Мы имеем три параметра: имя владельца, тип изделия и серийный номер. Серийные номера подразумевают конкретный тип изделия, так что они функционально зависимы. Если мы изменим тип изделия, но не будем изменять серийный номер, то наш серийный номер и тип изделия больше не будут соответствовать друг другу, и три рассматриваемых параметра больше не будут обозначать что-то одно. Пользователь не будет понимать, какое изделие имеет в виду владелец, — обозначенное серийным номером или типом изделия. Следующее высказывание поможет вам понять, чем занимаются Вторая нормальная форма (2НФ), Третья нормальная форма (ЗНФ), и Нормальная форма Бойса-Кодда (НФБК). Неключевые поля должны конкретизировать кроме ключа.
ключ записи, весь ключ, и ничего
Это означает, что неключевые поля должны далее описывать ключ записи и не описывать никакие другие атрибуты. Начнем со Второй нормальной формы (2НФ), которая является, по-видимому, самой простой из этих нормальных форм. Сущность, соответствующая 2НФ, должна иметь следующие характеристики: •
Сущность должна соответствовать 1НФ.
•
Каждый атрибут должен описывать весь ключ.
2НФ имеет смысл только тогда, когда сущность имеет составной ключ (ключ, состоящий из двух или большего числа столбцов).
Сущность должна соответствовать 1НФ Очень важно выполнить все шаги процесса нормализации, чтобы исключить проблемы в ваших данных. Будет трудно выделить проблемы 2НФ, если у вас все еще есть проблемы с 1НФ.
Каждый неключевой атрибут должен описывать весь ключ Это означает, что каждый неключевой атрибут должен описывать сущность, определяемую всеми атрибутами ключа, а не просто их частью. Если это не так, и любой из неключевых атрибутов функционально зависит от подмножества атрибутов ключа, то вы имеете шанс получить неприятности при корректировке данных. Например, рассмотрим следующую структуру: book author author_social_security_number bookJSBNjnumber royalty_percentage book_title author_fir st_name author_second_name Здесь book_author — сущность "автор книги"; author_social_security_number номер социального страхования автора, book_ISBN_number — ISBN-номер книги, royalty_persent age — процент оплаты, book_title — название книги, author_first_name — имя автора, author_second_name — фамилия автора.
—
159
Глава 6 Атрибут book_ISBN_number однозначно определяет книгу, а a u t h o r _ s o c i a l _ s e c u r i t y _ n u m b e r однозначно определяет автора. Следовательно, эти два столбца создают один ключ, который однозначно определяет автора конкретной книги. Проблема с другими атрибутами. Атрибут r o y a l t y _ p e r c e n t a g e определяет платеж, который автор получает за книгу, так что он связан со всем ключом. Атрибут b o o k _ t i t l e описывает книгу, но не описывает автора вообще. То же самое происходит с полями author_f irst_name и author_second_name. Они описывают автора, но никак не книгу. Это — яркий пример функциональной зависимости. Для каждого значения, которое имеется в столбце book_ISBN_number, вы должны иметь одно и то же название книги и автора. Но для каждого book_ISBN_number вы не обязаны иметь одно и то же значение r o y a l t y _ p e r c e n t a g e — оно фактически зависит и от автора, и от книжного договора, а не только от одного или другого. Следовательно, у нас есть проблемы, и мы должны создать три отдельных сущности, чтобы хранить эти данные: book book_ISBN_number book author author_social_security_number(FK) book_ISBN_number (FK) royalty percentage
book_title —
Г
m
author author_social_security_number authorjir st_name author_second_name
Здесь book_author — сущность "книга-автор", book — сущность "книга", author — сущность "автор". После того, как мы разобьем сущности, видно, что атрибут royalty_percentage все еще описывает автора, написавшего данную книгу, b o o k _ t i t l e теперь описывает сущность, определяемую ключом book_ISBN_number, а атрибуты, связанные с именем автора, описывают сущность author, определяемую ключом author_social__security_number. Обратите внимание, что отношение book — book_author является идентифицирующим отношением. Нарушения 2НФ часто логически моделируются идентифицирующими отношениями, где первичный ключ новой сущности мигрирует к сущности, в которой возникла первоначальная проблема. Наша предыдущая иллюстрация демонстрирует эту концепцию весьма xoponio. В исправленном примере мы изолировали функциональные зависимости так, что атрибуты, которые функционально зависят от другого атрибута, фактически функционально зависят от ключа. Так как здесь нет никаких столбцов, которые функционально зависят от части ключа, эти сущности соответствуют 2НФ.
160
Методы нормализации Проблемы программирования, которых можно избежать с помощью 2НФ Все проблемы программирования, которые возникают с 2НФ, а также с ЗНФ и НФБК, связаны с функциональными зависимостями. Проблема программирования весьма проста. Если вы выполните оператор, подобный приведенному ниже, над нашей таблицей из примера:
I
UPDATE book_author SET b o o k _ t i t l e - 'Database Design' WHERE book_ISBN_number = '923490328948039'
и измените более одной строки, то могут возникнуть проблемы с вашими структурами. Основная проблема здесь в том, что многие программисты не думают о названиях своих проектов БД, когда производят в большом количестве приложения, так что мы создадим таблицы с экранами клиента, подобно этому: Book Author Edit -Author Information SSN
|555-55-5555 First Name IFred Last Name
CII'IEI] Cancel
jSmith
Book IrtformationISBN
I.923490328948039
Title
(Database Design
Royalty Percentage
35
Здесь в дополнение к предыдущим обозначениям Author авторе, Book Information — информация о книге.
Information
— информация об
Рассмотрим, что произойдет, если мы используем этот экран, чтобы изменить название книги с несколькими авторами в БД, которая имеет таблицу book_author, наподобие показанной на первом рисунке предыдущего раздела "Каждый неключевой атрибут должен описывать весь ключ". Если книга имеет двух авторов, то будут две строки для этой книги. Теперь пользователь открывает экран редактирования и изменяет название книги, как показано здесь. Когда он сохранит изменение, это изменит строку таблицы book_author только для Фреда Смита, но не строку таблицы book_author для его соавтора. Две строки таблицы book_author, первоначально для одной и той же книги, теперь показывают различные названия. Эта проблема устраняется, используя 2НФ, как показано на втором рисунке в том же разделе. В этой форме элемент таблицы book будет соединен с двумя строками таблицы book_author. Изменение заголовка в этом экране редактора изменит поле b o o k _ t i t l e в этой единственной строке таблицы book; два экземпляра строк таблицы book_author связаны с таблицей book только полем book_ISBN_number, так что БД будет все еще показывать для обоих авторов, что они являются соавторами той же самой книги. Все остается синхронным. 161
Глава 6 Признаки, что ваши сущности не удовлетворяют 2НФ Признаки для обнаружения, находятся ли ваши сущности в 2НФ, не столь явны, как признаки для 1НФ. Они дают некоторое предостережение и требуют полной экспертизы ваших структур.
Повторение префиксов имен ключевых полей Эта ситуация — один из коварных подвохов. Рассмотрим повторно наш предыдущий пример: book author author social_security_jiumber book ISBN number royalty_percentage book_title author first jiame author second name Здесь мы имеем author_f irst_name и author_second_name, которые функционально зависят от author_social_security_number. Мы также имеем b o o k _ t i t l e и book_ISBN_number, которые находятся в той же самой ситуации. Наличие таких очевидных префиксов бывает не всегда, но следует их поискать, поскольку это довольно обычная ошибка, которую делают проектировщики-новички.
Повторение групп данных Более трудно распознать повторяющиеся группы данных. Предположим, что над таблицей, которая упорядочена по каждому из существенных столбцов, многократно выполняются операторы SELECT, каждый раз восстанавливая все строки (если возможно). Если имеется функционально зависимый атрибут от одного из других атрибутов, то везде, где поле будет равно X, мы будем видеть зависимое поле, равное Y. Рассмотрим некоторые примеры входов следующей таблицы.
162
!
author s o c i a l _ security number
book ISBN_number
royalty _persentage
DELA-777-888
1-861-000-156-7
2
DELA-777-888
1-861-000-338-7
3
GIBB-423-4421
1-861-000-156-7
3
book-title
author f i r s t name
author_second_name
Instant Tiddlywinks
Vervain
Delaware
Beginning Ludo
Vervain
Delaware
Instant Tiddlywinks
Gordon
Gibbon
Методы нормализации B o o k _ t i t l e , конечно, зависит от book_ISBN_Number, так что когда мы видим ISBN-номер = 1-861-000-156-7, то можем быть уверены, что название книги — "Instant Tiddlywinks". Если это не так, в БД что-то не в порядке.
Составные ключи без внешнего ключа Если в ключе имеется больше чем один атрибут, который не является внешним ключом, любые атрибуты, которые описывают эти атрибуты, вероятно, будут нарушать 2НФ.
Кодирование при наличии этой проблемы Разбор кода БД — один из хороших способов обнаружения проблем, основанных на технических ресурсах системы, которую мы анализируем. Часто программист будет просто писать код для обеспечения, что нарушение 2НФ не повредит данные, вместо перемоделирования их в надлежащую структуру. Какое-то время в истории реляционных СУБД это, возможно, было единственным способом разобраться с данной ситуацией; однако теперь, когда технология догнала реляционную теорию, такой подход встречается гораздо реже. Важно понять, что я не пытаюсь заявить, что теория несколько изменилась благодаря технологии. Фактически, реляционная теория была очень устойчивой в течение ряда лет с небольшими изменениями в последнее десятилетие. Десять лет назад мы имели весьма мало проблем, связанных с нормализованной системой, в связи с ограничениями аппаратных средств ЭВМ и ОС, так что углы в наших моделях были сглажены по причинам "реализации". Используя современные аппаратные средства, нет никакой потребности даже начинать сглаживать углы нормализации для реализации. Лучше решить эти проблемы с помощью SQL-соединений вместо спагетти-кода, используемого для обслуживания ненормализованных данных. Конечно, на этой стадии процесса проектирования лучше даже не рассматривать основы реализации, функционирования или любых других моментов, не связанных непосредственно с вопросами соответствующего логического размещения данных.
Третья нормальная форма Сущность, которая соответствует Третьей нормальной форме (ЗНФ), будет обладать следующими характеристиками: •
Сущность должна соответствовать 2НФ.
•
Сущность не соответствует ЗНФ, если неключевой атрибут определяется другим неключевым атрибутом.
Мы можем перефразировать второй момент так: Все атрибуты должны определяться ключом и только ключом. ЗНФ отличается от 2НФ в том, что она имеет дело с отношениями неключевых данных к неключевым данным. Проблемы те же самые и многие из признаков те же самые, но может быть тяжелее выявить общий случай нарушений, с которыми эта форма связана. В основном, главное различие заключается в том, что данные в одном поле вместо зависимости от ключа фактически зависят от данных в другом неключевом поле. Требования для ЗНФ следующие. 163
Глава 6 Сущность должна быть в 2НФ Снова это очень важно. Может быть трудно выделить проблемы ЗНФ, если вы все еще имеете проблемы с 2НФ.
Неключевые атрибуты не могут описывать неключевые атрибуты Если какой-либо из атрибутов функционально зависит от другого неключевого атрибута, то мы снова можем получить проблемы при корректировке данных. Так как мы уже находимся в 2НФ, мы обеспечили, что все наши атрибуты определяются всем ключом, но мы не рассмотрели отношение атрибутов друг к другу. На следующем рисунке мы возьмем нашу сущность book (книга) и расширим ее, включив издателя и город, где расположен издатель. book bookJSBN_number title price publisher_name publisher_city
T i t l e задает название книги, определенной ключомbook_lSBN_number, p r i c e указывает цену книги, publisher_name описывает издателя книги, но p u b l i s h e r _ c i t y не имеет смысла в этом контексте, поскольку непосредственно не описывает книгу. Чтобы исправить эту ситуацию, мы должны создать другую сущность, определяющую информацию об издателе. book
publisher
bookJSBNjiumber
publisher_name
publisher_name (FK) title price
publisher_city
Теперь сущность p u b l i s h e r содержит только данные, связанные с издателем, а сущность book — информацию о книге. Что здесь особенно ценно, так это то, что теперь, если мы хотим добавить к нашей схеме информацию об издателе, например, информацию о партнерах или адрес, то совершенно очевидно, куда мы добавим эту информацию. Теперь наш атрибут p u b l i s h e r _ c i t y определяет издателя, а не книгу. Когда мы перейдем к физическому моделированию, то обсудим достоинства использования атрибута publisher_narae в качестве первичного ключа, но пока это — разумный первичный ключ, и разумный набор атрибутов. Обратите внимание, что решение этой проблемы создало неидентифицирующее отношение p u b l i s h e r —» book. Так как противные атрибуты не были в ключе с самого начала, они не входят туда и теперь.
164
Методы нормализации Все атрибуты должны описывать ключ и ничего кроме ключа Если это выглядит знакомым, то так оно и есть. Это небольшое высказывание — основа для целой группы нормальных форм, связанных отношениями между ключевыми и неключевыми атрибутами. Помните об этом, поскольку это может сэкономить когда-нибудь время при проектировании вашей БД.
Проблемы программирования, которые можно избежать с помощью ЗНФ В то время как способы нарушения ЗНФ очень близки к нарушениям 2НФ, имеется несколько важных различий. Поскольку мы не связаны со значениями ключа, следует рассмотреть отношения каждого атрибута к каждому неключевому атрибуту и так выполнить для каждой комбинации атрибутов. В примере с книгой мы имели следующую структуру сущности: book book_ISBN_number title price publisher_name publisher_city
В то время как ради краткости я пошел на очевидное нарушение, не всегда так легко определить нарушения ЗНФ. Каждый атрибут следует рассмотреть совместно с каждым другим атрибутом. Если сущности имеют разумный размер (я вообще считаю, что десять-двадцать атрибутов в сущности, вероятно, это столько, сколько вы можете иметь без того, чтобы нарушить некоторые правила нормализации, хотя это и не всегда так), то процесс выявления проблем ЗНФ также не будет слишком длинным процессом. В нашем примере мы должны проверить каждый атрибут с другими тремя атрибутами. Так как имеется четыре атрибута, то мы должны рассмотреть N * (N - 1) или (4 * 3) = 12 (игнорируя факт, что мы будем проверять некоторые значения больше, чем один раз) различных перестановок отношений атрибутов, чтобы обезопасить себя. В примере с нашей сущностью мы должны проверить: •
t i t l e с p r i c e , publisher_name и
О
price с t i t l e ,
publisher_city;
•
publisher_name c p r i c e ,
title и
•
publisher_city с price,
t i t l e и publisher_name.
publisher_name и p u b l i s h e r _ c i t y ; publisher_city;
Отсюда мы замечаем, что, когда будем проверять publisher_name с тремя другими атрибутами, становится ясно, 4Topublisher_city функционально зависит от него, следовательно, нарушена ЗНФ. Если вы разработали несколько тысяч сущностей, то начнете различать общие атрибуты, которые могут вызвать проблемы, и нужно будет рассмотреть только несколько атрибутов при ваших проверках нормализации. Мы рассмотрим много таких примеров в разделе с учебным примером этой главы.
165
Глава 6 Обратите также внимание, что в нашем примере для простоты выбраны специальные названия, чтобы все выглядело проще, но в действительности названия часто гораздо более загадочные. Рассмотрим следующую сущность: book ISBN
!
pubname city title price
Эти названия, вероятно, менее загадочные, чем те, с которыми вы могли бы фактически столкнуться в некоторых традиционных сущностях БД; однако они уже достаточно неоднозначны, чтобы вызвать проблемы. Атрибут c i t y здесь кажется вполне подходящим, пока вы не поймете, что большинство книг не имеет атрибута c i t y , а издатели могли бы его иметь. Следующий пример кода показывает, что произойдет, если мы захотим изменить атрибут c i t y и сохранить синхронность данных. Возьмем, например, ситуацию, когда мы строим сначала таблицу: CREATE TABLE book ( ISBN v a r c h a r ( 2 0 ) NOT NULL, pubname v a r c h a r ( 6 0 ) NOT NULL, c i t y v a r c h a r ( 6 0 ) NOT NULL, t i t l e v a r c h a r ( 6 0 ) NOT NULL, p r i c e money NOT NULL )
Здесь имеются нарушения ЗНФ, которые мы определили. Рассмотрим ситуацию, когда мы хотим изменить поле c i t y для ISBN 23232380237 со значения V i r g i n i a Beach (Вирджиния Бич) на N a s h v i l l e (Нэшвилл). Сначала скорректируем одну запись:
I
UPDATE book SET c i t y - ' N a s h v i l l e ' WHERE ISBN = '23232380237'
Однако так как мы имеем функциональную зависимость для отношения p u b l i s h e r — c i t y , далее скорректируем все записи для книг, которые имеют одно и то же имя издателя, на новое значение:
I
UPDATE b o o k SET city = 'Nashville' WHERE city - 'Virginia Beach' AND pubname = 'Phantom Publishing' — исходное имя издателя
В то время как это — хороший способ гарантировать, что код групповой операции обновляет должным образом значение атрибута c i t y , так же как и атрибут book, в большинстве случаев этот код затеряется в вашем приложении, если он не связан с транзакцией и очень редко выполняет истинную корректировку. Любые ошибки в одном операторе UPDATE — и ваши данные могут быть испорчены. Для существующих приложений SQL Server, которые вы перепроектируете, можно использовать утилиту SQL Server Profiler для проверки, что SQL-запрос фактически послан на SQL Server из вашего приложения. 166
Методы нормализации Признаки, что ваши сущности не удовлетворяют ЗНФ Признаки для ЗНФ очень похожи на признаки для 2НФ, так как они связаны с тем же сортом проблем — быть уверенным, что все неключевые атрибуты связаны только с ключами сущности. Несколько полей с одним и тем же префиксом Заново рассмотрим наш предыдущий пример: book book_ISBN_number publisher_name publlsher_city title price
Очевидно, что publisher_name и p u b l i s h e r _ c i t y являются полями с одним и тем же префиксом. В некоторых случаях используемый префикс не будет столь очевидным, например, pub_name, p b l i s h _ c i t y или даже location_pub; все это служит серьезным основанием, чтобы установить нормальный стандарт обозначений. Повторяющиеся группы данных Здесь почти то же, что и в 2НФ, но вы должны рассмотреть большее количество перестановок, как мы обсудили ранее. Итоговые данные Одно из общих нарушений ЗНФ — итоговые данные. Это, когда к сущности-предку добавлены поля, которые ссылаются на записи-потомки и суммируют их. Итоговые данные были одним из наиболее часто встречающихся зол, с которым мы должны были иметь дело на протяжении всей истории реляционных БД. Имеется несколько новых особенностей в SQL Server 2000, которые мы используем, чтобы избежать итоговых данных в нашей реализации, но в логическом моделировании пет абсолютно никакого места для этого. Мало того, что итоговые данные зависят функционально от неключевых полей, они зависят от полей других сущностей. Это приводит к различным видам неприятностей, которые мы рассмотрим. Итоговые данные должны быть сохранены или для физического проекта, или для элементов хранилища данных. Для разъяснения возьмем следующий пример с автомобильными дилерами. Имеется сущность, содержащая список всех автомобилей, которые они продают, и сущность, где записываются все автомобильные продажи:80 automobile automobile name total_sold_ count total sold value
automobile_sale automobile_name (FK) ^ automobile_sale_number amount sold_for J
f
Здесь automobile — сущность "автомобиль", automobile_sale — сущность "продажа автомобиля", automobile_name — название автомобиля, automobile__sale_number — число проданных автомобилей, total_sold_count — общее число продаж, total_sold_value — общая сумма продаж, amount_sold_for — сумма за проданный тип автомобиля. 167
Глава 6 Вместо вычисления общего числа проданных транспортных средств и их стоимости, когда это необходимо, проектировщик решил добавить поля к сущности-предку, которые обращаются к записям-потомкам и суммируют их. Это может казаться приемлемым, но сложность реализуемой системы увеличится по крайней мере на порядок, поскольку мы должны будем иметь триггеры у сущности automobile_sale, которые вычисляют эти значения для любого изменения в сущности automobile_sale. Если это — высокоактивная БД с частыми записями, добавляемыми к сущности automobile_sale, такое обращение приведет к значительному замедлению БД. С другой стороны, если это — в основном бездействующая БД, то в сущности-потомке будет очень немного записей, и выгода от сделанной работы, чтобы быстро найти число проданных транспортных средств и сумму их стоимости, будет в таком случае очень небольшой.
Нормальная форма Бойса-Кодда Во время обсуждения 2НФ и ЗНФ я преднамеренно не конкретизировал слово 'ключ". Как я упомянул ранее, ключом может быть любой потенциальный ключ — и первичный ключ, и вторичный ключ. Однако главная забота 2НФ и ЗНФ — первичный ключ. Нормальная форма Бойса-Кодда (НФБК) — фактически лучше сконструированная замена для 2НФ и ЗНФ. Обратите внимание, что для обеспечения НФБК не нужно упоминать 2НФ и ЗНФ. НФБК фактически охватывает их обе и определяется следующим образом: •
Все атрибуты полностью зависят от ключа.
•
Сущность находится в НФБК, если каждый детерминант — ключ.
Давайте посмотрим на каждое из этих правил отдельно. Все атрибуты полностью зависят от ключа Мы можем перефразировать это следующим образом: Все атрибуты должны определяться ключом и только ключом. Это — небольшое, но важное отклонение от наших предыдущих правил для 2НФ и ЗНФ. В этом случае мы не определяем полный ключ, или только ключ — теперь это просто ключ. Чем это отличается? Хорошо это или нет? Это по существу расширяет смысл 2НФ и ЗНФ, чтобы иметь дело с самой типичной ситуацией, где мы имеем более одного ключа. Было сказано, что атрибут должен полностью зависеть от ключа, и этот ключ должен быть определен как уникальный идентификатор. Уникальный идентификатор нужно рассматривать как адрес или указатель сущности независимо от того, используем ли мы естественный ключ или нет. Сущность по определению является логическим представлением единственного объекта, реального или воображаемого. Если вы представляете каждый ключ сущности как идентификатор жетона, или номер социального страхования, или полное имя, вы можете понять, что представляет собой каждый из атрибутов.
168
Методы нормализации Например, возьмем человека, который работает на компанию, и смоделируем его. Сначала мы выберем ключ, скажем, номер социального страхования. person social_security_number
Здесь person — сущность "человек", social_security_number страхования.
— номер социального
Далее начнем добавлять другие атрибуты, которые характерны для наших служащих — их имя, цвет волос, цвет глаз, номер жетона, который они получают, номер водительских прав и т. д. Таким образом, мы имеем следующую сущность: person social_security_number first name last name height weight badge_number eye_color drivers license number hair color shoe_size Здесь дополнительно first_name — имя, last_name — фамилия, height — рост, weight — вес, badge_number — номер жетона, eye_color — цвет глаз, drivers_license_number — номер водительских прав, hair_color — цвет волос, • shoe_size — размер ботинок. Внимательное изучение сущности показывает, что она соответствует ЗНФ, так как каждый из атрибутов далее описывает сущность. First_name, h e i g h t , badge_number и другие, — все относятся к сущности. То же самое можно сказать о s o c i a l _ s e c u r i t y _ n u m b e r . Этот атрибут был выбран в качестве ключа прежде всего потому, что это было первое, на что мы натолкнулись. В логическом моделировании выбор поля в качестве первичного ключа не так уж прост, хотя обычно лучше выбрать подходящий. (Более полное обсуждение соответствующих первичных ключей будут приведено в главе 10.) В большинстве случаев, как раз на стадии логического моделирования, мы будем просто использовать искусственный ключ, как рассмотрено в главе 3. Следующее предложение в основном объясняет 2НФ и ЗНФ:
Все атрибуты должны далее описать сущность, всю сущность и только сущность.
Когда вы поймете эту концепцию, моделирование данных и нормализация станут намного более легкими.
169
Глава 6 Сущность соответствует НФБК, если каждый детерминант является ключом Вторая часть определения НФБК говорит, что нужно удостовериться в том, что каждый детерминант — ключ или уникальный идентификатор для сущности. Наше определение детерминанта в главе 3 было следующим:
Любой атрибут или комбинация атрибутов, от которых функционально зависит любой другой атрибут или комбинация атрибутов.
На основе изучения 2НФ и ЗНФ мы можем видеть, что это, по существу, определение ключа. Так как все атрибуты, которые не являются ключами, должны функционально зависеть от ключа, определение детерминанта — то же самое, что и определение ключа. НФБК просто расширяет предыдущие нормальные формы, говоря, что сущность может иметь много ключей, и все атрибуты должны зависеть от одного из этих ключей. Мы немного упростили это утверждение, отмечая, что каждый ключ должен уникально идентифицировать сущность, и каждый неключевой атрибут должен описывать сущность. Что НФБК подтверждает, так это то, что у сущности может быть несколько ключей. Они упоминаются как потенциальные ключи или вторичные ключи. Одна интересная вещь, которая должна быть отмечена, состоит в том, что каждый ключ является детерминантом для всех других ключей. Это потому, что в каждом месте, где вы видите одно значение ключа, вы можете заменить его другим значением ключа без потери смысла. Это не говорит о том, что вторичный ключ не может изменять значения — вовсе нет. Номер водительских прав — хороший ключ, но если Департамент автомашин выпускает все новые номера, это — все еще ключ, и все еще будет описывать сущность. Если значение любого потенциального ключа изменяется, это абсолютно приемлемо. Помня это определение, возьмем пример сущности, которую мы моделируем в этом разделе, и посмотрим на нее снова. person social_security_number first_name last_name height weight badgejiumber eye_color driver s_license_number drivers_license_state hair_color shoe size Дополнительно здесь drivers_license_state
— штат, выдавший водительские права.
На что мы теперь посмотрим, так это на атрибуты или группы атрибутов, которые зависят от ключа, и также будут уникальными для каждого экземпляра этой сущности.
170
Методы нормализации First_name отдельно не будет уникальным, и вряд ли будет хорошо предположить, что будут уникальными f i r s t _ n a m e и last_name. (Можете ли вы принять это или нет, зависит от размера множества значений, вы, вероятно, хотели бы включить инициал отчества и титул, но это все еще не очень хороший ключ.) Атрибут h e i g h t описывает человека, но не является уникальным. То же самое можно сказать и относительно weight. Badge_number, конечно, должен быть уникален, так что мы можем сделать его ключом. (Заметьте, у нас нет b a d g e _ i s s u e d _ d a t e (дата выпуска жетона), которая будет относиться к жетону и не помогла бы в реализации примера НФБК.) Атрибут d r i v e r s _ l i c e n s e _ n u m b e r , вероятно, уникален, но вы должны определенно рассмотреть изменения штата. H a i r _ c o l o r и s h o e _ s i z e описывают человека, но ни один из них не может считаться уникальным. Даже если вы берете одновременно h e i g h t , weight, eye_color, h a i r _ c o l o r , и s h o e _ s i z e человека, вы не можете гарантировать уникальность двух случайных людей. Таким образом, мы смоделируем сущность следующим образом: person social_security_number first name last name drivers_license_number (AK1) drivers_license_state (AK1) badge_number (AK2) height weight eye_color hair color shoe_size Теперь мы имеем три ключа для этого объекта. Когда мы будем выполнять физическое моделирование, то выберем надлежащий ключ из ключей, которые мы определили, или используем искусственный ключ. Как рассмотрено в главе 3, искусственный ключ — просто значение, которое используется как указатель на объект, подобно тому, что badge_number — указатель, который использует компания, чтобы идентифицировать служащего, или правительство, использующее номера социального страхования (SSN), чтобы идентифицировать индивидуумов в Соединенных Штатах. Стоит также сказать, что SSN — также не всегда очень хороший ключ. Даже если вы имеете дело только с людьми в США, имеется множество людей, которые не имеют SSN. И, конечно, если вы когда-либо будете иметь дело с людьми, прибывшими в США из-за границы, то SSN никогда не будет работать — послушайте кого-нибудь, кто должен был установить системы с этим условием! Это будет конечно зависеть от ситуации: сколь много случаев, когда у вас есть SSN, и доля ситуаций, когда вы имеете дело с SSN или Green Card (документ, разрешающий пребывание и работу в США), чтобы идентифицировать пользователя как жителя США. Ситуация будет всегда диктовать возможное решение, и задачей архитектора данных является выбор соответствующего пути. При выборе ключа мы всегда пробуем удостовериться, что ключи не накладываются. Например, h e i g h t — не уникальный ключ, но h e i g h t и badge_number — уникальный! Важно быть уверенным, что отдельные части уникальных ключей не могут быть уникальными сами по себе, иначе могут быть сделаны ошибки, если вы случайно помещаете неуникальные данные в столбцах, которые должны быть уникальными.
171
Глава 6 Признаки программных аномалий Признаки определения, что сущность находится в НФБК, — те же самые, что и для 2НФ и ЗНФ. Аномалии программирования, устраняемые с помощью НФБК, также те же самые. Главный момент, который нужно здесь упомянуть, — то, что если вы получаете все ваши детерминанты, смоделированные в течение этой стадии проектирования, то когда вы будете реализовывать БД, наиболее вероятно, детерминанты будут реализованы как уникальные ключи. Это предотвратит пользователей от ввода неуникальных значений в столбцы, которые должны содержать только уникальные значения. Это заканчивает наш краткий обзор первых четырех нормальных форм. Мы рассмотрим Четвертую и Пятую нормальные формы в следующей главе, но сейчас мы реализуем только что изученные концепции в нашем учебном примере.
Учебный пример В этой главе мы пройдем каждую из нормальных форм и исправим нашу модель согласно полученным правилам, затем перейдем к окончательной структуре.
Первая нормальная форма Наша модель имеет несколько нарушений 1НФ. Мы выделим каждое из нарушений, которые могут иметь место.
Атрибуты должны быть элементарными У нас есть один пример этого вида нарушения. Оно возникает в сущности s t a t e m e n t . Мы должны вспомнить, что эта сущность представляет отчет, который банк посылает каждый месяц, чтобы клиент мог проверить, все ли введенные чеки действительно имели место. Атрибут b a l a n c i n g l t e m s содержит все сделки, которые банк записал, и они должны совпасть с элементами регистра. Однако этот атрибут будет содержать много строк и много полей, которые вряд ли являются элементарными. statement bankld: PrimaryKey (FK) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date totalDebits: Amount totalCredits: Amount balancingltems: Balancingltems Таким образом, мы должны добавить новую сущность, чтобы разместить эти элементы. Поскольку мы не знаем точно, что войдет в эту сущность, мы позволим атрибутам мигрировать из сущности s t a t e m e n t , а затем добавим другой указатель к первичному ключу для его уникальности.
172
Методы нормализации
statement bankld: PrimaryKey (FK) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date totalDebits: Amount totalCredits: Amount balancingltems: Balancingltems
имеет элементы в
statementltem bankld: PrimaryKey (FK) statementld: PrimaryKey (FK) statementltemld: Primarytey
Нужно рассмотреть другие типы нарушений этого же вида. Возьмем сущность payee (получатель платежа) и два поля: a d d r e s s (адрес) и phoneNumber (номер телефона). payee payeeld: PrimaryKey name: Name (AK1.1) address: Address phoneNumber: PhoneNumber В нашем примере мы рассмотрим только атрибут a d d r e s s . Адрес состоит из нескольких частей: строк уличного адреса (обычно мы довольствуемся двумя строками, чтобы хранить уличную информацию), города, штата и почтового индекса. Мы также расширим поле phoneNumber (номер телефона) так, как обсуждали ранее. Как только мы закончим этот процесс, то получим следующий результат:
p
a
p
y
a
e
y
e
e
e
m
l
e
d
:
N
i
a
y
K
e
K
1
.
y
r
e
s
s
L
i
n
e
i
:
A
d
d
r
e
s
s
D
e
s
c
r
i
p
t
i
v
e
L
i
n
e
a
d
d
r
e
s
s
L
i
n
e
2
:
A
d
d
r
e
s
s
D
e
s
c
r
i
p
t
i
v
e
L
i
n
e
a
d
d
a
d
d
t
a
a
d
d
r
p
h
o
n
e
C
o
u
n
t
p
h
o
n
e
A
r
e
a
C
p
h
o
n
p
h
p
h
r
e
s
e
C
s
s
i
S
s
t
y
t
Z
:
a
i
(
A
d
s
e
r
d
s
m
m
a
e
a
r
a
r
:
P
n
C
t
p
e
C
i
t
:
d
e
)
y
S
o
1
t
:
e
Z
C
i
o
p
d
C
o
e
d
e
••
o
o
e
n
n
E
e
e
x
N
E
c
u
x
h
m
t
e
r
y
o
a
d
n
b
e
n
s
C
e
g
d
:
:
r
E
P
n
C
A
:
o
e
:
e
r
i
o
:
h
E
e
x
o
t
u
a
c
n
x
o
n
C
h
a
e
N
e
n
t
o
s
r
d
n
g
u
m
i
y
C
o
d
e
e
e
b
o
e
r
P
a
r
t
n
В этом примере теперь нет никаких полей, которые очевидно нарушают 1НФ и поэтому не элементарны. Фактически, мы могли бы иметь случай, что пoлeaddressLinel не является элементарным, так как оно будет содержать номер улицы, название улицы и другие части информации. Однако мы не собираемся моделировать эти атрибуты в нашем примере диаграммы, стараясь сохранить вещи простыми.
173
Глава 6 Заметьте, что мы создали домены для каждого нового атрибута. Атрибуты a d d r e s s L i n e l и a d d r e s s L i n e 2 — один и тот же вид элемента. Сделаем также замечание относительно атрибута phoneNumber. Он имеет то же самое название, что и прежде, но другой смысл, так как номер телефона состоит из кода страны, кода региона, телефонной станции и номера. Поскольку он имеет другой смысл, мы создали новый домен с новым названием, так как практически все еще могут быть сущности, которые используют исходный домен.
Все экземпляры записей сущности должны содержать одно и то же число значений В нашей новой сущности payee (получатель платежа) мы объединили вместе общий набор атрибутов, которые нарушают эту часть правила — поля a d d r e s s L i n e l и addressLine2. В то время как это обычное решение проблемы адреса, оно тем не менее является нарушением. Наличие установленного числа строк адреса кусало меня несколько раз, когда адреса нуждались в большем числе, иногда даже в четырех или пяти строках. Так как не каждый адрес будет иметь то же самое число элементов, это — проблема. Мы решаем ее, добавляя другую сущность-потомок для информации о строках адреса: payee payeeld: PrimaryKey name: Name(AK1.1) addressCity: City addressState: StateCode addressZipCode: ZipCode phoneCountr yCode: CountryCode phoneAreaCode: AreaCode phoneExchange: Exchange phoneNumber : PhoneNumberPart phoneExtension: Extension
имеет строки адреса в payeeAddress payeeld: PrimaryKey (FK) payeeAddressId: PrimaryKey addressLine: AddressDescriptiveLine VsortOrderld: SortOrderld J
Это может показаться немного странным, но получается именно так, рассматривая путь, которым БД разрабатывались в течение длительного периода времени. Однако такое проектирование адреса дает нам гибкость, чтобы хранить столько частей информации, сколько потребуется, вместо необходимости добавлять столбцы, если ситуация потребует этого. Это также является хорошим примером нормализации, создающей большее количество сущностей.
Все экземпляры записей сущности должны быть различными Мы начали заботиться об этом, добавляя первичные ключи и вторичные ключи к нашим сущностям. Обратите внимание, что просто добавление искусственного ключа не будет обеспечивать этого специфического правила. Одна из наших последних задач физического моделирования должна будет проверить, что все наши сущности имеют, по крайней мере, один определенный ключ, который не содержит немигрирующий искусственный ключ.
Нормальная форма Бойса-Кодда Так как НФБК фактически является расширением 2НФ и ЗНФ, мы можем рассматривать все нарушения и обсуждать их совместно. 174
Методы нормализации Итоговые данные Поля итоговых данных вообще-то самые легкие для устранения нарушения. Это потому, что мы можем обычно просто удалить значения из наших сущностей. Например, в сущности account (счет) мы можем удалить атрибут Running T o t a l (текущий итог), поскольку он может быть получен, подводя итог значений, которые запасены в сущности t r a n s a c t i o n (сделка). Это оставляет нам следующее:
Г
bankld: Prm i aryKey (FK) accountld: Prm i aryKey number: AlphaNumericNumber (АКТ .1) balanceDate: Date i
С другой стороны, мы имеем ситуацию, когда итоговые поля находятся в сущности s t a t e m e n t (отчет): statement. bankld: PrimaryKey (FK) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date totalDebits: Amount totalCredits: Amount balancingltems: Balancingltems Мы имеем previousBalance, c u r r e n t B a l a n c e и т. д. — фактически, все поля, кроме type (тип), ссылаются на некоторые другие значения сущностей. Однако в данном случае мы могли бы согласиться с этим. Поскольку сущность s t a t e m e n t (отчет) связана с документом, а именно, документом, формируемым банком, эти поля могли бы пригодиться, так как мы можем разместить здесь то, что банк думает об отчете. Мы, вероятно, будем желать иметь эти поля и использовать их, чтобы проверять данные, которые будем хранить в сущности s t a t e m e n t l t e m (элемент отчета).
Несколько полей с одинаковыми префиксами Мы имеем очень хороший пример этого вида проблемы в созданной нами сущности payee (получатель платежа). Атрибуты phoneCountryCode, phoneAreaCode и т. д., все имеют один и тот же префикс (обратите внимание, что это не всегда будет столь очевидным, как мы говорили ранее); аналогично для a d d r e s s C i t y и других атрибутов. зауее payeeld: PrimaryKey name: Name (AK1.1) addressCity: City addressState: StateCode addressZipCode: ZipCode phoneCountryCode: CountryCode phoneAreaCode: AreaCode phoneExchange: Exchange phonelMumber: PhoneNumberPart phoneExtension: Extension
7 — 1868
175
Глава 6 Телефонные номера и адреса — фактически объекты сами по себе. Каждое из телефонных полей фактически не описывает сущность payee (получатель платежа), но обеспечивает существование телефонного номера. То же самое относится и к полям адреса. Следовательно, мы разбиваем их следующим образом: phoneNumber определяет информацию о номере контактных телефонов в
payee payeeld: PrimaryKay
phoneNumberld: PrimaryKey countr yCode:CountryCode areaCode: AreaCode exchange: Exchange number: PhoneNumberPart extension: Extension
name: Name (AK1.1) addressld: PrimaryKey (FK) phoneNumberld: PrimaryKey (FK)
address addressld: PrimaryKey
определяет расположение
city: City state: StateCode zipCode: ZipCode
имеет дополнительную информацию в
addressLine addressld: PrimaryKey (FK) addressLJneld: PrimaryKey addressLine: AddressDescriptiveLine V^sortOrderld: SortOrderld
Теперь мы удовлетворили идею, чтобы каждое поле обращалось к ключу, поскольку a d d r e s s и phoneNumber относятся к адресу и телефонному номеру получателя платежа, a d d r e s s определяется городом, штатом и почтовым индексом плюс уличная информация в сущности a d d r e s s L i n e . Сущность phoneNumber также определяется своими атрибутами.
Каждый детерминант должен быть ключом Детерминант — то место, где может возникнуть ошибка. Рассмотрим сущностьрауее из нашего предыдущего примера. Атрибут payeeld является первичным ключом, a name — детерминантом сущности. Как мы ранее рассмотрели, имеется следующий набор зависимостей: •
Для каждого значения payeeld вы должны иметь то же самое значение name.
•
Для каждого значения name вы должны иметь то же самое значение a d d r e s s l d и phoneNumberld.
•
Не обязательно, чтобы для каждого значения a d d r e s s l d и phoneNumberld вы имели то же самое значение name, поскольку адрес или телефонный номер могут использоваться для нескольких получателей платежа.
Реальная проблема здесь в выборе соответствующего набора ключей. При использовании искусственного ключа в виде указателя уже на стадии логического моделирования можно решить основную проблему. Каждая сущность должна содержать уникальные значения, и это уникальное значение не должно включать только одно бесполезное значение, в то время как искусственный ключ бесполезен за исключением использования в качестве указателя к самому себе. В случае, где есть более одного ключа, мы должны определить потенциальный ключ из мигрирующего ключа и другого атрибута сущности.
176
Методы нормализации
Сначала возьмем сущность phoneNumber: phoneNumber phoneNumberld: PrimaryKey countryCode: CountryCode areaCode: AreaCode exchange: Exchange number: PhoneNumberPart extension: Extension
Она не имеет никакого определенного ключа, так что мы должны его сконструировать. В этом случае вся сущность без первичного ключа будет представлять ключ. Это, кстати, иллюстрирует одну из основных причин иметь отдельные тематические сущности. Теперь, когда мы выделили телефонный номер, мы можем быть уверенными, что если два получателя платежа имеют те же самые телефонные номера, мы не будем дублировать эти значения. Делая это, мы сохраняем компоненты понятными для последующего использования этих значений, в то время как имея двойные телефонные номера, все будет более запутанным. phoneNumber phoneNumberld: PrimaryKey countryCode:CountryCode (АКТ Л) areaCode: AreaCode (AK1.2) exchange: Exchange (АКТ .3) number: PhoneNumberPart (АКТ .4) extension: Extension (АКТ.5)
Далее рассмотрим сущность a d d r e s s : address addressld: PrimaryKey city: City state: StateCode zipCode: ZipCode
addressLine имеет дополнительную информацию в
addressld: PrimaryKey (FK) addressLineld: PrimaryKey addressLine: AddressDescriptiveLine V^sortOrderld: SottOrderld J
Это — очень интересный случай. Нет никакого способа, которым можно сделать c i t y , s t a t e и zipCode уникальными, поскольку мы будем почти всегда иметь больше чем один адрес одного и того же типа. В этом случае уникальность определяется атрибутами c i t y , s t a t e , zipCode плюс любые элементы addressLine в подходящем сочетании. Мы можем не иметь уникальности на основе значений сущности addressLine, поскольку это лишь логическая часть сущности a d d r e s s и address ссылается на нее для уникальности. Нельзя смоделировать ситуацию, когда только один адрес может содержать информацию "101 Main Street". Это не обязательно. Мы не будем реализовывать эту ситуацию именно таким образом, поскольку это слишком трудно. Но логически в данном случае это наиболее подходящий путь моделирования данных. 177
Глава 6 Наконец, рассмотрим сущность transaction: transaction bankld: PrimaryKey (FK) accountid: PrimaryKey (FK) transactionid: PrimaryKey date: Date number: AlphalMumericNumber (AK1.1) description: Description amount: Amount type: Code В этом наборе атрибутов мы имеем ключ, который составлен полностью из мигрирующих ключей (bankld, accountid) и еще одного искусственного ключа. В основном, назначение его в том, что мигрирующий ключ счета определяет часть сделки, и нам нужно разобраться с соответствующим дополнительным атрибутом. В этом случае он будет числом, и поскольку он является числом, то банк использует его, чтобы идентифицировать сделку, и мы предварительно выбрали его как собственный ключ: transaction bankld: PrimaryKey (FK) (AK1.1) accountid: PrimaryKey (FK) (AK1.2) transactionid: PrimaryKey date: Date number: AlphaNumericNumber (AK1.3) description: Description amount: Amount type: Code Далее мы должны пройти каждую другую сущность, чтобы убедиться, что назначен надлежащий ключ, и, что более важно, нет никаких атрибутов, которые функционально зависят от другого атрибута. Конечно, это то же самое, если бы мы определили достаточно атрибутов в данный момент, как и в случае s t a t e m e n t l t e m , но не определили никаких значений. Теперь учтем также, что bankld, a c c o u n t i d и t r a n s a c t i o n i d функционально определяют атрибут number, a bankld, a c c o u n t i d и number функционально определяют t r a n s a c t i o n i d . Это — подходящая ситуация, которая является довольно интересной! Одно дополнительное замечание следует сделать относительно сущностей-подтипов сущности transaction: check ^bankld: PrimaryKey (FK) accountid: PrimaryKey (FK) transactionid: PrimaryKey (FK) payeeld: PrimaryKey (FK) usageType: Code ^signature: Blob y Здесь не требуется никаких вторичных ключей, так как эта сущность находится в отношении "один к одному" с сущностью t r a n s a c t i o n , следовательно, первичный ключ является подходящим ключом. 178
Методы нормализации
Модель Вот наша модель после нормализации вплоть до НФБК. Обратите внимание, что многие из общих аномалий корректировки должны быть выяснены. Это далеко не полно, но тем не менее... Чтобы модель была простой, не будем добавлять никакой дальнейшей информации к ней. Мы представим заключительную модель с дополнительными полями в последнем разделе части книги, посвященной логическому моделированию.
bankld: Prm i ayiKey (FK) (AK1.1) accountd: Prm i aryKey number: Ap l haNumerciNumber (AK1.2) baa l nceDate: StreetName runningTotal: Amount
bankld: PrimaiyKey (FK) (AK1.1) statementld: PrimaryKev type: Code previousBalance: Amount previousBaianceDate: Date currentBalance: Amount statementDate: Date (AK1.2) totalDebrts: Amount totalCredlts: Amount balancingltems: Balancinglterr
bankld: PrimaryKey name: Name (AK1.1)
преобразует фонды вводом
bankld: PrimaiyKey (FK) (AK1.1) accountld: PrimaryKey (FK)(AK1.2) transactionld: PrimaryKey date: Date number: AlphaNumericNumber (AK1.3) description: Description amount: Amount type: Code
t
определяет информацию phoneNumberld: PrimaryKey о номере контактного телефона для countryCode:CountryCode (AK1.1) registerType: Code (AK1.1) areaCode: AreaCode (AK1.2) exchange: Exchange (AK1.3) number: PhoneNumberPart (AK1.4) extension: Extension (AK1.5) переданные с помощью payeeld: PrimaiyKey checkReHlsterld: PrimaiyKey
name: Name (AK1.1) address: Address phoneNumberld: PrimaryKey (FK)
bankld: PrimaiyKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) payeeld: PrimaiyKey (FK) usageType: Code ^signature: Blob
directWIthdrawal bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK)
statementltem bankld: PrimaryKey (FK) slatementld: PrimaiyKey (FK) statementltemld: PrimaryKey
addressld: PrimaiyKey определяет расположение state: StateCode zipCode: ZipCode
deposit bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) sactionld: PrimaryKey (FK)
addressld: PrimaryKey (FK) iddrsssLineld: PrimaryKey addressLine: AddressDescriptivel.ine sortOrderld: SortOrderld
Резюме В этой главе мы начали процесс превращения наших случайных структур таблиц в структуры, которые будут обеспечивать хранение данных намного более надежно. Строя эти структуры в очень конкретной манере, мы получим набор сущностей, которые являются эластичными для изменений, что приведет к уменьшению аномалий при корректировке. Внешне все это может казаться одним из наиболее загадочных процессов в информатике. Это, вероятно, объясняет, почему многие разработчики не нормализуют свои БД. Однако для этого нет никакой причины. За пределами иногда тайного, понятного только посвященным, жаргона все это представляет набор простых, хорошо сформулированных правил, которые устанавливают, как нужно разрабатывать БД. Что касается окончательных результатов, учебный пример должен был продемонстрировать, что окончательные сущности, которые мы разработали, более чистые, более опрятные и более безопасные для ввода наших данных, даже при том, что размещаемые данные будут выглядеть все менее и менее значащими для случайного наблюдателя.
179
Глава 6 Теперь очевидный вопрос: можем ли мы нормализовать наши данные каким-либо образом дальше? Так как я предупредил вас о том, что имеется семь нормальных форм, а мы рассмотрели только четыре, вы знаете, что ответ на этот вопрос утвердительный. С другой стороны, следует ли нам нормализовать далее? Да! ЗНФ в течение длительного времени рассматривалась как наиболее важный результат моделирования данных, и, может быть, когда мы будем физически реализовывать наше решение, мы не пойдем далее этого уровня. Однако мы пока находимся на логической стадии моделирования, и не должны интересоваться проблемами реализации; мы должны стремиться к идеальной модели. Разработка следующих нормальных форм раскроет дополнительные проблемы с нашими данными, которые мы не увидим иначе.
180
Расширенные темы нормализации Введение Если вы слышали это однажды, то вы, вероятно, слышали это миллион раз: "Эта БД находится в ЗНФ, так что давайте строить таблицы!" В этой главе мы рассмотрим дальнейшие методы нормализации, которые являются столь же важными, как и первые три, хотя они обычно и не используются так широко из-за имеющихся недостатков, как в смысле времени, необходимого для их реализации, так и стоимости создания результирующей БД. Мы рассмотрели в предыдущей главе некоторые различные аномалии программирования, которые ЗНФ исправляет, но, как мы обсудим ниже, могут все еще быть некоторые проблемы, остающиеся в нашем логическом проекте, главным образом, вызванные наличием тройных отношений. В сущности, так как большинство людей думает, что они завершают процесс нормализации при достижении ЗНФ, в чем они действительно должны быть заинтересованы, так это в получении в своих логических моделях, по крайней мере, ЗНФ. Нужно решить какая требуется степень физической реализации проекта, и определить, какой уровень нормализации является соответствующим этому. Однако как общий руководящий принцип, проектировщик должен всегда пытаться нормализовать все сущности в столь высокой форме, насколько это возможно. Если при испытании системы обнаружатся проблемы с реализацией, то систему можно денормализовать, о чем будет сказано позже в этой главе, а также в главе 14. Эта глава, по моему мнению, наиболее сложна для понимания. Методы в этой главе чрезвычайно легко выполнить, и если я выполнил свою работу, то и они также легки для понимания. Имеется несколько очень серьезных неправильных представлений относительно того, что нормальные формы после третьей являются бессмысленными, и они очень неправильны. Я чувствую, что, как только вы фактически нормализуете Четвертый уровень, то даже не будете рассматривать возможность возвращения.
Глава 7
Четвертая нормальная форма Наши правила нормализации пока решили проблему избытка среди столбцов в сущности, но не решили проблем, которые являются результатом сущностей, имеющих сложные первичные ключи, и пока все еще обладают избыточными данными между строками. Нормализация сущностей с помощью Четвертой нормальной формы (4НФ) занимается такими проблемами. В простом случае переход к 4НФ будет связан с проблемами типа моделирования атрибута, который должен хранить единственное значение, но завершается размещением нескольких значений. Второй тип проблемы более неуловим. Рассмотрим сущность учащихся в классе (обратите внимание, что несколько классов представляют совсем другие проблемы). Мы должны хранить преподавателя каждого учащегося, поэтому поместим это в сущность classAssignment следующим образом: ciassAssignment class teacher student
Здесь classAssignment преподаватель, student
— сущность "состав класса", class — учащийся.
— класс, teacher
—
Однако это неудовлетворительно, так как мы имеем связанные атрибуты c l a s s , t e a c h e r и s t u d e n t в одной и той же сущности. Чтобы привести это к 4НФ, мы должны разбить это троичное отношение. С этой целью мы будем считать, что центром отношений является атрибут c l a s s , так как класс имеет одного или большее количество преподавателей и, разумеется, одного или большее количество учащихся. Наш измененный проект теперь выглядит следующим образом: classTeacher class (FK) teacher class class
V
J
classStudent 'class
Этот пример иллюстрирует, что для сущности должны быть выполнены следующие условия, чтобы находиться в 4НФ: •
182
Сущность должна быть в НФБК. Это условие гарантирует, что все ключи будут соответственно определены, и все значения в сущности будут соответственно зависеть от ее ключа.
Расширенные темы нормализации •
Не должно быть больше одной зависимости со многими значениями, представленной в сущности. Не более одного атрибута может хранить несколько значений, которые связаны с ключом в любой сущности, иначе будет дублирование данных. Кроме того, должно быть обеспечено, что мы не повторяем атрибуты с одним значением в каждом атрибуте с несколькими величинами.
Мы посмотрим на несколько примеров, чтобы помочь сделать эти идеи более ясными. Давайте сначала рассмотрим три основных вида нарушений 4НФ: •
тройные отношения;
•
скрытые атрибуты с несколькими величинами;
Q
состояние и другие атрибуты, для которых мы должны знать предыдущие значения.
Тройные отношения Мы кратко рассмотрели тройные отношения ранее в главе 3. Не все реальные отношения проявятся в отношениях с двоичным типом, и тройные отношения действительно являются весьма общими. Любые три (или больше) идентифицирующих или обязательных неидентифицирующих отношений в сущности, вероятно, вызовут неприятности. Возьмем в качестве примера ситуацию, когда мы разработали набор сущностей для обеспечения планирования презентаций, размещая информацию в сущностях s e s s i o n (презентация), p r e s e n t e r (презентатор) и room (помещение), где должна произойти презентация: roomSessionPresenter room session presenter
Здесь roomSessionPresenter
— сущность "помещение-презентация-презентатор".
Предположим также, что должен выполняться следующий набор очень распространенных бизнес-правил: •
В список может быть внесен более чем один презентатор, осуществляющий презентацию.
•
Презентация может занимать более чем одно помещение.
183
Глава 7 Следующий рисунок моделирует отношение: "презентатор представляет презентацию в помещении". presenter
room
presenterld
roomld roomName (AK1) Л V
roomSessionPresenter
|—-<>J firstName(AKI) lastname (AK1)
roomSessionPresenterld roomld (FK)(AK1) sessionld (FK)(AK1) presenterld (FK)(AK1)
session
•
sessionld name (AK1) startTime endTime
Здесь room — сущность "помещение", roomld — идентификатор помещения, roomName — название помещения, presenter — сущность "презентатор", presenterld — идентификатор презентатора, firstName — имя, lastName — фамилия, roomSessionPresenter — сущность "помещение-презентация-презентатор", roomSessionPresenterld — идентификатор помещения-презентации-презентатора, session — сущность "презентация", sessionld — идентификатор презентации, name — название (презентации), startTime — время начала, endTime — время завершения. Напоминание: каждая из этих величин может иметь значение NULL, как обозначено ромбом на противоположном конце соединяющей линии. Каждая из этих сущностей находится в НФБК; однако отношения между этими тремя сущностями сомнительны. Посмотрим на набор типовых данных:
184
Session (презентация)
Presenter (презентатор)
Room (помещение)
101
Davidson
River Room
202
Davidson
Stream Room
202
Hazel
Stream Room
404
Hawkins
Brook Room
404
Hawkins
Stream Room
Расширенные темы нормализации В первой строке нет никаких проблем, поскольку мы имеем одну строку для атрибута s e s s i o n — 101, который имеет один атрибут p r e s e n t e r —Дэвидсон и один атрибут room — River Room. Проблема возникает в следующих двух строках, поскольку один атрибут s e s s i o n — 202 имеет два различных атрибута p r e s e n t e r , но все же один атрибут room. Это вынуждает нас лишний раз повторить данные в атрибуте room, так как мы теперь храним в двух местах то, что s e s s i o n — 202 находится в Stream Room. Если s e s s i o n перемещается в другое место, мы должны изменить это в двух местах, и если забываем об этой особенности и корректируем room значением, которое в настоящее время не отображаем (например, с помощью искусственного ключа), то получим: 202
Davidson
Stream Room
202
Hazel
"изменено на Room"
В этом примере мы дублировали данные в атрибутах s e s s i o n и room, а для атрибута s e s s i o n , равного 404, дублируются данные атрибутов s e s s i o n и p r e s e n t e r . Реальная проблема с нашим сценарием возникает при добавлении или изменении наших данных. Если мы должны корректировать значение атрибута s e s s i o n , которое используют Davidson с Hazel в Stream Room, то потребуется изменить две строки. Аналогично, если изменяется значение room, потребуется изменить несколько строк. Когда сущности реализованы таким способом, мы можем даже не получить все строки, полностью заполненные, как эти. В следующей сущности мы видим набор строк, которые являются функционально эквивалентными набору в предыдущей сущности. Session (презентация)
Presenter (презентатор)
Room (помещение)
101
Davidson
101
River Room
202
Davidson
202
Stream Room
202
Hazel
404
Brook Room
404
Hawkins
404
Stream Room
В этом примере имеются пустые указатели для некоторых значений атрибута room и некоторых значений атрибута p r e s e n t e r . Мы устранили дублированные данные, но теперь все, что мы имеем — некоторые довольно странно смотрящиеся данные с пустыми указателями повсюду. Кроме того, нельзя разумно использовать пустые указатели в ситуации, когда мы не знаем значения сущности p r e s e n t e r для конкретного значения сущности s e s s i o n . Мы фактически размещаем набор данных, эквивалентный данным в предыдущем примере, но с данными в этой форме очень трудно работать.
185
Глава 7 Чтобы получить решение этой проблемы, сначала возьмем сущность p r e s e n t e r в качестве первичной сущности: presenterRoom 'presenter (FK) room presenter presenter
ч
J
presenterSessior presenter (FK) session
Л
I
J
Обратите внимание, что наша первоначальная реализация была неидентифицирующим отношением. Чтобы получить диаграммы настолько понятными, насколько возможно, посмотрим на отношения в терминах идентифицирующих отношений. Для этого мы возьмем данные в сущности roomSessionPresenter и разобьем их на следующие сущности: Presenter
Presenter
Room
Davidson
Davidson
River Room
Hazel
Davidson
Stream Room
Hawkins
Hazel
Stream Room
Hawkins
Stream Room
Hawkins
Brook Room
Presenter
Session
Davidson
101
Davidson
202
Hazel
404
Hawkins
404
Это, очевидно, не лучшее решение, потому что мы никогда не смогли бы определить, в каком помещении (room) проводится презентация (session), пока не будет назначен презентатор ( p r e s e n t e r ) . Аналогично, Davidson организует презентацию в River Room, a также и в Stream Room, и нет никакой связи с презентацией (session), которая проводится в соответствующем помещении (room). Когда мы разбиваем любые отношения и при этом теряем значения данных, такое разбиение называется декомпозицией с потерями. Это один из таких случаев и не лучшее решение нашей проблемы. 186
Расширенные темы нормализации Далее мы попробуем сосредоточиться на помещениях (room), в которых проводятся презентации: roomSession "room (FK) session
room room
^
J
roomPresenter room (FK) presenter
Л
i
Взяв данные и поместив их в сущности еще раз, получим: Room
Room
Presenter
River Room
River Room
Davidson
Brook Room
Stream Room
Davidson
Stream Room
Stream Room
Hazel
Stream Room
Hawkins
Brook Room
Hawkins
Room
Session
River Room
101
Stream Room
202
Brook Room
202
Stream Room
404
Снова это — декомпозиция с потерями и также неподходящее решение, потому что мы не способны определить, например, точно, кто представляет202 презентацию (session). Это происходит в Stream Room, a Davidson, Hazel и Hawkins — все представляют в Stream Room, но они не все представляют 202 презентацию. Поэтому мы должны еще раз рассмотреть другой проект. На сей раз мы сосредоточиваем наш проект на проводимых презентациях:
187
Глава 7 sessionroom 'room (FK) session V
session session
e
ч
)
sessionPresenter 'session.(FK) ^ presenter
Посмотрим на данные: Session
Room
101
River Room
202
Stream Room
404
Brook Room
404
Stream Room
Session
Presenter
101
Davidson
202
Davidson
202
Hazel
404
Hawkins
Наконец-то мы натолкнулись на решение проблемы. Из этих данных мы способны определить точно, кто что представляет и где, и мы не будем иметь никаких проблем, добавляя или удаляя презентаторов или даже изменяя помещения. Возьмем, например, презентацию (session) 404. Мы имеем следующие данные в сущностях sessionRoom и s e s s i o n P r e s e n t e r для этой презентации:
188
Session
Room
404
Brook Room
404
Stream Room
Session
Presenter
404
Hawkins
Расширенные темы нормализации Чтобы добавить презентатора по имени Evans к списку, мы просто добавляем другую строку: Session
Presenter
404
Hawkins
404
Evans
Таким образом, это — надлежащее разложение, и не создаст нам проблем в нашей первоначальной сущности. Наша окончательная модель выглядит следующим образом: session Presenter sessionPresenterld • roomld(FK)(AK1) sessionld(FK)(AK1) session session name (AK1) startTime endTime
presenter presenterld firstName(AKI) firstName(AKI)
sessionRoom session Roomld roomld(FK)(AK1) sessionld(FK)(AK1)
roomld roomName(AKI)
Теперь, когда мы задаем сущность session отдельно от сущности p r e s e n t e r , и пустые указатели больше не требуются в значениях внешнего ключа, то если хотим показать, что помещение (room) не было выбрано, то просто не создаем запись сущности sessionRoom. Так же мы поступаем, если пока не выбрали презентатора (presenter). Более важно, что мы теперь можем задавать несколько значений room для значения сущности session без возможных нарушений, и мы, конечно, можем не дублировать значения сущностей session и room. Если мы должны иметь дополнительные данные, которые расширяют понятие сущности s e s s i o n P r e s e n t e r , например, обозначить дополнительного презентатора ( p r e s e n t e r ) (или даже первого и второго презентатора), мы теперь имеем логическое место, чтобы хранить эту информацию. Обратите внимание, что, если бы мы пробовали хранить эту информацию в первоначальной сущности, это нарушило бы НФБК, так как а т р и б у т a l t e r n a t e P r e s e n t e r будет связан только с s e s s i o n и p r e s e n t e r , но не с room.
Скрытые атрибуты с несколькими
величинами
Мы рассмотрим некоторые атрибуты, которые назовем скрытыми, потому что они на первый взгляд не создают проблем. Чтобы попытаться проиллюстрировать эту ситуацию, рассмотрим следующую модель проекта: contact contactld name phoneld (FK) • addressld (FK) 1 1 1
address addressld odditional atttributes> „
phone phoneld odditional atttributes>
(Используемые идентификаторы см., например, в главе 5.) 189
Глава 7 Здесь возникает проблема, когда мы рассматриваем сущность c o n t a c t (партнер), так как мы имеем три атрибута: имя (name) партнера (предполагаем, что этот атрибут находится в 1НФ), номер телефона (phone) и адрес (address). Имя не создает проблем, поскольку каждый партнер (contact) имеет единственное имя, по которому мы обращаемся к нему, но в настоящее время много людей имеют больше одного адреса и номера телефона! Следовательно, мы имеем атрибуты с несколькими значениями и требуется их дальнейшая нормализация, чтобы разрешить возникающие проблемы. Чтобы учесть такие адреса и телефоны с несколькими значениями, мы могли бы изменить наш проект следующим образом: contact contactld name homeAddressId (FK) officeAddressId (FK) officePhoneld (FK) homePhoneld (FK) faxPhoneld (FK) mobilePhoneld (FK)
address - o addressld - o odditional attributes>
о phone phoned l о odditional attributes>
Пока наличие нескольких номеров телефона не является нарушением 1НФ (так как все они являются различными типами номеров телефона, а не разными значениями одного и того же типа), и мы должны ожидать проблем лишь в будущем. Так как мы просто добавили тип атрибута к его имени (например, homeAddressId, faxPhoneld), в дальнейшем мы можем получить проблемы с атрибутами, имеющими несколько значений, если, например, партнер имеет два факса или даже два мобильных телефона. Кроме того, мы находимся в неприятной ситуации, когда может потребоваться значение NULL для каждого из атрибутов, когда они не существуют. Это неудачное представление отношений. Например, если клиенту потребуется атрибут номера телефона офиса супруга (супруги) партнера, мы должны будем изменить модель, что, по всей вероятности, приведет нас к переписыванию кода приложения. Изменим далее проект таким образом, чтобы иметь отдельные сущности c o n t a c t (партнер) и c o n t a c t l n f o r m a t i o n (информация о партнере):
contact contactld name
contactlnformation contactlnfor mationld type contactld (FK)(AK1) phoneld (FK)(AK1) addressld (FK)(AK1)
I I I 1 1
address addressld Odditional attributes
ahone phoneld Odditional attributes
Атрибут type (тип) обозначает тип информации о партнере, которую мы размещаем в экземпляре сущности, так что мы теперь можем иметь экземпляр сущности c o n t a c t l n f o r m a t i o n с типом (type) Home (домашний) и добавить сюда адрес и номер телефона. Это теперь позволит нам добавлять столько номеров телефона и адресов, сколько потребуется пользователю. Однако так как адрес и телефон размещаются в одной и той же таблице, нам потребуются значения NULL там, где партнер имеет различное число домашних адресов и номеров телефона. 190
Расширенные темы нормализации На этой стадии мы должны принять решение относительно того, что хотим сделать. Мы можем пожелать, чтобы номер телефона был связан с адресом (например, объединение домашнего адреса с домашним номером телефона). В нашем случае мы сделаем следующее: разобьем сущность c o n t a c t l n f o r m a t i o n на сущности contactAddress и contactPhone (хотя это и не следует рассматривать как единственно возможное решение проблемы):
contact contactld name
contactAddress contactAddressId type contactld (FK)(AK1) (AK1) j contactPhone contactPhoneNumberld type contactld(FK)(AK1) (AK1)
Эта корректировка устранила зависимости с несколькими значениями, так как мы теперь можем иметь несколько адресов и несколько номеров телефона независимо друг от друга и можем определить столько типов, столько потребуется, без необходимости изменения наших структур сущностей. Однако мы можем сделать еще один дальнейший шаг, моделируя номер телефона и адрес как отдельные сущности в логической модели, и добавить домены сущностей для типов столбцов. Таким образом, мы можем предотвратить пользователей от печатания "Домашний", "Домовой", "Дом." и т. д., когда они подразумевают "домашний". Это также даст нам пользовательское конфигурируемое ограничение, так что мы можем добавлять дополнительные типы без необходимости изменять модель. Мы добавим атрибут d e s c r i p t i o n (описание) к сущностям доменов, позволяющий нам описать фактическую цель типа. Это учитывает такие ситуации, когда мы имеем тип адреса "На стороне", который является симпатичным стандартом для данной организации, но запутывающим пользователя в первый раз. Описание типа: "Адрес партнера при путешествии в расширенных коммерческих поездках" тогда могло бы быть назначено. Наша заключительная модель теперь выглядит так: addressType addressTypeld name description
I contactAddress fcontactld (FK) Л addressld (FK) •[addressTypeld (FK)J
contact contactld name
phoneNumberTypeld phoneNumberType name description
contactPhone (contactld (FK) Л phoneNumberld (FK) L I phoneNumberTypeld (FK) I 1 1
address addressld odditional attributes> (AK1)
phoneNumber phoneNumberld odditional attributes> (AK1)
1 9 1
Глава 7
Следует отметить, что я сделал дополнительные атрибуты сущностей address (адрес) и phoneNumber (номер телефона) вторичными ключами, чтобы избежать дублирования адреса каждый раз, когда он используется в системе. В этом случае, если мы имеем пять партнеров, которые имеют один и тот же адрес их офиса, нужно изменить адрес только в одном месте. То, что напоминает массовое убийство, может поэтому фактически дать выгоды, хотя, когда вы начнете физическое моделирование этих сущностей, нужно будет решить, следует ли реализовывать этот уровень, имея в виду проблемы настройки, которые могут возникнуть.
Предыстория атрибутов Мы можем также столкнуться с проблемами у нашей модели проекта в ситуации, когда нужно сохранить тип состояния информации для экземпляра некоторой сущности. Например, в следующей диаграмме мы создали две сущности, которые размещают характеристики заказа, а также сущность домена для состояния заказа: order orderld orderStatusTypeld (FK) taken Date verified Date sentToShippingDate fulfilledDate sentToBillingDate billedDate paymentReceivedDate
orderStatusType orderStatusTypeld name
L Здесь order — сущность "заказ"; orderld — идентификатор заказа; takenDate — дата принятия; verifiedDate — дата проверки; sentToShippingDate — дата отгрузки; billedDate — дата выставления счета; paymentReceivedDate — дата получения оплаты; orderStatusType — сущность "тип состояния заказа"; orderStatusTypeld — идентификатор типа состояния заказа; name — название. Проблема здесь состоит в том, что изменения состояния заказа (и, следовательно, значение атрибута orderStatusTypeld) связаны и со значениями дат в других полях, и с другими внешними факторами. Например, когда заказ принят от заказчика, атрибут takenDate следует заполнить текущей датой. Заказ тогда мог бы иметь состояние "Незаконченный". После того, как система оплаты заказчика была проверена, мы изменим атрибут v e r i f i e d D a t e на дату проверки и установим состояние на "В процессе". Состояние "В процессе" может фактически означать больше чем одна какая-либо операция, типа "отгрузка" или "выставление счета". Нас интересует здесь атрибут o r d e r S t a t u s T y p e l d в сущности o r d e r . Он содержит текущее состояние для экземпляра заказа. Как вы ответите на вопросы относительно того, когда заказ послан отделу отгрузки, или когда отдел проверки заказа проверил заказ? Разработчик моделей данных добавил бы несколько полей в сущность o r d e r , чтобы разместить эти части информации, но что если проверка однажды потерпела неудачу? Интересует это нас? И что означает дата f u l f i l l e d D a t e — когда заказ был или полностью отправлен, или отменен, либо, строго говоря, тогда, когда он был полностью отправлен, так что мы должны добавить другой атрибут — c a n c e l e d D a t e (дата отмены)?
192
Расширенные темы нормализации
Чтобы решить эту проблему, мы должны будем изменить нашу модель, чтобы позволить хранение нескольких значений для каждого из атрибутов, которые мы создали. orderStatus order orderld
orderStatusId orderld (FK) orderStatusTypeld (FK)(AK1) effective Date (AK1)
Здесь дополнительно orderStatus состояния заказа; effectiveDate
orderStatusType orderStatusTypeld name
состояние заказа; orderStatusId эффективная дата.
— идентификатор
Теперь всякий раз, когда изменяется состояние заказа, все, что мы должны сделать — это добавить запись в сущность o r d e r S t a t u s . В любом случае запись с последним значением атрибута e f f e c t i v e D a t e — текущее состояние. Это также позволило бы нам иметь больше одного значения состояния одновременно. Например, не все состояния осуществляются последовательным способом. В сущность o r d e r S t a t u s , например, вы можете поместить информацию о проверке счета-фактуры, затем, как только он будет проверен, поместить информацию об отгрузке и выставлении счета одновременно. С новой структурой, которую мы создали, когда наш заказ не может быть отгружен, мы можем сделать запись об этом. Мы можем также делать запись того, что клиент оплатил заказ. Обратите внимание, что в этом случае мы хотим моделировать только весь заказ, а не состояние отдельных компонент заказа. Решение этого типа ситуации в 4НФ будет иногда требовать диаграммы состояний для определения, каким образом достигнуто состояние заказа. Например, может использоваться следующая диаграмма, чтобы описать процесс выполнения заказа со времени, когда он был открыт, и до времени его завершения. (Отмена или изменение заказа или даже возврат заказа не будут рассматриваться в этом примере.)
193
Глава 7 Мы можем легко смоделировать этот вид простой диаграммы состояния с помощью одной дополнительной сущности: orderStatusTypeStateDiagram orderStatusTypeStateDiagramld fromOrderStatusTypeld (FK) toOrderStatusTypeld (FK) I I I l orderStatusType orderStatusTypeld
Здесь orderStatusTypeStateDiagram — сущность "диаграмма состояний заказа"; orderStatusTypeStateDiagramld — идентификатор диаграммы состояний заказа; fromOrderStatusTypeld — идентификатор начального состояния заказа; toOrderStatusTypeld — идентификатор конечного состояния заказа; orderStatusType— сущность "тип состояния заказа"; orderStatusTypeld — идентификатор типа состояния заказа; name — название. Пусть мы имеем семь состояний в сущности orderStatusType: OrderStatusTypeld (идентификатор типа состояния заказа)
Name (название)
1
Рассмотрение заказа
2
Проверен
3
Отгрузка
4
Выставление счета
5
Выполнен
6
Оплата получена
7
Закрыт
Чтобы определить нашу диаграмму состояний, нам нужны следующие экземпляры сущности orderStatusTypeStateDiagram (на сей раз удалим указатели и включим для ясности названия):
194
fromOrderStatusTypeld (идентификатор начального состояния)
Название
toOrderStatusTypeld (идентификатор конечного состояния)
Название
1
Рассмотрение заказа
2
Проверен
2
Проверен
3
Отгрузка
Расширенные темы нормализации
fromOrderStatusTypeld (идентификатор начального состояния)
Название
toOrderStatusTypeld (идентификатор конечного состояния)
Название
2
Проверен
4
Выставление счета
3
Отгрузка
5
Выполнен
4
Выставление счета
6
Оплата получена
5
Выполнен
7
Закрыт
6
Оплата получена
7
Закрыт
Таким образом, мы можем видеть, находимся ли мы в конкретном состоянии, в каком состоянии мы были до этого и в каком состоянии мы можем оказаться затем. Это также позволяет нам определить поток наших конкретных бизнес-правил вместо жесткого кодирования набора фиксированных значений. В этом случае наше состояние может пройти несколько значений, и фактически может быть несколько состояний в любое заданное время. Вы могли бы задокументировать бизнес-правила, точно очертив, как процесс работает в данной ситуации, и фактически это является обработкой исключений, которая требует большого количества времени, при этом до 80 процентов времени кодирования обычно тратится на исключения из правил.
Дополнительные нормальные формы В этом разделе мы кратко упомянем пару существующих дополнительных нормальных форм, хотя мы и не будем включать никаких примеров для них, поскольку они часто выглядят слишком заумными, и вообще реализуются только на стадии логического моделирования. Однако они заслуживают, по крайней мере, упоминания ради законченности темы. Пятая нормальная форма (5НФ) — в отличие от примеров, которые мы рассмотрели ранее, не всякие троичные отношения могут быть разбиты на две сущности, связанные с третьей. Если это возможно, то сущности находятся в 4НФ. Цель 5НФ состоит в том, чтобы гарантировать, что любые троичные отношения, которые все еще существуют у сущностей в 4НФ, могут быть расчленены на сущности и затем объединены друг с другом, чтобы сформировать первоначальную сущность. Если это нельзя сделать, то предполагается, что отношение содержит неистинную информацию. Следует сказать, что случаи, когда приходится иметь дело с 5НФ, очень специфичны и поэтому обычно игнорируются. Нормальная форма домен-ключ (НФДК) — сущность находится в НФДК, если каждое ограничение на сущность является логическим следствием определения ключей и доменов. Дональд Фагин был первым человеком, который придумал это формальное определение в 1981 г.
195
Глава 7 Рассмотрим, что каждый из этих терминов означает. Q
"Ключ" означает любой потенциальный ключ, который уникально определяет каждую строку в сущности.
•
"Домен" означает любое ограничение на вид данных, которые могут быть размещены в столбце. Это могут также быть предписанные данные, использующие внешние ключи, если результирующая сущность определена должным образом и по крайней мере находится в 4НФ.
Q
"Ограничение" указывает любое правило работы с атрибутами (это включает правила редактирования, ограничения взаимосвязи, функциональную зависимость, и многопараметрическую зависимость, но НЕ ограничения во времени).
Такая форма считается "совершенной" нормальной формой, так как сущность не имеет аномалий, связанных с добавлением и удалением, и должна быть в НФДК.
Денормализация Денормализация используется прежде всего, чтобы улучшить функционирование в случаях, когда перенормализованные структуры вызывают непроизводительные издержки процессора запросов и, в свою очередь, других процессов в SQL Server, или понизить сложность, чтобы сделать структуры более легкими для реализации. Как мы пытались отметить в этой главе, если удается аргументировать, что денормализация до ЗНФ может упростить запросы, сокращая число необходимых соединений, то остается риск получения аномалии данных. Любой дополнительный код, написанный для борьбы с этими аномалиями, должен быть продублирован в каждом приложении, которое использует БД, тем самым увеличивая вероятность человеческой ошибки. Необходимо сделать оценку, что сделать в данной ситуации — или выбрать несколько более медленное приложение (но со 100-процентной точностью), или более быстрое приложение, но с более низкой точностью. В книге утверждается, что в течение логического моделирования мы никогда не должны возвращаться от наших нормализованных структур, чтобы профилактически настроить реализацию наших приложений. Поскольку наша книга связана с OLTP-структурами БД, наиболее важная часть нашего проектирования заключается в том, что мы должны быть уверены, что наша логическая модель представляет все сущности и атрибуты, которые будут находиться в окончательной БД. В течение, и наиболее важно, после процесса физического моделирования могут быть убедительные причины для денормализации структуры или для улучшения функционирования, или для уменьшения сложности реализации, но ни одна из них не имеет отношения к логической модели. Мы всегда будем иметь меньшее количество проблем, если реализуем физически то, что является истинным логически, и поэтому я всегда сторонник подождать с денормализацией до физической стадии моделирования или, по крайней мере, пока не найдется неотразимая причина поступить таким образом (например, некоторая часть нашей системы слишком плоха).
196
Расширенные темы нормализации
Учебный пример Рассмотрим повторно нашу логическую модель, которую мы оставили в конце прошлой главы. Если мы используем то, что изучали в этой главе, для нашего учебного примера, то мы найдем три примера нарушений 4НФ. Эти нарушения, оказывается, более часты, чем можно было бы подумать, и только когда вы продумаете каждый атрибут и его возможные значения, то начнете понимать, что нужно расширить вашу модель. Пример 1 — Account. BalanceDate Этот первый пример возникает, когда рассматриваем атрибут balanceDate (дата баланса) в сущности account (счет), которую мы первоначально разработали: account bankld: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey number: AlphaNumericNumber (AK1.2) balanceDate: Date Баланс по счету будет составляться часто, иначе любые пропущенные сделки или путаницы банка могут привести к тому, что мы случайно исчерпаем средства на счете. (Если даже и не так, он будет так же плох, как мой текущий счет, когда я вышел из колледжа!) Так как мы размещаем данные каждый раз, когда счет балансируется, атрибут balanceDate представляет по отношению к счету зависимость с несколькими значениями и должен быть перераспределен. Разбив эту зависимость, мы введем сущностьaccountReconcile (согласование счета), а также атрибуты r e c o n c i l e D a t e (дата согласования) и periodEndDate (период до последней даты). Они учитывают тот факт, что может быть некоторое время ожидания между временем получения счета и его фактическим согласованием. Атрибут r e c o n c i l e D a t e определяется как дата, когда был физически получен баланс по счету. Как последний штрих, мы можем добавить атрибутstatementld к сущности accountReconcile, чтобы записывать любую информацию, которую банк посылает пользователю, чтобы сбалансировать счет. Будет также лучше использовать s t a t e m e n t l d как часть первичного ключа, так как он определяет согласование. С этими введенными корректировками наша логическая модель теперь будет выглядеть следующим образом: bank bankld: PrimaryKey name: Name (AK1.1)
bankld: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey
number: AlphaNumericNumber (AK1.2)
имеет баланс счета в accountReconcile
используется ля согласования счета в
bankld: PrimaryKey (FK) (AK1.1) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.2) total Debits: Amount totaiCredits: Amount balancingltems: Balancingltems
bankld: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey statementld: PrimaryKey (FK) reeoncileDate: Date periodEndDate: Date
1 9 7
Глава 7 Пример 2 — Payee.Addressld и Payee.PhoneNumberld Второй пример обычно связан с большинством предприятий, у которых имеется несколько почтовых адресов и несколько номеров телефона. Мы рассмотрели это ранее в данной главе, когда говорили о скрытых атрибутах, и нам нужно применить тот же самый процесс к нашему учебному примеру. Наша первоначальная модель содержала единственные сущности address и phoneNumber, как показано ниже:
определяет информацию о номере контактного телефона дли
payee payeeld: PrimaryKey
phoneNumber phoneNumberld: PrimaryKey countryCode:CountryCode areaCode: AreaCode (AK1.2) exchange: Exchange (AK1.3) number: PhoneNumberPart (AK1.4) extension: Extension (AK1.5)
phoneNumberld: PrimaryKey (FK) addressld: Address (FK) name: Name (AK1.1)
address addressld: PrimaryKey определяет расположение
city: City state: StateCode zipCode: ZipCode
Чтобы иметь возможность вводить несколько номеров телефона, мы создаем сущность payeePhoneNumber (номер телефона получателя платежа) и добавляем сущность, определяющую тип домена, — phoneNumberType (тип номера телефона), чтобы иметь возможность выполнить классификацию этих номеров. Подобным способом мы создаем сущности payeeAddress (адрес получателя платежа) и addressType (тип адреса). Наш скорректированный проект модели теперь выглядит так:
198
Расширенные темы нормализации
phoneNumberType описывает тип
phoneNumberTypeld: PrimaryKey name: Name description: Description
payeePhoneNumber phoneNumberld: PrimaryKey (FK) payeeld: PrimaryKey (FK) phoneNumberTypeld: PrimaryKey (FK) phoneNumber phoneNumberld: PrimaryKey
связан с номером телефона через связан с получателем платежа через
countryCode:CountryCode (AK1.1) areaCode: AreaCode (AK1.2) exchange: Exchange(AK1.3) number: PhoneNumberPart (AK1.4) extension: Extension (AK1.5)
payee payeeld: PrimaryKey name: Name (AK1.1)
payeeAddress payeeld: PrimaryKsy(FK) addressld: PrimaryKey
связан с адресом через
связан с получателем платежа через addressld: PrimaryKey (FK) addresslineld: PrimaryKey
addressTypeld: PrimaryKey (FK) описывает тип
address addressld: PrimaryKey
addressType addressTypeld: PrimaryKey
addressLJne: AddressDescriptiveLine .sortOrderld: SortOrderld
city: City state: StateCode zipCode: ZipCode
name: Name description: Description
имеет дополнительную информацию в
Обратите внимание, что теперь получатель платежа может иметь несколько номеров телефона и несколько адресов в гибкой форме, которая позволяет нам определять новые типы адреса без какого-либо дополнительного кодирования; так что у пользователя никогда не возникнет проблем, когда он или она должны будут ввести адрес. Однако если мы повнимательней посмотрим на сущность a d d r e s s , здесь все еще имеется проблема, связанная с тем, что атрибуты c i t y (город) и s t a t e (штат) являются связанными (при этом не являются ключами), чем нарушают ЗНФ, находясь в одной сущности. Оба из них также связаны с атрибутом zipCode (почтовый индекс), поскольку если мы знаем почтовый индекс, то можем определить город и штат. Разбиение сущности a d d r e s s приводит нас к следующей измененной структуре: addressld: PrimaryKey (FK) addressLJneld: PrimaryKey addressUne: AddressDescriptiveLine .sortOrderld: SortOrderld address addressld: PrimaryKey city: City state: StateCode zipCode: ZipCode * f~ I I I I
определяет почтовый регион в
I I zipCode |
zipCodeld: Primarykey zipCode: Z/pCode
имеет дополнительную информацию в state stateld: PrimaryKey code: addressStateCode определяет * расположение
определяет расположение
[stateld: PrimaryKey (FK) cityld: PrimaryKey
1 9 9
Глава 7 Следующая стадия процесса должна определить точное отношение между сущностями c i t y (город) и zipCode (почтовый индекс). Так как один почтовый индекс может охватить целый маленький город, но большой город может иметь несколько почтовых индексов, отношение имеет вид "многие ко многим". Создав сущности c i t y , s t a t e и a d d r e s s , нам теперь нужно скомбинировать их атрибуты, чтобы можно было искать адреса, используя полные записи. Чтобы сделать это, создадим новую сущность zipCodeCityRef erence (связь почтового индекса и города), которая объединяет атрибуты City Id (идентификатор города), S t a t e l d (идентификатор штата) и zipCodeld (идентификатор почтового индекса) и включает новый атрибут (признак) PrimaryFl (первичный флаг). Первичный флаг позволит нам устанавливать, какой город в zipCode является "первичным", — атрибут, который почтовая служба продаст наряду с БД почтовых индексов и городов. Таким образом, наш пользователь может обычно получить все детали адреса, вводя в приложение только атрибут zipCode. Организация этого в нашей модели даст: addressLine addressld: PrimaryKey (FK) addressLineld: PrimaryKey addressLine: AddressDescriptiveLine sortOrderld: SortOrderld address addressld: PrimaryKey имеет дополнительную информацию в city: City state: StateCode zipCode: ZipCode
определяет почтовый регион в
J? преде ляет_ расположение
zipCode zipCodeld: Primarykey zipCode: ZipCode
zipCodeCityReference /^stateld: PrimaryKey (FK) cityld: PrimaryKey (FK) определяет возможные zipCodeld: PrimaryKey (FK) города через primaryFI: PrimaryFl
state stateld: PrimaryKey code: addressStateCode определяет расположение
city [stateld: PrimaryKey (FK) cityld: PrimaryKey [name: Name
определяет возможные почтовые индексы через
Пример 3 — check. UsageType Атрибут usageType (тип использования) сначала предполагался как способ указания, для каких целей используется чек. На практике, однако, один чек может быть написан для многих различных целей, и, следовательно, мы имеем нарушение 4НФ, так как пользователь не сможет поместить каждое использование чека в единственное значение. Чтобы устранить это, мы создаем сущность checkUsage (проверка использования) и связанный с ней тип домена, подобно тому, что мы видели в предыдущем примере. Это дает нам следующую модель: 200
Расширенные темы нормализации
check bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transaction Id: PrimaryKey (FK) payeeld: PrimaryKey (FK) usageType: Code signature: Blob checkUsage
bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) имеет использование, transactionld: PrimaryKey (FK) определенное в checkUsageTypeld: PrimaryKey (FK)
задает категорию чека с помощью checkUsageType checkUsageTypeld: PrimaryKey name: Name description: Description parentCheckUsageTypeld: PrimaryKey (FK)I
является
Обратите внимание, что сущность checkUsageType (тип использования чека) включает рекурсивное отношение, которое позволит пользователю создавать деревоизадеТуреэ (типы использования) для классификации чека, так как мы теперь позволяем одному экземпляру checkUsageType принадлежать другому такому же экземпляру. Например, мы могли бы определить следующее дерево: ! [)ebit %**£ Bill
Business Expense
Charity
^ \ Utilities
House Payment
Здесь Debit — дебет; Bill — счет; Business Expense — расходы по сделке; Charity — благотворительный взнос; Utilities — коммунальные услуги; House Payment — плата за жилище.
201
Глава 7 Теперь (используя алгоритм, которого мы коснемся в части книги, посвященной физической реализации, в главе 12) мы можем суммировать все счета, которые в нашем дереве включают коммунальные услуги и плату за жилище, или все дебеты, которые включают расходы по сделкам и благотворительные взносы. Я поместил этот пример в книгу, чтобы показать, что с рекурсивными отношениями не так уж сложно иметь дело. Основа алгоритма довольно проста, как только вы поймете это. Сначала вы получаете все строки, у которых нет предков (дебет), и спускаетесь по дереву на один уровень одновременно. Сначала это будет (счет, расходы по сделке и благотворительный взнос), а на следующем проходе — (коммунальные услуги и плата за жилище). На каждом проходе дерева мы просто суммируем величины и добавляем их к предыдущим уровням. Как уже упомянуто, мы сделаем очень простой пример этой операции в главе 12. Теперь нам нужно завершить сущность checkUsage. Мы позволили пользователю определять многократные использования чека, но мы не разрешили распределять по этим использованиям сумму чека. Имеются два возможных решения данной проблемы: или размещать использование чека в виде процента, или в виде количества. В нашем решении мы выберем распределение в виде количества. Такое распределение дает пользователю способ дискретно назначить суммы на каждый тип использования. Если сумма чека изменится, распределение также должно быть изменено. Если вы распределяете в виде процента, слишком легко забыть то, какие значения вы намеревались установить. Итак, мы завершаем со следующим набором сущностей и новым атрибутом allocationAmount (сумма распределения): check bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) payeeld: PrimaryKey (FK) usageType: Code signature: Blob checkUsage bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) имеет использование, transactionld: PrimaryKey (FK) определенное в checkUsageTypeld: PrimaryKey (FK) aliocationAmount: Amount задает категорию чека с помощью checkUsageType checkUsageTypeld: PrimaryKey name: Name description: Description parentCheckllsageTypeld: PrimaryKey (FK)I
202
Расширенные темы нормализации К нашей документации мы должны будем добавить наше первое определенное бизнес-правило отношения между сущностями. Мы не будем допускать, чтобы сумма значений checkUsage + allocationAmount была больше, чем сумма сделки по чеку. Нужно также иметь другое правило, которое устанавливает, что сумма значений checkUsage + allocationAmount никогда не должна быть меньше, чем сумма сделки по чеку. Это невозможно реализовать в SQL Server 2000, но должно быть отмечено для реализации промежуточного уровня или внешнего интерфейса, так как это тип ограничения, которое SQL Server не поддерживает (БД или ограничение конца сделки, где после сделки мы проверяем условие, чтобы удостовериться, что все сбалансировано). Мы теперь имеем большинство атрибутов, которые нужны для действительной реализации нашей системы контроля. Не будем увеличивать атрибуты модели, добавляя новую информацию в этот нормализованный пример. На данной стадии это очень важно, поскольку мы будем неоднократно пересматривать модель. Это вдвойне важно в реальной практической реализации. Настало также время рассмотреть модель данных с клиентом, чтобы быть уверенным, что она точно моделирует их бизнес. В последующих главах мы иногда будем должны делать изменения в наших структурах, поскольку мы сделаем некоторый дальнейший обзор и начнем рассматривать, как пользователи будут взаимодействовать с данными. Заключительная логическая модель данных нашей системы следующая:
Резюме В этой главе мы представили дополнительные критерии для нормализации наших БД помимо форм, представленных в предыдущей главе. Сейчас уместно быстро суммировать характер нормальных форм, которые мы выделили в этой и предыдущей главах: 203
Глава 7
Первая нормальная форма
Все атрибуты должны быть элементарные, одно единственное значение на атрибут Все экземпляры сущности должны содержать одно и то же число значений Все экземпляры сущности должны быть различны
Вторая нормальная форма
Сущность должна быть в 1НФ Все атрибуты должны определяться всем ключом, но не частью ключа
Третья нормальная форма
Сущность должна быть в 2НФ Сущность находится в ЗНФ, если каждый неключевой атрибут определяется атрибутами ключа Все атрибуты должны определяться ключом и только ключом
Нормальная форма Бойса-Кодда
Все атрибуты полностью зависят от ключа; все атрибуты должны определяться ключом и только ключом Сущность находится в НФБК, если каждый детерминант является ключом
Четвертая нормальная форма
Сущность должна быть в НФБК У сущности должно быть не более одной зависимости с несколькими значениями
Вся информация, которую мы изучили в этих двух главах, может быть сведена в одно предложение:
Все сущности должны быть отношениями и должны быть посвящены одной теме!
Формируя их для идентификации ЕДИНСТВЕННОГО человека, места, вещи или идеи, мы уменьшаем все аномалии корректировки хранимых нами данных. В этом случае, каждый раз, когда мы изменяем атрибут сущности, мы просто изменяем описание этой единственной сущности и не вызовем непредвиденное изменение в данных. Определение отношения было дано в главе 3. Оно охватывает 1НФ, а единственная тема — все остальные формы. Процесс продвижения через все нормальные формы приведет нас к ситуации, когда потребуется очень небольшое специальное кодирование, чтобы сохранять правильные данные. В следующей главе мы рассмотрим проблемы, которые остались на стадии логического проектирования нашего проекта.
204
Завершение фазы логического проектирования Введение Подытожим, чего мы достигли к данному моменту: •
мы собрали всю информацию;
•
мы разобрались с реляционной теорией;
•
мы узнали кое-что относительно UML;
•
мы построили модель действий;
•
мы разработали логическую модель нашей БД, используя IDEFlX-методологию моделирования;
•
мы нормализовали наши данные.
Теперь мы почти готовы начать нашу реализацию БД. Но имеется несколько проблем, которые следует выяснить прежде, чем мы начнем создавать таблицы и писать код. Так как мы все еще на стадии проектирования, где весьма мало общаемся с пользователями, сейчас — лучшее время, чтобы получить ответы на последние немногие вопросы. Что нужно делать, если логическая модель неточна, ведь это не физическая модель, и некоторые сказали бы, что это то, что отличает блестящий проект от просто адекватного или даже нестандартного проекта. На этой стадии мы можем также рассмотреть организацию встречи со спонсором проекта для последней проверки, учли ли мы все требования. Рассмотрите также подготовку итогового документа для поддержания документации.
Глава 8 С точки зрения БД мы теперь имеем очень хорошо структурированную модель данных, ясно представляем, кто является пользователями, и детализировали различные процессы, которые нужно реализовать. В этой главе мы вкратце рассмотрим задачи, которые нужно завершить до начала стадии реализации, и исследуем окончательные технические проблемы, которые являются важными для реализованной должным образом БД: •
использование данных;
•
размеры;
•
план проекта;
•
заключительный обзор документации.
Каждая из них важна для процесса реализации БД, которую мы начнем в следующей главе. Учет этих важных технических проблем теперь позволит вам сделать изменения, если вы кое-что пропустили, — это может случиться, поскольку никто не совершенен. Другой хорошей практикой при проектировании БД (или любого другого типа приложения того же назначения) является привлечение других разработчиков, чтобы утвердить ваш проект, а также проверить на них ваши идеи. Проведите встречу с ними и объясните цели приложения, главные препятствия, известный бизнес-процесс и предложенный вами проект. Представление вашей модели данных на простых чертежах позволит вашим партнерам легко понять и высказать критические замечания о работе, даже тем, кто не является архитектором БД. Дайте им просмотреть все, оценить то, что было сделано, и высказать свое мнение. Этот "процесс экспертной оценки" почти всегда сэкономит вам время и энергию, так как любые предлагаемые приложения будут иметь аналогию с другими приложениями. Привлекая помощь других, вы можете поучиться тому, как они подходят к своим проектам. Используйте идеи и предложения, предлагаемые в обзорах и критических материалах, чтобы улучшить вашу модель, и добавьте больше деталей. Наконец, независимо от того, насколько хорошо вы провели интервью на начальной стадии, вам редко будет удаваться получить все необходимые данные от ваших клиентов. Выберите время, чтобы встретиться с клиентами и задать им еще несколько вопросов — это то время, когда вы должны выяснить все, что еще не ясно. Вы также должны указать на то, какие предположения были сделаны в течение логического проектирования, и каковы возможные последствия этого. Это — не так много, чтобы защитить тылы и оставить клиента с реалистическими ожиданиями проекта. Если проект не соответствует желаниям клиента, следует тут же критически разработать детали, даже если результатом будет возврат к началу процесса, и выявить все то, что было неправильно истолковано, или даже отменить проект целиком. Истина будет всегда установлена, и именно сейчас для пользователя то время, когда он может обнаружить печальные детали того, как мало вы понимаете в его бизнесе, а не после того, как БД реализована и выясняется, что с ней нельзя работать.
Использование данных Пока мы наметили, в каких данных мы нуждаемся и для чего это необходимо. Мы использовали копии сообщений клиентов, экранов и документов, чтобы попробовать подобрать данные, которые нам нужно будет хранить, но окончательно не рассмотрели, для чего эти данные будут фактически использоваться. В этом разделе мы рассмотрим:
206
Завершение фазы логического проектирования
•
создание отчетов;
•
использование данных и право доступа;
•
взаимодействие с внешними системами;
•
планы преобразования данных.
Создание отчетов Создание отчетов — одна из наиболее важных частей СУБД. Почти каждая частица данных, которую мы собираемся моделировать, должна будет просматриваться кем-то в организации тем или иным способом или непосредственно, или, возможно, с использованием вычислений. При разработке многих проектов создание отчетов рассматривается как дополнение, и часто проектирование и реализация отчетов откладываются до тех пор, пока проект не будет близок к завершению. Мы это, конечно, не защищаем, но так бывает. Это бывает обычно с корпоративными/внутренними проектами, но случается также и с консалтинговыми/внешними проектами. Следующий график Гантта показывает типичную временную последовательность проектирования:
№
Задача Реализация модуля 1
Начало 2/1/2001
Конец 02/03/2001
Продолжительность (дни)
Февр. 2001
Март 2001
Апр.2001
2/4 2/17 2/78 2/25 3/4 3/17 3/7 £ 3/25 4/1 4/8 4/15
22
Реализация модуля 2 28/02/2001 26/03/2001
19
Реализация модуля 3
13/03/2001 13/04/2001
24
Проектирование отчетов
1 3 / 0 4 / 2 0 0 1 13/04/2001
Реализация отчетов
16/04/2001 18/04/2001
Даже когда для отчетов намечено разумное количество времени, они часто планируются на конец периода проектирования, так как мы действительно должны закончить разрабатывать БД прежде, чем мы сможем начать формировать запросы и фактически организовывать отчеты. Тогда, если время проектирования затягивается, что может произойти по той или иной причине, время, отведенное для отчетов, может сократиться даже еще в большей степени. Иногда, когда клиенты начинают понимать, что процесс проектирования завершается, а отчеты все еще не разработаны, они начинают говорить и создавать проблемы, пока не убедятся, что их отчеты рассматриваются. Однако даже если клиенты не подпрыгивают с криком, насколько для них важны отчеты, пользователи всегда чрезвычайно заинтересованы в отчетах, поскольку это именно то, с помощью чего они управляют своим бизнесом. Отчеты — это то, как они получают информацию из БД и видят ее, классифицируют, делят, планируют на основе ее и, что наиболее важно, — платят за них. В некоторых случаях отчеты — единственная частица проекта, которую пользователь будет видеть. Так что же нам делать? Прежде чем закончить стадию логического проектирования, очень важно удостовериться, что мы понимаем то, что пользователь хочет получить от БД. Даже если мы не можем сделать полный проект отчетов, по крайней мере, нам нужно понять то, что нужно иметь, и получить общую структуру требований. 8 1868
207
Глава 8 Стратегия получения отчетов Имеются два различных типа отчета, с которыми мы должны иметь дело: Q
•
Стандартные отчеты — отчеты, которые пользователь должен получать во время своей работы. Часто это — очень простые отчеты. Они обычно составляют ядро потребностей пользователя. Специализированные отчеты — отчеты, которые позволяют пользователям идти дальше, давая им нечто большее, чем средняя информация, о которой они сами могут подумать.
Это — две четко различающиеся вещи. Стандартные отчеты — отчеты, которые не требуют никаких специальных навыков для разработки, например, отображение баланса счета. Предполагается, что ваша система будет в состоянии формировать такие отчеты. Специализированные отчеты не столь очевидны — они используют некоторые специальные навыки или понимание бизнес-процесса, например, объединение счетов многонациональной корпорации. На этой стадии проектирования чрезвычайно важно планировать оба типа отчетов. Стандартные отчеты Большинство СУБД имеет некоторый фиксированный набор требуемых отчетов, которые являются необходимыми для управления бизнесом. Они могут быть ежедневными, еженедельными или ежегодными отчетами, все предназначены для отображения ситуации, смоделированной в СУБД, которую вы создали. Типичные примеры, которые мы встречаем на каждом шагу, могут включать: •
Оценки Нельсона — имеется где-то БД, охватывающая все телевизионные передачи, которая контролирует просмотр передач людьми на основе устройств, находящихся в их домах. Ежедневно, ежечасно и еженедельно она (БД) оценивает телевизионные передачи по числу пользователей, которые их смотрят. Эти числа используются, чтобы определить, какие передачи оставить, а какие закрыть.
•
Продажи билетов на кинофильмы — каждую неделю в газетах печатается список десяти лучших кинофильмов недели. Эта информация поступает от типичной БД точно так же, как любая другая информация.
Менее типичными примерами могли бы быть: •
•
Счет за коммунальные услуги — отчет о показаниях счетчика, величина текущего начисления, предыдущего начисления, и, возможно, даже начисления за последний год. Отчет о движении — в местности, где я недавно жил (Виргиния Бич, штат Виргиния), были камеры и датчики, установленные на всем протяжении шоссе, которые обеспечивали жителей пригородной зоны текущими отчетами о потоках движения.
Специализированные отчеты Кроме возможностей стандартных отчетов пользователи, с которыми вы разговариваете, будут иметь специальные пожелания отображения данных, хранящихся в БД. Важно встретиться с клиентами, чтобы определить, как эти специальные требования формирования отчетов могут быть встречены. Пользователи часто имеют чрезмерные идеи в своих головах, ожидая, что получится, и их не всегда следует слушать. 208
Завершение фазы логического проектирования Имейте в виду, что приемы, используемые для реализации отчета или хранения данных с этой целью, не важны во время интервью — хотя нужно быть осторожными, чтобы не зародить нереалистичные идеи в умах клиентов (вы должны создать ненаписанную спецификацию). Самая простая используемая стратегия состоит в том, чтобы попросить пользователей перечислить вопросы, которые они, очевидно, хотели бы задать относительно данных, которые вы будете хранить. Это служит двум очень важным целям. Мы сможем: •
понять, какие типы фактов их будут интересовать в будущем;
•
заметить, не пропустили ли мы что-нибудь в нашем первоначальном проекте БД.
Взяв наши примеры отчетов, можно было бы задать следующие типы вопросов: •
Оценки Нельсона — пользователь, вероятно, хотел бы знать кое-что относительно людей, которые просматривали программу ТВ, наблюдали ли они ее или записывали на пленку, и просматривали ли они ленту после того, как была записана программа.
•
Продажи билетов на кинофильмы — возраст и категория людей, просматривающих кинофильм, когда они видели его, сколько было людей в средней группе зрителей, и сколько людей ушло, потому что они посчитали кинофильм унылым.
•
Счет за коммунальные услуги — как на каждодневные значения платежей действует погода, выходные дни и другие ситуации.
•
Отчет о движении — анализ тенденций того, сколько транспортных средств находится на дороге, как движение протекало в течение дня, влияние несчастных случаев и т. д.
Имеется много проблем в разработке отчетов для пользователя. Фактически, сначала это может даже показаться невозможным. Однако классифицируя требования и оценивая их, вам будет проще сообщить, что возможно, а что — нет. Требования отчетов должны быть расположены по приоритетам и затем, принимая во внимание время и ресурсы, разбиты на необходимые и перспективные потребности. Обратите внимание, что, как проектировщик, вы не должны бояться говорить нет, если выдвинутое требование фактически невозможно выполнить. Возьмем счет за коммунальные услуги, который мы ранее упомянули: •
Необходимые требования: чтение показаний счетчика, величина текущего начисления, предыдущего начисления.
•
Перспективные требования: начисления за последний год.
•
Невозможные требования: ежедневные значения (не могут быть выполнены из-за ограничений аналогового оборудования).
Документ, документ и еще раз документ! Эта информация для кого-то будет важна. Если вы — архитектор данных с большим коллективом, то не сможете сами задать эти типы вопросов — это будет кто-то другой. И никогда не скрывайте "невозможно" от членов команды проектировщиков. Это нельзя скрывать — именно потому, что мы имеем целые команды.
209
Глава 8
Прототипы отчетов Как только мы определили, какие отчеты являются необходимыми, нам нужно построить прототипы того, как отчеты должны будут выглядеть, и какие данные будут помещены в них. Почти каждый проект компьютерной системы использует прототипы экранов, которые входят в приложение, как копию того, что клиент хочет и что мы ему поставим. Мы должны установить тот же вид и для самих отчетов. Когда наступает время проверки, имеются две точки зрения, с какими видами данных проверять прототип отчета. Я приведу здесь для вас обе: •
Использование нереальных данных. Это не означает, что мы представляем клиенту причудливо нереалистичные данные, но берем типовые данные достаточно нереальными, чтобы избежать длинных обсуждений того, какова точная ситуация в отчете, которая никогда на самом деле не случится, хотя это могло бы и быть, и т. д. Прототипы отчетов просто отражают возможные наборы данных для конкретной используемой в отчете ситуации, однако, не точную ситуацию. Во многих случаях точные текущие данные клиента полностью не будут известны до более поздних этапов процесса разработки.
•
Использование реальных данных. В этом случае прототип отчета формируется как можно ближе к окончательному отчету, это дает пользователю возможность разобраться, как хорошо поняты данные. Использование действительных и подобных данных приближает пользователя к реальному проекту. Кроме того, это обеспечивает раннюю обратную связь проектировщикам отчета относительно того, понимают ли они требования и произведет ли их проект желаемый результат.
Я обычно отношусь к первому лагерю, но это полностью зависит от ситуации, клиента, и, что наиболее важно, от того, насколько отчеты важны для процесса. В некоторых случаях они являются центральными, в других случаях они вспомогательны. Если мы должны разобраться с отчетом прежде, чем остальная часть системы будет успешно сформирована, то использование реалистических данных даст лучшие результаты.
Определение использования данных и права доступа Понимание, какие данные будут использоваться, и кто будет использовать их — важный фактор для завершения логической стадии. Прежде всего, нам нужно проверить, что наше понимание следующих моментов все еще актуально:
210
•
Кто будет использовать данные, и что они будут делать с ними?
•
Кому будут позволено что делать и с какими данными?
•
Откуда они будут осуществлять доступ к данным?
•
Какие приложения будут использоваться для доступа к данным?
•
Сколько пользователей будут иметь доступ к данным?
•
Скольким из этих пользователей будет нужно обращаться к данным одновременно?
Завершение фазы логического проектирования Эти пункты должны быть рассмотрены на ранних стадиях анализа. Однако наш долг быть уверенными, что мы полностью понимаем потребности в конкретных данных; рассматривая это, мы, возможно, обнаружим новые данные на стадии логического моделирования. Теперь перейдем к сложным структурным проблемам, вроде безопасности и структуры доступа к данным.
Безопасность Если клиент не имеет заранее заданную структуру для защиты своих данных от нежелательной корректировки/доступа, вопрос безопасности часто выдвигается на первый план даже по сравнению с формированием отчетов. Хотя это, возможно, может быть наименее важной частью проекта для архитектора данных, но, конечно, важно для администратора БД, так что данный вопрос нельзя просто игнорировать. Когда это связано с отчетами, безопасность становится ультра-важной проблемой. Ее важность зависит от информации, содержащейся в отчетах, и которую клиент может использовать, но редко в организации будут желать, чтобы каждый пользователь имел доступ ко всем данным. Хороший подход для проектировщика — установить "таблицу классификации отчетов" и получить от спонсора проекта инструкции по классификации каждого отчета, и какие пользователи или группы пользователей будут иметь доступ к каждому отчету. Включение информации о безопасности в описания действий каждого исполнителя в системе — очень полезная техника. Безопасность подразумевается каждым процессом на диаграмме действий, так как, если действия включают каждый возможный отчет и каждый возможный процесс, это будет означать, что когда действие не связано с пользователем, то он не может исполнять некоторую функцию. Например, возьмем наш пример действий из текста в главе 5:
Издатель
Принтер
211
Глава 8 Каждое из действий будет модулем системы, так что автор должен иметь доступ ко всем данным, используемым в модуле, который помечен названием "Написание книги". Никто другой не имеет доступа к этим данным, пока они используются в отдельном модуле следующим образом:
Автор
Теперь рассмотрим, что произойдет, если мы добавим отчет, названный "Отчет о книге". Для этого мы создадим действие и отметим, что оно содержит очень немного информации типа имени, описания, рисунка, служебных описаний и денежной информации:
Автор
Автор может рассматривать отчет, но его рассмотрение должно быть ограничено и не включать такие вещи, например, как детальная денежная информация. В структуры определения исполнителей мы могли бы включить: Имя
Автор
Описание
Исполнитель, который пишет текст книги
Замечания по безопасности
Может просматривать только упрощенную версию отчета о книге, который содержит служебную информацию
Конечно, вы будете, вероятно, использовать некоторый вид программных средств, который может работать с такой документацией или фиксирует используемые поля. При использовании "отчета о книге" мы будем фиксировать версии и ограничения безопасности, когда он будет использоваться.
Известные ограничения структуры Мы рассмотрим в книге определение окончательной структуры позже, но я представлю его здесь, поскольку на этой стадии процесса проектирования, вероятно, это последний шанс, когда вы можете спросить клиента относительно фактических требований, которые будут наложены на БД. Вот о чем нам нужно узнать: • •
212
Тип связи — как клиенты будут фактически осуществлять доступ к данным: через Web-страницы, через локальные приложения? Проблемы программирования — с каким языком программирования они связаны? Или мы можем использовать все, что захотим?
Завершение фазы логического проектирования •
Вероятное требование к системе — минимальное и максимальное число пользователей системы и требуемое время функционирования.
•
Ограничения аппаратных средств — это могло быть по политическим или денежным соображениям, но представьте, что мы полагаем, что пользователь собирается потратить на оборудование миллион долларов, а он рассчитывал на пять тысяч.
•
Физические ограничения — проблемы сетей подобны недостатку полосы пропускания, или даже лучше, отсутствию нужного помещения, так что приходится разместить некоторое оборудование в защищенном шкафу отдельно от сервера.
Мы должны получить некоторую возможность регулировать функционирование (как быстро работает БД) и масштабируемость (как изменяется функционирование при увеличении числа пользователей) прежде, чем мы перейдем к физической реализации.
Взаимодействие с внешними системами Когда я говорю относительно "внешних систем", я имею в виду другие БД, которые не являются соответствующей частью нашей БД, но с которыми нам нужно взаимодействовать. Общие примеры включают любой имеющийся в наличии механизм (система людских ресурсов, система платежных ведомостей и т. д.), который есть у организации и который нужно связать с нашей БД. Главная проблема с внешними системами состоит в том, что по тем или иным причинам некоторые были плохо разработаны. Немного имеющихся в наличии систем, все еще использующихся сегодня, были разработаны как системы на универсальных ЭВМ и в спешке перенесены на реляционную СУБД, потому что это было модно, при малом понимании реляционного программирования. Примеры в этом разделе основаны на реальных ситуациях в мире, так что, даже если они и покажутся нереальными, но все-таки копируют конкретные проблемы в реальных процессах. Они включены главным образом как шокирующие примеры, но и для того, чтобы дать представление, что может произойти, если мы слишком поздно займемся внешними системами, с которыми нужно установить взаимодействие.
Пример 1 — Проблематичная система планирования ресурса предприятия Приложение, связанное с покупкой и спросом, с которым я работал, имело почти тысячу таблиц, но только одну таблицу доменов, которая служила в качестве доменов для всех таблиц системы. Вместо отдельных таблиц, смоделированных примерно таким образом: specificTableDomain specific! ableDomainld name description Расширенные атрибуты specificTableDomain>
Здесь specificTableDomain — сущность "специальная таблица доменов"; specificTableDomainld — идентификатор элементов специальной таблицы доменов; name — название; description — описание.
213
Глава 8
... в каждом месте, где они нужны, например, тип оплаты, тип ресурса, тип закупки, и т. д. некоторые программисты предпочитают реализовать единую таблицу подобно следующей: genericTableDomain genericTableDomainld refersToTable value name description
Расширенные атрибуты для таблицы 1 > <Расширенные атрибуты для таблицы 2> Расширенные атрибуты для таблицы N>
Здесь genericTableDomain — сущность "таблица генерации доменов"; genericTableDomainld — идентификатор элементов таблицы генерации доменов; refersToTable — ссылка на таблицу; value — значение; name — имя; description — описание. Идея программиста, должно быть, состояла в том, что при использовании единственной таблицы, чтобы выполнять много операций, ему/ей потребуется только один редактор, чтобы управлять всеми значениями доменов. К сожалению, имеется весьма мало задач при использовании SQL с данными в такой реализации. Мы игнорируем очевидные трудности реализации (подобные невозможности использовать декларативную ссылочную целостность, так как специализированные таблицы требуют ключевых величин, которые невозможны в любой СУРБД) и рассмотрим проблемы преобразования данных. Было более восьмидесяти различных значений в столбце refersToTable таблицы доменов. Так как этот метод создания таблицы домена был нестандартным, никакие средства преобразования данных не поддерживали этот тип запроса, а новая система, которую мы разрабатывали, конечно, не повторяла этот проект. Очень много запросов пришлось вручную закодировать, чтобы извлечь данные.
Пример 2 — Другой пример "Как не надо делать " Следующий пример возник с программным продуктом третьего лица, с которым я имел "удовольствие" взаимодействовать, и это позволяет взглянуть на предыдущую ситуацию как на весьма легкую. Я реализовывал систему, которая должна была взаимодействовать с одной из наших бизнес-программ, которую мы использовали долгое время и не могли заменить. Система хранила свои данные в SQL Server, но была реализована таким образом, чтобы препятствовать взаимодействию с другими системами. БД использовала структуры, которые маскировали названия столбцов таким образом, что вы должны были пройти их интерфейсы, чтобы разгадать, какая таблица находится в ней. Например, следующая таблица показывает метаданные, которые используются, чтобы определить поля таблицы:
214
Фактическое название таблицы
Фактическое название столбца
Логическое имя поля (имя поля, которое мы должны знать для поля)
Тип
Возможность значения NULL
TABLE1
COLUMN 1
Keyld
varchar(30)
Нет
TABLE1
COLUMN2
CreatedBy
sysname
Да
Завершение фазы логического проектирования
Фактическое название таблицы
Фактическое название столбца
Логическое имя поля (имя поля, которое мы должны знать для поля)
Тип
Возможность значения NULL
TABLE 1
C0LUMN3
CreateDate
int
Нет
TABLE 1
C0LUMN5
ModifyDate
datetime
Нет
TABLE 1
C0LUMN6
Fieldl
varchar(10)
Да
TABLE1
C0LUMN7
Field2
varchar(10)
Да
TABLE1
C0LUMN8
Field3
varchar(15)
Да
TABLE 1
COLUMNn
FieldN
varchar(30)
Да
Проектировщики продукта сделали это, чтобы скрыть реализацию от пользователей. Как механизм защиты от понимания структур конкурентами это работает поразительно. Однако построение интерфейса для восстановления данных из этих таблиц является сущим кошмаром, требуя отчеты типа: SELECT
FROM WHERE
COLUMN7 as fieldl, COLUMNS as field2, COLUMN9 as field3, COLUMN10 as field4, COLUMN11 as field5, CCLUMNn as fieldN TABLE1 COLUMN9 - '<значение поля>'
Выполнение преобразований данных путем создания операторов для изменения данных в таблицах из нашей новой системы было отвратительной и отнимающей много времени задачей.
Пример 3 — Системы, страдающие от денормализации Одна из наиболее частых проблем, с которой вы будете сталкиваться при работе с другими системами — денормализация. Эти системы могут быть денормализованными по многим причинам. Возможно, проектировщик БД не был очень опытен или, возможно, эта система — наследство времени, когда технология не поддерживала полную нормализацию. Нисколько не удивительно видеть БД с одной, двумя, или, возможно, тремя таблицами, когда требуется пятьдесят, чтобы должным образом конкретизировать решение. Например, мы знаем, что таблица, которая моделирует человека, должна иметь только атрибуты, которые определенно описывают человека, но плохо разработанная таблица, может напоминать следующее:
215
Глава 8
person socialSecurityNumber firstName lastName homeAdddressLinei horrieAddressLine2 homeCity homeState homeZipCode officeAddressLinei officeZipCode spouseName dateOfBirth homeTelephoneNumber officeTelephoneNumber computerUserName computerF&ssword printerPassword salesTotalQuarteri ofYear2000 salesTotalQuarter2ofYear2000 salesTotalQuarter3ofYear2000 salesTotalQuarter4ofYear2000 childNamel childName2 <и это долго продолжается подобным образом>
Здесь person — сущность "человек"; socialSecurityNumber — номер социального страхования; firstName — имя; lastName — фамилия; homeAddressLinel — первая строка домашнего адреса; homeAddressLine2 — вторая строка домашнего адреса; homeCity — город проживания; homeState — штат проживания; homeZipCode — домашний почтовый индекс; officeAddressLinel — первая строка адреса офиса; officeZipCode — почтовый индекс офиса; spouseName — имя супруга (супруги); dateOfBirth — дата рождения; homeTelephoneNumber — номер домашнего телефона; officeTelephoneNumber — номер телефона офиса; computerUserName — название компьютера пользователя; computerPassword — пароль компьютера; printerPassword — пароль принтера; salesTotalQuarterlofYear2000 — общие продажи за 1-й квартал 2000 г.; salesTotalQuarter2ofYear2000, — общие продажи за 2-й квартал 2000 г.; salesTotalQuarter3ofYear2000 — общие продажи за 3-й квартал 2000 г.; salesTotalQuarter4ofYear2000 — общие продажи за 4-й квартал 2000 г.; childNamel — имя первого ребенка; childName2 — имя второго ребенка. К счастью, вы не встретитесь с таблицей, так смехотворно денормализованной, как эта, но даже наименьший уровень денормализации в исходной системе будет требовать сложных запросов, которые будут загружать данные в наши таблицы.
Дополнительные проблемы, возникающие при взаимодействии с системами третьих лиц Одна из основных проблем при взаимодействии с внешними системами третьих лиц — связывание. Причина в том, что мы моделировали всю нашу БД, не заботясь о внешних связях, в то время как для большинства случаев важно, чтобы наши системы свободно связывались с системами, которые находятся вне нашего контроля. Обычно хорошая идея 216
Завершение фазы логического проектирования импортирования данных из внешних таблиц в структуры, которые мы разработали, — это помещать данные в набор промежуточных таблиц, которые будут работать как импортирующая прослойка независимо от внешней используемой системы. Другая техника объединения с внешними системами, которые содержат подобные, но различные структуры данных, использует построение представлений данных, содержащихся во внешних системах. Предположим, что вы имеете систему хранения запасов с ядром, представляющим собой таблицу, которая хранит данные в специфической структуре, и систему кассового контроля, которая хранит их в несколько отличающемся формате, но из которой нам нужен доступ к той же самой информации. Абстрактный слой, созданный с этой целью, позволяет модифицировать основную систему без необходимости изменять внешнюю систему кассового контроля. Другая серьезная причина идентификации всех внешних систем, с которыми нам нужно взаимодействовать, — это то, что мы не хотим начать размещать данные с информацией, которая уже существует и доступна нам. Нет никакого смысла в создании новой таблицы служащих, если внешняя система имеет таблицу служащих, которую мы можем использовать.
Планы преобразования данных Нужно также учесть тот факт, что мы, возможно, заменяем систему, которая используется в настоящее время, даже если это система на бумажных носителях. В то время как преобразование данных — не столь уж логическая проблема проекта, нам все-таки нужен эскиз планов преобразования. Много проектов терпят неудачу из-за недостатка планов получения данных из существующей системы. Если ваша БД реализована на SQL Server 2000 должным образом, это будет, вероятно, намного лучше, чем предыдущая система, но люди будут не слишком счастливы, если потребуются огромные усилия для перехода от старого к новому. Еще хуже, если вы к тому же измените правила работы с данными, которые не позволят использовать данные предыдущей системы, особенно когда они уникальны. Часто большинство старых данных не будет вписываться в новую БД без некоторого их приглаживания из-за измененного программирования и/или проекта, используемого в старой системе.
Планирование требуемых объемов Требуемый объем определяется как "непосредственное или косвенное измерение объема" и связан с определением, как будут расти ваши таблицы и БД. Этот тип информации будет очень полезен, когда мы начнем физическое моделирование. Обычно вы получаете некоторые понятия относительно размера таблиц, которые вы разрабатываете, из бесед с клиентом и рассмотрения предыдущих систем, которые вы заменяете. Требуемый объем — в большей мере относится к вопросам физического моделирования, и мы рассмотрим это должным образом позже в книге. Однако лучше начать подбирать некоторую основную информацию, чтобы мы могли оценить размер БД и ее рост. Имеется несколько основных частей информации, которую вы можете начать собирать в этот момент процесса проектирования: Q
Средняя длина атрибутов данных (включая текст и BLOB-данные).
•
Процент данных, который будет заполнен для каждого атрибута. 217
Глава 8 •
Сколько данных будет, вероятно, в таблице первоначально.
•
Число строк в таблице и как это число будет расти.
•
Сколь долгой ожидается жизнь проекта.
Например, в нашей БД заказчика мы могли бы иметь следующую таблицу значений, которые помогут оценить размеры БД: Название таблицы
Начальные строки
Увеличение строк в месяц
Максимальное число строк
customerType (тип заказчика)
4
0
20
customer (заказчик)
0
30
3000
order (заказ)
0
1000
Не ограничено
В этом случае мы говорим, что начнем с 4 строк в таблице customerType (тип заказчика) и не ожидаем большого роста, поэтому планируем максимум 20 строк. В таблицесиэ^тег (заказчик) мы начнем без данных, но ожидаем добавлять 30 строк в месяц, с ожидаемым потолком в 3000 строк. В таблице order (заказ) мы надеемся, что будем продолжать добавлять 1000 строк каждый месяц. Наличие этого вида информации в начале планирования аппаратных средств ЭВМ будет чрезвычайно полезно.
План проекта В этот момент я должен упомянуть о необходимости плана проекта. Каждый шаг по планированию будет относительно прост, если вы прошли через процесс создания БД десять или двадцать раз. Должны быть включены следующие элементы информации: •
Какие задачи должны быть выполнены.
•
Какие задачи должны быть закончены в первую очередь.
•
Достигнутые этапы, которые должен рассмотреть пользователь.
•
Сколько времени займет каждая задача.
Задача создания плана проекта непосредственно связана с проектом управления командой проектировщиков и по существу находится за границами возможностей этой книги. Дополнительная информация может быть найдена в Professional VB6 Project Management, Jake Sturm (Wrox Press, ISBN 1-861002-93-9).
Окончательный обзор документации В течение процесса проектирования нашей системы мы накопили тонны документации. Окончательный обзор означает точно то, что он означает. Вся документация должна быть пролистана, исправлена и синхронизирована. Этот шаг часто пропускается или существенно сокращается по сравнению с планом проекта, когда нехватает времени, но дело в том, что 218
Завершение фазы логического проектирования даже при хорошей документации могут быть большие различия. В процессе проектирования часто нарушается синхронизм документации, особенно когда вовлечена большая команда проектировщиков. Как пример, я был архитектором данных проекта для предприятия обрабатывающей промышленности среднего размера. Суть проекта заключалась в размещении измерений образцов изделий, которые они производили, для целей управления качеством. Мы собрали информацию относительно того, что они делали, что они хотели делать и как они будут использовать данные. Мы определили их потребности, сымпровизировали экраны, и разработали БД для решения их проблем. Проблема состояла в том, что в то время, как мы поняли основные принципы их потребностей в формировании отчетов, мы не понимали все, что требовалось: •
Формирование отчетов было достаточно сложным из-за запутанных вычислений (один из запросов содержал более 250 строк кода), так что когда мы смоделировали тестовый запрос и получили результаты, мы не могли точно проверить, как они будут использовать данные. Требовалось более пятидесяти старых измерений, чтобы вычислить новое значение, и что произойдет, если кто-то вернется и изменит одно из тех старых показателей ...? Они хотели иметь графики с последней сотней вычисленных значений, так что вычислениям налету не было альтернативы. Мы только не могли понять, как эти расчетные значения можно использовать или как сделать их более ценными для персонала.
•
Далее, мы не могли понять организационную структуру компании, с которой мы работали. Мы имели дело с двумя людьми, которые управляли проектом, и они казались достаточно опытными, но впечатление, к сожалению, оказалось обманчивым. Во время стадии проектирования основной партнер пользователя все еще обучался. К концу шестимесячного проекта они, наконец, поняли точные потребности для системы, которую мы разрабатывали, но это было слишком поздно по времени.
•
Третья проблема была самой плохой. Мы не сумели должным образом рассмотреть нашу документацию и удостовериться, что все, что было согласовано, было записано. Произошло это потому, что был контракт на время исполнения и доступные материалы. К сожалению, нужно было дать им оценку предстоящих затрат. Мы получили величину, неправильную, по крайней мере, на 200%, и проект потребовал для завершения в три раза больше затрат. В результате мы потеряли последующую работу, которая досталась другой консалтинговой фирме.
Важная мораль этой истории — то, что все эти три проблемы испарились бы, если бы мы полностью рассмотрели документацию и заставили клиентов отказаться от претензий. Если бы весь наш план был придирчиво сформирован, дополнительное время не потребовалось бы, — это было бы предсказано. Вместо этого мы получили неприятность, потому что у нас были другие планы работы. Не всегда возможно документировать каждую деталь системы, которую вы намереваетесь создать. Однако чрезвычайно важно на этой стадии рассмотреть документацию, удостовериться в том, что все определенное вами, может действительно быть создано за то время, которое вы предложили, и что требования к вам не слишком серьезны, чтобы нельзя было завершить работу. Независимо от типа проекта, все это — корпоративность, время и материалы, или установленная цена — всегда будет иметь для вас смысл, чтобы защитить себя от некачественной документации.
219
Глава 8
Дальнейшие требования Как заключительный шаг, мы должны задокументировать любые последующие требования, которые могут возникнуть у клиента в отношении системы, которую мы создаем. Ваша работа как проектировщика — постараться определить дальнейшие планы проектирования, и какие могут возникнуть требования в результате этого. Везде, где это возможно, проект должен быть изменен, чтобы их учесть, пока это существенно не увеличивает затраты. Вы будете выглядеть довольно глупо (и к тому же намного более несостоятельным), если через шесть месяцев после завершения проекта клиент попросит сделать, как он считает, тривиальные изменения, а вы должны будете сообщить ему, что необходимо шесть месяцев, чтобы их реализовать, потому что вам потребуется изменить 50 таблиц, особенно, когда вы знали, что это могло бы быть возможным требованием с начала проектирования. Небольшое предварительное планирование могло бы означать благоприятный сдвиг во времени всего в две недели, счастливого заказчика, и возможный будущий бизнес. Очевидно, что вы не должны загубить себя, пытаясь пересмотреть каждую потребность или прихоть клиента, — вы должны отдавать себе отчет в этом. Будьте честны с клиентом. "Если мы затратим дополнительные два дня, добавляя X, это будет сделать намного легче, чем добавить Y, если это потребуется в будущем". Если клиент говорит, "Ха! Нам это никогда не потребуется", тогда все ясно, но следует попробовать получить это в письменной форме! Будьте действенными. Как профессиональный проектировщик, вы должны думать за пределами простых границ, установленных клиентом. Предположение о возможных будущих улучшениях и планирование их показывает, что вы понимаете бизнес клиента и редко вредите банковскому балансу.
Учебный пример Когда мы последний раз покинули учебный пример, мы создали модель наших данных, создали модель действий и были готовы начать создание БД. Но это еще не все — у нас все еще нет некоторых частей информации, которую нужно собрать.
Использование данных В нашей диаграмме действий мы определили двух различных пользователей системы: основного пользователя счета (тратящего средства) и клерка бухгалтерского учета (регистратора). Снова возьмем интервью у клиента, чтобы попытаться определить, какие действия могли бы быть выполнены над данными. При этих обсуждениях мы выясняем, что нужно добавить несколько дополнительных действий и дополнительного исполнителя. Исполнитель, которого прежде всего можно определить, — пользователь-менеджер, пользователь, который просматривает резюме счетов и действия других пользователей счета. Мы определили три новых действия: итоговые отчеты, отчеты пользователя и предупреждения о счетах. Обсудим точно, какие потребуются отчеты в следующих подразделах, но предупреждения о счетах определены как сообщения, посылаемые пользователю, чтобы сообщить ему, что счет требует некоторого внимания, типа общего превышения счета.
220
Завершение фазы логического проектирования
С Информация).
Л^
Г Ввод сделки J \
/
г \
/
1 Оси польз
осчете^^/Y
1л
( ^
Ввод чека — ^
С
Баланс
«использует>>\
\ / \
;
\
\\
к^^ ^ \
J
\
\\ \ v
.
У^~
"^\
\/ Оператор у Чзагрузки^/
С 1овнои
\ \ \ ователь \ \ шта \ \
\| Ч.
Изъятие Денег
\ бухгалтерского \ учета
) J
\ \ \ V ^ Организация >^ \ Ч депозита у
щ
^ ~ —
\
Y
Выписка
п
)
' Z.
/1
/
( Итоговые у^ Ч ^ ^ отчеты у
/ /
( Отчеты ^ Х / Чпользователей/
/
/
Менеджер
i/ /
/
/ ( ПредупреждениеУ
221
Глава 8
Отчеты Когда мы спрашиваем пользователей, какие отчеты они должны иметь, то получаем следующий список: • Резюме счета — просто катетеризированный список сделок, основанных на поле checkUsage (использование чека). Этот отчет должен позволить пользователю определить уровень показанных подкатегорий. Например, они могли бы просто запросить обо всех дебетах и кредитах, или другого уровня, где они могут видеть основную классификацию — все счета, расходы по сделкам и сделанные благотворительные взносы, как мы показали в примере таблицы checkUsageType (тип использования чека) в предыдущей главе. • Активность пользователя — список всех сделок, связанных с данным пользователем, или просто сгруппированный по пользователям. Также нужно иметь возможность показать любые сделки, не связанные с пользователем, который фактически потратил деньги. • Баланс счета — список всех счетов и балансов. Дальнейшее обсуждение приводит к тому, что пользователи упомянули о специализированных отчетах: • •
•
Тенденции расходов — очень полезный отчет, который довольно легко программировать, но это обычно не требуется; этот отчет дает тенденции того, куда идут деньги, в графической, более легкой для понимания манере, чем отчет-резюме по счету. Оценка последующего баланса — хотелось бы взять расход в течение предыдущих месяцев и экстраполировать его, чтобы получить оценку возможных затрат в будущих месяцах. Это было бы похоже на бюджет. Такой отчет также был бы полезен при формировании бюджетов. Автоматическая выписка чека — в будущем хотелось бы иметь средство автоматической оплаты счета, в том числе иметь автоматическую оплату с некоторыми интеллектуальными включениями для ситуаций, где требуемая сумма недоступна для счета и т. д.
Пользователи также хотели бы формировать витрины данных, основанные на имеющихся данных, чтобы получать многогрупповые отчеты, основанные на получателе платежа, местоположении получателя платежа, откуда поступают деньги, какое было количество денег и т. д. Это, конечно, сверх и вне наших текущих возможностей, но очень интересно об этом знать. Обратите внимание, что мы обнаружили, по крайней мере, один новый элемент данных и новую таблицу для нашей БД в отчете о деятельности пользователя. Так что мы добавляем таблицу u s e r (пользователь) и связываем ее с чеком и депозитом подобно следующему: directWithdrawal ''bankld: PrimaryKey (FK) ^ accountld: PrimaryKey (FK) transaction 1 d: Primary Key (FK) | userld: Primaryh£y(FK) J V 1
осуществляет
user \y userid: Primarykey user name: UserName firstName: RrstName middleName: MiddleName °" lastName: LastName
222
check accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) userld: PrimaryKey (FK) payeeld: PrimaryKey (FK) usageType: Code signature: Blob j выписывает
Завершение фазы логического проектирования Здесь обозначения те же, что и на соответствующих предыдущих рисунках. Это позволит нам отслеживать пользователя, который выписывает чек. Кстати, это — другой пример того, почему мы должны пройти полный процесс проектирования, не пропуская никаких шагов, ни одного неперевернутого камня. Прототип отчета Теперь мы создадим прототип отчета из тех, которые мы внесли в список в предыдущем разделе. Чтобы сэкономить место, в этом учебном примере я выбрал для целей иллюстрации только один отчет — отчет-резюме счета. Account Summary 00/00/000 Com patty Nam e
<'™
Вжк Frem < воЕ> E>«D0e» тош | DeUt 1 С If 4 1
• XXXX-XX "|хххх>х
Account Activity Check Usage ОгафТ Check Vfage В п п ( 3 — Cheowege осадеБи>8сец> t Cheousepr CXcKCCxbjra*» TJtii«ckUsagr drop? Checkuwgf o » a * * check usage a m i s CheckUtage ОгадрбСХЪфгаф I Check Usage 0«CM*d0>9«xtii c£ckut^
XwoCXx 5оо<хлх
ххххЛ
checkwaB^»a*6ai.e«x4.i
|ЗВ(ХЗО5Г
«^вд^иадбви.дпч.г
1ХХККЖ
courage Ofcn*!jU>9ra* 3 CheckUsagr CHa_pbdki>9i0k«> ^ CKeckU
JXXXXXX
Как вы можете видеть, мы добавили простое резюме к основным требованиям, которые нам выдали. Тогда мы имеем каждую из полученных в итоге групп типов использования чеков, и суммы, перечисленные во втором столбце. Мы создадим прототипы для каждого из трех требуемых отчетов. Будем заниматься здесь именно ими, поскольку они должны быть хорошо обработаны нашими разработанными структурами. Специализированными отчетами, которые требовали пользователи, можно было бы заняться в следующей итерации системы, которая последует в нашей работе. 223
Глава 8
Взаимодействие с внешними системами Единственная внешняя система, с которой мы должны будем иметь дело, принадлежит банку. Как вы можете вспомнить из исходных документов интервью, мы имеем формат автоматического регистра банка, повторенного здесь: Column
Data type
Required
Transaction Date
Date Only
Yes
Transaction Number
String(20)
Yes
Description
String(lOO)
Yes
Item Amount
Money
Yes
Transaction. Transaction Date Transaction Number
Обозначения см. в главе 4. В бумажной версии они имеют дополнительные итоговые типы значений, которые мы очевидно не получим из электронной версии. Мы будем, наверное, желать сделать поля нашего отчета, заполненные нулевыми значениями, или, возможно, заполнить их из итоговых элементов. Мы также должны добавить несколько атрибутов к нашей модели данных для s t a t e m e n t l t e m s (элементы отчета) и обращаем внимание, что даже не рассматриваем, как элемент будет согласован. Так что мы корректируем нашу модель данных следующим образом: account bankld: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey
statement
number: AlphaNumericNumber (AK1.2)
преобразует фонды с помощью ввода
transaction bankld: PrimaryKey (FK)(AK1.1) accountld: PrimaryKey (FK)(AK1.2) transactionld: PrimaryKey statementld: PrimaryKey (FK) statementltemld: PrimaryKey (FK) date: Date number: AlphaNumericNumber (AK1.3) description: Description amount: Amount type: Code
224 I! I
bankld: PrimaryKey (FK) (AK1.1) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.2) totalDebits: Amount totalCredits: Amount balancingltems: Balancingltems
имеет элементы в используется для согласования statementltem счета bankld: PrimaryKey (FK) с помощью
statementld: PrimaryKey (FK) statementltemld: PrimaryKey •--О date: Date number: AlphaNumericNumber description: Description amount: Amount
Завершение фазы логического проектирования Обратите внимание, что мы добавили четыре атрибута к statement Item (элемент отчета — см. эту сущность в гл. 6) для каждого из полей в загружаемых пунктах отчета банка. Мы также обращаем внимание, что для использования этих пунктов для баланса нужно связать каждый пункт, используемый для баланса, с пунктом в регистре.
Так же как и в реальной жизни, мы обнаруживаем новые вещи относительно наших представлений, поскольку продвигаемся вперед. Обратите внимание, что мы построили твердую основу в наших предыдущих главах и все, что теперь собираемся делать, — заняться вылизыванием, залатыванием дыр, которые мы вначале не замечали. На этом этапе мы должны попытаться избежать "паралича анализа". Постоянно анализируя, непрерывно опрашивая клиентов и внося все больше и больше изменений в проект, мы никогда не перейдем к стадии реализации. В некоторый момент под проектом должна быть поставлена точка, даже если он и не совершенен на 100 процентов. С этими недостатками придется иметь дело на стадии реализации.
План преобразования данных В нашей системе клиент решил начать с нуля с регистром чека; следовательно, нам не нужно осуществлять никаких преобразований данных. Мы просто начнем счет с кредита или дебета для начального баланса. Это — самый простой тип плана преобразования данных, то есть, ничего не нужно. Поскольку мы заменяем бумажную систему, то если бы клиенты хотели сделать преобразование данных, им потребовалось бы ввести все данные вручную. Часто это невозможно, поскольку слишком дорого и проблематично.
Требуемые объемы Для определения требуемых объемов мы строим простую таблицу, содержащую детали всех других таблиц нашей БД, с ожидаемым числом строк, которые таблицы будут первоначально содержать. Лучший способ оценки этого состоит в том, чтобы взять важные таблицы системы, как показано в нашей модели данных из главы 7, и начать с них. Будем считать, что хорошей отправной точкой для наших оценок была бы таблица^апзас^д-оп (сделка). Тогда мы просто спрашиваем у пользователя относительно каждой таблицы или группы таблиц, и экстраполируем это на некоторые другие таблицы, особенно те, которые мы сами добавили. Обратите внимание, что мы также будем должны собрать некоторую статистику по столбцам. Сделаем весьма немного решений относительно того, как реализованы столбцы, а пользователи могут помочь в оценке их значений. Вы можете также обратиться к существующей системе как к руководству. Возьмем сначала таблицу сделок. В нашем случае пользователи решили начинать с нуля, без фактических сделок. Они ожидают иметь около 300 сделок в месяц, из которых 245 являются чеками, 5 — депозитами, и 50 — прямыми изъятиями.
225
Глава 8 Отсюда мы получаем оценки числа различных получателей платежа, с которых можно начать, и сколько ожидается добавлять. Обычно мы предполагаем остальную часть количества параметров, зависящей от этих значений. Например, из оценки 245 чеков в месяц мы предполагаем, что они имеют 100 получателей платежа, которым эти чеки предназначены, и, вероятно, будет добавлено 30 новых получателей платежа в месяц. Очевидно, чем большую исходную информацию вы получаете от клиентов, тем более достоверны будут эти оценки. Мы завершим следующей таблицей оценок, которую следует рассмотреть с клиентом. Начальное число строк
Увеличение числа строк в месяц
Максимальное число строк
account (счет)
1
0
20
accountReconcile (согласование счета)
0
1
36
address (адрес)
50
30
600
address Line (строка адреса)
55
35
700
address Type (тип адреса)
5
0
10
bank (банк)
1
0
5
check (чек) checkRegister (регистр чека)
0
245
10000
0
1
36
checkUsage (использование чека)
75
400
12000
CheckUsageType (тип использования чека) c i t y (город)
20
0
40
450
20
25000
d e p o s i t (депозит)
0
5
4000
directWithdrawal (прямое изъятие) payee (получатель платежа)
0
50
1000
100
30
300
PayeeAddress (адрес получателя платежа) payeePhoneNumber (номер телефона получателя платежа) phoneNumber (номер телефона)
100
30
600
100
30
600
50
30
600
PhoneNumberType (тип номера телефона) s t a t e (штат)
5
0
10
50
0
70
s t a t e m e n t (отчет) s t a t e m e n t l t e m (элемент отчета)
0
1
36
0
300
15000
t r a n s a c t i o n (сделка)
0
300
15000
user
10
2
50
1000
10
99999
Название таблицы
(пользователь)
zipCode (почтовый индекс) 226
Завершение фазы логического проектирования Вот описание содержания таблицы:
•
Начальное число строк — сколько строк, как мы ожидаем, будет добавлено к таблице, когда система начинает функционировать (нуль указывает, что нет никаких строк).
•
Увеличение числа строк в месяц — число строк, на которое, как мы ожидаем, увеличится таблица (нуль означает отсутствие роста). Максимальное число строк строк.
грубая оценка максимального числа возможных
План проектирования Сделаем некоторые оценки относительно длительности процесса проектирования в виде плана проектирования. Например, чтобы закончить задачу создания этой БД, мы могли бы воспользоваться диаграммой Гантта, наподобие:
:Конек
Задача Физическое моделирование Проектирование защиты данных
5ЛДС01
Продолжи^ гельность ? (дни)
Июнь 2001
Май 2001 5/6
5/13
5/20
5/27
10 6/17 6/24
Июль 2001 ?):-: 7/S . 7/15
5/7Й001 6/1Д 001
19
Создание кода доступа
6Ж2001
7/5Й001
24
Создание отчетов
7/5Ш01
8Л5Д001
30
Нам нужно конкретизировать каждую задачу, например: Физическое моделирование: Выбор типов данных — 1 день Проектирование оптимистических механизмов блокировки — 2 дня Или кое-что вроде этого. Количество времени для задач будет, конечно, субъективным, но должно быть основано на предыдущем опыте, а не на том, сколько страниц Дилберта мы надеемся прочитать, когда сидим за своим столом и делаем вид, что заняты. Поскольку план проектирования вне возможностей этой книги, больше мы не будем это обсуждать.
227
Глава 8
Безопасность Она основана на нашей диаграмме действий, приведенной ранее в этой главе:
Основной пользователь счета
Клерк бухгалтерского учета
Менеджер
228
Завершение фазы логического проектирования Так как мы добавляли новую информацию, как только ее обнаруживали, это должно дать полную картину того, как мы должны будем реализовать безопасность. Мы можем видеть, что есть три роли пользователей и несколько функциональных групп действий, которые следует рассмотреть. Каждый отчет также определяется диаграммой действий. Эта диаграмма действий будет нашим шаблоном для обеспечения безопасности позже при реализации действий.
Окончательный обзор документации Как только мы завершим работу, мы складываем все наши бумаги, делаем копии, распределяем их и назначаем встречу несколькими днями позже. Во время этой встречи наша цель состоит в том, чтобы получить подписи; согласовать то, что все, что требуется в нашей документации, соответствует пожеланиям клиента. Это — то, что я люблю называть отправной точкой. Как только пользователь подпишет копии документов системы, вы готовы начать создание системы. Вы больше не должны тратить много времени на пользователя, за исключением сообщений об успехах. Это не говорит о том, что наши планы незыблемы, поскольку мы, вероятно, найдем вещи, в которых не преуспели в достаточной мере. Хотелось бы надеяться, тем не менее, что любые изменения будут в форме дополнительных атрибутов, а не целого модуля таблиц вместе с новыми экранами.
Резюме Цель, правда, недостижимая, логического проектирования состоит в том, чтобы обнаружить все, что нам нужно для реализации системы. Как только начнется работа по реализации системы, будет очень трудно изменить планы. Однако я предполагаю, что когда вы прочитаете эту книгу, то, по крайней мере, будете иметь здравый смысл, чтобы не продолжать процесс "проектирование — оценка — повторение" до тех пор, пока вас не охватит "паралич анализа", но найдете золотую середину, когда проект готов для реализации. На протяжении всей части книги, связанной с логическим проектированием, мы старались так проектировать систему, чтобы избегать изменения основного направления проектирования всякий раз, когда приходилось "делать откат". Мы собрали данные, взяли интервью у клиентов, детально изучили документацию, построили модели наших данных и, наконец, нормализовали их, так что данные стали близки к реализации. В этой главе мы очень приблизились к физической реализации в наших попытках завершить эту часть процесса. Рассмотрено планирование наших потребностей в формировании данных от проблем безопасности до формирования отчетов. Также добавлен подход к плану проектирования и сделаны некоторые оценки относительно размера и роста данных, которые будут храниться в таблицах. Как только эта стадия завершена, мы заново рассматриваем проект — возвращаемся, чтобы удостовериться, что мы разработали то, что намереваемся создать, и что любые изменения по сравнению с первоначальными требованиями были согласованы. Если нам повезет, наша документация будет теперь в таком состоянии, что не нужно фактически возвращаться назад и начинать все заново.
229
Глава 8 Результатом стадии логического проектирования являются чертежи наподобие тех, которые разрабатывает архитектор здания совместно с клиентом, его надежды и мечты относительно системы, далее увязка их с действительностью и физикой, требуемой, чтобы фактически построить это. Как только клиент одобрит окончательные чертежи, вызываются субподрядчики, чтобы добавить стены, осветительную арматуру, водопровод, провода и всякие другие части, которые входят в состав физической структуры. Наши чертежи мало чем отличаются от таких чертежей зданий — с описаниями, моделями, диаграммами и т. д. для физической реализации. Следующая стадия проекта теперь уведет нас довольно далеко от этого очень абстрактного мира, в котором мы находились, в стадию, где мы начинаем ковать гвозди, устанавливать стены и двигаться к БД, где наши данные могут жить и быть безопасными.
230
Часть II — Физическое проектирование и реализация Физическое проектирование БД и реализация — совсем другая ситуация по сравнению с логическим проектированием БД. В то время как на логической стадии мы рассматривали наш проект исключительно с точки зрения получения "надлежащих" структур, в течение физической стадии проектирования мы становимся более прагматичными. Таким образом, мы преобразуем наш чисто логический проект в практический проект, который уже можем реализовать. В то время как логическое моделирование — явно тяжелая часть процесса проектирования БД, физическое моделирование также не должно рассматриваться как простая задача. Если вы ранее не разрабатывали БД, физическая реализация БД очень отличается от реализации программного обеспечения любого другого вида, с которым вы могли иметь дело, по одной основной причине:
Структуры данных в БД обычно являются центральной частью разрабатываемой системы, следовательно, большая часть кода в проекте будет связана с ними. Поэтому любые изменения, которые мы будем производить, должны быть тщательно рассмотрены, так как они будут связаны с использованием как самих структур данных, как и кода, который мы должны реализовать.
Часть И — Физическое проектирование и реализация
Помня это, мы рассмотрим следующие главы:
232
•
Глава 9 Планирование физической структуры — В этой главе мы рассмотрим принятие решений относительно того, как мы реализуем структуру. Здесь мы начнем учитывать число пользователей, размер системы, и то, как будут использоваться данные.
•
Глава 10 Планирование и реализация основной физической структуры — В этой главе мы рассмотрим механику выбора типов данных, построения таблиц и создания индексов.
•
Глава 11 Обеспечение целостности данных — Имеется много различных проблем, которые управляют целостностью данных нашей системы. Мы рассмотрим некоторые различные типы бизнес-правил, которые хотим реализовать, и то, как можно сделать это с учетом функций и ограничений, определенных пользователем.
•
Глава 12 Расширенный доступ к данным и методы корректировки — В этой главе мы рассмотрим некоторые методы доступа к данным в созданной нами БД, в особенности с помощью представлений и хранимых процедур.
•
Глава 13 Определение требований к аппаратным средствам — Одно из наиболее трудных действий для многих архитекторов данных — перевести требования системы в фактические требования к аппаратным средствам ЭВМ. В каком формате диска я нуждаюсь? Сколько мне нужно дискового пространства, и как эти требования изменятся в будущем? Каков должен быть размер ОЗУ? Сколько требуется процессоров? Слишком много вопросов. Это обычно вопросы для администратора БД, но для многих архитекторов может и не быть администратора, который поможет принять надлежащие решения относительно аппаратных средств ЭВМ.
•
Глава 14 Завершение проектирования — В этой главе будет рассмотрено завершение работы. Как только мы имеем наши данные, наши структуры и построенные запросы, проект закончен, не так ли? Чтобы завершить наш проект, мы разберем точную настройку нашей системы, включая рассмотрение проблем функционирования, связанных с формированием отчетов, особенно в интенсивно используемых системах, испытание системы и развертывание, которое мы должны предпринять. Мы также рассмотрим составление аварийных планов, чтобы защитить наши данные от всех тех неудач, которые предсказал Мерфи (в его известном законе).
Планирование физической структуры Введение В этой главе мы рассмотрим, как нужно реализовать нашу БД. Чтобы проиллюстрировать тип суждений, которые нам, вероятно, придется делать, рассмотрим следующие сценарии и попробуем оценить то, чего каждый требует для успешной реализации, типа отдельных структур только для чтения, используемых для формирования отчетов, отдельного оперативного хранилища данных или просто дополнительных аппаратных средств, чтобы улучшить характеристики запросов. Каждый из сценариев является реалистическим примером БД, которые являются действительно весьма банальными сегодня. В каждом случае предположим, что имеется соответствующий логический проект. Сценарий 1 БД содержит 100 таблиц. Одна из таблиц будет иметь миллион строк и будет расти со скоростью одной сотни тысяч строк в день. Имеется 2000 пользователей, вводящих данные в систему, и только три человека, формирующих отчеты в течение всего дня. Сценарий 2 БД содержит 20 таблиц. Все таблицы связаны по крайней мере с одной другой таблицей, а десять из таблиц растут со скоростью 100 строк в день. Трое людей вводят все строки двадцать четыре часа в день, в то время как отчеты формируются автоматически и посылаются через электронную почту старшим администраторам в организации. Сценарий 3 БД имеет два миллиона пользователей, пятнадцать тысяч из которых будут обращаться к БД одновременно. Каждую минуту каждый из пользователей создает две новых записи в БД.
Глава 9 Решение Так который из этих сценариев требует специальной обработки? К сожалению, ответ неясен, так как ни один из сценариев не содержит достаточную информацию, чтобы позволить нам принять квалифицированное решение. В сценарии 1, например, чтобы определить, нужно ли строить специальную БД, чтобы поддержать ежедневные потребности формирования отчетов, мы были бы должны знать, сколько потребуется времени для двух тысяч пользователей, чтобы ввести сотню тысяч строк. Это могло занять весь день, или это могли быть строки от пользователей, которые фактически являются машинами, которые автоматически вводят данные в течение часа каждый день. Кроме того, хотя мы и можем ожидать, что данные в этом случае должны быть преобразованы, просуммированы и запасены в хранилище данных для формирования отчетов, точный характер формирования отчетов в этом случае не определен. Сценарий 2 может показаться более простым, так как мы могли бы предположить, что добавление одной тысячи строк к БД в день — не так уж и много, особенно если их вводит один пользователь. Поскольку автоматический процесс управляет отчетами, мы знаем, что их формирование может быть намечено на подходящее время. В этом случае мы, вероятно, не будем нуждаться ни в какой специальной БД для формирования отчетов, так как система не должна быть занята, а запросы можно непосредственно подавать на OLTP-БД. Однако нужно учесть, что мы собираемся добавлять несколько сотен тысяч строк в год к БД, что, хотя конечно, и не очень большое количество строк, но может создать проблемы в зависимости от типа данных в строках и типа требуемых отчетов. Простые агрегатные операции, которые внутренне поддерживаются в SQL Server, не будут, вероятно, вызывать никаких проблем, в то время как более сложные статистические операции могут потребовать специальной обработки. Мы рассмотрим глубже тему комплексных требований к отчетам позже в этой главе. Сценарий 3 показателен для Web-сайта. Также сомнительно, что он будет самым простым из сценариев для решения. Основная причина этого — то, что, если вы имеете БД с двумя миллионами пользователей, с пятнадцатью тысячами активных соединений, вы, вероятнее всего, будете иметь деньги, чтобы использовать в этой ситуации столько аппаратных средств, сколько необходимо. Однако кроме различных проблем инвестиций в аппаратные средства, которые у вас могут возникнуть, нужно решить другие важные проблемы относительно характера формируемых записей и требований к формированию отчетов. Эту главу было труднее всего писать по сравнению со всеми другими главами, так как хороший логический проект диктуется проанализированной проблемой, без компромиссов, возникаемых во время реализации, при учете требований эксплуатационных характеристик и удобства. Хороший разработчик логической модели придет обычно к тем же заключениям для тех же самых проблем. Мы следуем одному и тому же типу методологии проектирования независимо от того, работаем ли мы с маленькой БД, чтобы хранить телевизионное расписание, или большой БД, чтобы хранить технические данные управления качеством. Выполнив такое проектирование хорошо однажды, вы должны быть способны делать это много раз, с постоянным ожидаемым результатом. Определение, как физическое решение БД должно в конце концов быть подготовлено и реализовано, — вообще-то другая история. Здесь мы должны взять набор параметров и построить решение, которое представляет логическую модель в виде, который позволит фирме оплатить все ее ожидаемые требования. Будьте уверены, что нелегко быть правым каждый раз. В процессе реализации вы не только рассматриваете вашу модель логического проекта, но также основываетесь на работе ваших коллег по поддержке (администратор БД и программисты БД) для лучшего выбора аппаратных средств ЭВМ, кодирования SQL-процедур и формирования преобразований из OLTP-БД в ODS. Не всегда возможно сделать оптимальный выбор, и так как сервер БД обычно является центральной частью любой системы, он может часто включать львиную долю ответственности за проблемы системы. 234
Планирование физической структуры Данная задача может показаться вам несколько упрощенной, но не волнуйтесь, поскольку мы теперь начнем рассматривать все проблемы предреализации, с которыми нам нужно разобраться перед созданием таблиц. В то время как мы не можем описать любую ситуацию, с которой вы можете столкнуться при физической реализации, мы можем по крайней мере рассмотреть два главных фактора, на которые в состоянии влиять, а именно: •
Формирование отчетов — Ключевой проблемой всей нашей работы по проектированию является баланс потребности в частом обновлении нашей БД с чтением данных, чтобы обеспечить точное формирование отчетов. Поддержание средств обслуживания бизнеса очень важно для процесса формирования аргументированных решений относительно точных настроек, которые требуются для оптимизации эффективности бизнеса.
Q
Эксплуатационные характеристики — Эксплуатационные характеристики — совсем другой вид проблемы. Имеется большое количество ситуаций, где хорошо нормализованная БД может обеспечить менее чем адекватные характеристики относительно того, что требуется. Мы посмотрим на некоторые из основных вещей, которые можем сделать, чтобы регулировать характеристики, все еще сохраняя наши нормализованные структуры БД (в большинстве случаев).
Это — наше утверждение, что первичное намерение создания систем клиент-сервер состоит в том, чтобы позволить изменениям быть сделанными с нашими текущими данными и иметь возможность это использовать немедленно, чтобы не отсылать запрос и ждать его завершения. Следовательно, мы должны делать то, что может сохранить OLTP-систему необремененной запросами только для чтения. Рассматривая эксплуатационные характеристики, мы не должны ставить под угрозу целостность данных, которые мы храним. Это — основная обязанность архитектора данных хранить необходимые данные без изъянов, защищая их от аномалий, которые происходят в плохо структурированных БД.
Проблемы формирования отчетов Формирование отчетов предназначено для преобразования сырого материала в значащий формат, который позволит пользователям принимать своевременные и адекватные решения. Как рассмотрено в предыдущей главе, отчеты — обычно наиболее важная часть системы для конечных пользователей, и в этом разделе мы обсудим некоторые из путей, которыми отчеты могут быть оптимизированы, чтобы вписаться в диапазон структур БД. Возникает довольно немного проблем, когда мы обсуждаем точное и своевременное формирование отчетов, не все из них столь же очевидны, как можно было бы подумать.
Размер данных Одна из наиболее частых проблем в формировании отчетов — то, что объем данных, необходимых, чтобы ответить на вопросы пользователя, является слишком большим, чтобы исполнить запросы в разумной манере. Характер проблемы зависит от аппаратных средств, на которых вы запускаете SQL Server — является ли это настольным компьютером с шестнадцатью мегабайтами ОЗУ или кластером (соединением) с четырьмя узлами по 4 гигабайта каждый. Обычно, однако, техника запросов должна быть подогнана под объемы используемых данных, хотя определение, когда "маленькие" объемы станут "большими", может оказаться несколько проблематичным, как иллюстрируют следующие примеры: Предположим, что Правительство Соединенных Штатов где-то имеет БД, которая содержит все номера социального страхования, которые оно присвоило, наряду с каждой транзакцией, 235
Глава 9 которую оно произвело, чтобы добавить деньги к счету или распределить деньги из счета социального страхования. Ясно, что за прошедшие 50 с лишним лет были бы миллионы и миллионы записей, добавляемые каждый месяц. В то время как это может выглядеть как большое количество данных, оно может оказаться небольшим по сравнению с БД, создаваемыми некоторыми провайдерами Интернета и сайтами Интернета при желании регистрировать каждый щелчок, каждую закупку и каждый поиск, который выполняют их пользователи. Таким образом, Web-сайты электронной торговли отслеживают столько информации о своих посетителях, сколько могут, чтобы персонифицировать показ товаров, которые, по их мнению, будут интересны для посетителя. Такие приложения, вероятно, будут регистрировать огромное количество информации относительно миллионов ежедневных посетителей таких сайтов, все из которых будут нужны для анализа владельцам сайтов. Другой пример регистрации операций, который становится банальным, — производственные БД обеспечения качества, где роботы фиксируют сотни и тысячи измерений в минуту, чтобы гарантировать, что произведенные изделия попадают в допустимые границы параметров. Этот вид приложения, очевидно, сформирует огромное число записей. В этих ситуациях имеется несколько основных подходов, которые мы можем использовать, чтобы попробовать решить проблему:
236
•
Оптимизация — Если мы сталкиваемся с ситуацией, где нам кажется, что существует верхний предел для количества данных, с которыми наша система может работать, во многих случаях простое изменение настройки нашей системы (типа разделения данных по различным группам файлов) исправит проблему. В то время как особенности типа большого количества индексов (или, конечно, недостатка их) или плохо рассмотренных возможностей оптимизатора (без учета, как оптимизатор обращается с запросами), могут загубить характеристики, во многих случаях при возгласах "слишком много" можно сделать больше, даже при неоптимальных настройках сервера или БД.
•
Доступ к данным — Рассмотрим случай, когда мы используем программное средство типа Access со связанными таблицами. Выполнение операции соединения в Access может успешно использовать добавление каждой из связанных таблиц клиенту перед созданием соединения, используя собственный механизм запросов, что не сложно, когда мы имеем дело с тысячами строк, и невыполнимо в случае миллионов строк. Однако запрос мог бы выполняться превосходно при использовании SQL Server. Во многих случаях проблемы с большими количествами данных связаны в большей мере с фактической передачей данных по проводу, нежели с непосредственной работой SQL. Больше о времени доступа к данным будет сказано позже в этой главе.
•
Мощные аппаратные средства — Возможно, мы могли бы просто собрать более эффективные аппаратные средства, которые работают с таким количеством данных, которое нам нужно. Использование более быстрого центрального процессора — ЦП (CPU-Central Processing Unit), объема ОЗУ, допустимого для данной машины, и полностью оптимизированных массивов диска не могут одни решить проблему, но устранят, конечно, большую часть ее. Мы обсудим аппаратные средства несколько глубже в главе 13.
•
Формирование отчетов в БД — Как мы обсудили в главе 1 и осуществим в главе 14, мы можем создать копию наших данных, которая содержит ответы на многие запросы, которые пользователь захочет сформировать, в повседневной работе. В нашем примере электронной торговли, приведенном выше, относительно быстрый процесс — создать кэшированную копию наших данных, чтобы отвечать на запросы пользователя. Альтернативой этому является просмотр всех записей, которые регистрировали бы все действия не только одного, но и всех пользователей сайта — действительно головная боль реализации!
Планирование физической структуры По всей вероятности, мы будем завершать работу, используя смесь этих идей, хотя первые две — хорошая отправная точка, так как они не требуют огромных расходов капитала и являются довольно сложной задачей (но не допускайте, чтобы ваш менеджер слышал, что Вы говорите это)!
Сложность Сложность требуемого формирования отчетов непосредственно зависит от того, как мы используем основные SQL-операции. Простые запросы, используя только встроенные SQL-операции, обычно не вызывают никаких проблем (хотя плохо разработанные запросы могут вызывать неприятные ситуации, подобно пропущенному оператору WHERE и соответствующим образом запирающему всю таблицу, запрещая другим изменять ее), но не всегда возможно ограничить вычисления только такими операциями. Например, я сформировал отчеты, которые используют единственный SQL-оператор, содержащий более чем 300 строк кода. Такая длина кода получилась, потому что требуется около трех строк кода, чтобы выполнить стандартную функцию вычисления дисперсии, и было необходимо 20 или около того раз ее использования внутри SQL-оператора. Хотя время, требуемое для выполнения оператора, не было чрезмерно большим (порядка секунд), операция оказалась дорогостоящей в терминах использования ресурсов (использование диска и ЦП), и таким образом ограничивала параллельные операции. Если пользователи хотят видеть последние 100 вычислений этого запроса на графике, то для выполнения вычислений каждых предыдущих 100 значений используется агрегатирование в таблице. Было бы неблагоразумным для пользователей выполнять 500 раз в день по 100 запросов в 300 строк. В этом и в других подобных случаях, которые включают чрезвычайно сложные условия формирования отчетов, может оказаться необходимым хранить некоторые предварительно агрегатированные данные, которые нарушают ЗНФ, особенно, когда дополнительные данные или бизнес-правила требуют ответов на одни и те же запросы. Это — иллюстрация процесса денормализации, которого мы кратко коснулись в главе 7. Обратите внимание, что, конечно, предварительно агрегатированные данные могли бы получаться и при вычислениях вне SQL Server. При использовании предварительно агрегатированных данных вы должны гарантировать, что если основные данные изменяются, должен быть снова выполнен код, формирующий агрегатированные данные, для изменения запасенных значений. В нашем примере выше изменение единственного значения могло привести к 100 выполнениям запроса, чтобы восстановить денормализованные данные. Не так уж и много можно сделать в этой ситуации, поэтому аномалии корректировки этого вида являются стержневым аргументом нормализации. Чтобы гарантировать целостность данных, все ссылки на те же самые данные должны быть изменены одновременно.
Требования поиска Если отчет требует большой свободы поиска (например, когда пользователи должны выполнить запросы на многих различных полях из многих различных таблиц), это может увеличить сложность до полной неуправляемости. Таблицы в нормализованной БД не оптимизированы для запросов, потому что процесс нормализации пытается создать таблицы по отдельным темам. Один из способов работать с этим типом проблем — использование одной из новых возможностей, имеющейся в SQL Server 2000 — индексированные представления (Indexed Views). Это обеспечивает средства предварительного вычисления представления, основанного на его собственном определении, и таким образом может использоваться, чтобы построить представление, 237
Глава 9
которое обеспечивает пользователям данные в понятном интерфейсе, которые они далее могут запрашивать обычным способом. Этот метод страдает теми же самыми проблемами характеристик, когда размещаются наши собственные денормализованные данные, так как SQL Server должен обслуживать индексированное представление одинаковым способом всякий раз, когда исходные данные изменяются. Дальнейшие детали индексированных представлений могут быть найдены в "Professional SQL Server 2000 Programming" (Wrox Press, ISBN 1861004486).
Конкуренция доступа пользователей Для любой конкретной системы, чем больше параллельных пользователей, тем больше случаев конкуренции, которые мы, вероятно, будем иметь между ними. В OLTP-операциях транзакции и запросы должны быть разработаны так, чтобы длиться как можно меньше времени. Отчеты — другой вопрос. Многие запросы, формирующие отчеты, могут иметь длительное время выполнения; совсем неудивительно иметь отчеты, которые требуют для выполнения пятнадцать минут, час, или даже несколько часов. Проблемы могут возникнуть, например, тогда, когда вы имеете очень терпеливого и относительно хорошо осведомленного пользователя вашей системы, кто пишет нерегламентированные запросы. Если пользователь не имеет никакого понимания, как точно работает SQL Server, то он может формировать и выполнять запросы, которые включают многочисленные соединения больших таблиц данных, таким образом затрачивая много часов, чтобы завершить действия этих запросов. Таким образом он может в конечном счете начать блокировать действия других пользователей, несмотря на блокирующие возможности на уровне строк, имеющихся в SQL Server 7.0, чтобы существенно уменьшить вероятность именно этого типа сценария блокировки. Однако путь, которым реализуется блокировка на уровне строк, позволяет оптимизатору выбирать между блокировками на уровне строк, уровне страниц или уровне таблицы. Когда пользователь пишет особенно плохо построенный запрос, который должен обращаться ко многим страницам в таблице, оптимизатор будет часто наращивать блокировки на страницы или даже на таблицы. Часто, когда пользователи исполняют такие запросы, возможно, блокировки будут включать страницы и таблицы, которые в это время изменяются активными пользователями, причиняя много сбоев!
Своевременность Своевременность означает, какой должен быть порядок потока данных, чтобы поддержать потребности пользователя в принятии квалифицированных решений. Например, предположим, что мы формируем оперативное хранилище данных из наших OLTP-данных, заполняем хранилище данных и витрины данных из оперативного хранилища данных и освежаем данные ежедневно. Вся эта система будет бесполезна пользователю, если все решения должны быть сделаны, используя только самые последние данные. Как быстро мы можем обеспечить доступ к данным, зависит не только от количества времени, которое требуется для передачи данных с одного места в другое (так называемое время ожидания), но также и от нашего проекта. Наши различные подходы могут быть подытожены следующим образом: •
238
Немедленный — Немедленный доступ наиболее проблематичен, так как мы можем сделать очень мало для его обеспечения. Имеется много способов обеспечить эти потребности, и все они вызывают проблемы функционирования. Мы должны будем часто строить лишь хранимые процедуры, которые читают данные непосредственно от наших таблиц, обращаясь к данным таким образом, чтобы избежать их
Планирование физической структуры
блокировку. Если это невозможно сделать без организации "попадания", то мы можем включать денормализованные структуры или, возможно, даже индексированные представления в наших хранимых процедурах. Иногда организация попадания неизбежна и фактически представляет допустимый недостаток в наших потребностях представления данных. В равной степени мы можем придумать компромисс, типа вероятностной выборки данных вместо включения каждого значения, что улучшит нашу скорость поиска данных. •
Некоторое допустимое время ожидания — Если вам не нужно мгновенно читать данные, мы можем использовать любую из других методологий. Можно создать структуры в оперативном хранилище данных, которые обновляются настолько часто, насколько позволяет размер данных (и бизнес-правила). Нужно позаботиться о том, чтобы гарантировать, что частота обновления была больше частоты обращения пользователей.
•
Большое время ожидания — Когда время — не основная проблема, и особенно когда вовлечено чрезвычайно большое количество исторических данных (типа анализа тенденций через какое-то время), хранилище данных или сопутствующие витрины являются лучшим средством, потому что мы можем иметь больше возможностей (и все они часто используются) агрегатирования и настройки запросов.
Частота Частота в этом контексте означает число раз получения отчета. Одна из систем, над которой я раньше работал, содержала отчет, который считался самым важным в ее функционировании. Он запускался каждые 24 часа в полночь и занимал для своего формирования три часа. Так как он запускался в наименее важные часы суток, на работу пользователей это мало влияло. С другой стороны, у многих других систем пользователям нужно иметь актуальные таблицы в течение дня, например, менеджер банка, который должен быть уверен, что банк имеет достаточно денег на руках, или брокер, кто должен иметь самые последние отчеты с тенденциями по каждому из клиентов или фондов. Формирование отчетов этого типа будет, вероятно, требовать супермощных аппаратных средств и чрезвычайно оптимизированных запросов, а также некоторые денормализованные структуры.
Влияние характеристик Если мы возьмем пример БД, созданной для контроля и хранения результатов деятельности потребителей на Web-сайте, которую мы обсуждали ранее, мало того, что эта БД будет логически спроектирована, чтобы регистрировать каждую операцию, мы также могли бы размещать каждый элемент и каждую страницу, которые отображены на экране. Это очень большая БД (думаю размером в терабайты), и с ее данными, размещенными нормализованным образом, единственная БД, вероятно, не была бы способна выдержать обе группы пользователей — пользователей, изменяющих данные, и пользователей Web-приложения, запрашивающих его страницы. В следующем разделе мы рассмотрим некоторые возможные компьютеры и конфигурации сетей (обычно называемые топологией), которые будем использовать, чтобы бороться с множеством общих проблем функционирования.
9 1868
.
239
Глава 9
Скорость соединения Первостепенный интерес здесь имеют глобальные сети (Wide Area Networks — WAN) наряду с Web-приложениями (которые можно фактически расценивать как глобальные сети, но в другом облачении). Возьмем очень простой пример — классическую двухъярусную систему клиент-сервер:
Рабочая станция
Сервер БД
Изменение скорости соединения или расстояния между клиентом и сервером БД существенно трансформирует окончательное решение по организации нашей СУБД. В следующем перечне мы рассмотрим некоторые общие скорости в сети, и как они повлияют на наше решение изменить эту простую топологию:
240
•
Чрезвычайно быстрое соединение типа 10 Мбит, 100 Мбит или больше — эти типы соединений наиболее характерны в пределах физической сети компании. Если мы не имеем проблем с размером БД, то нетрудно использовать простую двухъярусную систему клиент-сервер, как показано на рисунке выше. Пользователь может присоединяться непосредственно к SQL Server, и требуется очень небольшая специальная оптимизация.
•
Умеренное соединение типа ISDN (Integrated Services Digital Network — цифровая сеть связи с комплексными услугами), DSL (Digital Subscriber Line — цифровая абонентская линия), кабельного (от 2 Мбит до 128 Кбит) соединения — обычно основные приложения клиент-сервер реализуются с такими соединениями, но нам нужно быть уверенными, что мы оптимизировали все наши запросы к БД, чтобы уменьшить число байтов, которые передаются туда и обратно по сети, и число различных используемых коммуникаций. Возможное улучшение могло бы быть получено, оптимизируя наше программное обеспечение клиента, кэшируя частые запросы к БД, которые могут повторяться, и группируя многократные действия вместе.
•
Медленное соединение типа коммутируемого соединения (Dual-up) (в принципе 56 Кбит, но более часто примерно 28.8 Кбит) — приложения с коммутируемым соединением более распространены, чем мы могли бы себе представить. Многие служащие, связанные с продажами или обеспечением, используют коммутируемые соединения для управления приложениями БД по ночам из своих гостиниц. Хотя пользователи, осуществляющие доступ к своим приложениям через коммутируемую связь, вообще-то чаще имеют в виду скорость доступа, при которой приложения должны работать соответственно. В этих случаях мы, конечно, должны будем оптимизировать приложения или, что даже более возможно, применить схему тиражирования части БД клиенту, используя некоторую форму сжатия. Мы рассмотрим тиражирование позже в этой главе.
Планирование физической структуры Независимо от быстродействия рабочей станции или сервера БД скорость, с которой вы соединяетесь с сервером, является критической с точки зрения производительности. Если вы пытаетесь передать 1000 строк назад клиенту, где строка содержит 3000 байт, и вы осуществляете это по 128-килобайтовой ISDN-линии, вашему клиенту будет, конечно, плохо с вами, так как будет требоваться приблизительно 3 минуты ((3000 * 1000 * 8 (бит на байт)) / (128 * 1000) = 187.5 секунд), чтобы закончить передачу. Это не учитывает издержки ни в самой сети, ни за счет любых других пользователей. Если бы десять лет назад пользователи попытались использовать ту же самую полосу пропускания, то потребовалась бы дополнительно 31 минута для получения своих данных! Следует отметить, что вы могли бы написать вашу программу так, чтобы восстанавливать записи и отображать их по мере необходимости вместо восстановления всех 1000 записей в одно и то же время. Например, вы могли бы показывать по 20 записей на одной странице, и поскольку пользователи просматривают результаты, остающиеся записи были бы тогда разделены на части. Однако для всех из нас, кто использовал анализатор запроса и возвращал дополнительно 1000 строк для тестирования запроса в полсекунды (включая и запрос, и выборку данных), это все еще было бы неприятным. Действительность такова, что очень немногие большие системы сегодня строятся с простой архитектурой клиент-сервер. Большинство из них имеет распределенные части по всей сети, и даже по всему миру. Следующий пример — проект, который выглядит довольно дико, и который включает Web-серверы, хранилища данных и даже, как наследство, систему универсальных ЭВМ. Лондон
Рабочая станция
Даллас
Рабочая станция
Наследство приложений на универсальных ЭВМ
Web-сервер
[о] бооооо] |ocj Концентратор
I . Рабочая станция
Сервер БД
Хранилище данных
Рабочая станция
Рабочая станция
Оперативное хранилище данных
2 4 1
Глава 9 Из этого рисунка вы можете видеть, что сложность нашего решения ограничена только фантазией архитектора. Некоторые организации имеют десятки или даже сотни серверов БД, многие из которых связаны вместе с помощью различных видов соединений, обладающих различными показателями надежности. В этом случае каждая линия между элементами представляет связь сети. В любом из этих мест связи мы имеем скорости, которые могут изменяться от чрезвычайно большой до чрезвычайно медленной, и каждое место связи должно рассматриваться как ответственное (с точки зрения проекта), так как сети имеют свойство быть меньше чем 100-процентной надежности. В то время как для проектировщика БД важно, чтобы, по крайней мере, знать, как скорость соединения влияет на работу приложений, более широкие аспекты проблем организации сети попадают в сферу деятельности системного администратора.
Количество данных SQL Server 2000 — широко масштабируемый сервер БД. Он масштабируется от карманного компьютера с ОС Windows через отдельный ноутбук Pentium-200 МГц с тридцатью двумя мегабайтами ОЗУ и Windows 95 или двухпроцессорный Pentium III с половиной гигабайт ОЗУ до кластера из шестнадцати серверов, каждый из которых содержит по восемь процессоров с четырьмя гигабайтами ОЗУ каждый, и которые связаны друг с другом. Количество данных, которые вы можете поддерживать, изменяется от мегабайта или двух у вашего карманного компьютера до многих терабайт у группы серверов. Размер данных будет существенно влиять на изменение наших стандартов кодирования. Мы можем легко отойти от некоторых удобств программирования (игнорирование оптимизации и подобные), если наша основная таблица в БД будет иметь одну тысячу записей в противоположность одному миллиарду. В качестве иллюстрации возьмем следующий простой пример: child childld child attribute 1 child attribute 2 child attribute N parentld (FK)
parent parentld parent attribute 1 parent attribute 2 parent attribute N
Здесь child — таблица "потомок", childld — идентификатор потомка, child attribute — атрибут потомка, parentld — идентификатор предка, parent — таблица "предок", parent attribute — атрибут предка. Мы заявим как определение, что, если вы имеете запись в таблице-потомке, должен существовать мигрирующий ключ из таблицы-предка. Теперь рассмотрим ситуацию, когда мы хотим удалить запись из таблицы-предка. Тогда становится очевидно, что размер таблицы-потомка будет чрезвычайно важен. Если в таблице-потомке имеется десять строк, то чтобы удалить одну строку в таблице-предке, вы должны просмотреть десять записей — не так уж много. Однако если имеется миллион строк, это — не столь уж простая задача для SQL Server (даже с индексами это могло потребовать непомерное количество времени). Из этого следует, что, если вы имеете миллиард строк в таблице, строку не удастся удалить даже в две недели. В этом случае мы, вероятно, поместили бы в таблицу-предок 242
Планирование физической структуры дополнительный столбец, чтобы указать, что запись больше не может использоваться, но не фактически удаляли запись. Однако задание этого флага в таблице-предке оставит записи и в таблице-потомке, связанные с помеченной флагом записью, и эти записи в таблице-потомке требуют осторожной обработки. Следовательно, в любом случае мы не можем управлять ситуацией оптимально.
Бюджет Любые решения, которые мы изобретаем, должны быть основаны на том, что подходит для организации, так как увеличение размера бюджета, чтобы оплатить дорогие аппаратные средства, не всегда может быть приемлемым. Имеются три главных области, связанные с этим: •
Аппаратные средства — Недостаточно сказать, что нехватка аппаратных средств вызовет у вас проблемы. В главе 13 мы рассмотрим определение количества аппаратных средств при увеличении БД, в зависимости от числа пользователей и т. д. Простой принцип — никогда не урезайте производительность аппаратных средств. В главе 14 мы обсудим довольно глубоко концепцию наличия по крайней мере трех различных видов оборудования — для разработки, испытания и производства. Чтобы создавать, проверять и реализовывать надлежащую СУБД, вам будет нужно, по крайней мере, два отдельных сервера.
•
Программное обеспечение — Бюджет должен быть достаточен для ОС, лицензионного программного обеспечения БД и любых программных средств, которые вам потребуются для создания программного обеспечения. Внимательно читайте лицензионные соглашения по программному обеспечению, чтобы избежать переплаты за него.
•
Укомплектование персоналом — В зависимости от размера проекта, вы должны быть готовы иметь штат, способный работать с каждой частью СУБД, которую вы разрабатываете. Очень немного архитекторов БД являются также и большими специалистами по организации сети, большими администраторами БД, большими документаторами, большими испытателями, большими программистами и большими ... — поставьте точку сами.
Число пользователей Одной из наиболее трудных характеристик, с которой приходится иметь дело, является число пользователей, которых вы должны будете поддерживать. Поскольку каждое соединение требует приблизительно 12 Кб + (3 * (размер сетевого пакета, который изменяется, но по умолчанию равен 4 Кб)), или 24 Кб, большинство системных архитекторов не очень обеспокоено числом пользователей, которых система будет иметь. Однако нынче, когда БД поддерживает Web-сайты, общее количество пользователей, обращающихся к нашей БД, может быть где-нибудь от 10 до 10 миллионов или даже больше! В попытке иметь дело с таким большим числом пользователей используется ряд методов, включая: •
Организация пула (накопителя) соединений — Если пользователь в своем приложении соединяется и разъединяется с данными много раз, может быть выгодно использовать возможности организации пула в ODBC 2.5 (и последующих версий) или OLE DB. Они позволяют организовать драйверы так, что пользователь "думает", что он отключился, но фактически сохраняет связь открытой, ожидающей дальнейших команд. 243
Глава 9 Как краткое предостережение, организация пула соединений НЕ производит собственную очистку, когда вы сообщаете, что соединение завершено. Наиболее важно не закрывать любые открытые транзакции, которые могут затрагивать более поздние соединения, даже в случае многих пользователей, использующих соединение, поэтому должно быть выдано соответствующее предостережение.
244
•
СОМ+ (или любая разновидность n-ярусного приложения) — СОМ+ позволяет вам перенести часть обработки, которая могла бы быть выполнена на стороне клиента, и организовать пул соединений среди многих различных пользователей, так что вы устраните добрую часть непроизводительных издержек, связанных с созданием и разрывом соединений. При организации пула соединений десять тысяч параллельных связей может напоминать 1000 или даже 100.
•
Увеличение производительности, увеличение памяти — Только добавляя большее количество аппаратных средств, черт побери! К сожалению, ограничения бюджета могут означать, что это решение невозможно. Как ранее упомянуто, каждое соединение требует - 24 Кб ОЗУ. Это может казаться не слишком большой проблемой, но если вы имеете 10000 соединений с сервером, величина могла бы равняться 240000 Кб, что является существенной частью от 256 Мб, которые может использовать стандартный SQL Server. Если мы снова и снова добавляем к накладным расходам процессора управление всем этим ОЗУ, мы быстро увидим, как возникнут проблемы.
•
Кластеризация серверов — Microsoft SQL Server и Windows 2000 поддерживают кластеризацию нескольких серверов БД и работу с ними как с единственным сервером. Использование кластеризации позволяет нам создавать очень большие, очень мощные серверные системы БД. Идея таких систем состоит в том, чтобы распределить груз приложения на ряд серверов, что приводит к увеличению выполняемых запросов, так как могут быть добавлены дополнительные серверы для обслуживания дополнительной нагрузки. Этот процесс известен как балансирование нагрузки. Так как каждый сервер (в идеале) может обрабатывать требования клиента индивидуально, то если один сервер сломается или потребуются дополнительные серверы, добавленные к кластеру, в работе не будет никакой неприятности. Дальнейшие детали относительно кластеризации могут быть найдены в "Professional Windows DNA: Building Distributed Web Applications with VB, COM+, MSMQ, SOAP, and ASP" (Wrox Press, ISBN 1861004451).
•
Приложение со специальными копиями данных только для чтения — Это — технология, которая хорошо работает при создании Web-сайтов. Большинство данных, которые нам нужны для формирования Web-страниц, — только для чтения. Например, рассмотрим гипотетическую БД описания изделий, в которой мы имеем все наши изделия, размещенные в хорошо нормализованном наборе структур, избегая все ловушки избыточности. Далее рассмотрим Web-страницу, которую пользователь будет видеть, и где на одной странице перечислены текущая цена, цвет, размеры, формы, спецификации и т. д. В этом случае мы могли бы построить единственную таблицу, которая имеет все значения, необходимые для Web-страницы, вместо пятидесяти таблиц и трех сотен строк. Мы, вероятно, не хотели бы предварительно формировать всю таблицу пользователя, так как, конечно, хотели бы иметь возможность персонифицировать ее (добавить скидки и т. д.) и, конечно, изменить его взгляд и намерения относительно предмета. Потребуется дополнительное хранилище, но требуемая обработка существенно уменьшится, благодаря сокращению числа выполняемых соединений.
Планирование физической структуры
Возможности SQL Server Рассмотрев многие из проблем, касающихся функционирования наших СУБД, мы должны также рассмотреть средства, которые SQL Server предлагает проектировщику. В этом разделе мы кратко рассмотрим, что они собой представляют и как могут использоваться прежде всего теми, кто не знаком близко с SQL Server. Для тех, кто является постоянным пользователем SQL Server, — пожалуйста, пропустите этот раздел вплоть до заголовка "Примеры основных топологий" в следующем разделе.
Тиражирование Тиражирование — замечательное средство, которое помогает в создании копий БД и поддержании их для нас в некотором интервале. Этот интервал может изменяться от "немедленно" до дней или недель. Тиражирование может быть выполнено между различными SQL Server или даже из SQL Server в Microsoft Access, Oracle, или почти любую БД, которая поддерживает требования к подписчикам для SQL Server (для дополнительной информации см. SQL Server Books Online). Пара терминов, которые нам нужно здесь ввести: •
Публикация — источник копируемых данных, хотя это может или не может быть первоначальным источником. Подобно физически напечатанной публикации (следовательно, имеющей название), публикация — это БД, которая была отмечена для других БД, чтобы они могли подписаться на нее. БД разбита на таблицы, помеченные для тиражирования ответа, на которые ссылаются как на статьи.
•
Подписка — когда БД использует публикацию, чтобы получить точную копию одной или нескольких статей этой публикации, то ее называют подписчиком. Подписка может быть как на одну статью, так и на все статьи.
SQL Server обеспечивает три различных модели для реализации тиражирования: •
Тиражирование копии — периодически делает копию данных публикации и заменяет полный набор данных у подписчика.
•
Тиражирование с помощью транзакций — первоначально делает копию публикации подписчику (точно так же, как тиражирование копии), а затем использует любую транзакцию асинхронно к подписчику, как только копия возникнет у публикации.
•
Тиражирование слиянием — позволяет вам иметь две БД (одна из которых выбрана как издатель, формирующий публикацию, а другая — как подписчик), где вы берете все изменения от издателя и добавляете их к подписчику и наоборот. Это позволяет вам редактировать данные в любом месте и использовать изменения в другой БД автоматически. Тиражирование слиянием очень полезно для передвижных клиентов, когда поддержание связи с основным сервером невыполнимо. Оно также имеет варианты по умолчанию и специальные, чтобы разрешать конфликты в случае, когда несколько пользователей редактируют одни и те же части данных.
245
Глава 9
Связанные серверы Связанные серверы позволяют нам организовать доступ к неоднородному SQL Server или источникам данных OLE DB непосредственно изнутри SQL Server. Мы не только можем обеспечить доступ, но и можем выполнять для этих данных изменения, команды и транзакции. Например, для доступа к SQL Server по имени LOUSERVER, мы выполнили бы следующее: sp_addlinkedserver @server • N'LinkServer', @provider - N'SQLOLEDB', @datasrc = N'LOUSERVER', @catalog = N'Pubs'
I
Затем мы можем выполнить запрос, чтобы получить строки из этого источника данных: 1
SELECT * FROM LinkServer.pubs.dbo.authors
Это — простой пример, но он кратко иллюстрирует наше утверждение. Со связанными серверами вы можете иметь доступ к дополнительным SQL Server, серверам Oracle, БД Access, электронным таблицам Excel лишь по их именам. И прелесть этого состоит в том, что вы не должны изучать никакой дополнительный синтаксис доступа к несметному числу различных типов данных. Уродливая сторона этого заключается в том, что для присоединения одного набора данных к другому набору, возможно, придется большое количество данных пропустить из одного SQL Server во временное хранилище на другом прежде, чем будет выполнено соединение, что может иметь серьезные побочные эффекты для работы.
Сервисы преобразования данных Для случая, когда тиражирование не будет работать из-за изменений (а, следовательно, и изменения названия), сделанных в данных, фирма Microsoft обеспечила сервисы преобразования данных (DTS — Data Transformation Services). Используя их, вы можете преобразовывать и передавать данные между источниками OLE DB. DTS также позволяет вам передавать индексы и хранимые процедуры между SQL Server. Используя DTS, вы можете создавать пакеты, которые группируют вместе многократные операции преобразования/объекты, которые могут быть запущены или синхронно (одна операция ожидает завершение другой) или асинхронно, используя все возможности SQL Server. Вы можете, например, выполнять поочередно очистку данных (удаление/чистка данных) в таблицах, при этом имеется возможность вычислить новые значения, используя простую программу на Visual Basic. DTS — полностью специализированный инструмент очистки данных и имеет множество настроек и возможностей. Дальнейшие детали могут быть найдены в "Professional SQL Server 2000 DTS", (Wrox Press, ISBN 1861004419).
Контроллер распределенных транзакций Контроллер распределенных транзакций (DTC — Distributed Transaction Controller) — средство, которое осуществляет двухфазную фиксацию транзакций, что позволяет вам создать транзакцию, которая использует больше чем один сервер. Причина, по которой 246
Планирование физической структуры процесс называется двухфазной фиксацией, заключается в том, что на каждый из серверов, который используется транзакцией, посылаются две специальные команды, чтобы начать и закончить процесс. •
Подготовка — менеджер транзакций посылает команду каждому серверу БД для его менеджера ресурсов. При этом сервер готовится принимать команды, которые будут посланы.
•
Фиксация или откат — как только "пользователь(и)" сформировал любую задачу и выдает команду, что он хочет ее выполнить, начинается фаза фиксации или отката, работая наподобие того, как вы выполняли бы фиксацию транзакции на каждом сервере.
Строго в манере кодирования для SQL Server, если DTC был включен и вы создали связанные серверы, то можно использовать следующий код, чтобы модернизировать таблицу авторов как на сервере, где этот код выполняется, так и на связанном сервере. USE pubs GO BEGIN DISTRIBUTED TRANSACTION UPDATE authors SET au_iname = 'Davidson' WHERE au id - '555-55-5555' IF t e r r o r <> 0 BEGIN ROLLBACK TRANSACTION GOTO e x i t END UPDATE linkserver.pubs.dbo.authors SET au_lname = 'Davidson' WHERE au_id - '555-55-5555' IF @@error <> 0 BEGIN ROLLBACK TRANSACTION GOTO exit END COMMIT
TRANSACTIONT
exit: GO
Обратите внимание, что контроллер DTC работает с данными любого типа, для которых существует драйвер OLE DB, который поддерживает интерфейсы распределенных транзакций. Следует также знать, что если DTC не установлен, оператор BEGIN DISTRIBUTED TRANSACTION не будет выполнен.
Объекты управления данными SQL Объекты управления данными SQL (SQL-DMO — SQL Data Management Objects) — набор СОМ-объектов, которые включают почти каждую особенность SQL Server. Из DMO вы можете выполнять задачи типа создания таблиц и индексов, создания объектов 247
Глава 9 автоматизации и задач администрирования. Объекты могут использоваться из любого программного средства, которое допускает реализацию СОМ-объектов, типа Visual Basic, VB Script, VB for Applications, C++ и, как вы увидите в следующем разделе, самого SQL Server. Есть много хороших примеров в SQL Server Books Online, которые иллюстрируют мощь SQL-DMO; дальнейшие детали можно также найти в "Professional SQL Server 7.0 Development Using SQL DMO, SQL-NS & DTS", (Wrox Press, ISBN 1861002807).
Реализация СОМ-объектов Другая изящная особенность, которая существует с версии 6.Х SQL Server — реализация СОМ-объектов. Используя симпатичный мощный интерфейс T-SQL, вы можете вызвать большинство СОМ-объектов и делать с ними почти все, что вам потребуется. В следующем примере мы реализуем DMO-объект SQLServer и затем соединимся с ним: DECLARE SobjectHandle int @recVal int, —реализация объекта sqlserver EXECUTE @retVal = sp_OACreate 'SQLDMO.SQLServer', @objectHandle OUT IF @retVal <> 0 BEGIN EXECUTE sp_displayoaerrorinfo @objectHandle, @retVal RETURN END --связь с сервером EXECUTE @retVal = sp_OAMethod @objectHandle, 'Connect', NULL, 'LOUSERVER', 'louis', '<не ваше дело, какой у меня пароль>' IF @retVal <> 0 BEGIN EXECUTE sp_displayoaerrorinfo @objectHandle, SretVal RETURN END
Хотя реализация СОМ-объекта и очень полезна, она может при этом быть медленным процессом. Объект должен быть создан (и память для него выделена), исполнен и, наконец, удален — все не в стиле SQL. Используйте эти функциональные возможности, только когда основанное на SQL решение невозможно, или в процедурах, которые вы не рассчитываете использовать часто.
Почта SQL Почта SQL (SQL Mail) позволяет системе SQL Server посылать и получать почту, используя простой интерфейс Outlook или Exchange либо серверы Exchange или РОРЗ (почтовый протокол Интернета). Сервер может быть сделан так, чтобы отвечать на сообщения электронной почты, которые содержат запросы, возвращая результаты ответа так же, как сообщения электронной почты. Он может быть также добавлен к хранимым процедурам и триггерам, если это потребуется. Примерами того, насколько это полезно, могут быть задачи типа посылки напоминаний в конкретные пункты по календарю или даже посылка предупреждений, чтобы сообщить пользователю, что произошло некоторое событие типа отрицательного баланса на его счете в банке. В следующем фрагменте кода мы используем почту SQL, чтобы послать очень приятное сообщение по электронной почте: 248
Планирование физической структуры EXEC xp_sendmail @recipients = '[email protected]', @query = 'select ''hi'' ', @subject = 'SQL Server Report', ^message = 'Hello!', @attach_results = 'TRUE'
Полнотекстовый поиск Полнотекстовый поиск с помощью SQL Server позволяет нам строить очень мощные средства, которые работают подобно любому поиску сайта в Интернете. Использование полнотекстового поиска позволяет нам искать в столбцах текста так же как во внешних системных файлах. Синтаксис запроса для полнотекстового поиска — просто расширение обычных команд SQL Server. Например, рассмотрим следующий запрос:
1
SELECT title_id, title, price FROM pubs..titles WHERE CONTAINS (title, '"Database" near "Design"')
Используя ключевое слово CONTAINS, мы ищем не только названия, которые содержат Database и Design, но и названия, где они встречаются в сочетании близко друг к другу. Обратите внимание, что хотя полнотекстовые способности добавлены в SQL Server 2000, они не устанавливаются по умолчанию. Если вы попробуете выполнить этот оператор в pubs без конфигурации БД публикаций, а также конфигурации столбца названий в таблице t i t l e s (названия) для полнотекстовой поддержки, пример, приведенный выше, не будет выполнен с сообщением об ошибке. Полнотекстовые индексы размещаются отдельно от SQL Server и поэтому могут быть медленнее, чем собственные операции SQL Server. Дополнительные детали относительно установки и осуществления полнотекстового поиска могут быть найдены в "Professional SQL Server 2000 Programming" (Wrox Press, ISBN 1861004486).
Агент SQL Server Последним в нашем разделе, посвященном возможностям SQL Server, и далеко не самым последним, является возможность планирования работы с помощью агента SQL Server, который является связующим элементом, обеспечивающим работу вашей системы. Тиражирование использует агента, чтобы обрабатывать транзакции, пакеты DTS могут быть запланированы на выполнение в определенное время, и вы можете также запланировать ваши собственные хранимые процедуры, чтобы выполнять и запускать программы ОС. Установка агента SQL Server — вне возможностей этой книги, и читатель должен проконсультироваться относительно деталей в SQL Server Books Online. Важно отметить, что SQL Server имеет встроенные возможности, которые запускают свой собственный контекст безопасности, который будет выполняться почти каждый раз, когда будет требоваться ваша БД.
249
Глава 9
Примеры основных топологий В этом разделе мы рассмотрим некоторые наиболее важные топологии, которые могут быть выбраны при разработке наших приложений.
Тонкий" клиент в сравнении с "толстым" клиентом "Толщина" клиента непосредственно связана с тем, за какую часть работы отвечает программа клиента. Его не следует путать с тем же самым термином, используемым в описании концепции программы, написанной на классическом языке программирования вроде Visual Basic или C++, где все представление логики закодировано в программе клиента в противоположность выделенному клиенту.
'Толстый" клиент Лучшим примером "толстого" клиента было бы приложение, написанное на Visual Basic, которое организует доступ к БД текстовых файлов, используя ODBC (Open DataBase Connectivity — открытый интерфейс доступа к базам данных). Так как система для текстовых файлов, конечно, не СУРБД и поэтому не может защищать себя, вы должны включить в клиент код проверки достоверности каждого бита данных. Использование очень "толстого" клиента может уменьшить влияние помех приложения, потому что, если каждая возможная проблема с данными обрабатывается клиентом, и нет никакой возможности серверу изменить какие-либо значения, мы можем полагать, что наши данные будут сохранены точно так, как мы ввели их в БД. Если происходит ошибка, то это будет из-за проблемы в SQL Server (типа отказа диска или некоторых видов порчи данных). Однако "толстый" клиент не без своих недостатков. Так как вы уже предварительно проверили данные, кажется чрезмерным иметь SQL Server для их проверки. Но с другой стороны, если только клиент — не единственный метод получения данных из сервера, вы оставляете ваши данные открытыми для ошибок всякий раз, когда с ними осуществляется работа не из приложения. Это может казаться маленькой ценой, которую мы платим, чтобы иметь дружественное приложение, но редко все редактирование данных можно выполнить строго с помощью единственной части программного обеспечения.
"Тонкий" клиент На другом конце спектра — тонкий клиент. В этом сценарии код клиента совсем не проверяет вводимые данные, и считается, что механизмы SQL Server будут заботиться о любых ошибках и сообщать о них. Хороший пример очень "тонкого" клиента — окно запроса SQL Server Enterprise Manager:
250
Планирование физической структуры
[2:Data tn Table 'authors']
}п SQL Seivei Enterprise Manager V'n • Iff?
;
> 1 aujrame Johnson Marjorie Cheryl Mc i hael I Dean | Meander Abraham j Ann JBurt i Chare l ne Mornn i gstar : Regn i ad l Akk io Innes Mc i hel Dirk Stearns Livia
°°-o" [13 Щ1
au id ; _•_ 172-32-1176 213-46-8915 238-95-7766 267-41-2394 274-80-9391 341-22-1782 409-56-7008 427-17-2319 472-27-2349 486-29-1786 527-72-3246 648-92-1872 672-71-3249 712-45-1867 722-51-5454 724-08-9931 724-80-9391 756-30-7391 iLJ
|au Iname : White Green Carson O'Leary [Straight ; Smith Bennet Dull j Gringlesby Locksley Greene ; Blotchet-Halls Yokomoto ; del Castilo DeFrance Stringer MacFeather Karsen W
*
1
г
•
1
p
4
h
0
o
n
1 address
e
8
4
9
6
-
7
2
2
3
:
4
1
5
9
8
6
-
7
0
2
0
j
4
1
5
5
4
8
-
7
7
2
3
4
2
8
1
9
;
4
0
8
4
1
5
9
1
3
4
1
2
8
3
6
5
8
8
5
6
4
-
-
4
8
2
2
3
-
9
-
0
9
9
4
6
3
2
2
:
4
1
5
8
3
6
-
7
1
2
:
7
0
7
9
3
8
-
6
4
4
5
5
8
5
-
4
6
2
0
3
1
4
;
1
5
8
6
1
5
2
9
7
-
2
7
2
5
0
3
7
4
5
-
6
4
0
2
4
1
5
9
3
5
-
4
2
2
8
5
6
1
5
9
9
6
-
8
2
7
2
1
9
5
4
7
-
9
9
8
4
1
5
8
4
3
-
2
4
4
1
1
5
5
9
9
2
1
3
5
4
-
7
1
2
8
5
3
4
-
9
2
1
9
ЩИ
1сУ
10932 BiggeRd. Г-~3 309 63rd St. #411 ( :] j589 Darwin Ln. ; E;: || 22 Cleveland Av. # « 5420 College Av. ( : 10 Mississippi Dr. L :6223BatemanSt. E • ;3410 Blonde St. F JPOBox792 С ;; 18 Broadway Av. S " 22 Graybar House 1Г :-я 55 Hillsdale Bl. ( Ц 3 Silver Ct. \ : 2286 Cram PI. #86 t ': 3 Balding Pi. X | j 5420 Telegraph Av ( 1 ! 44 Upland Hts. С 5720McAuleySt. c d > j :. • •
Здесь au_id — идентификатор автора, au_lname — фамилия автора, au_fname — имя автора, phone — номер телефона, address — адрес. Независимо от того, что вы будете вводить в пустые места, входная форма будет брать это и передавать к SQL Server. Он, в свою очередь, будет брать данные и размещать их, пока не будет проинструктирован, чтобы не делать это. Один из важных моментов использования "тонких" клиентов SQL Server для редактирования всех данных — то, что вы никогда не должны волноваться относительно того, откуда поступают данные, так как, если вы закодировали ваш сервер должным образом, все данные в БД будут чисты. Данные к тому же проверяются однажды и только однажды. Это улучшит работу ваших приложений, особенно, когда случаи ошибок очень редки. Обратная сторона — уменьшение используемости приложения. Так как приложение понятия не имеет о том, как должны выглядеть данные, оно не может уберечь пользователя от ошибок. Это особенно видно в приложениях, которые имеют правила, затрагивающие более одного столбца. Возьмем следующий пример: i i i i l i i i l E
m
a
i
l
A
d
d
r
e
s
'.jqtxl OK
s
Cancel f
/
Г
S
e
n
d
e
m
a
i
l
r
o
t
i
Email me offers
f
c
a
t
i
o
n
s
;
Здесь Email Address — адрес электронной почты, Send email notifications — послать уведомления по электронной почте, Email me offers — электронная почта мне предлагает. 251
Глава 9 Легко видеть, что мы должны иметь правило предотвращения проверки любого из этих выключателей, пока пользователь не имеет адреса электронной почты. В БД это просто осуществить, но если мы не запрограммируем какую-либо из подобных логик у клиента, нет никакой возможности избежать следующей ситуации:
Email Adc ^
i
lMicrosoft][ODBC SOL Server Driver][SQL1Server]INSERT Statement conflicted with TABLE CHECK Constraint 'cnkSemailAddressSemailBlankNoChecks. The conflict occurred in database Pubs' table 'aulh.email' [Micro$oft}[ODBCSQL Server Driver][SQL ServerJThe statrnement has been aborted
17 Send: Г' Email rne offers
Здесь выводится текст: [Microsoft][Драйвер ODBC SQL Server][SQL Server] Оператор INSERT конфликтует с ограничением TABLE CHECK 'chk$emailAddres$emailBlankNoChecks'. Конфликт возник в БД 'Pubs' (публикации), таблице 'aut_email' (электронные адреса авторов) [Microsoft][Драйвер ODBC SQL Server][SQL Server] Оператор был выброшен Мы могли бы придумать довольно сложную схему отображения ошибок, чтобы выводить ошибку в более дружественном формате, однако основной процесс никогда не будет очень хорошо воспроизводить конкретную ситуацию.
То, что в промежутке Что наиболее для нас приятно, так это "где-нибудь в промежутке". Идеальные приложения клиент-сервер (или n-звенная структура подобного типа) использовали бы БД в НФДК с клиентом, который контролирует наименьшее количество данных, необходимых для обеспечения потребностей пользователя, и были бы способны обрабатывать любые сообщения об ошибках, которые возвращаются от SQL Server. Некоторые типы правил не подходят для обеспечения на стороне клиента, к ним относятся: •
правила связей между таблицами;
•
уникальность данных;
•
отношения через внешние ключи.
Для этих правил вам, конечно, будет нужно позволить серверу обрабатывать ошибку и создавать средство для отображения пользователю информации об ошибке в разумной форме. Если мы вернемся к нашему предыдущему примеру в форме Visual Basic, нам бы потребовалось сделать кое-что подобно следующему: . Email Address
Lsl OK
Cancel
252
Планирование физической структуры Здесь два выключателя были заблокированы, чтобы пользователь не мог взаимодействовать с ними, пока не заполнен адрес электронной почты. Как далеко мы пойдем с предварительной проверкой данных — это осмотрительная смесь практичности с удобством сопровождения (например, изменения к схеме данных могут также потребовать изменений в программе на Visual Basic и, как следствие, повторного испытания и схемы, и кода на Visual Basic). Если бы мы хотели проверить, что напечатанный адрес электронной почты правильный, мы могли бы просто поручить системе SQL Server выполнить контроль, используя проверку ограничений, и отобразить результаты проверки в подходящем сообщении для пользователей.
Клиент и конфигурация данных Теперь мы посмотрим на некоторые из основных конфигураций, которые можем использовать при создании систем сервера БД. Хотя группа, которую мы рассмотрим, не является всеобъемлющей, она, определенно, даст нам основу для выбора настроенных конфигураций, которые решают наши проблемы.
Классический клиент-сервер Старый, верный, и все же все еще важный после стольких лет клиент-сервер! Десять лет назад это была очень новая концепция. Девять лет и триста шестьдесят четыре дня назад (На момент выхода в свет перевода этот срок существенно изменился. Прим. перев) все мы начали придумывать лучшую конфигурацию, и хотя многие приложения теперь n-звенного типа, клиент-сервер все еще подходит во многих ситуациях, когда используется SQL Server. Обычно вы имеете одну или несколько рабочих станций, непосредственно соединенных с сервером БД SQL Server. Соединение должно быть разумно быстрым, так как в первую очередь это нужно для передачи по сети SQL-команд, посылаемых серверу, и набора результатов, возвращаемых клиенту.
Рабочая станция
Сервер БД Рабочая станция
Одно из основных достоинств этой конфигурации — простота. Программное обеспечение клиента посылает команды непосредственно БД, чтобы отыскать или изменить то, что требуется. Если эти команды хорошо продуманы и хорошо оформлены (тема, которую мы рассмотрим несколько глубже в главе 12), а результаты, которые мы получаем от сервера, настолько узки (настолько мало столбцов, насколько возможно) и настолько коротки (настолько мало строк, насколько возможно), насколько возможно, достижение требуемого результата будет обычно не слишком трудным. Если вы имеете относительно небольшое число пользователей, которым разрешен доступ к вашим данным, эта конфигурация будет подходящей. Однако что означает "небольшое" число, будет зависеть непосредственно от типа аппаратных средств, на которых работает сервер БД. 253
Глава 9 Когда число пользователей становится очень большим, или вы должны использовать слабую машину для управления вводом данных, непроизводительные расходы конфигурации системы клиент-сервер могут стать слишком большими, чтобы их допустить. Одна из главных причин в том, что наиболее дорогостоящие действия — соединение/разъединение с сервером. Большинство приложений требует нескольких связей с сервером и, чтобы уменьшить непроизводительные расходы сети, они часто формируются так, чтобы постараться сохранить свои связи. Как мы сказали ранее, сохранение от пяти до десяти тысяч открытых связей сервера БД может привести к большому расходу ресурсов обработки данных.
Трехзвенная конфигурация Конфигурации с тремя звеньями (и более общие n-звенные конфигурации) становятся очень популярными как способ отделить логику представления от бизнес-логики и бизнес-логику от логики данных. Как изображено на следующем рисунке, рабочая станция пользователя никогда непосредственно не связывается с сервером БД, она просто соединяется с группой объектов (обычно написанных на языке, поддерживающем СОМ или CORBA). Представление
Данные
Рабочая станция
Объект-посредник
Сервер БД
Рабочая станция
В идеале, бизнес-объекты, находящиеся на бизнес-уровне, имеют две очень важных задачи: Формирование пула соединений Чтобы уменьшить непроизводительные затраты модели клиент-сервер, объект-посредник объединяет соединения. Так как клиенты имеют тенденцию делать одни и те же запросы много раз, вместо того, чтобы открыть соединение с сервером БД на время, немного меньшее, чем обычно используется, объект-посредник формирует пул соединений, который он использует, чтобы соединиться с сервером БД. Вы также можете иметь много экземпляров объектов, кэшированных для использования в объекте-посреднике, а также иметь несколько объектов-посредников. По существу, философия, которая стоит за этой конфигурацией, заключается в том, чтобы забрать работу по соединению с клиентом у сервера БД и передать ее более масштабируемому устройству. Дальнейшие детали этого могут быть найдены в "Professional Windows DNA: Building Distributed Web Applications with VB, COM +, MSMQ, SOAP, and ASP" (Wrox Press, ISBN 1861004451). Обеспечение бизнес-правил Мы можем легко столкнуться с проблемами функционирования, связанными с конкретными бизнес-правилами, которые кажутся несколько произвольными и включают большое количество операций AND (И), NOT (HE) и OR (ИЛИ), которые делают их неэффективными для кодирования. Примером такого правила могло бы быть следующее: 254
Планирование физической структуры Подписка пользователя охватывает будние дни с понедельника по пятницу, но НЕ субботы и воскресенья, если они не дают увеличение оплаты, связанное с уикэндами, хотя у банка не выходные дни. Если инженер требуется вне диапазона от 9 до 17 часов, это потребует дополнительной оплаты, причитающейся ему за посещение. Правила, подобные этим, вообще не способствуют применению кода Transact-SQL, так как обработка будет требовать обратной связи от пользователя после первого же раза, когда он попытается сохранить запись, а правила подвержены частым изменениям в зависимости от потребностей изменения управления. Если мы кодируем на функциональном языке, способном взаимодействовать с пользователями, бизнес-правила легче реализовать и, следовательно, изменять. Как мы увидим в следующих двух главах, правила SQL Server должны управлять всеми неизменяемыми правилами, предписанными для данных в БД.
Сложное использование Web-серверов Другой сценарий, который становится все более распространенным, — наличие тысяч или даже миллионов пользователей, только читающих информацию, связанных с нашей БД. В этой ситуации мы можем делать одну или большее количество копий наших данных только для чтения (в сильно денормализованной форме). Переключатель, балансирующий нагрузку (и целая серия настроек, которые лежат за пределами моих основных способностей), берет запросы от клиента, направляет эти запросы на Web-сервер (каждый Web-сервер, являющийся копией одного и того же Web-сервера), далее Web-сервер вызывает один из серверов БД (снова управляемый переключателем, балансирующим нагрузку). Это показано на следующем рисунке:
Web-сервер 1
Web-сервер 2
Web-сервер N
БД1 только для чтения
БД2 только для чтения
БДЫ только для чтения
Переключатель, балансирующий нагрузку
Сервер нормализованной БД
Нормализованная БД — источник всех отредактированных данных, и если пользователь должен изменить данные (типа сделанного заказа или сохранения некоторых настроек), то мы можем обеспечить интерфейсы к исходному серверу БД или, возможно, другому серверу, который мы используем только для редактирования данных. Возьмем, например, большие медиа-носители информации и электронные коммерческие сайты книг с десятками тысяч CD, книг и т. д. Вся информация относительно этих элементов будет находиться в нормализованной БД, сконфигурированной так, чтобы обеспечить целостность данных. 255
Глава 9 Учитывая особенности функционирования, мы могли бы взять эти данные из своей нормализованной формы и преобразовать их с помощью предварительно построенных, предварительно соединенных запросов, чтобы создать Web-страницы, которые будут видеть клиенты Web-сайта. Мы могли бы также разделить данные на серверы. На основе факторов типа частоты использования мы могли бы поместить классическую музыку на один сервер, рок-музыку на другой, и так далее.
Глобальная сеть В нашей глобальной экономике многие организации расположены по всей стране или даже на разных континентах. В некоторых случаях мы можем построить простой клиент-сервер или даже многозвенные приложения, которые будут достаточны для наших нужд редактирования данных. Однако есть много случаев, когда непрактично редактировать и передавать данные по глобальной сети. Следовательно, мы можем построить топологию, которая в основном выглядит следующим образом: Теннесси, США
Лондон, Англия
'
:
тш'1||щц ни $Щ,
Сервер БД
Флорида, США
OLAP-сервер
Сервер БД
OLAP-сервер
Мы можем использовать тиражирование слиянием, чтобы позволить редактировать на любом конце канала, а затем сливать изменения назад во все серверы. В этом случае каждый из офисов на нашем рисунке будет чувствовать, как будто он имеет свою собственную БД, где он может редактировать все строки в БД. Мы могли бы разделить данные, чтобы избежать ситуаций, когда один пользователь в штате Теннесси редактирует какую-то строку, в то же самое время другой в Лондоне делает то же самое, но мы могли бы просто использовать средства разрешения конфликтов SQL Server для управления ситуациями, когда они возникают.
Учебный пример В нашем учебном примере мы собираемся использовать самую простую из возможных топологий, в то же время отобразив все идеи, которые мы рассмотрели. Мы будем использовать простую установку клиент-сервер с дополнительным оперативным хранилищем данных:
256
Планирование физической структуры
Основной пользователь счета
Управляющий
Сервер БД
. Основной пользователь счета
Сервер оперативного хранилища данных
Одна из главных причин придерживаться испытанной и надежной топологии клиент-сервер заключается в том, что мы будем реализовывать бизнес-правила на сервере БД, когда это возможно. Мы, конечно, обсудим любую ситуацию, когда эта топология клиент-сервер является помехой. Последняя вещь, которую следует упомянуть относительно нашего решения использовать структуру клиент-сервер, — независимо от того, какую топологию нужно реализовать, следует обычно иметь как можно больше правил, помещенных на сервер БД, по причинам, которые мы опустим. Мы реализовали их здесь как пример, и именно так я построил бы систему.
Резюме Как вы, наверное, догадались, познакомившись с предыдущими главами этой книги, абсолютно нет НИКАКОГО простого ответа, как быть с чрезвычайно широким диапазоном возможных ситуаций/конфигураций/параметров, которые могут затрагивать реализацию. Очень важно, чтобы вы использовали не только то, что вы узнали в этой главе, но, в зависимости от вашей роли, изучили бы все, что можете изучить относительно того, как работают SQL Server и его дополнительные средства, используя как другие книги (некоторые из них мы упомянули в тексте), так и SQL Server Books Online. Мы рассмотрели маленький подраздел различных возможных способов того, как связать БД с клиентом или как выбрать структуру решения. Данная глава не дает вам ничего, что вы могли бы почерпнуть относительно физической структуры проекта БД. Это потребовало бы целой книги само по себе. Что мы стремимся сделать — дать вам основную часть информации, которая позволить прочувствовать предмет и работать с 80 процентами проблем, с которыми вы столкнетесь, и решить, где найти дальнейшую информацию, чтобы бороться с оставшимися 20 процентами проблем. Следующая глава возвращает нас назад на путь, где мы более глубоко посмотрим на процесс создания таблиц, включая полное рассмотрение основных объектов SQL Server, таких как индексы и типы данных, которые мы будем использовать, чтобы создавать наши окончательные БД.
257
'
Планирование и реализация основной физической структуры Введение Наша цель в этой главе — построение таблиц, которые составят нашу БД. Мы говорили относительно этого процесса, планировали его, даже рисовали картинки, но теперь настало время реализовать все физически. Процесс создания OLTP-таблиц может быть в ряде случаев трудоемким, но, если вы используете любое из известных средств проектирования БД, задача будет гораздо более легкая. На стадии реализации мы должны реорганизовать наши объекты, добавив практические детали и, наконец, преобразовать это все в код. В этой главе мы будем строить наш код без использования каких-либо средств проектирования или генераторов кода, так что вы сможете точно увидеть, что происходит. Вместо этого мы будем использовать для создания таблиц Query Analyzer (средство выполнения SQL-запросов) и ручное редактирование на языке определения данных (Data Definition Language — DDL). Мы рассмотрим код формирования таблиц и отношений, но не код определения доменов данных, которые используются для выбора типов данных. Мы исследуем в некоторой степени каждый из внутренних типов данных, обсудим отдельные расширения их, которые могут нам потребоваться, и определим некоторые основные столбцы и типы данных, которые будем, вероятно, неоднократно использовать. Следующая глава будет посвящена уточнению определения доменов, чтобы в дальнейшем ограничивать значения данных конкретными размерами или использовать их в задачах определения типов.
Глава 10 Мы определим уникальные ограничения и индексы для наших таблиц и рассмотрим физические структуры расположенных на диске индексов и таблиц. Основы понимания, как размещаются индексы и данные, помогут строить представления того, что происходит внутри SQL Server при размещении данных и выполнении запросов. В свою очередь, это поможет нам получить ясную картину, в каких индексах мы нуждаемся при тех или иных ситуациях. Когда мы завершим рассмотрение тем этой главы, то получим схему БД, включающую группу таблиц с индексами и отношениями. Наша БД будет далека от завершения, но мы создадим прочную основу для реализации бизнес-правил, которые будут рассмотрены в следующей главе.
Средства генерации БД Чего нам следует придерживаться на практике — никогда не создавать БД без приличного программного средства. Имеется несколько превосходных средств формирования БД, которые также хороши и для моделирования БД. Создание сервера БД возможно и без них, так как имеется ряд администраторов БД, которые имеют множество текстовых файлов, представляющих их БД, но они расточительно тратят время. На этой стадии реализации многие программисты будут стремиться начать писать код. Если мы начнем создавать все наши таблицы и код вручную, это потребует для выполнения огромное количество времени. Первая система, для которой я написал код БД, включала более чем 800 хранимых процедур и триггеров, написанных вручную более чем за восемь месяцев. Это было восемь лет назад, и это был фантастически поучительный пример. Сегодня современные средства проектирования БД позволяют мне строить БД с сопоставимым кодом в течение недели. Имеются средства, которые выполняют для нас многие "детали" работы, но если мы не вполне понимаем, как они делают это, очень трудно обеспечить их успешное применение. Выполнение этих "деталей" вручную перед использованием таких мощных средств будет стоить крови, пота и слез. Однако чтобы показать вам, как работает процесс физической реализации, мы не будем использовать никакие из этих средств в течение следующих трех глав. Мы будем строить вручную сценарии, которые средства проектирования сформировали бы автоматически. Это поможет нам понять то, что мы получаем с помощью таких средств. Оно является также хорошим упражнением для любого архитектора БД или администратора БД, чтобы рассмотреть синтаксис SQL Server; только не делайте это на БД с девяноста таблицами, если у вас нет в запасе восьми месяцев. В этих главах мы будем иметь дело, прежде всего, с разработкой БД. Разработка БД связана с размещением данных, которые не имеют никакой деловой ценности. Разработчики должны быть способны изменять или удалять любые элементы данных (но не структуры), разрабатывая код доступа к БД. К сожалению, многие программисты будут сразу же просить реальные данные для БД.
260
Планирование и реализация основной физической структуры
Схема физического проектирования В части книги, посвященной логическому проектированию, мы обсуждали весьма детально, как проектировать наши объекты. В этом разделе мы будем брать этот проект и разрабатывать на его основе реальные объекты БД. Так же как инженеры-конструкторы берут чертежи архитектора и исследуют их, чтобы разобраться в том, какие необходимы материалы и является ли все это реализуемым, мы возьмем нашу логическую модель и выделим те части, которые могли бы быть нереализуемы или слишком трудны для реализации. Некоторые части первоначального представления архитектора, вероятно, придется изменить, чтобы сделать его реалистическим на основе факторов, которые не были известны, когда проект был первоначально задуман, наподобие типа почвы, наличия холмов, и т. д. Мы также окажемся перед необходимостью выполнить процесс перевода представлений из сырой концепции в реализацию. В течение логического проектирования мы поверхностно смотрели на любые реальные примеры кода и старались рассматривать только особенности, общие для всех реляционных СУБД. Логическая модель, которую мы разработали, могла бы быть реализована и в Microsoft SQL Server, и в Microsoft Access, и в Oracle, и в Sybase и в любой другой СУРБД. Начиная с этого момента, я буду считать, что вы установили Microsoft SQL Server 2000 и создали рабочую БД. Мы рассмотрим многие из особенностей, которые SQL Server дает нам, хотя и не все. Для получения большего количества информации относительно Microsoft SQL Server 2000 читайте "Professional SQL Server 2000 Programming", Rob Vieira (Wrox Press, ISBN 1-867004-48-6). На протяжении всей главы нашей целью будет преобразование логического проекта в физическую реализацию, настолько близкую, насколько возможно. Мы рассмотрим следующее: •
преобразование логического проекта в физический проект;
•
создание таблиц;
•
создание столбцов;
•
типы данных;
•
служебные столбцы;
•
сопоставление;
•
ключи;
•
создание отношений;
•
распределение ваших метаданных между разработчиками.
Каждый из этих шагов будет подробно рассмотрен, чтобы получить надлежащее понимание того, что в основном доступно для получения реального проекта и правильной реализации.
Преобразование нашего логического проекта Раньше раздел, подобный этому, посвященный физическому проектированию, был бы намного более длинным, чем те немногие страницы, которые мы имеем здесь. Как мы уже говорили, потенциальные возможности аппаратных средств и программного обеспечения, совместно с методологиями хранилищ данных, потенциально позволяют нам реализовать БД почти точно в соответствии с логическим проектом. Однако нужно быть осторожным при проектировании физической БД, чтобы не попытаться реализовать что-то, что является слишком трудным для практического использования.
261
Глава 10
Подтипы Нам, вероятно, придется отклониться от нашего логического проекта, когда имеем дело с подтипами. Вспомните, что подтип в терминах моделирования БД указывает определенный тип отношений "один к одному", где мы имеем одну основную таблицу, например,person (человек), и одну или более таблиц подтипов, которые уточняют значения этой таблицы, например, employee (служащий), t e a c h e r (преподаватель), customer (заказчик) и т. д. Они особенно существенны в логическом проектировании, но имеются также серьезные основания, чтобы сохранить их и в нашем физическом проекте. В зависимости от того, как вы завершили ваше логическое моделирование, вам, вероятно, придется выделить дополнительные подтипы. Всегда имейте в виду, что имеется столько путей проектирования БД, сколько существует архитекторов данных. Я приведу примеры случаев, когда мы будем или не будем хотеть иметь таблицы подтипов. Пример 1 Пусть мы имеем таблицу movie (кинофильм), которая содержит названия кинофильмов (наряду с другой информацией типа жанра, описания и т. д.). Мы также имеем таблицу movieRentallnventory (каталог арендной платы кинофильмов), которая определяет арендную плату за видеокассеты или DVD. Зададим число, которое используется как вторичный ключ, и столбец itemCount (количество), определяющий число видеокассет или дисков, которые находятся в пакете арендуемых фильмов. Имеется также столбец mediaFormat (формат фильма), который определяет, является ли фильм видеофильмом или DVD.
Movie Movield
MovieRentallnventory MovieRentallnventoryld
Name
MovieidW Number (AK1.1) ItemCount MediaFormat
MediaFormat
L 262
VideoTape MovieRentallnventoryld (FK) HiFiSoundFI StereoSoundFI
DVD |_MovieRentallnventoryld (FK) RegionEncoding Version I AspectRatio
SoundFormat SoundFormatld Name(AKI.I) Description
DVDSoundFormat DVDSouindFormatld SoundFormatld (FK)(AK1.1) MovieRentallnventoryld (FK)(AK1.2)
Планирование и реализация основной физической структуры Здесь Movie — таблица "кинофильм"; Movield — идентификатор кинофильма; Name — название (кинофильма); MovieRentallnventory — таблица "каталог арендной платы за кинофильмы"; MovieRentallnventory Id — идентификатор арендной платы за кинофильмы; Number — номер элемента каталога; itemCount — число элементов; MediaFormat — формат фильма; VideoTape — таблица "видеокассета"; HiFiSoundFl — флаг звука высокого качества; StereoSoundFl — флаг стереозвука; DVD — таблица "DVD-диск"; RegionEncoding — код региона; Version — версия; AspectRatio — форматное соотношение; SoundFormat — таблица "формат звука"; SoundFormatld — идентификатор формата звука; Description — описание; DVDSoundFormat — таблица "формат звука DVD"; DVDSoundFormatld — идентификатор формата звука DVD. Мы определяем подтипы значения mediaFormat для videoTape и DVD. Нам нужно знать формат, потому что DVD имеют намного больше особенностей, чем видеозаписи, наиболее важными из которых являются код региона и формат звука. Мы реализуем специальный DVDSoundFormat для DVD как отношение "многие ко многим", так как DVD поддерживают любые наборы форматов звука; в то время как для типа videoTape мы имеем только поля для высококачественного и стерео звука. Имеется несколько проблем с этой реализацией: •
Чтобы сформировать новую арендную плату за видеофильм, мы должны создать записи, по крайней мере, в двух таблицах (movieRentallnventory и videoTape или DVD, и еще в DVDSoundFormat для DVD). Это — не слишком большая проблема, но может потребовать некоторой специальной обработки.
•
Если мы хотим видеть список всех элементов каталога арендной платы, включая звуковые характеристики, мы должны будем написать очень сложный запрос, который присоединяет movieRentallnventory к videoTape, и объединить с другим запросом между movieRentallnventory и DVD, что может быть достаточно медленно или просто слишком тяжело для реализации. (Мы детально рассмотрим запросы в главе 12.)
Это общие проблемы с подтипами. Что мы должны решить — это насколько целесообразно держать данный набор таблиц, реализованных как подтипы. При рассмотрении того, что делать с отношением подтипов, одна из наиболее важных задач, которую нужно решить, — сколько будет в общем сущностей в каждом подтипе и сколько атрибутов они в общем имеют. Простой обзор атрибутов таблиц подтипов в этом примере показывает, что они имеют много общего. В большинстве случаев это — просто вопрос количества элементов. Возьмем DVD. Код региона характерен для этого видеофильма, но некоторые видео имеют различные версии, и технически каждое изделие видео имеет форматное отношение (отношение высоты и ширины изображения), которое будет интересно любому энтузиасту. Поскольку мы можем определить характеристики звука видеозаписи, используя два простых выключателя, этот же самый метод определения звука мы можем использовать для DVD. Мы будем только должны ограничить количество элементов soundFormat на видео одним, в то время как DVD может иметь их до восьми.
263
Глава 10 Таким образом, мы изменяем отношения подтипов следующим образом: MovieRentallnventory Movie Movield Name
SoundFormat SoundFormatld Name (AK1 .1) Descriptiori
MovieRentallnventoryld Movield (FK) Number (AK1.1) Version itemCount MediaFormat AspectRatio DVDRegionEncoding
• MovieRentallnventorySoundFormat MovieRentallnventorySoundFormatld ш MovieRentallnventoryld (FK)(AK1.1) SoundFormatld (FK)(AK1.2)
Здесь дополнительно MovieRentallnventory SoundFormat — таблица "формат звука каталога арендной платы за кинофильмы"; MovieRentallnventorySoundFormatld — идентификатор формата звука каталога арендной платы за кинофильмы. Обратите внимание, что это нарушает правила нормализации. Однако действительно трудно объединить использование подтипов, которые не нарушают нормализацию, если два объекта, являющиеся подтипами, содержат в основном одни и те же составные части. Последний момент, который должен быть сделан, связан с тем, что поскольку мы изменили нашу структуру и храним различные типы информации в одной и той же сущности, то теперь должны быть осторожны, чтобы не допустить ввода в таблицу неподходящих данных. Например, когда mediaFormat — видеокассета, то поле DVDRegionEncoding не используется и должно быть установлено в NULL. Эти бизнес-правила будут реализованы, используя ограничения и триггеры, так как значения в одной таблице будут определяться значениями в другой. Может показаться, что сворачивание (или объединение) подтипов в единственную таблицу дает более легкую и более прямую реализацию, в то время как фактически облегчение работы возможно лишь где-нибудь еще. Знание, какие значения используются, конечно, поможет нам решить, стоит ли сворачивать подтипы. Пример
2
В качестве второго примера рассмотрим случай, когда не имеет смысла сворачивать подтипы. Вы будете обычно иметь подтипы, которые не стоит сворачивать, когда имеются два (или больше) объекта, которые делят общего предка, но, являясь подтипами, эти подтипы не имеют абсолютно никакого отношения друг к другу. Посмотрим на следующую структуру:
264
Планирование и реализация основной физической структуры
Person
Employee 'Personld (FK) Function Number
Customer Personld (FK) Number AccountNumber
Здесь Person — таблица "человек"; Personld — идентификатор человека; Type — категория; Name — имя; Employee — таблица "служащий"; Function — должность; Number — номер; Customer — таблица "заказчик"; AccountNumber — номер счета. Мы имеем таблицу person, которая содержит имя и другие атрибуты, включая такие, которые реализованы как дополнительные таблицы с отношениями к таблице person (такие как адрес, информация о партнерах, журнал партнеров и т. д.). В нашем примере и служащие, и заказчики являются людьми, но сходство на этом и завершается. Поэтому в данном случае лучше оставить подтипы, как они есть. Единственная проблема здесь состоит в том, что если кто-то хочет узнать имя служащего, вы должны будете использовать соединение. Это не является проблемой с точки зрения программирования, но это проблема с технической точки зрения. Во многих случаях основные особенности, которые определяют подтипы, более разумно использовать в создаваемых представлениях, которые позволяют рассматривать таблицу employee как будто она содержит также и параметры таблицы person. Определение, когда следует сворачивать таблицу — один из тех случаев, который приходит со временем и с опытом. Как правило, обычно лучше следующее: •
Свернуть, когда таблицы подтипов очень похожи характеристиками на таблицу-предка, особенно, когда мы часто хотим видеть их в списке вместе.
•
Оставить в качестве подтипов, когда данные в подтипах разделяют общие характеристики, но логически не связаны друг с другом каким-либо образом.
265
Глава 10 Другие причины отхода от логического проекта Наиболее частая причина отхода от логической модели — денормализованные структуры, обычно итоговые данные, суммирующие заданный набор строк, как в следующих таблицах: Account Accountid Number Balance
1
Transaction Transactionld Accountid (FK) Number Amount
Здесь Account — таблица "счет"; Accountid — идентификатор счета; Number — номер; Balance — баланс; Transaction — таблица "сделка"; Transactionld — идентификатор сделки; Amount — сумма. Здесь атрибут b a l a n c e будет эквивалентен итогу всех сумм сделок для данного accountid. Мы обсудим, как реализовать этот тип итоговых данных, используя триггеры, так как иногда может потребоваться введение денормализованных данных, чтобы решить конкретные задачи функционирования. Введение этого на ранних стадиях физического проектирования может показаться разумным. Однако следует подчеркнуть, что денормализацию не следует выполнять, если нас не вынуждают обстоятельства. Обычно денормализация причиняет больше проблем, чем их решает. Для архитекторов-новичков может быть тяжело понять, почему лучше восстановить значения, используя соединения вместо создания дубликатов значений в БД, чтобы ускорить запрос или группу запросов. Объединения в SQL Server чрезвычайно эффективны и очень часто обеспечивают адекватное функционирование.
Таблицы Таблицы являются центральными объектами в SQL Server. Основы таблиц очень просты. Если вы посмотрите на полный синтаксис для оператора CREATE TABLE, то увидите, что имеется много различных необязательных параметров. В этой главе и части следующей мы рассмотрим оператор создания таблицы и как лучше всего выбрать, что делать с каждой разновидностью. Первый необходимый момент очень прост: Щ CREATE TABLE [ < Б Д > . ] [ < в л а д е л е ц > . ] < и м я _ т а б л и ц ы >
Мы будем добавлять элементы в угловых скобках (<дополнительный_элемент>). Обратите внимание, что выражение в квадратных скобках ([дополнительный_элемент]) является необязательным и может быть опущено.
266
•
<БД> — обычно вам не нужно определять БД в операторе CREATE TABLE. Если она не определена, подразумевается текущая БД, где выполняется оператор.
•
<владелец> — это имя владельца таблицы. Им должен быть текущий пользователь или dbo — владелец БД. Если оператор выполняется лицом из категории системных администраторов (s уsadmin) сервера, имеющим функции либо владельца БД (db_dbowner), либо администратора данных (db_ddladmin), то таблица может быть создана для любого пользователя путем задания его имени.
Планирование и реализация основной физической структуры Лучшая практика — минимизация числа владельцев. Наличие многих владельцев создает проблемы "цепи владельцев" между зависимыми объектами. Обычно все объекты принадлежат только одному пользователю, имеющему имя dbo (database owner — владелец БД).
Обозначение Значение <имя_таблицы> дает имя таблицы. Имя должно соответствовать типу системных данных sysname. В SQL Server он определен как строка из 128 двухбайтовых символов Unicode. Сочетание owner (владелец) и tablename (имя таблицы) должно быть уникально для БД. Если первый символ имени таблицы — одинарный символ #, таблица является временной локальной. Если первые два символа имени таблицы — ##, это — глобальная временная таблица. Мы кратко упомянем временные таблицы в главе 12, хотя они не такая уж большая часть проекта БД, как механизм размещения промежуточных результатов в сложных запросах. Правила SQL Server для имен объектов состоят из двух разных методов обозначения. Первый использует ограничители (или квадратные скобки, или двойные кавычки) вокруг имени (хотя двойные кавычки допустимы только тогда, когда установлена опция SET QUOTED_IDENTIFIER — задание идентификатора в кавычках). Помещая ограничители вокруг имени объекта, вы можете использовать в качестве имени любую строку. Например, [Table Name] или [3232 f jfa*& (&л (] были бы оба законными (но раздражающими) именами. Использование имен с ограничителями вообще-то плохая идея при создании новых таблиц, и его нужно по возможности избегать, поскольку оно делает кодирование более трудным. Однако оно может быть необходимо для взаимодействия с таблицами данных в других средах. Второй и предпочтительный метод обозначения состоит в том, чтобы использовать имена без ограничителей, но они должны следовать нескольким основным правилам: Q
Первый символ должен быть символом, определенным в Unicode Standard 3.1 (короче говоря, латинские символы от А до Z верхнего или нижнего регистра), или символом подчеркивания (_). Стандарт Unicode может быть найден в www.unicode.org.
•
Последующие символы могут быть буквами Unicode, десятичными цифрами, амперсандом (@) или знаком доллара ($).
•
Имя не должно быть зарезервированным словом SQL Server. Имеется довольно большой список зарезервированных слов в электронном руководстве по SQL Server 2000 (смотрите раздел Reserved Keywords — зарезервированные ключевые слова).
Q
В имени не должно быть пробелов.
В то время как правила создания имен объектов достаточно очевидны, более важный вопрос — "Какие имена мы должны выбирать?" Ответ предсказуем: "Независимо от того, что вам кажется, лучшее — это когда другие могут их прочитать." Это может звучать как попытка уйти в сторону, но имеется столько много различных стандартов обозначения, сколько существует архитекторов данных. Стандарт, которого я обычно придерживаюсь, — стандарт, который мы будем использовать в этой книге, но это, конечно, не единственный способ. В основном, мы будем следовать тому же стандарту, который мы начали использовать в логических именах. Учитывая размер в 128 символов, есть немного причин делать много сокращений. Это наиболее важно понять при выборе имен. 267
Глава 10 Так как большинство компаний имеет уже существующие системы, неплохо бы узнать стандарт предприятия для обозначения, чтобы новые разработчики вашего проекта лучше понимали вашу БД и быстрее вошли в ритм. Как пример, дадим имя объекту, который мы могли бы использовать, чтобы хранить телевизионную программу. Следующий список даст несколько различных способов сформировать имя этого объекта: •
t e l e v i s i o n _ s c h e d u l e _ i t e m (телевизионная программа) — использование символов подчеркивания для отделения различных слов. Большинство программистов не очень любят символы подчеркивания, поскольку они непривычны в программе, пока вы не свыкнетесь с ними.
•
[ t e l e v i s i o n schedule item] или " t e l e v i s i o n schedule item" — ограничение скобками или кавычками. Не одобряется большинством программистов, поскольку невозможно использовать это название при задании переменных в коде и легко допустить ошибки.
•
t e l e v i s i o n S c h e d u l e l t e m — смешанный случай для разграничения слов. Это — стиль, который мы используем в нашем учебном примере, так как он мне нравится.
•
tvSchedltem или tv_sched_item или [tv sched item] —сокращенные формы. Они проблематичны, потому что вы должны быть осторожны, чтобы всегда сокращать одно и то же слово одинаковым образом во всех ваших БД. Необходимо вести словарь сокращений, или вы получите различные сокращения для одного и того же слова, например, description (описание) как desc, descr, descrip, и/или description.
Выбор имен для объектов, в конечном счете, — личный выбор, но никогда это не нужно делать произвольно, и он должен быть основан на существующих общих стандартах, существующем программном обеспечении и доходчивости.
Владелец Пользователь, который создает БД, называется владельцем (owner или dbo) БД. Владелец (dbo) — один из администраторов системы или системных специалистов, создающих БД. Любой пользователь может быть помещен в группу владельцев БД, добавляя функцию db_owner (владелец БД), означающую, что пользователь может создавать и удалять любые данные, таблицы, представления и хранимые процедуры. Когда любой пользователь, не-владелец, создает объект, этот объект принадлежит только этому пользователю (и, конечно, dbo или db_owner). Если пользователь по имени Боб пробует получить доступ к таблице без того, чтобы задать владельца примерно так: Щ SELECT * FROM tableName ... то SQL Server сначала проверяет, существует ли таблица, названная tableName и принадлежащая на самом деле Бобу. Если нет, то проверяется, существует ли таблица с этим именем, принадлежащая dbo. Если это так, но dbo не дал Бобу права доступа к таблице, оператор выдаст ошибку. Для доступа к таблице конкретного владельца непосредственно в коде мы должны определить владельца таблицы, используя две части имени, примерно так: §Ц SELECT * FROM dbo.tableName 268
Планирование и реализация основной физической структуры Если мы определим конкретную таблицу и ее владельца, как сделали в предыдущей строке кода, а эта таблица не существует, возникнет ошибка. Владелец получает неявные права создавать, изменять и удалять данные из таблицы, как и саму таблицу. Любому другому пользователю нужно давать права делать это. В данный момент просто важно понять, что каждая таблица, представление и хранимая процедура в SQL Server имеют владельца. Мы должны также обратить внимание, что SQL Server использует схему имен, включающую до четырех частей: [<сервер>.][<БД>.][<владелец>.]<имя_объекта> где: •
сервер — определяет сервер, на котором расположен объект. Для объектов на том же самом сервере имя сервера должно быть опущено. К объектам на других серверах можно обращаться только в том случае, если администратор системы задал сервер как связанный.
•
БД — определяет БД, где находится объект. Если эта часть опущена, объект разыскивается в БД, из которой к нему обращаются.
•
владелец — определяет пользователя, кто создает/владеет объект(ом). Если эта часть опущена, SQL Server использует пользователя, который связан с объектом по умолчанию.
•
имя объекта — определяет имя таблицы, представления или хранимой процедуры.
Ограничения Перед тем, как идти дальше, мы должны рассмотреть некоторые ограничения, с которыми должны будем работать при создании таблиц в SQL Server. При создании таблиц для нас важны четыре ограничения: •
Число столбцов — Максимальное число столбцов (то есть полей данных) в таблице — 1024. Если БД должным образом нормализована, это за границами обычных требований.
•
Число байт на строку — Максимальное число байт в строке — 8060. В отличие от числа столбцов максимальное число байт не так уж трудно достичь. Мы увидим это, когда начнем рассматривать символьные и двоичные данные.
•
Число байт на вход индекса — Предел — 900 байт, и для этого есть серьезные основания. Страница содержит примерно 8000 байт, так что если бы индекс был намного больше, чем 900, он стал бы очень неэффективным.
•
Число индексов — Предел — 1 кластерный и 249 некластерных. Мы могли бы подойти близко к этому пределу в системах хранилищ данных. Наличие 250 индексов таблицы в OLTP-системе существенно уменьшило бы эффективность работы при частых корректировках, которые, вероятно, будут производиться с данными. Мы обсудим кластерные и некластерные индексы позже в этой главе.
Более полный список основных требований будет дан в главе 13, когда мы рассмотрим требования к аппаратным средствам.
269
Глава 10
Столбцы Строки, выделенные полужирным шрифтом, являются строками, которые мы используем для определения столбца: CREATE TABLE [<БД>.][<владелец>.]<имя_таблицы> ( <имя_столбца> <тип_данных> [ «спецификация NULL>] -- или <имя_столбца> AS <вычисленное_значение> )
Имеются два типа столбцов, с которыми мы будем иметь дело, — физический и вычисленный (или виртуальный). •
Физические столбцы — это обычные столбцы, где реализовано физическое хранение, и данные фактически содержат значения.
•
Вычисленные (или виртуальные) столбцы — столбцы, которые получаются в результате вычислений, включающих значения любого из физических столбцов таблицы.
Мы, прежде всего, будем иметь дело с физическими столбцами, хотя вычисленные столбцы имеют некоторые симпатичные свежие использования.
Обозначение Значение <имя__столбца> — это то, где вы определяете имя, под которым будет известен столбец. Правила обозначения столбцов те же самые, что и для таблиц, и связаны с SQL Server. Что касается того, как мы выбираем имя для столбца — снова, это одна из тех задач для конкретного архитектора, основанная на тех же самых видах критериев, как и прежде (стандарты предприятия, лучшее использование и т. д.). В этой книге мы будем следовать такому перечню основных принципов: •
Q
270
За исключением первичных ключей, имя таблицы не стоит включать в имя столбца. Например, в сущности по имени person (человек) мы не должны иметь столбцы, называемые personName (имя человека) или personSocialSecurityNumber (номер соцального обеспечения человека). Никакой столбец не должен иметь в качестве префикса person за исключением первичного ключа p e r s o n l d . Это уменьшает потребность в функциональном имени (изменяющем имена атрибутов, чтобы корректировать смысл, особенно используемом в случаях, когда мы имеем много мигрирующих внешних ключей). Имя должно быть настолько описательным, насколько возможно. Мы будем использовать очень немного сокращений в наших именах. Имеются три известных . исключения: Сложные имена — Так же как и в именах таблиц, если вы имеете имя, которое содержит несколько частей, например, "Conglomerated Television Rating Scale" (комплексный показатель рейтинга ТВ-передач), вам хотелось бы иметь название вроде ConTvRatScale, даже если потребуется некоторая тренировка, прежде чем ваши пользователи привыкнут к его значению.
Планирование и реализация основной физической структуры Признанные сокращения — Например, если бы мы писали систему закупок и нам нужен бы был столбец для таблицы заказа, мы могли бы назвать объект РО (purchase order — заказ), потому что это очень широко используется. Указатели типов данных — Мы будем иногда добавлять короткую строку (скажем, два символа) в конец имени столбца. Например, столбец, который мы используем как Boolean, будем завершать суффиксом "fl" (сокращение от flag — флаг), а столбец даты, заканчивающийся суффиксом "dt", очевидно, будет означать сокращение даты. Заметьте, что мы не будем использовать эти суффиксы, чтобы указывать точные типы данных (например, вы могли бы реализовать тип Boolean с типами int, t i n y i n t , s m a l l i n t , b i t или даже s t r i n g , содержащей "Да" или "Нет"). Обратите внимание, что мы не упоминали о Венгерской нотации, чтобы обозначить тип столбца. Я никогда не был большим поклонником этого стиля. Если вы не знакомы с Венгерской нотацией, она означает, что мы используем префиксы имен столбцов и переменных, указывающие тип данных и возможное использование. Например, мы могли бы иметь переменную, называемую vclOO_columnName, что указывает тип данных varchar (100). Или мы могли бы иметь столбец типа Boolean или b i t , который назван ЬСаг или isCar. По моему мнению, такие приставки — массовое самоубийство, так как легко взять тип из другой документации, которую мы можем получить из SQL Server или другими методами. Наши индикаторы использования обычно идут в конце имени и необходимы только тогда, когда было бы трудно понять без них, что означает конкретная величина. Храня точный тип имен, мы не будем омрачать детали реализации идентификацией сущностей. Одна из красивых сторон использования реляционных БД — что имеется уровень абстракции, который скрывает детали реализации. Отображать их через обозначение столбца означает сделать их стабильными, хотя требования модернизации могут привести к тому, что они устареют (например, расширяя размер переменной, чтобы приспособить к будущим деловым потребностям).
Домены В логическом моделировании концепция доменов включала построение шаблонов для типов данных и столбцов, которые мы используем многократно. В физическом моделировании домены — те же самые, но с дополнительными свойствами, добавленными с точки зрения физических потребностей. Например, на логической стадии моделирования мы определили домены для таких столбцов, как name (имя) и d e s c r i p t i o n (описание), которые возникают регулярно на протяжении всей БД. Причина определения доменов полностью не могла быть понятна на стадии логического проектирования, но становится совершенно ясной на стадии физического моделирования. Например, для домена пате мы могли бы определить следующее: Свойство
Значение
Имя
Name
Тип данных
V a r c h a r (100)
Возможность значения NULL
Not NULL
Проверка ограничений
LEN (RTRIM(Name) ) > 0 — не может быть пустым
10 1868
271
Глава 10 Большинство таблиц будет иметь столбец name, и мы будем использовать данный шаблон для его построения. Это преследует, по крайней мере, две цели: •
Последовательность — если мы определяем каждый столбец name одним и тем же образом, никогда не возникнет вопрос, как обращаться со столбцом.
•
Простота реализации — если средство, которое вы используете для моделирования/реализации БД, поддерживает создание доменов/шаблонов столбцов, вы можете просто использовать шаблон, чтобы построить столбцы, и не должны многократно устанавливать значения. Если средство поддерживает наследование свойств, то когда вы изменяете свойство в определении, значения изменятся всюду.
Домены не являются необходимостью хорошего проекта БД, логического или физического, и не должны использоваться в обязательном порядке в SQL Server, но они позволяют легко и последовательно проектировать, что само по себе является хорошей идеей. Конечно, последовательное моделирование — всегда хорошо, независимо от того, действительно ли вы используете программное средство, которое делает работу для вас.
Выбор типов данных | <имя_столбца> <тип данных> [<спецификация_Ыиьь>]
Выражение <тип_данных> используется для выбора типа данных столбца. Выбор надлежащих типов данных, чтобы соответствовать домену, определенному на стадии логического моделирования — очень важная задача. Один тип данных мог бы быть более эффективен, чем другой, подобный тип. Например, размещение целых данных можно сделать с использованием типа целых данных, числовых данных или даже с плавающей точкой, но они, конечно, не эквивалентны в реализации или функционировании. В этом разделе мы рассмотрим все встроенные типы данных, которые обеспечиваются фирмой Microsoft, и обсудим ситуации, где их лучше использовать.
Точные числовые данные Имеется много базовых типов данных, в которых вы можете хранить числовые данные. Мы рассмотрим две различные категории числовых данных: точные и приближенные. Различия очень важны и должны быть хорошо поняты любым архитектором, кто создает систему, которая хранит показания, измерения или другие числовые данные. Точные числовые значения включают типы b i t , i n t , b i g i n t , s m a l l i n t , t i n y i n t , decimal и денежные типы данных (money и smallmoney). Точные значения не имеют никакой ошибки, как бы они ни были размещены — в виде целых чисел или чисел с плавающей точкой, потому что они имеют фиксированное число цифр до и после десятичной точки (или другой системы счисления). Однако когда мы должны хранить десятичные значения в точных типах данных, мы заплатим за работу с ними тем, что будем должны указать, как они хранятся и как с ними работать.
272
Планирование и реализация основной физической структуры bit Тип b i t (бит) имеет значения 0, 1 или NULL. Он обычно используется как тип Boolean. Это — не идеал типа Boolean, потому что SQL Server не имеет отдельного типа данных Boolean, но это — лучшее, что мы в настоящее время имеем. Столбцы типа b i t не могут быть индексированы — наличие только двух значений (фактически три с NULL) приводит к неэффективному индексу, так что он не должен использоваться в сложных поисковых комбинациях полей. Если возможно, вы должны избегать значения NULL в полях типа b i t ; включение значений NULL в логическое сравнение увеличивает сложность запроса и вызывает проблемы. Столбец типа b i t требует одного байта для хранения восьми экземпляров данных таблицы. Следовательно, наличие восьми столбцов типа b i t приведет к тому, что ваша таблица будет не больше, чем если бы она имела только один столбец типа b i t . int Целые числа от -2 147 483 648 до 2 147 483 647 (то есть от -2
31
до 2
31
- 1).
Тип данных i n t (целое число — можно также использовать идентификатор integer, прим. перев.) используется, чтобы хранить знаковые (+ или-) целые числа. Тип данных i n t часто используется как первичный ключ для таблиц, поскольку он очень мал (требует четырех байт хранения) и очень эффективен для хранения и извлечения. Единственный реальный недостаток типа данных i n t состоит в том, что он не включает беззнаковой версии, которая хранила бы неотрицательные значения от 0 до 4294967296 (или 32 2 ). Поскольку большинство значений первичного ключа начинается с 1, это дало бы нам более двух миллиардов дополнительных значений для первичного ключа. Это может казаться ненужным, но системы, которые имеют миллиарды строк, становятся все более обычными. Другое приложение, где поле типа i n t играет важную роль — хранение IP-адресов как целых чисел. IP-адрес — просто 32-битовое целое число, разбитое на четыре октета. Например, если вы имеете IP-адрес 234.23.45.123, то можете взять (234 * 2563) + (23 * 2562) + + (45 * 256 ) + (123 * 256 ). Это значение будет хорошо подходить под 32-битовое целое число без знака, но не подходит для знакового числа. Однако в SQL Server есть 64-битовое целое число, которое полностью покрывает текущий стандарт IP-адресов, но требует вдвое больше памяти. bigint Целые числа от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 (то есть от -2 6 3 до 2 ё з - 1). Единственная причина использовать 64-битовый тип данных b i g i n t (большое целое) — в качестве первичного ключа для таблиц, где вы будете иметь больше чем два миллиарда строк, или если ситуация непосредственно диктует это, наподобие ситуации с IP-адресами, которую мы только что обсудили. smallint Целые числа от -32768 до 32767 (или от - 2 1 5 до 2 1 5 - 1). Если мы можем гарантировать, что нам нужно меньше чем 32767 элементов, может быть полезен s m a l l i n t (малое целое). Он требует 2 байта хранения. 273
Глава 10 Использование s m a l l i n t может быть компромиссом. С одной стороны, это экономит два байта по сравнению с типом i n t , но с другой стороны s m a l l i n t недостаточно велик, чтобы использовать его в любых случаях. Если данные могут вписаться в ограничения этого домена, и ваша таблица будет очень большой, стоит использовать s m a l l i n t , чтобы сэкономить два байта. Однако использование s m a l l i n t для уменьшения БД может привести к исчерпанию места, что потребует изменить весь код, чтобы использовать другой тип данных, — все это может оказаться слишком сложным. Мы будем использовать для нашей БД тип i n t . Также обычно плохо использовать s m a l l i n t для первичного ключа (кроме особых ситуаций, типа того, когда нам нужны миллиарды строк, или когда байт или два приведут к изменению в функционировании). Однородность требует кодировать нашу БД более последовательно. Это может показаться несущественным, но большинство средних систем намного более легко закодировать, когда вы автоматически знаете, каков тип данных. Одно из использований s m a l l i n t , которое возникает время от времени, — использование его как тип Boolean. Я предполагаю, что это возникает потому, что в Visual Basic 0 равен False, a -1 — True (фактически Visual Basic будет обращаться с любым значением, отличным от нуля, как с True). При хранении данных таким образом — не только огромная трата места (2 байта против потенциально 1/8 байта), но и запутывание всех других программистов SQL Server. Драйверы ODBC и OLE DB делают этот перевод для вас, но даже если бы они не делали это, стоит потратить время, чтобы написать метод или функцию на Visual Basic, чтобы перевести True в значение 1. tinyint Целые числа от 0 до 255. Те же самые комментарии, что и для s m a l l i n t , могут быть сделаны для t i n y i n t (очень маленькое целое). Тип t i n y i n t очень маленький, использует один байт для хранения, но мы редко используем его, потому что имеется очень немного ситуаций, где мы можем гарантировать, что значение никогда не будет превышать 255 или принимать отрицательные значения. Единственное использование, которое я сделал по отношению к t i n y i n t за эти годы в предыдущих версиях SQL Server, — если мне нужно было группировать немного столбцов. Я не мог бы сделать это непосредственно, если бы запускал запрос подобно этому:
I
SELECT bitColumn, count (' FROM testbit Group GROUP BY bitColumn
В этом случае я получал ошибку. Однако я написал следующий запрос, и он заработал:
I
SELECT CAST(bitColumn AS t i n y i n t ) AS b i t C o l u m n , FROM t e s t b i t Group GROUP BY CAST(bitColumn AS t i n y i n t )
count(*)
SQL Server 2000 больше не делает это различие, но оно может все еще вызвать замешательство, если вы должны писать запросы, которые нужны для перехода от одной к другой версии SQL Server.
274
Планирование и реализация основной физической структуры decimal (или numeric) Все числовые данные между -10 38
1 и 10 3 8 - 1.
Тип данных decimal (десятичный) или numeric (числовой) — точный тип данных, потому что он размещается наподобие символов (как если бы данные имели только 12 символов, включающие цифры от 0 до 9, минус и десятичную точку). Способ, которым он (тип) размещается, предотвращает от неточности, которую мы увидим немного позже в типах чисел с плавающей точкой (float) и вещественных (real) числах. Это, однако, приводит к дополнительным затратам при получении и управлении значениями. Чтобы определить десятичное число, мы должны задать точность и масштаб: •
Точность — общее количество значащих цифр в числе. Например, число 10 имеет точность 2, а 43.00000004 — точность 10. Точность может изменяться в пределах от 1 до 38.
•
Масштаб — возможное число значащих цифр, расположенных правее десятичной точки. Используя наш предыдущий пример, 10 имело бы масштаб 0, а 43.00000004 потребует масштаба 8.
Числовые типы данных ограничены точностью и масштабом, определяющими, насколько велики данные. Например, возьмем следующее объявление числовой переменной: Ц
DECLARE
@testvar
decimal(3,1)
Это позволит нам вводить любую числовую величину больше чем -99.94 и меньше чем 99.94. Ввод 99.949999 допустим, но ввод 99.95 — нет, потому что это будет округлено до 100.0, что не может быть отображено с помощью decimal (3, 1). Например, следующие операторы:
О
SELECT @testvar = -10.155555555 SELECT @testvar
возвращают -10.2. Когда язык программирования компьютера делает это для вас, операция рассматривается как неявное преобразование. Это является и благословением, и проклятием. Следует иметь в виду, что вы должны быть очень осторожны у границы допустимых значений типа данных. Обратите внимание, что имеется параметр настройки SET NUMERIC_ROUNDABORT ON (задание отбрасывания при округлении числовых данных), который вызовет ошибку, если произойдет потеря точности от неявного преобразования данных. Его весьма опасно использовать, так как приложения, использующие SQL Server, могут быть выброшены, если параметр включен. Однако если вы должны предотвратить неявное округление с помощью системных ограничений, это очень ценный инструмент. Хранение числовых типов зависит от того, какая требуется точность: Точность
Требуемое число байт
1-9
5
1 0 - 19
9
2 0 - 28
13
29-38
17
275
Глава 10 Что касается использования, то тип данных decimal должен вообще использоваться экономно. Нет ничего плохого с типом вообще, но он требует немного большей обработки, чем целые или вещественные данные, и, следовательно, имеется помеха в выполнении. Вы должны использовать этот тип, когда имеете определенные значения, которые желательно хранить, не теряя точности. Дополнительно с темой потери точности мы более подробно будем иметь дело в разделе "Приближенные числовые данные". Денежные величины Денежные типы данных — обычно целые числа с десятичной точкой, помещенной в число. Так как это всегда имеет одну и ту же точность и масштаб, процессор может работать с такой величиной подобно обычному целому числу, а затем механизм SQL Server может вставить десятичную точку назад, как только математические функции будут выполнены. Имеются две разновидности денежных переменных: •
money (денежный) — величины от -922 337 203 685 477.5808 до 922 337 203 685 477.5807 с точностью одной десятитысячной денежной единицы. Как можно было бы ожидать, это, очевидно, целое число из 64 бит и требует восьми байт хранения.
•
smallmoney (малый денежный) — величины от -214 748.3648 до +214 748.3647 с точностью одной десятитысячной денежной единицы. Это требует четырех байтов хранения.
Типы данных money и smallmoney — превосходное средство хранения для денежных величин практически с любыми требованиями, если только не требуется хранить наши денежные величины с более чем четырьмя десятичными знаками, что обычно не встречается в большинстве ситуаций. Столбцы идентификации Для типов данных i n t или decimal (с масштабом 0) мы можем создавать автоинкрементный столбец, чьи значения гарантированно будут уникальными для таблицы. Столбец, который реализует идентификацию, должен быть также определен как NOT NULL (не равен NULL). CREATE TABLE testldentity ( identityColumn int NOT NULL IDENTITY (1, 2), value varchar(lO) NOT NULL
I Здесь создается таблица testldentity (проверка идентификации), состоящая из двух столбцов: первого столбца identityColumn (столбец идентификации) типа int, элементы которого не могут принимать значение NULL и использующего функцию IDENTITY (идентификация), и второго столбца value (значение) типа varchar (10), элементы которого не могут принимать значение NULL. В этом операторе CREATE TABLE я добавил функцию IDENTITY для столбца identityColumn. Дополнительные значения в круглых скобках известны как начальное значение и приращение. Начальное значение 1 указывает, что мы начнем с первого значения, равного 1, а приращение говорит, что второе значение будет 3, далее 5 и т. д.
276
Планирование и реализация основной физической структуры В следующем фрагменте мы вставляем три новых строки в таблицу t e s t l d e n t i t y : INSERT VALUES INSERT VALUES INSERT
INTO testldentity ('один') INTO testldentity ('два') INTO testldentity
(value) (value) (value)
VALUES ('три') SELECT * FROM t e s t l d e n t i t y
Это дает следующий результат: identityColumn 1 3 5
value один два три
Свойство идентификации фантастически удачно для создания первичного ключа типа указателя, который является и небольшим, и быстрым. (Следует помнить и другое — он не должен быть единственным ключом таблицы, или мы фактически не будем иметь никакой уникальности, за исключением этого произвольного значения!) Тип данных i n t требует только четырех байтов и очень хорош для большинства таблиц, которые мы создаем, если они будут иметь меньше чем 2 миллиарда строк. Одно замечание. Идентифицирующие значения могут иметь пропуски в последовательности. Если происходит ошибка при создании новой строки, идентифицирующее значение, которое должно быть использовано, будет потеряно. Аналогично, если строка удаляется, удаленная величина не будет повторно использоваться. Следовательно, вы не должны использовать столбцы идентификации, если не можете допустить это ограничение на значения в вашей таблице. Если вы должны гарантировать уникальность по всем таблицам или даже нескольким БД, рассмотрите создание столбца с типом данных uniqueidentifier (уникальный идентификатор) и с отказом от автоматического формирования величин. Мы обсудим эту реализацию позже в разделе, посвященном типам данных.
Приближенные числовые данные Приближенные числовые значения содержат десятичную точку и размещаются в формате, которым можно быстро манипулировать, но они точны только до 15-го десятичного знака. Приближенные числовые значения имеют некоторые очень важные преимущества, как мы увидим позже в этой главе. Приближенный — неприятный термин, но фактически правильный. Он означает типы данных r e a l (вещественный) и f l o a t (с плавающей точкой), которые соответствуют стандарту IEEE 75454 величин с плавающей точкой одинарной и двойной точности. Причина, почему они называются величинами с плавающей точкой, связана со способом их хранения. В основном, число хранится как 32- или 64-битовая величина с четырьмя частями: •
знак — определяет, имеет ли оно положительное или отрицательное значение; 277
Глава 10 •
показатель — показатель мантиссы по основанию 2;
•
мантисса — хранит фактическое число, которое умножается на показатель;
•
смещение — определяет, положительным или отрицательным является показатель.
Полное описание того, как эти типы данных фактически сформированы — вне возможностей этой книги, но может быть получено из содержания IEEE Bwww.ieee.org для формального понимания. Имеются две разновидности типов данных с плавающей точкой, доступных в SQL Server: •
f l o a t [ (N) ] — величины в диапазоне от -1.79Е +308 до 1.79Е +308. Тип данных f l o a t позволяет нам определять конкретное число бит, используемых в мантиссе — от 1 до 53. Мы определяем это число бит величиной N.
•
r e a l — величины в диапазоне от -3.40Е +38 до 3.40Е +38. r e a l — синоним для типа данных f l o a t (24).
Размещение и точность для типов данных f l o a t следующие:
N (число бит мантиссы) 1 -=- 24 25 + 53
Точность
Размер для хранения
7
4 байта
1 5
8
б а й т
Следовательно, мы способны представить большинство значений от -1.79Е +308 до 1.79Е +308 с точностью 15 значащих цифр. Это — не так уж и много значащих цифр по сравнению с числовыми типами данных, но, несомненно, достаточно почти для любого научного приложения. Обратите внимание, что в предыдущем абзаце мы сказали "представить большинство значений". Это — проблема, которую мы должны понять. Имеются некоторые значения, которые просто не могут быть размещены в формате с плавающей точкой. Классический пример этого — значение 1/10. Нет никакого способа хранить это значение точно. Когда мы выполним следующий фрагмент кода: Щ
SELECT C A S T ( 1 AS f l o a t ( 5 3 ) )
/ C A S T ( 1 0 AS f l o a t ( 5 3 ) )
AS
funny_result
то ожидаем, что результат будет точно равен 0.1. Однако, выполнив оператор, получим: funny_result 0.10000000000000001 Даже если мы изменим ввод на: Щ
SELECT CAST(.l AS float) AS funny_result
мы получим тот же неправильный результат. Различие интересно, но несущественно для всех вычислений, кроме высокоточных. Типы данных f l o a t и r e a l — встроенные типы данных, с которыми можно работать, используя сопроцессор для вещественных чисел. Сегодня во всех процессорах этот сопроцессор — часть главного центрального процессора, и хотя это несколько более дорого, чем математика целых чисел, которая может быть 278
Планирование и реализация основной физической структуры
выполнена непосредственно в регистрах центрального процессора, но неизмеримо лучше, чем типы данных точных нецелых чисел. Недостаток их в том, как организовано хранение данных с плавающей точкой. Любое дальнейшее обсуждение недостатков размещения величин с плавающей точкой — вне возможностей этой книги. Приближенные числовые типы данных очень важны для хранения измерений, когда требуются вычисления. Происходящее округление несущественно, поскольку пока очень немного устройств, которые могут производить измерения с 15 значащими цифрами, и, поскольку вы будете, вероятно, делать большое количество математических вычислений с измеренными величинами, результат будет значительно лучше по сравнению с использованием десятичного типа. При кодировании значений с плавающей точкой вообще лучше всего стараться избегать сравнивать эти значения, используя для сравнения операцию =. Поскольку значения приблизительны, не всегда возможно ввести точное значение, которое было размещено. Если вам нужно искать точные значения, то лучше использовать числовые типы данных.
Дата и время-дата Если вы вспомните, мы не первый раз обсуждаем типы данных d a t e t i m e и s m a l l d a t e t i m e . Первый раз мы столкнулись с ними при обсуждении нарушений 1НФ. В этом разделе мы рассмотрим характеристики типов d a t e t i m e , а затем обсудим некоторые альтернативы. smalldatetime Этот тип (укороченная дата-время) представляет данные даты и времени с 1 января 1900 года до 6 июня 2079 года. Тип s m a l l d a t e t i m e имеет точность в одну минуту. Это требует четырех байтов хранения. Тип s m a l l d a t e t i m e — лучший выбор, когда вы должны хранить дату и, возможно, время некоторого события, где точность в минуту — не проблема. Если требуется более высокая точность, то, вероятно, лучше выбрать тип d a t e t i m e (дата-время). datetime Этот тип представляет данные даты и времени с 1 января 1753 года по 31 декабря 9999 года. Размещение типов данных s m a l l d a t e t i m e и d a t e t i m e интересно. Тип d a t e t i m e размещается в два 4-байтовых целых числа. Первое целое число хранит число дней до или после 1 января 1990 года. Второе хранит число миллисекунд после полуночи конкретного дня. Значения s m a l l d a t e t i m e размещаются сходным образом, но используют 2-байтовые целые числа вместо 4-байтовых. Первое — 2-байтовое целое число без знака, а второе используется, чтобы хранить число минут после полуночи. Тип d a t e t i m e имеет точность 0.03 секунды. Использование восьми байт, однако, требует значительного объема памяти. Используйте d a t e t i m e или когда вам нужен расширенный диапазон, или когда вам нужна высокая точность. Редко бывает, когда вам нужна такая точность. Единственное приложение, которое приходит на ум — столбец временных меток (не путайте с типом данных timestamp, который мы обсудим в одном из последующих разделов этой главы), который используется, чтобы точно обозначить время операции. Нет ничего удивительного, если мы хотим получить информацию о времени, например, время в секундах, прошедшее между двумя действиями, или если мы хотим использовать значение datetime в механизме управления параллельным выполнением операций. Мы рассмотрим глубже тему управления параллельным выполнением операций позже в этой главе.
279
Глава 10 Использование определяемых пользователем типов данных для управления датами и временами Одно слово относительно хранения значений даты и времени. Использование типов данных datetime, когда вам нужна часть, связанная только с датой, или только со временем, — сплошные мучения. Возьмем следующий пример кода. Мы хотим найти каждую запись, где дата равна 12 июля 1967 года. Если мы закодируем это самым очевидным образом: SELECT * FROM employee WHERE birthDate - 'July 12, 19 67'
I
Здесь employee — служащий, b i r t h D a t e — дата рождения. Мы получим совпадение для каждого служащего, где b i r t h D a t e точно соответствует "Июль 12, 1967 00:00:00.000". Однако если дата имеет вид "Июль 12, 1967 10:05:23.300", как это могло бы быть в соответствии с проектом (то есть записано точное время рождения), или по ошибке (то есть, управление датой могло послать текущее время по умолчанию, если мы забыли очистить это) — мы можем замучиться необходимостью писать запросы подобно следующему, чтобы ответить на вопрос, кто был рожден 12 июля. Чтобы сделать это, мы были бы должны переписать наш запрос следующим образом:
I
SELECT * FROM employee WHERE birthDate >- 'July 12, 1967 0:00:00.000' AND birthDate < 'July 13, 1967 0:00:00.000'
Заметьте, что мы не использовали оператор BETWEEN в этой операции. Если бы мы хотели использовать этот оператор, то получили бы:
I
WHERE birthDate BETWEEN 'July 12, 1967 AND 'July 12, 1967 23:59:59.997'
0:00:00.00'
... сначала исключив всякие даты 13 июля, а затем избегая любые округления типа данных datetime. Так как точность — 0.03 секунды, то когда будет оцениваться величина 'July 12, 1967 23:59:59.997', она будет округлена до 'July 13, 1967 0:00:00.00'. Все это не только неприятно и тяжело, но и может быть неэффективно. В случае, где нам не нужна часть типа данных datetime, связанная с датой, или часть, связанная со временем, лучше всего подойдет создание собственного типа данных. Например, возьмем типичный случай хранения времени, когда кое-что происходит, без учета даты. Имеются две возможности разобраться с этой ситуацией. Обе — намного лучше, чем просто использование типа данных d a t e t i m e , но каждая имеет свой собственный набор проблем. Q
280
Использование нескольких столбцов — каждый просто для часов, минут, секунд и отдельных долей секунд, если это вам нужно. Проблема здесь состоит в том, что не просто сформировать запрос. Вам нужно, по крайней мере, три столбца, чтобы запросить нужное время, и вам потребуется проиндексировать все столбцы, используемые в запросе, чтобы получить хорошее выполнение на больших таблицах. Размещение будет лучше, поскольку мы можем использовать для отдельных столбцов тип t i n y i n t . Если вы хотите посмотреть на то, что происходило в течение десятого часа дня в течение недели, то вы могли бы использовать этот метод.
Планирование и реализация основной физической структуры •
Использование единственного столбца, который содержит число секунд (или частей секунд) между полуночью и текущим временем. Однако в этом случае нельзя сформировать разумный запрос по всем значениям, которые возникают в течение, скажем, десятого часа, и если бы мы определили число секунд, это было бы всякое значение между 36000 и 39600. Однако это идеально, если мы используем данные в некотором виде внутренней процедуры, с которой пользователи-люди не должны взаимодействовать.
При использовании этих столбцов мы могли бы создать определяемые пользователем функции, чтобы преобразовывать наши типы данных к удобочитаемым значениям. В качестве другого примера мы могли бы создать наш собственный тип даты, который просто реализован как целое число. Мы могли бы добавить определяемый пользователем тип, названный intDateType (большее освещение этой темы будет дано позже в этой главе): EXEC sp_addtype Stypename = intDateType, @phystype = i n t e g e r , @NULLtype = 'NULL', @owner = ' d b o ' GO
И затем формируется определяемая пользователем функция для преобразования значений переменной с нашими данными в реальную переменную типа d a t e t i m e , чтобы использовать в вычислениях даты или просто, чтобы показать клиенту. CREATE FUNCTION i n t D a t e T y p e $ c o n v e r t T o D a t e t i m e ( @dateTime intDateType ) RETURNS d a t e t i m e AS BEGIN RETURN(dateadd(day, @ d a t e t i m e , ' j a n 1, 2 0 0 1 ' ) ) END GO
Из этого основного примера должно быть довольно очевидно, что определяемая пользователем функция будет чрезвычайно важной частью нашего кодирования приложений SQL Server. Мы будем использовать ее несколько раз в этой главе, чтобы иллюстрировать то или иное положение, но мы рассмотрим ее более полно в следующей главе, где будем ее использовать для защиты наших данных. Мы увидим ее снова в главе 11, когда будем управлять данными. В данный момент просто важно обратить внимание, что эти особенности существуют. Чтобы проверить нашу новую функцию, мы могли бы начать с просмотра второго дня года, преобразовывая дату в переменную типа d a t e t i m e : §Ц SELECT d b o . i n t D a t e T y p e $ c o n v e r t T o D a t e t i m e ( 1 ) AS c o n v e r t e d V a l u e
что возвращает: convertedValue 2001-01-02 00:00:00.000 281
Глава 10 Мы должны были бы построить несколько других функций ввода данных, чтобы взять переменную типа d a t e t i m e и разместить ее как новый тип данных. Хотя мы преднамеренно не рассматривали многие детали определяемых пользователем функций, я предложил диапазон возможностей решения проблемы даты или времени. Тип данных d a t e t i m e представляет проблемы для программистов, с которыми вы должны иметь дело. Только будьте осторожны.
Двоичные данные Двоичные данные позволяют нам размещать последовательность бит с диапазоном от одного байта (восемь бит) до примерно двух гигабайт бит (величина 2г - 1 или 2 147 483 647 байт). Концепция, которую мы начнем рассматривать в связи с двоичными типами данных — использование данных переменной длины. В основном это означает, что размер хранилища данных для каждого экземпляра столбца или переменной этого типа данных может изменяться в зависимости от того, сколько данных размещается. Во всех предыдущих типах данных хранилище было фиксировано, потому что размещались представления величин фиксированной длины, которые интерпретировались, используя некоторую формулу (например, целое число в 32 бита или дата — число единиц времени, начиная с некоторого момента времени). Любой тип данных, который допускает значения переменной длины, обычно связан с вопросом размещения значений вместе, подобно буквам в слове. Данные переменной длины обычно включают две части — значение, определяющее, какова длина данных, и данные сами по себе. Имеется некоторый верхний предел для размещения данных переменной длины, но это обычно лучше, чем размещение данных в столбцах фиксированной длины со значительным потраченным впустую местом. Меньшие данные означают большее количество записей на страницу, что уменьшает время ввода/вывода, а операции ввода/вывода гораздо более дорогостоящи, чем расположение полей в записи. Двоичные данные размещаются в трех различных вариантах: •
b i n a r y (двоичный) — данные фиксированной длины с максимальной длиной 8000 байт. Используйте только тип binary, если вы знаете, насколько велики будут ваши данные. Хранилище будет эквивалентно числу байт, которое мы объявим для переменной.
•
v a r b i n a r y (переменный двоичный) — для двоичных данных с переменной длиной и максимальной длиной 8000 байт.
•
image (изображение) — для действительно больших данных изображений 3 переменной длины с максимальной длиной 2 - 1 (2 1.47 483 647) байт.
Одно из ограничений двоичных типов данных — то, что они не поддерживают побитовые операторы, которые позволили бы нам создавать конкретное очень мощное хранилище битовой маски, позволяющее сравнивать два двоичных столбца, чтобы видеть не только то, что они отличаются, но и как они отличаются. Общая идея двоичных типов данных состоит в том, что они хранят последовательности бит. Побитовые операторы могут работать с целыми числами, которые физически размещаются как биты. Причина этой несогласованности фактически довольно ясна с точки зрения внутреннего процессора запросов. Побитовые действия — фактически действия, которые выполняются в процессоре, в то время как двоичные типы данных — специфические особенности SQL Server.
282
Планирование и реализация основной физической структуры Дополнительная концепция, которую мы должны обсудить — то, как SQL Server хранит большие объемы данных, таких как тип image, обычно называемый как BLOB (Binary Large Objects) — большие двоичные объекты. Когда тип данных должен хранить данные, которые являются потенциально больше по размеру, чем страница SQL Server, требуется специальная обработка. Вместо хранения фактических данных на странице с записью, размещается лишь указатель на другое физическое место памяти. Указатель — 16-байтовая двоичная величина (тот же самый рассмотренный нами тип данных), которая указывает на другую страницу в БД. Страница может хранить значение одного или нескольких BLOB, и BLOB может занимать много страниц. Использование полей binary, особенно полей image, довольно ограничено. В основном они могут применяться для хранения любых двоичных значений, с которыми SQL Server не работает. Мы можем хранить текст, изображения в формате JPEG и GIF (JPEG — стандартный алгоритм сжатия изображений, GIF — формат графического обмена) и даже документы Word и электронные таблицы Excel. Так почему использование ограничено? Простой ответ — функционирование. Вообще-то плохо использовать тип данных image, чтобы хранить очень большие файлы, потому что их долго восстанавливать из БД. Когда нужно хранить данные изображения в БД, мы будем обычно хранить имя файла совместного доступа. Программа доступа будет просто использовать имя файла, чтобы обратиться к внешнему хранилищу файла. Файловые системы создаются именно для хранения и обслуживания файлов, так что мы используем их по прямому назначению.
Строки символов Большинство данных, размещаемых в SQL Server, использует символьные типы данных. Действительно, обычно достаточно большое количество данных размещается в символьных типах данных. Часто символьные поля используются, чтобы хранить несимвольные данные типа чисел и дат. Хотя это фактически и допустимо, но это — не идеал. Для систем инициализации, чтобы хранить число с восемью цифрами в символьной строке, требуется, по крайней мере, восемь байт, в то время как целое число требует четырех байтов. Поиск на целых числах гораздо легче, потому что 1 всегда предшествует 2, в то время как в символьной строке 11 предшествует 2. Кроме того, целые числа размещаются в формате, которым можно управлять, используя внутренние функции процессора в противоположность специальным функциям SQL Server для работы с данными. char Тип данных char (символьный) используется для символьных данных фиксированной длины. Вы должны выбрать размер данных, которые желаете хранить. Каждое значение будет размещено в одно и то же число символов, максимум до 8060 байт. Хранилище — точное число байт в соответствии с определением столбца независимо от фактически размещенных данных; любое остающееся место справа от последнего символа заполняется пробелом. Обратите внимание что в версиях SQL Server, ранее чем 7.0, реализация столбцов char отличается. Установка ключа ANSI_PADDING точно определяет, как это фактически происходит. Если этот ключ установлен, таблица точно такая, как мы описали; если нет, данные будут размещаться так, как мы рассмотрим в разделе ниже, посвященном типу varchar. Обычно лучше задавать установку набора значений ANSI для большей стандартизации (или даже большего ограничения), поскольку эти установки помогут вам избежать проблем с модернизацией SQL Server в вашем кодировании, которые имеются при переходе от версии к версии. 283
Глава 10 Максимальный предел для типа char — 8060 байтов, но очень маловероятно, что вы когда-либо доберетесь до этого предела. Для символьных данных, где ожидается большее число символов, вы использовали бы один из других символьных типов. Тип данных char следует использовать только в случаях, где гарантируется одно и то же число символов в каждой строке, и каждая строка не содержит значение NULL. По этой причине мы используем этот тип очень редко, так как для большинства данных не справедливо соглашение о равной длине. Имеется один пример, где тип char мог бы использоваться довольно часто — это идентификационные числа. Они могут содержать смесь алфавитных и числовых символов типа номеров транспортных средств (VIN — Vehicle Identification Number). Обратите внимание, что это — фактически составной атрибут, поскольку вы можете определить многое относительно автомобиля по его номеру. Другой пример, где поле char обычно используется — номера социального обеспечения. varchar Для типа данных v a r c h a r (переменная строка символов) вы выбираете максимальную длину данных, которые желаете разместить, — до 8000 байтов. Тип данных v a r c h a r гораздо более полезен, чем char, поскольку данные не должны иметь одну и ту же длину, и SQL Server не дополняет излишки памяти пробелами. Однако имеются некоторые дополнительные издержки в хранении данных переменной длины. Используйте тип данных varchar, когда ваши данные обычно небольшие, скажем, несколько сотен байт или меньше. Положительный момент относительно столбцов varchar состоит в том, что независимо от того, насколько большим вы задаете максимум, размещенные данные — это текст в столбце плюс немного дополнительных байт, которые определяют длину данных. Мы будем вообще стараться выбирать максимальный предел для нашего типа данных, который является подходящим значением, достаточно большим, чтобы обеспечить большинство ситуаций, но не слишком большим, чтобы быть непрактичным для наших приложений и отчетов. Например, возьмем имена людей. Они, очевидно, требуют типа varchar, но какой длины мы должны позволить быть данным? Имена обычно имеют максимум пятнадцать символов, хотя вы могли бы определить двадцать или тридцать символов для маловероятных исключений. Данные v a r c h a r — наиболее распространенный тип размещения для неключевых значений, который мы будем использовать. text Тип данных t e x t (текст) используется, чтобы размещать большое количество символьных данных. Он может хранить максимум 2 147 483 647 (2 - 1) символов, или 2 Гб. Размещение типа t e x t во многом подобно типу данных image. Однако дополнительная особенность позволяет нам обходиться без указателя и размещать данные t e x t непосредственно на странице. Это возможно в SQL Server, если выполняются следующие условия: •
величина столбца меньше, чем 7000 байт;
•
данные во всей записи не будут превышать одной страницы данных;
•
опция таблицы Text In Row установлена, используя процедуру s p _ t a b l e o p t i o n , примерно следующим образом:
§§ EXECUTE s p _ t a b l e o p t i o n ' T a b l e N a m e ' , ' t e x t 284
in row',
'1000'
Планирование и реализация основной физической структуры В этом случае любые столбцы текста в таблице TableName будут хранить свой текст со строкой вместо выделения их в другие страницы, если только столбец не достигает 1000 символов или полная строка не слишком велика, чтобы уместиться на странице. Если строка становится слишком большой, чтобы уместиться на странице, один или большее количество столбцов текста будет удалено из строки и помещено на отдельных страницах. Эта установка позволяет нам использовать значения текста разумным образом, так, что когда размещаемое значение маленькое, функционирование в основном то же самое, что и для столбца varchar. Следовательно, мы можем теперь использовать тип данных t e x t , чтобы иметь символьные данные, где длина может изменяться от десяти символов до 50 Кб текста. Это очень важная особенность для столбцов типа примечаний, когда пользователи хотят иметь место для неограниченных текстовых примечаний, но часто помещают только десять или двадцать символов в каждой строке. Другая проблема с полями t e x t — то, что они не могут быть индексированы, используя обычные методы индексации. Как рассмотрено в предыдущей главе, если вы должны выполнять сложный поиск в текстовых столбцах, то можете использовать возможности полнотекстового поиска в SQL Server 2000. Следует также отметить, что даже если бы мы могли индексировать текстовые столбцы, это вряд ли была хорошая идея. Так как столбцы типа t e x t или даже большие столбцы типа v a r c h a r могут быть настолько большие, то невозможно их использовать для индексов, имеющих предел в 900 байт. Строки символов Unicode До сих пор типы символьных данных, которые мы обсудили, касались хранения обычных ASCII-данных. В SQL Server 7.0 (и NT 4.0) фирма Microsoft реализовала новый стандартный формат символов, называемый Unicode. Он определяет 16-битовый формат символов, который может хранить символы не только латинского алфавита. В ASCII-системе символов из семи бит (с восемью битами для латинских расширений) мы были ограничены 256 различными символами, которых было достаточно для большинства англоговорящих людей, но недостаточно для других языков. Восточные языки имеют символ для каждого отдельного слога и эти символы не являются алфавитными; языки Среднего Востока используют несколько различных символов для одной и той же буквы в зависимости от ее положения в слове. По этой причине был создан 16-битовый стандарт символов, позволяющий нам иметь 65536 различных символов. Для этих данных мы имеем типы данных nchar, nvarchar и ntext. Они — точно те же самые, что и типы, имеющие подобные названия (без п), которые мы уже описали, если бы не одна вещь: использование Unicode удваивает число байт для хранения информации, так что требуется вдвое больше места; это уменьшает наполовину число символов, которые могут быть размещены. Один небольшой совет. Если вы хотите определить значение Unicode в строке, вы добавляете символ N к началу строки, наподобие следующего: Ц SELECT N'Unicode Value'
Вариантные данные Вариантный тип данных позволяет вам хранить почти любой тип данных, который был рассмотрен. Это позволяет нам создавать столбец или переменную, где мы не знаем точно, какие данные будут размещены. Это новая особенность SQL Server 2000. Название нового типа данных — s q l _ v a r i a n t (вариантный тип для SQL), и он позволяет нам хранить значения любого поддерживаемого SQL Server типа данных, кроме t e x t , n t e x t , timestamp и s q l _ v a r i a n t (может показаться странным, что вы не можете хранить вариантный тип в вариантном типе, но все это говорит о том, что тип данных s q l _ v a r i a n t фактически не существует, a SQL Server выбирает лучший тип хранения для размещения вашего значения). 285
Глава 10 Преимущества вариантного типа Тип данных s q l _ v a r i a n t позволит нам создавать определяемые пользователем таблицы типа "набора свойств", которые помогут нам избежать очень длинных таблиц со многими столбцами, которые могут быть, а могут и не быть заполнены. Возьмем таблицу e n t i t y P r o p e r t y (свойство сущности) в следующем примере: entity entityld: int Attribute!: varchar(20) Attribute2: varchar(20) AttributeN: varchar(20)
entityProperty entityProperty Id: int entityld; int propertyValuel: int propertyValue2: datetime propertyValue3: varbinary(6) propertyValue4: varchar(60) propertyValue5: varchar(2000) propertyValue6: varchar(30) propertyValue7: nchar(20)
i |
|
propertyValueN: varchar(30) Здесь entity — таблица "сущность"; entityld — идентификатор сущности; AttributeN— N-й атрибут; entityProperty — таблица "свойство сущности"; entityPropertyld — идентификатор свойства сущности; propertyValueN — N-e значение свойства. В этом примере мы имеем N столбцов с N типами данных, которые могут принимать значения NULL, что позволяет пользователю хранить любое из этих (или несколько) значений. Этот тип таблицы обычно называется разреженной таблицей. Проблема с этим примером состоит в том, что, если пользователь придумывает новое свойство, мы будем вынуждены изменить таблицу, пользовательский интерфейс и любые программы, которые связаны со столбцами таблицы. С другой стороны, если мы реализуем таблицу следующим образом: entity entityld: int Attribute!: varchar(20) Attribute2: varchar(20) AttributeN: varchar(20)
entityProperty entityPropertyld: int entityPropertyTypeld: int entityld: int propertyValue: sqLvariant •
entityPropertyType entityPropertyTypeld: int name: varchar(20) Здесь дополнительно entityPropertyType — таблица "тип свойства сущности"; entityPropertyTypeld — идентификатор типа свойства сущности; name — название типа свойства сущности.
286
Планирование и реализация основной физической структуры ... то каждое из свойств, которое мы реализовали в предыдущем примере, добавляя столбец в таблицу e n t i t y P r o p e r t y , будет теперь добавлено как экземпляр в таблице e n t i t y P r o p e r t y T y p e , и значение может быть размещено в столбцеpropertyValue. Какой бы тип данных ни понадобился для свойства, он будет размещен как s q l _ v a r i a n t . Таблица e n t i t y P r o p e r t y T y p e могла бы быть расширена для включения многих других свойств, при этом пользователю не нужно производить серьезные изменения в БД. И если мы реализуем наше решение формирования отчетов таким способом, чтобы наши новые отчеты знали относительно любых изменений, нас не будут смущать никакие новые свойства. Недостатки вариантного типа Если данные размещены в столбце типа s q l _ v a r i a n t , ими нелегко управлять. Я оставлю это читателю, который может полностью прочитать информацию в SQL Server Books Online относительно вариантных данных. Проблемы возникают в следующих случаях: •
Назначение данных из столбца типа s q l _ v a r i a n t в более строгий тип данных — мы должны быть очень осторожны, поскольку правила перевода переменной из одного типа данных в другой сложны и могут вызвать ошибки, если данные не могут быть преобразованы. Например, величина типа varchar(lO) 'He дата' не может быть преобразована к типу данных d a t e t i m e . Такие проблемы действительно станут актуальны, когда вы начинаете восстанавливать вариантные данные из типа данных s q l _ v a r i a n t и пытаться управлять ими.
•
Считается, что величины NULL типа s q l _ v a r i a n t не имеют никакого типа данных — следовательно, вы должны будете обращаться со значениями NULL в s q l _ v a r i a n t по-разному для разных типов данных.
•
Сравнения вариантных величин с другими типами данных могут вызвать ошибки программирования, потому что если только одна из величин имеет тип s q l _ v a r i a n t , компилятор будет знать, что вы запускаете оператор с двумя несравнимыми типами данных, например, @intVar = @varcharVar. Однако если эти две переменные были определены как sql__variant, и типы данных не соответствуют, то значения не будут соответствовать из-за несовместимости типов данных.
Имеется функция, которая определяет тип значения, находящегося в вариантном столбце:
I
DECLARE @varcharVariant s q l _ v a r i a n t SET @varcharVariant = '1234567890' SELECT @varcharVariant AS varcharVariant, SQL_VARIANT_PROPERTY(@varcharVariant, 'BaseType') as baseType, SQL VARIANT PROPERTY(@varcharVariant, 'MaxLength') as maxLength Здесь определяется переменная varcharVariant вариантного типа, в которую помещается строка ' 1234567890', после чего определяется содержимое переменной varcharVariant, его базовый тип (baseType) и максимальный размер (maxLength), используя функцию SQL_VARIANT_PROPERTY. Этот запрос возвращает: varcharVariant
baseType
maxLength
1234567890
varchar
10 287
Глава 10 Другие типы данных Следующие типы данных несколько менее часты, но они все же очень полезны timestamp
(или
rowversion)
Тип данных timestamp (время создания/обновления) или rowversion (версия строки) — уникальное число в рамках БД. Когда вы имеете столбец типа timestamp, значения его изменяются с каждой корректировкой каждой строки. Гарантируется, что значения в столбцах типа timestamp будут уникальными по всем таблицам. Он имеет довольно странное название, поскольку не имеет никакого отношения ко времени, это просто уникальное значение, чтобы сообщить вам, что ваша строка изменилась. Столбец таблицы типа timestamp (вы можете иметь только один столбец) обычно используется как данные для механизма "оптимистической блокировки". Мы обсудим это позже в данной главе, когда будем рассматривать служебные столбцы. Тип данных timestamp — сомнительное благо. Он размещается в восьмибайтовой величине типа v a r b i n a r y . С двоичными значениями не всегда легко иметь дело, и они зависят от того, какой механизм вы используете для доступа к вашим данным. При использовании ODBC или OLE DB и ADO они могут быть на первых порах коварными. Это связано с тем, что они являются двоичными значениями, и для восстановления их вы должны использовать механизмы формирования фрагментов BLOB-данных. SET NOCOUNT ON CREATE TABLE t e s t T i m e s t a m p ( v a l u e v a r c h a r ( 2 0 ) NOT NULL a u t o _ r v t i m e s t a m p NOT NULL ) INSERT INTO t e s t T i m e s t a m p
(value)
values
('Ввод')
SELECT v a l u e , a u t o _ r v FROM t e s t T i m e s t a m p UPDATE t e s t T i m e s t a m p SET v a l u e = ' П е р в о е и з м е н е н и е ' SELECT v a l u e , auto__rv from t e s t T i m e s t a m p UPDATE t e s t T i m e s t a m p SET v a l u e = ' П о с л е д н е е и з м е н е н и е ' SELECT v a l u e ,
a u t o _ r v FROM t e s t T i m e s t a m p
Здесь создается таблица testTimestamp (проверка timestamp), содержащая два столбца: value (значение) типа varchar (20) и auto_rv (автоматическая фиксация изменения строки) типа timestamp. Далее в столбец value помещаются поочередно значения'Ввод', 'Первое изменение' и 'Последнее изменение', а в промежутках между вводом данных проверяется содержимое столбцов value и auto rv.
288
Планирование и реализация основной физической структуры Возвращаются следующие значения: value
auto_rv
Ввод
0x0000000000000089
Value
auto_rv
Первое изменение
0х000000000000008А
Value
auto_rv
Последнее изменение
0х000000000000008В
Мы не трогали переменную auto_rv, и все же она дважды увеличилась. Однако вы не можете рассчитывать, что значение переменной типа timestamp будет последовательным, поскольку корректировки других таблиц будут изменять это значение. Лучше также не считать, что величина является числом изменений, выполненных вашим кодом. То, как конкретно реализован тип timestamp, — деталь, которая может быть изменена в будущем (из SQL Server 2000 Books Online: "Определение timestamp в будущих версиях SQL Server будет изменено, чтобы совместить с определением timestamp в SQL-99."). Если будет найден лучший метод задания уникальной величины в пределах БД, который окажется хоть на волос быстрее, он будет, вероятно, использоваться. Вы можете создавать переменные типа timestamp для получения значения timestamp, и можете восстановить последнее используемое значение timestamp через глобальную переменную @@dbts. Тема "оптимистической блокировки" будет рассмотрена более детально в главе 12. uniqueidentif ier Глобальный уникальный идентификатор (GUID — Globally Unique IDentifier) быстро становится оплотом вычислений Microsoft. Название говорит, что все они глобально уникальны. Учитывая то, как глобальные уникальные идентификаторы формируются, имеется чрезвычайно малый шанс, что когда-либо будет дублирование их значений. Они создаются с помощью формулы, которая включает использование идентификационного номера сетевой карты (если он существует), текущей даты и времени, уникального числа таймера центрального процессора и некоторого дополнительного "магического числа". В SQL Server 2000 u n i q u e i d e n t i f i e r (уникальный идентификатор) имеет очень важную цель. Когда вам нужно иметь уникальный ключ, который гарантирует, что будет уникальным во всех БД и сервере, то, например: DECLARE SguidVar uniqueidentifier SET @guidVar = new()
I
SELECT @guidVar as guidVar
Здесь задается переменная @guidVar типа uniqueidentif значение, а потом это значение проверяется.
ier, которой присваивается
289
Глава 10 ... возвращает значение: guidVar 6C7119D5-D48F-475C-8B60-50D0C41B6EBF
Они фактически хранятся как шестнадцатибайтовые двоичные значения. Обратите внимание, что это не точно непосредственно шестнадцатибайтовое двоичное значение. Вы не можете помещать просто любое двоичное значение в столбецuniqueidentif i e r , поскольку значение должно удовлетворять критериям генерации, которые не задокументированы по очевидным причинам. Если вы должны создать автогенерирующий столбец THnauniqueidentif i e r , имеется свойство, которое вы можете устанавливать в операторе создания таблицы (или изменить таблицу подобным же образом). Это — свойство rowguidcol (заполнение столбца глобальных идентификаторов для строк) и оно используется так: CREATE TABLE guidPrimaryKey ( guidPrimaryKeyld u n i q u e i d e n t i f i e r NOT NULL r o w g u i d c o l DEFAULT n e w l d ( ) , value varchar(lO) )
Здесь создается таблица guidPrimaryKey (первичный ключ в виде глобального уникального идентификатора), в которой формируются столбцы: guidPrimaryKeyld (идентификатор первичного ключа в виде глобального уникального идентификатора) типа uniqueidentif ier и value (значение) типа varchar (10). Мы ввели здесь пару новых моментов — свойство rowguidcol и значения по умолчанию, которые мы рассмотрим должным образом в следующей главе. Пока достаточно сказать, что если вы не формируете значение столбца в операции INSERT, операция по умолчанию сделает это. В нашем случае мы используем функцию newld (), чтобы получить новый u n i q u e i d e n t i f i e r . Так что, когда мы выполняем следующий оператор INSERT:
I
INSERT INTO g u i d P r i m a r y K e y ( v a l u e ) VALUES ( ' T e s t ' )
Здесь вводится только значение Test в столбец value. ... а затем выполняем следующий оператор, чтобы посмотреть введенные данные: SELECT *
I
FROM g u i d P r i m a r y K e y
... то получим guidPrimaryKeyld
value
8A57C6CD-7407-47C5-AC2F-E6A884C7B646 Test Свойство столбца rowguidcol, сформированное с пометкой u n i q u e i d e n t i f i e r , уведомляет систему, что оно аналогично значению столбца идентификаторов для таблицы —
290
Планирование и реализация основной физической структуры уникальный указатель на строку таблицы. Обратите внимание, что ни идентификатор, ни свойство rowguidcol не гарантируют уникальность. Чтобы обеспечить такую гарантию, мы должны реализовать их использование с уникальными ограничениями, которые рассмотрим позже в этой главе. Казалось бы, что u n i q u e i d e n t i f i e r будет лучшим способом реализации первичных ключей, так как, когда такие величины создаются, они уникальны по всем БД, серверам и платформам. Однако имеются две основные причины, почему мы не будем использовать столбец u n i q u e i d e n t i f i e r , чтобы реализовать все наши первичные ключи: •
Требования размещения — поскольку они размером в шестнадцать байт, то значительно больше по объему, чем обычное поле целого числа.
•
Возможность ввода — так как текстовая версия глобального уникального идентификатора содержит 36 символов, очень трудно поместить его значение в запрос, и нелегко его ввести.
cursor c u r s o r (курсор) — механизм, который позволяет выполнять действия над конкретной строкой вместо обычного способа работы с набором строк. Тип данных c u r s o r используется, чтобы хранить ссылку на курсор SQL Server в языке T-SQL. Вы не можете использовать тип данных c u r s o r как тип столбца в таблице. Их единственное использование — в коде T-SQL, чтобы хранить ссылку на курсор. Большее количество информации относительно курсоров см. в "Professional SQL Server 2000 Programming" Роба Виейра (Rob Vieira) (Wrox Press, ISBN 1-861004-48-6). table Тип данных t a b l e (таблица) несколько похож на тип данных cursor, но содержит ссылку на результирующий набор. Название типа данных на самом деле довольно неудачное, поскольку оно вынуждает функционального программиста думать, что он может хранить указатель на таблицу. На самом деле он используется, чтобы хранить набор результатов как временную таблицу. Фактически t a b l e подобен временной таблице при реализации. Следующий пример показывает синтаксис, необходимый для использования типа переменной t a b l e . DECLARE @tableVar TABLE ( id int IDENTITY, value varchar(lOO) ) INSERT INTO @tableVar (value) VALUES ('Это свежий тест') SELECT id, value FROM @tableVar
Здесь создается объект типа table с именем @tableVar (переменная-таблица) и столбцами id (идентификатор) типа int — автоинкрементный и value (значение) типа varchar (100). Затем в столбец value помещается значение 'Это свежий тест. Далее анализируется полученный в столбцах id и value результат. 291
Глава 10
Запрос возвращает следующее id
value
1
Это свежий тест
Как и с типом данных cursor, вы не можете использовать тип данных t a b l e как тип столбца в таблице, и он может только использоваться в коде T-SQL для указания на результирующий набор. Его основная цель — получить таблицу из определяемой пользователем функции, как в следующем примере: CREATE FUNCTION table$testFunction ( @returnValue varchar(100) ) RETURNS @tableVar table ( value varchar(100) ) AS BEGIN INSERT INTO @tableVar (value) VALUES (SreturnValue) RETURN END
Здесь создается функция table$testFunction (функция тестирования таблицы), которая возвращает величину типа varchar (100). Затем формируется объект QtableVar (переменная-таблица) типа table со столбцом value тоже типа varchar (100). В этот столбец помещается значение, сформированное функцией table$testFunction. После создания функции и таблицы можно использовать следующий синтаксис: I
SELECT * FROM dbo.table$testFunction('testValue')
... который вернет следующее: value testValue ТИПЫ
данных, определяемые пользователем
Мы коснулись этого ранее, когда обсуждали, как управлять типом данных d a t e t i m e . Основное назначение определяемых пользователем типов — позволить архитектору создать псевдонимы для часто используемых типов данных. В объявлении типа, определяемого пользователем, мы можем задать:
292
[_1
имя типа данных;
•
систему типа данных, включая информацию об их длине;
•
позволяет ли тип данных использовать значение NULL.
Планирование и реализация основной физической структуры Чтобы создавать определяемый пользователем тип для нашего общего серийного номера, мы могли бы сделать следующее:
I
EXEC sp_addtype @typename = @phystype = varchar(12), @NULLtype = 'not NULL', @owner = 'dbo' GO
serialNumber,
Здесь формируется новый тип данных с именем serialNumber (серийный номер), структурой, совпадающей с типом varchar (12), не допускающий использования значения NULL, владельцем которого является пользователь с именем dbo (см. выше). Когда этот тип будет применяться, мы можем изменить возможность использования значения NULL, определив это, но мы не можем изменить сам тип данных. Мы можем использовать этот тип подобно тому, как мы используем все типы данных SQL Server, как способ обеспечить детали реализации использования одного и того же типа. Мы можем объединить правила (чтобы ограничить значения, которые можно вводить при любом использовании) и значения по умолчанию (чтобы использовать значение по умолчанию, если конкретное значение не задано) для типа данных. Определяемые пользователем типы данных, поскольку они реализованы в SQL Server, имеют некоторые недостатки, которые делают их несколько неудобными для использования: •
сообщения об ошибках использования правил;
•
непоследовательное назначение переменных;
•
типы данных столбцов, определяемые пользователем, не будут изменяться, если изменится определяемый пользователем тип.
Сейчас мы рассмотрим эти проблемы. Сообщения об ошибках использования правил Можно создать определяемый пользователем тип данных, включающий правила и формирование ошибок. Создайте определяемый пользователем тип, создайте правило для него и свяжите это со столбцом. Теперь, когда создается таблица со столбцом, имеющим новый тип данных, правило будет использовано, как и ожидалось. Однако сообщения об ошибках, которые возникнут, ничего не стоят! Например, создадим очень полезный тип данных, который назовемsocialSecurityNumber, добавим правило и свяжем это со столбцом. - - создадим т и п , который н а з о в е м SSN типа v a r c h a r ( l l ) , имеющий р а з м е р - - номера с о ц и а л ь н о г о о б е с п е ч е н и я , включая символы тире EXEC s p _ a d d t y p e @typename - ' S S N ' , @phystype = ' v a r c h a r ( 1 1 ) ' , SNULLtype = ' n o t NULL' GO - - создадим п р а в и л о , к о т о р о е н а з о в е м SSNRule, со следующим шаблоном CREATE RULE SSNRule AS @value LIKE ' [0-9] [0-9] [ 0 - 9 ] - [ 0 - 9 ] [ 0 - 9 ] - [ 0 - 9 ] [0-9] [0-9] [ 0 - 9 ] ' GO
Продолжение кода на следующей странице
293
Глава 10 -- объединение правила с образцом типа EXEC sp_bindrule 'SSNRule', 'SSN' GO -- создание таблицы для проверки с новым типом данных CREATE TABLE testRule ( id int IDENTITY, socialSecurityNumber SSN ) GO
Здесь выполняются операции в соответствии с комментариями. Создаваемая таблица для проверки testRule (проверка правила) содержит два столбца: столбец id (идентификатор) автоинкрементного целого типа и socialSecurityNumber (номер социального обеспечения) типа SSN, созданного пользователем. Это создает нам таблицу со столбцом, который должен предотвратить ввод неправильного номера социального обеспечения. Так, в следующем фрагменте мы попробуем вставить данные в таблицу: -- ввод значений з таблицу INSERT INTO testRule (socialSecurityNumber) VALUES ('438-44-3343') INSERT INTO testRule (socialSecurityNumber) VALUES ('43B-43-2343') — заметьте, что В вместо 8 вызовет ошибку GO
Результат будет следующий: Server: Msg 513, Level 16, State 1, Line 1 A column insert or update conflicts with a rule imposed by a previous CREATE RULE statement. The statement was terminated. The conflict occured in database 'tempdb', table 'testRule', column 'socialSecurityNumber'. The statement has been terminated. (Сервер: сообщение 513, уровень 16, состояние 1, строка 1 Добавление или обновление столбца конфликтует с правилом, наложенным предыдущим оператором CREATE RULE. Оператор удален. Конфликт произошел в БД'tempdb', таблица 'testRule', столбец 'SocialSecurityNumber. Оператор удален.) Отсюда вы можете видеть, что сообщение об ошибке — бесполезно, поскольку оно даже не сообщает нам, какое правило было нарушено. Обратите внимание, что при нашем тестировании мы включаем опцию NOCOUNT (не вычислять), отключающую все соединения. Это позволит избежать сообщений, которые говорят нам, сколько строк изменено операцией. В данном случае это также сделано для более наглядного примера. Мы можем сделать это в коде, задав SET NOCOUNT ON (включить NOCOUNT).
294
Планирование и реализация основной физической структуры Противоречивая работа с
переменными
Сначала мы рассмотрим обработку значений NULL. Когда мы в разделе Сообщения об ошибках использования правил создавали тип в коде, то установили для типа значение NOT NULL. Теперь мы выполним следующее:
I
DECLARE @SSNVar SSN SET-@SSNVar = NULL SELECT @SSNVar
Здесь формируется переменная QSSNVar (переменная для номера социального обеспечения) типа SSN. Затем в нее помещается значение NULL и анализируется результат. Этот код показывает одну из непоследовательностей определяемых пользователем типов данных. При выполнении оператора мы могли бы ожидать, что произойдет ошибка, так как тип не допускает значение NULL. Однако мы получим следующее: SSNVar NULL Это, конечно, несущественно, но я предпочел бы, чтобы определяемый пользователем тип данных не позволял вводить значение NULL. Другая проблема, связанная с определяемыми пользователем типами данных, возникает, когда созданная переменная этого типа не удовлетворяет правилу, которое связано с этим типом. Тем не менее, возможно задать значения переменной, определенной с использованием заданного пользователем типа данных, причем эти значения не удовлетворяют добавленному правилу, но они не могут быть помещены в столбец, определенный тем же самым типом. Здесь мы покажем этот эффект: DECLARE @SSNVar SSN SET @SSNVar = ' B i l l ' SELECT @SSNVar AS SSNVar INSERT INTO t e s t R u l e ( s o c i a l S e c u r i t y N u m b e r ) VALUES (@SSNVar)
Здесь задается переменная SSNVar типа SSN, и ей присваивается значение 'ВИГ, после чего анализируется полученный результат. Далее введенный результат помещается в столбец socialSecurityNumber таблицы testRule (обозначения те же, что и в предыдущих примерах). Этот код приводит к следующей ошибке: SSNVar Bill Server: Msg 513, Level 16, State 1, Line 5 A column insert or update conflicts with a rule imposed by a previous CREATE RULE statement. The statement was terminated. The conflict occured in database'tempdb', table 'testRule1, column 'socialSecurityNumber'. The statement has been terminated.
295
Глава 10 {Сервер: сообщение 513, уровень 16, состояние 1, строка 5 Добавление или обновление столбца конфликтует с правилом, наложенным предыдущим оператором CREATE RULE. Оператор удален. Конфликт произошел в БД 'tempdb', таблица 'testRule, столбец 'SocialSecurityNumber. Оператор удален.) В следующей главе мы рассмотрим более подробно защиту наших данных от недопустимых значений, а в данный момент мы только должны понять, почему не используем средств наподобие полезного инструмента инкапсуляции. Обратите внимание, что правила не являются стандартным способом обеспечения таких защит — таким являются проверки ограничений. Однако для определяемого пользователем типа, чтобы быть полезными для нас, они должны быть объектом сами по себе. Для любого, кто читает эту книгу, и кто действительно любит использовать определяемые пользователем типы, я думаю, было бы важно указать некоторые из наиболее важных проблем, связанных с ними. Правила в SQL Server 2000 выглядят как особенность обратной совместимости, и как таковые не могут быть обойдены в следующей версии, по причине требований покупателей сохранить некоторые особенности. Мы будем использовать проверки ограничений, поскольку они связаны непосредственно со столбцом, а не типом данных. ТИПЫ,
определяемые пользователем, не могут быть изменены,
если они используются Типы данных столбцов, которые заданы с помощью определяемых пользователем типов, не будут изменяться, если определяемый пользователем тип изменяется. Это — самая неприятная проблема. Предположим, что мы должны изменить определяемый пользователем тип данных, скажем с char (1) на v a r c h a r (10) по какой-либо причине. Нет никакого средства "изменить" определяемый пользователем тип, и мы не можем сделать это, если он уже используется. Следовательно, мы должны изменить тип данных в каждом месте, где он используется, на базовый тип, изменить сам тип и повторно изменить тип. Не слишком приятно. По этим причинам мы не будем использовать в нашем учебном примере определяемые пользователем типы данных.
Необязательные данные В главе 12 мы потратим много времени, обсуждая причину проблем при использовании значений NULL, когда они используются при поиске данных. Однако мы должны посмотреть, почему нам приходится использовать столбцы, допускающие значения NULL. Одной из целей нормализации должно быть удаление стольких значений NULL, сколько возможно. Прежде, чем мы рассмотрим, как реализовать значения NULL, нам нужно кратко обсудить, что означает "NULL". В то время как можно сказать, что NULL означает необязательные данные, это — неполное определение. Значение NULL должно читаться как "неизвестное значение". В главе 12 мы представим таблицы истинности для сравнений, но пока будем говорить, что NULL означает параметр, который может быть заполнен в будущем. Для столбца создается предложение: Ш <имя_столбца> <тип_данных> <спецификация_ШЬЬ> ... где мы просто изменяем <спецификация_ыиьь> на NULL, если допускается использование значения NULL, или NOT NULL, если значения NULL не допускаются. Например, 296
Планирование и реализация основной физической структуры
CREATE TABLE NULLTest ( NULLColumn varchar(lO) NULL, notNULLColumn varchar(lO) NOT NULL )
Здесь создается таблица NULLTest (тестирование значения NULL), состоящая из двух столбцов: NULLColumn (столбец, допускающий NULL) и notNULLColumn (столбец, не допускающий значения NULL), оба типа varchar (10). Здесь нет ничего удивительного. Если мы опустим спецификацию NULL вообще, используется вариант по умолчанию для SQL Server. Чтобы определить, каково текущее свойство для БД по умолчанию, мы выполняем следующий оператор: Щ EXECUTE s p _ d b o p t i o n
@dbname = ' t e m p d b ' ,
@optname = 'ANSI NULL
default'
Здесь выполняется стандартная процедура sp_dboption (определение опции БД) для БД с именем tempdb (временная БД), проверяющая состояние опции с именем ANSI NULL default (NULL no умолчанию) Этот оператор дает OptionName (название опции) ANSI NULL default
CurrentSetting (текущая установка) off
Чтобы задать значение по умолчанию для БД, вы можете использовать процедуру sp_dboption. (Я рекомендовал бы, чтобы эта установка всегда отключалась, если вы забываете устанавливать это явно; в этом случае вы не увязнете в столбцах, допускающих значение NULL и которые быстро заполнятся такими значениями, которые вы будете должны чистить.) Чтобы установить значение по умолчанию для сеанса работы, используйте следующую команду: Щ
SET ANSI__NULL_DFLT_ON OFF — или ON, если по умолчанию используется NULL
Пример: -- отключение использование по умолчанию NULL SET ANSI_NULL_DFLT_ON OFF -- создание таблицы для проверки CREATE TABLE testNULL ( id int
-- проверка значений EXEC sp_help testNULL
Здесь создается таблица testNULL (проверка значения NULL) с одним столбцом id (идентификатор) типа int, а затем проверяется возможность использования значения NULL.
297
Глава 10 Этот запрос возвращает Column_name (имя столбца)
[...]
Nullable (возможность NULL)
id
...
по (нет)
Обратите внимание, что значительная доля выходных величин sp_help была удалена по причинам недостатка места. Функция sp_help возвращает информацию о таблице, столбцах и ограничениях. С другой стороны, если взять: -- изменение установки по умолчанию SET ANSI_NULL_DFLT_ON ON -- удаление таблицы и повторное создание DROP TABLE testNULL GO CREATE TABLE testNULL ( id int ) EXEC sp_help testNULL
... то получим
.
Column_name
[...]
Nullable
id
...
yes
Вычисляемые столбцы Теперь, когда мы представили типы данных, кратко рассмотрим вычисляемые столбцы. Они — действительно свежая особенность, которая была добавлена в SQL Server 7.0. Странно, что они не были реализованы в программе Enterprise Manager, которая просто прекращает работать, когда вы делаете любые изменения в таблице, так что вы были должны использовать ее с оригиналом таблицы. В SQL Server 2000 эта особенность должным образом поддержана. Щ <имя_столбца> AS <вычисленное_определение> Вычисляемые столбцы могут использоваться, чтобы взять нечитабельные данные и перевести их в удобочитаемый формат в определении таблицы. В SQL Server 2000 эти столбцы теперь могут быть индексированы, так что они даже более полезны. Пример, удобный для СУБД, который я создал, предназначен для организации группировки по дням, месяцам и годам. В следующем коде мы имеем пример, который довольно близок к этому. Он группирует по секундам, чтобы сделать пример более легким для проверки: CREATE TABLE calcColumns ( dateColumn datetime, dateSecond AS datepart(second, dateColumn),
I
298
-- вычисляемый столбец
Планирование и реализация основной физической структуры DECLARE @i int SET @i = 1 WHILE (@i < 1000) BEGIN INSERT INTO calcColumns (dateColumn) VALUES (getdateO) SET @i = @i + 1 END SELECT d a t e S e c o n d , m a x ( d a t e C o l u m n ) AS d a t e C o l u m n , c o u n t ( * ) AS c o u n t S t a r FROM c a l c C o l u m n s GROUP BY d a t e S e c o n d ORDER BY d a t e S e c o n d
Здесь создается таблица calcColumns (подсчет столбцов), содержащая два столбца: dateColumn (столбец дат) типа datetime и dateSecond (секунды даты) — вычисляемый столбец, в который помещаются секунды из даты первого столбца. Организуется цикл размещения 1000 текущих дат (которые в данном случае отличаются только секундами и долями секунды). Далее данные группируются по значениям вычисляемого столбца и упорядочиваются по этим же значениям вычисляемого столбца. После этой группировки для каждой группы формируется величина секунд (dateSecond), максимальное значение даты (dateColumn) и число элементов в группе (countStar). Когда этот код выполняется, он возвращает следующее: DateSecond
dateColumn
countStar
20 21 22
2000-11-14 11:07:20.993 2000-11-14 11:07:21.993 2000-11-14 11:07:22.993
8 900 91
Довольно опасная особенность с вычисляемыми столбцами заключается в том, что они будут игнорироваться, когда вы вставляете данные, если при этом опускаете список вставляемых полей. SQL Server будет игнорировать любые вычисляемые столбцы и относиться к этим полям, как будто они не существуют. Например, мы создаем следующую таблицу: CREATE TABLE testCalc ( value varchar(10), valueCalc AS UPPER(value), value2 varchar(lO)
Здесь создается таблица testCalc (проверка вычислений) с тремя столбцами: value (значение) типа varchar (10), valueCalc (вычисляемое значение) — вычисляемое поле, в которое помещается значение первого поля, написанное прописными буквами, и третье поле value2 (значение 2) типа varchar (10). Далее мы создадим некоторые новые значения без использования списка ввода:
I
INSERT INTO testCalc -- мы должны здесь иметь (value, value2) VALUES ('test', 'test2')
299
Глава 10
При этом никаких ошибок не произойдет. Если мы выполним запрос: SELECT * FROM testCalc
I
... он возвратит следующие результаты: value
valueCalc
value2
test
TEST
test2
Независимо от вычисляемых полей плохо кодировать оператор INSERT без списка ввода.
Вычисляемые столбцы — чрезвычайно ценная особенность, и должны волновать любого, кто когда-либо должен был неоднократно печатать и перепечатывать вычисления столбцов.
Служебные столбцы Не каждый столбец будет или должен быть обозначен на логической модели данных. В этом разделе мы обсудим некоторые из причин, почему мы имеем столбцы, которые не соответствуют образцу столбца логической модели, но которые требуются в физической реализации. Основная особенность использования служебного столбца в том, что он не является атрибутом сущности, которую моделирует таблица. Другими словами, столбец требуется, прежде всего, для целей реализации. Имеется несколько ситуаций, когда такие столбцы необходимы.
Управление параллелизмом Когда имется БД с больше чем одним пользователем, мы должны реализовать некоторую форму механизма управления параллелизмом. Мы должны быть уверены, что как только пользователь получает запись, которую он хочет изменить, никакой другой пользователь не может сделать изменения в записи, иначе мы можем потерять информацию. Имеются два различных метода для выполнения этой задачи. •
300
Пессимистическая блокировка — она реализуется с помощью блокировки любых записей, которые пользователь читает с намерением модернизировать. Такая блокировка называется пессимистической, потому что предполагает, что другие пользователи будут, вероятно, пробовать редактировать ту же самую запись, что и текущий пользователь. Это — очень строгое условие обслуживания и обычно не нужно, так как в большинстве случаев никакие два пользователя, вероятно, не будут смотреть ту же самую запись в то же самое время. Мы бы использовали пессимистическую блокировку только тогда, когда полностью необходимо заставить других клиентов ждать, пока мы не закончим работать с записью перед тем, как разрешим им получить доступ к ней. Проблема состоит в том, что мы должны заблокировать запись таким образом, чтобы никто другой не мог просматривать ее, или любой другой пользователь мог обратиться к нашим изменениям только после того, как мы снимем нашу блокировку.
Планирование и реализация основной физической структуры •
Оптимистическая блокировка — это гораздо более популярное решение. Чтобы реализовать механизм оптимистической блокировки, мы просто добавляем столбец к каждой таблице, который изменяется каждый раз, когда изменяется строка в БД. Например, пользователь № 1 извлекает строку значений. Вскоре пользователь № 2 извлекает ту же самую строку. Пользователь № 1 обновляет запись, и значение столбца блокировки изменяется. Теперь, если пользователь № 2'попытается изменить строку, SQL Server проверяет значение столбца блокировки, видит, что оно изменилось, и предотвращает обновление пользователем № 2: Пользователь № 1 Модифицирует запись
Извлекает запись время---•>
I
Извлекает запись
1
Пытается модифицировать запись, но ему запрещено
Пользователь № 2
Реализация оптимистической блокировки — довольно простая задача, но заметьте, однако, что каждый процесс, который использует таблицы, должен твердо придерживаться правил оптимистической блокировки. Если один из процессов игнорирует это, весь процесс терпит неудачу. Имеется несколько способов осуществить оптимистическую блокировку: •
Использование столбца типа timestamp — как было ранее рассмотрено, столбец timestamp является специальным столбцом, который изменяется каждый раз, когда изменяется строка. Столбцы timestamp — идеальный способ для создания оптимистической блокировки.
•
Использование даты последнего обновления и/или последнего корректирующего пользователя — в основном это тот же самый метод, за исключением того, что мы добавляем столбец, чтобы хранить пользователя, который произвел последнюю модифицированную запись, и время корректировки. Мы делаем это с помощью триггера, работающего с операторами INSERT и UPDATE. Этот метод лучше в том смысле, что дает удобочитаемую для человека величину, но хуже, потому что требует дополнительное кодирование триггеров.
•
Использование всех столбцов в выражении WHERE во время модификаций — это метод, обеспечиваемый программными средствами, наподобие Microsoft Access, где нет столбцов типа timestamp. Обычно, когда мы выполняем оператор обновления, то включаем каждый столбец в таблицу в выражении WHERE. Если значения изменились с того момента, когда мы извлекли их, то это означает, что кто-то еще, вероятно, обратился к нашим данным при нас. Следовательно, оптимистическая блокировка зафиксирует отказ в работе.
301
Глава 10 Мы будем использовать столбец timestamp, так как его весьма легко реализовать, и он не требует абсолютно никакого кода. Итак, мы создадим следующую таблицу: CREATE TABLE t e s t O p t i m i s t i c L o c k ( id
i n t NOT NULL,
value varchar(30) NOT NULL autoTimestarnp timestamp NOT NULL, primary key (id)
-- оптимистическая блокировка — задает первичный ключ в столбце id
Здесь создается таблица testOptimisticLock (проверка оптимистической блокировки) с тремя столбцами: id (идентификатор) — типа int, он же является и первичным ключом, value (значение) типа varchar (30) и аиtoTimestamp (автоматическая отметка времени) типа timestamp, который является оптимистическим ключом. Значения всех столбцов не могут быть NULL. Далее мы выполним следующий фрагмент. Первым шагом является создание новой строки в столбце и получение значения оптимистической блокировки для строки. INSERT INTO t e s t O p t i m i s t i c L o c k VALUES ( 1 , 'Тест 1')
(id,
value)
DECLARE @timestampHold timestamp SELECT @timestampHold • autoTimestamp FROM t e s t O p t i m i s t i c L o c k WHERE v a l u e
-
'Тест
1'
- - сначала будем работать/ UPDATE t e s t O p t i m i s t i c L o c k SET v a l u e ш 'Новое значение' WHERE i d = 1 AND autoTimestamp = @timestampHold IF @@rowcount - 0 BEGIN r a i s e r r o r 50000 'Строка изменена другим пользователем' END SELECT i d ,
value
FROM t e s t O p t i m i s t i c L o c k
- - затем установим отказ в работе UPDATE t e s t O p t i m i s t i c L o c k SET v a l u e - 'Второе новое значение' WHERE i d = 1 AND autoTimestamp = @timestampHold IF @@rowcount = 0 BEGIN r a i s e r r o r 50000 'Другой пользователь изменил строку' END SELECT i d , v a l u e FROM t e s t O p t i m i s t i c L o c k
Здесь в таблице testOptimisticLock создается строка с параметрами id** I, value = = "Тест 1". Далее объявляется переменная @timestampHold (фиксация значения типа timestamp) и в нее помещается значение поля autoTimestamp, для введенной строки таблицы testOptimisticLock (она содержит в поле value значение "Тест 1"). Далее выполняется 302
Планирование и реализация основной физической структуры часть запроса, которая имитирует нормальную работу. Здесь в строку (единственную) таблицы testOptimistocLock, у которой id = 1, в поле value помещается значение "Новое значение", если только поле autoTimestamp не изменило свое значение (а оно в данном случае не изменило своего значения, потому что строка не изменялась). Если бы изменение поля value не произошло (а в данном случае оно произошло), значение глобального параметра @@rowcount, определяющего число измененных строк, стало бы равным нулю (что возможно, если параметр autoTimestamp изменит свое значение, означающее, что строку изменил другой пользователь в течение рассматриваемого промежутка времени), будет выдано сообщение "Строка изменена другим пользователем". Этот фрагмент завершается выводом значений полей id и value для их контроля. Наконец, выполняется часть запроса, которая имитирует отказ в обновлении строки. Эта часть аналогична только что рассмотренному фрагменту, но в этом случае в поле value помещается значение "Второе новое значение", а в случае неудачи с изменением строки выводится сообщение "Другой пользователь изменил строку". В этом фрагменте реализуется именно этот случай, так как строку не удается скорректировать, потому что до этого в предыдущем фрагменте было изменено значение поля autoTimestamp ввиду успешного обновления строки. И этот фрагмент завершается выводом значений полей id и value. Этот запрос возвратит id
value
1
Новое значение
Server: Msg 50000, Level 16, State 1, Line 38 Другой пользователь изменил строку id
value
1
Новое значение
Устройства блокировки записей Созданный мной служебный столбец, который важен во многих приложениях, является устройством блокировки записей. Например, возьмем таблицу домена, который определяет тип некоторой записи, скажем, contactType (тип партнера), как на следующем рисунке: •
c
o
c
n
t
o
n
a
n
a
c
t
t
a
m
T
c
y
t
p
T
y
e
p
c
e
l
d
e
o
n
c
o
n
c
o
n
t
t
t
a
a
a
c
c
c
t
t
l
t
d
T
y
p
e
l
d
(
F
K
)
•
d
i
s
a
b
l
e
F
I
Здесь contactType — таблица "тип партнера"; contactTypeld — идентификатор типа партнера; name — название типа; disableFI — флаг запрета; contact — таблица "партнер"; contactld — идентификатор партнера. В таблице contactType мы имеем столбец типа b i t , названный d i s a b l e F I . Если этот столбец имеет значение 1, то мы больше не сможем использовать это значение как тип партнера. Нам не нужно удалять или изменять любые существующие значения партнера, чтобы установить этот флаг. (Обратите внимание, что это решение будет требовать, чтобы триггер обеспечил данное правило для проверки значения contactType . d i s a b l e F I , так как это — правило, установленное между таблицами.) 11-1868
3 0 3
Глава 10 Почему нам может потребоваться это сделать? Имеются три очевидных проблемы, которые мы должны решить: •
Если бы мы имели только 10 партнеров, не было бы проблемы физически удалить любые строки в таблице c o n t a c t , где используется конкретный экземпляр contactType. Однако если мы имеем 1 000 000 партнеров, все партнеры должны быть проверены. Это могло бы быть чересчур много.
Q
Так как пользователь может и не предвидеть любое дальнейшее использование конкретного значения, нет никакой причины полностью удалять запись из БД.
•
Могут существовать записи-потомки, которые мы не хотим удалять из БД.
Обратите внимание, что когда список значений contactType представляется пользователю, они должны быть отфильтрованы следующим образом: SELECT name FROM contactType WHERE disableFl = 0
I
Здесь из таблицы contactType выбираются записи (а в этих записях поля с именем name), в которых флаг disableFl равен нулю. Это подразумевает некоторый компромисс с точки зрения использования запроса, так как в таких случаях нам нужно балансировать затраты на удаление со стоимостью запроса. Мы могли бы также реализовать флаги удаления и запрещения записи, не допуская параллелизм для нашей БД и удаляя строки позже ночью, когда все спокойно. Следует упомянуть, что d i s a b l e F l — один из тех атрибутов, который требует удовлетворения 4НФ, поскольку мы будем, вероятно, хотеть знать не только о самом факте запрета, но и, например, когда появился этот запрет. Было бы можно, конечно, добавить таблицу c o n t a c t T y p e S t a t u s (состояние типа партнера), где мы контролируем предыдущее и текущее состояние. На практике это редко необходимо, но должно, конечно, быть рассмотрено в каждом случае отдельно, когда предварительно вы спрашиваете, когда и почему атрибут contactType, имеющий значение "Активный", был запрещен, но не можете получить простой ответ и находитесь в затруднении.
Сопоставление (порядок сортировки) Последовательность сопоставления для SQL Server определяет, как символьные данные будут организованы при хранении, как они будут при необходимости сортироваться, и как будут сравниваться. SQL Server и Windows обеспечивают огромное число типов сопоставления, из которых можно выбирать. Чтобы увидеть текущий тип сопоставления для сервера и БД, вы можете выполнить следующие команды: I
SELECT serverproperty ('collation') SELECT databasepropertyex('master',
'collation'
В большинстве систем, установленных для англоязычных стран, тип сопоставления по умолчанию — SQL_LATIN1_GENERAL_CP1_CI_AS, где Latinl_General представляет нормальный латинский алфавит, СР1 — обращение к кодовой странице 1252 (установленный по умолчанию набор символов Latin I ANSI для SQL Server), и последние части представляют нечувствительность к регистру и чувствительность к ударению соответственно. Полный перечень всех типов сопоставления можно найти в документации по SQL Server 2000. 304
Планирование и реализация основной физической структуры Для России используется значение Cyrillic_General для первого параметра. Прим. перев. Чтобы просмотреть список всех параметров сортировки, установленных на данном SQL Server, вы можете выполнить следующий оператор: I
SELECT * FROM ::fn_helpcollations()
На компьютере, который я использую для испытаний, этот запрос возвращает около 700 строк, но обычно нам не нужно будет изменять значения по умолчанию, которые администратор БД первоначально выбрал. Имеется несколько важных причин задавать различные сопоставления. •
Чувствительность к регистру — в зависимости от приложения может иметься потребность обращаться к коду и данным как чувствительным к регистру. Это может потребоваться от случая к случаю. Обратите внимание, что это делает ваш код и имена объектов также чувствительными к регистру. Помните, что чувствительность к регистру вызывает трудности при поиске. Например, при поиске всех имен, начинающихся с буквы "А", мы должны искать все элементы, начинающиеся с "А" или "а". Мы могли использовать функции преобразования к верхнему регистру для столбцов или переменных, но это нарушает индексацию и обычно — не лучший вариант.
•
Сортировка иностранных символов — другая причина — это иностранные символы. Используя Unicode, вы можете использовать любой символ любого языка в мире. Однако очень немного языков используют тот же самый A-Z набор символов, который используют англоязычные страны. Фактически, почти все другие европейские языки используют акцентированные символы, которые не являются частью набора 7-битовых ASCII-символов. Возможно использование различных типов сравнения при реализации столбцов на различных языках. Мы рассмотрим пример этого позже в данном разделе.
В предыдущих версиях SQL Server весь сервер имел единственный порядок сортировки. Это подразумевало, что в каждой БД, в каждой таблице, в каждом поле, в каждом запросе вы были вынуждены использовать один и тот же порядок сортировки. Это приводит к интересным трудностям, например, при попытке реализовать сортировку, чувствительную к регистру. Однако в SQL Server 2000 мы можем задавать сопоставление как для нашего сервера и БД, что упомянуто ранее, так и для столбца, и даже для оператора ORDER или SELECT. Чтобы установить последовательность сопоставления для столбцов типа char, varchar, t e x t , nchar, nvarchar или n t e x t при создании таблицы, вы задаете ее, используя оператор COLLATE в определении столбца, подобно следующему: CREATE TABLE o t h e r C o l l a t e ( i d i n t e g e r IDENTITY, name n v a r c h a r ( 3 0 ) NOT NULL, frenchName n v a r c h a r ( 3 0 ) COLLATE French_CI_AS_WS NULL, spanishName n v a r c h a r ( 3 0 ) COLLATE Modern_Spanish_CI_AS_WS NULL )
Здесь создается таблица otherCollate (другие сравнения), содержащая четыре столбца: автоинкрементный столбец id (идентификатор) типа integer; name (имя) типа nvarchar (30), который не может использовать значение NULL; frenchName (французское 305
Глава 10 имя) типа nvarchar (30), использующий французское сравнение, допускающий значение NULL, и spanishName (испанское имя) типа nvarchar (30), использующий модернизированное испанское сопоставление, допускающий значение NULL. Теперь, когда мы сортируем столбец f renchName, то сортировка будет нечувствительной к регистру, но организует строки в соответствии с французским набором символов. То же самое используется с испанским набором относительно столбца spanishName. В следующем примере мы рассмотрим другое свежее использование ключевого слова COLLATE. Можно использовать его, чтобы воздействовать на сравнения, выполняемые в выражении. Мы делаем это, изменяя одно или оба значения с обеих сторон выражения в двоичном сопоставлении. Создадим таблицу, называемую c o l l a t e T e s t (тестирование упорядочивания): CREATE TABLE collateTest ( name varchar(20) COLLATE SQL_Latinl_General_CPl_CI_AS ) INSERT INTO collateTest(name) VALUES ('BOB') INSERT INTO collateTest(name) VALUES ('bob')
NOT NULL
Здесь создается таблица collateTest, содержащая один столбец с именем name (название) типа varchar (20), в которую помещаются две записи 'ВОВ1 и 'bob'. Обратите внимание, что для целей демонстрации оператор COLLATE, который я включил, используется по умолчанию для моего сервера. Это, вероятно, будет и вашим сопоставлением, если вы используете сопоставление по умолчанию (мы примем это сопоставление для остальной части книги). Тогда можно выполнить следующий оператор над БД:
I
SELECT name FROM collateTest WHERE name = 'BOB'
... который возвратит обе строки: name bob BOB Однако если мы изменим сопоставление для символов "ВОВ" и выполним его:
I
SELECT name FROM c o l l a t e T e s t WHERE name = 'BOB' COLLATE L a t i n l General BIN
то оператор вернет только одну строку, которая точно соответствует символам 'ВОВ' name ВОВ
306
Планирование и реализация основной физической структуры Вы должны были заметить, что мы приводим только скалярное значение Ъов" для бинарного сопоставления. Определение сопоставления в первой части сравнения может быть хитрым вопросом, но мы не будем это рассматривать здесь. Вообще, лучше добавить функцию COLLATE к обеим сторонам выражения при выполнении такого действия. В нашем случае вместо применения сопоставления только к скалярной величине мы написали бы это следующим образом:
I
SELECT name FROM c o l l a t e T e s t WHERE name COLLATE L a t i n l G e n e r a l BIN = 'BOB'
COLLATE L a t i n l
G e n e r a l BIN
He будем глубже вникать в предмет сопоставления. В документации по SQL Server имеется большое количество информации относительно сопоставления, включая правила для сопоставления предшествующего элемента.
Ключи Как было рассмотрено в главах, посвященных логическому моделированию, определение ключей — одна из наиболее важных задач в проектировании БД. В этом разделе мы не будем рассматривать, почему мы определили выбранные ключи, а скорее, как мы реализуем их. Имеется несколько различных типов ключей, которые мы уже рассмотрели: Q
Первичный — содержит первичный указатель на строку в таблице.
•
Дополнительный — содержит дополнительные указатели на строку в таблице, обычно любые уникальные условия, которые, как мы можем удостовериться, представляют один или большее количество столбцов таблицы.
•
Внешний — внешние ключи — указатели на первичные ключи в других таблицах.
Первичные и дополнительные ключи — гибридные объекты — частично ограничения, частично индексы. Ограничения объявляют, что для объекта должен быть истинным некоторый фактор. Для ключей это означает, что значения в таблице должны быть уникальны. Объявления ограничений ключей реализуются так же, как индексы. Если вы не понимаете, как SQL Server хранит данные и реализует индексы, пожалуйста, прочитайте следующий раздел. Это жизненно важно для вашего понимания физического моделирования и вашего использования индексов и ключей, не говоря уже о понимании того, почему мы нормализуем наши БД. Снова много информации содержится в "Professional SQL Server 2000 Programming" Роба Виейра (Rob Vieira) (Wrox Press, ISBN 1-861004-48-6).
Индексы Любое обсуждение того, как строить ограничения, следует начать с обсуждения основ индексов и структур таблиц. Индексы — некоторые из наиболее важных объектов, входящих в физическое моделирование. Мы используем их, чтобы реализовать первичные ключи и вторичные ключи (уникальные ограничения), и можем использовать их, чтобы улучшить характеристики БД. Построение индексов будет рассмотрено в главе 14. Понимание, как работают индексы, даст нам основу, чтобы определить, какие виды индексов использовать. Мы дадим только краткую трактовку того, как SQL Server реализует индексы, поскольку эта сложная тема вне возможностей данной книги. 307
Глава 10 Основные структуры индексов Индексы ускоряют поиск строк в таблице, и обычно, хотя и не всегда, задают структуры, связанные с таблицей. Они строятся, используя один или большее количество столбцов таблицы и создавая специальную структуру для таблицы на основе этих выбранных ключевых столбцов. Имеются два различных класса индексов: •
Кластерные — упорядочивают физическую таблицу в соответствии с порядком индекса. Некластерные — полностью отдельные структуры, которые просто ускоряют доступ.
•
Мы можем иметь уникальный тип индексов любого класса. Уникальные индексы используются не только, чтобы ускорить доступ, но и обеспечить уникальность элементов столбца или группы столбцов. Причина того, что они используются для этой цели, заключается в том, что для определения уникальности значения мы должны просмотреть его в таблице, и так как индекс используется для ускорения доступа к данным, он отлично подойдет. Имеются другие назначения, но они — вне возможностей этой книги (просмотрите тему Create Index (создание индекса) в SQL Server 2000 Books Online, где создание индексов рассмотрено в деталях). Я здесь представлю только основные параметры, которые подходят для проектирования физических структур. Дополнительные настройки, связанные с функционированием, как и некоторыми другими особенностями, хотя и являются удобными, но вне наших возможностей. Индексы реализуются, используя структуру сбалансированного дерева, также называемую "В-дерево" (В-Tree). На следующей диаграмме мы даем основную структуру типа В-дерева, которую SQL Server использует для индексов.
__—-—"
.
Р
«1s
i»
М
11
—
_ — - — -
+ — — —
А
(
С
1
G
ii
J
II
К
г
р
«
S
i
т
«
^
Узлы-листья
Каждый из внешних прямоугольников — страница индекса объемом в 8 К, которую SQL Server использует для любого хранилища данных. Первая страница показывается наверху диаграммы: A i1
J I
р
1 Г
3 0 8
1
Планирование и реализация основной физической структуры Мы имеем три значения, которые являются ключами индекса. Чтобы решать, по какому пути следовать к более низкому уровню индекса, мы должны решить, между какими ключами находится значение: от А до I, от J до Р или больше, чем Р. Если значение — между А и I, мы идем по первому маршруту до следующего уровня:
Мы продолжаем эту процедуру до узлов-листьев индекса. Узлы-листья будут рассмотрены, когда мы перейдем к обсуждению деталей кластерных и некластерных индексов. Эта структура характерна для реализации всех индексов SQL Server. Однако следует отметить одну важную деталь. Мы сказали, что каждая из этих страниц — 8 K B размере. В зависимости от размера вашего ключа (определяемого суммой длин данных столбцов в ключе, максимально до 900 байтов), вы можете иметь где-то от восьми входов до более чем одной тысячи на одной странице. Следовательно, эти деревья вообще не растут в глубину, если разработаны должным образом. В нашем примере мы использовали упрощенный набор значений индекса, чтобы показать, как дерево индексов разворачивается. В следующем примере построим основную таблицу и основные индексы. Мы будем использовать эту таблицу в оставшейся части наших примеров данного раздела. CREATE TABLE testIndex ( id int IDENTITY, firstName varchar(20), middleName v a r c h a r ( 2 0 ) , lastName varchar(30) ) CREATE INDEX Xtestlndex ON t e s t l n d e x ( i d ) CREATE INDEX Xtestlndexl ON testlndex(firstName) CREATE INDEX Xtestlndex2 ON testlndex(middleName) CREATE INDEX Xtestlndex3 ON testlndex(lastName) CREATE INDEX Xtestlndex4 ON t e s t l n d e x ( f i r s t N a m e , middleName, lastName) Здесь создается таблица testlndex (тестирование индекса), содержащая четыре столбца: автоинкрементный столбец id (идентификатор) типа int, firstName (имя) типа varchar (20), middleName (отчество) типа varchar (20) и lastName (фамилия) типа varchar (30). Далее создаются 5 индексов с именами Xtestlndex, Xtestlndexl * Xtestlndex4. Первый содержит столбец id, второй — столбец firstName, третий — столбец middleName, четвертый — столбец lastName и пятый — столбцы firstName, middleName и lastName. Если все нормально, оператор будет успешно выполнен. Чтобы удалить индекс, вы используете следующий оператор: •
DROP INDEX testlndex.Xtestlndex
309
Глава 10 Кластерные индексы У злы-листья кластерного индекса фактически являются страницами данных. Следовательно, может быть только один кластерный индекс для таблицы. Точная реализация страниц данных — не особенно важная деталь; скорее, достаточно понять, что данные в таблице физически упорядочены в порядке индекса. При выборе, какие поля использовать как базу для кластерного индекса, лучше принять удобочитаемый для человека индекс. Другими словами, если ваша таблица имеет характерное упорядочивание, которое будет иметь смысл с точки зрения человека, это — хороший кандидат для кластерного индекса. Возьмем, например, словарь. Каждое слово в словаре можно рассматривать как строку в БД. Тогда эта БД кластеризована словами. Наиболее важная причина этого — то, что мы часто должны просматривать группу слов и их значений, чтобы видеть, нашли ли мы то, что нужно. Так как все данные сортируются в соответствии со словами, мы можем просматривать слова на страницах без каких-либо затруднений. Мы используем почти такую же логику, чтобы решить, нужно ли использовать кластерный индекс. Кластерные индексы должны использоваться для: •
• •
ключевых наборов, которые содержат ограниченное число различных значений — так как если данные сортируются ключами, все просматриваемые величины при поиске по значениям индекса мы видим немедленно; запросов диапазонов — получение всех данных в последовательности обычно имеет смысл, когда вы должны получить диапазон, скажем, от "А" до " F " ; данных, к которым обращаются последовательно, — очевидно, что если вам нужен доступ к данным в этом порядке, то имея уже отсортированные данные, выполнение будет существенно улучшено;
•
запросов, которые возвращают большие наборы результатов, — этот момент будет иметь больше смысла в сравнении с некластерным индексом, но расположение данных непосредственно на узлах-листьях структуры уменьшает непроизводительные издержки;
•
наборов ключей, к которым часто обращаются с помощью запросов, использующих операторы JOIN или GROUP BY, — часто они могут быть столбцами внешнего ключа, но не обязательно. Операторы GROUP BY всегда требуют, чтобы данные были отсортированы, отсюда следует, что наличие отсортированных данных так или иначе улучшит работу.
Так как кластерный индекс физически упорядочивает таблицу, добавление новых значений будет обычно вынуждать вас помещать новые данные в середину таблицы — единственное исключение из этого, если вы используете кластерный индекс с автоинкрементным типом полей. Если новая строка не будет умещаться на странице, SQL Server должен разбить страницу (что происходит, когда страница полная, так что половина данных отделяется на новую страницу — следовательно, происходит и добавление имени). Разбиения страницы — довольно дорогостоящие действия и вредят работе, так как данные не будут расположены на последовательных страницах, не давая возможность синхронного чтения дисковой подсистемой. Если вы не имеете набора ключей, которые удовлетворяют одному из критериев кластерного индекса, обычно лучше всего не строить его. Также важно не использовать кластерный индекс на столбцах, которые часто изменяются. Так как таблица упорядочена по значениям в столбце, SQL Server, вероятно, будет вынужден перестраивать строки на своих страницах, что является дорогостоящим действием. Это особенно важно, если вы должны модифицировать значения ключа, которые в свою очередь изменяют большое количество различных строк. 310
Планирование и реализация основной физической структуры Другой важный момент, на который следует обратить внимание, — то, что вы должны иметь кластерных ключей как можно меньше, так как они будут использоваться в каждом некластерном индексе (как мы увидим дальше). Так что, если вы изменяете кластерный ключ, вы добавляете работу и для всех некластерных индексов также. Для кластерной таблицы локатор строк (см. ниже) — всегда кластерный индексный ключ. Если кластерный ключ не уникален, SQL Server добавляет случайное значение, чтобы сделать его уникальным. Некластерные индексы Некластерный индекс аналогичен индексу в тексте книги, когда книга индексируется номером страницы. Как и в индексе книги, некластерный индекс полностью отделен от строк данных. Узел-лист структуры некластерного индекса содержит только указатель на строку данных, содержащую основное значение. Указатель из некластерного индекса на строку данных называется локатором строк. Структура локатора строк зависит от того, имеет ли таблица кластерный индекс или нет. Верхние уровни индекса — те же самые, что и у кластерного индекса. Мы рассмотрим диаграммы, показывающие различные локаторы строк, несколько позже после обсуждения основ некластерных индексов. Мы обсудили таблицы с кластерными индексами — они называются кластерными таблицами. Если нет кластерного индекса, таблица называется "кучей". Мое собственное определение "кучи" — "группа вещей, помещенных или положенных друг на друга". Это хороший способ объяснить, что происходит в таблице, если нет кластерного индекса. SQL Server просто помещает каждую новую строку в конец таблицы. Обратите также внимание, что, если вы производите удаление, SQL Server просто выбросит строку, не используя повторно место. Однако если вы проводите обновление, имеются две возможности: • •
обновление на месте — значения в строке просто изменяются; удаление и вставка — вся строка удаляется из таблицы и затем повторно вставляется.
Имеется несколько критериев, которые SQL Server использует, чтобы определить, действительно ли следует выполнить обновление строки. Два наиболее частых включают: •
Наличие у таблицы триггера AFTER, выполняемого после обновления, — когда вы имеете триггер для таблицы, который работает после того, как строка обновлена; строка сначала удаляется, а затем вставляется.
•
Попытка вставить большее количество данных, чем может разместиться на странице, — если вы имеете данные переменной длины в вашей таблице, и пытаетесь увеличить размер размещенных данных, SQL Server должен будет удалить строку и повторно вставить ее где-нибудь еще, возможно, на другой странице. Эта процедура отличается от случая, когда имеется кластерный индекс, потому что теперь нет никакой причины разбивать страницу в середине структуры, — быстрее и легче переместить данные к концу "кучи".
Теперь нам нужно рассмотреть два различных типа таблиц и как реализуются их некластерные локаторы строк.
311
Глава 10 "Куча" Для таблицы типа "кучи" без кластерного индекса локатор строк является указателем на страницу, которая содержит строку. Предположим, что мы имеем таблицу по имени itemTable (таблица элементов), с кластерным индексом, состоящим из столбца d e s c r i p t i o n (описание), и некластерным индексом, состоящим из столбца itemld. В следующей диаграмме мы видим, что для того чтобы найти itemld 3 в таблице, нужно взять левую ветвь дерева, потому что 3 находится между 1 и 20, затем следовать вниз по пути между 1 и 10. Как только мы добираемся до страницы, то имеем указатель на страницу, которая содержит нужное значение. Предположим, что этот указатель состоит из номера страницы и смещения на странице (страницы нумеруются с нуля, а смещения нумеруются с 1). Наиболее важный момент относительно этого указателя заключается в том, что он приводит непосредственно к строке на странице, которая содержит значения, которые мы ищем. Это не так, как в случае, когда мы работаем с кластерной таблицей, и важно понять различие. 1 20 | 20 23 28 39
Индекс в столбце Itemld таблицы ItemTable Номер строки, itemld смещение
Таблица ItemTable Страница 0
1
1.2
itemld
description
2
0.1
2
Chicken Soup
3
0.2
-• 3
7
1.1
8
8
0.3
Broccoli Lentils
Страница 1 itemld
description
7
Milk
1
Popcorn
Некластерные индексы в кластерной таблице В этой ситуации, когда вы прошли некластерный индекс и достигли его узла-листа, вместо получения указателя, который приводит вас непосредственно к данным, вы получаете кластерный индекс, который вы должны пройти, чтобы получить данные. На следующем графике нижний левый прямоугольник — некластерный индекс, как в нашем предыдущем примере, за исключением того, что указатель изменился от определенного местоположения к значению кластерного ключа. Далее мы используем кластерный ключ, чтобы пройти кластерный индекс и добраться до данных. 312
Планирование и реализация основной физической структуры
Кластерный индекс в столбце description Таблица Item Другие страницы Страница О itemld
description Milk
Узлы-листья некластерного индекса в столбце Itemld item Id
Mustard Popcorn
Кластерный ключ
1
Popcorn
2
Chicken Soup
3
Rutabagas
7
Milk
8
Lentils
9
Playas
''Страница 1 itemld
description Red Meat Rutabagas Rye Bread
Другие страницы
Издержки этой операции минимальны, и она "теоретически" лучше, чем использование прямых указателей на таблицу, потому что требуется лишь минимальная реорганизация для любой корректировки кластерного индекса или кластерного ключа. Когда корректировка выполнена, указатели не перемещаются. Вместо этого, в таблице оставляется указатель, который указывает на новую страницу, где теперь расположены данные. Все существующие индексы, которые имеют старые указатели, просто указывают на старые страницы, затем следует новый указатель на страницу с новым размещением данных. Уникальные индексы Уникальный индекс — второй наиболее важный момент, который вы определяете при реализации кластерного индекса. Вы можете создавать уникальные индексы как для кластерных, так и для некластерных индексов. Это гарантирует, что среди индексных ключей не будет каких-либо двойных значений. Например, если мы создали следующие индексы в нашей таблице t e s t l n d e x : I
CREATE UNIQUE INDEX Xtestlndexl ON testlndex(lastName) CREATE UNIQUE INDEX Xtestlndex2 ON testlndex(firstName, middleName, lastName)
(обозначения см. в предыдущих примерах) ... мы не могли бы ни вставить двойные значения в столбец lastName, ни создать вхождения, которые имеют то же самое сочетание значений firstName, middleName и lastName. Мы обычно не используем уникальные индексы, чтобы обеспечить уникальность вторичных ключей. SQL Server имеет механизм, известный как ограничение UNIQUE, а также ограничение PRIMARY KEY, которые мы будем использовать. Используйте уникальные индексы, когда вам нужно создать индекс для улучшения работы. 313
Глава 10 Следует также отметить, что для функционирования ваших систем очень важно, чтобы вы использовали уникальные индексы всюду, где только возможно, поскольку это увеличивает возможности оптимизатора SQL Server (SQL Server optimizer) предсказывать, сколько строк будет возвращено запросом, который использует индекс. В нашем примере, если мы попробуем выполнить следующий запрос: INSERT VALUES INSERT VALUES
INTO testlndex (firstName, middleName, lastName) ('Louis', 'Banks', 'Davidson') INTO testlndex (firstName, middleName, lastName) ('James', 'Banks', 'Davidson')
... то получим следующую ошибку:
I
Server: Msg 2601, Level 14, State 1, Line 3 Cannot insert duplicate key row in object 'testlndex' with unique index Xtestlndexl'. The statement has been terminated.
(Сервер: Сообщение 2601, уровень 14, состояние 1, строка 3 Нельзя поместить строку с повторяющимся ключом в объект testlndex индексом Xtestlndexl. Оператор удален.)
с уникальным
Хотя сообщение об ошибке могло бы быть и лучше, ясно, что предотвращается помещение дубликата строки с той же фамилией. IGNORE_DUP_KEY Часто полезно задать уникальность индексов с помощью параметра IGNORE_DUP_KEY (игнорировать дубликаты ключей). Используя это, мы можем сообщить системе SQL Server, чтобы она игнорировала любые строки с дубликатами ключей, если мы так желаем. Если мы изменим наш: индекс в предыдущем примере:
I
DROP INDEX testlndex.Xtestlndex4 CREATE UNIQUE INDEX Xtestlndex4 ON testlndex(firstName, middleName, lastName) WITH IGNORE DUP KEY
Здесь сначала удаляется индекс Xtestlndex4 для таблицы testlndex этот же индекс, но уже с параметром IGNORE_DUP_KEY. ... а далее выполним тот же код, что и в предыдущем случае: INSERT INTO t e s t l n d e x (firstName, middleName, lastName) VALUES ( ' L o u i s ' , ' B a n k s ' , ' D a v i d s o n ' ) INSERT INTO t e s t l n d e x (firstName, middleName, lastName) VALUES ( ' J a m e s ' , ' B a n k s ' , ' D a v i d s o n ' )
мы получим следующий результат: I
Server: Msg 3604, Level 16, State 1, Line 3 Duplicate key was ignored.
(Сервер: Сообщение 3604, уровень 16, состояние 1, строка 3 Дубликат ключа проигнорирован.) 314
и заново создается
Планирование и реализация основной физической структуры Эта настройка, вероятно, не то, что вам нужно в большинстве случаев, но может оказаться удобной, когда вы строите копию некоторых данных. Вы можете в этом случае вставлять данные, не волнуясь о том, что было туда помещено прежде, поскольку индекс отбросит двойные значения. Обратите внимание, что это не работает при обновлениях, а только при вставке строк.
Первичные ключи Выбор первичного ключа — один из наиболее важных выборов, которые мы делаем для конкретной таблицы. Он является первичным ключом, который будет мигрировать в другие таблицы как указатель на конкретное значение. В логическом моделировании мы выбрали использование указателя в виде 4-байтового целого числа для первичных ключей сущностей, которые мигрируют к зависимым таблицам-потомкам следующим образом: parent parentld
grandchild childld (FK) parentld (FK) grandChildld
Здесь parent — таблица "предок"; parentld — идентификатор предка; child — таблица "потомок"; childld — идентификатор потомка; grandchild — таблица "потомок потомка"; grandchildld — идентификатор потомка потомка. В физическом моделировании это заставляет реализованные ключи становиться довольно большими, не столько в смысле размера данных, сколько в смысле большого числа значений, которые мы должны использовать. Другая проблема с реализацией первичных ключей, таким образом, заключается в том, что мы обычно забываем тот факт, что ключ таблицы не должен состоять только из служебных столбцов. Мигрирующие ключи хороши, поскольку они представляют значения таблицы, из которой они пришли, но, добавляя уникальное значение, мы по существу сделали его служебным ключом (так как независимо от того, что вы добавляете к служебному значению, вы будете всегда получать служебное значение). В логическом моделировании это представление таблиц-предков и таблиц-потомков показало нам, что вы не могли иметь потомка потомка (внука) без потомка (ребенка) или предка (родителя), и мы всегда моделировали вторичный ключ, чтобы было ясно, что понимается под вторичным ключом. Однако теперь, находясь на физической стадии моделирования, мы будем следовать несколько иной стратегии. Каждая таблица будет иметь единственный служебный первичный ключ, и мы реализуем вторичные ключи в наших таблицах, как показано в следующей диаграмме: parent parentld otherParentAttForAltKey (AK1.1)
child childld parentld (FK)(AK1.1) otherChildAttributeForAltKey (AK1.2)
grandchild grandChildld childld (FK)(AK1.1) otherGrandChildAttribateForAIIKey (AK1.2)
Здесь дополнительно OtherParentAttForAltKey — другой атрибут предка для вторичного ключа; OtherChildAttributeForAltKey — другой атрибут потомка для вторичного ключа; otherGrandChildAttributeForAltKey — другой атрибут потомка потомка для вторичного ключа. 315
Глава 10 Мы выберем этот метод по следующим важным причинам: • •
•
•
Мы никогда не должны волноваться относительно того, что делать, когда значение первичного ключа изменяется. Каждая таблица имеет единственный столбец указателя первичного ключа, и в этом случае намного легче разрабатывать приложения, использующие этот указатель, потому что каждая таблица будет иметь единственное значение для первичного ключа. Наш индекс первичного ключа будет очень мал, и таким образом операции, которые используют его для доступа к таблице, будут быстрее. Большинство операций обновления и удаления будут, вероятно, изменять данные, обращаясь к данным с помощью первичных ключей, использующих этот индекс. Соединения таблиц будет более просто проектировать, так как все мигрирующие ключи будут содержать единственное поле.
Имеются неудобства в этом методе, типа того, что при соединении таблиц всегда нужно выяснять, что фактически означает первичный ключ, не говоря уж о других соединениях, которые необходимы для доступа к данным. Так как цели нашей OLTP-системы состоят в том, чтобы иметь небольшие ключи, ускорять корректировки и иметь согласованные данные, эта стратегия не только приемлема, но и благоприятна. Реализовать первичный ключ таким образом очень просто:
I
CREATE TABLE nameTable id integer NOT NULL IDENTITY PRIMARY KEY NONCLUSTERED )
Здесь создается таблица с именем nameTable (имя таблицы) с единственным автоинкрементным столбцом id типа integer, который не может иметь значений NULL, являющимся некластерным первичным нулем. Обратите внимание, что вы не можете использовать столбцы, допускающие значения NULL в первичном ключе, даже если вы можете иметь столбцы, допускающие значения NULL в уникальном индексе. Причина этого на самом деле довольно проста. Первичный ключ — идентификатор записи, a NULL не обеспечивает никакую форму идентификации записи. Обратите также внимание, что мы явно определили NONCLUSTERED (некластерный). Если мы опустим эту спецификацию, ограничения первичного ключа будут реализованы, используя кластерный индекс; мы часто не вспоминаем этот факт, пока не будет слишком поздно. Следующий оператор будет также работать и в случае, если вам нужно определить больше чем один столбец для первичного ключа. CREATE TABLE nameTable i d i n t e g e r NOT NULL IDENTITY, pkeyColumnl i n t e g e r NOT NULL, pkeyColumn2 i n t e g e r NOT NULL PRIMARY KEY NONCLUSTERED ( i d )
Здесь создается таблица с именем nameTable, содержащая три столбца: автоинкрементный столбец id типа integer, являющийся некластерным первичным ключом, и два дополнительных столбца первичного ключа pkeyColumnl и pkeyColumn2 (первый столбец первичного ключа и второй столбец первичного ключа). 316
Планирование и реализация основной физической структуры Обычно, когда мы реализуем первичный ключ как указатель в виде целого числа, мы не заинтересованы тратить на это впустую наш кластерный индекс. С первичным ключом этого вида нельзя обращаться как с каким-либо упорядоченным ключом. Причина этого в том, что вам редко будет нужно обращаться к строкам в порядке, определяемом первичным ключом, или рассматривать диапазон строк на основе первичного ключа. Если требуется упорядочивание, лучше добавить дополнительный столбец, потому что если потребуется переупорядочить записи, вы не сможете изменить значение идентифицирующего столбца; кроме того, вы не должны будете часто изменять первичный ключ по каким-либо причинам. Как упомянуто выше, если вы должны гарантированно обеспечить ключ, который будет уникальным по всей БД или даже по всему серверу, то можно использовать столбец типа u n i q u e i d e n t i f i e r со свойством ROWGUIDCOL, подобно следующему:
I
CREATE TABLE nameTable ( id uniqueidentifier NOT NULL ROWGUIDCOL DEFAULT newid(), PRIMARY KEY NONCLUSTERED (id)
Обратите внимание, что ROWGUIDCOL не обеспечивает ни уникальности, ни значения по умолчанию, как в идентифицирующем столбце. Только использование при необходимости типа u n i q u e i d e n t i f i e r , поскольку он использует шестнадцать байтов вместо четырех для целого числа, дает это.
Вторичные ключи Формирование вторичных ключей — очень важная задача физического моделирования. При реализации вторичных ключей мы имеем два выбора: •
уникальные ограничения;
•
уникальные индексы.
В качестве примера возьмем таблицу nameTable и расширим ее, включив имя и фамилию. При этом мы хотим обеспечить правило, что необходимо иметь уникальные полные имена в таблице. Мы могли бы создать индекс, подобно следующему: CREATE TABLE nameTable ( id integer NOT NULL IDENTITY PRIMARY KEY NONCLUSTERED, firstName varchar(15) NOT NULL, lastName varchar(15) NOT NULL ) CREATE UNIQUE INDEX XnameTable ON nameTable(firstName, lastName)
Или можно использовать уникальное ограничение типа: CREATE TABLE nameTable ( id integer NOT NULL IDENTITY PRIMARY KEY, firstName varchar(15) NOT NULL, lastName varchar(15) NOT NULL, UNIQUE (firstName, lastName)
317
Глава 10 Хотя фактически оба они основаны на уникальных индексах, уникальные ограничения — более предпочтительный метод реализации вторичного ключа и обеспечения уникальности. Это потому, что ограничения семантически предназначены для обеспечения ограничений на данные, а индексы предназначены для ускорения доступа к данным. Фактически не имеет значения, как реализована уникальность, но мы, конечно, должны иметь в одном месте или уникальные индексы или уникальные ограничения. При реализации ограничений есть одно дополнительное преимущество — возможность просматривать ограничения на таблицу, выполняя следующую хранимую процедуру: | sp_helpconstraint
'nameTable'
Она возвращает набор результатов, сокращенный вариант которого имеет вид: constraint_type
constraint_name
[...]
constraint_keys
PRIMARY KEY(clustered) UNIQUE (non-clustered)
PK_nameTable_1466F737 UQ_nameTable_155B1B70
... ...
id firstName, lastName
Хранимая процедура будет использоваться для просмотра всех ограничений, добавляемых к нашим таблицам, в следующих главах.
Обозначение Обозначение ключей не столь существенно для общей структуры таблицы. Однако все же важно давать ключам некоторые распознаваемые названия для использования в сообщениях об ошибках. SQL Server не требует от нас давать названия нашим индексам. Если мы не даем названий объявлениям ограничений, как поступали в предыдущих разделах, SQL Server назначает названия за нас. Например, следующие названия были выбраны, когда мы запускали код предыдущего примера: PK_nameTable_1 ED998B2 UQ_nameTable_1 FCDBCEB UQ_nameTable_20C1 E124 UQ_nameTable_21B6055D Они немного говорят читателю. Хотя и можно их рассматривать с помощью программных средств, наподобие Enterprise Manager, вероятно, лучше сделать их как-то более понятными. Стандарт обозначения, который мы будем использовать в этой книге, довольно прост: Щ <тип> <имя_таблицы> <описание> Для первичного ключа тип был бы РК (Primary Key — первичный ключ), а для вторичного ключа мы будем использовать АК (Alternate Key — вторичный ключ). Что касается описания, мы опустили бы его для первичного ключа, потому что у нас может быть только единственный экземпляр первичного ключа. Для описания вторичных ключей используйте или название столбца для единственного столбца, или краткое описание вторичного ключа, включающего больше одного столбца. Так, для нашего примера мы получили бы следующее: CREATE TABLE n a m e T a b l e i d i n t e g e r NOT NULL IDENTITY CONSTRAINT PKnameTable PRIMARY KEY,
318
Планирование и реализация основной физической структуры f i r s t N a m e v a r c h a r ( 1 5 ) NOT NULL CONSTRAINT AKnameTable_firstName UNIQUE, l a s t N a m e v a r c h a r ( 1 5 ) NULL CONSTRAINT AKnameTable_lastName UNIQUE, CONSTRAINT AKnameTable_fullName UNIQUE ( f i r s t N a m e , l a s t N a m e )
Это дало бы нам следующие названия, делая их более легкими для работы с ограничениями: PKnameTable AKnameTable_fullName AknameTableJastName AknameTable_firstName
Другие индексы О любых индексах, которые мы добавляем к индексам, обеспечивающим уникальность, нужно позаботиться на стадии настройки функционирования. Это — очень важный момент, на который я буду обращать внимание много раз. Индексы делают доступ к данным некоторым образом быстрее, но они также связаны с издержками. Каждый раз, когда происходят любые изменения в индексируемых столбцах, индекс также должен быть изменен. При единственном изменении это время действительно мало, но чем более активна система, тем больше это затронет функционирование. В нашей OLTP-системе только в очевидных случаях я бы защищал использование "догадок" — добавление индексов прежде, чем возникает потребность — вместо использования "настроек", где мы реагируем на известные проблемы функционирования. Только в этом случае мы будем способны решить, как "настройка" функционирования затронет остальную часть БД.
Отношения Мы уже рассмотрели в некоторой степени отношения, так что не будем говорить слишком много относительно причин их использования. В этом разделе мы просто обсудим, как реализовать отношения. Первое, что нужно сделать — ввести новый оператор. Это оператор ALTER TABLE: | ALTER TABLE <имя__таблицы>
Оператор ALTER TABLE позволяет нам изменять и добавлять столбцы, проверять ограничения, а также позволять и запрещать работу триггеров. В этом разделе мы рассмотрим, как добавить ограничения к таблицам. Обратите внимание, что вы можете это сделать с помощью оператора CREATE TABLE. Однако так как мы часто создаем все наши таблицы сразу, лучше использовать команду ALTER TABLE для уже существующих таблиц, нежели создавать таблицы в порядке, когда таблицы-предки создаются перед зависимыми таблицами-потомками.
Внешние ключи Внешний ключ — фактически тот же первичный ключ, мигрирующий в таблицу-потомок, но другой таблицы, которая представляет сущность, из которой он берется. Реализация внешних ключей — довольно простая задача в SQL Server 2000. Имеется несколько проблем, с которыми мы сталкиваемся при формировании отношений: •
Каскадированные удаления (если удалена строка предка, то удаляются любые связанные значения потомков, которые обращаются к ключу удаленной строки).
•
Отношения, которые охватывают разные БД. 319
Глава 10 Основной синтаксис оператора ALTER TABLE для добавления ограничений внешнего ключа довольно прост (команда ALTER TABLE будет использоваться неоднократно в этой и последующей главах, чтобы добавить ограничения): ALTER TABLE <имя_таблицы> ADD CONSTRAINT [<имя_ограничения>] FOREIGN KEY REFERENCES <таблица для ссылки> (<столбцы для ссылки>)
I
Обратите внимание, что этот код демонстрирует добавление ограничения. Относительно других действий, которые вы можете выполнить, используя ALTER TABLE, см. документацию SQL Server 2000. •
<имя_таблицы> — таблица-потомок в отношении.
•
<таблица_для_ссылки> — таблица-предок в отношении.
•
<столбцы_для_ссылки> — разделенный запятыми список столбцов в таблице-потомке в том же порядке, что и столбцы в первичном ключе таблицы-предка.
Если вам нужно реализовать необязательное отношение (где мигрирующий ключ может принимать значение NULL) подобно следующему: parent parentld: int NOT NULL parentAttribute: varchar (20) NULL
child ^~^_ имеет
childld: int NOT NULL parentld: int NULL childAttribute: varchar(20) NOT NULL
... то это реализуется почти так же, как в предыдущем случае. Оператор ALTER TABLE — тот же самый, но вы можете заметить, что столбец c h i l d , p a r e n t Id может принимать значение NULL. Когда ключ ссылки допускает значение NULL, SQL Server знает, что вы хотите, чтобы он был необязательным. Вы не должны иметь NULL в первичном ключе, потому что, как мы выяснили, первичный ключ не может иметь атрибут, допускающий значение NULL. Это столь же просто, как и защитить отношения предок-потомок, которые мы установили в нашем проекте. Вскоре будут представлены примеры отношений и каскадированных удалений, а также краткое обсуждение отношений между БД.
Обозначение При выборе имен для наших объектов мы обычно создавали имена, которые позволяли пользователю взаимодействовать с объектами. Для ограничений и индексов нам нужны имена, которые указывают на использование объекта. То же самое справедливо и для имен отношений; хотя возможно при некоторых обстоятельствах выбрать одно и то же имя для двух отношений, мы не сможем это реализовать в SQL Server. Следовательно, мы должны рассмотреть назначение имен для отношений. Стандартные обозначения для отношений, которые мы будем использовать в этой книге, берут для имени объекта глагол от логической модели и включают имена таблицы-предка и таблицы-потомка, чтобы создать уникальное имя, а также чтобы легче определить любые ограничения, когда просматривается список имен.
320
Планирование и реализация основной физической структуры Мы используем следующий шаблон для нашего имени: | <таблица-предок>$<глагол>$<таблица-потомок> Например, рассмотрим в качестве примера пару таблиц.
company companyld name
sells
product productld companyld (FK) name
Здесь company — таблица "компания"; companyld — идентификатор компании; name — имя; sells — продает; product — таблица "товар"; productld — идентификатор товара; companyld — идентификатор компании. Чтобы реализовать это отношение, мы можем определить имя company$sells$product и так же как и с именами внешних ключей реализовать следующий синтаксис:
I
ALTER TABLE p r o d u c t ADD CONSTRAINT c o m p a n y $ s e l l s $ p r o d u c t FOREIGN KEY
REFERENCES p a r e n t (companyld)
Обратите внимание на использование знака доллара в имени. Вспомнив наши стандарты идентификаторов, которые мы обсуждали ранее в главе, можно прийти к выводу, что нет никаких действительно хороших значений для разграничения или разделения отдельных частей имени, корме знака доллара. Кроме букв и цифр мы можем использовать только три специальных символа: знак доллара, амперсанд и знак подчеркивания. Знак подчеркивания часто используется внутри имен объектов (вместо способа "верблюда", который мы используем). Амперсанд часто используется для переменных, а отношение таковым не является. Следовательно, для ясности мы будем использовать знак доллара между именами. Мои имена объектов, по-видимому, будут удивительными для некоторых, но это лишь мое собственное соглашение относительно имен, и я использую знак доллара, чтобы разделять части имени объекта, потому что это не имеет никакого значения для SQL Server, и я могу использовать это соглашение независимо от выбранного стандарта обозначений. Это — другая область, где играют роль личные вкусы. Например, если мы имеем следующие таблицы в нашей модели: parent parentld: int NOT NULL parentAttribute: varchar(20) NULL
child имеет
childld: int NOT NULL parentld: int NOT NULL childAttribute: varchar(20) NOT NULL
... можно реализовать, используя следующий фрагмент создания таблиц:
I
CREATE TABLE parent parentld int IDENTITY NOT NULL CONSTRAINT PKparent PRIMARY KEY,
Продолжение кода на следующей странице
321
Глава 10 parentAttribute varchar(20) NULL ) CREATE TABLE child ( childld int IDENTITY NOT NULL CONSTRAINT FKchild PRIMARY KEY, parentld int NOT NULL, childAttribute varchar(20) NOT NULL )
Мы можем реализовать отношение, используя следующий синтаксис:
1
ALTER TABLE child ADD CONSTRAINT child$has$parent FOREIGN KEY (parentld) REFERENCES parent
Если мы создали это ограничение, то можем проверить его, пытаясь ввести данные в таблицу-предок, а затем в таблицу-потомок: INSERT INTO parent(parentAttribute) VALUES ('parent') -- получение последнего идентифицирующего значения, помещенного в текущий контекст DECLARE @parentld int SELECT @parentld = scope_identity() INSERT INTO child (parentld, childAttribute) VALUES (@parentld, 'child') DELETE FROM parent
Это вызывает следующую ошибку в БД при выполнении: Server: Msg 547, Level 16, State 1, Line 1 DELETE statement conflicted with COLUMN REFERENCE constraint'child$hasSparent'. The conflict occurred in database 'tempdb', table 'child', column 'parentld'. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор DELETE конфликтует с ограничением COLUMN REFERENCE (ссылка на столбец) 'child$has$parent'. Конфликт возник в БД 'tempdb', таблица 'child', столбец 'parentld'.)
Каскадирование удалений и обновлений В нашем предыдущем примере БД предотвращала нас от удаления записи в таблице-предке, если существовала запись в таблице-потомке, имеющая отношение к таблице-предку как к атрибуту. В большинстве случаев это хорошо, но имеются случаи, когда данные в таблице-потомке так объединены с данными таблицы-предка, что когда запись родительской таблицы изменена или удалена, SQL Server будет всегда стремиться изменить или удалить запись в таблице-потомке без какого-либо дальнейшего взаимодействия с пользователем.
322
Планирование и реализация основной физической структуры Рассмотрим случай таблицы товаров и таблицы, которая хранит описательную спецификацию: product productld
prod uctSpecif ication productSpecificationld имеет описательную информацию в
name
productld (FK) name value
Здесь product — таблица "товар"; productld — идентификатор товара; name — имя; productSpecif ication — таблица "спецификация товара"; productSpecificationld — идентификатор спецификации товара; value — величина. В этом случае, если бы мы хотели удалить запись из таблицы товаров, маловероятно, что мы хотели бы сохранить соответствующую запись в таблице product Specif i c a t i o n . Способность выполнять автоматически некоторое неявное изменение в таблице-потомке, как результат явного действия в таблице-предке, известна как каскадирование изменений. Для удаления вы можете установить ограничение, которое автоматически удалит потомков отношения при некоторых действиях на таблице-предке. В случае обновления, если вы изменяете значение первичного ключа таблицы-предка, то изменение каскадировало бы значение внешнего ключа таблицы-потомка. Имеется другое менее используемое действие каскадирования, которое SQL Server фактически не осуществляет — тип каскадируемых действий, когда запись из таблицы-потомка полностью не удаляется, но ее ключевая ссылка устанавливается в NULL. Это может быть реализовано только на необязательных отношениях, так как если внешний ключ не допускает значение NULL, данную операцию выполнить не удастся. Обсуждение того, как осуществить эту операцию, будет проведено в следующей главе, поскольку она реализуется с помощью триггеров. Две наиболее захватывающие новые особенности SQL Server 2000 — это возможность иметь каскадированное удаление в объявленной ссылочной целостности и каскадированное обновление. В предыдущих версиях SQL Server операция каскадирования требовала триггера. Это было неприятно, потому что мы не могли при этом использовать отношения с помощью внешнего ключа и средства, которые могли бы использовать объявленные отношения. Синтаксис для осуществления операций каскадирования прост. Мы просто добавляем ON DELETE CASCADE или ON UPDATE CASCADE к нашему объявлению ограничения: ALTER TABLE <имя_таблицы> ADD CONSTRAINT <имя_ограничения> FOREIGN KEY REFERENCES <таблица_для_ссылки> [ON DELETE | ] [ON UPDATE | ]
(<столбцы_для_ссылки>)
Использование опции NO ACTION для любого объявления устанавливает отношения в "нормальный" запрещающий случай, когда в случае наличия записи-потомка и изменения ключа или удаления, выполняемых с записью предка, возникает ошибка.
323
Глава 10 Может показаться странным так использовать NO ACTION, но что делать, если операция удаления не удалась, — мы просто берем NO ACTION и завершаем операцию. Для продолжения нашего примера с ограничением c h i l d $ h a s $ p a r e n t отметим, что после создания записи-потомка мы не могли удалять исходную запись-предка. Модифицируем этот пример, чтобы позволить каскадированные удаления. -- заметим, что здесь нет команды constraint в операторе alter ALTER TABLE child DROP CONSTRAINT child$has$parent ALTER TABLE child ADD CONSTRAINT child$has$parent FOREIGN KEY (parentld) REFERENCES parent ON DELETE CASCADE SELECT * FROM parent SELECT * FROM child
Этот запрос возвращает parentld
parentAttribute
2
parent
childld
parentld
childAttribute
2
2
child
Далее запустим удаление и снова выполним операции SELECT:
I
DELETE from parent SELECT * FROM parent SELECT * FROM child
... что возвратит: parentld childld
parentAttribute parentld
childAttribute
Просто реализовать таким же способом и пример каскадирования обновления. Обратите внимание, однако, что первичный ключ не может быть реализован как столбец идентификации, чтобы обеспечить каскадирование обновления, так как идентифицирующие значения не могут быть изменены.
Взаимные отношения БД Основная трудность с внешними ключами, основанными на ограничениях — то, что таблицы, участвующие в отношениях, не могут охватывать различные БД. Когда возникает эта ситуация, мы должны реализовать наши отношения через триггеры. 324
Планирование и реализация основной физической структуры Вообще-то плохо проектировать БД с взаимными отношениями между ними. БД должна рассматриваться как совокупность связанных таблиц, которые всегда синхронизированы. Когда мы проектируем БД, которые распространяются на различные БД или даже серверы, мы распространяем ссылки на данные, находящиеся не в пределах БД, и не можем гарантировать их существование. Однако бывают случаи, когда взаимные отношения БД неизбежны.
Распределение деталей вашей БД между разработчиками На стадии логического моделирования проекта мы потратили много часов, вводя определения наших таблиц, столбцов и других объектов. В этом разделе мы рассмотрим некоторые средства, которые должны дать нашим клиентам понимание того, что мы создали для них. Эти средства обеспечат отчеты по всем объектам БД и их свойствам, и реализованным, и информационным. Однако построение этих отчетов, поддержание их в актуальном состоянии и распределение их может быть серьезной задачей. На протяжении остальной части книги мы собираемся лишь исследовать средства, которые SQL Server 2000 предоставляет нам. Имеется несколько методов, помещенных в SQL Server: •
информационная схема и системные хранимые процедуры;
•
новые наглядные средства;
а
сервисы метаданных.
Мы детально рассмотрим первые два метода из этого списка, но опустим сервисы метаданных. Они очень ценны, но их непросто использовать. Эти сервисы, прежде всего, используются для обмена метаданными между средствами моделирования — DTS и OLAP и не совсем уместны с точки зрения тематики этой книги. Если вы интересуетесь, то можете выяснить детали относительно сервисов метаданных (Meta Data Services) в SQL Server 2000 Books Online.
Информационная схема и системные хранимые процедуры Чтобы пользователям можно было просто рассматривать структуры ваших таблиц, SQL Server предлагает нам информационную схему (Information Schema) и системные хранимые процедуры (system stored procedures). Информационная схема — набор двадцати представлений, основанных на стандартном (ANSI SQL-92) определении механизма формирования стандартного набора метатаблиц, обеспечивающего разработку с использованием нескольких платформ. Они определяют многие из объектов SQL Server — все, которые понятны для стандарта SQL-92 — и обеспечивают быстрый доступ к структурам наших таблиц. В этих представлениях мы используем несколько отличающийся, но достаточно понятный набор семантических представлений для общих объектов: Название в SQL Server
Название в информационной схеме (SQL-92)
database (БД) owner (владелец) user-defined data type (тип данных, определяемый пользователем)
Catalog (каталог) Schema (схема) Domain (домен)
325
Глава 10 Следующая таблица дает список процедур метаданных и соответствующих представлений информационной схемы (INFORMATION_SCHEMA). Объяснение, которое дается в третьем столбце таблицы, — просто краткое примечание, для чего используются различные процедуры и представления. Для любого программиста БД желательно хорошо понимать, как использовать эти объекты, чтобы запросить метаданные модели. Лучший способ для этого — посмотреть полную информацию в SQL Server 2000 Books Online. Таким образом, SQL Server предлагает два совершенно разных способа просмотра тех же самых системных данных. Сравнивая два различных метода, можно выделить два момента, которые важны для меня. Системные хранимые процедуры — сильно ориентируемы на реализацию в SQL Server. Представления информационной схемы дают разработанный общими усилиями взгляд, который может подойти не для каждого. Однако определения, которые они дают, хотя и специфические, но все-таки очень полезны. По моему мнению, лучше использовать представления информационной схемы всегда, когда это возможно, потому что они основаны на стандарте SQL 92 и будут, вероятно, улучшены в будущем. Кроме того, так как каждая БД, созданная на основе SQL 92, будет в конечном счете иметь идентичный набор реализованных представлений, использование этих представлений должно облегчить понимание других систем БД. Системная хранимая процедура
Соответствующее представление информационной схемы
sp s e r v e r info (информация сервера)
отсутствует
sp d a t a b a s e s (базы данных)
SCHEMATA (схемы)
•
sp t a b l e s (таблицы)
326
TABLES, VIEWS (таблицы, представления)
Объяснение
Дает список настроек и информацию, связанную с сервером. Информация о сервере не может быть помещена в информационную схему. Оба дают список БД и серверов. Список представления SCHEMATA содержит более полезную информацию о создателе БД, в то время как sp databases дает размеры БД. Дает список таблиц и представлений в системе. sp t a b l e s содержит системные таблицы, таблицы пользователя и представления. Как ни странно, TABLES • содержит перечень таблиц и представлений, a VIEWS — только представления. Оно содержит расширенную информацию относительно представлений, такую как обновляемость, определения, контролируемые параметры и некоторые другие характеристики.
Планирование и реализация основной физической структуры
Системная хранимая процедура
Соответствующее представление информационной схемы
Объяснение
sp columns (столбцы)
COLUMNS (столбцы)
sp s t o r e d procedures (хранимые процедуры)
ROUTINES (подпрограммы)
В то время как процедура sp colunms требует параметра, характеризующего таблицу, для которой она используется, представление COLUMNS этого не требует; оба содержат полезную и интересную информацию относительно столбцов. Оба перечисляют хранимые процедуры и функции, но sp s t o r e d procedures имеет несколько столбцов, которые еще не были реализованы, a ROUTINES — все. Оба этих метода имеют подсказку, как войти в SQL и SQL Server.
sp sproc columns (параметры хранимых процедур)
PARAMETERS (параметры)
Оба перечисляют параметры хранимых процедур и функций.
Отсутствует
ROUTINE_COLUMNS (столбцы подпрограмм)
Перечисляет столбцы таблиц, возвращаемых функциями, конечно, когда они возвращают таблицы.
VIEWS, ROUTINES (представления, подпрограммы)
sp h e l p t e x t — одна из тех процедур, которая должна быть понятна любому администратору. Используя эту процедуру, вы можете получать текст любой процедуры, значения по умолчанию, расширенной хранимой процедуры (которая возвращает используемую DLL) или функции, находящейся в системе. VIEWS и ROUTINES похожи, поскольку они также возвратят характеристики представления или подпрограммы, наподобие справочного текста.
sp helptext (справочный текст)
'
Продолжение таблицы на следующей странице 327
Глава 10
328
Системная хранимая процедура
Соответствующее представление информационной схемы
Объяснение
sp column p r i v i l e g e s (права доступа к столбцам)
COLUMN_PRIVILEGES (права доступа к столбцам)
Оба перечисляют права доступа к столбцам таблицы.
sp s p e c i a l columns (специальные столбцы)
Отсутствует
Возвращает первичный ключ таблицы.
sp s t a t i s t i c s (статистика)
Отсутствует
sp s t a t i s t i c s отображает информацию об индексах таблицы. В информационной схеме нет информации об индексах.
sp fkeys (внешние ключи)
TABLE_CONSTRAINTS, CONSTRAINT_TABLE_USAGE, REFERENTIAL CONSTRAINTS, KEY COLUMNJJSAGE (ограничения таблицы, использование ограничений таблицы, ссылочные ограничения, использование ключевых столбцов)
sp_f keys дает список ограничений за счет внешних ключей таблицы. TABLE CONSTRAINTS и CONSTRAINT_TABLE_USAGE перечисляют все ограничения таблицы, первое — возможные, а второе — действительные. REFERENTIAL CONSTRAINTS перечисляет все ограничения БД за счет внешних ключей. KEY_COLUMN_USAGE перечисляет столбцы в первичном и внешних ключах, обеспечивающих ограничения.
sp_pkeys (первичные ключи)
KEY_COLUMN_USAGE, TABLE_CONSTRAINTS (использование ключевых столбцов, ограничения таблицы)
sp pkeys перечисляет первичные ключи таблицы. KEY_COLUMN USAGE и TABLE_CONSTRAINTS у ж е упоминались в предыдущем разделе sp fkeys.
sp h e l p c o n s t r a i n t (справка об ограничениях)
REFERENTIAL_CONSTRAINTS, CHECK_CONSTRAINTS, TABLE_CONSTRAINTS, CONSTRAINT_COLUMN_USAGE, CONSTRAINT_TABLE_USAGE (ссылочные ограничения, проверка ограничений, ограничения таблиц, использование ограничений столбцов, использование ограничений таблиц)
sp h e l p c o n s t r a i n t дает перечень ограничений таблицы. Все элементы информационной схемы уже были обсуждены.
Планирование и реализация основной физической структуры
Системная хранимая процедура
Соответствующее представление информационной схемы
Объяснение
sp table privileges (права доступа к таблицам)
TABLE_PRIVILEGES (права доступа к таблицам)
sp table privileges дает список прав доступа для данной таблицы, которые можно предоставлять. TABLE_PRIVILEGES отображает по одной строке на каждое право доступа, предоставленное пользователю. sp t a b l e p r i v i l e g e s и TABLE PRIVILEGES связаны, но — фактически несут разную информацию. sp table_privileges говорит о том, что можно предоставлять, в то время к а к TABLE_PRIVILEGES связано с тем, что было предоставлено.
sp datatype info (информация о типах данных)
DOMAINS (домены)
sp datatype info перечисляет все типы данных, которые используются в системе и ее свойствах, в то время как DOMAINS отображает только типы данных, определяемые пользователем.
Отсутствует
DOMAIN CONSTRAINTS, COLUMN_DOMAIN_USAGE (ограничения доменов, использование доменов столбцов)
Два дополнительных представления добавлены для доменов и могут быть действительно удобны. DOMAIN CONSTRAINTS перечисляет все ограничения (правила), которые были добавлены к домену. COLUMN DOMAIN_USAGE перечисляет все таблицы, где использовался домен.
Имеется некоторая информация в этой группе функций, которую мы не обсудили, в первую очередь связанная с функциями, обеспечивающими права доступа и которые мы будем рассматривать в главе 12.
329
Глава 10 Оставим читателю разобраться, как реализовать выполнение процедур, просмотр таблиц и изучение SQL Server 2000 Books Online, раздел System Stored Procedure (Системные хранимые процедуры) (особенно catalog procedures — процедуры каталога, но лучше изучить все), а также раздел Information Schema Views (Представления информационной схемы). С • комбинацией процедур каталога, объектов информационной схемы и функций T-SQL мы можем просмотреть практически любую часть метаданных, содержащихся в БД, которую создали.
Новые описательные свойства Во время нашего моделирования мы создали описания, записи и различные наборы данных, которые являются чрезвычайно полезными для разработчика, чтобы понять все "почему" и "для чего" использования таблиц, которые мы создали. В предыдущих версиях SQL Server было очень трудно фактически использовать эти данные. В SQL Server 2000 фирма Microsoft ввела расширенные свойства, которые позволяют нам размещать определенную информацию об объектах. Это действительно хорошо, потому что позволяет нам расширить метаданные наших таблиц способами, которые могут использоваться нашими приложениями с помощью простых SQL-операторов. Создавая эти свойства, мы можем формировать хранилище информации, которое прикладные разработчики могут использовать для следующих целей: • •
чтобы понять, какие данные используются в столбцах; чтобы разместить информацию, используемую в приложениях, например: •
заголовки, чтобы показать их форму при отображении столбцов;
•
сообщения об ошибках, выводимые при нарушении ограничений;
•
правила форматирования для отображения вводимых данных.
Расширенные свойства размещаются как столбцы THnasql_variant, так что они могут содержать любые типы данных кроме полей типа t e x t или image. С целью подключения расширенных свойств объекты в SQL Server классифицируются на три уровня, называемые схемой. Следующее дерево иллюстрирует эту иерархию: Уровень 0
Уровень 1
Уровень 2
User (пользователь)
Table(таблица)
Column (столбец) Index (индекс) Constraint (ограничение) Trigger (триггер)
View
(представление)
Column (столбец) Index (индекс — только схемносвязанные представления) Trigger (триггер — только INSTEAD OF
Stored procedure (хранимая процедура)
Parameter (параметр)
Rule (правило) Default (значение по умолчанию) Function (функция)
Column (столбец) Constraint (ограничение) Parameter (параметр)
3 3 0
Планирование и реализация основной физической структуры
Обратите внимание: в этой диаграмме мы упомянули новый термин: схемно-связанный. Связывание схемы означает механизм, который является новым в SQL Server 2000: когда вы создаете схемно-связанное представление или схемно-связывающую функцию, определяемую пользователем, то можете проинструктировать SQL Server не допускать никаких изменений основных таблиц, которые оно (представление) или она (функция) использует. Мы рассмотрим это более детально в следующей главе.
Только для этих объектов мы можем определять расширенные свойства, поскольку SQL Server проверит имена, которые мы используем для этих свойств. Схема присвоения имен довольно проста. Мы можем добавить свойство пользователю, что не требует никакой другой информации, но чтобы добавить свойство к таблице, мы сначала должны иметь ссылку на пользователя, который владеет таблицей. Чтобы добавить свойство к столбцу, мы должны знать пользователя, который владеет таблицей, а также имя таблицы и имя столбца. Чтобы обслуживать расширенные свойства, мы имеем следующие функции и хранимые процедуры: •
sp_addextendedproperty — используется для добавления нового расширенного свойства;
•
sp_dropextendedproperty — используется для удаления существующего расширенного свойства;
Q
sp_updateextendedproperty — используется для корректировки существующего расширенного свойства;
•
fn l i s t e x t e n d e d p r o p e r t y — системная функция, которая может использоваться, чтобы сформировать список расширенных свойств.
В следующем примере мы рассмотрим конкретный синтаксис каждой команды, когда будем использовать ее, но важно понять основной путь, в соответствии с которым они работают. Каждая имеет следующие параметры: •
@name — имя определяемого пользователем свойства;
•
@value — какое задать значение при создании или изменении свойства;
•
QlevelOtype — или тип пользователя, или определяемый пользователем тип данных;
•
@ l e v e l 0name — имя объекта того типа, который определен параметром @levelOtype;
•
@levelltype — имя типа объекта, который находится на 1-м уровне ветви дерева непосредственно под параметром user, типа Table, View и т. д.;
331
Глава 10 •
@levellname — название объекта, имеющего тип, который задан в параметре Qlevelltype;
•
@level2type — имя типа объекта, который находится на 2-м уровне ветви дерева под значением, заданным параметром Qlevelltype; например, если @levelltype Table, то @level2type может быть Column, Index, Constraint или Trigger;
•
@level2name — имя объекта, имеющего тип, который задан в параметре @level2type.
Например, пусть в следующем фрагменте м ы создаем таблицу, которую назовем person (человек): CREATE TABLE person ( personld int NOT NULL IDENTITY, firstName varchar(40) NOT NULL, lastName varchar(40) NOT NULL, socialSecurityNumber varchar(lO) NOT NULL
Чтобы сделать это, мы собираемся добавить к таблице и столбцам свойство по имени MS_DESCRIPTION. Почему MS_DESCRIPTION? Потому что это — имя, которое выбрано фирмой Microsoft, чтобы использовать в Enterprise Manager для размещения описаний. Обратите внимание, что это имя может меняться. Я нашел имя свойства, используя SQL Profiler, чтобы проследить, как SQL Server сохранил свойство, а затем сформировал свою собственную версию сценария. Профилирование таким же образом, как это делает Enterprise Manager, является быстрым способом определить, как описать задачи, которые, казалось бы, возможно сделать, используя только средства графического пользовательского интерфейса (GUI— Graphical User Interface). Итак, мы выполняем следующий фрагмент после создания таблицы: -- GUI-описание таблицы dbo.person (таблица person владельца dbo) EXEC sp_addextendedproperty @name = 'MS_Description', @value = 'Пример расширенных свойств в таблице person', @levelOtype = 'User', @levelOname - 'dbo', @levelltype = 'table', @levellname = 'person' -- GUI-описание dbo.person.personld (атрибут personld -- таблицы person владельца dbo) EXEC sp_addexendedproperty @name = 'MS_Description', @value • 'указатель первичного ключа на экземпляр записи в таблице person', OlevelOtype = 'User', @levelOname = 'dbo', glevelltype • 'table', @levellname = 'person', @level2type = 'column', @Level2name = 'personld' —
332
GUI-описание dbo.person.firstName
(атрибут firstName
Планирование и реализация основной физической структуры -- таблицы person владельца dbo) EXEC sp_addexendedproperty @name = 'MS_Description', @value = 'Имя человека', SlevelOtype'User', @levelOname = 'dbo', @levelltype = 'table', @levellname = 'person', @level2type • 'column', @Level2name = 'firstName' — GUI-описание dbo.person.lastName (атрибут lastName -- таблицы person владельца dbo) EXEC sp_addexendedproperty @name = 'MS_Description', @value = 'Фамилия человека', @levelOtype = 'User', dlevelOname = 'dbo', @levelltype = 'table', @levellname = 'person', @level2type = 'column', @Level2name = 'lastName' — GUI-описание dbo.person.socialSecurityNumber (атрибут — socialSecurityNumber таблицы person владельца dbo) EXEC sp_addexendedproperty @name = 'MS_Description', @value = 'описание столбца с номерами социального обеспечения' @levelOtype = 'User', @levelOname = 'dbo', @levelltype = 'table', @levellname = 'person', @level2type = 'column', @Level2name = 'socialSecurityNumber' Теперь войдем в Enterprise Manager, щелкнув правой клавишей м ы ш и на нашей таблице и выбрав Design Table (схема таблицы); м ы увидим наше описание следующим образом: •ж SQL Server Enterprise Manager - [3:Design Table 'perso. 2 Console
I
Window
Column Name
Help
Data Type
I Length j Allow Nulls
Ь personld
f irstName ; varcbar lastName varchar socialSecurityNumber varcbar
ID
Columns Description
primary key pointer to a person instance
Identity Identity Seed Identity Increment
Yes 1 1
Is ' • ,u..i . ... Formula: •
r:> '
333
Глава 10
А взглянув на свойства таблицы (щелкнув иконку справа от Save — сохранить) мы заметим, что теперь имеем описание: Properties Tabe l s j Relationships ] In «d e* s/Key$ Check Constraints j 13
Selected table: Qwner; Tablename:
jgdbo ; person
: Table:identity Cou l mn: personld
zl
Tabe l ROWGU D I Cou l mn:
d
Tabe l Ftfegroup:
PRM I ARY
d
Text Filegroug: ;
PRM I ARY
zl
Description:
Exampe l of extended properties on the J person table J
Co i se
Help
Очевидно, это не совсем то, что м ы обычно делаем, но если м ы имеем усовершенствованные возможности в наших средствах моделирования данных, то должны быть способны выводить эти данные, используя Enterprise Manager, который намного более полезен как средство для наших программистов. Чтобы изменить свойство, м ы могли бы использовать следующий тип сценария: -- смотрим, существует ли свойство IF EXISTS (SELECT * FROM : :FN_LISTEXTENDEDPROPERTY('MS_Description' , 'User', 'dbo', 'table', 'person', 'column', 'socialSecurityNumber') ) BEGIN -- если свойство уже существует EXEC sp_updateextendedproperty @name = 'MS_Description', @value = 'государственный идентификационный номер человека', @levelOtype = 'User', SlevelOname = 'dbo', @levelltype = 'table', @levellname = 'person', @level2type = 'column', @level2name = 'socialSecurityNumber' END ELSE BEGIN -- в противном случае создать его EXEC sp_addexendedproperty @name = 'MS_Description', @value = 'государственный идентификационный номер человека'.
334
Планирование и реализация основной физической структуры @levelOtype = 'User', @levelOname • 'dbo', @levelltype = 'table', @levellname = 'person', @level2type = 'column', @level2name = 'socialSecurityNumber'
I
END Объект fn_listExtendedProperty (список расширенных свойств) является системной функцией, которую мы рассмотрим позже в главе 12.
Наше последнее дополнение к этому примеру должно добавить расширенное свойство к таблице, которое процессы пользователя могут использовать при выполнении программы. Для этого мы добавим свойство, представляющее собой маску для ввода в таблицу, чтобы мы могли создать общий метод для ввода данных в столбец.
I
EXEC sp_addexendedproperty @name = 'Маска ввода', @value = ' * * * - * * - * * * * ' , @levelOtype • ' U s e r ' , @levelOname = ' d b o ' , @levelltype = ' t a b l e ' , @levellname = ' p e r s o n ' , @level2type = 'column', @level2name = 'socialSecurityNumber'
Заметьте, что мы можем создавать любое расширенное свойство, которое пожелаем. Названия должны вписаться в тип данных sysname (системное имя), и могут включать пробелы, неалфавитно-цифровые символы или даже двоичные значения. Эта новая особенность документации имеет некоторые интересные возможности для создания приложений и использования большей части метаданных, которые мы хранили в различных местах, даже не используя особенно важную документацию. Дополнительную информацию можно получить в SQL Server 2000 Books Online, раздел Using Extended Properties on Database Objects (Использование расширенных свойств в объектах баз данных).
Учебный пример Возвращаясь к нашему учебному примеру, мы теперь должны заняться созданием физических структур, чтобы воплотить теорию в практику. Будем следовать нашей протоптанной дорожке, когда мы определяем ситуации, которые могут быть слишком трудными для реализации, и устраняем их, затем выбираем первичные ключи и типы данных и, наконец, представляем физическую модель, которую мы создадим. Будем использовать макроязык T-SQL для реализации нашей БД, включая добавление описательной информации из нашего исходного анализа.
Очистка модели Первым шагом должна быть корректировка любых мест в модели, которые являются слишком трудными для реализации. В некоторых случаях нам придется опустить большие области реализации, которые м ы хотели бы выполнить, естественно, получая при этом не лучшее решение. В зависимости от ситуации, средств или даже общих стандартов нам, вероятно, придется изменять пути реализации отдельных частей, хотя мы должны, в конце концов, получить решение, которое является функционально эквивалентным логической модели. Каждое преобразование, которое мы делаем в модели во время стадии физического моделирования, не должно изменить набор данных, которые размещаются в нашей БД, если только м ы не обнаружим в дальнейшем атрибутов, которые следует добавить к логической модели. 12 1868
335
Глава 10
Подтипы Одна из наиболее трудных ситуаций для реализации в SQL Server — это наличие подтипов. В логической модели (которую мы закончили создавать в конце главы 8) имеется пример как раз такого случая: подтип сделки. Мы моделировали его как полный подтип, то есть, все возможные случаи были представлены в диаграмме. transaction bankld: PrimaryKey(FK) (AK1.1) accountld: PrimaryKey (FK)(AK1.2) transactionld: PrimaryKey date: Date number: AlphaNumerlcNumber (AK1.3) description: Description amount: Amount type: Code deposit | bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK)
Type
directWithdra wal bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) userld: PrimaryKey (FK)
check bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) payeeld: PrimaryKey (FK) usageType: Code ^signature: Blob
checkUsage имеет использование, определенное в
checkllsageType
userid: Primarykey user name: UserName ofirstName: FirstName middleName: Md i de l Name lastName: LastName
bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) checkllsageTypeld: PrimaryKey (FK)
задает категории чека с помощью
checkUsageTypeld: PrimaryKey name: Name description: Description parentCheckUsageTypeld: PrimaryKey (FK)I
(Обозначения см. в главах 5 и 7.) Однако мы не определили никаких дополнительных атрибутов для сущностей d e p o s i t (депозит) и directWithdrawal (прямое изъятие), чтобы добавить их к модели. В этом случае, мы могли просто заменить наш подтип на неполный вариант (где не всякое значение подтипа фактически реализовано таблицей) и удалить все, что связано с понятиями "депозит" и "прямое изъятие". В зависимости от того, насколько велик подтип (один или два атрибута или больше плюс связанные таблицы), мы можем пожелать назначить эти атрибуты предку отношения. Именно это мы сделаем в нашем примере. Основная причина здесь — практичность, так как каждый из подтипов очень близок друг к другу. Позже, мы должны будем написать некоторый специальный код, чтобы защитить данные от неправильного ввода в таблицы подтипа. Основываясь на значении дискриминатора, в подтипе довольно очевидно, какие поля действительно имеют силу (в этом случае дискриминатор — тип сделки, потому что он определяет, в какой таблице-потомке мы должны искать значения подтипа), но как только мы берем все атрибуты всех сущностей подтипа и возвращаем их предку, это может стать менее очевидным. В нашем случае мы имеем три атрибута, которые можно использовать: 336
Планирование и реализация основной физической структуры •
payee (получатель платежа) — чеки всегда имеют получателей платежа. Прямые изъятия могут их иметь, но депозиты никогда не имеют получателей платежа. Мы могли бы увеличить наши таблицы, чтобы разместить информацию о том, откуда поступают деньги на депозит, но не будем делать этого, потому что хотим ограничить размер проекта и потому что такое изменение нарушило бы планы реализации, которые были предварительно одобрены клиентом.
•
s i g n a t u r e (подпись) — только чеки имеют подписи. Этот атрибут кажется несколько бессмысленным, но мы оставим его как пример. Если бы мы эту систему реализовывали полностью, данный атрибут, вероятно, соответствовал бы большому дереву доменов и принадлежал бы депозитам, прямым изъятиям и чекам. Однако мы будем использовать логику предыдущего пункта и скажем, что, поскольку клиент не просил об этом, мы не будем делать это. Мы также изменим эту величину до строки, потому что будем только хранить полное имя человека, который подписал чек, а не изображение подписи. Конечно, если бы это была реальная система, мы, видимо, спросили бы клиента, являются ли эти корректировки приемлемыми и, может быть, приняли бы другое решение.
•
checkUsageType (тип использования чека) — этот атрибут довольно прост; мы изменим его на TransactionAllocationType (тип размещения сделки), где пользователь может установить использование/размещение для каждого типа сделки.
Итак, мы реконструируем таблицы следующим образом: payee transaction воздействует на фонды payeeld: PrimaryKey bankld: PrimaryKey (FK)(AK1.1) через accountld: PrimaryKey (FK)(AK1.2) • - о name: Name(AK1.1) transactionld: PrimaryKey date: Date информацию number: AlphaNumerc i Number (AK1.3) имеет о размещении в description: Description amount: Amount type: Code __ T I I 1 transactionAllocation i модифицирует баланс счета через согласование с i 1
Л userid: Primarykey user name: UserName firstName: RrstName middleName: Md i de l Name lastName: LastName
transactionAllocationTypeld: PrimaryKey (FK) bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey (FK) allocationAmount: Amount
задает категории чека
transactionAllocationType transactionAllocationTypeld: PrimaryKey name: Name description: Description parentCheckUsageTypeld: PrimaryKey (FK) V
t- — 'i I
1 является
I I I
Здесь дополнительно transactionAllocation — таблица "размещение сделки", transactionAllocationTypeld — идентификатор типа размещения сделки, bankld идентификатор банка, accountld — идентификатор счета, transactionld — идентификатор сделки, allocationAmount — сумма размещения.
—
337
Глава 10 Нужно отметить следующий момент. Таблица сделок не может быть названа t r a n s a c t i o n (сделка), потому что это зарезервированное слово, и SQL Server не позволяет использовать его в коде. В английском языке transaction — и банковская сделка, и транзакция операция). Прим. перев.
(групповая
Мы имеем несколько вариантов, как поступить в этом случае: •
дать таблице другое имя, например, x a c t i o n (х-операция);
•
всегда обращаться к таблице с названием в скобках: [ t r a n s a c t i o n ] .
Ни один из них не очень хорош, и t r a n s a c t i o n (сделка) было бы лучшим именем для таблицы и обращающихся к ней таблиц. Мы выберем имя t r a n s a c t i o n и использование скобок при обращении к таблице. Это сохранит имя таблицы более понятным для клиента. Однако будем ссылаться на таблицу с помощью [ t r a n s a c t i o n ] только тогда, когда используем это в коде. Посмотрим, как реализовать конкретные правила формирования данных, которые мы будем использовать в следующей главе.
Упрощение сложных отношений В некоторых случаях мы будем моделировать объекты, выбранные на стадии логического проектирования, которые являются несколько сложными для практической реализации. В нашей логической модели мы имеем одну ситуацию, которую могли бы посчитать слишком трудной для реализации в таком виде, как мы ее разработали. Чтобы иметь уникальный почтовый адрес, мы должны просмотреть две таблицы. В нашей модели таблицы address (адрес) и addressLine (строка адреса) имеют вид: address addressld: PrimaryKey cityld: PrimaryKey (FK) stateld: PrimaryKey (FK) zipCodeld: PrimaryKey (FK)
addressLine имеет дополнительную информацию в
addressld: PrimaryKey (FK) addressLineld: PrimaryKey addressLine: AddressDescriptiveLine sortOrderld: SortOrderld J
Они учитывают адрес неограниченного размера и структуры, что часто не является необходимым для физической реализации. Так что мы просто реорганизуем таблицу address следующим образом: address addressld: PrimaryKey addressLine: AddressDescriptiveLine (AK1.1) cityld: PrimaryKey (FK) (AK1.2) stateld: PrimaryKey (FK) (AK1.3) zipCodeld: PrimaryKey (FK) (AK1.4) Поле addressLine (строка адреса) используется и для названия улицы, и почтового ящика, и т. д., размер которого мы определим, когда выберем типы данных.
338
Планирование и реализация основной физической структуры Таблицы доменов В таблице t r a n s a c t i o n (сделка) мы имеет столбец type (тип), чтобы задать тип сделки. transaction 'bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) transactionld: PrimaryKey date: Date number: Number description: Description amount: Amount type: String signature: String ijDayeeld: PrimaryKey (FK) Обычно мы не будем использовать поле ввода данных в свободной форме, когда нам нужно группировать поля, или, как в случае столбца type таблицы t r a n s a c t i o n , где мы должны будем, вероятно, использовать значение домена для специальной обработки в нашем пользовательском интерфейсе. Мы хотели бы даже добавить характерную для реализации информацию к нашей таблице, содержащей тип адреса, наподобие расположения имен файлов с иконками, так, чтобы пользовательский интерфейс мог выполнить некоторую проверку типа чека, кредитной карточки при прямом изъятии и т. д. В нашей таблице t r a n s a c t i o n T y p e (тип сделки) мы добавляем столбцы name (имя) и d e s c r i p t i o n (описание). Таким образом мы можем включить большее количество информации, чтобы объяснить, что каждое значение в таблице означает. Снова каждый из этих атрибутов следовало бы показать клиенту и, возможно, добавить к логической модели. Мы видим эти изменения плюс три других поля логического типа в следующей диаграмме: transaction 'bankld: PrimaryKey (FK) (AK1.1) ? accountld: PrimaryKey (FK)(AK1.2) transaction^: PrimaryKey date: Date number: AlphaNumericNumber (AK1.3) description: Description amount: Amount signature: String payeeld: PrimaryKey (FK) userld: PrimaryKey (FK) statementld: PrimaryKey (FK) .statementltemld: PrimaryKey (FK)
классифицирует
transactionType transactionType Id: PrimaryKey name: Name description: Description requiresSignature:FI: Fl requiresPayeeFI: Fl allowsPayeeFI: Fl
у
Здесь в дополнение к предыдущим обозначениям transactionType — таблица "тип сделки", transactionTypeld — идентификатор типа сделки, name — название (сделки), description — описание, requiresSignatureFl — флаг необходимости подписи, requiresPayeeFI — флаг необходимости иметь получателя платежа, allowsPayeeFI — флаг возможности иметь получателя платежа. Теперь мы можем писать код, который вынуждает заполнять поле s i g n a t u r e (подпись) для некоторого типа сделки и может определить также, обязателен ли получатель платежа. 339
Глава 10 Построение пользовательского интерфейса будет более легким, потому что мы имеем данные, которые сообщат нам, когда требуется значение, и изменение требований не будет приводить к написанию дополнительного кода. Каждое из этих полей флагов, вероятно, лучше рассматривать как логические поля, потому что они поддерживают реализацию информации уже на текущей стадии проектирования. Мы будем также использовать эту таблицу как наш тип сделок для таблицы s t a t e m e n t l t e m (элемент отчета) (хотя в этом случае мы не будем использовать поля флагов). Так как наши типы сделок будут характерны для банка, это оградит нас от наличия двойных данных в таблице, плюс это будет обеспечивать соответствие элементов во время балансирования счета намного более простым, но мы не будем, конечно, требовать, чтобы типы сделок совпадали.
Удаление ненужных сущностей После дальнейшего просмотра мы замечаем, что таблицаcheckRegister на самом деле не будет реализована как таблица вообще, так что будет только электронный регистр чека, как только будет создана БД. Поэтому мы удалим ее из нашей физической модели, хотя она все еще имеет значение в логической модели, поскольку это — фактическая сущность, с которой нам, вероятно, придется иметь дело. checkRegister checkRegisterld: PrimaryKey registerType:
Здесь checkRegister — сущность "Проверка регистра"; checkRegisterld — идентификатор проверки регистра; registerType — тип регистра.
Коррекция ошибок в модели Когда мы выполняем этот процесс, то найдем ошибки в модели, даже если мы используем лучшие средства моделирования и работаем не спеша. Например, в исходной модели мы имели следующие отношения: bank bankld: PrimaryKey name: Name (AK1.1) предлагает account bankld: PrimaryKey (FK) (AK1.1) имеет баланс счета в accountld: PrimaryKey number: AlphaNumericNumber (AK1.2) accountReconcile bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) statementld: PrimaryKey (FK) reconcileDate: Date periodEndDate: Date
3 4 0
Используется для согласования счета с помощью
bankld: PrimaryKey (FK)(AK1.1) statementld: PrimaryKey type: Code previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.2) totalDebits: Amount totalCredits: Amount
Планирование и реализация основной физической структуры Здесь ошибка в отношении между таблицами bank (банк) и s t a t e m e n t (отчет): "банки посылают отчеты". Отношение должно быть "банки, посылают отчеты для счетов" и поэтому при реализации отношение между bank и s t a t e m e n t должно быть заменено отношением между account (счет) и s t a t e m e n t . Мы перемещаем столбец periodEndDate (период до последней даты) в таблицу s t a t e m e n t , поскольку это будет часть отчета, а не значение, которое мы размещаем, когда отмечаем, что отчет выверен. Мы также опустим отношение между таблицами account и accountReconcile (согласование счета), так как будем отмечать отчет как выверенный. bank bankld: PrimaryKey name: Name(AK1.1) предлагает account bankld: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey number: AlphaNumericNumber (AK1.2)
совпадает с данными записей в классифицирует
statementTypei statementTypeld: PrimaryKey name: Name description: Description
statement statementid: PrimaryKey bankld: PrimaryKey (FK) (AK1.2) accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount используется для согласования счета с помощью accountReconcile bankld: PrimaryKey (FK) accountld: PrimaryKey (FK) statementid: PrimaryKey (FK) reconcileDate: Date periodEndDate: Date
Один из интересных моментов этой модели заключается в том, что для выяснения, когда счет был последний раз выверен, мы должны пройти через таблицу s t a t e m e n t к таблице accountReconcile. Хотя это и может казаться странным непосвященным в программирование БД с использованием нормализации, однако это является одним из контрольных признаков хорошо разработанной БД. В большей системе вам пришлось бы пройти пять, десять или двадцать таблиц, чтобы выяснить информацию, подобную этой. Принцип наличия одной части данных, размещенной в одном месте, может делать БД более сложными для прослеживания данных, но, с другой стороны, устранит избыточные данные и аномалии корректировки — если номер счета размещен более чем в одном месте, и потребуется его изменить, то изменение должно быть сделано в каждом месте, иначе ваши данные станут противоречивыми. Как мы увидим в следующей главе, чтобы не было возможности получения случайного беспорядка наших данных, придется выполнить дополнительное внутреннее кодирование. Вы не должны рассматривать обнаружение ошибок, наподобие этой, в вашей модели как неудачу. Очень часто мы будем пропускать материал, когда выполняем стадию проектирования. Часто наши клиенты будут изменять свои требования к БД, с которыми мы должны иметь дело. Ошибки не могут быть исключены, поскольку каждый архитектор БД — человек. Часто они незаметны, пока вы не закончили первый набросок проекта и можете определять более сложные проблемы и рассматривать их решение. Иногда одно проектное решение может определять или изменять несколько других. Проектирование БД всегда является итерационным процессом.
341
Глава 10
Подготовка модели для реализации Как только мы разобрались с логической моделью так, как только что отметили, можно заняться подготовкой наших таблиц для создания. Здесь мы должны предпринять шаги, чтобы сделать систему легкой для создания, добавляя столбцы для оптимистической блокировки и запрета работы с записью (и что-нибудь еще, удовлетворяющее ваши специфические потребности). Затем мы добавим первичные ключи, уникальные ключи и, наконец, выберем типы данных. Как только мы завершим эти заключительные задачи, мы будем готовы к написанию наших объектов БД и реализации БД на SQL Server.
Служебные столбцы Кратко напомним, что служебные столбцы не относятся ни к каким физическим объектам БД. Они просто включены в БД, чтобы сделать кодирование более легким. Два примера, которые мы ранее рассмотрели, можно учесть в следующей таблице: ! payee payeeld: PrimaryKey name: Name (AK1.1) date: disableDate autoTimestamp: autoTimestamp В этой таблице мы имеем столбец типа d i s a b l e D a t e (дата с возможностью запрета) в противоположность d i s a b l e F l (флаг запрета), который мы ранее обсудили. Параметр типа d i s a b l e D a t e устанавливает дату и время, после которого получатель платежа больше не может получать суммы денег по сделкам в нашей БД. Мы могли бы также добавить таблицы, чтобы хранить причины этого и историю состояния получателя платежа, но мы не будем здесь это делать, чтобы упростить задачу. Параметр типа d i s a b l e D a t e работает так же, как d i s a b l e F l , за исключением того, что вместо простого флага мы реализуем дату, так что мы можем видеть, какие будущие дату и время мы устанавливаем. Мы также добавили столбец autoTimestamp (автоматическое время создания/обновления) к каждой таблице модели, который, как мы предварительно обсудили, формирует основы механизма оптимистической блокировки. Они — исключительно служебные столбцы, которые мы будем добавлять к нашей текущей БД.
Первичные ключи Столбцы первичного ключа — фактически тоже служебные столбцы, так как мы никогда не будем показывать их пользователю, и они существуют прежде всего для нужд нашей реализации, хотя мы и представили их и на логической, и на физической моделях. Возьмем следующий фрагмент модели: 342
Планирование и реализация основной физической структуры
bank bankd i : Prm i aryKey name: Name(AK1.1)
предлагает
bankid: PrimaryKey (FK) (AK1.1) accountld: PrimaryKey number: AlphaNumericNumber (AK1.2)
совпадает с данными
statementld: PrimaryKey bankid: PrimaryKey (FK) (AK1.2) accountld: PrimaryKey (FK)
имеет элементы в
statementTypeld: PrimaryKey (FK) previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount
statementltem statementld: PrimaryKey (FK) bankid: PrimaryKey (FK) accountld: PrimaryKey (FK) statementltemld: PrimaryKey date: Date number: AlphaNumericNumber description: Description amount: Amount
Указатель bankid (идентификатор банка) мигрирует из таблицы bank (банк) всюду до таблицы s t a t e m e n t l t e m (элемент отчета). Это прекрасно для логического моделирования, но ключи в физических таблицах, состоящие из нескольких частей, вызывают больше неприятностей, чем имеют достоинств. Главная причина здесь заключается в том, что нам может потребоваться два, три или даже большее количество столбцов, чтобы уникально идентифицировать записи таблицы. Если вам нужно присоединить десять таблиц, чтобы найти связанную информацию, то это будет неприятно. Таким образом, если наша БД была реализована указанным выше образом, и мы задали вопрос — "В каких городах в Первом Национальном Банке мы потратили большую часть денег на текстиль?", то этот запрос включил бы каждую таблицу, если бы мы использовали все ключи, состоящие из нескольких частей, и запрос оказался бы неуправляем. Другая причина для создания первичного ключа, состоящего из служебного столбца, состоит в том, что если вы изменяете состав логического первичного ключа для объекта, то не затрагиваете реализованный первичный ключ объекта. Это уменьшит влияние изменения на любой зависимый объект. В нашем примере возьмем набор отношений 'bank - account - s t a t e m e n t statementltem". Если мы решили, что таблица bank больше не нужна, и удалили таблицу из модели, таблицы account, statement и statementltem изменят свои первичные ключи: совпадает с данными bankid: PrimaryKey (FK) (AK1.1) записей в accountld: PrimaryKey number: AlphaNumericNumber (AK1.2) statement statementld: PrimaryKey имеет элементы в bankid: PrimaryKey (FK) (AK1.2) accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) statementltem statementld: PrimaryKey (FK) previousBalance: Amount bankid: PrimaryKey (FK) previousBalanceDate: Date accountld: PrimaryKey (FK) currentBalance: Amount statementltemld: PrimaryKey statementDate: Date (AK1.1) totalDebits: Amount date: Date totalCredits: Amount number: AlphaNumericNumber description: Description amount: Amount
343
Глава 10 Это — не оптимальное решение нашей проблемы кодирования, потому что весь код, который использовал эти сущности, будет теперь нарушен. Однако если бы каждая таблица имела первичный ключ, состоящий из одного столбца, только таблицу account нужно будет изменить. Содержание объектов изменилось бы, так как они больше не будут определяться соответствующим банком, но для программиста фактические значения первичных ключей были бы все еще теми же самыми. Счет бы все еще содержал отчеты и сделки. bank bankld: PrimaryKey name: Name (AK1.1)
предлагает
account
i
4
accountld: PrimaryKey bankld: PrimaryKey (FK) (AK.1.1) number: AlphaNumericNumber (AK1.2)
совпадает сданными записей в I I I statement * statementld: PrimaryKey accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount
имеет элементы в I I statementltem • statementltemld: PrimaryKey statementld: PrimaryKey (FK) date: Date number: AlphaNurrsricNumber description: Description amount: Amount
Теперь посмотрим, что произойдет, если мы удалим таблицу bank из этой модели: account совпадает с_данными accountld: PrimaryKey записей в I bankld: PrimaryKey (FK) (AK1.1) I number: AlphaNumericNumber (AK1.2) I statement * statementld: PrimaryKey accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) previousBalance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount
имеет элементы в I I statementltem • statementltemld: PrimaryKey statementld: PrimaryKey (FK) date: Date number: AlphaNumericNumber description: Description amount: Amount
Таблицы s t a t e m e n t и s t a t e m e n t l t e m не изменяются. Следовательно, программы, осуществляющие доступ к ним, также не будут изменяться. Только программы, связанные с таблицей account, должны будут измениться, чтобы отразить, что в таблице больше нет поля bankld, — незначительное изменение по сравнению с тем, что могло бы потребоваться. Обратите внимание, что, как только мы изменили ключи, реализация таблиц изменилась, хотя то, что таблицы представляют, не изменилось.
344
Планирование и реализация основной физической структуры Один момент относительно новых таблиц — каждая из них имеет уникальный ключ, который содержит мигрирующую часть предыдущего первичного ключа. Например, перед внесением изменений таблица s t a t e m e n t выглядела следующим образом: statement statementld: PrimaryKey bankld: PrimaryKey (FK) (AK1.2) accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) previousBaiance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount Эта таблица имеет не простой для определения естественный ключ, так как мы предварительно не задавали никаких вторичных ключей в нашем проекте. Если повнимательнее посмотреть, то можно заметить, что имеется возможный ключ, который мы можем использовать — значение даты (вероятно, со временем будет удалено). Мы должны сформировать программу, позволяющую выполнять балансирование счета в диалоговом режиме, которое мы могли бы делать ежедневно, но не более часто (проконсультируйтесь с клиентом перед принятием таких решений). Итак, мы могли бы использовать дату как часть вторичного ключа, что представлено в следующей диаграмме: statement statementld: PrimaryKey accountld: PrimaryKey (FK) statementTypeld: PrimaryKey (FK) previousBaiance: Amount previousBalanceDate: Date currentBalance: Amount statementDate: Date (AK1.1) totalDebits: Amount totalCredits: Amount Обычно мы не используем даты в наших ключах, даже вспомогательных, если можем обойтись без этого — особенно потому, что мы реализуем дату как полную восьмибитовую величину даты-времени. Имеются две очень важных причины избегать этой практики: •
Простота обращения к данным — когда мы должны использовать ключ, чтобы получить доступ к конкретной строке в таблице, то если наш ключ определен как значение даты, может быть довольно трудно ввести вручную фактическое значение даты. Например, значение d a t e t i m e содержит день, месяц, год, час, минуту, секунду и части секунды (тип s m a l l d a t e t i m e включает значения вплоть до минут). Это может быть довольно тяжело, особенно для среднего пользователя.
•
Точность — в большинстве случаев точность переменных, содержащих даты, больше, чем требуется для среднего ключа. Обычно, когда вы хотите поместить дату в ключ, вы просто задаете день и год.
В нашем случае мы будем использовать тип d a t e t i m e , вероятнее, s m a l l d a t e t i m e , чтобы иметь возможность выполнить отчет в различные времена в течение дня, и этот тип здесь наиболее удачен. Плюс к этому, мы будем удалять некоторые из значений времени. 345
Глава 10 Типы данных Во время моделирования мы выбрали логические домены для наших атрибутов. Теперь мы возьмем наш проект и преобразуем атрибуты в реальные поля и назначим реальные типы данных. Чтобы выбрать типы данных, мы создадим таблицу выбранных нами доменов и затем назначим типы данных и описания для них — выбор типа данных, возможность задания значения NULL и любая дополнительная информация относительно домена. Как только мы выполним эту задачу, то пройдем по каждой из наших таблиц, задавая типы данных значениям доменов, которые мы выбрали. Домены, которые мы должны использовать в нашей логической модели в течение этого упражнения, имеют форму: tableName pkeyAttributeld: PrimaryKey attributel: otherDomainName Здесь tableName — имя таблицы, pkeyAttributeld первичного ключа, attributel — первый атрибут.
— идентификатор атрибута
... где primaryKey (первичный ключ) и otherDomainName (имя другого домена) рассматриваются как домены для различных атрибутов. Следующий список содержит все домены, которые мы использовали по имени. Мы пройдем их и сделаем некоторые предположения относительно того, что представляют собой типы данных, а затем, когда закончим эту работу, будем использовать результаты первого просмотра для выбора наших типов данных. Мы найдем, что имеется очень хорошее совпадение, и нам не нужно будет многие из них изменять. Однако нам будет нужно пройти все таблицы, чтобы проверить, что они правильные (конечно, здесь мы рассмотрим только несколько таблиц). Название
Комментарий
Тип данных
Возможность использования NULL
addressDscriptive Line (строка описания адреса)
Используется для размещения информации об адресе.
varchar(900), чтобы размещать уникальные индексы размером до 900 байт
NOT NULL
addressStateCode (код штата в адресе)
Два символа кода штата, которые определяются почтовым ведомством США. Алфавитно-цифровое "число", которое обычно является значением вторичного ключа для записи в таблице. Пример — номер счета.
c h a r (2)
NOT NULL
varchar(20)
NOT NULL
alphaNumericNumber (алфавитно-цифровое число)
3
4
6
•
Планирование и реализация основной физической структуры
Название
Комментарий
Тип данных
Возможность использования NULL
amount (сумма)
Домен общего вида для денежных величин.
money
NOT NULL
areaCode (код региона)
Код, который определяет регион штата.
varchar(3)
NOT NULL
аиtoTimestamp (автоматическое определение времени создания/обновления)
Автоинкрементная величина оптимистической блокировки.
timestamp
' NOT NULL
countryCode (код страны)
Набираемый код, который определяет страну.
char(1)
NOT NULL
date (дата)
Используемые по умолчанию значения даты и времени. По умолчанию используются величины smalldatetime, так как нам редко нужна точность типа данных datetime.
smalldatetime
NOT NULL
description (описание)
Краткие неструктурируемые комментарии, описывающие элемент таблицы, чтобы иметь возможность передать это другим пользователям.
Varchar(100)
NOT NULL
disableDate
Используется, чтобы запретить работу с записью, не удаляя ее, например, для целей архивирования.
smalldatetime
NULL
exchange (номер коммутатора)
Код региона, который помещается перед номером телефона.
char(5)
NOT NULL
extension (расширение)
Строка, которая содержит ряд чисел, которые могут быть набраны после выполненного соединения.
varchar(20)
NULL
firstName (имя)
Используется для размещения имени человека.
varchar(60)
NOT NULL
(дата запрещения)
Продолжение таблицы на следующей странице
347
Глава 10
Название
Комментарий
Тип данных
Возможность использования NULL
F1 (флаг)
Обычно — логическая величина. Реализуется как столбец в бит, который не может содержать значение NULL и значение которого, равное нулю, означает False (ложь), а ненулевое значение — True (истина).
bit
NOT NULL
lastName(фамилия)
Используется для размещения фамилии человека.
varchar(60)
NOT NULL
middleName (отчество)
Используется для размещения отчества человека.
varchar(60)
NOT NULL
name (имя)
Имя, которое будет однозначно определять конкретную запись. Это — не имя человека, а скорее значение вторичного ключа для объекта.
varchar(60)
NOT NULL
number (число)
Обобщенная величина, являющаяся числом. По умолчанию целого типа.
int
NOT NULL
phoneNumberPart (собственно номер телефона)
Номер телефона.
char(4)
NOT NULL
primaryFl (первичный флаг)
Флаг в один бит, который используется, чтобы указать первичную запись группы.
bit
NOT NULL
primaryKey (первичный ключ)
Автоинкрементный столбец идентификации, используемый как указатель на элемент сущности.
int
NOT NULL, IDENTITY
Обобщенные строковые значения. Мы использовали ее по умолчанию, когда не знали, какую строку собираемся использовать. По умолчанию имеет значение NOT NULL.
varchar(20)
s t r i n g (строка)
348
(автоинкрементный) NOT NULL
Планирование и реализация основной физической структуры
Название
Комментарий
Тип данных
Возможность использования NULL
username (имя пользователя)
Используется для размещения имени пользователя в системе.
sysname (тип данных, определяемый пользователем в SQL Server, который используется для размещения имен пользователей). Мы будем размещать в SQL Server в этом типе данных имена, которые отображают реальные имена, так мы дадим права любому пользователю, который может вводить чек, просматривать сделки и/или счета, используемые в учебном примере. char(5) (Обратите внимание, что мы использовали zipCode, чтобы связаться с городом. Он представлен пятью символами почтового индекса. Мы игнорировали дополнительные 4 символа почтового индекса снова для простоты.)
NOT NULL
•
zipCode (почтовый индекс)
Пять цифр почтового индекса, определяющего почтовый регион, который может охватывать различные города, штаты, а может также просто идентифицировать часть города.
NOT NULL
Следует отметить, что все мои адреса и номера телефонов находятся в США. Это преследует одну цель — простоту.
349
Глава 10 Как только наш список будет закончен, мы просматриваем каждую из таблиц и рассматриваем каждый тип данных и спецификацию NULL для каждого поля, чтобы увидеть, как они работают в конкретной ситуации. Если мы выясняем, что некоторые из них не соответствуют спецификациям, отмеченным выше, и решим повторить этот процесс снова, то, вероятно, захотим создать новые домены для дальнейшего использования. Однако не будем это делать; вернее, будем делать только необходимые корректировки, если потребуется. Один дополнительный момент — мы будем также иметь желание завершить определения для наших атрибутов, поскольку добавили и изменили многие из них на протяжении проектирования. В каждом из следующих разделов мы будем брать одну из наших сущностей, подбирать имя домена из списка, определять значения по умолчанию для типов данных и затем обсуждать и корректировать тип данных, взятый по умолчанию. Начнем с сущности bank (банк): bank bankld PrimaryKey 1 name: Name (AK1.1) 1 •
bankld (идентификатор банка) priraaryKey, i n t NOT NULL IDENTITY — именно то, что необходимо. Определяется как: "Не читаемый пользователем указатель для идентификации конкретного банка".
•
name (название) name, v a r c h a r (60) — это должно быть достаточно для любого названия банка. Определяется как: "Уникальное название конкретного банка".
Обратите внимание, что мы используем очень формальные определения для атрибутов, так как они будут использоваться разработчиками, которые не участвовали на стадии проектирования, и это будет первый раз, когда они увидят спецификацию БД. На стадии логического моделировании (начиная с главы 4) мы определили атрибут "Bank Name" как "название банка, с которым мы имеем дело", что означает то же самое, но в более мягкой форме, и это будет понятно всем пользователям. При разработке систем с использованием средств моделирования БД вы должны будете скорректировать определения, которые мы выбрали. Наконец, мы завершаем следующим: bank bankld: int IDENTITY name: varchar(60) NOT NULL (AK1.1) autoTimestamp: timestamp NOT NULL Обратите внимание, что мы также добавили столбец autoTimestamp, определяемый как "Автоинкрементная величина оптимистической блокировки". Мы добавим это ко всем таблицам.
350
Планирование и реализация основной физической структуры Далее рассмотрим таблицу account (счет): account accountld: PrimaryKey bankld: PrimaryKey (FK) (AK1.1) number: AlphaNumericNumber (АКТ .2)
accountld (идентификатор счета) primaryKey, i n t NOT NULL IDENTITY — это точно то, что необходимо. Определяется как: "Не читаемый пользователем указатель для идентификации конкретного счета". bankld (идентификатор банка) primaryKey, i n t NOT NULL — это точно то, что необходимо, поскольку является значениями внешнего ключа, не допускающего значение NULL. Определяется как: "Не читаемый пользователем указатель для идентификации конкретного банка".
•
number (номер) alphaNumericNumber, v a r c h a r (20) NOT NULL — который должен быть достаточен почти для любого номера счета. Должны быть выполнены некоторые дополнительные исследования, чтобы определить, будет ли это годиться для всех значений, используемых клиентом. Определяется как: "Номер счета, который формируется банком, чтобы идентифицировать счет". Обратите внимание, что, хотя значение упомянуто как число, оно фактически является алфавитно-цифровой строкой, потому что может содержать ведущие нули или буквы.
Это приводит к: account accountld: int IDENTITY bankld: int NOT NULL (FK) (AK1.1) number: varchar(20) NOT NULL (AK1.2) autoTimestamp: timestamp NOT NULL Для нашей последней иллюстрации рассмотрим таблицу t r a n s a c t i o n (сделка): transaction transactionld: PrimaryKey accountld: PrimaryKey (FK) (AK1.1) date: Date number: AlphaNumericNumber (AK1.2) description: Description amount: Amount signature: String payeeld: PrimaryKey (FK userld: PrimaryKey (FK) statementltemld: PrimaryKey (FK) transactionTypeld: PrimaryKey (FK)
351
Глава 10 •
t r a n s a c t i o n l d (идентификатор сделки) primaryKey, i n t NOT NULL IDENTITY — это точно то, что необходимо. Определяется как: "Не читаемый пользователем указатель для идентификации конкретной сделки".
•
accountld (идентификатор счета) primaryKey, i n t NOT NULL — это точно то, что необходимо, поскольку является внешним ключом, не допускающим значение NULL. Определяется как: "Не читаемый пользователем указатель для идентификации конкретного счета".
•
number (номер) alphaNumericNumber, varchar (20) NOT NULL — который должен быть достаточен почти для любого номера чека, номера депозита или кредитной карточки. Должны быть выполнены некоторые дополнительные исследования, чтобы определить, будет ли это годиться для всех значений, используемых клиентом. Определяется как: "Номер, используемый в счете для идентификации сделки". Обратите внимание, что, хотя значение упомянуто как число, оно фактически является алфавитно-цифровой строкой, потому что может содержать ведущие нули или буквы.
•
d e s c r i p t i o n (описание) d e s c r i p t i o n , varchar (100) NOT NULL — в этом случае мы должны, вероятно, выделить дополнительное место, чтобы описать использование чека, кредитной карточки или депозита. Однако у нас также есть таблица checkAllocation, где использование детализируется. По этой причине мы изменим тип столбца описания на varchar (1000), чтобы дать свободу для описания деталей сделки, если потребуется. Определяется как: "Неструктурированные комментарии, описывающие конкретную сделку, которые в дальнейшем могут быть расширены для других возможностей пользователей, чтобы характеризовать сделку".
•
amount (сумма) amount, money NOT NULL — это будет достаточно, чтобы описать сумму сделки. Определяется как: "Количество денег, которые должны быть перемещены с помощью сделки".
•
signature (подпись) s t r i n g , v a r c h a r (20) NOT NULL — возможно, следует расширить строку, чтобы разместить полное имя. Мы также могли бы пожелать использовать тип данных image (изображение), чтобы хранить образец подписи. В наших таблицах мы просто изменим тип с v a r c h a r (20) на v a r c h a r (100), чтобы позволить пользователю вводить полное имя человека, кому принадлежит подпись. Определяется как: "Текстовое представление подписи, которая была использована в сделке". payee Id (идентификатор получателя платежа) primaryKey, i n t NOT NULL — это не совсем то, что необходимо, так как оно — необязательное отношение. Поэтому мы зададим столбцу характеристику NULL. Определяется как: "Не читаемый пользователем указатель для идентификации конкретного получателя платежа, который может быть заполнен лишь на основании значения allowsPayeeFl (флаг разрешения иметь получателя платежа) в связанной таблице t r a n s a c t i o n T y p e (тип сделки)".
•
•
352
u s e r l d (идентификатор пользователя) primaryKey, i n t NOT NULL — не является необходимым, так как это — необязательное отношение. Поэтому мы установим столбцу характеристику NULL. Определяется как: "Не читаемый пользователем указатель, используемый для идентификации пользователя, который отвечает за сделку".
Планирование и реализация основной физической структуры Q
statementltemld (идентификатор элемента отчета) primaryKey, i n t NOT NULL — не является необходимым, так как это — необязательное отношение. Поэтому мы установим столбцу характеристику NULL. Определяется как: "Не читаемый пользователем указатель, используемый для идентификации s t a t e m e n t l t e m (элемент отчета), который используется для конкретной сделки".
•
transactionTypeld (идентификатор типа сделки) primaryKey, i n t NOT NULL — это точно то, что необходимо, так как это обязательное поле. Определяется как: "Не читаемый пользователем указатель, используемый для идентификации записи типа сделки, который определяет тип конкретной сделки'
Завершим это следующей таблицей: transaction transaction^: int NOT NULL accountld: int NOT NULL (FK) (AK1.1) date: smallDateTime NOT NULL number: varchar(2O) NOT NULL (AK1.2) description: varchar(1000) NOT NULL amount: money NOT NULL signature: varchar(20) NOT NULL payeeld: int NULL (FK) userld: int NULL (FK) statementltemld: int NULL (FK) transactionTypeld: int NOT NULL (FK) autoTimeStamp: timestamp NOT NULL Таким же образом рассмотрим все таблицы, пока не определим все типы данных и формулировки. В следующих двух разделах мы покажем окончательную физическую модель, которую будем реализовывать, учитывая описания трех таблиц, которые мы создали в этом разделе. Полное описание схемы (и сопровождающий код) размещено в www.wrox.com. Прежде, чем мы продолжим, следует сделать примечание относительно сопоставления типов данных (наборы символов и упорядочивание, которые мы сформировали ранее). Задание индивидуальных сопоставлений для столбца — весьма продвинутая тема, и полезна для определенного набора ситуаций. В нашем учебном примере мы не будем использовать сопоставления, но я упоминаю их здесь снова, чтобы напомнить вам, что возможно установить сопоставление для отдельного столбца, если потребуется.
Физическая модель На данном этапе следует создать модель на бумаге для "проверки здравого смысла", предпочтительно кем-либо из сотрудников, если это возможно, чтобы можно было гарантировать, что ваша модель не только эффективна и годна к употреблению, но также и понятна кому-то еще. Итак, мы подходим к представлению окончательной физической модели нашей СУБД. Мы должны иметь в виду, что это модели, по которым мы создадим и реализуем сервер БД. 353
Глава 10
sstatementTypeld: tatementType int IDENTITY
bank b nkeld::va inrtchD IaE IYNOT NULL (AK1.1) naam r(N 60T)T autoTm i estamp: tm i estamp NOT NULL предлагает
name: varchar(60) NOT NULL (AK1.1) description: varchar(IOO) NOT NULL(AK2.1) autoTimestamp: timestamp NOT NULL
поддерживается согласованным с записями в
statementld: int IDENTITY
классифицирует
accountld: int NOT NULL(FK) (AK1.1) statementTypeld: int NOT NULL (FK) previousBalance: money NOT NULL previousBalanceDate: smallDateTime NOT NULL cunrentBalance: money NOT NULL date: smallDateTime NOT NULL(AK1.2) totalDebits: money NOT NULL totalCredits: money NOT NULL periodEndDate: smallDateTime NOT NULL autoTimestamp: timestamp NOT NULL
accountld: int IDENTITY bankld: int NOT NULL (FK) (AK1.1) number: varchar(20) NOT NULL (AK1.2) autoTimestamp: timestamp NOT NULL
используется для согласования счета с помощью
имеет элементы в statementltem
accountReconcile accountReconcileld: int IDENTITY
statementltemld: int IDENTITY
statementld: int NOT NULL (FK) (AK1.1) reconcileDate: smallDateTime NOT NULL (AK1.2) autoTimestamp: timestamp NOT NULL
statementld: int NOT NULL (FK)N(AK1.1) date: smallDateTime NOT NULL number: varchar(20) NOTNULL(AK1.2) description: varchar(IOO) NOT NULL amount: money NOT NULL transactionTypeld: int NOT NULL(FK) autoTimestamp: timestamp NOT NULL
transaction transactionld: int NOT NULL accountld: int NOT NULL (FK) (AK1.1) date: smallDateTime NOT NULL number. varchar(20) NOT NULL (AK1.2) •description: varchar{1000) NOT NULL amount: money NOT NULL signature: varchar(20) NOT NULL payeeld: int NULL (FK) userld: int NULL (FK) statementltemld: int NULL (FK) transactionTypeld: int NOT NULL (FK) autoTimestamp: timestamp NOT NULL f I I I корректирует балланс счета, запрашивая
классифицирует используется для согласования
transactionType transactionTypeld: int IDENTITY name: varchar(60) NOT NULL (AK1.1) description: varchar(IOO) NOT NULL (AK2.1) requiresSignatureFl: bit NOT NULL requiresRayeeFI: bit NOT NULL allowsPayeeFI: bit NOT NULL autoTimestamp: timestamp NOT NULL
классифицирует
имеет информацию о распределении, размещенную в
userid: int IDENTITY username: sysname NOT NULL (AK1.1) firstName: varchar(60) NOT NULL middleName: varchar(60) NOT NULL lastName: varchar(60) NOT NULL autoTimestamp: timestamp NOT NULL
transactionAllocationType transactionAllocationTypeld: int NOT NULL name: varchar(60) NOT NULL description: varchar(IOO) NOT NULL parentCheckUsage^peld: int NOT NULL (FK) autoTimestamp: timestamp NOT NULL
3 5 4
классифицирует чек с помощью
transactionAllocation transactionAllocationld: int IDENTITY transactionAllocationTypeld: int NOT NULL (FK) (AK1.1) transactionld: int NOT NULL (FK) (AK1.2) allocationAmount: money NOT NULL autoTimestamp: timestamp NOT NULL
Планирование и реализация основной физической структуры
papye e Pehe od n N uN m bm eO rT a yoe P n e ud b IU nLT iLD IE lhe :o In tbe N (N FN K .)(AK11 p h n e N u m r l : n ied trl:N N UT L)IY(AL (FKK1)2 . pu ho nm tO a toT ieN esua tm mbpa:r^pm itedl:esatInm pNOT NOTNUL NUL(FK) арактеризуется номеров телефона с помощью payeeld: «it IDENTTTY name: varchar(60) NOT NULL (AK1.1) date: smallDateTime NULL autoTlrnestamp: timestamp NOT NULL
phoneNumbeV rpe n ahem :N vnam rch a 6 0dl): n N U K 11 .) .) p osncerp u ba e-cfirr(c ph e i1O t0T D I)EN T ILYT (A d i o t i : 0 N O NU ULL auo tTm i esa tmvp :m itaer(satm p N O TN L (AK21 phoneNumberld: int IDENTITY countryCode:char(1) NOT NULL (AK1.1) areaCode: varchar(3) NOT NULL (AK1.2) exchange: char(5| NOT NUU(AK1.3) number char(4) NOT NULL(AKt .4) extension: varchar(20) NOT NULL (AK1.5) autoTlroestamp: timestamp NOT NULL
payeeAddressId: int IDENTITY
ара^теризуется адресом
payeeid: int NOT NULL (FK) (AK1.1) addressld: int NOT NULL(FK) (AK1.2) addressTypeld: int NOT NULL (FK) autoTimestamp: timestamp NOT NULL
характеризует*
addressld: Int IDENTITY addressiype addressTypeld: int IDENTITY name: varchar(60) NOT NULL (AK1.1) description: varchar(lOO) NOT NULL (AK2.1) autoTimestamp: timestamp NOT NULL
addressUr»: varehar(800) NOT NULL (AK1.1) cllyld: intN0TNULL(FK)(AK1.2) zipCodeld: inl NOT NULL (FK)(AK1.3) autoTimestamp: timestamp NOT NULL
првделяв!
stateld: int IDENTITY code: char(2) NOT NULL (AK1.1) autoTimestamp: timestamp NOT NULL
пределяет распо
регион zipCodeld: Int IDENTITY
1арактеризует( подходящим почтовым индексом
stateld: int NOT NULL (FK) (AK1.1) lame: varcnar(60) NOT NULL (AK1.2) utoTimestamp: timestamp NOT NULL
zipCodeCityReferenceld: int IDENTITY zipCodeld: int NOT NULL (FK) (AK1.1) oityld: int NOT NULL (FK) (AK1.2) primaryR: bit NOT NULL autoTimestamp: timestamp NOT NULL
характеризуете подходящим почтовым индексом с помощью
Полная версия исходного кода для создания объектов БД наряду с версиями PDF-файла (Package Definition File — файл определения пакета) моделей данных доступны в www.wrox.com. Полное описание даже такой маленькой БД — весьма длинное и во многом повторяющееся.
Резюме Да, это была длинная глава, охватывающая большое количество понятий. Она переполнена информацией, которая является очень важной для каждого читателя. Понимание, как строить таблицы и как они реализуются — основа знаний каждого проектировщика БД. Мы взяли нашу логическую модель и исследовали каждую сущность, чтобы определить, насколько возможно их реализовать. Особо были рассмотрены подтипы, поскольку с ними могут быть проблемы. Мы также рассмотрели возможные отклонения от наших строгих правил нормализации в особых случаях (хотя мы боролись с этим в максимально возможной степени), и очистили структуры нашей БД от ошибок, которые имеются в модели. После того, как мы удостоверились, что имеем именно ту модель, которую могли бы реализовать, мы попристальней взглянули на таблицы SQL Server, прошлись по ограничениям таблиц и рассмотрели синтаксис операторов CREATE TABLE и ALTER TABLE. Были рассмотрены также правила обозначения объектов, столбцов, индексов и ограничений внешнего ключа.
355
Глава 10 Два наиболее важных раздела этой главы были посвящены типам данных и индексам. Когда мы реализуем наши таблицы, очень важно понять концептуальный способ, которым наши данные будут фактически размещены в SQL Server. Закончен этот процесс определением первичных ключей и добавлением, по крайней мере, по одному вторичному ключу на таблицу и столбцу типа timestamp для целей оптимистической блокировки. Окончательная модель и описание, как реализовать систему, доступны на Web-сайте Wrox Press. В следующей главе мы завершим задачу реализации OLTP-системы, обеспечив бизнес-правила, необходимые, чтобы содержать данные в нашей БД настолько чистыми, насколько возможно.
356
Обеспечение целостности данных Введение В этой главе мы займемся предикатами и доменами, которые были определены во время проектирования. Когда мы обсуждаем целостность данных, то рассматриваем применение основных правил к данной таблице или набору таблиц, которые гарантируют, что размещенные данные всегда полноценны. Поддержание нашей БД в состоянии, когда данные всегда соответствуют нашим исходным техническим требованиям проекта, является основой большинства разработок. Независимо от того, насколько хорошо или плохо разработаны таблицы, как это воспринимает пользователь, БД всегда будут определяться тем, насколько хорошо защищены данные. При разработке стратегии целостности данных мы должны рассмотреть несколько различных сценариев: • • • •
пользователи, применяющие традиционные клиентские средства; пользователи, применяющие настраиваемые средства работы с данными типа Microsoft Access; программы, которые импортируют данные из внешних источников; необработанные запросы, выполняемые администраторами данных, чтобы выявить проблемы, связанные с ошибками пользователя.
Каждый из них обладает различными проблемами по отношению к нашей схеме целостности и, что более всего важно, каждый из этих сценариев (может быть, за исключением второго) составляет часть каждой СУБД, которую мы разрабатываем. Чтобы лучше обеспечить каждый сценарий, мы должны защитить данные, используя механизмы, которые работают независимо от пользователя и которых нельзя случайно обойти. Имеются четыре возможных пути, как мы можем использовать код, чтобы обеспечить непротиворечивые данные:
Глава 11 •
Типы данных и ограничения являются простыми непосредственными средствами, которые составляют неотъемлемую часть определения таблицы. Они являются очень быстрыми средствами, которые требуют немного кодирования, и' их использование может дать существенное улучшение функционирования. Такие ограничения включают действия типа определения диапазонов данных в столбцах (например, элементы столбца должны быть больше, чем 10, или — если значение в столбце 1 равно 1, то значение столбца 2 больше 1). Это — лучшее место для защиты данных, поскольку большая часть работы выполняется с помощью SQL Server, и пользователь не сможет обойти это при любой ошибке во внешней программе.
•
Триггеры отличаются от ограничений тем, что они являются частями кода, который вы добавляете к таблице (или таблицам). Этот код автоматически запускается всякий раз, когда происходит событие (события), которое (которые) вы определили для таблицы (таблиц). Они чрезвычайно гибки и могут обращаться с несколькими столбцами, несколькими строками, несколькими таблицами и даже несколькими БД. Как простой случай, рассмотрим ситуацию, где мы хотим гарантировать, что обновление значения выполняется в обеих таблицах, где находятся данные. Мы можем написать триггер, который запретит обновление, если оно не происходит в обеих таблицах. Это — второе лучшее место, чтобы защитить данные. Мы должны закодировать каждое правило триггера в T-SQL, но пользователь не сможет обойти триггер при любой ошибке во внешней программе.
•
Хранимые процедуры — части кода, которые размещены в БД и очень гибко могут работать с таблицами; например, использование различных бизнес-правил при различных обстоятельствах. Простой пример хранимой процедуры — которая возвращает все данные, находящиеся в данной таблице. В более сложном сценарии они могут использоваться, чтобы давать различные разрешения различным пользователям относительно работы с таблицами. Это — не лучшее решение для защиты данных, поскольку мы должны закодировать правила в какой-то процедуре, которая имеет доступ к данным. Однако так как она находится в центральном месте, мы можем использовать ее, чтобы реализовать правила, которые могут быть различными в разных ситуациях.
•
Выполняемый клиентом код — полезен в ситуациях, когда бизнес-правила необязательны или гибки по своей природе. Довольно общий пример, когда SQL Server формирует вопрос пользователю "Вы уверены, что желаете удалить эту запись?" и если вы просите удалить данные, он их удалит. Большинство результатов работы сервера выполнено в этой манере, оставляя программам пользователя возможность реализации гибких бизнес-правил и предупреждений. Имейте в виду, что приложения приходят и уходят, но данные должны быть всегда защищены.
При перемещении вниз по этому списку решения становятся менее желательными, но все же каждое имеет определенные выгоды, которые полезны в некоторых ситуациях. Все эти особенности будут рассмотрены в свое время, однако мы начнем с новой особенности в SQL Server 2000, которая является существенной при формировании ограничений, триггеров и хранимых процедур. Эта особенность — определяемые пользователем функции (User Defined Functions).
358
Обеспечение целостности данных
Примеры таблиц В примерах данной главы, предназначенных для иллюстрации различных концепций, мы будем использовать следующие таблицы: CREATE TABLE artist ( artistld int NOT NULL IDENTITY, name varchar(60), -- заметьте, что первичный ключ является кластерным, -- что будет рассмотрено позже в этой главе CONSTRAINT XPKartist PRIMARY KEY CLUSTERED (artistld) CONSTRAINT XAKartist name UNIQUE NONCLUSTERED (name)
INSERT VALUES INSERT VALUES GO
INTO ('the INTO ('the
artist(name) beetles') artist(name) who')
CREATE TABLE album ( albumld int NOT NULL IDENTITY, name varchar(60), artistld int NOT NULL, -- заметьте, что первичный ключ является кластерным, -- что будет рассмотрено позже в этой главе CONSTRAINT XPKalbum PRIMARY KEY CLUSTERED(albumld), CONSTRAINT XAKalbum_name UNIQUE NONCLUSTERED(name), CONSTRAINT artist$records$album FOREIGN KEY(artistld) REFERENCES artist
INSERT VALUES INSERT VALUES INSERT VALUES
INTO album (name, artistld) ('the white album', 1) INTO album (name, artistld) ('revolver', 1) INTO album (name, artistld) ('quadrophonia', 2)
Здесь создаются две таблицы. Первая таблица — artist (артист), содержащая два столбца: первый — artistld (идентификатор артиста), автоинкрементный целого типа, который не может принимать значение NULL, и второй — name (имя) типа varchar (60). Для этой таблицы задаются ограничения: XPKartist (первичный ключ таблицы artist), представляющее собой кластерный первичный ключ, использующее столбец artistld, и XAKartist_name (вторичный ключ — имя артиста), обеспечивающее некластерный столбец name, содержащий уникальные значения. В эту таблицу помещаются две записи, содержащие в столбце name строки "the beatles" (Битлз) и "the who" (Xy). Вторая таблица album (альбом) содержит три столбца: albumld (идентификатор альбома), автоинкрементный целого типа, который не может принимать значение NULL, второй — name (имя) типа varchar (60) и третий — artistld целого типа, который не может принимать значение NULL. Для этой таблицы задаются ограничения: XPKalbum (первичный ключ таблицы album), представляющее собой 359
Глава 11 кластерный первичный ключ, использующее столбец albumld, XAKalbum_name (вторичный ключ — название альбома), обеспечивающее некластерный столбец name, содержащий уникальные значения, и ограничение artist$records$album (артист записывает альбом), использующее столбец artistld для ссылки на таблицу artist. В эту таблицу помещаются три записи, у которых в столбцы name и artistld будут помещены записи: "the white album", 1; "revolver", 1 и "quadrophonia", 2.
Определяемые пользователем функции Определяемые пользователем функции (ОПФ) (User Defined Functions — UDF) могут применяться, чтобы гарантировать целостность данных, и (по моему мнению) являются одной из наиболее важных особенностей SQL Server 2000. Обычно ОПФ тесно связаны с хранимыми процедурами, но вместо того, чтобы быть средствами, в первую очередь предназначенными для корректировки данных и возвращения результатов, они являются T-SQL-программами, которые возвращают значение. Как мы увидим, компилятор SQL Server не позволит вам выполнить внутри ОПФ любой код, который модифицирует данные, и поэтому ОПФ может лишь использоваться для операций, выполняющих только чтение. Возвращаемое ОПФ значение может быть или простой скалярной величиной, или таблицей, или набором результатов. Что делает их сказочно полезными — то, что они могут быть вложены в запросы, таким образом сокращая нашу потребность в огромных запросах со многими строками повторяющегося кода. Единственная функция может содержать программы, которые могут вызываться из запроса по желанию. В оставшейся части этой главы и в следующей мы будем широко использовать ОПФ, чтобы решать задачи, которые в более ранних версиях SQL Server было или невозможно, или очень трудно решить. ОПФ имеют следующую основную структуру: CREATE FUNCTION [<имя_пользователя>.]<имя_функции> [<параметры>] - - точно т а к же, к а к у хранимых ) RETURNS <тип_данных> AS BEGIN [<оператры>] RETURN <выражение> END
процедур
При создании этих функций в противоположность хранимым процедурам имеется несколько очень важных различий в синтаксисе, которые мы должны помнить:
360
•
Мы используем CREATE FUNCTION вместо CREATE PROCEDURE.
•
Круглые скобки всегда требуются, независимо от того, имеются ли параметры.
•
Операторы RETURNS используются для объявления типа возвращаемых данных. Обратите внимание, что оператор RETURNS является обязательным для ОПФ в отличие от хранимой процедуры.
•
BEGIN и END требуются для скалярных функций и функций, состоящих из набора многих операторов. Имеются три разных версии ОПФ, которые мы рассмотрим ниже.
Обеспечение целостности данных Скалярные функции Скалярные функции возвращают единственное значение. Они могут использоваться в различных местах, чтобы реализовать простые или сложные функциональные возможности, не имеющиеся обычно в SQL Server. Наш первый пример скалярной функции — один из самых простых возможных примеров: увеличение значения целого числа. Функция просто берет единственный целый параметр, увеличивает его на единицу и затем возвращает это значение: CREATE FUNCTION i n t e g e r $ i n c r e m e n t ( SintegerVal i n t ) RETURNS i n t AS BEGIN SET @integerVal = @integerVal RETURN S i n t e g e r V a l END GO
+ 1
Обратите внимание, что я снова использовал для названия функции знак доллара. Я называю все функции, триггеры и хранимые процедуры в такой манере, чтобы отделить объект (в этом случае integer — целое число) от метода (в данном случае — increment — увеличение). Это — просто мое личное предпочтение. Мы можем выполнять эту функцию с помощью классического синтаксиса выполнения из предыдущих версий SQL Server:
I
DECLARE @integer int EXEC Sinteger = integer$increment @integerVal = 1 SELECT @integer AS value
... который возвратит: value
Теперь, когда мы создали функцию и выполнили ее, логично пожелать вызвать ее из оператора SELECT. Однако это более хитро, чем могло бы вначале показаться. Если мы попробуем выполнить следующий оператор: Щ SELECT integer$increment (1) AS value ... то получим следующее сообщение: Server: Msg 195, Level 15, State 10, Line 1 'integer$increment' is not recognized function name (Сервер: сообщение 195, уровень 15, состояние 10, строка 1 'integer$increment' — неопознанное имя функции)
361
Глава 11 ОПФ требует, чтобы вы определили владельца функции для ее успешного выполнения. §§§ SELECT dbo. integer$increment (1) AS value Теперь мы получим желаемый результат: value
Имеется еще одно замечание по использованию, о котором мы должны упомянуть. Вы не можете передавать названные пользователем параметры функции, как вы можете поступать с хранимыми процедурами. Так, следующие два метода вызова кода не будут работать:
I
SELECT dbo.integer$increment @integerVal = 1 AS value SELECT dbo.integer$increment (@integerVal = 1) AS value
Выполнение их даст: Server: Msg 170, Level 15, State 1, Line 1 Line 1: Incorrect syntax near 'integerVal'. Server: Msg 137, Level 15, State 1, Line 3 Must declare the variable '(SintegerVal'. (Сервер: сообщение 170, уровень 15, состояние 1, строка 1 Строка 1: Неправильный синтаксис в окрестности @integerVal. Сервер: сообщение 137, уровень 15, состояние 1, строка 3 Должна быть объявлена переменная @integerVal.) К сожалению, только первое сообщение об ошибке имеет какой-либо смысл (второе пробует передать логическое значение (QintegerValue = 1) в функцию). Странно, но мы можем использовать синтаксис "@parmname = " только с оператором EXEC, как видели в примере выше. Как заключительный пример, использующий функцию увеличения, мы три раза увеличим значение в одном и том же операторе (обратим внимание на вложенные вызовы): I
SELECT d b o . i n t e g e r $ i n c r e m e n t ( d b o . i n t e g e г $ i n c r e m e n t ( d b o . i n t e g e r $ i n c r e m e n t ( 1 ) AS v a l u e
что, как ожидается, возвратит value
Давайте рассмотрим теперь более сложный пример функции, который, как я уверен, многие читатели хотели бы реализовать: функция, которая берет строку и пишет прописную букву в начале каждого слова внутри этого текста:
362
Обеспечение целостности данных CREATE FUNCTION string$properCase ( OinputString varchar(2000) RETURNS varchar(2000) AS BEGIN — устанавливаем все буквы строчными SET @inputString = LOWER(SinputString) -- затем используем процедуру stuff (вставить) для первого символа SET @inputString = -- вставляем прописную букву следующего символа -- вместо строчной буквы STUFF(@inputString, I, 1, UPPER(SUBSTRING(OinputString, 1, 1))) — @ i - параметр цикла, инициализируемый значением 1 DECLARE @i int SET @i = 1 -- цикл от второго символа до конца строки WHILE @i < LEN(dinputString) BEGIN -- если символ - пробел IF SUBSTRING(dinputString, @i, 1) = ' ' BEGIN -- помещение прописной буквы на место следующего символа SET @inputString = STUFF(@inputString, @i + 1, 1, UPPER(SUBSTRING(@inputString, @i + 1, 1))) END -- увеличение параметра цикла SET @i = @i + 1 END RETURN SinputString END GO
Здесь в переменную QinputString (вводимая строка) типа varchar (2000) при обращении к этой функции помещается преобразуемая строка. Далее с помощью стандартной процедуры LOWER все буквы строки преобразуются в строчные. Затем первый символ строки преобразуется в прописную букву. Для этого с помощью стандартной функции SUBSTRING выделяется строка, начиная с первого символа и длиной в один символ (фактически выделяется один символ), которая с помощью стандартной функции UPPER преобразуется в прописную букву, которая опять-таки с помощью стандартной функции STUFF замещает первую букву строки. Далее организуется цикл просмотра всех символов с первого до предпоследнего (длина строки определяется с помощью стандартной функции LEN). Если очередной символ является пробелом, то следующий символ преобразуется в прописную букву точно так же, как и первый символ строки. Функция возвращает преобразованную строку. Итак, мы можем затем выполнить следующий оператор:
I
SELECT name, dbo.string$properCase(name) AS artistProper FROM artist
363
Глава 11
и получить старые названия и новые, name
artist
the beatles the who
The Beatles The Who
Хотя это и не совершенная функция для изменения строки добавлением заглавных букв, мы могли бы легко расширить ее на "специальные" случаи, которые могут возникнуть (подобно McCartney или MacDougall). Последний пример, который мы рассмотрим, — доступ к таблице альбомов, подсчет числа записей и возвращение числа записей. Что хорошо в этой функции — то, что вызывающий не будет знать, что осуществляется доступ к таблице, так как это сделано изнутри ОПФ. Если мы создадим следующую функцию: CREATE FUNCTION album$recordCount (
Эта функция возвращает число альбомов (в параметре count), находящихся в таблице album для конкретного артиста, определяемого идентификатором артиста (Qartistld). ... то выполнив ее с помощью единственного оператора SELECT: I
SELECT dbo.album$recordCount(1) AS albumCount FROM album
будет получено: albumCount 2 Однако если мы выполним ее следующим образом:
I
364
SELECT name, artistld, dbo.album$recordCount(artistld) AS albumCount FROM album
Обеспечение целостности данных будет возвращено: artistid
albumCount
1 2
2 1
ОПФ будет выполняться один раз для каждой строки. Это не проблема для такого маленького запроса, но выполнение станет проблемой, когда мы будем иметь большое число больших таблиц.
Встроенные функции формирования таблицы Встроенная функция формирования таблицы, также известная как функция формирования таблицы с единственным оператором, является более сложным вариантом скалярной функции, так как она возвращает таблицу, а не единственное значение. В следующем примере кода мы строим функцию, которая возвращает таблицу ключей альбомов для конкретного артиста. Мы должны обратить внимание на последовательность, с которой мы обращаемся к функции, возвращающей таблицу, когда набор результатов получается из процедуры. CREATE FUNCTION album$returnKeysByArtist ( @artistld int ) RETURNS TABLE WITH SCHEMABINDING AS RETURN( SELECT albumld FROM dbo.album WHERE artistid = @artistld)
Функция возвращает таблицу table, точнее те ее записи, у которых значение идентификатора записи artistid равно значению передаваемого в функцию параметра Qartistld. Теперь мы можем выполнить функцию. Но как? Это полностью отличается от любого из ресурсов, с которыми мы имели дело в SQL Server до настоящего времени. Мы не можем выполнить ее как процедуру: Щ EXEC dbo.album$returnKeysByArtist
@artistld = 1
Такой оператор возвратит ошибку: Server: Msg 2809, Level 18, State 1, Line 1 The request for procedure 'album$returnKeysByArtist' failed because 'albumSreturnKeysByArtist' is a function object. {Сервер: сообщение 2809, уровень 18, состояние 1, строка 1 Обращение к процедуре 'album$returnKeysByArtist' потерпело неудачу, потому что 'album$returnKeysByArtist' — функция.)
365
Глава 11
Что мы можем сделать, однако, так это подставить вызов функции формирования таблицы вместо таблицы в любом запросе. Поэтому мы выполним функцию подобно следующему: SELECT album.albumld, album.name FROM album JOIN dbo.album$returnKeysByArtist(1) AS functionTest ON album.albumld = functionTest.albumld
I
что даст следующее albumld
name the white album revolver
Является ли это лучше, чем обычное присоединение? Нет, конечно. Это не стандартная операция программирования БД, и она, вероятно, препятствовала бы оптимизатору запросов сформировать наиболее оптимальный код. Однако это — симпатичный свежий способ построить специальную часть кода, который мы можем добавлять к нашему набору средств, если имеем небольшие наборы данных, которые хотим использовать в наших запросах.
Связывание схемы В объявлении нашей функции, которую мы рассмотрели только что, содержится незнакомый термин: §§ WITH SCHEMABINDING
Связывание схемы — новая особенность, которую SQL Server предоставляет для функций и представлений, позволяя брать элементы, от которых зависит ваше представление (типа таблиц или представлений), и "связывая" их в это представление. Таким образом, никто не может сделать изменений в этих объектах, пока схема связывания представления не удалена. Это создает довольно общую проблему, которая возникает в первые же дни программирования системы клиент-сервер. Что произойдет, если изменится сервер? Как клиент будет работать при таком изменении? Например, в нашей созданной функции мы использовали таблицу альбомов. Если мы хотим удалить таблицу альбомов: К DROP TABLE album то получим следующую ошибку: Server: Msg 3729, Level 16, State 1, Line 2 Could not DROP TABLE 'album'. It is being referenced by object 'album$returnKeysByArtist'. {Сервер: сообщение 3729, уровень 16, состояние 1, строка 2 Нельзя удалить таблицу 'album. К ней нужно обращаться как к объекту 'album$returnKeysByArtist\)
Обратите внимание, что это — неплохая вещь. Действительно, данный пример демонстрирует встроенный механизм защиты SQL Server своих данных.
366
Обеспечение целостности данных А что, если мы хотим просто изменить таблицу? ОПФ отвергнет любую попытку изменить используемый столбец, хотя мы будем иметь возможность добавить столбец или изменить неиспользуемый. Чтобы проиллюстрировать это, мы добавим столбец и затем удалим его, чтобы оставить таблицу в первоначальном состоянии: ALTER TABLE album ADD description varchar (100) NULL ALTER TABLE album DROP COLUMN description ... что успешно будет выполнено. Однако если мы попытаемся удалить столбец, который используется функцией, например, a r t i s t i d :
I
ALTER TABLE album DROP COLUMN artistid
... то SQL Server знает, что этот столбец используется функцией, и сообщит нам: Server: Msg 5074, Level 16, State 3, Line 1 The object 'album$returnKeysByArtist' is dependent on column 'artistid 1 . ALTER TABLE DROP COLUMN artistid failed because one or more objects access this column. (Сервер: сообщение 5074, уровень 16, состояние 3, строка 1 Объект 'album$returnKeysByArtist' зависит от столбца 'artistid'. ALTER TABLE DROP COLUMN artistid терпит неудачу, потому что один или несколько объектов используют этот столбец.) Вы должны всегда использовать связывание схемы при создании системы, к которой будут обращаться клиенты. Очевидно, если вы собираетесь часто удалять и заново создавать таблицу, то связывание схемы не могло бы дать вам существенных преимуществ, но большинство таблиц в разветвленной БД будет редко, если когда-либо, изменяться, и если это произойдет, всегда благоразумно знать код доступа к ним. Не будет н и к а к и х проблем функционирования кроме скорости, с которой объект, использующий связывание схемы, может быть изменен. Если вы должны изменить таблицу или столбец, то можете: Q
выключить SCHEMABINDING, выполнить требуемую корректировку и снова вернуться назад;
•
удалить функцию, изменить таблицу или столбец (столбцы) и затем заново создать связывание схемы.
Объект может быть схемно-связанный, если выполняются следующие условия:
13 -1868
•
все используемые объекты схемно-связанные;
•
ко всем объектам можно получить доступ, используя две части названия ( o w n e r . o b j ectName) ;
•
все объекты находятся в одной и той ж е Б Д ;
Q
пользователь должен иметь соответствующие разрешения для обращения ко всем объектам в коде.
367
Глава 11 Например, если мы попробуем изменить нашу функцию следующим образом: ALTER FUNCTION album$returnKeysByArtist @artistld i n t RETURNS TABLE WITH SCHEMABINDING AS RETURN( SELECT a l b u m l d FROM album - - должно быть dbo.album WHERE a r t i s t l d - gartistld)
Это вызовет следующую ошибку: Server: Msg 4512, Level 16, State 3, Procedure album$returnKeysByArtist, Line 9 Cannot schema bind function 'album$returnKeysByArtist' because name 'album' is invalid for schema binding. Names must be two-part format and object cannot referevce itself. (Сервер: сообщение 4512, уровень 16, состояние 3, процедура album$returnKeysByArtist, строка 9 Нельзя использовать связывание схемы для функции "'album$returnKeysByArtist", потому что имя "album" не подходит для связывания схемы. Имена должны иметь формат из двух частей, а объект не может ссылаться на себя) Чтобы удалить связывание схемы, просто выполните ALTER FUNCTION без операции WITH SCHEMABINDING; например, возьмем первую функцию, которую мы создали: CREATE FUNCTION album$returnKeysByArtist Sartistld int RETURNS TABLE WITH SCHEMABINDING AS RETURN( SELECT albumld FROM dbo.album WHERE a r t i s t l d = Qartistld)
Чтобы изменить ее для запуска без связывания схемы, мы должны выполнить этот код следующим образом: CREATE FUNCTION album$returnKeysByArtist @artistld i n t RETURNS
TABLE
AS RETURN(
SELECT albumld FROM dbo.album WHERE artistld = @artistld)
368
Обеспечение целостности данных Функции со многими операторами, формирующие таблицы Если встроенные функции формирования таблицы представляли собой более сложные скалярные функции, то функции со многими операторами, формирующие таблицы, еще более сложны! Что делает их важными, — это то, что вы можете определять функции с неограниченным числом команд и строить единственную таблицу, содержание которой мы определяем в объявлении функции. В нашем примере мы возвратим все альбомы для конкретного артиста и зададим все названия прописными буквами: CREATE FUNCTION album$returnNamesInUpperCase ( @artistld i n t ) RETURNS @outTable t a b l e ( albumld i n t , name varchar(60) ) WITH SCHEMABINDING AS BEGIN — добавление всех альбомов в выходную таблицу INSERT INTO @outTable SELECT albumld, name FROM dbo.album WHERE artistld - @artistld — второй ненужный оператор UPDATE введен, чтобы показать, -- что это функция со многими операторами UPDATE @outTable SET name = UPPER(name) RETURN END GO
Когда мы выполним следующий оператор: Ц
SELECT * FROM dbo.album$returnNamesInUpperCase(1)
... то получим: albumld
name
1 2
THE WHITE ALBUM REVOLVER
Использование таких функций во многом похоже на использование функций формирования таблиц с единственным оператором, но они открывают множество новых возможностей. Операции, которые ранее выполнялись, используя временные таблицы с собственным использованием кода, могут теперь выполняться в функции, причем они могут многократно использоваться и без кодирования беспорядочных временных таблиц.
Что определяемые пользователем функции не могут делать Имеется несколько моментов, которые мы не можем выполнять с помощью ОПФ. Первое, на что следует обратить внимание, — имеется несколько важных встроенных в SQL Server функций, которые не могут быть включены в ОПФ. Причины этого довольно просты: они не 369
Глава 11 являются детерминированными. (Детерминированные функции — это функции, которые при выполнении с конкретным набором параметров всегда возвращают тот же самый набор значений.) Аргументация этого очевидна. ОПФ может использоваться для построения вычисляемых столбцов или столбцов в представлениях, и каждый из них может быть индексирован. Индексация может происходить только тогда, когда для столбцов можно гарантировать, что возвращаются одни и те же значения при любом вызове функции. Следующая таблица перечисляет недетерминированные функции, которые не могут использоваться в ОПФ. @@CONNECTIONS
@@TOTAL_ERRORS
(число соединений)
(общее число ошибок дискового чтения/записи)
@@IDLE (время простоя сервера с момента его последнего запуска)
@@TOTAL_WRITE
(число запросов на запись на диск)
@@CPU_BUSY (время ЦП, занятое сервером с момента последнего запуска сервера)
@@TOTAL_READ
(число запросов на чтение с диска)
@@IO_BUSY (время операций чтения/записи с момента последнего запуска сервера)
@@МАХ CONNECTIONS
(максимальное число одновременных соединений)
@@PACK_RECEIVED
@@РАСК SENT
@@PACKET ERRORS
@@TIMETICKS
(число пакетов, прочитанных сервером из сети)
(число пакетов, посланных сервером в сеть)
(число ошибок передач пакетов)
(число микросекунд на тик)
TEXTPTR (указатель на текст типа text, ntext или image)
NEWID (создание значения уникального идентификатора типа uniqueidentifier)
RAND (случайная величина)
GETDATE (текущие системные дата и время)
GETUTCDATE
(текущее время по Гринвичу)
1
Мы не будем вносить в список все функции SQL Server и их детерминированность. Это полностью рассмотрено в SQL Server Books Online, раздел Deterministic and Nondeterministic Functions (Детерминированные и недетерминированные функции). Второй ключевой момент заключается в том, что состояние сервера и все данные должны быть точно одними и теми же, когда вы выполняете операторы. Следовательно, любые изменения, которые простираются на объекты, не являющиеся внутренними для функции, типа возвращаемых результатов, данных или структур данных, курсоров, которые не локальны для функции, или любая другая корректировка состояния системы, недопустимы. Чтобы проиллюстрировать это, давайте создадим функцию, которая обновляет все альбомы в наших таблицах на "the Beatles" и затем возвращает число величин, которые мы обновили.
370
Обеспечение целостности данных CREATE FUNCTION album$setAHToTheBeatles
RETURNS int AS BEGIN DECLARE growcount int UPDATE album SET artistld m 1 WHERE artistld <> 1 RETURN END
При компиляции возникнет ошибка из-за оператора UPDATE: Server: Msg 443, Level 16, State 2, Procedure album$setAIIToTheBeatles, Line 9 Illegal use of 'UPDATE' within a FUNCTION. (Сервер: сообщение 443, уровень 16, состояние 2, процедура album$setAUToTheBeatles, строка 9 Неправильное использование UPDATE внутри функции)
Ограничения Как мы видели раньше, ограничения — ресурсы SQL Server, которые используются для автоматического обеспечения целостности данных. Одна из основных причин использования ограничений вместо триггеров — это то, что оптимизатор запросов может использовать их для оптимизации запросов. Например, если вы сообщаете SQL Server, что некоторая переменная может иметь значения только между 5 и 10, и затем выполняете запрос для всех строк, которые имеют значение этой переменной большее, чем 100, оптимизатор будет знать даже без какого-либо просмотра данных, что нет никаких строк, которые удовлетворяют критерию. В SQL Server имеется пять различных видов ограничений: •
NULL-ограничения — NULL-ограничения фактически не являются ограничениями, но обычно рассматриваются таковыми.
•
Первичные ключи и уникальные ограничения — чтобы удостовериться, что наши ключи содержат только уникальные комбинации значений.
•
Внешние ключи — используются, чтобы быть уверенными, что любые мигрирующие ключи имеют только подходящие значения, которые соответствуют ключам, на которые они ссылаются.
•
Значения по умолчанию — ограничения, связанные со значениями по умолчанию, используются, чтобы установить приемлемое значение по умолчанию для столбца.
•
Проверка — ограничения, которые используются, чтобы ограничить значения, которые могут быть введены в отдельный столбец или всю таблицу.
Так как первые три ограничения рассмотрены ранее в главе 10, здесь мы сосредоточим наше внимание на последних двух.
371
Глава 11 Ограничения, связанные со значениями по умолчанию Я считаю, что значения по умолчанию являются очень важной частью защиты данных в том случае, если пользователь не знает, какое значение вводить в таблицу; значение может быть опущено, и установлено ограничение по умолчанию, определяющее подходящее предварительное значение. Хотя мы и можем предохранить пользователя от ввода неправильного значения, но не можем предотвратить его от ввода нелогичных значений. Например, предположим, что опытный пользователь SQL Server должен ввести значение в столбец, называемый boogleFl, который является полем типа b i t (boogleFl был выбран, потому что он означает для нас столько же, сколько некоторые из полей, которые имеют для нас смысл, значат для других пользователей). Не зная смысла boogleFl, но вынужденный создать новую запись, пользователь может выбрать значение 0. Использование значений по умолчанию дает пользователям пример вероятного значения для столбца. Нет никаких проблем при задании значений по умолчанию для полей и это очень удобно для пользователя! Команда для добавления значения по умолчанию, имеет следующую структуру: ALTER TABLE <имя_столбца> ADD [ CONSTRAINT <имя_значения_по_умолчанию> DEFAULT <константное_выражение> FOR <имя столбца>
I
Мы будем использовать структуру обозначения, схожую с нашим предыдущим обозначением, расширяя имя значения по умолчанию, используя следующий образец: df1^имя_таблицы>$<имя_столбца>$<общий_тип_данных>$<описание> ] Это приводит к следующему альтернативному синтаксису: ALTER TABLE <имя_столбца> ADD [ CONSTRAINT df11<имя_таблицы>$<имя_столбца>$<общий_тип_данных>$<описание> ] DEFAULT <константное_выражение> FOR <имя_столбца>
Конечно мы должны знать к данному моменту, что<имя_таблицы> и <имя_столбца> определяют место заполнения, но что означают другие части?
372
•
<общий_тип_данных> — Мы должны определить общий тип данных. Причиной этого является вопрос удобства. В нашем перечне общих объектов мы создадим группу определений ограничений, используемых по умолчанию, которые можем использовать неоднократно. Например, если бы мы определяли задание ограничения столбца типа i n t , равного 1, нам не потребовалось бы другого ограничения, используемого по умолчанию, задаваемого для типа t i n y i n t (в отличие от случая поля типа b i g i n t ) .
•
<описание> — Это — общее описание цели значения по умолчанию, например, если ограничение используется, чтобы поддерживать все данные в столбце типа i n t больше нуля, мы могли бы называть это greaterThanZero (больше, чем нуль). Это описание сделает имена значений по умолчанию более понятными. Не только это, но и когда рассматривается "нормализованный", многократного используемый пакет с общим типом данных, мы будем использовать определение значения по умолчанию, которое будет работать во всех ситуациях, где нам нужны одни и те же значения по умолчанию.
Обеспечение целостности данных Q
<константное_выражение> — скалярное выражение, которое могло бы быть или литеральным значением, или NULL, или функцией. Мы рассмотрим несколько различных сценариев для константных выражений в следующем разделе.
Литеральные значения по умолчанию Литерал — просто единственное значение того же самого типа данных, которое не требует никакой трансляции в SQL Server. Например, следующая таблица содержит примеры значений по умолчанию для нескольких типов данных:
Тип данных
Возможное значение по умолчанию
int
1
varchar (10)
'Value'
binary(2)
0x0000
datetime
'12/31/2000'
Так, в нашей БД, скажем, мы хотим использовать по умолчанию все альбомы "the Beatles", если пользователь не определяет другого; тогда мы могли бы сделать выбор из таблицы a r t i s t (артист), чтобы узнать, каково значение a r t i s t l d (идентификатор артиста) для "the Beatles": Щ SELECT * FROM a r t i s t artistld
name
1 2
t h e beatles the who
Таким образом, мы определяем следующее значение по умолчанию:
\
ALTER TABLE album ADD CONSTRAINT d f l t A l b u m $ a r t i s t I d $ i n t e g e r $ o n e DEFAULT (1) FOR a r t i s t l d
Небольшое предупреждение относительно литеральных значений по умолчанию. Лучше не давать литеральное значение по умолчанию столбцу, который является частью уникального индекса. Причина в том, что любой столбец, который имеет набор значений по умолчанию, вряд ли будет содержать уникальные значения. Аналогично и в любом индексе большое число повторений не будет удачным при функционировании.
Функции Альтернативой жесткому кодированию значения по умолчанию является использование функции, чтобы получить значение по умолчанию. Давайте рассмотрим наш предыдущий пример, когда мы хотели установить артиста по умолчанию в "the Beatles". Чтобы сделать это, мы могли бы добавить к таблице a r t i s t поле def a u l t F l (флаг по умолчанию) со значением по умолчанию 0. Мы могли бы сделать это, если не хотим иметь только единственное значение, которое равно 1, плюс мы хотим обеспечить, чтобы пользователь мог выбирать запись по умолчанию.
373
Глава 11
I
ALTER TABLE artist ADD defaultFl bit NOT NULL CONSTRAINT dfltArtist$defaultFl$boolean$false DEFAULT (0)
Теперь установим строку "the Beatles" в 1:
I
UPDATE a r t i s t SET d e f a u l t F l = 1 WHERE name = ' t h e
beatles'
Таблица теперь включает поле, где пользователь может выбирать, какое требуется значение в качестве используемого по умолчанию; так что мы должны делать дальше? Для каждой версии SQL Server, вплоть до 2000, вы могли создавать запрос, который возвращает одну строку, подобную этой, содержащую единственное скалярное значение. Однако это сформирует сообщение об ошибке, так что для выполнения запроса мы создаем нашу собственную функцию: CREATE FUNCTION
artist$returnDefaultld
RETURNS int WITH SCHEMABINDING AS BEGIN DECLARE @artistld int SELECT Sartistld = artistld FROM dbo.artist WHERE defaultFl = 1 RETURN @artistld END GO
... и теперь, если мы выполним оператор: Щ
SELECT dbo.artist$returnDefaultld() AS defaultValue
... то получим: defaultValue 1 Итак, для нашего решения мы можем записать следующий код:
I 374
ALTER TABLE album ADD CONSTRAINT dfltAlbum$artistId$function$useDefault DEFAULT dbo.artist$returnDefaultld() FOR artistld
Обеспечение целостности данных Теперь мы имеем точную функциональность, которую пытаемся получить из подзапроса, и, сделав это, создаем функцию, которую может использовать любой другой процесс. Чтобы проверить это, выполним следующий оператор INSERT, опустив поле a r t i s t l d : INSERT INTO album(name) VALUES ('rubber soul') -- rubber soul — "резиновая душа" SELECT album.name AS albumName, artist.name AS artistName FROM album JOIN artist ON album.artistld = artist.artistld WHERE album.name = 'rubber soul'
Результат, как можно видеть, содержит строку, которую мы создали: albumName
artistName
rubber soul
the beatles
Ограничения по умолчанию в SQL Server 2000 — чрезвычайно мощное средство для задания начальных значений полям. Так как вы можете устанавливать значения по умолчанию в одной таблице из другой, есть несколько ограничений для их использования.
Ограничения-проверки Ограничения-проверки используются, чтобы запретить ввод неподходящих данных в наши таблицы. Они выполняются после ограничений по умолчанию (так что вы не можете задать значение по умолчанию, чтобы заменить неподходящее значение) и выполнения триггеров INSTEAD OF (вместо). Они не могут влиять на сами значения, которые мы пытаемся поместить в таблицу, и используются лишь для того, чтобы проверить допустимость используемых значений. Имеются два разных типа ограничений-проверок: ограничение столбца и ограничение таблицы. Ограничение-проверка столбца относится к единственному столбцу и используется только тогда, когда в корректировке участвует отдельный столбец. Ограничение-проверка таблицы используется независимо от того, какие упоминаются* столбцы. К счастью для нас мы не должны волноваться относительно объявления ограничения, является ли это ограничением столбца или ограничением таблицы. Когда SQL Server выполняет ограничение, он анализирует, требуется ли проверять более одного столбца, и применяет соответствующее ограничение. В каждом из наших примеров ограничений мы обсудим, является ли это ограничением столбца или таблицы. Основной синтаксис оператора ALTER TABLE, который касается ограничений: ALTER TABLE <имя_таблицы> [WITH CHECK | WITH NOCHECK] ADD [CONSTRAINT спк<имя таблицы>$[<имя столбца>$<общий_тип_данных>$]<использование>] CHECK <условия_поиска>
Часть синтаксиса здесь не знакома, поэтому будет объяснение новых частей. Когда вы создаете ограничение-проверку, задание WITH CHECK (используется по умолчанию) дает вам удобный случай решить, нужно или нет проверять существующие данные в таблице. WITH NOCHECK оставляет неправильные существующие данные, но не будет позволять новым значениям нарушать требования ограничения. Иногда задание WITH NOCHECK может быть необходимо, если вы добавляете ограничение к существующей таблице, где нереально проверить все существующие данные, но вы хотите проверять все последующие вводимые данные. Лучше избегать использования NOCHECK, если только это не является абсолютно необходимым. 375
Глава 11 В то время как понятно, на что ссылается <имя_столбца>, то почему оно необязательно? Это ведь просто средство, чтобы включить имя столбца, который вы пробуете защитить, в название ограничения. Однако когда мы имеем дело с ограничением, работающим с несколькими столбцами, мы не должны включать имя столбца в название ограничения. Значение <общий_тип_данных> — просто описание класса типов данных, с которым могло бы работать ограничение. Когда это связано с ограничением-проверкой одного столбца, нет никакой причины, почему мы должны использовать различный код дляvarchar (10) или char (10) или даже char (100), если просто хотим гарантировать, что значение входной величины должно быть больше, чем пять символов. Вместо этого мы можем использовать ссылку на строку в нашем коде, чтобы обозначить все эти типы данных. Это, конечно, невозможно в случае проверки таблицы. Значение <использование> будет в основном маленькой строкой текста, чтобы объяснить, для чего используется ограничение. Например, если проверка должна выяснить, находится ли значение между нулем и десятью, мы поместим zeroThroughTen. Значение <условия_поиска> аналогично оператору WHERE обычного запроса SELECT без возможных подзапросов. Это может быть или системой доступа, или ОПФ и использует имя или имена столбцов таблицы. Однако оно не может содержать обращение к любой другой таблице или к любой другой строке, кроме текущей строки. Если обновляется несколько строк, ограничение имеет дело с каждой строкой индивидуально. Результатом части <условие_поиска> должно быть логическое выражение только с двумя возможными значениями TRUE или FALSE. Обычное ограничение-проверка, которое мы будем добавлять к большинству строк ( v a r c h a r , c h a r и т. д.), позволяет предотвратить ввод пустых данных. Например, в нашей таблице album мы имеем столбец name. Этот столбец не допускает значения NULL, так что пользователь должен ввести что-то, но что если будет введен один символ пробела? I
INSERT INTO album (name, artistld) VALUES (' ', 1)
Если бы мы позволили это в нашей БД, мы могли бы получить пустую запись, потому что возможна ситуация, когда пользователь введет такую строку, сделав ошибку в задании названия альбома. Во второй раз ввод пробела вместо названия альбома приведет к появлению ошибки: Server: Msg 2627, Level 14, State 2, Line 1 Violation of UNIQUE KEY constraint 'XAKalbumjiame'. Cannot insert duplicate key in object 'album'. The statement has been terminated. (Сервер: сообщение 2627, уровень 14, состояние 2, строка 1 Нарушение ограничения UNIQUE KEY (уникальный ключ) 'ХАКalbum_патё'. Нельзя ввести двойной ключ в объект 'album'. Оператор был удален.) Однако мы можем иметь неуникальное ограничение на связанный столбец, подобно полю d e s c r i p t i o n (описание) или n o t e s (примечание), где могли бы иметь много пустых вхождений. По этой причине мы могли бы добавить следующее ограничение, чтобы предотвратить повторное появление пустых столбцов: 376
Обеспечение целостности данных ALTER TABLE album WITH CHECK ADD CONSTRAINT chkAlbum$name$string$noEmptyString -- урезание значения имени, добавляя звездочки, чтобы обратиться -- к значениям более безопасно, чем только проверка длины величины, — возвращаемой функцией RTRIM, которое даст NULL, если название - NULL, — и единственный символ в противном случае. Это делает функция COALESCE. CHECK (( '*' + COALESCE(RTRIM(name), ' ') + '*') <> '**')
Здесь RTRIM — функция, которая удаляет из аргумента завершающие пробелы. Окончательная функция CHECK здесь использует операцию COALESCE, чтобы обеспечить в случае, когда нет никаких символов, отличных от пробела, возвращение строки нулевой длины, а если введено значение NULL, то это значение и возвращается. Конечно, мы предполагаем, что уже ввели значение, которое конфликтует с нашим ограничением, так что получим следующее сообщение об ошибке: Server: Msg 547, Level 16, State 1, Line 1 ALTER TABLE statement conflicted with COLUMN CHECK constraint 'chkAlbum$name$string$noEmptyString'. The conflict occured in database 'master', table 'album', column 'name'. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор ALTER TABLE противоречит ограничению COLUMN CHECK ''chkAlbum$name$string$noEmptyString'. Конфликт произошел в БД 'master, таблица 'album, столбец 'пате'.) Если м ы определим ограничение, используя WITH NOCHECK: ALTER TABLE album WITH NOCHECK ADD CONSTRAINT chkAlbum$name$string$noEmptyString -- урезание значения имени, добавляя звездочки, чтобы обратиться -- к значениям более безопасно, чем только проверка длины величины, -- возвращаемой функцией RTRIM CHECK (( '*' + COALESCE (RTRIM (name) , ' ') + '*') <> '.**')
Оператор будет добавлен к определению таблицы, хотя NOCHECK означает, что неправильные значения останутся в таблице. Однако при любой корректировке, когда оператор обращается к полю, будет запускаться ограничение-проверка. Далее вы пробуете установить одно из недопустимых значений в таблицу, которое вызовет ошибку.
I
UPDATE album SET name == name WHERE name = ' '
Это даст сообщение об ошибке:
Server: Msg 547, Level 16, State 1, Line 1 UPDATE statement conflicted with COLUMN CHECK constraint 'chkAlbum$name$string$noErnptyString'. The conflict occured in database 'master', table 'album', column 'name'. The statement has been terminated. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор UPDATE противоречит ограничению COLUMN CHECK 'chkAlbum$name$string$noEmptyString\ Конфликт произошел в БД 'master, таблица 'album', столбец 'пате'. Оператор удален.) 377
Глава 11 В нашем следующем примере мы рассмотрим способы включения в ОПФ кода, чтобы выполнять и простые, и сложные проверки. В случае функции, которая проверяет пустые строки, мы можем создать нашу собственную ОПФ следующим образом: CREATE FUNCTION string$checkForEmptyString ( @stringValue varchar(8000) ) RETURNS bit AS BEGIN DECLARE @logicalValue bit IF ('*' + COALESCE(RTRIM(@stringValue), ' ' ) + ' * ' ) = ' * * ' SET @logicalValue = 1 ELSE SET @logicalValue RETURN S l o g i c a l V a l u e
- 0
END
А затем сформируем следующее ограничение: ALTER TABLE album WITH NOCHECK ADD CONSTRAINT chkAlbum$name$string$noEmptyString -- урезание значения имени, добавляя звездочки, чтобы обратиться -- к значениям более безопасно, чем только проверка длины величины, -- возвращаемой функцией RTRIM CHECK (dbo.string$checkForEmptyString(name) = 0)
Теперь, если метод когда-либо изменится, это скрыто в пределах функции, и будет изменяться только в единственном месте. Большинство ограничений-проверок будет состоять из простой задачи проверки формата единственного поля. В следующих двух примерах мы будем использовать ОПФ, чтобы обеспечить общие функциональные возможности проверки диапазонов и сложные правила контроля, которые реализуют сложные правила и которые было бы трудно закодировать, используя непосредственно SQL-запрос. В первом примере мы собираемся добавить столбец с номером каталога к таблице album, учитывая, что номера каталога — алфавитно-цифровые значения следующего формата: <цифра><цифра><цифра><цифра><цифра><буква_или_цифра><буква_или_цифра><буква_или цифра>-<цифра><цифра> Примером такого номера каталога мог бы быть '433-43ASD-33' с символами в строке, которые являются обязательными. Итак, мы определим ОПФ, называемую album$catalogNumberValidate, которая будет включать проверку значения catalogNumber (номер каталога). Эта функция будет иметь две цели, а именно, проверку столбца в ограничении-проверке и проверку любого количества вставок строк и удалений данных. Наша функция возвратит значение типа b i t — 1, 0 или NULL для величины NULL, которые мы можем использовать для проверки данных. Первый шаг должен добавить наш новый столбец в БД.
378
Обеспечение целостности данных ALTER TABLE album ADD catalogNumber char (12) NOT NULL -- временное значение по умолчанию, позволяющее создать столбец — NOT NULL при первоначальном создании CONSTRAINT tempDefaultAlbumCatalogNumber DEFAULT ('111-11111-11') -- удаление его, потому что это - не подходящее значение по умолчанию ALTER TABLE album DROP tempDefaultAlbumCatalogNumber
Затем м ы создадим нашу функцию, которая будет использоваться для проверки значений столбца album. catalogNumber: CREATE FUNCTION album$catalogNumberValidate ( @catalogNumber char (12) ) RETURNS bit AS BEGIN DECLARE @logicalValue bit -- проверка, удовлетворяет ли значение установленной маске IF @catalogNumber LIKE ' [0-9] [0-9] [0-9]-[0-9] [0-9] [0-9a-z] [0-9a-z] [0T9a-z]-[0-9] [0-9]' SET @logicalValue = 1 -- да, удовлетворяет ELSE SET @logicalValue = 0 -- нет, не удовлетворяет -- заметим, что мы не можем просто написать RETURN 1 или RETURN 0, -- так как функция может иметь только одну точку выхода RETURN @logicalValue END
Проверим, работает ли ограничение.
I
IF dbo.album$catalogNumberValidate('433-llqww-33') = 1 SELECT 'Yes' AS result ELSE SELECT 'NO' AS result
Этот оператор дает: result Yes Все, что осталось, — это построить функцию на основе ограничения-проверки: ALTER TABLE album ADD CONSTRAINT chkAlbum$catalogNumber$function$artist$catalogNumberValidate -- размещаем это прямо здесь, включая все необходимое для функции CHECK (dbo.album$catalogNumberValidate(catalogNumber) = 1)
379
Глава 11
... и проверить новое ограничение; мы видим, что следующий неправильный номер каталога даст системную ошибку: UPDATE album SET catalogNumber = ' 1 ' WHERE number • ' t h e white
I
album'
что возвратит Server: Msg 547, Level 16, State 1, Line 1 UPDATE statement conflicted with COLUMN CHECK constraint 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate'. The conflict occured in database 'master', table 'album1, column 'catalogNumber'. The statement has been terminated. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор UPDATE конфликтует с ограничением COLUMN CHECK 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate. Конфликт произошел в БД 'master, таблице 'album, столбце 'catalogNumber. Оператор удален.) Выполнение его с правильными величинами будет вполне успешным: UPDATE album SET catalogNumber = '433-43ASD-33'
I
WHERE name = ' t h e white
album'
В качестве второго примера добавим столбец к таблице a r t i s t , который будет содержать маску LIKE. Обратите внимание, что требуется маска значительно больше, чем размер фактического столбца, так как некоторые из возможных масок используют несколько символов, чтобы указать единственный символ. Мы должны также обратить внимание, что это — varchar, так как использование переменных типа char проблематично в маске LIKE из-за добавления пробелов.
I
ALTER TABLE artist ADD catalogNumberMask varchar(100) NOT NULL — здесь приемлемо иметь маску в виде процента CONSTRAINT dfltArtist$catalogNumberMask$string$percent DEFAULT ('%')
Затем сформируем функцию, которая будет осуществлять доступ к столбцу, чтобы проверить, что наше значение соответствует маске: ALTER FUNCTION album$catalogNumberValidate ( @catalogNumber char (12), @artistld int -- теперь основываемся на поле artistld ) RETURNS bit AS BEGIN DECLARE @logicalValue bit, CatalogNumberMask varchar(100)
380
Обеспечение целостности данных SELECT @cata!ogNumberMask = catalogNumberMask FROM artist WHERE artistld = @artistld IF ScatalogNumber LIKE @catalogNumberMask SET @logicalValue = 1 ELSE SET @logicalValue - 0 RETURN SlogicalValue END GO
Теперь дадим столбцу a r t i s t правильные маски: UPDATE artist SET catalogNumberMask = '[0-9][0-9][0-9]-[0-9][0-9][0-9a-z][0-9a-z][0-9a-z]-[0-9][0-9]' WHERE a r t i s t l d = 1 — Б и т л з UPDATE artist SET catalogNumberMask = ' [ a - z ][ a - z ][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] [0-9]' WHERE a r t i s t l d = 2 - - X y
и протестируем работу функции следующим образом: IF dbo.album$catalogNumberValidate('443-43ASD-33', SELECT 'Yes' AS r e s u l t ELSE SELECT 'No' AS r e s u l t
1) = 1
IF d b o . a l b u m $ c a t a l o g N u m b e r V a l i d a t e ( ' a a l l l l l l l l l l ' , SELECT 'Yes' AS r e s u l t ELSE SELECT 'NO' AS r e s u l t GO
2) = 1
Теперь добавим ограничение к таблице: ALTER TABLE album ADD CONSTRAINT chkAlbum$catalogNumber$function$artist$catalogNumberValidate CHECK (dbo.album$catalogNumberValidate(catalogNumber, a r t i s t l d )
= 1)
Однако наши данные не подходят: Server: Msg 547, Level 16, State 1, Line 1 ALTER TABLE statement conflicted with TABLE CHECK constraint 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate'. The conflict occured in database 'master1, table 'album'. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор ALTER TABLE конфликтует с ограничением TABLE CHECK 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate\ Конфликт произошел в БД 'master, таблице 'album'.) 381
Глава 11 Мы могли бы использовать вариант NOCHECK, так как это будет означать, что любые существующие данные могут быть неподходящими, когда мы вводим ограничение, в то время как любые новые данные будут автоматически проверяться. Однако в этом примере есть проблема, связанная с тем, что ограничение не защитит пользователя при изменении маски. Если это существенно, мы могли бы реализовать таблицу, основанную на времени, где можно установить, что маска номера каталога будет иметь некоторое значение для даты публикации в период с 1 января 1970 года по 31 декабря 1995 года и другое значение в период с 1 января 1996 года по настоящее время. В нашем примере мы не будем использовать параметр NOCHECK, только для того, чтобы показать вам, как легко найти отклонения в данных. Следующий запрос выявит проблемы в таблице: - - чтобы найти, где наши данные не соответствуют ограничению, - - мы запускаем следующий запрос: SELECT dbo.album$catalogNumberValidate(catalogNumber, artistld) AS validCatalogNumber, artistld,
name, catalogNumber
FROM album WHERE dbo.album$catalogNumberValidate(catalogNumber,
artistld)
<> 1
Он дает нам список строк с неправильными данными. Используя подобный код для наших предыдущих примеров, мы можем исправить ошибки и затем добавить ограничение. Попытка добавить неправильное значение:
I
UPDATE album SET catalogNumber = ' 1 ' WHERE name - ' t h e w h i t e album'
возвратит ошибку Server: Msg 547, Level 16, State 1, Line 1 UPDATE statement conflicted with COLUMN CHECK constraint 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate'. The conflict occured in database 'master', table 'album', column 'catalogNumber1. The statement has been terminated. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор UPDATE конфликтует с ограничением COLUMN CHECK 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate\ Конфликт произошел в БД 'master, таблице 'album , столбец 'catalogNumber. Оператор удален.) Теперь мы можем попытаться скорректировать оператор: UPDATE album
I
SET catalogNumber = '413-43ASD-33' WHERE name = ' t h e white album' - - артист - Битлз
Используя такие подходы, мы можем формировать код проверки любой отдельной строки для наших таблиц.
382
Обеспечение целостности данных Однако в этом способе кодирования имеется недостаток. Как мы сказали предварительно, каждая ОПФ будет выполняться каждый раз для каждой строки и каждого столбца, которые были изменены при корректировке, а выполнение многократных проверок может привести к ухудшению работы. Альтернативный способ, который мы представим позже, состоит в том, чтобы использовать триггер для проверки всех изменений таблицы в единственном запросе. Несмотря на недостаток, этот способ решения проблемы проверки строки очень прост, понятен и легок в обслуживании. Однако если наши ограничения-проверки сложны, требуют доступ ко многим таблицам, то функционирование может оказаться неэффективным и использование триггера будет более предпочтительно.
Обработка ошибок, вызванных ограничениями Обработка ошибок — одна из наиболее важных частей написания любого оператора, изменяющего данные в SQL Server. Всякий раз, когда оператором нарушаются требования ограничения, как мы видели, формируется сообщение об ошибке. SQL Server обеспечивает нас простейшими средствами обработки ошибок, чтобы определить, что должно произойти в операторе, и мы здесь их рассмотрим. На протяжении этой главы мы выполняли операторы, чтобы доказать, что ограничения выполняются. Когда ограничения нарушаются, мы получаем сообщения об ошибке, подобные следующему: Server: Msg 547, Level 16, State 1, Line 1 UPDATE statement conflicted with COLUMN CHECK constraint 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate. The conflict occured in database 'master', table 'album', column 'catalogNumber'. The statement has been terminated. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор UPDATE конфликтует с ограничением COLUMN CHECK 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate'. Конфликт произошел в БД 'master, таблице 'album, столбец 'catalogNumber. Оператор удален.) Это сообщение об ошибке, которое видит анализатор запроса (Query Analyzer) и которое может видеть приложение пользователя. Сообщение об ошибке состоит из нескольких основных частей: •
Код ошибки (Error number), Msg 547 — код ошибки, который возвращается к вызывающей программе. В некоторых случаях этот код ошибки существенен; однако в большинстве случаев достаточно просто знать, что он отличен от нуля.
•
Уровень (Level), Level 16 — уровень серьезности сообщения. CN-18 обычно рассматриваются как сообщения пользователю, где 16 является значением по умолчанию. Уровни 19^-25 — действительно серьезные ошибки, которые приводят к разрыву связи (с сообщением, записываемым в протокол) и обычно связаны с порчей данных.
•
Состояние (State), State 1 — значение от 1 до 127, которое характеризует состояние процесса, когда возникла ошибка. Всегда равняется 1, если это не является частью оператора RAISERROR.
•
Строка (Line), Line 1 — строка в групповой операции или объекте, где возникла ошибка. Очень полезна для целей отладки.
•
Текст ошибки (Error Text) — текстовое объяснение возникшей ошибки. 383
Глава 11 Однако в наших SQL-операторах мы имеем доступ только к коду ошибки. Сделать это легко, используя глобальную переменную @@error. Д SET @<имя_переменной_ошибки> = @@error Таким образом мы получаем значение кода ошибки любого сообщения, которое должно появиться в ответ на предыдущий оператор. Это значение будет: О
Нет ошибки.
1+49999
Системные ошибки либо из-за ограничений, либо из-за действительных ошибок системы, которые не могут нами контролироваться. Примеры таких системных ошибок включают синтаксические ошибки, дисковые ошибки, неправильные SQL-операции и т. д.
50000
Нерегламентированные сообщения об ошибках без заданного кода или любые общие сообщения, когда операции пользователя не нуждаются в выделении их из любых других сообщений.
50001 и больше
Определяемые пользователем сообщения. Если клиентская система может использовать код ошибки, то неплохо разработать общий или персональный стандарт для кодов ошибок. Мы рассмотрим схемы обработки ошибок в главе 12. Максимальный код ошибки определяется максимальным значением типа данных big int.
Когда выполняются любые операторы INSERT, UPDATE или DELETE в коде нашей программы, триггера или хранимой процедуры, мы должны всегда проверять состояние кода ошибки команды. Обратите внимание, что это значение изменяется с каждой командой в нашей БД, так что любой код после каждого оператора корректировки должен получать значение, находящееся в @@error. (Вы также можете получить значение параметра @@rowcount (который определяет число скорректированных строк последней операцией) после оператора SET, поскольку этот оператор не воздействует на значение @@rowcount.) Что делать со значением ошибки, зависит от ситуации. Обычно лучше выполнять каждый оператор, который изменяет данные, внутри своей собственной транзакции, если Вы не выполняете единственный оператор. В следующем примере кода мы попробуем обновить альбом с неправильным номером каталога. Это вызовет ошибку, которую мы обработаем, сформировав оператор RAISERROR, который сообщит пользователю, что произошло. Каждый раз, когда мы снова будем выполнять команду UPDATE, то будем проверять, равна ли переменная @@error нулю, и если нет, будем знать, что команда не выполнена: BEGIN TRANSACTION DECLARE @errorHold int UPDATE album SET catalogNumber = '1' WHERE name = 'the white album' SET @errorHold = @@error
384
Обеспечение целостности данных IF gerrorHold <> О BEGIN RAISERROR 50000 'Ошибка в корректировке первого номера каталога в таблице album' ROLLBACK TRANSACTION END IF 8errorHold - 0 BEGIN UPDATE album SET catalogNumber - '1' WHERE name = 'the white album' SET SerrorHold = @@error IF @errorHold <> 0 BEGIN RAISERROR 50000 'Ошибка в корректировке второго номера каталога' ROLLBACK TRANSACTION END END IF @errorHold = 0 COMMIT TRANSACTION ELSE RAISERROR 50000 'Программа обновления не выполнена'
Здесь формируется транзакция, где сначала объявляется переменная SerrorHold (размещение ошибки) типа int. Далее в таблице album полю catalogNumber записи, где поле name имеет значение 'the white album', задается значение '1'. После этой операции проверяется глобальная переменная @@error и ее значение записывается в переменную QerrorHold. Если это значение не равно нулю (произошла ошибка), формируется сообщение об ошибке с номером 50000 и организуется откат транзакции к началу. Если же ошибки не было, повторно выполняются те же действия. Наконец, в завершение, если значение переменной QerrorHold равняется нулю, транзакция успешно завершается, в противном случае формируется сообщение об ошибке с кодом 50000. Выполняя эту программу, мы получим три ошибки: Server: Msg 547, Level 16, State 1, Line 1 UPDATE statement conflicted with TABLE CHECK constraint 'chkAlbum$catalogNumber$function$artist$catalogNumberValidate'. The conflict occured in database 'master', table 'album'. The statement has been terminated. Server: Msg 50000, Level 16, State 1 Line 12 Ошибка в корректировке первого номера каталога в таблице album Server: Msg 50000, Level 16, State 1, Line 33 Программа обновления не выполнена (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор UPDATE конфликтует с ограничением TABLE CHECK J chkAlbum$catalogNumber$function$artist$catalogNumberValidate. Конфликт произошел в БД 'master, таблице 'album. Оператор удален. 385
Глава 11 Сервер: сообщение 50000, уровень 1, состояние 1, строка 12 Ошибка в корректировке первого номера каталога в таблице album Сервер: сообщение 50000, уровень 1, состояние 1, строка 33 Программа обновления не выполнена.) При кодировании дополнительных сообщений об ошибках мы можем создать последовательности сообщений, которые точно нам скажут, где в программе произошла ошибка, что позволит легче выполнить отладку.
Триггеры Триггеры представляют собой тип хранимых процедур, связанных с таблицей или представлением и выполняемых только тогда, когда содержимое таблицы изменяется. Они могут использоваться, чтобы обеспечить любые бизнес-правила, и особенно полезны в ситуациях, которые слишком сложны для управления ограничениями-проверками. Мы будем рассматривать триггеры как средства выполнения следующих операций: •
обеспечивать ссылочную целостность между БД;
•
проверять межстрочные ограничения внутри таблицы;
•
проверять межтабличные ограничения;
•
вводить желаемые побочные эффекты в наши запросы.
Главное преимущество триггеров над ограничениями — возможность непосредственного доступа к другим таблицам и работы с несколькими строками одновременно. В триггере мы можем использовать почти все команды T-SQL за исключением следующих. ALTER DATABASE
CREATE DATABASE
DROP DATABASE
RESTORE LOG
RECONFIGURE
RESTORE DATABASE
Можно иметь триггеры, которые запускают INSERT, UPDATE или DELETE или любую их комбинацию. Можно также иметь для таблицы несколько триггеров, различных или одинаковых типов. Небольшое предупреждение: никогда не включайте операторы, которые в триггере возвращают наборы результатов, если они не допустимы во всех случаях, где триггер может быть запущен. Имеются две различных модели триггера: •
386
INSTEAD OF (вместо) — Означает, что триггер работает вместо команды (INSERT, UPDATE или DELETE), воздействуя на таблицу или представление. В этом случае мы можем делать с данными все, что захотим, или обновить их там, где они находятся, или переместить их в другое место. Мы можем иметь по отдельному триггеру этого типа для операций INSERT, UPDATE и DELETE на таблицу. Однако вы можете и объединить все три в один и иметь единственный триггер, который запускается для всех трех действий.
Обеспечение целостности данных •
AFTER (после) — Означает, что триггер запускается после команды, которая воздействовала на таблицу, но не на представления. Триггеры AFTER используются для задания обработчиков расширенных бизнес-правил, и вы обычно будете помещать в них различные виды механизмов регистрации. Вы можете иметь неограниченное число триггеров AFTER, которые запускаются операциями INSERT, UPDATE и DELETE или любой их комбинацией. Даже если мы имеем триггер INSTEAD OF, мы все же можем иметь столько триггеров AFTER, сколько потребуется, так как все они могут быть объединены в общий триггер.
Кодирование триггеров Следующая структура содержит основной синтаксис триггера: CREATE TRIGGER <имя__таблицы$тип_триггера$тип_использования> ON <имя_таблицы> | <имя представления> INSTEAD OF | AFTER [DELETE][,] tINSERT][,][UPDATE] AS <SQL__onepaTopbi>
Следующие пункты рассматривают различные части оператора CREATE TRIGGER, которые нуждаются в дополнительном разъяснении. •
CREATE TRIGGER <имя_таблицы$тип_триггера$тип_использования> Имя триггера не очень уж важно, поскольку вы не будете вызывать его явно из кода. Я обычно люблю давать им названия, похожие на названия всех других объектов, которые мы рассматривали ранее, хотя, как и с любым объектом, который мы создавали, выбор имени в полной мере зависит от личного вкуса.
•
INSTEAD OF | AFTER — сообщает триггеру, когда он должен быть запущен, как было отмечено выше.
•
[DELETE] [, ] [INSERT] [, ] [UPDATE] — мы можем задать один или несколько из этих элементов, чтобы сообщить нашему триггеру, когда следует запускаться; например, задав
§§ AFTER DELETE, INSERT
... будет сообщено триггеру, что он должен запускаться после операторов DELETE и INSERT, но не UPDATE. Задание §§ INSTEAD OF INSERT, UPDATE
... дает нам триггер, который запускается вместо INSERT и UPDATE, но DELETE будет работать как обычно. •
<SQL_onepaTopbi> — любые SQL-операторы.
Остальная часть этого раздела будет посвящена тому, какие операторы помещать в триггер. Мы рассмотрим •
доступ к измененным строкам информации;
•
определение, какие столбцы были изменены;
•
организацию контроля нескольких строк одновременно; 387
Глава 11 •
создание нескольких триггеров для одной и той же операции (INSERT, UPDATE, DELETE);
•
обработку ошибок.
Доступ к измененным строкам Основное достоинство кодирования триггеров заключается в том, что независимо от того, сколько строк изменяется, мы получаем доступ ко всем строкам. Мы обращаемся к этим значениям через две таблицы, которые позволяют нам видеть состояние наших данных до и после корректировки. Эти таблицы называются DELETED (удаленные) и INSERTED (добавленные) и существуют они в памяти только в период работы триггера и недоступны вне этих границ. Таблица INSERTED будет заполнена во время выполнения операций INSERT или UPDATE, a таблица DELETED заполняется во время операций UPDATE или DELETE. Операции INSERT и DELETE понятны, но обработка в SQL Server операции UPDATE менее очевидна. Все операции UPDATE в SQL Server логически делятся на операции DELETE и INSERT, удаляя строки, которые должны быть изменены, а затем вставляя их в новом формате. В следующем примере мы создадим триггер, чтобы показать содержание таблиц INSERTED и DELETED после операции UPDATE: -- создание триггера, который возвращает в качестве результата набор как -- введенных, так и удаленных элементов таблицы только в демонстрационных — целях. Для реальных триггеров это плохая идея CREATE TRIGGER artist$afterUpdate$showInsertedAndDeleted ON artist AFTER UPDATE — запускается после выполнения UPDATE AS SELECT 'содержимое таблицы INSERTED' — SELECT * FROM INSERTED
информационный вывод
SELECT 'содержимое таблицы DELETED' -- информационный вывод SELECT * FROM DELETED GO
Затем для таблицы запустим оператор UPDATE, который будет устанавливать у всех имен заглавные буквы как пример различий между содержимым таблиц INSERTED и UPDATED: I
UPDATE artist SET name • UPPER(name)
Этот оператор возвращает следующие четыре результата: содержимое таблицы INSERTED
388
artistld
name ...
1 2
THE BEATLES ... THE WHO
Обеспечение целостности данных
содержимое таблицы DELETED artistld
name ...
1 2
the beatles the who
Определение, какие столбцы были модифицированы Имеются две функции, которые мы можем использовать в наших триггерах, чтобы определить, какие столбцы были использованы в операциях INSERT или UPDATE, но заметьте, что нет эквивалента для операции DELETE. Мы будем это использовать, чтобы избежать проверки данных, которые не затронуты в операции, и, следовательно, увеличим производительность наших триггеров. Конкретно, функции получают информацию из: •
перечня столбцов в операторе INSERT:
§f§ INSERT tableName (<столбец 1>, <столбец 2>, . . . , <столбец N>) •
выражения SET оператора UPDATE:
UPDATE tableName SET <столбец 1> = <значение 1>, <столбец 2> = <значение 2>, <столбец N> •= <значение N> Первый метод определения, что столбец был обновлен — функция update (<имя столбца>) . Она возвратит логическое значение, которое сообщает нам, был ли столбец упомянут в операторе корректировки. Например, следующий код будет проверять, были ли столбцы name или d e s c r i p t i o n модифицированы:
I
IF update(name) OR u p d a t e ( d e s c r i p t i o n ) BEGIN <SQL-onepaTOpbi> END
В качестве примера создадим следующую таблицу: CREATE TABLE columnUpdateTest ( columnUpdatedTestld int IDENTITY, columnl i n t , column2 i n t , column3 int
— --—
битовая битовая битовая битовая
маска, маска, маска, маска,
значение значение значение значение
1 2 4 8
Здесь создается таблица columnUpdatedTest (тестирование столбцов с помощью функции update), состоящая из четырех столбцов типа int, первый из которых является автоинкрементным.
389
Глава 11
Далее мы можем определить следующий триггер и добавить его к таблице: CREATE TRIGGER testlt ON columnUpdatedTest AFTER INSERT, UPDATE AS IF update(columnUpdatedTestld) BEGIN SELECT 'columnUpdatedTestld модифицирован' END IF update(columnl) BEGIN SELECT 'columnl модифицирован' END IF update(column2) BEGIN SELECT 'column2 модифицирован' END IF update(column3) BEGIN SELECT 'column3 модифицирован' END GO Теперь, когда м ы создадим новую запись:
I
INSERT columnUpdatedTest (columnl, column2, column3) VALUES(1, 1, 1)
... будут возвращены следующие строки
columnUpdatedTest модифицирован
columnl модифицирован
column2 модифицирован
column3 модифицирован Если мы модифицируем один столбец:
I
UPDATE columnUpdatedTest SET column2 = 2 WHERE columnUpdatedTestld
= 1
... тот же самый триггер проинформирует нас:
column2 модифицирован
390
Обеспечение целостности данных Другой метод, который стоит упомянуть, на самом деле не столь употребителен. Это — функция columns_updated () . Выполняя функцию columns_updated () , мы вернем шестнадцатеричное целое число с битовой маской столбцов в таблице. Например, в созданной нами таблице мы имеем следующие столбцы: columnUpdatedTestld columni column2 column3
— битовая маска, значение 1 -- битовая маска, значение 2 -- битовая маска, значение 4 -- битовая маска, значение 8
Битовая маска для всех столбцов в таблице будет: 1 + 2 + 4 + 8 = 15. Итак, в предыдущем триггере мы можем использовать columns_updated () вместо update () , чтобы получить битовую маску столбцов, которые были обновлены. Например, если мы введем все столбцы в операторе INSERT: INSERT columnUpdatedTest
I
VALUES
( 1 , 1,
(columni,
column2,
column3)
1)
... и выполним функцию columns_updated () в подключенном триггере, то получим следующую битовую маску: 0x0 F ... которая равна 15, или битовую маску для всей таблицы, как мы показали ранее. Если мы используем следующий код:
й
UPDATE columnUpdatedTest SET column2 column2 = 2 2 WHERE columnUpdatedTestld = 1
... то получим битовую маску для этого единственного столбца, который мы изменили, и никаких других, которые не были в операторе: 0x04 Итак, мы можем протестировать эту величину, чтобы посмотреть, какие столбцы были обновлены, используя побитовые операции с помощью кода наподобие следующего: IF columns updated() & 4 = 4
I
-- третий столбец в таблице был -- модифицирован IF columns updated() & 15 = 15 -- первые четыре столбца были -- модифицированы
Что плохого в использовании функции columns_updated () ? Дело в том, что код, написанный с использованием этой функции, должен измениться, если изменится порядок наших столбцов. Это делает наш код непредсказуемым, поскольку мы должны проверить код триггера, если добавляем столбец в любую позицию таблицы, кроме последней. Функция update () намного более предпочтительна для проверки изменяемых столбцов. Функция columns updated () здесь показана для полноты рассмотрения.
391
Глава 11 Написание межстрочных проверок Так как триггеры запускаются один раз для многострочных операций, операторы внутри триггера должны быть закодированы с учетом работы с несколькими строками. Довольно легко написать триггеры, которые предназначены только для корректировки одной строки; однако каждая часть кода, который вы пишете, должна будет обычно вместо одной просматривать строки, которые не удовлетворяют вашим критериям. Если вы не хотите заставлять пользователей вводить только одну строку за раз, то должны закодировать ваши триггеры таким образом, который допускает, что может быть модифицировано больше одной строки в таблице. В следующем примере мы построили триггер с двумя очень похожими средствами проверки, и хотя оба работают с одинаковыми строками, только одно будет обнаруживать многострочные неприятности. CREATE TRIGGER artist$afterUpdate$demoMultiRow ON artist AFTER INSERT, UPDATE -- запускается после добавления и обновления AS IF NOT EXISTS (SELECT * FROM INSERTED WHERE name ('the who', 'the beatles', 'jethro tull') ) BEGIN SELECT 'Неправильная первая проверка артиста' END
I
IF EXISTS (SELECT * FROM INSERTED WHERE name NOT IN ('the who', 'the beatles', 'jethro tull') ) BEGIN SELECT 'Неправильная вторая проверка артиста' END G0
Итак, мы написали наш триггер и хотим проверить его. Так как мы собираемся писать запрос, который будет работать правильно, поместим наш оператор INSERT между операторами BEGIN TRANSACTION и ROLLBACK TRANSACTION, чтобы иметь возможность проверять оператор, одновременно предотвращая любую корректировку наших данных от фактического размещения в таблице. Триггер, который мы написали — только для демонстрации, а в разделах, описывающих триггеры AFTER и INSTEAD OF, мы рассмотрим возможные способы фактического возвращения ошибки из триггеров.
I
BEGIN TRANSACTION INSERT INTO artist (name, defaultFl, catalogNumberMask) VALUES ('ROLLING STONES', 0, ' %') ROLLBACK TRANSACTION
—
отменить проверку строк
Этот запрос возвратит следующее: Неправильная первая проверка артиста Неправильная вторая проверка артиста
392
Обеспечение целостности данных потому что в обеих наших проверках I F NOT EXISTS (SELECT * FROM INSERTED WHERE name IN ( ' t h e w h o ' ,
I
'the beatles',
'jethro
tull')
и I F EXISTS (SELECT * FROM INSERTED WHERE name NOT IN ( ' t h e w h o ' , ' t h e b e a t l e s ' ,
I
'jethro tull'))
... 'ROLLING STONES' не попадает внутрь границ. В первом случае имени нет в группе, так что нет никаких строк, которые удовлетворяют тесту NOT EXISTS. Во втором случае имени также нет в группе, так что все строки возвращаются запросом, и строка существует. Далее мы попробуем использовать многострочный тест. Лучший способ выполнить многострочный тест — использовать операцию UNION:
I
SELECT 'ROLLING STONES', 0, '%' UNION SELECT 'JETHRO TULL', 0, '%'
которая возвращает JETHRO TULL ROLLING STONES
0 0
% %
Выполняя такие объединения (в этом случае один оператор дает подходящий результат, а другой — нет), мы можем проверять многострочные возможности наших триггеров: BEGIN TRANSACTION INSERT INTO artist (name, defaultFl, catalogNumberMask) SELECT 'ROLLING STONES', 0, ' %' UNION SELECT 'JETHRO TULL', 0, ' %' ROLLBACK TRANSACTION
- - отменить п р о в е р к у с т р о к
На этот раз только одна из наших проверок не удается: Неправильная вторая проверка артиста Нам следует разобраться, почему это происходит. В следующей проверке триггер вообще не пошлет нам сообщение:
I
IF NOT EXISTS (SELECT * FROM INSERTED WHERE name IN ('the who', 'the beatles', 'jethro tull'))
393
Глава 11 Так как мы имеем одну строку, которая удовлетворяет критерию (строка j e t h r o t u l l ) , только у нее INSERTED, name попадает в нашу испытательную группу. Так что должна быть возвращена единственная строка, и такая строка существует. Это — довольно общая проблема при написании триггеров. Даже после десяти лет создания триггеров я должен проверять мои триггеры, чтобы удостовериться, что не попал в эту западню. Конечно, лучше всего проверить каждый из ваших фрагментов кода подобным образом, чтобы удостовериться, что он может обращаться с любыми данными, помещенными в него. Обратите также внимание, что мы удалим триггер a r t i s t $ a f terUpdate$demoMultiRow из нашей БД, поскольку он больше не потребуется: Щ DROP TRIGGER artist$afterUpdate$demoMultiRow Использование нескольких триггеров для одной операции Одна свежая особенность SQL Server 2000 — возможность иметь несколько триггеров AFTER для различных контролирующих операций. Это позволяет нам строить триггеры, которые являются очень специфичными по природе, как в следующих вариантах: •
Вариант вложенных триггеров — глобальная настройка сервера воздействует через: sp_configure 'nested triggers', 1 | 0 — где 1 — включить, 0 — выключить
Если операция каскадирования включена, то запускаются триггеры всех используемых таблиц. Имеется предел в 32 вложенные операции триггеров. Обратите внимание, что мы можем создавать неограниченные циклы триггеров, которые будут каскадировать операции одной таблицы, и если происходит любая корректировка исходной таблицы, то это начнет процесс снова. Вложенные триггеры установлены на уровне сервера. •
Вариант рекурсивных триггеров — настройка для конкретной БД воздействует через: sp_dboption '<имя БД>', 'recursive triggers', 'TRUE' | 'FALSE'
Данный вариант заставляет триггеры заново запуститься, когда триггер выполняет корректировки в таблице, к которой он подключен. Это требует, чтобы был установлен вариант вложенных триггеров и он также устанавливается на уровне БД. В следующем примере мы демонстрируем, как работают настройки вложенных и рекурсивных триггеров. Сначала мы создаем две очень простых таблицы, в которые сможем поместить одно и то же значение: CREATE TABLE t a b l e A ( field
varchar(40)
NOT NULL
) CREATE TABLE t a b l e B ( field
varchar(40)
NOT NULL
) GO
Здесь создаются две таблицы tableA и tableB, содержащие по одному полю field типа varchar (40), которые не могут принимать значение NULL. 394
(поле)
Обеспечение целостности данных
... и затем два очень простых триггера, каждый из которых помещает те же величины в другую таблицу: CREATE TRIGGER tableA$afterUpdate$demoNestRecurse ON tableA AFTER INSERT AS -- сообщение пользователю, что триггер запущен SELECT 'триггер добавления в tableA' — помещение значений в tableB INSERT INTO tableB (field) SELECT field FROM INSERTED GO CREATE TRIGGER tableB$afterUpdate$demoNestRecurse ON tableB AFTER INSERT AS -- сообщение пользователю, что триггер запущен SELECT 'триггер добавления в tableB' -- помещение значений в tableA INSERT INTO tableA (field) SELECT field FROM INSERTED GO
Теперь выполним наши установки — выключим вложенные триггеры для сервера: I
EXEC sp_configure 'nested triggers', 0 -- (1 - включен, 0 - выключен) RECONFIGURE -- требуется для немедленной конфигурации сервера
Далее мы введем строку в tableA:
I
INSERT INTO tableA (field) VALUES ('пробная величина')
и запустим только триггер таблицы tableA: триггер добавления в tableA Если мы включим вложенность триггеров и сделаем так, чтобы рекурсивные триггеры для нашей испытательной БД были выключены:
I
EXEC sp_configure 'nested triggers', 1 -- где 1 - включено, 0 - выключено RECONFIGURE EXEC sp__dboption 'bookTest', 'recursive triggers', 'FALSE'
395
Глава 11 тогда мы можем выполнить тот же код для таблицы tableA:
I
INSERT INTO tableA (field) VALUES ('пробная величина')
В этом случае мы получим нечто другое: триггер добавления в tableA триггер добавления в tableB
триггер добавления в tableA триггер добавления в tableB Server: Msg 217, Level 16, State 1, Procedure tableB$afterUpdate$demoNestRecurse, Line 7 Maximum stored procedure, function or trigger nesting level exceeded (limit 32). (Сервер: сообщение 217, уровень 16, состояние 1, процедура tableB$afterUpdate$demoNestRecurse, строка 7 Максимальный уровень итерации вложенной процедуры, функции или триггера превышен (предел 32).) Это — в значительной степени то, что мы могли бы ожидать. Правильная ситуация с рекурсивным триггером происходит только тогда, когда триггер действует на ту же самую таблицу, на которой он установлен. Чтобы проиллюстрировать это, лишь изменим триггер таблицы tableA так, чтобы вставлять строку в таблицу tableA, а не в tableB: ALTER TRIGGER tableA$afterUpdate$demoNestRecurse ON tableA AFTER INSERT AS -- сообщение пользователю, что триггер запущен SELECT 'триггер добавления в tableA' -- помещение значений в tableA INSERT INTO tableA (field) SELECT field FROM INSERTED GO
Когда м ы введем одну строку:
I 396
INSERT INTO tableA (field) VALUES ('пробная величина')
Обеспечение целостности данных оператор потерпит неудачу и откатит назад транзакцию. Это иллюстрирует, что рекурсивные триггеры могут использоваться, если очень внимательно кодировать триггеры, чтобы они не могли запускаться более 32 раз: триггер добавления в tableA триггер добавления в tableA
Server: Msg 217, Level 16, State 1, Procedure tableA$afterUpdate$demoNestRecurse, Line 7 Maximum stored procedure, function or trigger nesting level exceeded (limit 32). (Сервер: сообщение 217, уровень 16, состояние 1, процедура tableA$afterUpdate$demoNestRecurse, строка 7 Максимальный уровень итерации вложенной процедуры, функции или триггера превышен (предел 32).)
Обработка ошибок Имеются два способа реализации обработки ошибок, используя триггер. Или код обработки ошибок помещается в триггер, или он содержится внутри вызывающего кода клиента.
Обработка ошибок внутри триггера В разделе, посвященном ограничениям, мы видели, что должны проверять состояния ошибки после каждого SQL-оператора, который изменяет данные. Печально, но такая обработка ошибок не дает нам возможность получать коды ошибок от триггеров по двум причинам: •
Выполнение отката транзакции в триггере откатывает транзакцию и останавливает все выполнения продолжающихся операторов — Затруднение этой проблемы в том, что нет абсолютно никакого способа сообщить всем триггерам, что текущая операция потерпела неудачу и что только единственную операцию можно откатить. Пусть вы имеете несколько триггеров, работающих последовательно, и вы вложили их в транзакции. Если заключительная транзакция терпит неудачу, то группа откатывается независимо от успеха предыдущих транзакций. Вы не можете откатить только неудавшуюся транзакцию и продолжать работать с успешными.
•
Если обнаружена серьезная ошибка, вся транзакция автоматически откатывается — Определение серьезной ошибки весьма произвольно. Оно, однако, включает нарушения ограничений, так что любые корректировки данных, которые мы выполняем в триггере и которые нарушают ограничения других таблиц, не только терпят неудачу, но и как только триггер завершит работу, транзакция откатывается, и никакие дальнейшие команды не выполняются.
Имеются три основных пути, чтобы управлять ошибками в наших триггерах, которые могут быть объединены следующим образом: •
проверка, что значения в таблице правильные;
•
каскадирование операций;
•
вызов хранимой процедуры. 397
Глава 11 Проверка правильности значений в таблице Основной метод, который мы будем использовать, чтобы проверить правильность условий, следующий: IF EXISTS ( SELECT * FROM INSERTED WHERE <условие равно True для неправильных значений>) BEGIN -- всегда формирует другую ошибку, чтобы сообщить вызывающему, где она находится RAISERROR 50000 'В размещенной таблице имеются неправильные данные' ROLLBACK TRANSACTION RETURN -- заметьте, что триггер не завершит работу, пока вы не -- остановите его, так что пакет завершается, когда триггер — AFTER завершит работу END
Здесь проверяется наличие в массиве INSERTED элементов, которые удовлетворяют условию неправильных значений, и если они существуют, формируется собственное сообщение об ошибке и производится откат транзакции. Каскадирование операций В следующем фрагменте кода мы покажем, как каскадировать обновления одной величины в другой таблице. [DECLARE @ e r r o r H o l d i n t , @msg v a r c h a r (8000)
- - и с п о л ь з у е т с я для хранения ошибки после о п е р а т о р а - - обновления для форматируемых сообщений
UPDATE a n o t h e r T a b l e SET <столбец> = <значение> FROM a n o t h e r T a b l e JOIN INSERTED ON a n o t h e r T a b l e . t a b l e l d - I N S E R T E D . t a b l e l d SET @ e r r o r H o l d • @@error получение з н а ч е н и я кода ошибки после обновления IF @ e r r o r H o l d <> 0 - - ч т о - т о неправильно BEGIN SET @msg = 'Ошибка ' + CAST(@errorHold) + ' в о з н и к л а при к а с к а д и р о в а н и и обновления в т а б л и ц е ' — в с е г д а формирует другую ошибку, чтобы сообщить вызывающему, - - г д е она в о з н и к л а RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN - - з а м е т ь т е , ч т о т р и г г е р не завершит р а б о т у , пока вы не - - остановите е г о , так что пакет завершается, когда т р и г г е р - - AFTER завершит р а б о т у END
Здесь для формирования сообщения об ошибке используются две переменные: @errorHold (хранение кода ошибки) для размещения кода ошибки и @msg (сообщение) для формирования текста сообщения об ошибке. В этом фрагменте в строки таблицы anotherTable (другая таблица), у которых идентификатор строки (tableld) совпадает с одним из идентификаторов таблицы INSERTED, в заданный столбец помещается заданное значение. После этой операции в переменную QerrorHold помещается код ошибки, и если он отличен от нуля (ошибка произошла), формируется соответствующее сообщение, инициируется собственная ошибка и организуется откат транзакции. 398
Обеспечение целостности данных Выполнение хранимой процедуры Следующий фрагмент кода демонстрирует метод вызова хранимой процедуры из нашего триггера: DECLARE @errorHold i n t ,
@returnValue int @msg varchar(8000)
— — — —
используется для хранения ошибки после оператора обновления и размещения возвращаемой хранимой процедурой величины для форматируемых сообщений
EXEC @returnValue = <имя_процедуры><параметры> SET @errorHold = @@error — получение кода ошибки после обновления IF @errorHold <> 0 or @returnValue < 0 — что-то произошло BEGIN SET @msg = CASE WHEN @errorHold <> 0 THEN 'Ошибка ' + CAST(@errorHold) + ' произошла при каскадированном обновлении таблицы' ELSE 'Возвращаемая величина ' + CAST(@returnValue AS varchar(lO)) + + ' получена из процедуры' END -- Всегда формируется другая ошибка, чтобы сообщить вызывающему, — где она произошла RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN — заметьте, что триггер не завершит работу, пока вы не — остановите его, так что пакет завершается, когда триггер — AFTER завершит работу | END
В этом фрагменте используются три переменные: QerrorHold (хранение ошибки) для размещения кода ошибки, QreturnValue (возвращаемая величина) для размещения величины, возвращаемой хранимой процедурой, и Qmsg (сообщение) для формирования текста сообщения об ошибке. Здесь запускается хранимая процедура, имеющая заданное имя и заданные параметры, и фиксируется возвращаемая величина в переменной QreturnValue. После завершения работы процедуры в переменную QerrorHold помещается код ошибки. Если во время работы процедуры произошла коллизия (код ошибки не равен нулю или возвращаемая величина меньше нуля), формируется собственное сообщение об ошибке; в первом случае формируется сообщение, что при каскадированном обновлении произошла ошибка, а во втором случае выводится возвращенная процедурой величина. В обоих случаях осуществляется откат транзакции. Как мы видели, сообщения об ошибках, которые мы получаем при нарушении ограничений, ни удобочитаемы, ни достаточно значащие. Однако когда мы строим триггеры, то пишем наши собственные сообщения об ошибках, которые могут быть сделаны настолько удобочитаемыми и значащими, как захотим. Мы могли пойти далее и поместить каждый предикат БД в наши триггеры (включая уникальные ограничения), но это потребовало бы большего объема кодирования и не будет выгодно для нас; триггеры принципиально медленнее, чем ограничения, и не являются лучшим методом защиты наших данных, кроме случаев, когда ограничения не могут использоваться.
14—1868
3 9 9
Глава 11
Обработка ошибок в вызываемом коде Большим достоинством обработки ошибок в триггерах является то, что так как при возникновении ошибок вся последовательность команд триггера откатывается назад, не нужно заботиться относительно внешнего управления ошибками, кроме обработки состояния ошибки при защите от ошибок, связанных с ограничениями.
Триггеры AFTER Как мы раньше сказали, триггеры AFTER названы так, потому что они запускаются после того, как данные фактически окажутся в таблице. Это позволяет нам проверить, независимо от того, что оператор сделал с данными в таблице, не нарушают ли они наши спецификации. В предыдущих версиях SQL Server это был единственный вид доступного триггера, но вместо ключевого слова AFTER вы использовали ключевое слово FOR. В SQL Server 2000 последнее ключевое слово больше не должно использоваться как тип триггера, но оно сохранено для обратной совместимости. Давайте разберем простой пример триггера AFTER, рассматривая состояние таблицы INSERTED и содержание таблиц БД во время выполнения триггера: CREATE TRIGGER a r t i s t S t r l n s e r t D e m o AFTER INSERT AS
ON a r t i s t
SELECT 'содержимое таблицы INSERTED' SELECT artistld, name -- только первичный ключ и имя, — чтобы разместить на странице книги FROM INSERTED SELECT 'содержимое таблицы artist' SELECT artistld, name FROM artist GO
Затем мы выполним оператор INSERT для задания имени: I
INSERT INTO a r t i s t ( n a m e ) VALUES ( ' j e t h r o tull')
который возвратит следующий набор результатов: содержимое таблицы INSERTED artistld 27
name jethro tull
содержимое таблицы artist
400
Обеспечение целостности данных artistid name 27 1 2
jethro tull the beatles the who
Таким образом, мы видим, что строка в таблице INSERTED содержит данные вставленной строки (что естественно) и мы видим из результатов второго запроса, что таблица уже содержит значение, которое мы поместили в нее. (Если бы мы делали это с триггером INSTEAD OF, второй запрос не возвратил бы нам новую запись.) Реализация нескольких триггеров AFTER Давайте теперь расширим наш пример с несколькими триггерами, рассмотренный выше, изменяя кодирование триггера tableA$afterUpdate$demoNestRecurse, чтобы поместить название процедуры в поле t a b l e B . f i e l d , что позволяет нам видеть различные запускаемые триггеры: ALTER TRIGGER tableA$afterUpdate$demoNestRecurse ON tableA AFTER INSERT AS SELECT 'триггер добавления в tableA' INSERT INTO tableB(field) VALUES (object_name (@@procld) )
-,- @@procld дает object_id (идентификатор) -- текущей выполняемой процедуры
GO Затем создадим еще два триггера, идентичные первому (но с другими именами): CREATE TRIGGER tableA$afterUpdate$demoNestRecurse2 ON tableA AFTER INSERT AS SELECT 'триггер 2 добавления в таблицу А' INSERT INTO tableB(field) VALUES (object_name(@@procld)) GO CREATE TRIGGER tableA$afterUpdate$demoNestRecurse3 ON tableA AFTER INSERT AS SELECT 'триггер З добавления в таблицу А' INSERT INTO tableB(field) VALUES (object_name(@@procld)) GO Далее очистим таблицы:
I
— TRUNCATE TABLE удаляет строки очень быстро за единственный вход в — протокол плюс он переустанавливает параметры идентичности TRUNCATE TABLE tableA TRUNCATE TABLE tableB
401
Глава 11
и поместим одну строку
I
INSERT INTO tableA VALUES ('Проверка значений')
... что возвращает т р и следующие с т р о к и :
триггер добавления в таблицу А триггер 2 добавления в таблицу А триггер 3 добавления в таблицу А Чтобы показать, что таблицы таковы, как мы ожидаем, выполним для таблиц следующие операторы: I
SELECT * FROM tableA SELECT * FROM tableB
... и мы увидим, что tableA содержит единственную строку, как мы и ожидали, a tableB содержит строки, являющиеся результатом каждого из трех триггеров таблицы tableA, которые были выполнены: field Проверка значений field tableA$afterUpdate$demoNestRecurse tableA$afterUpdate$demoNestRecurse2 tableA$afterUpdate$demoNestRecurse3 Наконец, мы покажем, как переупорядочить последовательность выполнения триггеров:
I
EXEC sp_settriggerorder @triggerName = 'tableA$afterUpdate$demoNestRecurse3' , @order - 'first', @stmttype = 'insert'
Здесь QtriggerName — имя триггера; border — порядок; Qstmttype запускающего триггер.
— тип оператора,
Заметим, что @ o r d e r может иметь значение ' f i r s t ' (первый), ' last'(последний), или ' п о п е ' (никакой), чтобы запустить триггер в неустановленном порядке. Мы можем устанавливать запуск только первого и последнего триггера, в то время как любые другие триггеры запускаются в установленном (как мы задали) порядке. Значение @stmttype может быть ' i n s e r t ' (добавить), ' u p d a t e ' (обновить) или ' d e l e t e ' (удалить). 402
Обеспечение целостности данных Мы можем теперь выполнять команды, чтобы удалять элементы таблицы, вставлять строку и, наконец, выбирать строки из таблицы: TRUNCATE TABLE tableA TRUNCATE TABLE tableB INSERT INTO tableA VALUES ('Проверка значений') SELECT * FROM tableA SELECT * FROM tableB
... и в этом случае возвратим другой порядок триггеров: field Проверка значений field tableA$afterUpdate$demoNestRecurse3 tableA$afterUpdate$demoNestRecurse tableA$afterUpdate$demoNestRecurse2 В качестве заключительного примечания в этом разделе я бы предостерег от использования нескольких триггеров для одной и той же операции в одной таблице. Часто проще создать единственный триггер, который управляет всеми вашими потребностями. Обычно мы проектируем наши системы, используя один триггер INSTEAD OF и один триггер AFTER для каждого действия, если это необходимо. Мы будем также редко использовать многоцелевой триггер, который будет запускаться и оператором INSERT, и оператором UPDATE. В основном наш принцип должен заключаться в том, чтобы иметь триггер максимально простым для избежания любых проблем с упорядочиванием. Использование триггеров AFTER для решения общих проблем В этом разделе мы рассмотрим, как вы используете триггеры, чтобы решить следующие типичные проблемы: О
каскадирование добавлений;
Q •
проверки диапазонов; каскадированные удаления установкой значений таблиц-потомков в NULL.
Важно знать, что независимо от того, какие операции мы выполняем с помощью триггеров, все зависит от существующих ранее данных в нашей таблице, для которых выполнены все требования ограничений. Например, не стоит вставлять строки в таблицу-потомок (и, следовательно, вызывать запуск всей последовательности триггеров/ограничений), пока данные предка не проверены. Одновременно не следует проверять состояние всех строк в нашей таблице, пока мы не завершили все наши изменения с ними; то же самое можно сказать об операциях каскадирования удалений. Три последующих примера — всего лишь маленький набор возможных использований триггеров; они — только образец того, как все происходит. Каждый из фрагментов, которые мы покажем в следующих трех подразделах, будет подходить для триггеров (любого типа) и будет иметь следующий основной формат:
403
Глава 11 CREATE TRIGGER <имя_триггера> ON <имя_таблицы> FOR <операция> AS —
Цель: Триггер для <операции>, который запускается для любой <операции> языка манипулирования данными (ЯМД)
BEGIN DECLARE SrowsAffected int, @errorNumber int,
-— -@msg varchar(255), --@errorSeverity varchar(20), -@triggerName sysname --
размещение числа измененных строк используется для размещения кода ошибки после выполнения оператора ЯМД используется для размещения сообщения об ошибке степень ошибки имя триггера
SET SrowsAffected = @@rowcount SET @triggerName = object__name (@@procld)
-- используется для сообщений
-- не нужно продолжать, если нет измененных строк IF @rowsAffected - 0 RETURN <здесь помещаются фрагменты> END
Мы будем обычно писать наши триггеры так, что, когда произойдет ошибка, то сформируем ошибку и выполним откат транзакции, чтобы остановить любые дальнейшие команды. Каскадирование
добавлений
При каскадировании добавлений мы имеем ситуацию, когда после того, как запись добавлена в таблицу, одна или несколько других новых записей автоматически добавляется в другие таблицы. Это особенно валено, когда мы создаем принудительно отношения "один к одному" или "один ко многим". Например, пусть мы имеем следующий набор таблиц: urIStatus urIStatusId: int NOT NULL urIStatusTypeld: int NOT NULL (FK) urlld: int NOT NULL (FK) t I TP I I
I url I urlld: int NOT NULL url: varchar(4OO) NOT NULL
I urlStatusType I urIStatusType: int NOT NULL name: varchar(60) NOT NULL defaultFI: bit NOT NULL
Здесь urIStatus — таблица "состояние URL"; urIStatusId — идентификатор состояния URL; urIStatusTypeld — идентификатор типа состояния URL; URL — таблица "адрес в Интернете"; urlld — идентификатор URL; urlStatusType — таблица "тип состояния URL"; name — название (типа состояния URL); defaultFI — флаг задания по умолчанию.
404
Обеспечение целостности данных В этом случае таблица u r l определяет множество URL для нашей системы, в то время как отношение между u r l и u r l S t a t u s — "один ко многим". Мы начинаем построение триггера, который вставляет строку в таблицу u r l S t a t u s с помощью оператора INSERT, который создает новую запись с u r l l d и urlStatusType, используемым по умолчанию на основе d e f a u l t F l , имеющего значение 1. (Мы пока предположим, что имеется максимум одна строка с d e f a u l t F l , равным 1, и выполняется проверка, что это именно так.) -- добавление записи к таблице urlStatus, сообщая ей, что эта новая запись — должна вначале рассматриваться как состояние по умолчанию INSERT INTO u r l S t a t u s ( u r l l d , urlStatusTypeld) SELECT INSERTED.urlld, u r l S t a t u s T y p e . u r l S t a t u s T y p e l d FROM INSERTED CROSS JOIN u r l S t a t u s T y p e
WHERE u r l S t a t u s T y p e . d e f a u l t F l
— -— =
использование CROSS JOIN с оператором WHERE, к а к в этом с л у ч а е , фактически не я в л я е т с я объединением INSERTED и u r l S t a t u s T y p e 1
SET @errorNumber = @@error IF @errorNumber <> 0 BEGIN SET @msg = 'Ошибка: '
'
+ CAST(@errorNumber AS v a r c h a r ( 1 0 ) ) +
возникла
во время
создания
строки таблицы
urlStatus'
RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END
Триггер мог бы также быть необходим для запрещения удаления urlStatusType, используемого по умолчанию, так как мы будем всегда хотеть иметь значение urlStatusType, используемое по умолчанию. Однако формирование такого триггера создаст ситуацию, где мы хотим: •
удалить строки таблицы u r l , но не можем из-за существующих строк таблицы urlStatus;
•
удалить строки таблицы u r l S t a t u s , но не можем, так как они — последние для строк u r l .
Это — трудная ситуация для решения с использованием обычных триггеров и ограничений, и может быть легче решена, добавляя поле к таблице, сообщающее триггерам (или ограничениям) что запись доступна для удаления. Это — общая проблема в случае таблиц, где вы хотите осуществить двустороннее отношение, но при некоторых обстоятельствах хотите реализовать одностороннее отношение.
Проверка диапазона Проверка диапазона просто означает, что определенный диапазон данных соответствует правилам проверки данных. Некоторые примеры: •
Составление баланса счета, чтобы удостовериться, что нет отрицательного баланса в результате сделки.
•
Обеспечение того, что для отношения существует соответствующее число записей (мощность), типа отношения "один к между пятью и семью".
•
Проверка, что в таблице (или в группе строк в таблице) существует только единственная запись, которая является первичной или используемой по умолчанию. 405
Глава 11 В нашем примере мы убедимся, что в таблице имеется не больше одного установленного по умолчанию флага, равного True или 1; другие должны быть 0 или False. Мы также поместим код (что является хорошим стилем), который будет брать любое новое значение, где поле def a u l t F l равно 1, и устанавливает все другие в 0. Если пользователь вручную установит больше одного значения def a u l t F l в 1, то проверка, сделанная впоследствии, должна отменить эту операцию. Мы будем использовать в качестве примера таблицу u r l S t a t u s T y p e , которую создали ранее: urlStatusType urlStatusType: int NOT NULL name: varchar(60) NOT NULL defaultFI: bit NOT NULL и выполним проверку диапазона следующим образом: — если столбец defaultFI был модифицирован IF UPDATE(defaultFI) BEGIN — обновление любой другой строки таблицы urlStatusType, задавая — значения, не равные значению по умолчанию, если введенная строка — установлена по умолчанию UPDATE urlStatusType SET defaultFI = 0 FROM urlStatusType — только строки, уже установленные по умолчанию WHERE urlStatusType.defaultFl = 1 — и не вставленная строка AND urlStatusTypeld NOT IN (SELECT urlStatusTypeld FROM INSERTED WHERE defaultFI = 1
SET @errorNumber • @@error IF @errorNumber <> 0 BEGIN SET @msg = 'Ошибка: ' + CAST(@errorNumber AS varchar(lO)) + ' возникла при обновлении значений defaultFI' RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END — смотрим, нет ли более одной строки, установленной по умолчанию, если — пользователь установил их в одной операции IF ( SELECT COUNT (*) FROM urlStatusType WHERE urlStatusType.defaultFl - 1) > 1 BEGIN SET @msg = 'Слишком много строк с флагом по умолчанию = 1' RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END END
406
Обеспечение целостности данных
Каскадирование удалений установкой значений таблицы-потомка в NULL Когда мы имеем таблицы с необязательными вторичными ключами, вместо каскадирования, когда удаляется предок, мы можем иногда пожелать удалить связь и установить все ссылки внешнего ключа в NULL. Чтобы сделать это, в триггере, связанном с оператором DELETE, мы просто обновляем каждую строку в таблице-потомке, которая ссылается на значение в строке (строках), которую мы удаляем. Возьмем, например, следующее отношение: company companyld: int NOT NULL name: varchar(60) NOT NULL urlld: int NULL (FK)
•
1 1 1 1 url 6 urlld: int NOT NULL url: varchar(400) NOT NULL
Здесь в дополнение к предыдущим обозначениям company — таблица "компания"; companyld — идентификатор компании; name — название (компании). Таблица u r l содержит все адреса Интернета для нашей системы, и таблица company использует одно из значений для каждой компании. Если мы хотим удалить одну или несколько записей таблицы u r l , нам не нужно удалять все компании, которые используют эти значения URL. Это мы могли бы реализовать, задавая строкам-потомкам значения NULL вместо удаления самих строк. Так как SQL Server осуществляет каскадирование только удалением, когда удаляется строка-потомок, мы должны использовать триггер. Код триггера, который делает это: UPDATE company SET urlld = NULL FROM DELETED JOIN company ON DELETED.urlld - company.urlld SET @errorNumber = @@error IF @errorNumber <> 0 BEGIN SET @msg - 'Ошибка: ' + CAST(@errorNumber AS varchar(lO)) + 'возникла во время каскадного удаления установкой в NULL' RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END
Триггеры INSTEAD OF Триггеры INSTEAD OF запускаются вместо операций манипулирования с данными и до того, как выполняются ограничения-проверки. Вы можете иметь только единственный триггер INSTEAD OF для каждой операции в каждой таблице. Обратите внимание, что хотя этот триггер обычно упоминается как "триггер до...", он означает нечто большее. При кодировании триггера INSTEAD OF мы должны переделать операцию в триггере. Например, в случае операции INSTEAD OF DELETE триггер содержит удаление точно так же как операция, которая первоначально запускает триггер. 407
Глава 11 Следующий пример показывает триггер, который возвращает состояние таблицы INSERTED и затем физической таблицы. CREATE TRIGGER artist$insteadOfInsert ON artist INSTEAD OF INSERT AS -- вывод содержимого таблицы INSERTED SELECT 'содержимое таблицы INSERTED' SELECT * FROM INSERTED -- вывод содержимого физической таблицы SELECT 'содержимое таблицы artist' SELECT * FROM artist GO
Далее, если мы выполним добавление в таблицу: I
INSERT INTO artist(name) VALUES ('John Tesh')
... то получим следующий результат содержимое таблицы INSERTED artistld
name
0
John Tesh
содержимое таблицы artist artistld
name
1 2
THE BEATLES ... THE WHO
Это — точно то, что мы ожидали; триггер запускается перед тем, как столбцы будут помещены в таблицу, что объясняет, почему запись John Tesh (Джон Теш) не появилась во втором результате. Однако, если мы посмотрим содержимое таблицы после выполнения триггера: I
SELECT artistld, name FROM artist мы получим: artistld 1 2
408
name THE BEATLES ... THE WHO
Обеспечение целостности данных Данный результат иллюстрирует одну трудность с триггерами INSTEAD OF, а именно, что любые вставки добавляются к таблице INSERTED или DELETED, а не к БД. Это означает, что мы должны вручную вставить данные в таблицу, чтобы выполнить работу триггера должным образом. Поэтому триггеры INSTEAD OF вынуждают нас полностью повторно изобрести путь, которым мы закодируем операции по манипуляции данными, чтобы максимально использовать их. В оставшейся части этого раздела мы рассмотрим некоторые другие проблемы триггеров INSTEAD OF, связанные с парой важных использований, которые делают их очень полезным дополнением к комплекту средств SQL Server. Предостережения Проблемы, связанные с триггерами INSTEAD OF, следующие: •
Вы должны повторить операцию, выполняемую пользователем;
•
нелегко выполнить контроль границ для нескольких строк;
•
триггеры INSTEAD OF игнорируют операции каскадирования.
Мы рассмотрим каждую из трудностей несколько глубже в следующих разделах. Вы должны повторить операцию, выполняемую пользователем В следующем триггере мы показываем простую демонстрацию этого: CREATE TRIGGER a r t i s t $ i n s t e a d O f I n s e r t
ON a r t i s t
INSTEAD OF INSERT AS
-- должны имитировать операцию, которую мы выполняем в триггере INSTEAD OF. -- заметим, что нужно определить список столбцов для размещения данных — и мы не можем просто выполнить SELECT * FROM INSERTED из-за -- столбца идентификации, который не допускает помещения в него значения. INSERT INTO artist(name, defaultFl, catalogNumberMask) SELECT name, defaultFl, catalogNumberMask FROM INSERTED GO CREATE TRIGGER artist$insteadOfDelete ON artist INSTEAD OF DELETE AS -- должны имитировать операцию, которую мы выполняем в триггере INSTEAD OF. DELETE FROM artist FROM DELETED -- мы всегда присоединяем таблицы INSERTED и DELETED -- к реальной таблице через первичный ключ. JOIN ARTIST ON DELETED.artistld - artist.artistld GO
409
Глава 11 Создав триггеры, чтобы работать вместо операций INSERT и DELETE, мы проверим их: INSERT INTO artist(name, defaultFl, catalogNumberMask) VALUES ('raffi', 0, '%') Щ
SELECT * FROM artist WHERE name = 'raffi'
... что возвратит: artistld name ... 14
raffi
...
Затем мы удалим строку и выполним оператор SELECT, чтобы проверить, существует ли она: DELRTE FROM a r t i s t WHERE name = ' r a f f i '
I
SELECT * FROM a r t i s t WHERE name = ' r a f f i '
что ничего не возвратит, так как удаление выполнено правильно. Нелегко выполнить контроль границ для нескольких строк Так как в триггерах AFTER данные уже были добавлены к таблице, легко осуществить контроль, если будет многострочное ограничение. Однако триггер INSTEAD OF не размещает данные в БД. В следующих двух примерах кода мы рассмотрим два различных метода для выполнения проверки, что таблица имеет неотрицательный баланс для поля amount (сумма) в таблице a c c o u n t T r a n s a c t i o n , где группируются балансы для одних и тех же значений accountld (идентификатор счета). Триггер AFTER для сравнения IF EXISTS (SELECT accountld FROM accountTransaction JOIN (SELECT DISTINCT accountld FROM INSERTED) AS accounts ON accountTransaction.accountld • accounts.accountld GROUP BY accountld HAVING sum(amount) > 0) BEGIN RAISERROR 50000 'При операции возникает отрицательный баланс' END
Триггер INSTEAD OF IF EXISTS (SELECT accountld FROM — операция UNION помещает величины сделок для — всех счетов, зависящих от операции (SELECT accountTransactionld, accountld, amount FROM INSERTED UNION SELECT accountTransactionld, accountld, amount FROM accountTransaction
410
Обеспечение целостности данных JOIN (SELECT DISTINCT accountld FROM INSERTED) AS accounts ON accountTransaction.accountld = accounts.accountld — удаление всех записей accountTransaction, которые мы обновляем, — удаляя строки, которые теперь находятся в таблице DELETED WHERE accountTransactionld NOT IN ( SELECT accountTransactionld FROM DELETED) ) AS accountValues GROUP BY accountld HAVING sum(amount) > 0) BEGIN RAISERROR 50000 'При операции возникает отрицательный баланс' END В триггере AFTER все, что требуется сделать — проверить текущее состояние таблицы, в то время как в триггере INSTEAD OF мы должны добавить значения из таблицы INSERTED к значениям фактической таблицы. Одна интересная вещь, на которую следует обратить внимание, что это не простой запрос к таблице INSERTED. Мы всегда должны рассматривать все строки текущей таблице, которые связаны со строками в таблице INSERTED, что требует комплексного оператора JOIN. Триггеры INSTEAD OF игнорируют операции каскадирования Одна из наиболее неприятных проблем триггеров INSTEAD OF — то, что ими игнорируются операции каскадирования, так что использование триггеров INSTEAD OF в обеспечении бизнес-правил может оказаться проблематичным. Например, если бы в данной таблице первичный ключ изменил значение так, что он перестал бы соответствовать проверкам триггера INSTEAD OF, такое изменение не будет предотвращено, так как триггер не будет запускаться. Эти ограничения означают, что триггеры INSTEAD OF не должны использоваться ни для проверки бизнес-правил, которые включают первичный ключ, ни для проверки бизнес-правил, которые включают удаления. Но это не запрещает использовать их для других целей, к которым мы теперь перейдем.
Использования триггеров INSTEAD OF Триггеры INSTEAD OF могут использоваться, чтобы автоматически задавать или изменять значения в наших операторах. Зная, что триггеры INSTEAD OF запускаются и до проверки ограничений, и до триггеров AFTER, они могут использоваться для текущих изменений данных в таблице. Мы рассмотрим три примера того, как они могут использоваться: Автоматическое поддержание полей В качестве примера мы собираемся построить два триггера: INSTEAD OF с оператором INSERT и INSTEAD OF с оператором UPDATE, которые будут автоматически устанавливать первую прописную букву в именах наших артистов вместо формата, содержащего только строчные буквы: CREATE TRIGGER artist$insteadOfInsert ON artist INSTEAD OF INSERT AS INSERT INTO artist(name, defaultFl, catalogNumberMask) SELECT dbo.string$properCase(name), defaultFl, catalogNumberMask FROM INSERTED GO Продолжение кода на следующей странице 411
Глава 11 CREATE TRIGGER artist$insteadOfUpdate ON artist INSTEAD OF UPDATE AS UPDATE artist SET name = string$properCase(INSERTED, name), defaultFl = INSERTED.defaultFl catalogNumberMask = INSERTED.catalogNumberMask FROM artist JOIN INSERTED ON artist.artistld - INSERTED.artistld GO
Чтобы проверить наш триггер с оператором INSERT, выполним следующий оператор: — помещаем очевидно неправильно сформатированное имя INSERT INTO artist (name, defaultFl, catalogNumberMask) VALUES CeLvIs CoStElLo', 0, '77 ') — затем извлекаем последнюю помещенную величину в эту таблицу SELECT artistld, name FROM artist WHERE artistld = ident_current('artist')
... который возвращает требуемый результат: artistld 19
name Elvis Costello
Далее мы проверим триггер с оператором UPDATE. Сначала проверим, являются ли все величины в таблице правильными:
I
SELECT artistld, name FROM artist
Запрос возвратит список неаккуратно сформатированных строк: artistld
name
19 1 15 2
Elvis Costello THE BEATLES The Monkees THE WHO
Далее мы запустим следующий запрос, который формирует набор имен, написанных прописными буквами:
I
UPDATE artist SET name = UPPER(name)
Однако, мы видим, что все строки не содержат прописные буквы, хотя и сформатированы аккуратным образом:
412
Обеспечение целостности данных artistld
name
19 1 15 2
Elvis Costello The Beatles The Monkees The Who
Триггеры INSTEAD OF — лучший способ выполнить такие манипуляции с данными, так как они спасают нас от размещения неудачных данных, что требует выполнить дополнительный шаг с помощью триггера AFTER для их обновления. Всякий раз, когда вы должны расширить универсальные действия, которые выполняют операции INSERT, UPDATE или DELETE, использование триггеров INSTEAD OF является лучшим способом. Условное добавление Во время нашей демонстрации обработки ошибки была создана групповая операция добавления записей, которая добавляла в таблицу альбомы на основе значения catalogNumber (номер каталога), если только маска, которую мы создали, соответствует артисту. Сейчас мы построим более простой пример, где таблица (или таблицы) в такой системе будет допускать любые данные, вводимые пользователем, но при этом размещая любые неправильные данные в таблице обработки ошибок, так что можно будет позже рассмотреть все проблемы. Сначала мы должны удалить первоначальное ограничение: ALTER TABLE album DROP CONSTRAINT chkAlbum$catalogNumber$function$artist$catalogNumberValidate
В этот момент мы не защищены от любых неправильных значений, помещаемых в поле catalogNumber. Но мы создадим триггер INSTEAD OF, который будет брать все правильные строки и вставлять их в таблицу album (или обновлять их). Если же они неправильные, мы поместим их в новую созданную нами таблицу, которую назовем albumException (ошибочный альбом) и которая будет иметь ту же самую структуру, как и таблица album с другим значением первичного ключа и новым полем, называемым o p e r a t i o n (операция) для фиксации добавления или обновления. CREATE TABLE albumException albumExceptionld i n t NOT NULL IDENTITY, name v a r c h a r ( 6 0 ) NOT NULL, a r t i s t l d i n t NOT NULL, catalogNumber char (12) NOT NULL, exceptionAction c h a r ( l ) , exceptionDate d a t e t i m e
Здесь albumException — ошибочный альбом; albumExceptionld — идентификатор ошибочного альбома; name — название (альбома); artistld — идентификатор артиста; catalogNumber — номер каталога; exceptionAction — операция с ошибочным альбомом; exceptionDate — дата ошибки. Теперь создадим простой триггер INSTEAD OF, чтобы перехватить данные пользователя и попытаться проконтролировать номер каталога. Если данные правильные, мы обновляем таблицу, в противном случае они будут добавлены в таблицу ошибок.
413
Глава 11 CREATE TRIGGER album$insteadOfUpdate ON album INSTEAD OF UPDATE AS DECLARE @errorValue int
—
это переменная для размещения состояния ошибки
UPDATE album SET name = INSERTED.name, artistld = INSERTED.artistld, catalogNumber = INSERTED.catalogNumber FROM INSERTED JOIN album ON INSERTED.albumld = album.albumld — обновляем только строки, где встречен критерий WHERE • dbo.album$catalogNumberValidate(INSERTED.catalogNumber, INSERTED.artistld) = 1 — выполнение в этом операторе некоторых проверок, что нет ошибки SET @errorValue - @@error IF @errorValue <> 0 BEGIN RAISERROR 50000 'Ошибка в добавлении правильной записи альбома' ROLLBACK TRANSACTION RETURN END -- получение всех строк, где критерий не встретился INSERT INTO albumException(name, artistld, catalogNumber, exceptionAction, exceptionDate) SELECT name, artistld, catalogNumber, 'U', getdate() FROM INSERTED WHERE NOT( -- обычно самый простой способ скопировать критерий — и выполнить NOT(WHERE ...) dbo.album$catalogNumberValidate (INSERTED.catalogNumber, INSERTED.artistld) = 1 ) SET SerrorValue = @@error IF @errorValue <> 0 BEGIN RAISERROR 50000 'Ошибка в регистрации неправильных записей альбомов' ROLLBACK TRANSACTION RETURN END GO
Теперь обновление соответствующей величины в строке можно просто выполнить с помощью следующих операторов: — обновление таблицы album с явно хорошим совпадением номера каталога UPDATE album SET catalogNumber = '222-22222-22' WHERE name = 'the white album' — затем перечисляем artistld и catalogNumber альбомов в "реальной" таблице SELECT artistld, catalogNumber FROM album
414
Обеспечение целостности данных WHERE name - 'the white album' — а также в таблице ошибок SELECT artistld, catalogNumber, exceptionAction, exceptionDate FROM albumException WHERE name - 'the white album'
Номер каталога соответствует тому, что мы должны получить, и поэтому нет ошибок в таблице albumException: artistld
catalogNumber
1
222-22222-22
artistld
catalogNumber
exceptionAction
exceptionDate
Затем м ы сделаем очевидное неправильное обновление: UPDATE album SET catalogNumber = '1' WHERE name = 'the white album' -- затем перечисляем artistld и catalogNumber альбомов в "реальной" таблице SELECT artistld, catalogNumber FROM album WHERE name = 'the white album' -- а также в таблице ошибок SELECT artistld, catalogNumber, exceptionAction, exceptionDate FROM albumException WHERE name = 'the white album'
Мы видим, что нет скорректированных строк и не возвращены никакие сообщения об ошибках, однако теперь добавили строку к нашей таблице ошибок, обращая внимание на ошибку: artistld
catalogNumber
I
222-22222-22
artistld
catalogNumber
exceptionAction
exceptionDate
II U 2001-01-07 01:02:59.363 Оставим читателю создание триггера INSTEAD OF с операцией INSERT в дополнение к триггеру с операцией UPDATE. Мы также, вероятно, хотели бы расширить таблицу ошибок, включив некоторый вид причины ошибки, если может быть более одной возможности. Представления Обычно выполнение добавлений, обновлений и удалений в представлениях затруднено, потому что должны выполняться следующие условия вьшолнении операторов корректировки представления: •
Операторы UPDATE и INSERT могут лишь тогда корректировать представление, если они ссылаются на столбцы только одной таблицы одновременно. 415
Глава 11 •
Операторы DELETE могут использоваться лишь тогда, когда представление ссылается только на одну таблицу.
Однако используя триггеры INSTEAD OF, мы можем реализовать механизм операторов INSERT, UPDATE и DELETE для представлений. В этом примере мы создадим чрезвычайно простое представление и поместим триггер для организации добавлений: -- создание представления без столбца defaultFl, так как не следует -- пользователю видеть его в представлении, кроме того хотим — представить имена большими буквами CREATE VIEW vArtistExcludeDefault AS SELECT artistid, UPPER(name) AS name, catalogNumberMask FROM artist GO -- далее мы создаем очень простой триггер INSTEAD OF для добавления CREATE TRIGGER vArtistExcludeDefault$insteadOfInsert ON vArtistExcludeDefault INSTEAD OF INSERT AS BEGIN -- заметим, что мы не используем поле artistid из таблицы INSERTED INSERT INTO artist(name, catalogNumberMask, defaultFl) SELECT name, catalogNumberMask, 0 -- используя представление можно — установить defaultFl только в О FROM INSERTED END GO
Затем мы просто используем оператор INSERT так же как и для таблицы (исключая столбец идентификаторов, для которого мы не можем задать значение):
I
INSERT INTO vArtistExcludeDefault(name, catalogNumberMask) VALUES('The Monkees', '44 ')
Однако представление ведет себя иначе: Server: Msg 233, Level 16, State 2, Line 1 The column 'artistid 1 in table VArtistExcludeDefault' cannot be NULL. {Сервер: сообщение 233, уровень 16, состояние 2, строка 1 Столбец artistid таблицы vArtistExcludeDefault не может иметь значение NULL) Это не то, что мы ожидали или хотели иметь. Поэтому мы должны переформулировать наш оператор INSERT, чтобы включить идентификатор a r t i s t i d и задать ему неправильное значение. Теперь добавленный запрос работает правильно:
I
INSERT INTO vArtistExcludeDefault(artistid, name, catalogNumberMask) VALUES (-1, 'The Monkees', '44 ') SELECT * FROM vArtistExcludeDefault WHERE artistid = ident current('vArtistExcludeDefault')
416
Обеспечение целостности данных
и возвращает значение, которое мы предполагали иметь: artistld
name
defaultFI
catalogNumberMask
15
THE MONKEES
0
44
Следует отметить, что если у нас в представлении используются две или больше таблиц, и мы выполняем операцию INSERT для представления, то могли бы поместить данные во все таблицы, которые составляют представление. При этом нужно аккуратно обойти требование, что операторы INSERT и UPDATE, используемые для представлений, могут иметь дело только с единственной таблицей одновременно, которое является очень тяжелым. (Дальнейшие детали относительно представлений можно найти в следующей главе.)
Код клиента и хранимые процедуры В последнее время появилось программное средство перемещения большинства бизнес-правил и кода защиты данных из SQL Server в набор интерфейсных объектов промежуточного уровня. Таким образом, БД, клиент и бизнес-правила размещаются в трех независимых реализующих их единицах. В этом случае бизнес-правила, о которых мы, возможно, думали, что их хорошо реализовать через ограничения и триггеры, перемещаются из этого слоя "данных" в код, использующий клинетскую часть типа СОМ-объекта или хранимых процедур. Такой многоуровневый проект также предназначен облегчить жизнь пользователю, так как пользователи редактируют данные, используя обычные клиентские средства, в то время как сервисы промежуточного уровня обеспечивают защиту данных, которые проходят через них, таким образом изолируя пользователей от всего, что требует SQL-кода. Но не только это, сервисы могут также непосредственно управлять любыми ошибками, которые формирует и выдает пользователь, создавая осмысленные сообщения об ошибках. Так как прикладные пользователи прежде всего редактируют строки по одной, нежели сразу большое количество строк, это на практике работает достаточно хорошо. Другой момент заключается в том, что в большинстве приложений для предприятий (например, ситуации с сотнями тысяч пользователей Web-сайта) БД обычно рассматриваются как "узкое место системы". Хотя и возможно разместить нагрузку на единственном сервере, во многих случаях может быть намного легче распределить нагрузку на несколько серверов приложения, чего мы коснулись в главе 9. Однако почти любой механизм защиты данных, который сформирован без использования ограничений или триггеров, может оказаться проблематичным. Давайте рассмотрим наш список возможных пользователей, которых мы ввели в самом начале главы, а именно: •
Пользователи, использующие традиционные интерфейсные средства. Когда все пользователи применяют традиционные клиентские средства, разработанные для интерфейса, не возникает никаких проблем с использованием промежуточного уровня. Фактически это может дать большие преимущества, поскольку, как мы обсудили, методы объекта, используемые для реализации бизнес-правил, могут привести к уменьшению производительности.
417
Глава 11 •
Пользователи, использующие настраиваемые средства работы с данными типа Microsoft Access. Рассмотрим случай, когда пользователь должен изменять набор "живых" данных, но нуждается в этом только в течение недели или уикэнда, и нет времени написать полноценное приложение. Мы не сможем допустить непосредственный доступ к данным, поскольку они находятся "в сыром незащищенном состоянии. Следовательно, мы будем должны или закодировать соответствующее бизнес-правило в БД Access, или отказать пользователю и заставить его ждать, пока не будет разработано приложение. Эта ситуация относительно редка, и мы обычно строго запрещаем такой доступ.
•
Программы, которые импортируют данные из внешних источников. Почти каждая система любого объема будет включать некоторые средства импортирования данных в сыром формате из внешних систем, возможно, от другой части вашей компании или вообще из другой компании и размещать эти данные в таблице на сервере БД. Это может быть в столь же простой форме, как и приложение пользователя для импортирования электронной таблицы, или в столь же сложной форме, как интерфейс между всеми школами в штате и соответствующим департаментом образования штата. Средства имеют диапазон от приложений пользователя до DTS (сервисы преобразования данных), или даже ВСР (Bulk Copy Program — программа массового копирования), которая поступает совместно с SQL Server. Когда средний уровень включает все бизнес-правила и проверки целостности данных, мы должны будем или проходить через средний уровень (часто по одному), или извлекать все бизнес-правила и кодировать их в наших программах импортирования.
•
Необработанные запросы, выполняемые администраторами данных, чтобы выявить проблемы, связанные с ошибками пользователя. Почти любой с опытом администрирования был вынужден удалять несколько строк из БД, которые пользователи ошибочно создали, но не мог удалить и по ошибке удалил не те записи, скажем, активные записи счета вместо неактивных. В этой ситуации, если мы имеем бизнес-правила, включенные в триггер, который позволяет удаление только неактивных счетов, пользователю будет возвращено сообщение об ошибке, предупреждающее, что активные счета не могут быть удалены. Очевидно, что мы не можем защититься от сугубо неправильного действия, типа систематического удаления любой строки в таблице, но когда реализована полностью разработанная БД и защищены данные, используя ограничения и триггеры, почти невозможно сделать даже маленькие ошибки в целостности данных.
Как архитектор данных, я предпочитаю возможности, предлагаемые многоуровневыми разработками. Однако бремя бизнес-правил и реализация правил целостности данных должны быть "распределены" между промежуточным уровнем и сервером БД соответствующим образом. Два очень конкретных типа таких правил, которые лучше всего реализовать в БД, следующие: •
418
Любое правило, которое может иметь значение NULL, быть внешним ключом или ограничением-проверкой — Это происходит вследствие того, что при создании дополнительных внешних интерфейсов такой вид проверок будет обычно требовать довольно немного усилий по кодированию. Кроме того, целостность БД нужно гарантировать на самом низком из возможных уровней, который позволит закодировать столько программ, сколько возможно для БД. Как заключительный аргумент, эти ограничения будут использоваться оптимизатором, делающим работу запросов быстрее.
Обеспечение целостности данных Q
Правила, которые требуют межтабличных проверок — Это случай, когда при сохранении значения мы должны проверить, существует ли значение в других таблицах. К дополнительной таблице следует обращаться автоматически, чтобы проверить, что ожидаемое состояние все еще существует. В некоторых случаях промежуточный уровень будет стараться кэшировать данные БД, чтобы использовать их при проверках, но нет никакого способа распространить эти кэшированные данные на несколько серверов таким образом, что значение, которое мы вводим, имеет надлежащую целостность данных.
Рассмотрев некоторые из возможных недостатков кодирования на промежуточном уровне, разберем теперь случаи, когда хранимая процедура или код пользователя — оптимальное место для размещения бизнес-правил.
Изменчивые правила Эти правила таковы, что для данного набора критериев в одном случае, когда они встречаются, правило принимает значение True, и действие выполняется, в то время как в другом случае правило принимает значение False, и выполняется другое действие. Лучшее лакмусовое испытание для такого правила — посмотреть, может ли пользователь обойти его. Например, в нашем примере из этой главы мы могли бы попытаться реализовать правило: "Пользователь должен ввести соответствующего артиста для альбома". Напомним, что мы имеем таблицу album, которая имеет отношение внешнего ключа (который не может принимать значение NULL) к таблице a r t i s t . Следовательно, мы должны поместить подходящее значение a r t i s t l d , когда изменяем таблицу. Отсюда мы имеем два направления действий. •
Сделать поле a r t i s t l d в таблице album таким, что оно может принимать значение NULL, и, следовательно, необязательным, позволяя пользователю выбрать альбом без задания значения a r t i s t l d . Чтобы следовать правилу "должен", интерфейс вынужден тогда организовать что-то вроде диалога, спросив пользователя: "Вы уверены, что не хотите назначить артиста для этого альбома?"
•
В качестве другого варианта мы могли бы перефразировать бизнес-правило следующим образом: "пользователь мог бы ввести и неправильного артиста". Это означало бы, что БД могла бы позволить любое значение a r t i s t l d и действительно позволяет приложению клиента проверять значение, чтобы увидеть, правильно ли оно, и затем организовать диалог: "Вы уверены, что не хотите назначить артиста для этого альбома? " или, что еще хуже: " Вы ввели неправильное 3Ha4eHneartistId и должны ввести правильное". Мы были бы вынуждены пропустить проверку соответствия в БД в случае, если пользователь говорит: "Ха, пусть будет введено неправильное значение."
Особенность, которую я стараюсь здесь отметить, заключается в том, что SQL Server не может организовывать диалог с пользователем. Жесткий и неизменчивый триггер и проверка ограничения все еще зависят в значительной степени от процесса передачи запроса и ожидания, успешно ли они завершены, и мы нуждаемся в более интерактивном методе связи, где можем влиять на события после того, как запрос был представлен, и до того, как результат будет возвращен. В следующих примерах мы рассмотрим ситуации, связанные с правилами, которые не могут быть удовлетворительно реализованы с помощью триггеров и ограничений.
419
Глава 11 Как известно, кто ищет, тот всегда найдет. Это возможно, используя временные таблицы как механизм сообщений, чтобы "передавать" значения триггеру или ограничению. В этом случае мы можем произвольно корректировать функциональные возможности. Однако триггеры и ограничения обычно не считаются удобными для реализации меняющихся бизнес-правил. Необязательное каскадирование удалений Каскадированные удаления имеют большие возможности, но вы должны использовать их с осторожностью. Как мы обсудили, они автоматически удаляют записи-потомки, которые зависят от содержания удаленной записи-предка, и нам редко требуется предупреждать пользователя относительно существования этих записей-предков прежде, чем они будут удалены. Однако это не было бы лучшим вариантом в случае банковского счета. Пусть мы имеем следующие таблицы: account
accountProperty - -
ш
i transactic n
Здесь account — таблица "счет"; accountProperty transaction — таблица "сделка".
— таблица "свойства счета";
В этом случае будет иметь смысл автоматически каскадировать удаления записей таблицы accountProperty, если мы хотим удалить запись из таблицы account. Однако если счет связан с таблицей t r a n s a c t i o n , мы должны гарантировать, что пользователь знает об этом, и, следовательно, предупреждать его, если требуется каскадирование удаления. Это, конечно, увеличит сложность, так как мы не сможем удалить счет, его свойства и сделки в единственном операторе. Вместо единственного оператора мы будем должны выполнить следующие шаги: •
Запустить оператор SELECT для каждой таблицы-потомка, которую мы хотим необязательно каскадированно удалить, чтобы показать пользователю, что там существует.
•
Если пользователь соглашается, выполнить оператор DELETE для каждой таблицы-потомка, которая имела записи, связанные с нашей первичной таблицей, и в этой же элементарной операции удалить нужную строку.
Этот код мог бы быть включен в отдельную хранимую процедуру, которая проверяет потомков, и если они существуют, возвращает набор строк для пользователя, чтобы видеть, какие из них следует удалить. Она будет также включать параметр, позволяющий пользователю игнорировать существование таких строк, продолжить и удалить их. Следующий момент — как мы можем управлять этой ситуацией (обратите внимание, что для ясности примера мы удалили транзакции и обработку ошибок, которые будут подробно рассмотрены в следующей главе). 420
Обеспечение целостности данных CREATE PROCEDURE account$delete ( @accountld int, @removeChildTransactionsFl bit = 0 AS — если просят удалить запись, то удалить ее IF @removeChildTransactionsFl = 1 DELETE [transaction] -- имя таблицы совпадает с ключевым словом WHERE accountld = @accountld ELSE -- проверить существование BEGIN IF EXISTS (SELECT * FROM [transaction] WHERE accountld = @accountld) BEGIN RAISERROR 50000 'Потомок в таблице transaction существует' RETURN -100 END END DELETE account WHERE accountld = gaccountld
Здесь создается процедура с двумя передаваемыми параметрами: Qaccountld типа int, в котором передается идентификатор счета, который должен быть удален, и QremoveChildTransactionsFl типа bit для передачи флага удаления записи-потомка (если равен 1 или True, запись должна быть удалена). В этой процедуре проверяется значение флага GremoveChildTransactionsFl, и если он равен 1, то запись из таблицы-потомка transaction удаляется. Если же этот флаг равен 0, проверяется, существует ли соответствующая запись в таблице-потомке, и если существует, пользователю выводится сообщение, что такая запись существует, но само удаление не происходит и происходит выход из процедуры. В заключение процедура удаляет запись из таблицы-предка account. Теперь пользователь может попробовать выполнить хранимую процедуру с флагом, равным О, и если какие-либо потомки существуют, пользователь получит уведомление. Если же нет, счет будет удален, и, возможно, также и свойства будут удалены через отношение каскадирования.
Правила, основанные на том, как представляет их человек Это печальная действительность для системного архитектора, которую нужно учитывать. В типичной системе мы будем иметь много бизнес-правил, которые кажутся жесткими и неизменчивыми и которые нельзя нарушить. Однако большое количество изменчивых правил выглядят жесткими и неизменчивыми. В качестве примера рассмотрим следующее утверждение: У компании политика такова, что мы не отправляем изделие заказчику, пока не получим оплату. Оно не кажется проблематичным, пока не возникнет просьба об отгрузке как жест доброй воли.
421
Глава 11 В качестве еще одного примера рассмотрим следующее правило: Изделия будут отправлены в той же последовательности, в какой были получены заказы. Рассмотрим случай системы управления качеством, которая имеет дело с изделием Y. Пусть ограничение применяется так, что данная спецификация заказчика должна быть выполнена, иначе изделие не может быть отправлено заказчику. Таким образом, пользователь вызывает и запрашивает большую отгрузку, а клерк, который управляет этими заказами, вызывает интерфейс пользователя, чтобы выбрать некоторое изделие Y, чтобы его отправить. Однако нет достаточного количества изделий, чтобы выполнить заказ. Когда клерк связывается с заказчиком, чтобы сообщить ему об этой проблеме, заказчик обычно просит отправить подходящую продукцию вместо ожидания, когда будет большее количества изделия Y. При этом клерк теперь обнаруживает, что в соответствии с бизнес-правилами он не может обработать этот запрос и должен послать его менеджеру заказов для выполнения. Использование хранимой процедуры в этой задаче могло бы позволить пользователям различных групп безопасности отменить данные назначения, когда это нужно. Таким образом, клерк может получить список изделия Y для отправки, выбрать материалы, добавить их к заказу и нажать кнопку для начала отгрузки изделий, вызывая хранимую процедуру p r o d u c t O r d e r $ s h i p (порядок отправки изделий). Эта процедура должна проверить, что изделия находятся в пределах спецификаций заказчика, и затем установить порядок отправки. Если изделие выходит за пределы спецификации, процедура отменит пользователя. Однако менеджер заказов будет иметь возможность выполнить другую хранимую процедуру, например, productOrder$shipOutOf Spec (заказ продукта, отправка за границами спецификации), которая позволила бы пользователю отправить изделия, находящиеся за границами спецификации. Два замечания к такому сценарию: •
Интерфейс должен обеспечивать информацией, что изделие оказалось за пределами спецификации и не разрешать пользователю выполнять процедуру productOrder$shipOutOf Spec, но знание этого могло бы быть основано на запросе прав каждой хранимой процедуры (используя функцию PERMISSION).
•
Процедуры p r o d u c t O r d e r $ s h i p и productOrder$shipOutOf Spec, вероятно, будут закончены другой хранимой процедурой, которая будет использоваться для обеспечения фактического начала отгрузки заказа. Однако, представляя фактический объект и имена других методов, мы можем ограничить любое ситуационное кодирование в наших процедурах (если этот тип пользователя, делайте это, иначе делайте другое) и представить безопасный интерфейс, основанный только на хранимых процедурах, которые используются для реализации.
Действительностью для архитектора данных является то, что всегда будут те, кто работает "вне правил" как по уместным, так и неуместным причинам, и мы должны принимать это во внимание всякий раз, когда строим систему, дружественную к пользователю.
422
Обеспечение целостности данных
Учебный пример В нашем учебном примере мы возьмем основную реализацию, которую создали в предыдущей главе, реализуем бизнес-правила, которые были ранее найдены на стадии логического проектирования, и добавим всякие меры защиты данных, которые посчитаем необходимыми. Мы будем следовать той же самой дорожкой, которой мы следовали на протяжении этой главы, сначала строя наши ограничения, далее создавая триггеры и затем различные необязательные правила, которые мы выделили. Мы уже назначили типы данных, построили NULL-ограничения и ограничения на внешние ключи, когда создавали наши таблицы.
Ограничения по умолчанию Когда мы строим хранимые процедуры, чтобы выполнить добавление в таблицу, то должны перечислить все поля и подставить значения, даже NULL, в каждое из них. Если при добавлении мы используем значение по умолчанию для пропущенного столбца, это устранит проблемы, которые возникают, когда мы решаем, какие значения используются по умолчанию и какие значения пользователь запланировал, чтобы избежать значения NULL. Обратите внимание, что мы поместили zipCode (почтовый индекс) в таблицу, ссылающуюся на таблицу c i t y (город). Может показаться, что хорошо позволить пользователю просто выбрать почтовый индекс или город и использовать значение по умолчанию для другого параметра. Однако оказывается невозможно использовать такое значение по умолчанию, поскольку мы должны ввести значения c i t y и zipCode одновременно, а значения по умолчанию не могут организовать доступ к любым другим полям во время добавления или обновления. Будем использовать эту таблицу как основу для проверок с помощью триггера позже в нашем учебном примере. Поля типа bit в таблице transactlonType В таблице t r a n s a c t i o n T y p e (тип сделки) мы имеем три поля, где можем задать ограничения по умолчанию. Мы установим их для наиболее простых случаев задания параметров. Q Q •
r e q u i r e s S i g n a t u r e F l (флаг необходимости подписи) — устанавливаем в 0, чтобы указать, что подпись не требуется. r e q u i r e s P a y e e F l (флаг необходимости иметь получателя платежа) — устанавливаем в 0, чтобы указать, что получатель платежа не требуется. allowsPayeeFl (флаг возможности иметь получателя платежа) — устанавливаем в 1, чтобы указать, что можно поместить получателя платежа.
Все это мы оформим следующим образом: ALTER TABLE transactionType ADD CONSTRAINT dfltTransactionType$requiresSignatureFl$bit$false DEFAULT 0 FOR requiresSignatureFl ALTER TABLE transactionType ADD CONSTRAINT dfltTransactionType$requiresPayeeFl$bit$false DEFAULT 0 FOR requiresPayeeFl ALTER TABLE transactionType ADD CONSTRAINT dfltTransactionType$allowsPayeeFl$bit$false DEFAULT 1 FOR allowsPayeeFl
423
Глава 11 Поля номера телефона Другое использование значений по умолчанию — задание полей, которые пользователи обычно оставляют незаполненными. Два довольно хороших примера номера телефона — код страны и код региона. Мы установим код страны в ' 1 ' , так как это — значение по умолчанию для всех телефонных номеров Соединенных Штатов, и, скажем,'615' для кода региона или код региона, где расположены пользователи системы. ALTER TABLE phoneNumber ADD CONSTRAINT dfltPhoneNumber$countryCode$string$charNumberOne DEFAULT '1' FOR countryCode ALTER TABLE phoneNumber ADD CONSTRAINT dfltPhoneNumber$areaCode$string$localAreaCode DEFAULT '615' FOR areaCode
Ограничения-проверки Почти в любой проектируемой БД мы будем требовать гораздо больше ограничений-проверок, чем дают триггеры или необязательные правила. Наша БД не является исключением. В качестве примеров мы добавим четыре различных группы ограничений-проверок, чтобы запретить пустые строки, предохранить ввод финансовых сделок с будущими датами, сформировать спецификацию диапазона финансовых сделок и обеспечить, что адреса могут иметь только некоторые заполненные поля в заданных условиях. Мы будем, конечно, строить определяемые пользователем функции, чтобы поддержать многие из этих ограничений так, как нужно. Запрет пустых строк Для многих строковых полей мы будем желать предохранить наших пользователей от ввода пустых строк. Как мы обсудили ранее в этой главе, можно предотвратить ввод пустых строк, используя следующий код: Ц
CHECK (('*' + COALESCE (RTRIM (<столбец>) , " ) + ' * ' ) '**')
но на практике мы обычно хотим ввести такие алгоритмы в функцию, где это возможно. В этом случае мы получим следующую функцию: CREATE FUNCTION string$checkEntered ( @value varchar(8000) — наибольшая величина тип varchar ) RETURNS bit AS -- используется для проверки, является ли введенная величина varchar пустой — замечание: для Unicode потребуется дополнительная функция, если будет нужно BEGIN DECLARE dreturnVal bit -- возвращает только "да" или "нет" —
выполнить RTRIM, COALESCE для '/, Если NULL, окружить звездочками (*) и выполнить сравнение
IF ('*' + COALESCE (RTRIM{@value), " ) + '*') <> '**' SET @returnVal = 1 — не пустая строка ELSE
424
Обеспечение целостности данных SET @returnVal = 0
-- пустая строка
RETURN @returnVal
i
END GO
Теперь проверим код. Когда возникают проблемы, всегда полезно иметь возможность быстро проверить код, поэтому мы проверим наш код ограничения в случае неправильных и правильных данных, как показано ниже: -- условие пустой строки IF dbo.string$checkEntered('') = 0 SELECT 'Пустая строка' ELSE SELECT 'He пустая строка' -- условие непустой строки IF dbo.string$checkEntered('Подойдет любой текст') • 0 SELECT 'Пустая строка' ELSE SELECT 'He пустая строка' Как и ожидалось, первый код возвратит "Пустая строка", а второй — "Не пустая строка". Далее мы хотим создать наши ограничения-проверки. Нам нужно рассмотреть каждое поле в БД, которое мы создали, а также поля идентификации, но лучше начать с нашего списка доменов. В следующей таблице мы имеем все домены THnavarchar и char, которые раньше идентифицировали, и определяем, которые из них должны быть проконтролированы с помощью ограничений-проверок. Снова имейте в виду, что наши примеры для простоты не рассматривают неамериканские адреса и телефоны
Имя
Тип данных
Возможность значения NULL
Проверка пустых строк
addressDescriptiveLine (строка описания адреса)
varchar(2000)
NOT NULL
Да
addressStateCode (код штата в адресе)
char(2)
NOT NULL
Да
alphaNumericNumber (алфавитно-цифровое число)
varchar(20)
NOT NULL
Да
areaCode (код региона)
varchar(3)
NOT NULL
Да
countryCode (код страны)
char(l)
NOT NULL
Да
Продолжение таблицы на следующей странице
425
Глава 11
426
Имя
Тип данных
Возможность значения NULL
Проверка пустых строк
description (описание)
varchar(100)
NOT NULL
Да
exchange (номер коммутатора)
char(5)
NOT NULL
Да
extension (расширение)
varchar(20)
NULL
Нет, если в этом случае известно, что нет расширения, пустая строка будет это означать; для неизвестного случая подходит NULL
firstName (имя)
varchar (60)
NOT NULL
Да
lastName(фамилия)
varchar(60)
NOT NULL
Да
middleName (отчество)
varchar(60)
NOT NULL
Нет, если пользователь не задает отчество, это можно отразить пустой строкой
name (имя)
varchar(60)
NOT NULL
Да
phoneNumber (номер телефона)
char(4)
NOT NULL
Да
string (строка)
varchar(20)
NOT NULL
Нет, так как это общий случай, пустые строки могут понадобиться для типов, вытекающих из этого типа
Обеспечение целостности данных
Имя
username (системное имя пользователя)
zipCode (почтовый индекс)
Тип данных
Возможность значения NULL
Проверка пустых строк
sysname
NOT NULL
Да
NOT NULL
Да
(определяемый пользователем в SQL Server тип данных, который используется для размещения имен пользователей. Мы будем здесь размещать имена SQL Server, чтобы показать реальные имена, так как любому пользователю, кто имеет право входа для контроля, мы будем давать конкретные права просматривать сделки и/или счета на основе схемы действий) char (5) (заметьте, что мы использовали zipCode, чтобы установить связь с городом. Это сделано с помощью пяти символов zipCode. Мы игнорировали дополнительные 4 символа снова для простоты)
Чтобы проиллюстрировать путь, которым мы формируем наши ограничения, рассмотрим пример таблицы bank (банк). Чтобы создать ограничение для ее столбца name (имя), используем следующий фрагмент:
I
ALTER TABLE bank ADD CONSTRAINT chkBank$name$string$notEmpty CHECK string$checkEmpty(name) = 1
To же самое можно сделать для поля number (номер) таблицы account (счет):
I
ALTER TABLE account ADD CONSTRAINT chkAccount$number$string$notEmpty CHECK (dbo.string$checkEmpty(number) = 1)
427
Глава 11 Предотвращение финансовых сделок с будущими датами В наших технических требованиях отмечено, что нужно предотвратить ввод сделок с будущими датами, поэтому применим с этой целью ограничения. Чтобы определить, произошла ли все-таки сделка, мы должны выполнить некоторые предварительные операции с нашими данными типа даты-времени. В частности, нам не нужно хранить время в наших сделках, так что мы должны удалить элементы времени из наших переменных типа даты-времени в ограничениях-проверках. Мы выполним удаление времени из нашего столбца в разделе данной главы, посвященном триггерам, а здесь мы будем просто считать, что это сделано. Способность проверять, является ли дата будущей или прошлой, является весьма полезным средством, поэтому создадим определяемую пользователем функцию, выполняющую эту операцию: CREATE FUNCTION date$rangeCheck ( @dateValue datetime, -- первое значение времени @dateToCompareAgainst datetime -- передача даты для сравнения ) RETURN int AS BEGIN DECLARE OreturnVal int IF SdateValue > @dateToCompareAgainst BEGIN SET SreturnVal = 1 -- будущая дата END ELSE IF SdateValue < @dateToCompareAgainst BEGIN SET @returnVal = -1 -- прошлая дата END ELSE SET @returnVal = 0 -- даты одинаковы RETURN @returnVal END GO
Снова нам нужно проверить нашу функцию, чтобы быть уверенными, что она работает:
I
-- простое условие SELECT dbo.date$rangeCheck('1/1/1989', getdateO) AS [должно быть -1] SELECT dbo.date$rangeCheck(getdate О , getdateO) AS [должно быть 0] SELECT dbo.date$rangeCheck('1/1/2020', getdateO) AS [должно быть 1] Конечно, я приношу извинения, если текущий год больше, чем 2020, а вы все еще используете эту книгу. Пожалуйста измените эту дату, если необходимо!
Далее мы создадим функцию для удаления времени из величины типа даты-времени:
I
428
CREATE FUNCTION date$removeTime @date d a t e t i m e RETURNS datetime AS BEGIN SET @date = CAST(datePart(month, @date) AS varchar(2)) + '/' + CAST (datePart(day, @date) AS varchar(2)) + '/' +
Обеспечение целостности данных CAST(datePart(year, (Mate) AS varchar(4))
I
RETURN @date END GO
Теперь обратимся к нашей таблице t r a n s a c t i o n (сделка) и создадим ограничение для столбца d a t e (дата) следующим образом: ALTER TABLE [transaction] ADD CONSTRAINT chkTransaction$date$date$notFuture -- 0 - текущая дата, -1 - означает прошлую дату, 1 - означает будущую дату CHECK (dbo.date$rangeCheck(date, dbo.date$removeTime(getdate()) > 0)
Определение диапазона финансовой сделки Вернемся к таблицам transaction (сделка) и transactionType (тип сделки): transaction transaction^: primaryKey transactionType accountld: primaryKey (FK) (AK 1.1) transactionTypeld: primaryKey number: stringNumber (AK 1.2) name: name date: Datetime классифицирует description: description description: description requiresSignatureFi: fl amount: amount requiresPayeeFI: f! signature: String allowPayeeR: fl payeeld: primaryKey (FK) autoTimestamp: autoTimeStamp transactionTypeld: primaryKey (FK) autoTimestamp: autoTimeStamp Нам нужно быть уверенными, что все депозиты являются положительными значениями, и что все чеки и электронные изъятия являются отрицательными величинами. Вместо создания ограничения-проверки, основанного на значении внешнего ключа, мы можем добавить новый столбец в таблицу t r a n s a c t i o n T y p e , называемый positiveAmountFl (флаг положительной величины), но это недостаточно гибко для наших потребностей. Для некоторых элементов можно разрешить отрицательные значения, нуль или только положительные значения. Мы могли бы также решить, что конкретный тип сделки имеет ограниченное максимальное значение, типа автоматического ограничения изъятия в 500 долларов у банкомата. Таким образом, непосредственным способом реализовать это было бы дать возможность пользователю создавать диапазон, добавляя атрибуты minimumAmount (минимальная величина), HmaximumAmount (максимальная величина) к нашей таблице transactionType: transaction transactionType transactionld: int IDENTITY transactionTypeld: int IDENTITY accountld: int NOT NULL (FK) (AK 1.1) name: varchan(20) NOT NULL number: varchan(20) NOT NULL (AK 1.2) description: varchan( 100) NOT NULL date: smalldatetime NOT NULL классифицирует requiresSignatureFi: bit NOT NULL description: varchan(1000) NOT NULL requiresPayeeFI: bit NOT NULL amount: money NOT NULL allowPayeeFI: bit NOT NULL signature: varchan(IOO) NOT NULL minimumAmount: money NOT NULL payeeld: int NOT NULL (FK) maximumAmount: money NOT NULL transactionTypeld: int NOT NULL(FK) autoTimestamp: smalldatetime NOT NULL autoTimestamp: smalldatetime NOT NULL
4 2 9
Глава 11 Здесь мы добавляем простое ограничение, чтобы быть уверенными, что minimumAmount не больше maximumAmount, которые мы ввели: ALTER TABLE transactionType ADD minimumAmount money NOT NULL ALTER TABLE transactionType ADD maximumAmount money NOT NULL -- простое ограничение-проверка, что минимум меньше максимума ALTER TABLE transactionType ADD CONSTRAINT chkTransactionType$minAndMaxRangeCheck CHECK (minimumAmount <= maximumAmount)
Теперь м ы создадим определяемую пользователем функцию, которая подтвердит, что значение сделки с типом, определяемым таблицей transactionType, находится внутри диапазона, определяемого значениями столбцов minimumAmount и maximumAmount: CREATE FUNCTION transactionType$validateAmountInRange ( @transactionTypeId int, @amount money ) RETURNS bit AS BEGIN — 1 - внутри диапазона, 0 - вне диапазона DECLARE SreturnValue bit IF EXISTS (SELECT * FROM transactionType WHERE Samount NOT BETWEEN minimumAmount AND maximumAmount AND transactionTypeld = @transactionTypeId) BEGIN SET @returnValue = 0 END ELSE SET @returnValue = 1 END
Наконец, м ы изменим таблицу transaction и добавим ограничение-проверку для вводимых в таблицу данных:
I
ALTER TABLE [ t r a n s a c t i o n ] ADD CONSTRAINT chk.Transaction$amountProperForTransactionType AS CHECK (dbo.transactionType$validateAmountInRange(transactionTypeld, amount) = 1)
Обратите внимание, что ограничение, требующее подпись, ограничения, требующие получателя платежа или допускающие получателя платежа, основанные на флагах в таблице t r a n s a c t i o n T y p e , можно построить примерно таким же образом, так что мы не будем здесь их включать.
430
Обеспечение целостности данных
Триггеры Удаление времени из дат сделок Первый триггер, который мы создадим, — триггер INSTEAD OF — используется для форматирования даты сделки. Нет никакой причины хранить в дате сделки время, и поэтому мы начнем с шаблона кода триггера и добавим код, чтобы удалить элемент времени из дат, использующих функцию date$removeTime, используемую ранее для ограничения-проверки: CREATE TRIGGER t r a n s a c t i o n $ i n s t e a d O f I n s e r t ON [ t r a n s a c t i o n ] INSTEAD OF INSERT AS - - Цели: А.
Триггер для операции INSERT, который запускается для
любого оператора ввода языка манипулирования данными (ЯМД) Б. Триггер форматирует столбец d a t e , удаляя время
-BEGIN
DECLARE @rowAffected int @errorNumber int,
---@msg varchar(255), — -@errorSeverity varchar (20), @triggerName sysname
размещение числа измененных строк используется для размещения кода ошибки после выполнения оператора ЯМД используется для размещения сообщения об ошибке
SET @rowsAffected = @@rowcount SET @triggerName = object_name(@@procld) — используется для сообщений -- не нужно продолжать, если нет измененных строк IF @rowsAffected = 0 RETURN -- выполняем добавление, вместо которого мы создали триггер INSERT INTO [transaction] (accountld, number, date, description, amount, signature, payeeld, transactionTypeld) SELECT accountld, number, dbo.date$removeTime(date) AS date, description, amount, signature, payeeld, transactionTypeld FROM INSERTED SET @errorNumber = @@error IF @errorNumber <> 0 BEGIN SET @msg = 'Ошибка: ' + CAST(@errorNumber AS varchar(lO)) + ' возникла при добавлении строк в ' + ' таблицу transaction.' RAISERROR 50000 @msg ROLLBACK TRANSACTION RETUR END END
Триггер для обновления на 95% имеет тот же код, так что мы оставим его читателю. 15 1868
4 3 1
Глава 11 Проверка согласования города и почтового индекса В нашей таблице a d d r e s s (адрес), мы имеем поля c i t y l d (идентификатор города) и zipCodeld (идентификатор почтового индекса), которые являются внешними ключами к таблицам zipCode (почтовый индекс) и c i t y (город) соответственно. Мы также включили таблицу zipCodeCityRef erence (соответствие почтового индекса и города), которая используется, чтобы помочь пользователю выбрать город и штат по почтовому индексу. Мы можем заполнить эту таблицу из БД почтовых индексов, которую можно купить (скажем, в Почтовой службе США). Окончательная цель этой таблицы — проверить соответствие значений c i t y и zipCodeld. Как напоминание, вот физическая модель данных для этих таблиц: address addressid: int IDENTITYaddressid: int IDENTITY adressline: varchar(2000)NOT NULL (AK1.1) Cityid: int NOT NULL(FK) (AK1.2) zipCided: int NOT NULL (FK) (AK1.3) autoTimestamp: timestamp NOT NULL
1> state stateid: int IDENTITY определяет расположение определяет почтовый регион
zipCode
city
zipCodedid int IDENTITY
cityid: int IDENTITY
zipCoded: char(5) NOT NULL (AK1.1) autoTimestamp: t mestamp NOT NULL
code: char(") NOT NULL (AK1.1) autoTimestamp: timestamp NOT NULL
stateid: int NOT NULL (FK) (AK1.1) name: varchar(60) NOT NULL (AK1.2) autoTimestamp: timestamp NOT NULL
определяет расположение
zipCodedCityReference
zipCodedCityReferenced: nt IDENTITY
зан c r o p адом n e f ез СВЯ
zipCodedid: int NOT NULL (FK) (AK1.1) cityid: int NOT NULL (FK) (AK1.2) primaryFI: bit NOT NULL autoTimestamp: timestamp NOT NULL
характер изуется соответствующим почтов >|м индексом с помощью
Мы можем сделать это, убедившись, что пара c i t y i d и zipCodeld, которая была введена, находится в таблице zipCodeCityReference: CREATE TRIGGER a d d r e s s $ a f t e r l n s e r t ON address AFTER UPDATE AS — Цель: Триггер для <операции>, который запускается для любой <операции> языка манипулирования данными (ЯМД) 432
Обеспечение целостности данных
BEGIN DECLARE @rowsAffected int, - - размещение числа измененных строк @errorNumber int, - используется для размещения кода - ошибки после выполнения оператора ЯМД @msg varchar (255), - используется для размещения сообщения - об ошибке @errorSeverity varchar (20), @triggerName sysname SET @rowsAffected = @@rowcount SET @triggerName = object_name(@@procld) -- используется для сообщений -- не нужно продолжать, если нет измененных строк IF @rowsAffected = 0 RETURN DECLARE OnumberOfRows int SELECT 0numberOfRows - (SELECT count (*) FROM INSERTED JOIN zipCodeCityReference AS zcr ON zcr.cityld = INSERTED.cityld AND zcr.zipCodeld = INSERTED.zipCodeld IF @numberOfRows <> @rowsAffected BEGIN SET @msg = CASE WHEN @rowsAffected - 1 THEN 'Строка, которую вы ввели, содержит ' + 'неправильную пару cityld и zipCodeld.' ELSE 'Одна их строк, которую вы пробовали ввести, ' + 'содержит неправильную пару cityld и zipCodeld.' END RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END END
Триггер для добавления на 99% имеет тот же код, так что мы оставим его читателю.
Распределения сделок не могут превышать размещенную сумму В этом примере мы рассматриваем суммы распределения сделки для различных видов сделок. Нас будет интересовать поле allocationAmount (сумма распределения) таблицы t r a n s a c t i o n A l l o c a t i o n (распределение сделки):
433
Глава 11
transacto in tranactionid: int NOT NULL accountd i : int NOT NULL (FK) (AK1.1) number: varchar(20) NOT NULL (AK1.2) date: smad l atetm i e NOT NULL description: varchar(1000) NOT NULL amount: money NOT NULL sg i nature: varchar(20) NOT NULL payeed l : int NOT NULL (FK) userid: int NULL (FK) statementiemd i : int NOT NULL (FK) transacto i nTyped i : int NOT NULL (FK) autoTm i estamp: tm i estamp NOT NULL
имеет информацию о распределении, размещенную в
transactionAllocation
1
transactionAllocationid: int IDENTITY transactionAllocationTypeid: NOT NULL (FK) (AK1.1) transactionid: int NOT NULL (FK) (AK1.2) allocationAmount: Money NOT NULL autoTimestamp: timestamp NOT NULL 4
transacto i nAo l cato i nType transacto i nAo l cato i nTyped i : int NOT NULL классифицирует чек с помощью name: varchar(60) NOT NULL description: varchar(IOO) NOT NULL parentCheckUsagefc 'pe s: Int NULL (FK) autoTm i estamp: tm i estamp NOT NULL является
Всякий раз, когда пользователь корректирует существующее распределение, мы проводим проверку, чтобы быть уверенными, что распределено не больше денег сделки, фактически существующих. Для этого мы пишем следующий триггер: CREATE TRIGGER transactionAllocation$Afterlnsert ON transactionAllocation AFTER INSERT AS -- Цель: А. Триггер, выполняющий операцию добавления, который запускается при каждом операторе добавления -Б. Триггер защищает от распределения более 100% суммы BEGIN DECLARE @rowsAffected int, - @errorNumber int, @msg varchar (255), @errorSeverity varchar(20), @triggerName sysname
размещение числа измененных строк используется для размещения кода ошибки после выполнения оператора ЯМД используется для размещения сообщения об ошибке
SET @rowsAffected = @@rowCount SET @triggerName = object_name(@@procld) —
используется для сообщений
— не нужно продолжать, если нет измененных строк IF @rowsAffected = 0 RETURN
434
Обеспечение целостности данных — получение суммы распределений сделок, на которые воздействует -- наше добавление и получение всех сделок, на которые воздействует — добавление IF EXISTS ( SELECT * FROM ( SELECT transactionld, sum(allocationAmount) AS amountTotal FROM transactionAllocation -- заметьте, что transactionld расширяет наш запрос — на просмотр всех распределений всех сделок, -- чьи распределения мы затронули WHERE transactionld IN (SELECT transactionld FROM INSERTED) GROUP BY transactionld) AS allocAmounts -- объединение со сделкой, чтобы получить сумму сделки JOIN [transaction] ON transactionAllocation.transactionld = [transaction].transactionld -- проверка, что сумма сделки не больше, чем — распределенная сумма WHERE [transaction].amount > allocAmounts.amountTotal BEGIN SET @msg = CASE WHEN @rowsAffected - 1 THEN 'Строка, которую вы ввели, ' + 'делает сделку больше суммы распределения.' ELSE 'Одна из строк, которую вы пробовали ввести, 'делает сделку больше суммы распределения.' END RAISERROR 50000 @msg ROLLBACK TRANSACTION RETURN END END
Обратите внимание, что мы решили довольно сложную проблему, используя составной запрос, и при этом не использовали ни курсоров, ни временных таблиц. Важно формулировать наши запросы в триггерах, используя как можно меньше операторов, чтобы минимизировать возможность возникновения логических ошибок в очень тяжелых для определения местах при формировании сложных запросов. Всегда будут возникать компромиссы между практичностью и удобочитаемостью/понятностью. В большинстве систем медленные триггеры будут проблемой, особенно, если мы должны добавить большое количество строк одновременно. Снова, триггер для обновления этой таблицы ( t r a n s a c t i o n A l l o c a t i o n ) и таблицы t r a n s a c t i o n содержит 95% этого кода, так что мы не будем его здесь включать.
Необязательные правила В нашей системе мы имеем несколько необязательных бизнес-правил, относительно которых было решено, что их нужно реализовать. Они были определены при первоначальном анализе: •
Пользователь должен получать предупреждение, если попытается забирать слишком много денег — Мы, вероятно, создали бы хранимую процедуру, которая проверяет баланс счета пользователя до сохранения суммы сделки. Код стороны клиента сравнил бы баланс с суммой сделки, которую пытаются выполнить. 435
Глава 11 •
Баланс по счету должен составляться еженедельно — При запуске приложение должно вызвать хранимую процедуру, которая выясняет, когда был составлен последний баланс счета, сравнивает это с текущей датой и выдает напоминание или предупреждение: напоминание могло бы быть послано менеджеру, чтобы уведомить, что баланс был очевидно просрочен.
В следующей главе мы рассмотрим лучшие методы создания хранимых процедур.
Резюме На этом завершается задача разработки хранения данных для наших БД. В прошлой главе мы организовали физическое хранение для данных, создавая таблицы. В этой главе мы выполнили следующий шаг и построили схему защиты их. Во время этого процесса были рассмотрены следующие средства, которые дает нам SQL Server, чтобы защитить наши данные от неправильных значений: •
Q
Значения по умолчанию — Хотя они и не кажутся таковыми, но значения по умолчанию могут рассматриваться как средства защиты данных; они могут использоваться, чтобы автоматически задавать поля, когда назначение поля может быть неочевидно пользователю (и БД добавляет подходящее значение для поля). Ограничения-проверки — Они очень важны для обеспечения того, чтобы наши данные были в пределах спецификации. Мы можем использовать почти любые скалярные функции (определенные пользователем или системные), пока не обеспечим конкретное логическое выражение.
•
Триггеры — Триггеры — очень важные средства, которые позволяют нам создавать части кода, которые запускаются автоматически при любых операциях INSERT, UPDATE и DELETE, которые выполняются над одной таблицей.
•
Хранимые процедуры/код клиента — Мы просто упомянули эти механизмы защиты данных, потому что хотим быть уверенными, что они используются разумно в обработке бизнес-правил.
Во время этого процесса мы осуществили важное использование определенных пользователем функций, являющихся новым средством, которое предлагает нам SQL Server 2000. С ними можно включить многие из используемых алгоритмов, чтобы увеличить и расширить возможности наших значений по умолчанию, ограничений-проверок, триггеров и, как мы выясним в следующей главе, хранимых процедур. Как только мы построили и реализовали набор соответствующих средств защиты данных, можно считать, что данные в нашей БД были проверены. Нам никогда не нужно будет повторно проверять данные, как только они размещены в нашей БД, но все же неплохо случайным образом делать проверки только для того, чтобы убедиться, что нет нарушений целостности, которые проскочили мимо нас. Каждое отношение, каждое ограничение-проверка, каждый триггер, который мы изобретем, поможет удостовериться, что это так. В следующей главе мы продолжим рассмотрение некоторых расширенных средств доступа к данным и методов модификации и в особенности использование представлений и хранимых процедур.
436
Расширенный доступ к данным и методы корректировки Введение В предыдущих главах мы разработали наши БД, организовали размещение и реализовали схемы защиты наших данных. На протяжении этой книги мы предполагали, что читатель относительно знаком с языком Transact-SQL и имеет некоторый опыт написания хранимых процедур и представлений, навыки — которые мы далее разовьем в ходе этой главы. Теперь мы займемся расширенными концепциями доступа, создания, корректировки и удаления данных в нашей OLTP-БД. Будут обсуждены главные соображения относительно организации запросов, типа того, как транзакции, временные таблицы, курсоры и значения NULL делают наше кодирование и более легким, и более интересным. Мы достаточно глубоко рассмотрим проблемы, связанные с использованием представлений и хранимых процедур и полностью обсудим все "за" и "против" создания приложений, использующих доступ с помощью хранимых процедур, доступ, использующий нерегламентированные запросы, и их смесь. После обсуждения сущности запросов мы рассмотрим, как можно использовать средства безопасности SQL Server, чтобы ограничить доступ к данным в наших системах. Наконец, мы кратко исследуем, как создание приложений, которые обращаются к данным через разные серверы, влияет на нашу архитектуру и кодирование.
Глава 12 Одно замечание. В некоторых организациях для архитектора данных выполнение некоторых задач, описанных в этой главе, будет вне круга его обязанностей. Возможно, создание хранимых процедур и представлений будет ближе администратору БД или программисту БД. С другой стороны, вполне может быть, что архитектор данных является единым во многих лицах. Независимо от ситуации, я чувствую, что это важно для каждого архитектора данных, чтобы не потерять навыки SQL-кодирования, а полное понимание этих проблем очень полезно для проектирования. Если вы не являетесь квалифицированным специалистом в SQL, то рискуете спроектировать БД, которая вызовет у разработчиков много трудностей в процессе ее создания.
Рассмотрение запросов В этом разделе мы рассмотрим некоторые из важных проблем, которые нужно понять, чтобы реализовать эффективный доступ к БД и возможности корректировки. Транзакции, блокировка, значения NULL, курсоры, временные таблицы, хранимые процедуры и представления — все существенны для архитектуры СУБД. Мы обсудили многие из них ранее в этой книге. Однако в данной главе мы собираемся посмотреть, как эти проблемы затрагивают кодирование наших приложений, а не как они затрагивают создание наших объектов.
Транзакции Транзакции играют очень важную роль в том, как мы осуществляем доступ к нашим данным. Транзакция — последовательность действий, выполненных как единственная логически законченная единица работы. Имеются четыре классических свойства, которые требуется поддерживать в транзакциях, обычно называемые как АСШ-тест. Это является аббревиатурой следующих понятий: •
Элементарность (Atomicity) — каждое действие, которое участвует в транзакции, должно быть выполнено как логическая единица работы, так что каждое действие или выполняется, или не выполняется.
•
Непротиворечивость (Consistency) — как только транзакция закончена, система должна быть оставлена в непротиворечивом состоянии, включая физические ресурсы, такие как данные, индексы и связи между страницами данных.
•
Изоляция (Isolation) — все корректировки в пределах транзакции должны быть изолированы от других транзакций. При выполнении команд в пределах транзакции никакая другая транзакция не может видеть данные в промежуточном состоянии.
•
Живучесть (Durability) — как только транзакция закончилась, все операции, которые выполнялись в транзакции, должны быть сохранены.
Транзакция начинается с оператора BEGIN TRANSACTION. Выполнение BEGIN TRANSACTION помещает нашу связь с БД в состояние, когда все, что мы будем делать, не будет сохраняться в постоянном физическом хранилище, пока мы явно не скажем это. Исследуем это в следующем наборе примеров. В первом примере мы начнем транзакцию, вставим строку и затем откатим транзакцию (другими словами, действия транзакции полностью отменяются, и БД возвращается к своему первоначальному состоянию). Будем работать с таблицей a r t i s t (артист) из предыдущей главы и начнем с получения основного списка артистов в нашей таблице: 438
Расширенный доступ к данным и методы корректировки
I
SELECT artistid, name FROM artist
Мы видим, что имеются три сохраненных артиста (имейте в виду, что значения столбца a r t i s t i d могут меняться в зависимости от того, сколько значений вы ввели, плюс то, сколько произошло ошибок): artistid
name
27 1 2
jethro tull the beatles the who
Далее выполним следующий набор команд: BEGIN TRANSACTION
-- начало транзакции
--- ввод двух записей INSERT INTO artist (name) VALUES ('mariah carey') INSERT INTO artist (name) VALUES ('britney spears') —
проверка, что они в БД
SELECT artistid, name FROM artist
Этот фрагмент возвратит результат: artistid 37 27 36 1 2
name britney spears jethro tull mariah carey the beatles the who
Теперь мы решаем, что на самом деле не хотим иметь эти записи в нашей БД и поэтому откатываем транзакцию, за которой, однако, следует другая проверка строк в таблице: Как мы обсудим позже, иметь транзакцию, охватывающую несколько действий, — не лучший вариант, но я делаю это здесь, чтобы показать вам значения в таблице.
I
ROLLBACK TRANSACTION SELECT artistid, name FROM artist
Теперь строки исчезли, как будто их никогда не было, artistid
name
27 1 2
jethro tull the beatles the who 439
Глава 12 Формально говоря, строки физически существовали, но SQL Server отметил их так, что мы можем или зафиксировать их как группу, или, как в нашем случае, не фиксировать их. Транзакции могут быть вложены так, что для каждого оператора BEGIN TRANSACTION, который мы выполняем над БД, мы должны выполнить один оператор COMMIT TRANSACTION. С другой стороны, оператор ROLLBACK TRANSACTION отменяет неограниченное число транзакций. Мы можем определить, сколько транзакций было начато, исследуя глобальную переменную @@trancount. В следующем примере мы покажем влияние выполнения операторов BEGIN TRANSACTION и COMMIT TRANSACTION, отслеживая значение @@trancount до и после команд транзакции: SELECT @@trancount AS zero BEGIN TRANSACTION SELECT @@trancount AS one BEGIN TRANSACTION SELECT @@trancount AS two BEGIN TRANSACTION SELECT @@trancount AS three COMMIT TRANSACTION SELECT @@trancount AS two COMMIT TRANSACTION SELECT @@trancount AS one COMMIT TRANSACTION SELECT @@trancount AS zero
Этот фрагмент возвратит следующий результат. Заметьте, что он увеличивается от нуля до трех, а затем уменьшается до нуля. zero О one 1
two 2 three 3 two 2 one 1
440
Расширенный доступ к данным и методы корректировки
zero О Мы должны завершить каждую транзакцию, иначе ресурсы будут заперты от других наших транзакций на основе свойства изоляции ACID-теста. Однако, как мы сказали ранее, оператор ROLLBACK отменяет все операторы BEGIN TRANSACTION сразу: SELECT @@trancount AS one BEGIN TRANSACTION SELECT @@trancount AS two BEGIN TRANSACTION SELECT @@trancount AS three BEGIN TRANSACTION SELECT @@trancount AS four ROLLBACK TRANSACTION SELECT @@trancount AS zero
Этот фрагмент возвратит: one О two 1 three 2 four 3 zero 0 Это — большое достоинство, а также и проблема. Для команд запуска у вас, вероятно, будет несколько операторов COMMIT после оператора отката, потому что вы будете обычно использовать логику, которая утверждает: если что-то работает, завершайте это; если не работает, выполните откат. После отката мы должны сообщить каждому следующему оператору, что не собираемся выполнять его. С этой целью SQL Server дает нам дополнительную возможность для транзакций, которая позволяет выборочно откатывать некоторые части транзакции. Выборочный откат Здесь используется оператор SAVE TRANSACTION. Вы можете задать имя точке сохранения (на основе обычных критериев для идентификаторов, хотя существенными являются только первые 32 символа) следующим образом: §Ц SAVE TRANSACTION
<имя_точки_сохранения>
441
Глава 12 В этом случае, если вы решили, что часть транзакции выполнена неверно, то можете откатить только часть ее, но не всю, выполняя оператор наподобие этого: §Ц ROLLBACK TRANSACTION <имя_точки_сохранения>
Обратите внимание, что это отличается от BEGIN TRANSACTION <иия_ тр анза КЦИИ>. Итак, в нашем заключительном примере, посвященном транзакциям, мы используем команду SAVE TRANSACTION, чтобы выборочно откатить одну из команд, которую мы выполняем, а другую — нет: BEGIN TRANSACTION
-- начало транзакции
— помещаем две записи INSERT INTO artist (name) VALUES ('moody blues') SAVE TANSACTION britney INSERT INTO artist (name) VALUES ('britney spears') -- проверка, что они в БД SELECT artistld, name FROM artist
который дает результат: artistld
name
45 27 44 1 2
britney spears jethro tull moody blues the beatles the who
Далее мы выполним откат к созданной нами точке сохранения b r i t n e y , завершим основную транзакцию и выберем наши строки, чтобы посмотреть состояние таблицы a r t i s t : ROLLBACK TRANSACTION britney COMMIT TRANSACTION SELECT artistld, name FROM artist
М ы видим, что этот фрагмент должен откатить строку "britney spears", но не строку "moody blues".
442
artistld
name
27 44 1 2
jethro tull moody blues the beatles the who
Расширенный доступ к данным и методы корректировки Мы будем использовать механизм SAVE TRANSACTION весьма часто при создании транзакций. Он позволяет осуществлять откат только части транзакции и решить, какое действие выполнить. Это будет особенно важно, когда мы имеем хранимые процедуры, так как хранимая процедура не позволяет изменять значение @@trancount с помощью своих операций.
Область действия транзакций Пакет (batch) представляет собой логическую группу операторов T-SQL, которые анализируются, а затем выполняются совместно. Мы используем оператор GO, чтобы разделить код на пакеты; GO не является командой T-SQL, а только ключевым словом, распознаваемым утилитами команд SQL Server, наподобие Query Analyzer. Транзакция имеет область действия на все соединение с SQL Server и может теоретически охватывать несколько пакетов операторов, пересылаемых между клиентом и сервером. Обратите внимание, что соединение не то же самое, что пользователь. Пользователь может иметь много соединений с сервером, и фактически он может быть вынужден ждать завершения транзакции в одном из своих соединений, чтобы продолжить работу. Одна транзакция (или набор вложенных транзакций) должна быть закончена, прежде чем следующая может начаться. Как SQL Server может удостовериться, что, если связь разорвана до того, как завершилась транзакция, произошел откат к ее первоначальному состоянию? Как SQL Server может удостовериться, что транзакции не накладываются друг на друга, нарушая таким образом требование изоляции АСШ-теста? В следующем разделе мы рассмотрим механизмы блокировки, которые SQL Server использует, чтобы отслеживать работу транзакций в БД.
Блокировка Блокировка в SQL Server несколько напоминает знак занятости, который вы видите на двери ванной комнаты. Она сообщает внешнему миру, что что-то происходит внутри, и каждый другой должен будет ожидать. Блокировка играет важную роль в обеспечении правильной работы транзакций и подчиняется ACID-правилам. Откат транзакции может быть опасен, если другая транзакция уже начала работать с тем же самым набором данных. Эта ситуация, когда несколько пользователей пытаются обратиться к ресурсам в одно и то же время, обычно называется параллелизмом. В этом разделе мы дадим основной краткий обзор того, как внутренне реализуется блокировка (в противоположность нашему использованию оптимистической блокировки, рассмотренной в главе 10) и как она предотвращает искажение данных, если несколько процессов пытаются обратиться к данным одновременно.
Типы блокировок SQL Server внутренне реализует схему блокировок, которая позволяет пометить транзакцию или операцию, что она связана с данным ресурсом или коллекцией ресурсов. Метка указывает другим процессам, что ресурс заблокирован. SQL Server предотвращает явную установку и очистку блокировок, так что ресурс с такой меткой защищен. В зависимости от уровня использования могут быть заблокированы следующие ресурсы (типы блокировок перечислены в порядке уменьшения детализации): • • •
Строка — отдельная строка в таблице. Ключ или диапазон ключа — ключ или диапазон ключа в индексе. Страница — страница структуры в 8 Кб, которая содержит индекс или данные. 443
Глава 12 •
Таблица — полная таблица, то есть, все строки и ключи индекса.
•
БД — полная БД (обычно используется только при изменении схемы БД).
Обратите внимание, что имеется также блокировка экстента (экстент обычно используется при распределении памяти в БД и имеет размер в восемь страниц по 8 Кб). Начальный тип используемой блокировки определяется оптимизатором SQL Server, который также решает, когда переходить от одного типа блокировки к другому, чтобы экономить память и увеличить производительность. Вообще говоря, оптимизатор выберет наименьшую блокировку — тип блокировки строки, если это возможно, но если возникнет вероятность блокировки всех или части строк, может быть выбран тип блокировки таблицы. Используя указания оптимизатору в наших запросах, мы можем до некоторой степени регулировать типы используемых блокировок. Это будет кратко рассмотрено.
Способы блокировки Конечно, системному архитектору полезно понимать, как SQL Server выбирает степень используемой детализации уровня. Сначала мы опишем различные существующие способы блокировки: •
Совместные блокировки — используются, когда вы просто хотите просмотреть данные. Несколько соединений могут иметь совместные блокировки одного и того же ресурса.
•
Блокировки обновления — они позволяют нам указать, что мы планируем обновить данные, но пока не готовы это делать, например, если мы все еще обрабатываем выражение WHERE, чтобы определить, какие строки нужно обновить. Мы можем все еще иметь совместные блокировки таблицы, и другие соединения могут быть освобождены в течение периода времени, пока мы все еще готовимся к обновлению.
•
Эксклюзивные блокировки — они дают исключительный доступ к ресурсу. Никакая другая связь не может иметь доступ вообще. Они часто используются после того, как физический ресурс был изменен в пределах транзакции.
•
Направленные блокировки — они передают информацию другим процессам, которые имеют или собираются создавать блокировки некоторого типа. Имеются три типа направленных блокировок: направленные совместные, направленные эксклюзивные и совместные с направленными эксклюзивными (чтение с намерением сделать изменения). Для любого, кто когда-либо был около водолаза, направленные блокировки аналогичны бакенам, которые водолазы помещают на поверхности воды, чтобы отметить свое положение. Поскольку мы видим бакен, то знаем, что имеется деятельность, протекающая под водой. SQL Server видит эти маркеры на объектах и знает, что если, например, направленная совместная блокировка расположена на таблице, то невозможно получить исключительную блокировку для этой таблицы, поскольку другие соединения уже используют ресурсы.
Наряду с предыдущим списком ресурсов, которые могут быть заблокированы, имеется несколько дополнительных способов, которые мы упомянем мимоходом, но которые находятся за пределами возможностей главы. Блокировки схемы используются, когда изменяется структура БД, перекомпилируется хранимая процедура или что-нибудь еще изменяется в физической структуре БД. Блокировки массового обновления используются при выполнении массовых корректировок. Для дополнительной информации см. SQL Server 2000 Books Online. 444
Расширенный доступ к данным и методы корректировки Итак, мы теперь знаем, что имеются блокировки различных уровней процесса, и каждый раз, когда мы делаем что-нибудь в SQL Server, устанавливаются метки, сообщающие другим процессам, что мы работаем с ресурсом. Это должно заставить их ожидать, пока мы не закончим использовать ресурс, если они должны выполнять задачу, которая является несовместимой с тем, что мы в настоящее время делаем, — например, пробуя просматривать запись, которую мы изменяем, или пытаясь удалить запись, которую мы в настоящее время выбрали для чтения.
Уровни изоляции Блокировки устанавливаются на время выполнения элементарных операций, которые мы попросили, чтобы SQL Server выполнил. Рассмотрим, например, случай, когда мы начинаем транзакцию с таблицей, называемой tableX (таблица X): Щ SELECT * FROM dbo.tableX
Блокировки (в этом случае совместные блокировки) устанавливаются для этого ресурса на время, определяемое так называемым уровнем изоляции. SQL Server обеспечивает следующие четыре уровня изоляции, перечисленные здесь от наименее до наиболее ограничительного: •
Свободное чтение (Read Uncommitted) — игнорирует все блокировки и не формирует блокировки. При свободно читаемом уровне изоляции мы можем просматривать данные при элементарных операциях, которые не были пока закончены, и которые можно откатить. Обратите внимание, что никаких корректировок на этом уровне изоляции фактически не происходит; если вы изменяете строку в пределах транзакции, вы устанавливаете блокировки, которые предотвращают другие процессы от несовместимых изменений. Это очень опасная вещь — выполнять такую изоляцию в общем случае. Так как мы можем при этом просматривать свободные транзакции, никогда не ясно, насколько ценно то, что мы читали. Однако такой вариант может использоваться во время формирования отчетов, если при этом можно достигнуть границ ошибок; это может позволить читать данные, которые не удовлетворяют всем установленным ограничениям и которые могут исчезнуть после чтения.
•
•
Фиксированное чтение (Read Committed) — это намного чаще используемый уровень изоляции, поскольку он обеспечивает разумный баланс между безопасностью и параллелизмом. Мы не можем видеть свободные данные, хотя и допускаем неповторяемое чтение (условие, при котором если мы читаем набор строк, то ожидаем, что строки будут существовать на всем протяжении транзакции) и "невидимые" строки, являющиеся новыми записями, которые создаются другими пользователями в то время как мы находимся в нашей транзакции. Обычно, это — лучший выбор для уровня изоляции и фактически используется по умолчанию в SQL Server. Повторяемое чтение (Repeatable Read) — повторяемое чтение гарантирует, что если пользователь должен выполнить один и тот же оператор SELECT дважды и при этом он не изменил данные между этими двумя операциями, то он получит точно те же самые данные дважды. Неповторяемое чтение не может гарантировать этого, если другой параллельный пользователь может изменить данные.
445
Глава 12 Как вы могли предполагать, повторяемое чтение защищает уровень фиксированной изоляции и предотвращает неповторяемое чтение, помещая блокировки на все данные, которые используются в запросе, предотвращая другие связи от изменения данных, которые вы уже читали. Когда вы выполняете запрос второй раз, он возвратит, по крайней мере, тот же самый набор строк, как это было в первый раз. Однако это не предохраняет от "невидимых" строк. •
Упорядочиваемый (Serializable) — этот уровень изоляции размещает блокировки диапазонов на индексах и наборах данных, предотвращая пользователей от создания любых "невидимых" строк. Чтобы проиллюстрировать то, что я сказал, рассмотрим пример того, что произойдет, если мы возьмем уровень изоляции с фиксированным чтением, используемый по умолчанию. Сначала мы создадим таблицу, которую будем использовать для испытания, и добавим некоторые записи:
CREATE TABLE testlsolationLevel ( testlsolationLevelld int IDENTITY, value varchar(lO)
INSERT VALUES INSERT VALUES
INTO dbo.testlsolationLevel(value) ('Значение ]/) INTO dbo.testlsolationLevel(value) ('Значение 2')
Здесь создается таблица testlsolationLevel (испытание уровня изоляции) с двумя столбцами: автоинкрементный столбец testlsolationLevelld (идентификатор строки таблицы "испытание уровня изоляции") типа int и столбец value (значение) типа varchar (10). Далее в эту таблицу помещаются две записи, у которых столбец value имеет значения "Значение 1" и "Значение 2". Далее мы можем выполнить следующие команды в одном соединении с SQL Server: SET TRANSACTION ISOLATION LEVEL READ COMMITTED -- задается только для целей иллюстрации. Если мы не установим это -- для соединения, то тем не менее будем находиться в режиме — фиксированного чтения, поскольку это значение по умолчанию BEGIN TRANSACTION SELECT * FROM dbo.testlsolationLevel
и увидим следующий результат: testlsolationLevelld 1 2
value Значение 1 Значение 2
Используя другое соединение, выполним следующие операции:
I
446
DELETE FROM dbo.testlsolationLevel WHERE testlsolationLevelld = 1
Расширенный доступ к данным и методы корректировки Затем вернемся к нашему первому соединению, которое все еще открыто, и внутри транзакции выполним операцию:
I
SELECT * FROM d b o . t e s t l s o l a t i o n L e v e l COMMIT TRANSACTION
и у нас нет проблем с удалением строки: testlsolationLevelld
value
2
Значение 2
Это было неповторяемое чтение. Затем мы удалим таблицу, заново инициализируем ее и повторно выполним наш небольшой тест, изменив уровень изоляции: Щ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
На сей раз, когда мы пробуем выполнить оператор DELETE во втором соединении, он не будет завершен, пока не будет закрыто наше первое соединение. Второе соединение будет заблокировано первым соединением и не сможет выполнять свои команды. Следовательно, наш второй оператор SELECT возвратит, по крайней мере, те же самые значения — другими словами, это — повторяемое чтение. Но мы все еще должны рассматривать проблему "невидимок" (то есть "невидимых" строк). Мы предотвратили любые удаления строк, но что касается добавления строк? Из названия "повторяемое чтение" мы могли бы полагать, что защищены от этой ситуации. Однако давайте повторим нашу ситуацию снова. Мы удалим таблицу и заново создадим ее, поместив те же самые две строки. Затем выполним первоначальную транзакцию с первым соединением: SET TRANSACTION ISOLATION LEVEL READ COMMITTED BEGIN TRANSACTION SELECT * FROM dbo.testlsolationLevel
I
{Здесь, по-видимому, сделана ошибка, и требуется написать Щ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
как следует из последующих рассуждений. Прим. перев.) ... и снова увидим следующие результаты: testlsolationLevelld
value
1 2
Значение 1 Значение 2
Во втором соединении мы выполним следующую команду: I
INSERT INTO dbo.testlsolationLevel(value) VALUES (Значение 3')
447
Глава 12 Эта операция не блокируется, как было с операцией DELETE, поскольку это допустимо. Теперь, если мы выполняем следующую команду в первом соединении, как прежде: SELECT * FROM dbo.testlsolationLevel COMMIT TRANSACTION
I
... мы получим дополнительную строку: testlsolationLevelld
value
1 2 3
Значение 1 Значение 2 Значение 3
Теперь мы имеем повторяемое чтение, но также имеем и "невидимую" строку. Наконец, если мы вернемся и запустим этот пример в последний раз, но определим уровень изоляции SERIALIZABLE (упорядочиваемый), оператор INSERT в этом примере будет ожидать завершения соединения. Из наших примеров может показаться, что упорядочиваемый уровень изоляции является лучшим вариантом, если мы хотим запретить другим пользователям обращаться к нашим данным в то время, когда мы используем их. Если данные изменяются, используя транзакцию, то вероятнее всего желательно, чтобы данная ситуация существовала, пока не закончатся все действия. Всякий раз, когда мы изменяем данные, SQL Server размещает эксклюзивные блокировки в ресурсы, которые мы изменяем. Если другой пользователь пробует получить этот конкретный ресурс, он должен будет ожидать. Однако все другие ресурсы, которые используются как часть транзакции, также будут заблокированы, и параллельный пользователь не может обращаться ни к какому из этих ресурсов, когда они используются в транзакции. Когда уровень изоляции увеличивается, SQL Server должен разместить и поддерживать более серьезные блокировки ресурсов в процессе: •
Свободное чтение (Read Uncommitted) — никакие блокировки не устанавливаются и никакие не учитываются, даже эксклюзивные блокировки.
•
Фиксированное чтение (Read Committed) — устанавливаются совместные блокировки во время просмотра ресурса, но как только процесс заканчивается, они освобождаются.
•
Повторяемое чтение (Repeatable Read) — устанавливаются те же самые совместные блокировки, однако они не освобождаются до конца транзакции.
•
Упорядочиваемый (Serializable) — все диапазоны данных (включая целые таблицы), которые могут встретиться при любом выполняемом нами чтении, блокируются, и никакой внешний пользователь не может делать никаких изменений в этих данных.
Одно примечание. При использовании любого уровня изоляции, отличного от упорядочиваемого, мы оставляем себе маленькую лазейку в целостности данных. Рассмотрим, что произойдет в триггерах и ограничениях-проверках, которые используют функции для доступа к другим таблицам, чтобы посмотреть, встречались ли ранее условия для сохранения записи. В зависимости от нашего уровня изоляции, мы могли бы проверить, что существует некоторое условие, а десятью миллисекундами позже другой пользователь мог бы изменить данные в нашей системе, при этом данные находятся в пределах спецификаций. Другими десятью миллисекундами позже мы можем передать на диск теперь уже неправильные данные. 448
Расширенный доступ к данным и методы корректировки Это дополнительное свидетельство, подтверждающее наше раннее утверждение, что упорядочиваемый уровень наиболее подходящий для всех ваших транзакций. Однако мы должны объяснить причину, почему фиксированное чтение является уровнем изоляции по умолчанию для SQL Server. Он имеет все, чтобы сбалансировать потенциально противоречивые проблемы последовательности и параллелизма. Использование упорядочиваемого уровня изоляции может запретить другим пользователям получать данные, в которых они нуждаются, следовательно, сильно сокращая параллелизм, особенно, когда вы имеете много пользователей, использующих одни и те же ресурсы. Использование фиксированного чтения оставляет щель в нашей защите в течение чрезвычайно маленького периода времени (обычно порядка миллисекунд), с минимальным риском. Рекомендуется, чтобы упорядочиваемый уровень и повторяемое чтение использовались только тогда, когда они абсолютно необходимы. Примером такого случая могла бы быть банковская или финансовая система, где даже самая маленькая возможная ошибка в счете пользователя могла бы вызвать реальные проблемы. Другая рекомендация заключается в том, что все транзакции должны быть как можно меньше, чтобы сохранять блокировки как можно короче. Это влечет за собой попытку держать транзакции в едином пакете. Любые заблокированные ресурсы остаются такими, пока SQL Server ожидает завершения вашей работы. Очевидно, что работа ухудшается, если вы дольше ожидаете и выше уровень изоляции. SQL Server будет всегда стараться закончить любой пакет, который он начал выполнять. Если мы попытаемся разорвать соединение в середине открытой транзакции, соединение не будет разорвано немедленно, нарушая дальнейшую работу. Указания оптимизатору Сейчас мы кратко упомянем об одной возможности на уровне таблицы — указаниях оптимизатору. Если нам не нужно полное соединение или операция использования конкретного уровня изоляции, но мы просто хотим каким-то образом использовать в операторе таблицу, то можем определить уровень изоляции как указание оптимизатору.
I
SELECT <имя поля_1>, <имя_поля_2>, ... , <имя_поля_Ы> FROM <имя_таблицы_1> WITH (optimizerHintl, optimizerHint2, ... optimizerHintX) WHERE <условие>
Здесь optimizerHintK—
K-e указание
оптимизатору.
Предположим, что мы имеем, например, таблицу customer (заказчик) и таблицу i n v o i c e (счет-фактура), которая имеет внешний ключ к таблице customer.
I
SELECT i n v o i c e . d a t e AS i n v o i c e D a t e FROM dbo.customer WITH (READUNCOMMITTED) JOIN d b o . i n v o i c e ON c u s t o m e r . c u s t o m e r l d = i n v o i c e . c u s t o m e r l d
В этом экземпляре нет никаких общих блокировок, помещенных в таблицу customer, но они имеются в таблице i n v o i c e , поскольку к ее ресурсам обращаются. Имеются указания оптимизатору для каждого значения уровня изоляции, а также и некоторые другие, чтобы сформировать некоторые виды блокировок: •
READUNCOMMITTED (также известный как NOLOCK) — никакие блокировки не устанавливаются и не используются. 449
Глава 12 • •
READCOMMITTED — использует все блокировки, но блокировки не сохраняются, как только завершается работа с ними (используется по умолчанию). REPEATABLEREAD — никогда не удаляются блокировки, раз они были установлены.
•
SERIALIZABLE (также известный как HOLDLOCK) — никогда не освобождает блокировки, пока транзакция не завершит работу, и блокирует все другие операторы корректировки строк, которые могли бы когда-либо соответствовать любому оператору WHERE, который ограничивает выборку данных в указанной таблице.
•
READPAST — используется только для операторов SELECT; он сообщает процессору запросов, чтобы он перескакивал через блокируемые строки и перемещался к неблокируемым. Игнорирует только блокировки строк.
•
ROWLOCK — инициирует внутренний механизм блокировки, выбранный для операций блокировки на уровне строк, даже если бы оптимизатор выбрал бы более мелкую блокировку.
•
TABLOCK — инициирует блокировку таблицы, используемую даже тогда, когда обычно используется более мелкая блокировка. TABLOCKX — то же, что и TABLOCK, но используются эксклюзивные блокировки вместо любого другого типа блокировки, который мог бы быть выбран. UPDLOCK — инициирует использование блокировки обновления вместо совместной блокировки.
• •
В то время как каждое из этих указаний оптимизатору имеет свое применение и может быть полезно, их следует использовать с чрезвычайной осторожностью, и в большинстве случаев будет лучше использовать уровни изоляции вместо попытки непосредственно управлять типами и способами блокировки.
Тупиковые ситуации Тупиковая ситуация — ситуация, которая происходит, когда каждое из двух (или большего числа) соединений блокирует ресурсы, в которых нуждается другое соединение. Для иллюстрации предположим, что мы имеем два соединения — А и В — которые выполняют команды, чтобы модернизировать две таблицы — t a b l e А (таблица А) и t a b l e В (таблица В). Проблема возникает, потому что соединение А сначала корректирует tableA, а затем tableB, в то время как соединение В корректирует те же самые таблицы, но в обратном порядке. Соединение А
Соединение В
Соединение с таблицей А Таблица А блокирована Корректировка выполнена Попытка связаться с таблицей В Блокировка таблицы В
Соединение с таблицей В Таблица В блокирована Корректировка выполнена Попытка связаться с таблицей А Блокировка таблицы А
Так как нет выражения WHERE, будет блокироваться вся таблица, чтобы выполнить корректировку, так что блокировка будет применяться к tableA в то время, когда соединение использует ее. То же самое произойдет с командой корректировки всех строк t a b l e B соединения В. Когда эти два соединения пытаются выполнить свои вторые операторы UPDATE, они оба блокируются — создается тупиковая ситуация. Тогда SQL Server объявляет одно из соединений жертвой тупиковой ситуации и освобождает другое соединение, откатывая блокировки, которые были выполнены на ресурсе. 450
Расширенный доступ к данным и методы корректировки Чтобы определить, какое соединение завершается в тупике, SQL Server выбирает соединение, которое проще откатить назад. Однако имеются настройки, которые вы можете использовать, чтобы поднять приоритет тупика соединений, помогая SQL Server выбрать менее важную связь, чтобы ее удалить. Следующая команда SET установит приоритет тупика соединений, равный LOW (меньший) или NORMAL (нормальный). NORMAL — значение, используемое по умолчанию, а настройка LOW для соединения означает, что наиболее вероятно оно будет жертвой тупика и будет удалено. Ц
SET DEADLOCK_PRIORITY
[LOW | NORMAL]
Тупики раздражают, но они довольно редки, и если вы ожидаете их появление и создаете код, чтобы избежать их, они могут быть легко обработаны. Вы можете просто закодировать ваше приложение, чтобы искать сообщение об ошибке, связанное с возникновением тупика (1205), и сделать так, чтобы повторно выполнить команду, при этом маловероятно, что вы получите тупиковую ситуацию второй раз подряд. Обратите внимание, что эту ситуацию не следует путать с блокировкой, которую мы обсудили ранее. Тупиковые ситуации заставляют одно из соединений отключаться и сформировать ошибку; блокированные соединения ожидают время, определяемое значением LOCK_TIMEOUT (время отката блокировки), и затем откатывают связь. Вы можете установить это свойство LOCK_TIMEOUT, выполняя оператор: §§f SET LOCK_TIMEOUT <время_в_миллисекундах> Ключ к уменьшению тупиковых ситуаций опять-таки в коротких транзакциях. Чем короче транзакция, тем меньше шансов, что будет блокирование или получение тупиковой ситуации другими транзакциями. Одно предложение, которое постоянно упоминается: всегда в ваших транзакциях организовывать доступ к объектам в одном и том же порядке, может быть, в алфавитном порядке. Конечно, такие предложения будут работать только в том случае, если каждый соблюдает это правило.
Основные рекомендации Имеются две очень важных рекомендации для любых клиентов, которые будут использовать наши данные. Избегайте долгой работы транзакций Мы встречались с этим советом уже несколько раз. Однако, что означает долгая транзакция? Долго работающая транзакция может занимать половину секунды, одну минуту, один час или даже день. Что здесь надо иметь в виду, так это то, что мы минимизируем транзакции, которые взаимодействуют с нашими пользователями, обеспечивая их доступ к данным. Вместо того чтобы загружать значения из внешнего источника непосредственно в таблицу наших пользователей, может оказаться лучше загрузить строки в новую промежуточную таблицу, а затем вставлять строки в таблицу пользователей в течение непиковых часов. Непроизводительные издержки при этом могут быть гораздо меньше, чем копирование большой массы данных непосредственно в таблицу.
451
Глава 12 В особых случаях, когда не удается реально выполнять транзакцию за короткое время, вы должны будете или перепроектировать процедуру, или смириться с этим. Когда длительность транзакций занимает пару секунд или меньше того, не так уж много программных возможностей, чтобы уменьшить эту неприятность. Если эта продолжительность нас не устраивает, то может помочь настройка параметров. Транзакции не должны выходить за границы пакета Если вы посылаете на сервер несколько пакетов команд, лучше не начинать транзакцию в одном пакете, а заканчивать ее в другом. Следуя этой важной рекомендации, мы можем сильно уменьшить число некоторых наиболее неприятных ситуаций в работе, которые могут произойти. Рассмотрим следующий пример. На рисунке мы имеем две команды, которые модернизируют tableA (таблица А), а затем tableB (таблица В). Было определено, что нужно выполнить эти команды в транзакции, и мы создали с этой целью аккуратный небольшой набор методов. Вызовем метод b e g i n _ t r a n s a c t i o n () , который соединяется с сервером и начинает транзакцию. Затем он посылает команду, чтобы скорректировать tableA, а за ней команду для корректировки tableB, но где-то в течение последнего вызова локальная сеть разрывается:
/
Начало транзакции и ожидание
Корректировка таблицы А и ожидание
Корректировка таблицы В и ожидание
Завершение транзакции
/
Связь разорвана
SQL Server не будет знать, что ваша связь была разъединена, так что он продолжает сохранять соединение открытым, ожидая, когда вы передадите команду COMMIT или организуете откат. Однако, поскольку вы теперь отошли от транзакции, ни COMMIT, ни откат не произойдет, пока SQL Server не поймет, что связь была нарушена ввиду ее бездеятельности. Наши данные будут правильными только тогда, когда транзакция создает блокировки обновления, чтобы защитить их. Единственная проблема заключается в том, что в зависимости от фактического оператора WHERE деблокировка строки, строк или даже всей таблицы будет затруднена. Следовательно, когда другой пользователь собирается просто просмотреть запись в tableA, он/она вынужден ожидать или нажать кнопку Stop (остановка) на броузере (browser). Ответом на эту проблему может быть передача этих команд в единственном пакете, который выполняет все одновременно. Например: BEGIN TRANSACTION
I
452
UPDATE tableA
Расширенный доступ к данным и методы корректировки
UPDATE tableB
I
COMMIT TRANSACTION GO
В этом случае, если SQL Server получает команды и начинает выполнять их, то или будут выполнены все операции, или ни одна из них. Мы не будем завершать работу с какими-либо заблокированными ресурсами. Так с какой стати пользователь рассматривает посылку транзакций в нескольких пакетах? Здесь пара причин: Q
Наблюдаемая скорость — как только каждый оператор завершается, можно скорректировать индикатор выполнения и сообщить пользователю, что tableA действительно была обновлена, теперь я перемещаюсь к tableB и т. д.
•
Все это легко получить, даже не зная об этом, особенно используя средства типа ADO. Рассмотрим следующий пример кода ADO, взятый из SQL Server Books Online, раздел Performing Transaction in ADO (Выполнение транзакций в ADO):
Dim en As New ADOB.Connection Dim rs As New ADOB.Recordset ' Открытие соединения. en.Open ' Открытие таблицы titles (заголовки). rs.Open "SELECT * FROM titles", en, adOpenDynamic, adLockPessimistic ' Начало транзакции, rs.MoveFirst cn.BeginTrans ' Пользователь просматривает набор записей, выполняя изменения. ' Вопрос, собирается ли пользователь сохранить все сделанные изменения. If MsgBox("Сохранить все изменения?", vbYesNo) = vbYes Then en.CommitTrans Else en.RollbackTrans End If
Этот код будет делать точно то же самое, что мы обсуждали ранее, но в намного более приятном оформлении. Выполняя в соединении метод BeginTrans, серверу посылается команда BEGIN TRANSACTION в одном пакете. Для каждого контакта с записью в цикле (который не закодирован, а только упоминается) оператор корректировки данных будет послан к серверу. Наконец, если все идет хорошо в коде, будут вызываться методы CommitTrans или RollbackTrans, посылая команду COMMIT TRANSACTION (или ROLLBACK TRANSACTION — Прим. перев.). Если внутри возникнет любая ошибка или даже разрыв соединения, то пользователи таблицы t i t l e s будут долгое время ожидать. Обратите внимание также, что вызываемая блокировка adLockPessimistic будет, вероятно, реализовывать уровень изоляции READCOMMITTED (фиксированное чтение) или, может быть даже SERIALIZABLE (упорядочиваемый), так что никакой другой пользователь не может касаться этих строк, пока данный пользователь работает с ними в этом цикле. Простой пример кода показывает, как работают несколько методов в соединении с объектом, но это не лучший пример при использовании многопользовательских программ. 453
Глава 12 Однако если в этой ситуации мы просто размещаем каждый оператор вместо того, чтобы посылать их к SQL Server по одному, мы могли бы выполнить пакет операторов, как только пользователь закончит его формировать. Пользователю, вероятно, придется ждать немного дольше, когда закончится каждая операция, по сравнению с тем, когда каждый шаг выполняется отдельно. Фактически, на все будет затрачено меньшее количество времени, но пользователю будет казаться, что ничто не происходит, и время будет представляться более длинным. Однако в этом случае, если связь будет прервана после того, как сервер получит для работы пакет, мы можем убедиться, что все изменения пользователя будут присутствовать, и все блокировки, используемые для обеспечения непротиворечивости, будут немедленно освобождены. Следуя очень простому правилу хранения транзакций в пределах одного пакета, мы не будем оставлять транзакции подвешенными, поскольку SQL Server будет знать, что все требуемое для транзакции готово до того, как будет выполнена единственная команда. Если мы разорвем соединение, нам гарантируют, что или все, или ни одной из наших команд не будет выполнено, и мы не будем затрагивать работу никакого другого пользователя дольше, чем необходимо. Мы возвратимся к теме составления пакета команд позже в этой главе, когда рассмотрим хранимые процедуры.
Основные проблемы кодирования Любое обсуждение доступа к данным и методов корректировки, использующих запросы, не было бы полно без рассмотрения трех особенностей, которые больше всего затрагивают работу SQL Server: •
временные таблицы;
•
курсоры;
•
значения NULL.
Мы рассмотрим эти темы достаточно глубоко, чтобы вы могли с ними работать эффективно и избегать потенциальных проблем во время работы, которые могут возникнуть из-за их неправильного использования.
Временные таблицы Временные таблицы были кратко упомянуты ранее в книге, когда мы обсуждали создание таблиц. Они подобны постоянным таблицам, но удаляются из системы, когда перестают использоваться. Имеются два различных вида таких таблиц: •
Локальные временные таблицы (чьи имена предваряются символом #), которые используются для соединений с SQL Server.
Q
Глобальные временные таблицы (имена предваряются символами ##), которые могут рассматриваться любой связью и любым пользователем в системе. Они не перестают использоваться, пока соединение, которое их создало, не будет разорвано, и любые другие связи, которые используют эти таблицы, не прекратят обращаться к ним.
Временные таблицы позволяют нам создавать наше собственное временное хранилище и добавлять в них некоторые данные, используя те же самые операторы INSERT, UPDATE и DELETE, как и с постоянными таблицами. Они могут также быть индексированы, если необходимо. Фактически, временная таблица — обычная таблица, которая имеет очень короткую продолжительность жизни. Обратите внимание также, что любой пользователь может создавать и использовать их, когда считает это нужным. 454
Расширенный доступ к данным и методы корректировки
В более ранних версиях SQL Server временные таблицы часто использовались как обходной путь преодолеть некоторые ограничения. В SQL Server 2000 они используются гораздо реже. Использование временных таблиц в запросе будет всегда медленнее, чем непосредственное использование SQL. Например, предположим, что мы хотим попробовать определить число хранимых процедур и функций, которые имеют или два, или три параметра. Использование временной таблицы для этого могло бы оказаться идеальным решением: — создание таблицы для размещения CREATE TABLE #counting
имен объектов
{
specific_name sysname ) -- размещение всех строк, которые имеют от 2 до 3 параметров INSERT INTO #counting(specific_name) SELECT specific_name FROM information_schema.parameters AS parameters GROUP BY specific_name HAVING COUNT (*) BETWEEN 2 AND 3 -- подсчет числа значений в таблице SELECT COUNT(*) FROM #counting DROP TABLE #counting
Здесь создается локальная временная таблица icounting (подсчет), содержащая один столбец specif ic_name (специальное имя) типа sysname. Далее в нее помещаются имена хранимых процедур, у которых имеются 2 или 3 параметра, после чего подсчитывается число записей. В заключение таблица удаляется. Этот код возвращает нужное число (которое, конечно, будет изменяться от сервера к серверу): 355 Немного подумав, мы можем избежать использования временной таблицы вообще и выполнить только SQL-запрос, увеличив скорость запроса:
I
SELECT count (*) FROM (SELECT specific_name FROM information_schema.parameters AS parameters GROUP BY specific_name HAVING count!*) BETWEEN 2 AND 3) AS TwoParms
Мы исследуем этот пример подсчета параметров снова в следующем разделе, посвященном курсорам. 455
Глава 12 Временные таблицы можно было бы избежать более просто, если использовать следующие редко применяемые особенности T-SQL: •
Вторичные таблицы — одно из наиболее важных дополнений к языку T-SQL — возможность включать подзапросы в операции присоединения. Это — чрезвычайно полезная техника и позволяет нам выполнять большинство действий, которые мы раньше делали бы с использованием временных таблиц в наших запросах, наподобие следующего:
SELECT tableName.subqueryTableName, alias.subqueryTableNameld AS aliasKey FROM tableName JOIN (SELECT subqueryTableNameld, tableNameld FROM subqueryTableName) AS alias ON tableName.tableNameld • alias.tableNameld
Здесь в основном запросе выделяются столбцы subqueryTableName (имя таблицы подзапроса) из таблицы tableName (имя таблицы) и subqueryTableNameld (идентификатор имени таблицы подзапроса), формируемый в подзапросе, в котором одновременно выделяется столбец tableNameld (идентификатор имени таблицы) и которые образуют вторичную таблицу alias (псевдоним). Подзапрос действует так же, как однопользовательское представление в запросе, и позволяет вам использовать операторы GROUP BY и HAVING, при этом вам не нужна временная таблица, чтобы выполнить запрос, который по этой причине будет намного быстрее. Обратите внимание, что, если необходимо, мы можем вкладывать вторичные таблицы в другие вложенные таблицы. •
UNION [ALL] — часто временная таблица будет строиться, используя два набора и помещая их вместе. Во многих случаях вместо этого можно использовать операцию UNION ALL. Было время, когда я получил 40 процентов улучшения скорости, используя UNION ALL вместо временной таблицы. Аналогично, если для полного набора используется заключительный оператор WHERE, мы можем поместить объединенный набор во вторичную таблицу!
•
Большое число таблиц в одном запросе — в некоторых случаях может быть целесообразно разделить длинные запросы на небольшие, более приемлемые части кода, разбивая их на запросы к временным таблицам. Однако SQL Server 2000 позволит организовывать использование в одном запросе до 256 постоянных таблиц, и это часто позволит выполнить запрос лучше, чем вы могли раньше.
Это все выглядит как последний гвоздь в гроб временных таблиц. Однако они могут быть все еще полезны, когда мы должны некоторым образом управлять данными, когда требуется более одного прохода. Я использую временные таблицы весьма часто при реализации рекурсивного дерева для отображения на экране — добавления значений для каждого уровня дерева следующего уровня. Временная таблица идеальна в этой конкретной ситуации, потому что мы должны повторять проход через набор, чтобы обратиться к каждому уровню дерева, и должны где-то хранить промежуточные значения. В следующем примере мы возьмем рекурсивную таблицу, которую назовем type (тип). Каждый из типов может иметь неограниченное число подтипов. Мы должны упорядочить их и показать как дерево с подэлементами, непосредственно следующими за другими подтипами. Дерево данных имеет следующие узлы, с которыми мы будем иметь дело:
456
Расширенный доступ к данным и методы корректировки
-- сначала создаем таблицу type CREATE TABLE type ( typeld int, -- не автоинкрементный, поскольку нам нужна ссылка к -- этому элементу в данном фрагменте parentTypeld int NULL, name varchar(60) NOT NULL
— затем мы должны заполнить таблицу type INSERT INTO type (typeld, parentTypeld, name) VALUES (1, NULL, 'Вершина') INSERT INTO type (typeld, parentTypeld, name) VALUES (2, 1, 'Потомок 1 вершины') INSERT INTO type (typeld, parentTypeld, name) VALUES (3, 1, 'Потомок 2 вершины') INSERT INTO type (typeld, parentTypeld, name) VALUES (4, 2, 'Потомок 1 потомка 1вершины') INSERT INTO type (typeld, parentTypeld, name) VALUES (5, 2, 'Потомок 2 потомка 1 вершины') Теперь создадим запрос, используя временную таблицу, которая будет отображать дерево. Заметьте, что м ы здесь используем временную таблицу в качестве массива для размещения некоторых величин, когда м ы выполняем дополнительные проходы — оператор SELECT здесь будет недостаточен: CREATE TABLE #holdltems ( typeld int NOT NULL, parentTypeld int NULL, name varchar (100), identLevel int, sortOrder varchar (1000)
--— -— --
первичный ключ таблицы table рекурсивный атрибут название типа уровень в дереве для определения отображения порядок сортировки для окончательного отображения
Здесь мы создаем таблицу iholdltems (хранилище элементов) с пятью столбцами: typeld (идентификатор типа) типа int, который не может принимать значение NULL, parentTypeld (идентификатор типа предка) типа int, который может принимать значение NULL, name (название типа) типа varchar (100), identLevel (уровень отображения) типа int, sortOrder (порядок сортировки) типа varchar (1000). 457
Глава 12 Затем мы можем добавить вершину элементов дерева во временную таблицу: -- получение верхнего уровня элементов INSERT INTO Iholdltems -- Первый проход начнется с identLevel, равного 1. Мы поместим -- первичный ключ предка в поле sortOrder, дополнив нулями справа, -- затем добавим потомки каждого подуровня SELECT typeld, parentTypeld, name, 1 AS identLevel, LTRIM(replace ('0', 10 - LEN(CAST(typeld AS char (10)))) + CAST(typeld AS char(10))) AS sortOrder FROM dbo.type WHERE parentTypeld IS NULL — предок, равный NULL означает верхний элемент
Далее мы должны закодировать цикл, чтобы работать с нижними уровнями таблицы по одному, организуя строки для выхода: DECLARE ScurrLevel int SET @currLevel • 2 WHILE 1 = 1 BEGIN
—
так как в T-SQL нет цикла REPEAT . . UNTIL
INSERT INTO #holdltems -- добавление порядка сортировки этого элемента к текущему порядку сортировки, — учитывая порядок предка SELECT type.typeld, type.parentTypeld, type.name, @currLevel AS identLevel, RTRIM(#holdItems.sortOrder) + LTRIM(replicate('0', 10 — LEN(CAST(type.typeld AS char(10)))) + CAST(type.typeld AS char(10))) FROM dbo.type AS type — это присоединение дает нам запись-потомок элемента -- в таблице tholdltems JOIN #holdltems ON type.parentTypeld • #holdltems.typeld — currLevel дает нам уровень, который мы добавляем, — так что нам нужен предок в JOIN WHERE #holdltems.identLevel = @currLevel - 1 -- если нет потомков на новом уровне, то выходим из цикла IF @@rowcount = 0 BREAK -- иначе просто увеличиваем счетчик и продолжаем SET @currLevel = @currLevel + 1 END
458
Расширенный доступ к данным и методы корректировки Теперь м ы имеем все заданные величины, которые нужны для отображения дерева SELECT typeld, parentTypeld, CAST(name AS varchar(15)) AS name, identLevel FROM #holdltems ORDER BY sortOrder, parentTypeld -- удаление таблицы явно не требуется, если она в хранимой процедуре -- или если мы разрываем соединение между выполнениями DROP TABLE tholdltems
Все это дает следующий набор строк: typeld
parentTypeld name
identLevel
1 2 4 5 3
NULL 1 2 2 1
1 2 3 3 2
Вершина Потомок 1 вершины Потомок 1 потомка 1 вершины Потомок 2 потомка 1 вершины Потомок 2 вершины
Все это формирует основу для программы, которая имеет дело с двоичными (рекурсивными) деревьями, закодированными в SQL-таблицах. Расширяя этот алгоритм, мы можем делать почти все, что должны делать с двоичными деревьями, и при этом иметь хорошие результаты. Однако не забудьте: если бы мы могли делать это без временных таблиц, то все было бы быстрее. Всегда перед использованием временной таблицы удостоверьтесь, что невозможно закодировать решение без нее.
Курсоры В SQL обычно имеют дело с данными, представляющими одновременный набор. Мы видели это много раз в наших примерах. Однако не всегда возможно иметь дело с наборами данных. Курсоры — механизмы в T-SQL, которые позволяют нам обратиться к отдельной строке и выполнить над ней действия, а не работать в предварительно выделенной группе строк. Имеются два различных типа курсоров, которые часто используются: •
Внешние API-курсоры БД (API — Application Programming Interface — интерфейс прикладного программирования) — это специфические курсоры, которые используются, чтобы обеспечить внешние интерфейсы к SQL Server. Обычные примеры — ODBC, OLE DB и ADO. Используя курсоры, связанные с этими технологиями, программисты могут строить потоки данных для своих приложений. API-курсоры, прежде всего, используются прикладными программистами, и раз это так, они вне возможностей данной книги.
•
Transact-SQL курсоры — эти курсоры используются в коде T-SQL, и мы будем подробно рассматривать их в данном разделе. Они будут использоваться в цикле с SQL-шагом, чтобы выполнить операцию над одной строкой в одно время.
459
Глава 12 Люди имеют противоположные мнения относительно курсоров в коде T-SQL. Здесь имеются две основные точки зрения: •
Курсоры — зло.
•
Курсоры — очень полезные средства, которые мы должны использовать.
Причины, почему курсоры были названы "злом", двояки: •
Во-первых, они приводят к неважным реализациям решения задач, которые лучше решаются на основе наборов SQL-операций. В этих случаях выполнение запроса, использующего курсоры, будет часто на порядок медленнее, чем при использовании операций с набором данных.
•
Если мы не используем соответствующие типы курсоров, то завершаем размещением блокировок для каждой строки нашей БД, к которой обращаемся. При использовании T-SQL-курсоров проблема может быть менее существенна, так как наш T-SQL-код никогда не будет ожидать реакции пользователя, но проблема, конечно, все еще существует.
Вторая точка зрения на использование курсоров также справедлива, так как имеются случаи, где вы должны их использовать. Однако с появлением определяемых пользователем функций имеется на самом деле только одна ситуация, где курсор обеспечивает единственную возможность реализации, —когда вы должны выполнить некоторую команду с каждой строкой в наборе. Например, курсор позволил бы вам выполнить хранимую процедуру заказа, содержащую бизнес-логику для каждой строки в таблице. Обсудим теперь основной синтаксис курсоров. Чтобы использовать курсор, вы должны взять команду SET: SET @<имя_курсора> = CURSOR [FORWARDJDNLY | SCROLL] [STATIC | KEYSET | DYNAMIC | FAST_FORWARD] [READ__ONLY | SCROLL_LOCKS | OPTIMISTIC] [TYPEJflARNING] FOR <оператор SELECT> [FOR UPDATE [OF <столбец_1>, <столбец_2>, ... , <столбец_Ы>]]
Если вы использовали курсоры раньше, то, вероятно, более знакомы с синтаксисом DECLARE CURSOR. По моему мнению, использование оператора SET, чтобы установить переменную курсора, является более последовательным путем работы с ним. Это приводит к лучшему коду, так как все курсоры, построенные таким образом, — всегда локальные курсоры и выглядят почти такими же, как обычные переменные SQL Server. Поскольку они ограничены конкретным пакетом, их не нужно размещать и освобождать.
Имя §Ц SET @<имя_курсора> - CURSOR
Этот оператор объявляет переменную как величину типа c u r s o r . Ее можно затем передавать всюду различным процедурам как параметр. Курсор обладает теми же самыми возможностями, как и обычная SQL-переменная, и это облегчает необходимость явным образом закрывать и перемещать курсоры. 460
Расширенный доступ к данным и методы корректировки Направление Щ
[FORWARDJDNLY
|
SCROLL]
Обычно вам удобно использовать курсор FORWARD_ONLY (только вперед), если только у вас нет каких-либо важных причин перемещаться в таблице назад и вперед (SCROLL — прокрутка). Курсор FORWARD_ONLY используют меньшее количество ресурсов, чем курсор SCROLL, но если вы должны перемещаться на предыдущие строки, он может не подойти. Мы обычно оставляем эту настройку незаполненной, поскольку настройка FAST_FORWARD из следующего набора необязательных параметров не будет работать ни cFORWARD_ONLY, ни со SCROLL, a FAST_FORWARD создает наиболее оптимизированный курсор в случае обычного использования. Тип курсора Ц
[STATIC | KEYSET | DYNAMIC | FAST_FORWARD]
С параметром STATIC (статический) SQL Server копирует нужные вам данные в свою БД tempdb, и затем вы можете переместить их назад. Никакие изменения, сделанные в основных данных, не будут видны в курсоре. Это значение используется по умолчанию. Курсоры KEYSET (набор ключей) просто делают копию ключей в tempdb и затем, когда вы запрашиваете строку, SQL Server выходит на физическую таблицу и получает из нее данные. Вы не можете видеть никаких новых данных, но будете видеть любые изменения в существующих данных, включая удаление. Курсоры DYNAMIC (динамический) несколько более дорогостоящие в смысле функционирования, поскольку значения курсора не преобразуются в набор операций, пока вы реально не осуществляете выборку данных с их помощью. С курсорами KEYSET элементы таблицы фиксируются на момент создания курсора. Курсоры же DYNAMIC получают свои элементы, определенные только тогда, когда курсор перемещен. Лучший выбор должен обычно определяться курсором FAST_FORWARD (быстрый вперед), но эта настройка устраняет любое из назначений направления и не позволяет выполнять прокрутку. Это не статический курсор, потому что если вы выполняете любые изменения данных, они будут отображены. В одном из примеров мы кратко покажем, как это свойство может влиять на курсоры, которые мы используем. Обновляемость Щ
[READJDNLY
| SCROLL_LOCKS
| OPTIMISTIC]
Параметр READ_ONLY (только чтение) должен быть очевиден, но другие два — не столь очевидны. Оба они позволяют вам корректировать данные в курсоре. Однако использование позиционных обновлений в коде T-SQL не лучший вариант, поскольку это может действительно запутать, где вы точно находитесь в наборе данных. Если вы должны выполнять обновление, используйте оператор UPDATE с нестатическим типом курсора (KEYSET или DYNAMIC):
I
UPDATE <имя_таблицы> SET <поле> = value FROM <имя_таблицы> WHERE CURRENT OF @<имя_переменной-курсора>
461
Глава 12 Ключевое поле CURRENT OF сообщает оператору, что нужно изменить строку, куда в настоящее время помещен курсор. Вы можете также выполнить и операцию удаления. Чтобы выполнить обновления в месте расположения курсора, мы можем использовать настройки SCROLL_LOCKS (прокрутка с блокировкой) и OPTIMISTIC (оптимистический). SCROLL_LOCKS гарантирует, что наши обновления и удаления (выполненные в положении, определяемом выражением CURRENT OF) выполняются, помещая блокировки на основные таблицы. OPTIMISTIC использует столбец таблицы типа timestamp, а если он недоступен, строит контрольную сумму для каждой строки. Эта контрольная сумма используется точно так же, как оптимистическая блокировка, которую мы обсудили ранее, заново вычисляя перед изменением, чтобы видеть, изменил ли другой пользователь строку. Однако, если вы не находите действительно серьезных оснований для обновления данных с помощью курсора, не делайте этого. Намного более просто решить ту же самую задачу, используя код, подобный следующему:
I
UPDATE <имя_таблицы> SET <поле> = value FROM <имя_таблицы> WHERE <первичный ключ> = @<переменная_'выборки' использующая курсор>
Он делает почти то же самое, что и курсор CURRENT OF, но более легок для чтения и отладки, так как мы можем распечатать переменную, когда выполняем наши примеры. Понижение типа курсора Ц
[TYPE_WARNING]
Если оператор SELECT, который вы определили, не может использовать данный тип курсора, эта настройка (TYPE_WARNING — предупреждение о типе) формирует сообщение об ошибке из SQL Server, предупреждая об этом. Такая ситуация может возникнуть, если вы пробуете использовать курсор KEYSET на наборе данных, не имеющем уникального ключа, на основе которого строится набор ключей, — SQL Server может использовать вместо этого курсор STATIC. Работа курсора Этот пример иллюстрирует некоторые проблемы использования курсоров. Мы хотим выяснить, какие хранимые процедуры и функции в основной БД имеют два или три параметра (как мы делали в примере с временной таблицей) на основе представления PARAMETERS информационной схемы (INFORMATION_SCHEMA). Будем рассчитывать время операций, как будто они выполняются на моем портативном компьютере Dell Pentium II 400 Мгц с 256 Мб ОЗУ. Сначала создадим временную таблицу, используя курсор: SELECT getdate ()
-- получение времени перед запуском кода
— временная таблица для размещения имен процедур во время цикла CREATE TABLE #holdProcedures ( specific_name sysname ) DECLARE @cursor cursor, @c_routineName sysname
-- переменная типа курсор -- переменная для хранения значений во время цикла
SET @cursor - CURSOR FAST_FORWARD FOR SELECT DISTINCT specific_name FROM INFORMATION_SCHEMA.parameters 462
Расширенный доступ к данным и методы корректировки OPEN ^cursor -- активизация курсора FETCH NEXT FROM @cursor INTO @c_routineName —
получение первой строки
WHILE @@fetch_status - 0 -- означает, что строка выбрана правильно BEGIN -- проверка, находится ли число параметров данного конкретного -- имени в пределах от 2 до 3 IF (SELECT count (*) FROM INFORMATION_SCHEMA.parameters AS parameters WHERE parameters.specific_name = @c_routineName) BETWEEN 2 AND 3 BEGIN -- если это так, помещение во временное хранилище INSERT INTO #holdPpocedures VALUES (@c__routineName) END FETCH NEXT FROM @cursor INTO @c_routineName -- получение следующей строки END
Здесь создается временная таблица iholdPpocedures (размещение процедур) с единственным столбцом specific_name (конкретное имя) типа sysname, а также две переменные Qcursor (курсор) типа cursor и @c_routineName (имя программы) типа sysname. Далее инициируется курсор и в переменную @c_routineName помещается имя первой процедуры. Затем организуется цикл просмотра всех имен процедур, и если очередная процедура имеет от двух до трех параметров, ее имя помещается во временную таблицу. ... и если мы это сделали, то можем подсчитать число записей: SELECT count (*) FROM #holdProcedures I SELECT g e t d a t e O
— вывод времени после завершения
кода
Выполнив этот фрагмент, мы получим следующий результат 2001-01-10 23:46:59.477 355 2001-01-10 23:47:00.917 Это указывает, что операция заняла примерно 1.5 секунды времени выполнения. Это не так уж и медленно, но имеется, конечно, лучший способ выполнения этой задачи, как показано ниже: SELECT g e t d a t e O
— снова для демонстрационных целей
SELECT count (*) - - использование вложенного подзапроса для получения всех функций — с соответствующим числом параметров FROM (SELECT p a r a m e t e r s . s p e c i f i c _ n a m e FROM INFORMATION_SCHEMA.parameters AS parameters GROUP BY p a r a m e t e r s . s p e c i f i c _ n a m e HAVING c o u n t ! * ) BETWEEN 2 AND 3) AS counts SELECT g e t d a t e O 16-1868
463
Глава 12 В этот раз мы получим несколько лучший результат: 2001-01-10 23:50:25.160 355 2001-01-10 23:50:25.823 Теперь требуется 0.7 секунды, чтобы выполнить запрос, что составляет приблизительно половину времени, которое потребуется, чтобы решить ту же самую задачу, используя курсор. Кроме того, код гораздо короче и более легок для оптимизации в SQL Server. Хотелось бы надеяться, что этот пример хорошо демонстрирует, что курсоры являются большой проблемой при работе системы, используя намного больше ресурсов, чем требуют операции, основанные на наборе данных. Использования курсоров Хотя я и предостерег бы против входящего в привычку использования курсоров, как я упомянул во введении к этому разделу, они могут быть очень полезны в некоторых ситуациях. Имеются обстоятельства, при которых, как я полагаю, курсоры будут очень полезны: •
Выполнение системных команд типа DBCC (Database Consistency Checker — модуль контроля непротиворечивости БД) или системных хранимых процедур.
•
Выполнение хранимых процедур для каждой строки в наборе данных.
•
В случае, когда мы должны обращаться к каждой строке индивидуально, а не в наборе данных.
Первые два из них довольно очевидны. Простой пример системной хранимой процедуры, когда нам нужно использовать курсоры, — процедура sp_adduser (стандартная процедура добавления пользователя). Если бы мы получали пользователей, например, из электронной таблицы, мы, вероятно, импортировали бы данные во временную таблицу в SQL Server, пробежали бы через все значения, используя курсор, и добавили новых пользователей в БД. Второй случай отличается от первого, но не намного. Если вы должны выполнить хранимую процедуру отдельно для каждой строки в наборе данных, вы обычно должны использовать курсор. Это — довольно стандартная операция (типа реализации бизнес-правил с помощью хранимой процедуры), хотя и не оптимальный способ использовать такую ситуацию, если вы не владелец системы и вам нужно получить данные. Например, если мы не являемся владельцами системных таблиц, но должны добавить в них данные, то, вероятно, решим, что вызов хранимой процедуры в цикле — единственный способ выполнить эту задачу. Если бы мы были владельцем этих таблиц, то могли бы выполнить эту задачу, используя вместо этого триггеры. Третья ситуация такова, что мы можем получить неприятности. Имеется чрезвычайно ограниченное число причин, почему нам потребовалось бы просмотреть каждую строку в нашем наборе данных одну за другой. Единственная общая ситуация, где я использовал бы курсор, и где не вызывал бы некоторый тип функции, демонстрируется в следующем примере. Здесь мы собираемся реализовать специальное упорядочивание набора данных. Загрузим набор данных со случайными значениями и затем единообразно перенумеруем набор, используя курсор. Пусть мы имеем следующую таблицу: 464
Расширенный доступ к данным и методы корректировки
-- таблица для демонстрации курсоров CREATE TABLE item ( itemid int IDENTITY CONSTRAINT XPKItem PRIMARY KEY, sortOrderld int CONSTRAINT xAKItem_sortOrder UNIQUE
Здесь создается таблица item (элемент), содержащая два столбца: автоинкрементный столбец itemid (идентификатор элемента) типа int и sortOrderld (идентификатор порядка сортировки). Оба столбца являются ключами и представляют соответствующие ограничения. Первый шаг — загрузить таблицу, заполняя порядок сортировки случайными числами: DECLARE 0holdSortOrderId int, @counter int SET @counter = 1 -- инициализация счетчика WHILE @counter <= 5 BEGIN -- используем случайные числа между 0 и 100 SET @holdSortOrderId = CAST(RAND() * 100 AS int) -- проверка, что случайное число не существует уже -- как порядок сортировки IF NOT EXISTS (SELECT * FROM dbo.item WHERE sortOrderld = 0holdSortOrderId) BEGIN — поместить число INSERT INTO dbo.item(sortOrderld) VALUES (0holdSortOrderId) -- увеличение счетчика SET @counter = (scounter + 1 END END
Как только мы загрузим нашу таблицу, посмотрим результат:
I
SELECT * FROM dbo.item ORDER BY sortOrderld
Это дает нам достаточно случайный набор чисел, которые мы захотим реорганизовать на следующем шаге: itemid
sortOrderld
4 2 1 3 5
29 35 71 72 81 465
Глава 12
Теперь мы создадим курсор для перемещения по всем элементам в нашей таблице, упорядоченной по значениям порядка сортировки, и будем обновлять каждую строку отдельно: DECLARE @cursor CURSOR, @c_itemld int, @c_sortOrderId int, ScurrentSortOrderld int SET @currentSortOrderId = 1
--— -—
переменная типа CURSOR переменные элементов курсора используется для увеличения порядка сортировки инициализация счетчика
— используем курсор STATIC, так как мы просматриваем измененные величины SET @cursor = CURSOR FORWARD_ONLY STATIC FOR SELECT itemld, sortOrderld FROM dbo.item ORDER BY sortOrderld OPEN Scursor — активизация курсора -- получение первой строки FETCH NEXT FROM @cursor INTO @c_itemld, @c_sortOrderId -- fetch_status = 0 говорит, что выборка точная WHILE @@FETCH_STATUS = 0 — это означает, что строка была выбрана аккуратно BEGIN -- обновление таблицы помещением новой величины порядка сортировки UPDATE dbo.item SET sortOrderld = @currentSortOrderId WHERE itemld - @c itemld -- увеличение счетчика порядка сортировки, чтобы добавить пустой -- промежуток, равный 100 SET @currentSortOrderId = @currentSortOrderId + 100 — получение следующей строки FETCH NEXT FROM dcursor INTO @c itemld, @c sortOrderld END Так как мы добавили строку, чтобы вывести ключи каждой строки таблицы, мы получим следующий выход:
466
@cjtemld
@c_sortOrderld
4
29
@c_itemld
@c_sortOrderld
@currentSortOrderld
35
101
@c_itemld
@c_sortOrderld
@currentSortOrderld
1
71
201
@c_itemld
@c_sortOrderld
@currentSortOrderld
3
72
301
@currentSortOrderld
Расширенный доступ к данным и методы корректировки @cjtemld
@c_sortOrderld
@currentSortOrderld
5
81
401
Теперь посмотрим значения в таблице:
!i
SELECT * FROM dbo.item ORDER BY sortOrderld
Если все выполнено правильно, строки выхода будут в том же порядке: itemld
sortOrderld 1 101 201 301 401
Но что, если мы изменим тип курсора на DYNAMIC, подобно курсору FAST_FORWARD? Если мы заменим часть объявления курсора FORWARD_ONLY STATIC на FAST_FORWARD, то получим результаты наподобие следующего: @c_itemld
@c_sortOrderld
4
29
@c_itemld
@c_sortOrderld
2
35
@c_itemld
@c_sortOrderld
1
71
@c_itemld
@c_sortOrderld
3
72
(cpcjtemld
@c_sortOrderld
5
81
@c_itemld
@c_sortOrderld
2
101
@c_itemld
@c_sortOrderld
1
201
@currentSortOrderld 1 @currentSortOrderld 101 @currentSortOrderld 201 @currentSortOrderld 301 @currentSortOrderld 401 @currentSortOrderld 501 @currentSortOrderld 601
467
Глава 12 @c_itemld 3
@c_sortOrderld
@c_itemld
@c_sortOrderld
5
401
@cjtemld
@c_sortOrderld
2
501
301
@currentSortOrder!d 701 @currentSortOrderld 801 @currentSortOrderld 901
. . . и т а к далее.
Причина этого в том, что значения в месте расположения курсора изменяются и завершаются переупорядочиванием в соответствии с порядком сортировки. Так как порядок сортировки изменяется более чем до следующей величины, вы никогда не достигнете конца последовательности.
Всегда определяйте тип курсора, который вы хотите использовать, иначе вы можете получать непредсказуемые результаты.
Обработка значений NULL В главе 10 мы обсуждали поля, допускающие значения NULL. В этом разделе мы должны пролить свет на программирование с использованием значений NULL и особенно, как сравнения с NULL обрабатываются в SQL Server, и что это означает в вашем коде. Со значениями NULL нужно довольно хитро работать, но их трудно избежать в ваших БД. Они могут оказаться довольно эффектными операторами. Можем ли мы задать каждому полю в БД условие NOT NULL? Да, но каждый раз, когда мы используем в операторе внешнее соединение, все невозвращаемые значения будут заполнены значениями NULL. Сейчас мы рассмотрим несколько другой подход к значениям NULL, которые используются в логическом выражении, и я обязан доктору Дэвиду Розенштейну за разъяснение мне этой концепции. Рассмотрим некоторые из наиболее интересных проблем, которые возникают, когда мы сталкиваемся с данными NULL, и которые обычно игнорируются. Что означает NULL? Согласно SQL Server 2000 Books Online "Значение NULL указывает неизвестную величину. Значение NULL отличается от пустого или нулевого значения. Никакие два значения NULL не равны. Сравнения между двумя значениями NULL или между NULL и любым другим значением возвращают неизвестный результат, потому что значение каждого NULL неизвестно". Итак, довольно ясно, что NULL означает неизвестное значение. И это понятие помогает нам в случае, когда поле может иметь значение NULL, и даже в случае объединения, когда соответствие одного и другого набора данных неизвестно. Однако при программировании с использованием логики с тремя значениями (истинный, ложный и NULL) мы должны быть немного хитрее, чем в этом случае. Проблему довольно просто продемонстрировать. Рассмотрим следующий оператор:
468
Расширенный доступ к данным и методы корректировки
§§§ SELECT CASE WHEN NOT (1 = NULL) THEN ' T r u e '
ELSE ' F a l s e ' END
Очевидно, что (1 = 1) дает True, a (1 = 0) дает False. А как относительно (1 = NULL)? Я полагаю, что F a l s e — логичный ответ. И очевидно, что NOT (False) = True. Однако выполнение этого запроса возвращает другое значение: False Прекрасно, но если NOT (1 = NULL) равно F a l s e , тогда NOT (NOT (1 = NULL)) должно быть True, не так ли? Итак, мы выполняем следующий оператор: Щ SELECT CASE WHEN NOT (NOT (1 = NULL)) THEN ' T r u e '
ELSE ' F a l s e ' END
И он снова возвращает False! Это фундаментальное свойство трехзначной логики — один из наименее понятых принципов в программировании SQL Server. Проблема связана с определением NULL как неизвестной величины (Unknown). Логически говоря, что является противоположностью неизвестной величине? Известная величина (Known). Итак, заменим (1 = NULL) на Unknown, потому что логически, если NULL — Unknown, то Unknown является и равенство Unknown и единицы. Поэтому NOT (I = NULL) должно быть NOT (Unknown). И это равняется Known, правильно? Возможно, в английском языке (и в русском — Прим. перев), но не в трехзначной логике. Возможно Что, если мы используем "Возможно" (Maybe) вместо неизвестной величины (Unknown)? Итак, (1=NULL) — Maybe. А противоположность Maybe? .Снова Maybe. Рассмотрим следующие таблицы истинности: AND
True
False
OR
True
False
True
True
False
True
True
True
False
False
False
False
True
False
Рассмотрев несколько примеров для этой таблицы, мы видим, что True AND True = True и True AND F a l s e = F a l s e . Далее мы рассмотрим стандартные таблицы истинности, включая NULL. Эти таблицы взяты из SQL Server 2000 Books Online, но значение Unknown заменено на Maybe: AND
True
False
Null
OR
True
False
Null
True
True
False
Maybe
True
True
True
True
False
False
False
Maybe
False
True
False
Maybe
Null
Maybe
Maybe
Maybe
Null
False
Maybe
Maybe
469
Глава 12 Итак, мы видим, что T r u e AND NULL = Maybe, T r u e OR NULL = T r u e . Другое интересное сравнение — (NULL = NULL) . Явно, это выглядит к а к T r u e , но к а к могут быть равны две неизвестных величины? Непонятно, и результат — Maybe. Далее приведены таблицы истинности NOT AND и NOT OR:
NOT AND
True
False
Null
NOT OR
True
False
Null
True
False
True
Maybe
True
False
False
False
False
True
True
Maybe
False
False
True
Maybe
Null
Maybe
Maybe
Maybe
Null
False
Maybe
Maybe
Важно проверить эти две последние таблицы, используя (1 = 1) — T r u e , (1 = 0) — F a l s e и (1 = NULL) — Maybe. Например: SELECT CASE WHEN (NOT ( ( 1 = 1 ) OR (1 - NULL)) AND ( 1 = 1 ) ) THEN 'True' ELSE 'False' END
I
Мы возьмем каждое логическое выражение и оценим его: (NOT
(True OR Maybe)
AND True)
= NOT (True AND True)
= NOT(True) • F a l s e
... получив следующее значение, выполняя оператор: False Основной ключ в понимании некоторых уродливых логических ошибок в программировании SQL Server связан с фактом, что SQL Server оценивает оператор, используя соответствующую трехзначную логику, но в заключительном анализе обращается со значением NULL к а к F a l s e . Однако так к а к большое число наиболее важных запросов, которые вы будете писать, включает много критериев поиска, важно понять, что значение NULL сохраняется к а к NULL до самого конца. Рассмотрим оператор WHERE для данной строки, пытаясь понять, почему он не возвращает значение, которое мы ожидаем: §§ WHERE NOT ( ( f i e l d l = @fieldl) OR (field2 = @field2)) В следующей строке мы заменим все значения полей и переменных в операторе данными, которые мы сравниваем: §§§ WHERE NOT ( ( I - 1) OR (NULL = NULL)) Итак, мы завершаем следующими равенствами: NOT (True OR Maybe) • NOT (True) = F a l s e . Это кажется довольно разумным. Однако давайте слегка изменим значения: Щ
470
WHERE NOT ( ( 2 = 1 )
OR (NULL = N U L L ) )
Расширенный доступ к данным и методы корректировки Теперь мы имеем NOT (False OR Maybe) = NOT (Maybe) = Maybe.
Обратите внимание, что когда SQL Server заканчивает сравнение со значением Maybe, в завершение он преобразует его в False.
Возможно, более безопасно работать таким пессимистическим способом, так как возможно, что эта величина действительно является ложной. В то время как ANSI-стандарт для сравнения (NULL = NULL) дает NULL, SQL Server 6.5 и более ранние версии считали, что (NULL = NULL) = True. В SQL Server 2000 есть настройка (SET ANSI_NULLS — установка ANSI-NULL), которая позволяет переключиться на (NULL = NULL) » True. Однако использование этой настройки не рекомендуется, если у вас нет проблем совместимости с ранее написанными программами. Она может причинить большее количество неприятностей, чем даст выгод, и эта настройка, вероятно, в дальнейшем не будет существовать. Проблемы со сравнением данных, которые включают значения NULL, являются довольно общими, и такие ситуации можно использовать, но с чрезвычайной осторожностью, если там есть значения NULL. Мы должны быть уверены, что поняли следующее: •
(NULL = 3) имеет значение Maybe;
•
(NULL > 3) также имеет значение Maybe.
И пример, где это будет вас шокировать: NOT (f i e l d l = 3), где f i e l d l — NULL, даст NOT (Maybe) = Maybe и SQL Server окончательно оценит это как F a l s e . /S NULL Единственное сравнение, которое работает в любом случае со значениями NULL — это оператор IS NULL (является NULL). Мы можем определить, какие значения являются значениями NULL, используя: Ц
WHERE f i e l d l
IS NULL
Этот оператор возвратит True, если поле — NULL, и False — в противном случае. Имеется также оператор IS NOT NULL, являющийся логической противоположностью оператора IS NULL. Используя эти два оператора, мы можем формулировать соответствующие сравнения с двумя значениями NULL. Например, если мы хотим определить, являются ли два значения, которые могут принимать значения NULL, "равными", означающее, что они имеют одно и то же значение или оба равны NULL, нам потребовались бы три части сравнения:
I
WHERE fieldl = @fieldl OR (fieldl IS NULL AND field2 IS NULL)
Это может казаться довольно очевидным, но из всех ошибок логического кодирования, которые я нашел или сделал, наиболее распространенные те, которые включают преобразования с полем, которое требует значений NULL. 471
Глава 12 Предположим, что мы имеем представление, из которого я строю преобразование, а запись-потомок одной из таблиц, дающей адрес человека, недоступна. Однако не все люди имеют адрес офиса, поэтому мы должны использовать внешнее соединение. CREATE VIEW v__person AS SELECT person.personld, person.firstName, person.lastName, address.streetName FROM dbo.person AS person LEFT OUTER JOIN dbo.personAddress AS personAddress JOIN dbo.address AS address ON address.addressld = personAddress.addressld AND personAddress.type = 1 -- адрес офиса ON person.personld • personAddress.personld
Здесь v_person — представление "человек"; person — таблица "человек"; personld — идентификатор человека; firstName — имя; lastName — фамилия; address — таблица "адрес"; streetName — название улицы; personAddress — адрес человека; addressld — идентификатор адреса; type — тип (адреса). Теперь создадим преобразованный запрос, чтобы вставить любые строки, где p e r s o n l d в таблице не существует. Это довольно легко сделать. Затем мы закодируем следующий оператор UPDATE, чтобы обновить значения для человека, если что-либо изменится: UPDATE person SET person.firstName = v_person.firstName, person.lastName • v_person.lastName, person.streetName = v_person.streetName FROM dbo.person AS person JOIN dbo.v_person AS v_person ON person.personld = v_person.personld WHERE person.lastName <> v person.lastName OR person.firstName <> v_person.firstName OR person.streetName <> v_person.streetName Это выглядит в значительной степени правильным, но все же имеется зияющая брешь. Даже считая, что все поля в наших основных таблицах не допускают значений NULL, в нашем представлении существует левое внешнее соединение, при котором streetName может быть NULL. Если запись personAddress не существует, могут возникнуть обстоятельства, когда v_person . streetName будет иметь значение NULL, но если person . streetName не NULL, эта величина все же оценивается как False. Подставив реальные данные в следующий оператор:
I
WHERE person.lastName <> v_person.lastName OR person.streetName <> v_jperson. streetName
... мы получим: I
WHERE 'Davidson' <> 'Davidson' OR 'Worthdale' <> NULL
Возвратившись к таблицам истинности, получим, что (False OR Maybe) = Maybe. И SQL Server оценивает это как False! Это очень легко сделать, так что вы должны написать запрос следующим образом: 472
Расширенный доступ к данным и методы корректировки WHERE person.lastName <> v_person.lastName OR person . firstName <> v j e r s o n . f irstName OR (person . streetNarne <> v_person . streetNarae OR (person.streetName IS NULL AND v_person.streetName IS NOT NULL) OR (person.streetName IS NOT NULL AND v_person.streetName IS NULL))
I
Этот код мучителен для написания, но это единственный путь, как мы можем эффективно сравнивать с NULL. NULL в других операциях Последнее замечание, которое следует сделать относительно значений NULL, касается использования их в других операциях. Когда мы имеем дело с величиной NULL не в логическом сравнении, а в других ситуациях, лучше считать, что это "Неизвестно". NULL не означает, и не должен означать "ничего". NULL означает что-то, с чем нужно также обращаться, хотя его идентичность неизвестна. Например: •
Числовые вычисления: NULL + 1 0 означает "Неизвестно", так как если мы не знаем, с чего начинаем, а затем добавляем 10, ничего не изменяется.
•
Объединение строк: ' * ' + NULL + ' * ' также означает "Неизвестно", так как нет никакого способа узнать, какая строка фактически получилась.
•
NULL + NULL: очевидно, что два "Неизвестно" не могут дать ничего другого, кроме "Неизвестно".
Следовательно, когда мы имеем дело с операцией, когда один или несколько параметров может быть NULL, вы должны использовать один из нескольких механизмов, которые обеспечивает SQL Server — ключевое слово CASE (вариант) и функции ISNULL (равно NULL) и COALESCE (объединение). Например, чтобы выполнить следующее объединение: Ц SELECT ' * ' + @field + ' * ' где @field может быть NULL, и мы хотим получить результат, не равный NULL, если @field имеет значение NULL, то можем взять следующие три части: SELECT CASE @field WHEN NULL THEN '' ELSE gfield END ИЛИ j§
SELECT ISNULL ( @ f i e l d ,
' ' )
ИЛИ
Ц SELECT COALESCE ( g f i e l d ,
")
Каждая из них имеет свои достоинства: •
CASE — использование ключевого слова CASE здесь симпатично, потому что оно само все объясняет. Это — очень мощное средство, которое можно использовать не только для простой проверки NULL. 473
Глава 12 •
ISNULL имеет два параметра. Она проверяет, является ли первый параметр значением NULL, и затем возвращает второй параметр (в приведенном примере — пустая строка), если это так. Если второй параметр —NULL, NULL и возвращается.
•
COALESCE позволяет нам задать несколько параметров, и функция выбирает первое значение, не равное NULL. Например, COALESCE (@field, @variable, @variable2 + ' *' , ' ' ) сначала будет проверять @ f i e l d , затем @variable и так далее. Это эквивалентно оператору CASE, который дает:
SELECT CASE WHEN @field IS NOT NULL THEN @field WHEN @variable IS NOT NULL THEN gvariable WHEN @variable2 +' '*' IS NOT NULL THEN @variable2 + '*' ELSE '' END
i
Хотелось бы надеяться, что этот раздел прояснил некоторые из проблем, которые нужно иметь в виду при программировании с возможностью появления значений NULL. Они могут быть достаточно неприятны при реализации, но нет никакого способа избежать их использования.
Представления Как мы обсуждали в главе 3, о представлениях можно думать как о виртуальных таблицах, и они обладают большинством характеристик обычной таблицы БД. С точки зрения наших SQL-операторов они — таблицы, хотя обычно и не поддерживают операторы корректировки (за исключением очень специфических случаев). На самом деле мы, как правило, и не хотим использовать корректировку представлений, так как они предназначены для упрощения рассмотрения нами данных. Обратите внимание, что представления не являются компилируемыми объектами подобно хранимым процедурам, а, скорее, реализованы как текст, который может быть вставлен в запрос, где это необходимо, точно так же как вы вставляете полученный запрос в оператор. Имеется несколько общих случаев, когда представления будут использоваться в нашей БД: •
Для маскирования деталей реализации — они могут использоваться, чтобы скрыть от пользователя большое количество соединений.
•
Как средство формирования отчетов в реальном масштабе времени — они могут использоваться, чтобы создать отчеты, которые связаны с нашими текущими данными и не затрагивают само функционирование, определяя набор конкретных запросов, которые часто нужны пользователям.
•
Как механизмы безопасности — давая доступ к представлению, а не к основным таблицам, мы можем реализовать безопасность на основе строк или столбцов.
Далее рассмотрим эти ситуации. Представления как устройства маскирования Представления — превосходные средства для маскирования данных. Мы можем взять наши высоко нормализованные таблицы, соединить их с другими таблицами в нашей БД и дать нашим пользователям данные, как им желательно, скрывая от них основную БД и сложности фактического механизма, с помощью которого мы обращались к данным. Представления могут также скрыть все нежелательные данные, позволяя пользователю сосредоточиться на том, что является действительно важным. 474
Расширенный доступ к данным и методы корректировки Предположим, что мы имеем следующие две таблицы в коммерческой БД. Таблица item (изделие) представляет продаваемые изделия, а таблица s a l e (продажа) определяет число проданных изделий: — изделия, которые мы продаем CREATE TABLE item ( itemld int NOT NULL IDENTITY, name varchar(60) NOT NULL, price money
— записи продаж CREATE TABLE s a l e ( s a l e l d i n t NOT NULL IDENTITY, itemld i n t NOT NULL, -- внешний ключ к таблице item date datetime NOT NULL, itemCount i n t NOT NULL -- число проданных изделий )
Здесь создаются две таблицы item (изделие) и sale (продажа). Первая таблица содержит три столбца: автоинкрементный itemld (идентификатор изделия) типа int; name (название изделия) типа varchar (60) и price (стоимость) типа money. Вторая таблица содержит четыре столбца: автоинкрементный saleld (идентификатор продажи) типа int; itemld (идентификатор изделия) типа int; date (дата) типа datetime и itemCount (число изделий) типа int. Мы могли бы захотеть создать представление, чтобы разместить информацию об изделиях и продажах в одной таблице для просмотра пользователем. CREATE VIEW v_sale AS
SELECT sale.saleld, sale.itemld, sale.date, sale.itemCount, item.name, item.price FROM dbo.sale AS sale JOIN dbo.item AS item ON sale.itemld = item.itemld Теперь, всякий раз, когда пользователю нужно посмотреть на продажу, представление будет оберегать его от необходимости вручную присоединять ее к таблице item. Это — довольно простой пример, но когда десять или большее количество таблиц включены в представление, сбережения времени при кодировании могут быть довольно большие. Плюс, если изменяются столбцы таблиц, например, когда мы делаем дальнейшую нормализацию и вместо хранения стоимости в таблице item храним ссылку на отдельную таблицу стоимостей, где размещается само значение, пользователь представления не должен даже и знать об этом. Представления в отчетах, выполняемых в реальном масштабе времени Формирование отчетов в реальном масштабе времени — термин, который указывает, что отчет не может иметь никакого времени ожидания, то есть его данные непрерывно обновляются. Если мы хотим иметь немедленный, сиюсекундный отчет, одним из способов реализовать это является использование представления. Представление дает нам пару выгод по сравнению с написанием запроса непосредственно для таблиц, как вы обычно пишете код для отчета: 475
Глава 12 •
•
Представления предотвращают нерегламентированные запросы отчета — нерегламентированные запросы к реальным таблицам могут привести к большим всплескам в загрузке сервера. Создавая представление или представления, мы можем попробовать передать по каналу любые запрашиваемые потребности в реальном масштабе времени, использующие набор запросов, которые были оптимизированы, проверены, и имеют меньший шанс воздействия на общее функционирование системы. Представления позволяют избежать блокировок — обычно мы можем создавать отчеты таким образом, что они будут игнорировать блокировки БД и, что более важно, также не оставлять блокировок в БД. Это достигается, используя указание оптимизатору readuncommitted (или nolock — без блокировок) — см. выше. Такое указание должно использоваться только тогда, когда оцененные данные приемлемы, так как некоторые из читаемых записей могут быть в транзакции, которая может откатиться. Этот путь, независимо от того, как пользователь пробует работать с представлением, не будет блокировать никаких ресурсов.
Возвращаясь к нашим таблицам, пусть пользователю нужно видеть сиюминутные почасовые цифры продаж. Мы выясняем у пользователя, что можем игнорировать те случаи, когда продажа зарегистрирована, но может быть позже удалена. Таким образом, мы могли бы создать следующее представление для пользователя, чтобы строить с ним запросы: CREATE VIEW v_saleHourlyReport AS SELECT item.itemld, dbo.date$removeTime(sale.date) AS saleDate, datePart (hour, sale.date) AS saleHour, sum(sale.itemCount) AS totalSold, sum(sale.itemCount * item.price) AS soldValue FROM dbo.item AS item(readuncommited) JOIN dbo.sale AS sale(readuncommited) ON sale.itemld • item.itemld -- группировка информации по itemld, затем по дням и часам дня -- чтобы получить то, что запрошено GROUP BY item.itemld, item.name, dbo.date$removeTime(sale.date), datePart (hour, sale.date)
Обратите внимание, что здесь мы используем системную функцию datePart для выделения часов из даты, и функцию из главы 11, чтобы отделить время от даты. Теперь этот запрос может быть выполнен неоднократно с меньшим воздействием на продолжающиеся процессы, так как мы удалили возможности блокировки чтения хранящихся данных, когда они используются (с операторами readuncommitted). Конечно, эти меры настолько хороши, насколько хороши средства, которые созданы для их использования. Даже без блокировки возможно использовать обширные системные ресурсы, если представление соединяется с другими представлениями неблагоприятным образом. Представления как механизмы безопасности Имеются два свойства представлений, которые мы можем использовать, чтобы строить более безопасную БД. Первое — назначение таких прав доступа пользователю, когда он может использовать представление, но не основные таблицы. Например, пусть у нас есть таблица person (человек): 476
Расширенный доступ к данным и методы корректировки CREATE TABLE person ( personld int IDENTITY, firstName varchar(20), lastName varchar(40)
Здесь создается таблица person (человек), содержащая три столбца: автоинкрементный personld (идентификатор человека) типа int; firstName (имя) типа varchar (20); lastName (фамилия) типа varchar (40). Мы сможем сформировать представление на основе этой таблицы:
I
CREATE VIEW v__person AS SELECT personld, firstName, lastName FROM dbo.person
Выбор данных из таблицы или представления возвращает одни и те же значения. Однако они представляют собой две разные структуры, так что мы можем отдельно назначать права доступа к ним. Второе свойство представлений, которое полезно как механизм безопасности — способность делить структуру таблицы, ограничивая строки или столбцы, видимые пользователю. Предположим, что мы имеем следующую структуру таблицы: Столбец 1
Столбец 2
Столбец N
Строка 1
Зарплата 1-го секретаря
Номер социального страхования 1-го секретаря
Адрес 1-го секретаря
Строка 2
Зарплата 1-го менеджера
Номер социального страхования 1-го менеджера
Адрес 1-го менеджера
Строка N
Зарплата главного администратора
Номер социального страхования главного администратора
Адрес главного администратора
Представления позволяют нам разрезать таблицу на секции, которые включают все столбцы, но не все строки на основе некоторых критериев. Это — безопасность на уровне строк, или горизонтальное разделение наших данных. В нашем примере мы могли бы позволить 1-му секретарю просматривать только строку 1, в то время как остальная часть строк будет казаться, что не существует:
Строка 1
Столбец 1
Столбец 2
Зарплата 1-го секретаря
Номер социального страхования 1-го секретаря
. .
Столбец N Адрес 1-го секретаря
477
Глава 12
Следовательно, давая доступ к представлению только в виде этой строки, любой пользователь, кто имеет право доступа к представлению, будет иметь право видеть только эту конкретную строку. Возвращаясь к нашему примеру кода, мы возьмем предыдущее созданное нами представление и используем его как основу более конкретного представления, которое будет интересно только менеджеру изделия с itemld, равным 1. Это позволит менеджеру контролировать уровень продаж данного изделия в течение дня: CREATE VIEW v_saleHourlyReport_socksOnly AS SELECT itemld, saleDate, saleHour, totalSold, soldValue FROM dbo.v_saleHourlyReport WHERE itemld = 1
Мы обычно реализуем более широкий план безопасности, чем жесткое кодирование itemld в представлении, но это дает простой пример безопасности на уровне строки. Такой план мы рассмотрим позже в этой главе. С другой стороны мы можем использовать безопасность на уровне столбцов. Неудивительно, что это также называется вертикальным разделением данных, поскольку мы будем разделять столбцы представления. В таблице item (изделие) имеется три столбца: itemld (идентификатор изделия), name (название) и p r i c e (цена). Было бы неправильно не разрешать каждому пользователю БД видеть все изделия с их названиями, но цена — это другое дело. Следовательно, мы могли бы сформировать представление следующим образом: CREATE VIEW v _ i t e m N o P r i c e AS SELECT itemld, name FROM dbo.item
Конечно, нет никакой причины, почему мы не могли бы одновременно делить наши таблицы горизонтально и вертикально, давая пользователю столько данных, сколько необходимо: Столбец 1 Строка 1
Как только мы построили необходимые представления, мы не разрешаем доступ к основной таблице, но дадим пользователям, кто должен видеть данные, вместо этого доступ к представлениям. Более полное обсуждение безопасности включено в последнюю часть этой главы. ЧТО НОВОГО В
представлениях?
Имеются две очень интересные новые особенности представлений в SQL Server 2000. Эти особенности: •
индексирование представлений;
•
разделение представлений.
Обе эти особенности будут детально исследованы в следующих разделах. 478
Расширенный доступ к данным и методы корректировки Индексированные представления Индексация представлений — очень интересная особенность, но доступна только в версии Enterprise (и версиях Developer и Evaluation) SQL Server 2000. Индексируя представление, мы теперь имеем возможность получать без триггеров итоговые данные, которые автоматически поддерживаются средой SQL Server таким образом, который делает обычно сложное действие столь же простым, как написание запроса. Вместо определения представления, используемого как обычное представление, оптимизатор может выбрать использование индекса, который содержит текущие значения предварительно созданного индекса. Итак, вместо создания триггеров, которые суммируют наши данные в других таблицах, мы могли бы создать представление. Например, возвращаясь к нашим таблицам item (изделие) и s a l e (продажа), мы могли бы создать следующее представление. Обратите внимание, что только схемно-связанные представления могут быть индексированы. Это дает уверенность, что таблицы и структуры, на основе которых создан индекс, не будут изменять полученное представление. CREATE VIEW v_itemSale WITH SCHEMABINDING AS SELECT item.itemld, item.name, sum(sale, itemCount) AS soldCount, sum(sale.itemCount * item.price) AS soldValue FROM dbo.item JOIN dbo.sale ON item.itemld - sale.itemld GROUP BY item.itemld, item.name
Вычисления будут производиться во время выполнения. Если оно недостаточно быстро, или если использует слишком много ресурсов, мы могли бы добавить индекс к представлению, наподобие следующего: I
CREATE INDEX XV__itemSale_materialize ON v itemSale (itemld, name, soldCount,
soldValue)
SQL Server тогда построил бы представление и разместил индекс. Теперь наши запросы к представлению будут очень быстрые. Однако хотя мы и избежали всех проблем кодирования, связанных с хранением итоговых данных, мы должны держать наши данные в актуальном состоянии. Всякий раз, когда в основных таблицах изменяются данные, индекс представления будет изменять свои данные, так что имеется увеличение продолжительности работы из-за поддержания индекса представления. Следовательно, индексирование представления означает, что ускоряется производительность для чтения, но не обязательно для обновления. Имеются также и некоторые другие предостережения. Ограничения того, что может использоваться в представлении, до этого проиндексированном, являются довольно тяжелыми. Наиболее важные вещи, которые вам не следует делать: • • • •
Использование синтаксиса SELECT * — столбцы должны быть заданы явно. Использование UNION в представлении. Использование любых подзапросов. Использование любых внешних соединений или рекурсивных соединений с той же самой таблицей. 479
Глава 12 • • • •
Использование ключевого слова ТОР. Использование DISTINCT. Включение функции sum(), если она ссылается более чем на один столбец. Применение почти любой агрегатной функции к выражениям, которые могут принимать значение NULL.
•
Ссылка на любые другие представления.
•
Ссылка на любые недетерминированные функции.
И это — не все! Имеется несколько страниц требований, которые должны быть выполнены, в SQL Server Books Online, раздел Creating an Indexed View Section (Создание секций индексированных представлений), но эти — наиболее существенные, которые должны быть рассмотрены перед использованием индексированных представлений. Хотя все это может показаться довольно строгим, имеются серьезные основания для каждого из этих правил. Для нас обслуживание индексированного представления аналогично написанию собственной функции обслуживания денормализованных данных. Чем более сложный запрос должен быть сформирован для денормализованных данных, тем больше сложности в поддержании его, как я считаю; но, вероятно, эти правила будут все более легкими в новых версиях SQL Server. Распределенные секционированные представления Распределенное секционированное представление в основном подобно любому другому представлению, в котором использованы данные из нескольких таблиц. При этом данные таблицы могут существовать на различных SQL Server. Этот процесс известен как интеграция (federation). Здесь мы будем иметь дело с группой серверов, которые управляются независимо, но которые разделяют бремя обработки в системе. Это может дать существенные усовершенствования работы, увеличивая параллельность ввода/вывода системы. Так как мы прежде всего интересуемся аспектами проектирования такого представления, то рассмотрим шаги создания структуры: • •
Возьмем таблицу и создадим несколько точных копий ее. Разделим данные по некоторому значению (месяц, тип и т. д.) для этих таблиц, и применим ограничение-проверку для осуществления разделения.
•
Наконец, определим представление, которое использует операцию UNION ALL (без удаления двойных значений и каких-либо критериев сортировки), чтобы объединить копии таблицы в единственный набор результатов.
Например, возьмем нашу таблицу s a l e (продажа). Пусть мы имеем только два изделия — ItemA с i t e m l d = 1, и ItemB с itemld = 2. Мы могли бы тогда определить следующие таблицы: CREATE TABLE s a l e _ i t e m A ( s a l e l d i n t NOT NULL, i t e m l d i n t NOT NULL CONSTRAINT c h k $ s a l e _ i t e m A $ i t e m I d $ O n e i t e m C o u n t i n t NOT NULL ) CREATE TABLE s a l e _ i t e m B
4 8 0
CHECK ( i t e m l d = 1 ) ,
Расширенный доступ к данным и методы корректировки
saleld int NOT NULL, itemld int NOT NULL CONSTRAINT chk$sale_itemB$itemId$Two CHECK (itemld • 2), itemCount int NOT NULL
Далее мы создадим представление, которое ссылается на эти две новые таблицы: •
CREATE VIEW v_sale AS SELECT saleld, itemld, itemCount FROM dbo.sale itemA UNION ALL
-- без сортировки и удаления дубликатов
SELECT saleld, itemld, itemCount FROM dbo.sale_itemB
SQL Server достаточно силен, чтобы использовать лишь требуемую таблицу, и при этом может автоматически назначить два процессора, если это доступно, чтобы работать с этим запросом. Имеются некоторые затраты функционирования, связанные с использованием средств вне сервера, чтобы обработать запрос, но в зависимости от того, насколько большие данные установлены, использование интегрированных разделенных представлений может создать громадные выгоды в функционировании. Очевидно, мы не будем использовать это, чтобы хранить простой список партнеров, но когда вы размещаете информацию о сеансе с полумиллионом активных пользователей Интернета одновременно, это может быть очень хорошей платой за распределение ваших данных по нескольким серверам. Дополнительную информации, см. в "Professional SQL Server 2000 Programming" Роба Виейры (Wrox Press, ISBN 1-861004-48-6).
Хранимые процедуры Хранимые процедуры — в основном простые программы, размещенные в БД. Основное различие между хранимыми процедурами и пакетами заключается в выполнении; хранимые процедуры предварительно компилируются. В этом разделе мы конкретно рассмотрим, как создать хранимую процедуру, точнее, некоторые особенности хранимых процедур, которые интересны при реализации СУБД. Хранимые процедуры очень похожи на определяемые пользователем функции с несколькими важными различиями: •
Хранимые процедуры возвращают наборы результата в очень свободной манере, когда мы не должны объявлять то, что будем возвращать клиенту, и можем возвращать любое число результатов.
•
Хранимые процедуры могут иметь несколько выходных параметров, в то время как скалярные функции могут иметь только одно возвращаемое значение любого типа или могут не иметь выходного параметра.
481
Глава 12 • Q
Скалярные функции могут использоваться в скалярных выражениях, в то время как хранимые процедуры — не могут. Хранимые процедуры позволяют нам изменять БД (в форме обновления, изменения уставок и т. д.), в то время как функции не могут изменять ничего во время выполнения.
Каждое из этих различий отпределяет, когда использовать хранимые процедуры вместо функций, особенно, последние два. В остальной части этого раздела мы рассмотрим: • •
возвращаемые хранимыми процедурами значения; обработка ошибок;
• •
использование хранимых процедур для организации связи между нашими приложениями; использование хранимых процедур в качестве механизмов безопасности;
•
использование хранимых процедур для решения общих проблем.
Независимо от общей архитектуры, хранимые процедуры — особенности БД, которые каждый SQL-программист должен использовать и широко использует. Написание эффективных хранимых процедур требует много навыков и опыта, так что неплохо экспериментировать на фиктивных данных и испытывать различные методы создания.
Возвращаемые хранимыми процедурами значения Имеются три пути, которыми хранимые процедуры могут связываться с внешним миром: •
возвращаемые значения — обычно возвращают значение состояния;
• •
выходные параметры — обычно возвращают отдельные значения параметров; наборы результатов — обычно возвращают таблицы информации.
Каждый из них имеет вполне определенную цель, которую мы рассмотрим в следующих разделах. Возвращаемые значения В отличие от определяемых пользователем функций оператор RETURN хранимой процедуры может возвращать только значение целого числа, которое обычно используется, чтобы указать состояние. Следующая таблица дает общие указания для протокола возвращаемого значения с объяснением каждой величины:
482
Значение
Описание
>О
Процедура успешно выполнена с некоторым значением, которое может быть выведено, используя определение процедуры.
О
Процедура успешно выполнена, но никакая дополнительная информация не рассматривается.
-1 + -99
Процедура потерпела неудачу с системной ошибкой. Не может быть выведено значение, отличное от значения возникшей системной ошибки. В настоящее время используются только значения от - 1 до -14.
Расширенный доступ к данным и методы корректировки
Значение
Описание
-100
Основная определяемая пользователем ошибка, которая не может быть отработана в Transact-SQL.
< -100
Определяемое пользователем сообщение об ошибке, основанное на том, что произошло в хранимой процедуре.
Вы не должны использовать значения от —1 до - 1 4 для возвращаемой информации хранимой процедуры. Эти значения используются в SQL Server.
Причина для такого протокола довольно проста. Если возникает любая ошибка в хранимой процедуре, и вам нужен сигнал ее возникновения, то простая проверка, что возвращаемое значение отрицательно, означает наличие проблемы, а неотрицательное возвращаемое значение означает успешную работу. Например, возьмем следующую простую процедуру, которая возвращает целое значение, переданное ей: CREATE PROCEDURE returnValue$test @returnThis int = 0 AS
RETURN @returnThis Если мы выполним эту процедуру, задав значение переменной:
I
DECLARE @returnValue int EXEC @returnValue = returnValue$test @returnThis = 0 SELECT @returnValue AS returnValue
... м ы получим следующий результат: returnValue 0 Следующая хранимая процедура создана, чтобы использовать значение первой процедуры: CREATE PROCEDURE caller$testReturnValue ( @testValue int ) AS DECLARE @returnValue int EXEC @returnValue = returnValue$test @testValue SELECT OreturnValue AS returnValue
Продолжение кода на следующей странице
483
Глава 12 IF @returnValue < 0 -- отрицательное значение - ошибка BEGIN SELECT 'Произошла ошибка: ' + CAST(@returnValue AS varchar(lO)) AS status END ELSE BEGIN
- - значение равно нулю или успешная
SELECT 'Вызов успешен: ' + CAST(SreturnValue AS s t a t u s
AS
работа
varchar(lO))
END GO
Выполнение ее с отрицательной величиной: Щ EXEC caller$testReturnValue -10 ... сообщает нам, что произошла ошибка: returnValue -10 status Произошла ошибка: -10 Очевидно, если мы изменим параметр на неотрицательное число, то получим сообщение, что никакая ошибка не произошла. Выходные параметры Выходные параметры позволяют нам вернуть скалярное значение любого типа при вызове процедуры, и позволяют передать, если необходимо, данные типа c u r s o r . Выходной параметр — лучший способ возвратить скалярную величину из одной процедуры в другую, так как считается плохим тоном использовать возвращаемое значение для этой цели, даже если значение, которое мы возвращаем, окажется целым числом. Мы можем иметь столько выходных параметров, сколько нужно. Используйте следующий синтаксис: I
DECLARE @variable int EXEC storedProcedure @variable = @variable OUTPUT
Обратите внимание, что вы можете увеличить производительность в приложениях SQL Server, используя выходные параметры, чтобы возвратить единственную строку, что устраняет накладные расходы для получения набора результатов. Наборы результатов Возвращение наборов результатов клиенту столь же просто, как включение оператора SELECT в ваш код:
I
4 8 4
CREATE PROCEDURE AS
test$statement
SELECT 'Hi' AS hiThere SELECT 'Bye' AS byeThere GO
Расширенный доступ к данным и методы корректировки Выполнение этой хранимой процедуры: Ц§ EXEC test$statement ... возвратит два набора результатов hiThere Hi byeThere Bye Это кажется очень простым, но имейте в виду, что так как хранимые процедуры — заранее компилируемые программные коды, они должны быть автономными. Они не должны содержать код, который требует взаимодействия с клиентом — или человеком, или другой программой. Хранимые процедуры — сами по себе не являются транзакциями, если соединение разрывается на полпути выполнения хранимой процедуры, остальная часть процедуры не будет ни закончена, ни будет выполнен откат. Вот почему выходные параметры и возвращаемые значения недоступны вызывающему, пока все наборы результатов не были получены или отменены, и хранимая процедура закончена.
Обработка ошибок Обработка ошибок в коде SQL Server — один из его самых слабых пунктов. В SQL Server нет никакой организации ловушек ошибок. Однако мы всегда можем определить, вызвал ли оператор ошибку, выполнив соответствующие действия. Рассмотрим, например, следующий вызов несуществующей хранимой процедуры: Щ EXEC dbo.doesntExist Он возвратит следующее стандартное сообщение об ошибке: Server: Msg 2812, Level 16, State 62, Line 1 Could not find stored procedure 'dbo.doesntExist'. {Сервер: сообщение 2812, уровень 16, состояние 62, строка 1 Не удается найти хранимую процедуру 'dbo.doesntExist'.) Если мы посмотрим в таблице m a s t e r . dbo . sysmessages значение для ошибки с номером 2812:
I
SELECT error, description FROM master.dbo.sysmessages WHERE error = 2812
... мы увидим сообщение с полным статическим текстом, необходимым, чтобы построить сообщение об ошибке, и признак для размещения значений параметра: error
description
2812
Could not find stored procedure '%.*ls'. 485
Глава 12 Мы можем также сформировать ошибку в коде, используя оператор RAISERROR (формирование ошибки), который мы раньше использовали, чтобы послать клиенту сообщение об ошибке. RAISERROR — очень мощное и ясное средство, и мы можем делать с ним намного больше. Мы можем использовать команду RAISERROR, чтобы создать наши собственные сообщения и сохранить их в таблице sysmessages. Важно упомянуть о гибкости RAISERROR. Она имеет встроенную языковую поддержку, так что если вы должны интернационализировать ваш код БД, то можете создавать версии ваших сообщений на других языках. Соответствующее сообщение будет отображено в соответствии с настройками языка пользователя. Используя RAISERROR, мы можем: •
возвратить нерегламентированное сообщение;
•
возвратить сообщение, что мы храним в таблице master .dbo . sysmessages со специальным форматированием;
•
возвратить номер ошибки (между 50000 и максимальной величиной типа b i g i n t ) ;
•
возвратить серьезность ошибки, которая сообщает нам, как плохо сообщение, от "informational" (информационное) до "hardware corruption" (повреждение аппаратных средств);
•
возвратить значение состояния, которое указывает источник, где возникла ошибка.
Самый простой синтаксис для команды RAISERROR: Ц
RAISERROR <номер__ошибки> <сообщение>
Это позволяет нам определить номер ошибки и некоторый текст, описывающий ошибку. Значение сообщения может быть значеним THnavarchar или содержанием переменной типа v a r c h a r . Например: §Ц RAISERROR 911911911 'Это сообщение-тест' возвратит: Server: Msg 911911911, Level 16, State 1, Line 1 Это сообщение-тест Так, в наших хранимых процедурах мы можем создать оператор, который проверяет, является ли значение выходного параметра меньше нуля, и если это так, формирует ошибку: CREATE PROCEDURE t e s t $ v a l i d a t e P a r m ( QparmValue i n t ) AS I F @parmValue < 0 BEGIN DECLARE @msg v a r c h a r ( 1 0 0 ) SET @msg = ' Н е п р а в и л ь н о е з н а ч е н и е @parmValue: CAST(SparmValue AS v a r c h a r ( 1 0 ) ) + 4 8 6
' +
Расширенный доступ к данным и методы корректировки
' . Значение должно быть RAISERROR 50 000 @msg RETURN -100 END SELECT ' Т е с т успешно выполнен.' RETURN 0 GO
неотрицательное.'
Тогда, если мы передадим 0 для проверки: Щ dbo.test$validateParm 0 то получим: Тест успешно выполнен. Если мы передадим отрицательное число, то получим ошибку: Server: Msg 50000, Level 16, State 1, Procedure test$validateParm, Line 11 Неправильное значение @parmValue: - 1 . Значение должно быть неотрицательное. Другой случай, когда полезно включить сообщение об ошибке — после операторов INSERT, UPDATE или DELETE. Причина — та же самая. Если возникает ошибка, мы можем точно определить, какой оператор вызвал отказ. Пусть мы имеем следующую таблицу: CREATE TABLE test testid int IDENTITY, name varchar(60) NOT NULL CONSTRAINT AKtest name UNIQUE
Здесь создается таблица test (тестирование), которая содержит два столбца: автоинкрементный testid (идентификатор теста) типа int и name (название) varchar (60).
типа
Сформируем следующую хранимую процедуру, чтобы создавать новые значения: CREATE PROCEDURE test$ins @name varchar(60) AS INSERT INTO dbo.test(name) VALUES (@name) IF @@error <> 0 BEGIN — формирует ошибку, которая сообщает пользователю, что произошло. RAISERROR 50000 'Ошибка размещения данных в таблице test' END GO
4 8 7
Глава 12 а затем выполним оператор: §j EXEC dbo.test$ins @name = NULL Результат будет следующий: Server: Msg 515, Level 16, State 2, Procedure test$ins, Line 6 Cannot insert the value NULL into column 'name', table 'master.dbo.test', column does not allow NULLs. INSERT fails. Server: Msg 50000, Level 16, State 1, Procedure test$ins, Line 10 Error inserting into test table The statement has been terminated. (Сервер: сообщение 515, уровень 16, состояние 2, процедура test$ins, строка 6 Нельзя поместить значение NULL в столбец 'пате , таблица 'master.dbo.test', столбец не допускает значения NULL. Оператор INSERT потерпел неудачу. Сервер: сообщение 50000, уровень 16, состояние 1, процедура test$ins, строка 10 Ошибка размещения данных в таблице test Оператор был удален.) Может показаться, что это делает код более грязным, поскольку мы везде имеем обработчики ошибок, но важно проверить код возврата и статус ошибки после каждого оператора корректировки или вызова хранимой процедуры. Если мы это делаем, то можем увидеть, какая ошибка произошла, и точно знаем где. Это очень полезно, так как могут быть случаи, когда вместо размещения одной строки в одну таблицу, мы могли бы разместить несколько строк в таблицу из приложения нашего клиента, или могли бы вызывать эту хранимую процедуру из другого сервера. Такой вид обработки ошибки будет экономить время отладки. Имеются две операции, которые мы можем выполнить, чтобы улучшить формирование сообщений об ошибках в SQL Server: Q
Разместить сообщения об ошибках БД, чтобы отобразить сообщения об ограничениях в более значащих конкретных сообщениях.
О
Создать формат тега сообщений об ошибках, чтобы возвращать более полные сообщения клиенту.
Схема
ограничений
В этом разделе мы рассмотрим технику размещения ошибок, формируемых нашими ограничениями для получения более ясных сообщений, что может быть в значительной степени автоматизированным процессом. Стандартные сообщения SQL Server об ограничениях не предназначены для использования конечным пользователем. Рассмотрим случай, где мы пробовали ввести значение NULL в столбец — в данном случае в столбец name (название) таблицы t e s t (тестирование). Server: Msg 515, Level 16, State 2, Procedure test$ins, Line 6 Cannot insert the value NULL into column 'name', table "master.dbo.test', column does not allow NULLs. INSERT fails. {Сервер: сообщение 515, уровень 16, состояние 2, процедура test$ins, строка 6 Нельзя поместить значение NULL в столбец 'пате', таблица 'master.dbo.test', столбец не допускает значения NULL. Оператор INSERT потерпел неудачу) 488
Расширенный доступ к данным и методы корректировки Анализируя основную информацию из этого сообщения (номер ошибки, который сообщает нам, что это нарушение использования NULL, а также столбец и таблицу из сообщения): Server: Msg 515, Level 16, State 2, Procedure test$ins, Line 6 Cannot insert the value NULL into column 'name', table 'master.dbo.test', column does not allow NULLs. INSERT fails. ... мы можем сформировать следующее сообщение об ошибке, которое намного лучше для пользователя: Вы должны ввести значение в столбец name таблицы test. Нарушения ограничений использования NULL довольно просто идентифицировать и формировать сообщения, но как относительно других ограничений? Рассмотрим ограничения-проверки. Предположим, что мы добавляем ограничение-проверку, чтобы запретить пользователю Bob доступ к таблице t e s t :
I
ALTER TABLE dbo.test ADD CONSTRAINT chkTest$name$string$notEqualToBob CHECK (name <> 'Bob')
Если теперь пользователь попытается ввести имя Bob: I
INSERT INTO dbo.test(name) VALUES ('Bob')
результатом этого будет стандартное сообщение об ошибке SQL Server: Server: Msg 547, Level 16, State 1, Line 1 INSERT statement conflicted with COLUMN CHECK constraint 'chkTest$name$string$notEqualToBob'. The conflict occured in database 'master', table 'test1, column 'name'. The statement has been terminated. (Сервер: сообщение 547, уровень 16, состояние 1, строка 1 Оператор INSERT конфликтует с ограничением COLUMN CHECK 'chkTest$name$string$notEqualToBob'. Конфликт возник в БД 'master, таблице 'test', столбце 'пате'. Оператор удален.) Мы предпочли бы возвратить специальное сообщение, что пользователь по имени Bob попытался обратиться к таблице. Стандартное сообщение об ошибке отрывочно, потому что мы только вернем определение ошибки, но мы можем использовать это как основание для специального сообщения. Чтобы сделать это, мы используем новые расширенные свойства, которые обсуждали в главе 10. В этом случае мы создадим свойство для ограничения, которое объявляет сообщение, что мы хотим запретить пользователям просмотр: EXEC sp_addexendedproperty @name = 'ErrorMessage', @value = 'Вы не можете обращаться к таблице t e s t ' , @levelOtype = ' U s e r ' , @levelOname = ' d b o ' , @levelltype = ' t a b l e ' , dlevellname = ' t e s t ' , @level2type = ' c o n s t r a i n t ' , @level2name = 'chkTest$name$string$notEqualToBob' 489
Глава 12 Как только расширенное свойство помещено в БД, SQL Server выводит нормальное сообщение, когда возникает ошибка. Механизм клиента перехватывает это, выделяет название ограничения и запрашивает расширенное свойство, чтобы получить специальное сообщение, которое затем посылается клиенту.
I
SELECT value AS message FROM ::FN_LISTEXTENDEDPROPERTY('ErrorMessage', 'User', 'dbo', 'table', 'test', 'constraint', 'chkTest$name$string$notEqualToBob'
Итак, пользователь увидит следующее новое сообщение: message Вы не можете обращаться к таблице test Эта модель может быть расширена, чтобы сделать ее намного более гибкой, автоматически устанавливая сообщения для каждого из возможных ограничений. Тогда мы можем отобразить каждое сообщение реальным сообщением для клиента. Хотя, одна вещь... Я обычно предпочитаю иметь метод на клиенте, чтобы видеть стандартное SQL Server сообщение об ошибке, так что я могу видеть то, что действительно произошло (например, дополнительная информационная кнопка в диалоговом окне сообщения). Этот метод стоит дополнительного путешествия к серверу и обратно. Клиентская часть объекта могла бы быть закодирована так, что когда мы разрабатываем наши приложения, то идем к серверу, чтобы обрабатывать сообщения об ошибках, но когда это используется в окончательной версии нашего программного обеспечения, то помещаем сообщения об ошибках в DLL для более быстрого чтения. Теги сообщений об ошибках Если мы имеем размещенные сообщения, которые SQL Server посылает нам, важно удостовериться, что сообщения, которые мы посылаем назад клиенту, максимально полезны. При создании нерегламентированных сообщений мы очень ограничены тем, что можем возвращать в сообщении. Я добавляю кодируемый тег в конец сообщения:
I
'текст_сообщения' + '<имя_тега_1=значение_тега_1;имя_тега_2=значение_тега_2; '...;имя тега Ы=значение тега N>'
Здесь каждый тег указывает значение, которое может быть важно для клиента программного обеспечения, такое, как: Объект, который формирует ошибку (Р = procedure — процедура, TR = trigger — триггер и т. д.). Тип объекта, который формирует ошибку. Предварительный вызов действия, которое вызывает ошибку. Значения столбца, которые вызывают ошибку. Значения ключа строки, имеющей проблемы. Это могло бы использоваться, если весь пакет вызовов хранимой процедуры был послан к SQL Server, и мы должны дифференцировать их.
490
Расширенный доступ к данным и методы корректировки
action
Специальное действие, которое вызывающий должен выполнить, чтобы восстановить проблему.
severity
Серьезность сообщения, подобно "warning" (предупреждение), "severe" (серьезное) и т. д.
occurrence
Число раз, какое может произойти выполнение одного и того же типа.
В наших примерах мы для простоты будем использовать теги obj, type и
action.
Например, в блоке каждой хранимой процедуры, где мы объявляем наши переменные (такие, как переменные, чтобы поместить число строк, сообщения и т. д.), мы имеем следующее:
I
-- используется, чтобы разместить первую часть заголовка сообщения DECLARE @msgTag varchar(255) -- открывающий тег сообщения с именем объекта и его типа SET @msgTag = '
Тогда всякий раз, когда нам нужно сформировать ошибку, м ы формируем вызов следующим образом:
I
SET @msg = 'Ad hoc message' + @msgTag + ';action=(insert test)>' RAISERROR 50000 @msg
и он возвратит сообщение, наподобие следующего: Server: Msg 50000, Level 16, State 1, Line 5 Ad hoc message (Сервер: сообщение 50000, уровень 16, состояние 1, строка 5 Нерегламентированное сообщение <объект = test$ins; тип = процедура; действие — (размещение теста)>) Это сообщение позволяет проще точно выяснить, что произошло и где это произошло в хранимой процедуре t e s t $ i n s . Выделение текста между скобками < и >, а также анализ каждого значения тега, чтобы сформировать более удобочитаемое сообщение пользователю, является также простым. Теперь мы можем сообщить клиенту, является ли наше сообщение только предупреждением, или это что-то более серьезное. Этот метод имеет то преимущество, что не требует никакого дополнительного кода. Рассмотрим следующую хранимую процедуру. Она моделирует ошибку, задавая переменной @error (которую мы обычно используем, чтобы разместить значение @@error) значение, отличное от нуля: CREATE PROCEDURE test$errorMessage ( @key_testld int ) AS — объявление переменных, используемых для обработки ошибок в блоках DECLARE @error int, -- используется для размещения значения ошибки
Продолжение кода на следующей странице
491
Глава 12 @msg varchar (255), @msgTag varchar (255)
-- используется для формирования -- сообщения об ошибке -- используется для размещения тегов — сообщения об ошибке
-- задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld) + ';type=P' + ';keyvalue=' + '@key_routineld:' + CONVERT(varchar(10), @key_testld) -- вызов какой-либо хранимой процедуры или оператора обновления для -- какой-либо таблицы -- получение номера строки и уровня ошибки для кода обработки ошибки SELECT @error = 100 -- моделируемая ошибка IF @error != 0 — ошибка возникла вне этой процедуры BEGIN SELECT @msg = 'Проблема возникла при вызове nonExistentProcedure. ' + @msgTag + ';call=(процедура nonExistentProcedure)>' RAISERROR 50000 @msg RETURN -100 END
Эта процедура довольно длинна, однако, все, что она делает, это формирует ошибку! К сожалению, почти весь этот код (кроме кода формирования тегов ошибки) обычно требуется, когда мы вызываем другие хранимые процедуры или делаем изменения в других таблицах. Мы должны проверить возвращаемое значение и значение @ @ e r r or на каждом шагу выполнения кода. Выполнение этой процедуры: Ц test$errorMessage @key_testld - 10 возвратит следующее сообщение Server: Msg 50000, Level 16, State 1, Procedure test$errorMessage, Line 23 A problem occured calling the nonExistentProcedure. (Сервер: сообщение 50000, уровень 16, состояние 1, процедура test$errorMessage, строка 23 Проблема возникла при вызове nonExistentProcedure. ) Оно передает обслуживающему персоналу несколько данных. Во-первых, здесь, вероятно, была ошибка, возникшая перед этим сообщением, которая сообщила нам, какова реальная проблема. Во-вторых, nonExistentProcedure была источником проблемы, — это было получено в процедуре test$errorMessage, и значение параметра @key_routineld было равно 10.
Инкапсуляция Инкапсуляция SQL-кода для пользователя (сокрытие от пользователя) — одна из наиболее важных причин использования хранимых процедур, позволяющих нам выполнить много команд с помощью единственного вызова хранимой процедуры. Это интересно по двум причинам:
492
Расширенный доступ к данным и методы корректировки •
Хотя результаты действия могут изменяться, само действие не изменится. Другими словами, если код, который мы написали, работает, фактическая реализация не имеет значения. Таким образом, мы можем настраивать внутреннюю работу хранимой процедуры без необходимости изменять интерфейс, который видят внешние пользователи. Мы могли бы даже изменить все имена основных таблиц и структуры, а хранимая процедура будет все так же работать.
•
Интерфейс может быть построен, полностью инкапсулируя SQL-вызовы хранимых процедур. В этом случае программы клиента никогда не будут включать никаких хитрых запросов в SQL к серверу БД, они будут просто иметь возможность использовать хранимые процедуры. Это объясняет, какие действия могут быть выполнены, так как мы можем видеть список возможных процедур.
Я попробую продемонстрировать первый момент на быстром примере. Предположим, что вы просите написать процедуру, которая получает имена клиентов из таблицы person (человек), и вам дали следующие спецификации: •
имя — person$get;
•
параметры — @personld (int);
•
наборы результатов — единственный результирующий набор, содержащий строку из таблицы person, включающую p e r s o n l d , f irstName и lastName со значением p e r s o n l d , отфильтрованным параметром @personld.
Вы могли бы написать следующий код: CREATE PROCEDURE person$get , @personld int AS SELECT personld, firstName, lastName FROM person WHERE personld = @personld
Если, однако, начать работать только в мире SQL-программирования, мы могли бы написать следующее: CREATE PROCEDURE person$get @personld i n t AS CREATE TABLE fperson personld i n t firstName varchar(30) lastName varchar(30) ) DECLARE @cursor cursor, @c_personld i n t , @c_firstName v a r c h a r ( 3 0 ) , @c_lastName varchar(30)
Продолжение кода на следующей странице 493
Глава 12 SET @cursor = CURSOR FAST_FORWARD FOR SELECT personld, firstName, lastName FROM person OPEN @cursor FETCH NEXT FROM @cursor INTO @c_personld, @c_firstName, @c_lastName WHILE @@fetch_status = 0 BEGIN IF @personld = 1 BEGIN INSERT INTO #person VALUES (@c_personld, @c_firstName, @c_lastName) END FETCH NEXT FROM @cursor INTO @c__personld, @c_firstName, @c_lastName END §§ SELECT * FROM #person Хотя этот код написан в плохом стиле SQL, онработает. Процедура возвратит надлежащие значения, и если количество данных невелико, ее выполнение даже не будет слишком плохим. Однако при инкапсуляции запроса легко переписать этот код в соответствующем виде, показанном ранее. Следует добавить, что программное обеспечение клиента, использующее эту хранимую процедуру, не изменяется. Последний момент, который я хочу отметить, — это то, что, используя хранимые процедуры, мы даже не нуждаемся в фактической структуре таблицы, чтобы начать создавать приложения. Используя некоторую форму генерации кода, мы должны быть способны создать набор методов (хранимых процедур) для нашей БД, которые ничего не делают, но предлагаются программисту, так как их достаточно для начала. Обычно называемые фиктивными процедурами, они часто используются, чтобы проверить интерфейс пользователя, прежде чем все полностью реализовано. В предыдущем примере мы могли построить следующую фиктивную процедуру, чтобы начать работу программиста: CREATE PROCEDURE person$get ( Spersonld
int
) AS SELECT 1 AS p e r s o n l d ,
'Joe'
AS firstName,
'Jones'
AS lastName
Хотя эта процедура и не делает ничего и даже игнорирует параметр, она возвратит строку, которую прикладной программист может использовать, чтобы видеть, работает ли экран, когда написан соответствующий код. Что важно, так это то, что внешний интерфейс не нарушается.
Безопасность Как и в представлениях, безопасность в хранимых процедурах осуществляется на уровне объектов. Лишь потому, что пользователь может использовать хранимую процедуру, не обязательно, что он/она должен иметь права доступа к каждому объекту, к которому обращается хранимая процедура. Мы можем использовать хранимые процедуры как наш первичный механизм безопасности, требуя, чтобы весь доступ к серверу выполнялся через них. Строя процедуры, которые инкапсулируют все функциональные возможности, мы можем использовать разрешения доступа к хранимым процедурам, чтобы ограничить то, что пользователь может делать. 494
Расширенный доступ к данным и методы корректировки Это позволяет нам иметь ситуационный контроль доступа к таблице. Другими словами, мы могли бы иметь две различных процедуры, которые выполняют точно те же самые действия, но предоставление прав пользователю на одну процедуру не подразумевает, что он/она имеет права на другую. Так, если экран построен, используя одну процедуру, пользователь может быть способен использовать это, но если только экран не использует другую процедуру. Наличие двух процедур, которые выполняют точно те же самые действия, далеко от совершенства, но это иллюстрирует факт, что мы можем строить объекты, которые имеют доступ к одним и тем же таблицам различными способами, и мы можем обеспечить доступ независимо от основных объектов. Можно иметь итоговую процедуру, которая дает список людей, чьи имена начинаются с буквы А, и которую пользователь может использовать, а также процедуру, которая формирует список имен пользователей, которая ему/ей недоступна. Подобная ситуация может быть реализована не только с представлениями, но, конечно, и с процедурами, когда можно не только просматривать данные, но и также создавать, изменять и удалять их. Более подробно мы рассмотрим использование безопасности в SQL для блокировки доступа к нашим объектам позже в данной главе.
Счетчик транзакций Кроме очевидных выгод от инкапсуляции SQL-кода для пользователя и установления модели безопасности, хранимые процедуры могут использоваться для подсчета текущих транзакций — очень важный фактор при использовании транзакций. Счетчик транзакций (обозначается через @@trancount) сообщает нам, сколько активных транзакций в настоящее время открыто. Когда мы используем транзакции в наших пакетах кода, то должны быть очень осторожны, чтобы открывать и закрывать их перед завершением пакета и не оставлять никаких блокировок, которые могут влиять на других пользователей. Независимо от того, каким был счетчик транзакций, когда вы начали процедуру, он должен иметь то же самое значение, когда вы завершаете процедуру. Эта концепция имеет важный смысл и делает кодирование содержательным. Возьмем следующие две процедуры: CREATE PROCEDURE t r a n T e s t $ i n n e r AS
BEGIN TRANSACTION -- код определяет ошибку, приводящую к откату ROLLBACK TRANSACTION RETURN -100
CREATE PROCEDURE tranTest$outer AS DECLARE @retValue int BEGIN TRANSACTION EXEC @retValue = tranTest$inner IF @retValue < 0 BEGIN ROLLBACK TRANSACTION RETURN OretValue END COMMIT TRANSACTION
17 1868
495
Глава 12 Чтобы посмотреть, что произойдет, мы рассмотрим следующий запрос и будем медленно выполнять его. Это может быть болезненный процесс, но во многих случаях он является единственным способом полностью понять код: Ш EXEC tranTest$outer 1
tranTest$outer
DECLARE SretValue int
Задает переменную для размещения возвращаемого значения из процедуры
@retValue = NULL
2
tranTest$outer
BEGIN TRANSACTION
Начинает новую транзакцию
@@trancount = 1
з
tranTest$outer
EXEC @retValue = tranTest$inner
Вызывает процедуру tranTest$inner
4
tranTest$inner
BEGIN TRANSACTION
Начинает вторую транзакцию
@@trancount = 2
5
tranTest$inner
ROLLBACK TRANSACTION
Откатывает все транзакции
@@trancount = 0
6
tranTest$inner
RETURN -100
Возвращает вызывающей процедуре отрицательное значение, чтобы сообщить, что что-то произошло
ОШИБКА:
7
tranTest$outer
После возврата значение QretValue заполнено
QretValue = -100
8
tranTest$outer
IF @retValue < 0
Проверка, возникла ли ошибка
g
tranTest$outer
ROLLBACK TRANSACTION
Откатывает все транзакции
счетчик транзакций изменился
ОШИБКА: @@trancount уже равен нулю
Так как счетчик транзакций изменен между шагом 3, когда мы начали процедуру, и шагом 6, когда закончили ее, то получим следующее сообщение об ошибке: Server: Msg 266, Level 16, State 2, Procedure tranTest$inner, Line 6 Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 1, current count = 0. (Сервер: сообщение 266, уровень 16, состояние 2, процедура tranTest$inner, строка 6 Счетчик транзакций после EXECUTE указывает, что пропущен оператор COMMIT или ROLLBACK TRANSACTION. Предыдущий счетчик = 1, текущий счетчик = 0) 496
Расширенный доступ к данным и методы корректировки Неблагоразумно добавлять код в хранимую процедуру, чтобы проверить, был ли счетчик транзакций изменен другим процессом, так что мы должны заново продумать проблему. Вместо использования прямых транзакций, мы начнем новую транзакцию, когда начнем хранимую процедуру, а затем установим точку сохранения транзакции. Обратите внимание, что мы устанавливаем переменную для точки сохранения с именем процедуры плюс уровень вложенности, так что оно будет уникально независимо от того, как глубоко мы вложили вызов нашей процедуры или триггера: DECLARE @ t r a n P o i n t sysname — тип данных д л я идентификаторов SET @ t r a n P o i n t .= o b j e c t _ n a m e ( @ @ p r o c l d ) + CAST(@@nestlevel AS v a r c h a r ( i O ) ) BEGIN TRANSACTION SAVE TRANSACTION @ t r a n P o i n t
Итак, мы перекодируем наши процедуры следующим образом: CREATE PROCEDURE tranTest$inner AS DECLARE @tranPoint sysname -- тип данных для идентификаторов SET QtranPoint = object_name(@@procld) + CAST(@@nestlevel) AS varchar(lO)) BEGIN TRANSACTION SAVE TRANSACTION dtranPoint -- снова ничего не делаем для простоты ROLLBACK TRANSACTION @tranPoint COMMIT TRANSACTION RETURN -100 GO CREATE PROCEDURE tranTest$outer AS DECLARE @retValue int DECLARE @tranPoint sysname -- тип данных для идентификаторов SET @tranPoint = object_name(@@procld) + CAST(@@nestlevel) AS varchar(lO)) BEGIN TRANSACTION SAVE TRANSACTION @tranPoint EXEC @retValue = tranTest$inner IF @retValue < 0 BEGIN ROLLBACK TRANSACTION @tranPoint COMMIT TRANSACTION RETURN @retValue END COMMIT TRANSACTION GO
Если мы запустим наш предыдущий пример: В
EXEC tranTest$outer
4 9 7
Глава 12
то получим следующее:
1
tranTest$outer
DECLARE @retValue int
Задает переменную для размещения возвращаемого значения из процедуры
QretValue = NULL
2
tranTest$outer
DECLARE @tranPoint sysname
Задает имя для точки сохранения транзакции
@@tranPoint = 'tranTest$outerl'
SET @tranPoint object name(@@procld) + CAST(@@nestlevel AS varchar (10))
3
tranTest$outer
BEGIN TRANSACTION
Начинает новую транзакцию
@@trancount == 1
4
tranTest$outer
SAVE TRANSACTION 'tranTest$outerl'
Сохраняет транзакцию, но не изменяет @@trancount
@@trancount == 1
5
tranTest$outer
EXEC @retValue tranTest$inner
Вызывает процедуру tranTest$inner
6
tranTest$inner
DECLARE gtranPoint sysname
Задает имя для точки сохранения транзакции
@@tranPoint = 'tranTest$inner2'
SET @tranPoint = object name(@@procld) + CAST(@@nestlevel AS varchar(10))
7
tranTest$inner
BEGIN TRANSACTION
Начинает вторую транзакцию
@@trancount == 2
8
tranTest$inner
SAVE TRANSACTION 'tranTest$inner2'
Сохраняет транзакцию, но не изменяет @@trancount
QStrancount = 2
g
tranTest$inner
ROLLBACK TRANSACTION 'tranTest$inner2'
@@trancount = 2 Откатывает к точке сохранения tranTest$inner2
498
Расширенный доступ к данным и методы корректировки
10
tranTest$inner
11 tranTest$inner
12
COMMIT TRANSACTION
Завершает @@trancount = 1 транзакцию и возвращает @@trancount к значению, когда мы начали процедуру на шаге 5
RETURN - 1 0 0
Возвращает отрицательное значение вызывающей процедуре, чтобы сообщить, что что-то произошло
tranTest$outer
После возврата значение
gretValue = -100
@retValue
заполнено 13
14 Л.~ж.
tranTest$outer
@retValue < 0
Определение, произошла ли ошибка
tranTest$outer
ROLLBACK
Возвращает к точке сохранения, не изменяя @@trancount
@@trancount = 1
@@trancount = 0
TRANSACTION
'tranTest$outerl'
15
tranTest$outer
COMMIT TRANSACTION
Завершает конечную транзакцию
16
tranTest$outer
RETURN @retValue
Возвращает значение внешнего вызова вызывающей программе
Этот код помогает оградить нас от какой-либо защиты транзакции и, как результат, от возможности иметь неправильные сохраненные данные после отката транзакции — потому что, как мы видим, код во второй хранимой процедуре вовсе не прекращает выполнение из-за ошибки счетчика транзакций. Изменения кода, которые мы сделали, на самом деле связаны только с внутренней хранимой процедурой и таковы, что ошибка внутренней транзакции не должна нарушить внешнюю транзакцию. Ошибка внешней транзакции не является проблемой, так как @@trancount обратится в нуль, величину, с которой этот параметр и начинался.
499
Глава 12 Положительный побочный эффект всего этого — то, что мы имеем возможность позволить выполнить внешнюю транзакцию. Имеются такие случаи, когда мы хотели бы завершить часть транзакции, когда какая-то часть ее вызвала ошибку, и приведенная гибкость программы позволяет нам это сделать.
Общие способы работы с хранимыми процедурами Фактическая цель этого раздела — дать основные блоки создания общих хранимых процедур — материал, который я редко видел в печати или в Интернете. Мы исследуем эти процедуры и обсудим некоторые "за" и "против" каждого решения. Хранимые процедуры — отличные средства, хотя, как увидим, напоминают ситуацию, когда мы вынуждены забивать квадратный гвоздь в круглое отверстие. Создадим наши объекты, чтобы иметь доступ до следующей таблицы: CREATE TABLE routine ( routineld int NOT NULL IDENTITY CONSTRAINT PKroutine PRIMARY KEY, name varchar(384) NOT NULL CONSTRAINT AKroutine_name UNIQUE, description varchar(100) NOT NULL, ts_timestamp timestamp )
Здесь создается таблица routine (программа) с четырьмя столбцами: автоинкрементный столбец routineld (идентификатор программы) типа int, являющийся первичным ключом; name (название) типа varchar (384), не может принимать значение NULL, содержащий уникальные значения; description типа varchar (100), не может принимать значение NULL и ts_timestamp (время создания/обновления) типа timestamp. Чтобы загрузить некоторые исходные данные, мы выполним фрагмент: INSERT INTO routine (name, description) SELECT specific_catalog + '.' + specific_schema + '.' + specific_name, routine_type + ' в ' + specific_catalog + ' БД создана ' + CAST(created AS varchar(20)) FROM <БД>.information_schema.routines SELECT count (*) FROM routine
Здесь мы заменим <БД> именами наших БД. В моем примере я буду использовать Northwind, Master и Pubs с общим количеством примерно 1000 строк. Я выполняю эти примеры на портативном компьютере Pentium II 400 с 256 Мб ОЗУ — довольно устаревшем компьютере. Все это дает нам основание для некоторого основного испытания работы хранимой процедуры, так как мы рассматриваем возможные способы построения хранимых процедур для наших приложений.
Восстановление данных Этот код заполняет список элементов управления в приложении и является обычной процедурой в моих приложениях. Каждый из параметров является фильтром списка, так что пользователь может просматривать список по своему усмотрению. Хранимая процедура следующая: 500
Расширенный доступ к данным и методы корректировки
CREATE PROCEDURE routine$list ( @routineld int = NULL,
--@name varchar(60) = ' %', -@description varchar(lOO) = ' % ' - -
первичный ключ для определения единственной строки так же, как в routine.name так же, как в routine.description
-- Описание : получает записи программ для отображения в списке -- Возвращаемая величина : неотрицательная - успешное выполнение; : от -1 до -99 - системная ошибка в качестве выходной величины; : -100 - ошибка, формируемая процедурой AS — т а к к а к сообщения о сумме будут непонятны для к л и е н т о в SET NOCOUNT ON - - по умолчанию п а р а м е т р @name - ' %', е с л и п е р е д а в а е м а я величина - NULL IF @name I S NULL SELECT @name = ' %' - - по умолчанию п а р а м е т р @ d e s c r i p t i o n - ' %', е с л и п е р е д а в а е м а я величина - NULL I F ( ^ d e s c r i p t i o n IS NULL SELECT @ d e s c r i p t i o n = ' %' - - выделение и з таблицы в с е х полей (кроме t s _ t i m e s t a m p ) д л я просмотра SELECT r o u t i n e . r o u t i n e l d AS r o u t i n e l d , r o u t i n e . n a m e AS name, r o u t i n e . d e s c r i p t i o n AS description FROM dbo.routine AS routine WHERE ( r o u t i n e . r o u t i n e l d = Sroutineld OR @routineld IS NULL) AND (routine.name LIKE @name) AND ( r o u t i n e . d e s c r i p t i o n LIKE @description) ORDER BY routine.name RETURN
Хотя сама хранимая процедура довольно элементарна, мы должны обсудить оператор WHERE. Я проверяю все параметры набора, независимо от того, какие параметры посылает пользователь. SQL Server строит свой план работы, учитывая тот факт, что некоторое значение может быть пропущено. Но как он это выполняет? Для таблиц средних размеров чрезвычайно быстро. Однако это не оптимальное решение проблемы. Чтобы показать это, мы возьмем фактический запрос из хранимой процедуры и выполним его, окружив операторами GETDATE () и включив SET STATISTICS 10. Это позволит нам непосредственно проверить выполнение кода. Обратите внимание, что это — лишь один из вариантов проверки работы вашего кода. Посмотреть на план запроса в Query Analyzer — также очень хороший способ понять, что происходит. Мне кажется, что легче отслеживать аргумент, наблюдая количество времени, и сколько требуется операций ввода-вывода, чтобы выполнить оператор.
501
Глава 12 Кроме определения времени выполнения кода, мы будем измерять: •
количество просмотров (Scan count) — число необходимых просмотров таблицы;
•
число логических чтений (Logical reads) — число страниц, читаемых из кэша данных;
•
число физических чтений (Physical reads) — число страниц, которые читались с физического диска;
Q
число упреждающих чтений (Read-ahead reads) — число страниц, помещенных в кэш.
Объединение логического чтения и физического чтения вместе — хороший показатель выполнения запроса. Для запросов, имеющих больший размер, и более занятых систем логическое чтение может стать физическим чтением при увеличении содержимого кэша. SET STATISTICS 10 ON DBCC freeproccache
—
очистка кэша, чтобы позволить выбрать лучший план
SELECT GETDATEO AS before -- запуск варианта процедуры EXEC routine$list @routineld = 848 SELECT GETDATEO AS middle -- затем запускаем ее снова как немедленный запрос SELECT routine.routineld AS routineld, routine.name AS name, routine.description AS description FROM dbo.routine AS routine WHERE (routine.routineld = 848 OR 848 IS NULL) AND (routine.name LIKE ' %') AND (routine.description LIKE ' %') ORDER BY routine.name SELECT GETDATEO AS after
Этот код дает следующие результаты (я не показал фактические результаты запроса для экономии места): before 2001-01-17 20:41:16.300 Table 'routine'. Scan count 1, logical reads 12, physical reads 0, read-ahead reads 0. (Таблица 'routine'. Количество просмотров — 1, число логических чтений — 12, число физических чтений — 0, число упреждающих чтений — 0.) middle 2001-01-17 20:41:16.330
502
Расширенный доступ к данным и методы корректировки Table 'routine". Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0. (Таблица 'routine. Количество просмотров — 1, число логических чтений — 2, число физических чтений — 0, число упреждающих чтений — 0.) after 2001-01-17 20:41:16.330 Это сообщает нам, что выполнение оператора и восстановление данных занимают 0.03 секунды для хранимой процедуры и меньше 0.03 секунды для случая нерегламентируемого запроса (точность типа данных d a t e t i m e — 0.03 секунды). В общем, небольшое различие. Но будет ли это так всегда? Нет, как мы увидим, выполняя тот же самый тест, фильтруя столбец d e s c r i p t i o n (описание) (который не индексирован): SET STATISTICS IO ON DBCC freeproccache
- - очистка кэша, чтобы позволить
выбрать лучший план
SELECT GETDATEO AS before EXEC r o u t i n e S l i s t
@description = 'процедура
в БД master
с именем s p _ h e l p s q l %'
SELECT GETDATEO AS middle SELECT r o u t i n e . r o u t i n e l d AS r o u t i n e l d , routine.name AS name, r o u t i n e . d e s c r i p t i o n AS d e s c r i p t i o n FROM d b o . r o u t i n e AS r o u t i n e WHERE ( r o u t i n e . r o u t i n e l d - NULL OR NULL IS NULL) AND (routine.name LIKE ' %') AND ( r o u t i n e . d e s c r i p t i o n LIKE 'процедура в БД master с именем sp_helpsql %' ORDER BY routine.name SELECT GETDATEO AS
after
Удалив фактические строки, возвращаемые в виде результатов, получим следующее: before 2001-01-17 21:13:49.550 Table 'routine'. Scan count 1, logical reads 2022, physical reads 0, read-ahead reads 0. (Таблица 'routine'. Количество просмотров — 1, число логических чтений — 2022, число физических чтений — 0, число упреждающих чтений — 0) middle 2001-01-17 21:13:49.620 Table 'routine'. Scan count 1, logical reads 19, physical reads 0, read-ahead reads 0. (Таблица 'routine'. Количество просмотров — 1, число логических чтений — 19, число физических чтений — 0, число упреждающих чтений — 0)
503
Глава 12 after 2001-01-17 21:13:49.640 Мы видим, что здесь нет большого различия между временами, требуемыми для выполнения теста — 0.07 по сравнению с 0.02 секунды. Но на сей раз хранимая процедура требует в десятки раз больше логических чтений. Так как логические чтения осуществляются непосредственно из памяти, это не дает почти никакого различия даже на моей медленной машине. Имеется пара альтернатив хранимой процедуры. Мы могли бы включить: •
Использование в хранимой процедуре параметра WITH RECOMPILE. Это вызывает формирование новой схемы всякий раз когда выполняется запрос. Формирование схемы требует времени, так как плохая схема приведет к реальным проблемам функционирования; вообще-то это неплохая идея. Если процедура не вызывается слишком часто, стоимость компиляции обычно весьма мала по сравнению с выгодой из-за лучшей схемы.
•
Построение вложения IF THEN с любой возможной комбинацией параметров, которую пользователь может послать, и обычно создание запроса для каждой возможности. Здесь главная проблема в том, что имеется (п! + 1) возможных комбинаций параметров.
Q
Выбор отдельных случаев и построение дополнительных хранимых процедур, чтобы работать с самыми медленными из них. В нашем предыдущем случае с использованием описания, если это выполнение неприемлемо, могла бы быть создана вторая хранимая процедура, что было бы быстрее. Тогда перед оператором SELECT мы для данного случая переходим на другую хранимую процедуру.
•
Продолжение использования хранимой процедуры, но с помощью динамического запроса. Это решение будет выполняться лучше всего, но когда мы выполняем код в хранимой процедуре, то теряем выгоды безопасности хранимых процедур, поскольку пользователь в этом случае должен иметь право управлять динамическим запросом.
Давайте продемонстрируем динамическую альтернативу запроса. Здесь мы будем создавать текст в переменной типа varchar и затем будем запускать запрос, используя EXEC: CREATE PROCEDURE routine$list ( @routineld int = NULL, @name varchar(60) = ' %' , @description varchar(100)
---—
первичный ключ для определения единственной строки так же, как в routine.name так же, как в routine.description
) AS -- так как сообщения о сумме будут непонятны для клиентов SET NOCOUNT ON -- создание переменной для размещения основного запроса DECLARE Qquery varchar(8000) -- выделение всех полей таблицы для просмотра SET @query = 'SELECT routine.routineld AS routineld, routine.name AS name, routine.description AS description FROM dbo.routine AS routine'
504
Расширенный доступ к данным и методы корректировки -- создание переменной для размещения операторов WHERE, которые мы -- будем создавать для конкретных условий DECLARE @where varchar(8000) SET @where = '' — так как NULL + 'что-то' = NULL — добавление имени для поиска к оператору WHERE IF gname <> ' %' AND @name IS NOT NULL SELECT @where = @where + CASE WHEN LEN(RTRIM(@where)) > 0 THEN 'AND ' ELSE " END + ' name LIKE ' " + @name + " " -- добавление имени для поиска к оператору WHERE IF @description <> ' %' AND @description IS NOT NULL SELECT @where - @where + CASE WHEN LEN(RTRIM(@where)) > 0 THEN ' AND ' ELSE '' END + ' name LIKE " ' + @description + "
"
-- выделение всех полей таблицы для просмотра IF @routineld IS NOT NULL SELECT @where = @where + CASE WHEN LEN(RTRIM(@where)) > 0 THEN ' AND ' ELSE '' END + ' routineld = ' + CAST(@routineld AS varchar(10)) -- создание переменной для оператора WHERE DECLARE @orderBy varchar(8000) -- задание порядка по именам возвращаемых строк SET @orderBy = ' ORDER BY routine.name' EXEC (@query + @where + @orderBy) GO
Эта процедура будет работать оптимально для любого набора параметров, передаваемых в нее, но для такого простого запроса она представляет довольно большой тяжело поддерживаемый код. Учтем факт, что некоторые основные хранимые процедуры для восстановления набора строк могут иметь двадцать и более строк SQL-кода с весьма небольшим количеством соединений. Тогда вы начнете понимать, почему динамическое создание запроса в хранимой процедуре (или даже поступающего от клиента) — такая сложная вещь для реализации. По моему опыту процедура оптимизируется, если она работает в пределах спецификаций и не сталкивается с другими процессами. Я предпочитаю создавать код БД, являющийся столь же общим, как в примере со списком процедур, и оптимизировать его, если необходимо. Мне редко нужно будет оптимизировать этот специфический запрос, но в тех случаях, где он работает слишком медленно или заставляет другие процессы ожидать, одна из перечисленных альтернатив будет всегда решать проблему. В следующих трех разделах мы рассмотрим хранимые процедуры, которые создают, изменяют и удаляют данные. В то время как получение данных вызывает трудности, эти типы модификации данных — в значительной степени более легкая задача. Хотя пользователи будут часто требовать просмотра до тысячи строк данных, они редко будут желать корректировать тысячу строк одновременно. Это, конечно, бывает, и мы можем при необходимости закодировать такие операторы, но модификация данных обычно осуществляется с помощью первичного ключа. Почти каждое формирование OLTP, с которым я был связан в течение ряда лет, выполнялось в следующем порядке: •
находим строку из списка; 505
Глава 12
•
просматриваем детали строки;
•
корректируем строку, удаляем строку или добавляем новую строку;
•
просматриваем строку после выполнения действий;
•
помещаем строку в список.
В этом разделе мы рассмотрим создание основных хранимых процедур, которые создают, изменяют и уничтожают отдельные строки. Можно будет легко расширить примеры для изменения многих строк, просто повторяя части кода.
Создание строк Хранимая процедура, использующая оператор CREATE, имеет вид: CREATE PROCEDURE routine$line @name varchar(384), @description varchar (500), @new rouitneld int = NULL OUTPUT ) AS -- объявление переменных, используемых для управления ошибками в блоках - содержит число строк, возвращаемых вызовом ЯМД DECLARE @rowcount int, -- используется для размещения кода ошибки @error int, -- после вызова ЯМД @msg varchar(255), -- используется для предварительного -- форматирования сообщений об ошибках -- общая переменная для восстановления Sretval int, — возвращаемых значений от вызовов -- хранимых процедур -- используется для размещения имени транзакции @tranName sysname, GmsgTag varchar(255) -- используется для размещения тега сообщения об ошибке -- задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld)
';type=P'
-- код управления ошибкой по умолчанию, используемый для большинства процедур -- настраиваемое формирование имени транзакции гарантирует уникальное имя транзакции, -- даже если процедура вызывается дважды в цепочке, так как если одна активна, -- а другая вызывается, то она не может быть на том же уровне вложенности SET @tranName = object_name(@@procld) + CAST(@@nestlevel AS varchar (2)) -----
заметим, что включение этой транзакции не является принципиально необходимым, так как мы имеем только один оператор. Я люблю включать ее лишь для того, чтобы предусмотреть возможность изменения кода.
BEGIN TRANSACTION — начало транзакции SAVE TRANSACTION @tranName -- точка сохранения транзакции, куда мы — можем откатить наше действие без -- ликвидации всей транзакции — выполнение добавления INSERT INTO routine(name, description)
506
Расширенный доступ к данным и методы корректировки VALUES (@name, @description) — проверка ошибки IF (@@error ! = 0) BEGIN — формирование дополнительной ошибки, чтобы помочь видеть, где произошла ошибка SELECT @msg = 'Есть проблема при добавлении новой строки в ' + 'таблицу routine.' + msgTag + ';call=(оператор INSERT)>' RAISERROR 50000 @msg ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END -- затем восстановление автоинкрементной величины из строки, -- которая вызвала ошибку SET @new_rouitneld = scope_identity() COMMIT TRANSACTION GO (SQL Server допускает вместо ключевого слова TRANSACTION использовать его сокращение TRAN, так что можно, например, вместо COMMIT TRANSACTION написать COMMIT TRAN. Прим. перев.) Далее проверим код: BEGIN TRANSACTION - - помещение кода т е с т а в транзакцию, - - чтобы не в л и я т ь на БД DECLARE @ n e w _ r o u i t n e l d i n t EXEC r o u t i n e S i n s @name ' T e s t ' , @ d e s c r i p t i o n = ' Т е с т ' , @ n e w _ r o u i t n e l d = @ n e w _ r o u i t n e l d OUTPUT - - смотрим, к а к и е произошли ошибки SELECT name, d e s c r i p t i o n FROM r o u t i n e WHERE r o u t i n e l d = @ n e w _ r o u i t n e l d - - вызываем д у б л и р о в а н и е ошибки EXEC r o u t i n e $ i n s @name = ' T e s t ' , @ d e s c r i p t i o n = ' Т е с т ' , @ n e w _ r o u i t n e l d - @ n e w _ r o u i t n e l d OUTPUT ROLLBACK TRANSACTION — н е т изменений в БД Этот код возвращает два элемента. Первый — результирующий набор новой записи: name
description
Test
Тест
Далее — набор трех разных сообщений: Server: Msg 2627, Level 14, State 2, Procedure routineSins, Line 29 Violation of UNIQUE KEY constraint 'AKroutinejiame'. Cannot insert duplicate key in object 'routine'. (Сервер: сообщение 2527, уровень 14, состояние 2, процедура routine$ins, строка 29 Нарушение ограничения UNIQUE KEY 'AKroutine_name. Нельзя поместить повторный ключ в объект 'routine'.) 507
Глава 12 Server: Msg 50000, Level 16, State 1, Procedure routine$ins, Line 37 There was a problem inserting a new row into the routine table. (Сервер: сообщение 50000, уровень 16, состояние 1, процедура routine$ins, строка 37 Есть проблема при добавлении новой строки в таблицу routine.) The statement has been terminated. (Оператор удален.) Наши клиенты должны будут перевести первое сообщение во что-то, что может прочитать пользователь. Второе сообщение говорит нам, какая процедура была вызвана, и какие предпринимались действия, так что мы можем выполнить отладку. Однако обратите внимание, что там, где с процедурой отбора произошла неприятность при создании подходящего оператора WHERE для оптимизатора, мы собираемся определить фиксированное число столбцов для добавления информации, что имеет довольно раздражающий побочный эффект, не позволяющий использовать значения по умолчанию, которые были определены для наших столбцов. Это обычно не является проблемой, так как когда требуется взаимодействие пользователя, любые значения по умолчанию, которые пользователь мог бы выбирать, будут показаны в пользовательском интерфейсе, и любые, которые он не может изменить, не должны быть частью оператора INSERT.
Корректировка строк Корректировка или обновление данных в таблице — снова довольно простая задача: CREATE PROCEDURE routine$upd ( @key_routineld int, -- столбец ключа, который мы используем как — ключ для восстановления. Обратите внимание, -- что мы не можем обновлять первичный ключ @name varchar (384), @description varchar (500), @ts_timestamp timestamp -- оптимистическая блокировка ) AS -- объявление переменных, используемых для управления ошибками в блоках DECLARE Srowcount int, -- число строк, возвращаемых вызовом ЯМД @error int, -- код ошибки после вызова ЯМД @msg varchar (255), — предварительное форматирование сообщений об ошибках @retval int, — общая переменная для возвращаемых значений -- от вызовов хранимых процедур @tranName sysname, -- размещение имени транзакции @msgTag varchar(255) -- размещение тега сообщения об ошибке -- задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld) + ';type=P' + ';keyvalue=' + '@key_routineld:' + CONVERT(varchar(10), @key_routineld)
508
Расширенный доступ к данным и методы корректировки
SET @tranName - object_name(@@procld) + CAST(@@nestlevel AS varchar(2)) — удостоверьтесь, что пользователь ввел значение timestamp, -- поскольку будет очень расточительно, если его не использовать IF @ts_timestamp IS NULL BEGIN SET @msg = 'Значение timestamp не должно равняться NULL' + @msgTag + '>' RAISERROR 50000 @msg RETURN -100 END BEGIN TRANSACTION SAVE TRANSACTION @tranName UPDATE routine SET name = @name, description = @description WHERE routineld = @key_routineld AND timestamp = @ts_timestamp -- получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error IF @error != 0 — ошибка возникла за пределами этой процедуры BEGIN SELECT @msg = 'Проблема возникла при корректировке записи в таблице routine.' + @msgTag + ';call= (оператор UPDATE)>' RAISERROR 50000 @msg ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END ELSE IF @rowcount <> 1 -- это не должно быть первичным ключом -- или запись не существует BEGIN IF (@rowcount = 0) BEGIN — если запись существует без timestamp, — она должна быть модифицирована другим пользователем IF EXISTS (SELECT * FROM routine WHERE routineld = @key_routineld) BEGIN SELECT @msg = 'Запись таблицы routine модифицирована' + ' другим пользователем.' END ELSE BEGIN SELECT @msg = 'Запись программы не существует.' END END ELSE BEGIN SELECT @msg = 'Слишком много записей было модифицировано.' END SELECT @msg = @msg + @msgTag + 'call=(оператор UPDATE)>'
Продолжение кода на следующей странице
509
Глава 12 RAISERROR 50000 @msg ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END COMMIT TRAN RETURN 0
Я не буду демонстрировать этот код; вы должны будете запустить его из примера на www.wrox.com. Обратите внимание на использование столбца time stamp в качестве оптимистической блокировки и на то, как мы с ним работаем: О
Мы пробуем выполнить обновление, используя первичный ключ и столбец типа timestamp.
•
Далее, если не возникло никаких ошибок, мы проверяем, сколько строк изменено:
Q •
Если изменена одна строка, все хорошо. Если изменено больше одной строки, то изменено слишком много строк. Эта проверка могла бы быть сплошным сумасшествием, но это — минимальная стоимость за предотвращение неправильной работы. Важно удостовериться, что фактически происходит точно то, что вы ожидаете получить. В данном случае только одна строка должна быть обновлена, так что мы выполняем проверку, дабы удостовериться, что именно это произошло.
•
Если никакие строки не были обновлены, мы проверяем, существует ли в таблице строка со значением первичного ключа. Если это так, то строка существует, но она должна быть изменена. Иначе она никогда первоначально не существовала.
Мы проверяем каждый из этих случаев, потому что это предохраняет от появления дурацких ошибок. Много раз я видел запросы, написанные для модификации строк, и вместо передачи реальных значений они передают нули. Пять минут использования Profiler (наиболее важное средство для проверки кода БД!) — и проблемы найдены. Добавление полной обработки ошибок к процедуре предотвращает любую ситуацию, которая может произойти по нашей вине, с очень небольшими затратами. Обратите внимание, что мы снова должны обновлять все столбцы сразу. Было бы очень неэффективно писать хранимую процедуру, которая выполняет обновление, используя только передаваемый столбец. Однако следует выполнить одну вещь — рассмотреть все столбцы, проверка которых чрезвычайно дорога (или в некоторых случаях нарушает работу программы). Мы можем выбрать два пути:
510
•
Разбить оператор на два обновления. Одно корректирует дорогостоящее поле (поля), а другое корректирует другие поля. Этот вариант подходит, если вы имеете несколько полей, а дорогостоящие поля редко обновляются.
•
Более разумный путь (если имеется только одно или два поля) — проверить, изменились ли значения, и затем решить, действительно ли их обновлять. Создаются два (или больше) возможных оператора обновления, которые модернизируют рассматриваемые поля.
Расширенный доступ к данным и методы корректировки Давайте рассмотрим второй "путь. Возьмем случай, когда изменение поля d e s c r i p t i o n (описание) нашей таблицы вызывает все виды дорогостоящих проверок и денормализаций, требующих более секунды времени, но только изменение значения столбца d e s c r i p t i o n занимает 10 процентов времени. Мы могли заменить: UPDATE routine SET name = @name, description = @description WHERE routineld = @key_routineld AND timestamp = @ts_timestamp — получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error
на следующий код: -- удаляем timestamp из этого оператора, поскольку мы не хотим -- обновлять поле description при изменении timestamp IF EXISTS (SELECT * FROM routine WHERE routineld = @key_routineld AND description = @description) BEGIN UPDATE routine SET name = @name WHERE routineld = @key_routineld AND timestamp = @ts_timestamp — получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error END ELSE BEGIN UPDATE rowcount SET name • @name, d e s c r i p t i o n = @description WHERE r o u t i n e l d = @key_routineld AND timestamp = @ts timestamp - - получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error END
Заметьте, что мы не изменяем значение и все еще проверяем те же самые моменты в коде обработки ошибки. Кажется, что процедура будет теперь выполняться медленнее, так как имеется большее количество кода, и будет теперь работать медленнее, когда изменяется значение d e s c r i p t i o n . Однако, в целом, полное время, затраченное на все вызовы, будет значительно меньше, так как столбец d e s c r i p t i o n будет проверяться значительно реже, и мы не будем нести затраты, когда он не изменяется.
Удаление строк Удаление, или хранимая процедура DELETE, которую мы демонстрируем здесь, очень похожа на процедуру обновления за исключением того, конечно, что строка перестает существовать.
I
ALTER PROCEDURE routine$del ( @key_routineld int, @ts_timestamp timestamp = NULL
—
оптимистическая блокировка
Продолжение кода на следующей странице 511
Глава 12
AS — объявление переменных, используемых для управления ошибками в блоках DECLARE @rowcount int, — число строк, возвращаемых вызовом ЯМД @error int, — код ошибки после вызова ЯМД @msg varchar(255) , — предварительное форматирование сообщений об ошибках @retval int, -- общая переменная для возвращаемых значений -- от вызовов хранимых процедур @tranName sysname, — размещение имени транзакции @msgTag varchar(255) -- размещение тега сообщения об ошибке — задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld) + ';type=P' + ';keyvalue=' + '@key_routineld:' + CONVERT(varchar(10), @key_routineld) SET @tranName = object_name(@@procld) + CAST(@@nestlevel AS varchar (2)) BEGIN TRANSACTION SAVE TRANSACTION @tranName DELETE routine WHERE routineld = @key_routineld AND @ts_timestamp = timestamp -- получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error IF @error != 0 -- ошибка возникла за пределами этой процедуры BEGIN SELECT @msg = 'Проблема возникла при удалении записи в таблице routine.' + SmsgTag + ';call=(оператор DELETE)>' RAISERROR 50000 @msg ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END ELSE IF @rowcount > 1 — это не должно быть первичным ключом — или запись не существует BEGIN SELECT @msg = 'Слишком много записей программы было удалено. ' + @msgTag + ';call= (оператор DELETE)>' RAISERROR 50000 @msg ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END ELSE IF @rowcount = 0 BEGIN IF EXISTS (SELECT * FROM routine WHERE routineld - @key_routineld) BEGIN SELECT @msg = 'Запись программы модифицирована' + ' другим пользователем.' + 'call=(оператор DELETE)>' RAISERROR 50000 @msg
512
Расширенный доступ к данным и методы корректировки ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RETURN -100 END ELSE BEGIN SELECT @msg = 'Запись программы, которую вы пытались удалить' + ' не существует.' + @msgTag + ';call=(оператор DELETE)>' RAISERROR 50000 @msg -- В системе мы должны решить, действительно ли нужно -- резализовать эту ошибку, или мы должны завершить работу и -- возвратить отрицательное значение. Если вы пытались -- удалить что-то, а это не существует, - это плохо? END END COMMIT TRANSACTION RETURN 0
Выполнение процедуры DELETE чрезвычайно просто. Мы хотим уничтожить одну строку с помощью первичного ключа, для этого берем ее первичный ключ и удаляем строку. Нет никакой возможности удалить лишь части строки или передать неправильное число значений. Это мои четыре центральных хранимых процедуры. Мы теперь эффективно построили набор методов доступа, которые дают довольно удобный в работе интерфейс для создания приложения. Они также обеспечат ловушки в системе безопасности для нашего приложения, потому что мы можем соответственно выбрать операции на более дифференцированном уровне, чем INSERT, UPDATE, DELETE или SELECT. Очевидно, что мне нужно больше чем эти четыре хранимые процедуры, чтобы строить приложение, но они охватывают 80 процентов требований и позволяют создавать редактируемые приложения в довольно общем плане. Когда интерфейс БД должен удовлетворять очень похожим проектам, код доступа также может выполняться аналогичным образом.
Пакеты SQL-кода Пакет SQL-кода представляет собой один или большее количество SQL-операторов, выполняемых сервером в группе. Мы рассмотрим два различных типа пакетов и варианты работы с ними: • •
Пакеты с одним оператором — они содержат только единственный оператор, посылаемый серверу. Пакеты из нескольких операторов — используются для выполнения нескольких операций, когда единственный оператор или даже отдельную хранимую процедуру не удастся легко реализовать.
Обратите внимание, что концепция пакетов, содержащих один или несколько операторов, не имеет никакого отношения к SQL Server. Он выполняет весь пакет независимо от того, сколько операторов он содержит и что возвращает. Мы делаем различие, потому что должны по-разному кодировать эти два случая.
513
Глава 12
Один маленький момент, касающийся этого раздела, — мы не занимались сценариями создания БД, которые являются одноразовыми действиями. Напротив, мы обсуждаем создание приложения, чтобы работать с данными и изменять их.
Пакеты с ОДНИМ
оператором
Пакет с одним оператором — обычно оператор INSERT, UPDATE, DELETE или SELECT, но это может быть и хранимая процедура или системная функция, вроде функций DBCC. Единственный случай, когда мы не должны рассматривать добавление дополнительного кода, чтобы выполнить пакет, — это когда мы выполняем единственный оператор. Однако обратите внимание, что мы очень ограничены в возможностях работы с этими операторами. Мы можем послать SQL-оператор любого типа, но не можем делать больше одной корректировки данных. Следовательно, не допускается никакая предварительная проверка или манипуляция данными, и обработка ошибок в пределах оператора запрещена. Например, если мы выполняем следующее:
I
UPDATE customer SET firstName = 'Энди' WHERE customerld = 100
Здесь в таблице customer (заказчик) обновляется запись с customerld (идентификатор заказчика), равным 100, — в поле firstName (имя) помещается значение 'Энди. ... то не можем считать, что это действие выполнено успешно, так как здесь возможна ошибка. Проверка состояния ошибки и работа с самой ошибкой возлагаются на конкретную вызывающую программу. Однако имеется и другое беспокойство. Существует ли вообще эта строка? Если нет, ваша команда не будет обновлять никаких строк. Много раз я видел операторы (выполняемые на VBScript в ASP-странице) подобно следующему (VBScript — язык сценариев на основе Visual Basic; ASP — Active Server Page — активная страница сервера):
I
UPDATE customer SET firstName = 'Энди' WHERE customerld • 0
который не выполнялся, потому что пользователь сделал орфографическую ошибку в имени, забыл включить Option Explicit (явное описание ошибки), но включил On Error Resume Next (в случае ошибки переключиться на следующий оператор). Что более неприятно, если Вы не забываете использовать timestamp, или выбираете любую оптимистическую блокировку для вашей БД, всякий раз, когда вы изменяете строку, оптимистическая блокировка бесполезна. Мой совет — если вы собираетесь использовать пакеты некомпилируемого кода, чтобы модифицировать данные, вам следует создать общий объект для управления корректировками. Удостоверьтесь также, что вы получили правильный объект, потому что опыт подсказывает мне, что имеется много способов сделать глупую ошибку, которую очень трудно определить в вашем коде.
Пакеты с несколькими операторами При обсуждении пакетов с несколькими операторами мы будем в основном следовать той же самой структуре, которую использовали в хранимых процедурах. Однако мы должны рассмотреть две темы, которые нуждаются в другой обработке в данном контексте: 514
Расширенный доступ к данным и методы корректировки • •
транзакции; обработка ошибок.
Транзакции Как мы отметили в разделе Транзакции в начале главы, очень важно, что когда мы преобразовываем данные в SQL Server, то должны удостовериться, что никогда не начинаем транзакцию программного обеспечения нашего клиента без того, чтобы закрыть ее в том же самом пакете. Единственная проблема здесь связана с реализацией, потому что каждый раз, когда мы используем оператор GO, Query Analyzer начинает новый пакет. Обработка ошибок Когда мы объединяем вместе несколько операторов, то должны рассмотреть, что делать в случае отказа. Мы имеем оператор RETURN при кодировании хранимых процедур, но RETURN не работает в пакете. Когда мы решаем, что заканчиваем пакет, и желаем завершить, один из способов состоит в том, чтобы задать переменную и использовать ее для условного выполнения блоков кода. Предположим, что мы имеем операторы, которые хотим выполнить, и должны удостовериться, что каждый из них выполняется до окончания транзакции. Мы помещаем их в транзакцию, но что, если один из них терпит неудачу? BEGIN TRANSACTION <оператор__1> <оператор_2> <оператор_3> COMMIT TRANSACTION
Мы можем написать код вокруг этих операторов DECLARE @errorNbr int, @returnValue int, @errorOut bit
---— ---
содержит номера ошибок после каждого оператора содержит возвращаемое значение от хранимой процедуры флаг, сообщающий, выполнять или нет следующие операторы
SET @errorOut = 1 BEGIN TRANSACTION <оператор_1> SET @errorNbr = @@error IF @errorNbr <> 0 OR @returnValue < 0 BEGIN RAISERROR 50000 'Ошибка в выполнении <оператор_1>' SET @errorOut = 0 END IF @errorOut = 1 BEGIN <оператор 2> SET @errorNbr - @@error IF QerrorNbr <> 0 OR @returnValue < 0
Продолжение кода на следующей странице
515
Глава 12 BEGIN RAISERROR 50000 'Ошибка в выполнении <оператор_2>' SET @errorOut = 0 END END IF @errorOut - 1 BEGIN <оператор_3> SET @errorNbr = @@error IF @errorNbr <> 0 OR @returnValue < 0 BEGIN RAISERROR 50000 'Ошибка в выполнении <оператор_3>' SET @errorOut = 0 END END IF @errorOut <> 0 BEGIN RAISERROR 50000 'Ошибка в выполнении пакета' ROLLBACK TRANSACTION END ELSE COMMIT TRANSACTION
Снова не забудьте, что если вы получили ошибку в триггере, пакет с трудом остановится, и вся ваша внешняя обработка ошибки никогда не будет вызвана. Кодирование же доступа таким образом в любом случае — хорошая идея, потому что никакие ошибки ограничений не будут разрушать пакет. Всякий раз, когда вы хотите послать от клиента несколько операторов, чтобы все их выполнить или не выполнить ни одного, вы должны использовать схему, подобную этой. Обратите внимание, что я включил большое количество, по-видимому, избыточных сообщений об ошибках для целей отладки. Очевидно, сообщение относительно первой ошибки, которая вынуждает пакет выполняться не так, как надо, наиболее важно передать пользователю, переведя его в удобочитаемую для него форму. Обратите внимание на различную закраску отдельных частей этого пакета. Если вы просто объединяете вместе набор операторов, возможно, без наборов формируемых результатов, нет никакой проблемы в построении механизма пакета, чтобы добавить набор операторов и затем построить сценарий этого. Вы могли бы также добавить немного описательного текста к сообщению об ошибке так, чтобы что-то наподобие "Ошибка в выполнении <оператор_1>" стало бы более информативным относительно используемых строк: "Ошибка при удалении заказчика с customerld (идентификатором) = 1234". Когда мы приступаем к выполнению этого пакета, добавляются закрашенные части пакета и обработчики ошибок после каждого оператора. Конечно, это не единственный способ строить обработчик ошибок. Я часто использую другую технику. Вместо использования: IF @errorOut - 1
при построении пакетов я использую GOTO в обработчике событий: 516
Расширенный доступ к данным и методы корректировки <оператор_2> SET @errorNbr = @@error IF @errorNbr <> 0 OR @returnValue < 0 BEGIN RAISERROR 50000 'Ошибка в выполнении < о п е р а т о р _ 2 > ' GOTO e r r o r O u t END
и завершающую часть пакета в виде COMMIT TRANSACTION GOTO e n d O f B a t c h errorOut: RAISERROR 50000 'Ошибка в выполнении пакета' ROLLBACK TRANSACTION endOfBatch:
Это приемлемое решение, но оно использует неприятный оператор GOTO. Основное назначение этого метода — возможность просто добавить код ошибки в конец каждого оператора, но вообще-то это связано с моей ленью. Наиболее важный момент заключается в том, что мы держим наш пакет в безопасном состоянии и должны быть уверены, что транзакция закрыта. Если она не закрывается, могут произойти две действительно причудливые вещи: •
Каждый раз, когда вы изменяете данные, они будут надежно заблокированы, возможно, блокируя других пользователей после завершения этой транзакции.
•
Так как ваше приложение не знает, что это оно подвесило транзакцию, любые другие команды, которые вы посылаете, будут также запускаться внутри этой же самой транзакции.
Как упомянуто в Обработке ошибок в разделе Хранимые процедуры, нужно выполнить завершение всех пакетов, посланных серверу, или полностью отменить их. Если ошибка происходит в середине пакета, пакет ожидает что-либо, чтобы обработать ошибку перед продолжением.
Компилируемый SQL-код по сравнению с нерегламентированным SQL-кодом при создании приложений При создании OLTP-приложений мы должны осуществить определенный выбор. Должны мы или нет создавать хранимые процедуры? И если мы их создаем, то должны требовать, чтобы весь доступ к БД выполнялся через них? В этом разделе мы исследуем различные аргументы и дадим рекомендации по лучшему способу создания приложений.
Нерегламентированный SQL-код Нерегламентпированный SQL-код иногда называется прямым SQL-кодом, поскольку к этому коду ничего не добавляется. "Нормальный" путь, которым большинство средств общается с SQL Server — использование нерегламентированных пакетов SQL-кода. Как мы видели, такой код очень полезен, когда происходит объединение в пакет, и иногда это единственный путь, которым мы можем строить сценарии для одноразовых изменений данных. Но что можно сказать, если мы строим пользовательский интерфейс для нашей OLTP-СУБД? 517
Глава 12 Использование некомпилируемого нерегламентированный SQL-кода имеет некоторые преимущества по сравнению с формированием компилируемых хранимых процедур: • •
большая гибкость; более быстрая разработка;
•
лучшее функционирование в некоторых ситуациях.
Большая гибкость Поскольку мы создаем его в самую последнюю минуту, нерегламентированный SQL-код не зависит от некоторых основных требований к хранимым процедурам. Например, пусть мы хотим создать пользовательский интерфейс для списка заказчиков. Мы имеем десять столбцов, которые могут быть добавлены к оператору SELECT, входящие в таблицы, которые перечислены в операторе FROM. Очень просто включить в пользовательский интерфейс список столбцов, который пользователь может брать, чтобы настроить свой собственный список. Тогда программа может формировать список только с теми столбцами в списке SELECT, которые нужны пользователю. Так как некоторые столбцы могут быть очень большими, но содержать весьма немного данных, лучше возвратить несколько столбцов вместо всех десяти. Например, предположим, что мы имеем представление v_customer (представление-заказчик) со столбцами customerld (идентификатор заказчика), name (имя), number (номер), of f iceAddress (адрес офиса) с каждой строкой адреса, отделенной слэшем, phoneNumber (номер телефона), l a s t S a l e D a t e (последняя дата продажи) и yearToDateSales (продажи за год). Нам, конечно, не потребуется показывать пользователям customerld, хотя он всегда нам будет нужен. Если пользователь осуществляет продажу, то он мог бы сформировать совокупность параметров, содержащую name, number, l a s t S a l e D a t e , l a s t C o n t a c t D a t e (последняя дата контакта) и yearToDateSales. Интерфейс пользователя послал бы следующий запрос серверу, чтобы получить требуемые строки:
•
SELECT customerld, name, number, lastSaleDate, yearToDateSales FROM v_customer
Другому пользователю просто потребуется посмотреть имя и номер, тогда мы можем выполнить следующее: I
SELECT customerld, name, number FROM v customer
Это сохранит нам ширину полосы при возврате результата пользователю. Но мы все еще вычисляли бы yearToDateSales, l a s t C o n t a c t D a t e и l a s t S a l e D a t e . Как мы видели, в хорошо разработанной БД эти значения, вероятно, будут рассчитываться, а не храниться, что приведет к тому, что этот запрос будет содержать массу ненужных данных. В этом случае мы можем разбить запрос на две возможности. Если пользователь запрашивает столбец о продажах, то клиенту будет послан полный запрос:
I
518
SELECT customer.customerld, customer.name, customer.number , sales.yearToDateSales, sales.lastSaleDate FROM customer LEFT OUTER JOIN (SELECT customerld,
Расширенный доступ к данным и методы корректировки SUM(itemCount * pricePaid) AS yearToDateSales, MAX(date) AS lastSaleDate FROM s a l e s GROUP BY customerld) AS s a l e s ON customer.customerld = s a l e s . c u s t o m e r l d
i
Если пользователь не запрашивает столбец о продажах, клиент пошлет Д SELECT customer.customerld, customer.name, customer.number -- обратите внимание на запятые в начале строки. Это позволяет легче — прокомментировать столбцы строки или программно отделить их в -- реальном приложении , sales.yearToDateSales, s a l e s . l a s t S a l e D a t e Щ FROM customer LEFT OUTER JOIN (SELECT c u s t o m e r l d , SUM(itemCount * p r i c e P a i d ) AS y e a r T o D a t e S a l e s , MAX(date) AS l a s t S a l e D a t e FROM s a l e s GROUP BY customerld) AS s a l e s ON c u s t o m e r . c u s t o m e r l d = s a l e s . c u s t o m e r l d
Таким образом, мы получаем гибкость выполнять только то, что действительно нужно, То же самое можно сказать об операторах INSERT и UPDATE. Одна из проблем, которую мы выявили, когда создавали эти два типа хранимых процедур, — это то, что очень трудно изменить столбцы в списках INSERT и UPDATE. Это будет тривиально (с точки зрения SQL) при построении наших приложений, лишь включив столбцы, которые должны измениться в списке столбцов. Например, возьмем из вышеупомянутого поля таблицы customer (заказчик) — customerld, name и number. В нашей хранимой процедуре оператор UPDATE всегда должен посылать все столбцы:
I
UPDATE customer SET name = @name, number = dnumber WHERE customerld = @customerld
Но что, если изменяется только столбец name? И что, если столбец number — часть индекса, и он имеет проверки данных, которые требуют для выполнения три секунды? Используя прямой SQL-код, мы просто выполняем: UPDATE customer SET name = @name , number = @number 2fj WHERE customerld = @customerld Более быстрая разработка Скорость разработки, на мой взгляд, — неприятный момент и зависит исключительно от персонала. В учреждениях, где я работал, был случай, когда один хороший SQL-программист мог не отставать от пяти или около того программистов пользовательских интерфейсов, строящих приложения, а используя генераторы кода, мы могли бы увеличить это до десяти или пятнадцати. Однако здесь имеются два препятствия. Во-первых, хороших SQL-программистов трудно найти и, во-вторых, многие менеджеры не считают нужным иметь программистов, связанных только с SQL-программированием.
519
Глава 12 Таким образом, если вы не имеете специализированного специалиста, пишущего запросы, разработка кода пользовательского интерфейса с помощью пакетов SQL-запросов может быть одним из способов решения задачи. Однако стоит упомянуть, что главные прорехи возникнут, если общие правила (сохранять транзакции в одном пакете и стараться сделать один пакет совпадающим с одной транзакцией) не выполняются. Лучшее функционирование Выгода в функционировании от использования прямых SQL-запросов в основном связана с моментами, которые мы рассмотрели в предыдущем разделе, посвященном гибкости. Поскольку мы можем опустить части запроса, которые не имеют смысла в некоторых случаях, мы не попадаем в ситуации, когда выполняем ненужный код. SQL SERVER 7.0 и 2000 хранят планы этих запросов, но следует иметь в виду, что посылаемые операторы должны быть идентичны, кроме, возможно, скалярных значений аргументов поиска. "Идентичные" означает их действительную идентичность; добавление комментариев или даже символов пробела нарушают план. SQL Server может строить планы запросов, которые имеют параметры, — которые позволяют повторное использование плана последующими запросами — но, по всем статьям, хранимые процедуры лучше, когда следует использовать хранимые планы для реализации.
Компилируемый SQL-код Компилируемый SQL-код делится на два типа объектов. Один тип — хранимые процедуры (и функции, компилируемые внутри них). Другой тип — триггеры; мы не будем здесь рассматривать триггеры, поскольку предварительно установили, что они полезны только в очень определенных обстоятельствах. В этом разделе мы рассмотрим четыре специфических свойства хранимых процедур, которые делают их замечательными средствами создания приложений: •
они ограничивают доступ к данным;
•
функционирование;
•
инкапсуляция;
•
безопасность.
Ограниченный доступ к данным В то время как хранимая процедура может исполнять многократные действия, она выглядит для пользователя как один вызов. Как было рассмотрено ранее в разделе Хранимые процедуры, мы можем строить почти объектно-ориентируемые методы для наших объектов БД, так что пользователю никогда не понадобится выполнять вызовы нерегламентпированных запросов. Это дает нам очень большие организационные преимущества. Если мы ограничим пользователей обращением только с хранимыми процедурами, существует конечный набор путей, которыми пользователи могут обращаться к данным. Следовательно, если мы имеем проблему в функционировании, то можем оптимизировать хранимую процедуру довольно простым способом (во многом подобно тому, как мы делали выше в разделе Инкапсуляция) вместо необходимости прочесывать строки и строки кода, чтобы найти проблему. Функционирование Хранимые процедуры могут дать нам лучшее функционирование, размещая план оптимизированного запроса при первом выполнении процедуры. Последующие запросы используют точно тот же план, что и первый. Со всеми типами соединений, возможными таблицами, индексами, расширениями текста представления и т. д. оптимизация запроса — 520
Расширенный доступ к данным и методы корректировки нетривиальная задача, которая может потребовать всего несколько миллисекунд. Теперь, вероятно, при создании однопользовательского приложения, вы сможете сказать, кто интересуется этим запросом, но когда число ваших пользователей увеличивается, это начинает усложняться. Как мы только что обсудили в разделе Нерегламентированный SQL-код. Лучшее функционирование, SQL Server может хранить планы запросов SQL, и хотя при этом имеется несколько строгих правил, управляющих их формированием, на самом деле могут быть различия. Однако при доступе с помощью хранимых процедур правила гораздо менее строгие. Каждый раз, когда вы вызываете хранимую процедуру с любым набором параметров, она использует один и тот же план (если только мы не вызываем ее, используя WITH RECOMPILE). Это сохранит вам миллисекунды, что является важным, если имеется большое количество пользователей вашей системы. В то время как SQL Server обеспечивает довольно хорошую работу, кэшируя нерегламентированные SQL-запросы, прекрасно, когда он кэширует сами запросы хранимой процедуры. Нужно понять, что хранимые процедуры оптимизируются на основе начальных параметров, которые передаются ей. Это иногда вызывает проблемы, если вы запускаете одну и ту же процедуру с двумя различными наборами параметров. Например, мы можем иметь таблицу с некластерным индексом в поле, где 60 процентов элементов таблицы имеют одинаковые значения, в то время как все другие значения в таблице уникальны. Если бы ваш поиск был связан с неуникальными значениями, индекс не был бы лучшим способом поиска строк (так как издержки выборки при обращении через индекс к страницам данных будут, вероятно, больше, чем просто рассмотрение всех строк данных непосредственно). Если же поиск осуществляется по уникальным значениям, лучше использовать индекс. Какое бы значение ни использовалось первым, оно становится планом, который используется всеми последующими запросами. Имеются два способа работать в такой ситуации, если это становится проблемой: • •
Добавить условие WITH RECOMPILE в объявлении хранимой процедуры, вызывая каждый раз удаление плана. Создать процедуру, которая вызывает две других процедуры, одну — для уникальных случаев и другую — для неуникальных. Это довольно тяжело, но если это — часто используемая хранимая процедура, то может быть лучшим способом.
Инкапсуляция Одно действительно большое преимущество использования хранимых процедур как средства маскировки между пользователем и таблицами заключается в том, что они упрощают доступ к данным. Даже простой оператор SELECT, который обращается к единственной таблице, может быть сложно сформулировать и послать от клиента, хотя он и намного меньше многотабличного запроса или даже запроса с несколькими операторами. Например, рассмотрим следующий оператор к одной таблице:
I
SELECT personld, firstName, lastName FROM person WHERE personld = 1
Здесь в таблице person (человек) ищутся поля personld (идентификатор firstName (имя) и lastName (фамилия) строки, у которой personld = 1.
человека),
521
Глава 12 В символическом коде мы можем получить следующее: Ш query • "SELECT p e r s o n l d ,
firstName,
lastName FROM person WHERE personld = 1"
Мы могли бы выполнить его, используя ADO-технологию, или какой-нибудь другой набор объектов, который позволяет клиентам использовать данные из SQL Server. Если же мы выполняем хранимую процедуру, то можем просто сделать следующее:
I
DIM query AS STRING query = 'EXECUTE person$get Spersonld = 1"
Мы можем создать хранимую процедуру на сервере следующим образом: CREATE PROCEDURE p e r s o n $ g e t ( @personld int ) AS SELECT p e r s o n l d ,
firstName,
lastName FROM person WHERE personld = @personld
Первый момент, который вы, вероятно, заметили — это то, что хранимая процедура потребовала больше усилий для своего создания. Однако это очень простой пример. При рассмотрении в моем коде более сложных операторов SELECT, состоящих, по крайней мере, из пяти или шести строк, а иногда и до пятнадцати или двадцати, гораздо более легко для клиентов использовать код в виде единственной строки текста. Учтем также то, что в то время как наши примеры содержали запросы с единственным оператором, ранее мы рассмотрели запросы с несколькими операторами. Когда мы должны выполнить в процедуре более одного оператора, будь то заполнение временной таблицы или, возможно, даже обновление нескольких таблиц, мы должны использовать гораздо более сложный синтаксис формирования пакета, который был рассмотрен в разделеЛатсеты SQL-кода. Попытка реализовать сценарий программным путем и затем выполнить его, — довольно грязный путь. Маскирующий слой между пользователем и таблицами в виде хранимых процедур дает также преимущества оптимизации. Как мы обсуждали в разделе Хранимые процедуры, поскольку детали замаскированы и поэтому скрыты от пользователя данных, мы имеем конечный набор запросов для оптимизации; когда пользователь говорит, что данная процедура медленна, мы можем это легко установить. В то время как совершенная оптимизация может быть более трудна при использовании хранимых процедур, сами они позволяют закодировать повторное использование. Безопасность Мы можем ограничить доступ пользователей к конкретным хранимым процедурам и, следовательно, к конкретным основным таблицам. Результатом является то, что, используя хранимые процедуры, легко формировать надлежащий набор прав, который ограничивает пользователя возможностью видеть только то, что мы хотим, чтобы он видел.
522
Расширенный доступ к данным и методы корректировки Советы Каждый из этих двух вариантов SQL-кода имеет "за" и "против". С одной стороны, мы имеем нерегламенпгированный SQL-код. Он очень гибок — требует небольшого времени разработки, но приводит к некоторым связанным с ним небольшим всплескам загрузки во время функционирования. Вся эта гибкость может также создать хаос при попытке выполнить настройку приложения. С другой стороны, используя хранимые процедуры для доступа, мы экономим некоторое время компиляции при многократных использованиях, и имеем преимущества маскировки структуры БД от фактического пользователя данных, в сущности, помещая защитный уровень доступа к БД между пользователем и самими данными. Все рассмотренное говорит, что лучшим выбором для большинства OLTP-БД будет построение их, используя доступ с помощью хранимых процедур. Вы могли бы предположить, что основная причина этого — функционирование. Использование хранимых процедур улучшает функционирование, особенно, когда вы имеете чрезвычайно занятую БД. Однако, как только фирма Microsoft добавила планы запросов, хранимых для прямых SQL-запросов, эти хранимые виды запросов, которые использует OLTP-БД, — обычно не такая уж большая проблема, так что они устранили много недостатков при работе с этими объектами. Таким образом, самый большой выигрыш от использования хранимых процедур — инкапсуляция. Поддержание маскирующего слоя между приложениями и структурами данных — неоценимое средство для создания надежных приложений и сокращения затрат на обслуживание. Ключ к этому прост. Как только пользователи поработают с приложением некоторое время, изменения неизбежны. С хранимыми процедурами мы можем включить эти изменения в таблицы и при этом не нужно изменять наши приложения. Хранимые процедуры также делают наши приложения чище, забирая большую часть обработки ошибок ЯМД-операций из кода представления. Когда мы посылаем пакеты кода к SQL Server, то обычно так или иначе посылаем определение хранимой процедуры, так почему бы не строить SQL-запрос однажды и обычно это и использовать?
Рассмотрение безопасности Обеспечение безопасности — другая обязанность администратора БД, которую мы должны понять достаточно глубоко, чтобы могли создавать соответствующие приложения, которые защищают не столько от неправильных данных, сколько от неправильного использования данных. Как приложения, обеспечивающие доступ к данным, будут использовать безопасность, — это то, что мы детально и последовательно изложим. Мы обсудим, как спроектировать план безопасности для нашей БД, и рассмотрим, как можно использовать безопасность SQL Server в нашем собственном коде. Мы отойдем от любого технического обсуждения безопасности — типа того, использовать ли доверительные соединения (Windows Authentication) или стандартную безопасность SQL Server — в значительной степени потому, что с точки зрения проекта это нас действительно не волнует. Внутри БД безопасность будет означать то же самое.
523
Глава 12 Используя безопасность SQL Server, мы будем строить план безопасности, который предотвратит любое нежелательное использование наших объектов любым пользователем, будь то член нашей организации или хакер. Будучи архитектором данных, чрезвычайно важно тщательно проектировать наши потребности безопасности в терминах того, кто что может делать и с чем (как мы начали поступать с нашими действиями ранее в главе 5). Наша цель состоит в том, чтобы позволить пользователям выполнять любые задачи, которые им нужны, но запретить решение любых других задач или просмотр любых данных, которые им запрещены. Формирование ролей и связанных с ними пользователей довольно легко выполнить с помощью Enterprise Manager, и мы не будем это рассматривать. В SQL Server мы можем управлять правами доступа почти к каждому типу объекта. Следующая таблица показывает виды операций, которые нас будут интересовать, для каждого типа объекта с точки зрения безопасности наших БД: Объект
Права
Table (таблица)
SELECT,
INSERT,
UPDATE,
DELETE
View (представление) Column (столбец) — в таблицах и представлениях
SELECT,
INSERT,
UPDATE,
DELETE
SELECT,
INSERT,
UPDATE,
DELETE
Function (функция)
EXECUTE
Stored Procedure (хранимая процедура)
EXECUTE
Существуют также права на References подходят к этому обсуждению.
(ссылки) к таблицам, которые не
Далее мы можем разрешить или запретить использование этих объектов нашим пользователям. Мы, конечно, не собираемся предоставлять доступ каждому пользователю индивидуально, поскольку мы можем иметь множество пользователей в наших БД. Поэтому SQL Server обеспечивает нас понятием роли (ранее известное как группы в SQL Server 6.X и предыдущих), которой мы можем предоставить права доступа к данным и затем связать ее с пользователями. Каждый пользователь в БД является членом, по крайней мере, общедоступной роли, но может быть членом нескольких ролей. В принципе роли могут быть членами других ролей. Возьмем, например, любую типичную систему учета кадров, которая содержит информацию о служащем, наподобие имени, адреса, должности, хозяина, вида оплаты и т. д. Нам будут, вероятно, нужны разные роли, например:
524
•
Менеджеры отдела кадров (HRManagers) — может выполнять любую задачу в системе.
•
Сотрудники отдела кадров (HRWorkers) — могут обслуживать любой атрибут в системе, но требуется санкционирование записей, чтобы изменить информацию о жаловании.
•
Менеджеры (Managers) — все менеджеры в компании будут в этой роли, но они могут видеть только информацию о своих собственных рабочих.
•
Служащие (Employees) — могут видеть только свою собственную информацию, и могут изменять только свою собственную информацию об адресе.
Расширенный доступ к данным и методы корректировки Каждой из ролей затем предоставляется доступ ко всем ресурсам, в которых они нуждаются. Член роли менеджеров будет, вероятно, также и членом роли служащих. Тогда, как сказано выше, они могут видеть информацию о своих служащих, а также и свою. Имеются три различных команды, которые SQL Server использует для добавления или удаления прав в каждой из наших ролей: • • •
GRANT — дает права использования объекта. DENY — запрещает доступ к объекту независимо от того, предоставили ли пользователю права доступа по любой другой роли. REVOKE — используется, чтобы удалять любой оператор GRANT или DENY, который был выполнен. Ведет себя подобно отмене разрешения.
Почти каждый оператор разрешения, который вы будете выполнять, будет оператором GRANT. Как правило, мы просто даем разрешения роли, чтобы выполнить задачи, которые являются характерными для нее. Команда DENY должна использоваться только так, как большинство команд типа запрета, когда независимо от того, сколько до этого вы предоставили прав доступа к объекту, пользователь не будет иметь доступа к нему, если имеется лишь одна команда DENY. В качестве примера возьмем систему людских ресурсов, которую мы только что обсуждали. Мы не отменили бы доступ к роли служащих, так как менеджеры также являются и служащими и должны иметь права на это. Как только мы создали все наши объекты — включая таблицы, хранимые процедуры, представления и т. д. — можно предоставлять права на них. Мы уже упомянули в специальных разделах, посвященных объектам, что при наличии хранимых процедур, функций и представлений не нужно предоставлять права на основные объекты, которые используются в этих процедурах, функциях и представлениях. Это справедливо в том случае, когда пользователь, создающий объект, является также и пользователем, владеющим этим объектом. Так как мы будем создавать наши объекты как владельцы БД, обычно это не является проблемой. Одна дополнительная проблема, на которую мы должны обратить внимание, — это то, что каждая БД имеет специальный набор встроенных ролей: •
db_accessadmin — пользователи, относящиеся к этой роли, могут добавлять или удалять пользователей БД;
Q
db_backupoperator — пользователи, относящиеся к этой роли, могут поддерживать БД; db_datareader — пользователям, связанным с этой ролью, позволено читать любые данные в любой таблице; d b _ d a t a w r i t e r — пользователям, связанным с этой ролью, позволено записывать любые данные в любую таблицу;
• • •
db_ddladrain — пользователи, связанные с этой ролью, могут добавить, изменить или удалить любые объекты в БД (другими словами, выполнять любые ЯОД-операторы);
•
db_denydatareader — пользователи, связанные с этой ролью, не могут видеть никакие данные в БД, хотя они все еще могут видеть данные через хранимые процедуры; db_denydatawriter — во многом напоминает роль db_denydatareader; пользователи, связанные с этой ролью, не могут изменять никакие данные в БД, хотя они все еще могут изменять данные через хранимые процедуры;
•
525
Глава 12 • •
db_owner — пользователи, связанные с этой ролью, могут выполнять любые операции в БД; db_securityadmin — пользователи, связанные с этой ролью, могут модифицировать и изменять допуск и роли в БД.
Решение, делать ли ваших пользователей членами какой-либо из этих ролей, полностью зависит от ситуации. Я люблю создавать роль пользователя-администратора — пользователя, кто может добавлять пользователей к БД и связывать их с ролями. Создание приложения, которое отображает эти значения — довольно легкая задача с использованием SQL-DMO (SQL Data Management Objects — объекты управления данными SQL). Тогда, как только пользователь был создан администратором БД, пользователь в роли администратора может связать их с его ролями БД. Программно мы можем определить некоторую основную информацию относительно безопасности пользователя в БД: •
IS_MEMBER (<роль>) — сообщает нам, является ли текущий пользователь членом данной роли;
•
USER — сообщает нам имя текущего пользователя;
•
PERMISSIONS (OBJECT_ID (' <имя_объекта>' ) ) — позволяет нам видеть, имеет ли пользователь права выполнить некоторое действие.
Например, мы могли бы построить запрос, чтобы определить все соответствующие хранимые процедуры, которые пользователь может выполнять: SELECT s p e c i f i c _ n a m e , CASE WHEN PERMISSIONS(OBJECT_ID(specific_name)) THEM 1 ELSE 0 END AS canExecute FROM inf ormation_schema . routines WHERE specific_name LIKE 'routine$%'
& 32 = 32
Когда мы выполним его, то получим следующий список specificjiame
canExecute
routine$del routine$ins routine$list routine$upd
1 1 1 1
Если вы ограничили доступ к основным таблицам, то уже создали каждый элемент безопасности в вашей системе, с которыми пользователь или программист должен будет когда-либо иметь дело. Например, возьмем четыре процедуры, которые мы перечислили выше. Ограничивая доступ к процедуре r o u t i n e $ l i s t , пользовательский интерфейс может просмотреть этот список доступа и определить это, если потребуется когда-либо выполнить данную процедуру. То же самое верно для процедур добавления, обновления и удаления. Если пользовательский интерфейс не может использовать процедуру r o u t i n e $ d e l , он даже не будет показывать Delete (удаление) в своих меню.
526
Расширенный доступ к данным и методы корректировки PERMISSIONS () — довольно универсальная функция, которая может использоваться, чтобы определить, может ли пользователь выполнять большинство операций в SQL Server (чтобы получить дополнительную информацию, проконсультируйтесь в SQL Server Books Online). При создании приложений мы будем пробовать все лучшее, чтобы использовать возможности SQL Server для наших нужд безопасности — это поможет нам избежать когда-либо формирования таблиц пользователей и их прав. В остальной части этого раздела мы расширим концепцию использования безопасности SQL Server, чтобы иметь дело с некоторыми методами, которые должны будем иногда использовать. Мы рассмотрим: •
безопасность на уровне строк;
•
безопасность на уровне столбцов.
Безопасность на уровне строк Обычно это относится к ситуации, когда мы хотим дать пользователю права доступа к таблице и всем столбцам, но не хотим, чтобы они могли видеть все строки, учитывая некоторые критерии. Безопасность на уровне строк было очень трудно осуществить в предыдущих версиях SQL Server. В SQL Server 2000 мы можем выбрать довольно простое решение этой проблемы, используя определяемые пользователем функции. Для нашего первого примера возьмем таблицы a r t i s t (артист) и album (альбом) из предыдущей главы. Мы собираемся рассмотреть разрешение пользователям видеть только альбомы Beatles (Битлз), если они являются членами ролиBeatlesFan. Предположив, что мы имеем созданную роль, сформируем следующую определяемую пользователем функцию: CREATE VIEW v_albura AS SELECT albumld, name, a r t i s t l d FROM dbo.album WHERE a r t i s t l d <> 1 — e Битлз OR i s _ m e m b e r ( ' B e a t l e s F a n ' ) = 1 GO
Затем мы выделим отсюда:
О
SELECT *
FROM dbo.v album
Этот запрос возвратит: albumld
name
artistld
3 9
quadrophonia tommy
2 2
потому что я не член роли B e a t l e s F a n . Это, конечно, не очень хорошо. Что, если мы хотим запретить пользователям просматривать работу других артистов? Нам нужно расширить пример. Можно было бы создать таблицу с группой отношений a r t i s t (артист), которая здесь названа rlsAlbum ( r l s означает row-level security — безопасность на уровне строк).
18-1868
527
Глава 12 CREATE TABLE rlsAlbum ( rlsAlbumld int NOT NULL IDENTITY, artistld int, memberOfGroupName sysname
Здесь создается таблица rlsAlbum (альбом с безопасностью на уровне строк), содержащая три поля: автоинкрементное rlsAlbumld (идентификатор альбома с безопасностью на уровне строк) типа int, не может принимать значение NULL; artistld (идентификатор артиста) типа int и memberOfGroupName (имя члена группы) типа sysname. Затем мы создадим представление, которое коррелирует с безопасностью в таблице a r t i s t : -- мы строим представление, которое включает нашу таблицу -- безопасности на уровне строк: ALTER VIEW v_album AS SELECT album.albumld, album.name, album.artistld FROM dbo.album AS album JOIN (SELECT DISTINCT artistld FROM rlsAlbum WHERE IS_MEMBER(memberOfGroupName) = 1 AS security ON album.artistld = security.artistld GO
Затем мы выделим столбцы из представления до добавления каких-либо групп
I
-- выполнение как членов никаких групп: SELECT * FROM v album
и мы не получаем строк: albumld
name
artistld
Далее мы можем добавить некоторые группы к таблице rlsAlbum, чтобы позволить с помощью db_owner и разных логических ролей видеть значения в таблице: - ~ параметр db_owner, вероятно, добавлен в триггер INSERT INTO rlsAlbum ( a r t i s t l d , memberOfGroupName) VALUES ( 1 , 'db_owner')
INSERT для таблицы album
INSERT INTO rlsAlbum (artistld, memberOfGroupName) VALUES (2, 'db_owner') INSERT VALUES INSERT VALUES GO
INTO rlsAlbum(artistld, memberOfGroupName) (1, 'BeatiesFan') INTO rlsAlbum (artistld, memberOfGroupName) (2, 'WhoFan')
Когда мы будем выделять строки из таблицы, то получим:
528
Расширенный доступ к данным и методы корректировки albumld 1 2 4 8 10 11 9 3
name
artistld
the white album revolver abbey road rubber soil yellow submarine please please me tommy quadrophonia
1 1 1 1 1 1 2 2
Теперь, если пользователи не имеют прав на основную таблицу, они не смогут видеть какие-либо строки, если вы не дали для представления конкретные права роли, членами которой они являются.
Безопасность на уровне столбцов Безопасность SQL Server имеет средства для разрешения и отмены прав на конкретный столбец, но они тяжелы для использования. Они также не будут работать, когда мы используем представления и хранимые процедуры для доступа к нашим данным, так как объекты используют безопасность своего владельца. Если мы хотим использовать хранимые процедуры для доступа ко всем нашим данным (что я предпочитаю), нужно рассмотреть различные возможности. В этом разделе мы дадим возможную альтернативу создания нескольких представлений или процедур, чтобы вертикально делить наши данные. Возможный способ реализовать это состоит в использовании технологии, напоминающей пример уровня строк, чтобы закрыть столбец, который пользователь не имеет право видеть. Используя разрешения SQL Server, мы можем расширить наше предыдущее представление, чтобы возвратить значения только для столбцов, к которым пользователи имеют доступ: ALTER VIEW v_album AS SELECT album.albumld,
— предполагаем, что пользователь может видеть - - у к а з а т е л ь первичного ключа CASE WHEN PERMISSIONS(OBJECT_ID('album'), ' n a m e ' ) & 1 = 1 THEN album.name ELSE NULL END AS name, CASE WHEN PERMISSIONS(OBJECT_ID('album'), ' a r t i s t l d ' ) THEN a l b u m . a r t i s t l d ELSE NULL END AS a r t i s t l d FROM dbo.album AS album JOIN (SELECT DISTINCT a r t i s t l d FROM rlsAlbum
& 1 =1
WHERE IS_MEMBER(memberOfGroupName) = 1) AS s e c u r i t y ON album.artistld = security.artistld
Обратите внимание, что мы здесь возвращаем NULL как индикатор, потому что столбец не допускает значение NULL. В некоторых случаях, когда, например, NULL является фактически допустимым значением для столбца, мы могли бы добавить дополнительный столбец, возможно, кое-что наподобие canSeeNameFl (флаг возможности видеть имя) типа b i t , который сообщает приложению, может ли пользователь видеть столбец. 529
Глава 12 Это может привести к небольшому количеству издержек, но при этом можно обеспечить почти любую безопасность на уровне столбцов и строк. Важный аспект этого решения — то, что мы можем теперь использовать это представление в хранимой процедуре, и независимо от того, кто является владельцем хранимой процедуры, мы способны ограничить использование столбца в довольно общем случае, который использует только безопасность SQL Server.
Рассмотрение взаимодействия между БД До сих пор все, что мы обсуждали относительно кода и проблем, касалось локальных БД. Вообще говоря, кроме факта, что данные не в пределах той же самой логической единицы функционирования, имеется небольшое различие в доступе к данным вне границ местной БД. Это обычно делает работу с данными тяжелее, тем более что ограничения внешнего ключа не могут использоваться для управления потребностями ссылочной целостности. Кроме того, так как другая БД будет поддерживаться и использоваться по-другому, мы теряем часть защиты, которую имеем в мире отдельных БД. Хотя доступ к данным во внешних БД создает трудности, иногда это неизбежно. Типичным примером могла бы быть попытка связать имеющуюся в наличии систему с вашей доморощенной системой. Может оказаться легче организовать доступ к таблицам из различных серверов, чем попытаться включить их в вашу БД. В этом разделе мы определим некоторые из проблем, которые возникают при работе с другими БД. Разобьем все случаи на два варианта: •
тот же самый сервер;
Q
другой сервер.
Тот же самый сервер Доступ к данным в различных БД, но на одном и том же сервере во многом подобен обычной (та же самая БД, тот лее самый сервер) ситуации. Однако имеется несколько проблем, связанных с обозначением, безопасностью и корректировкой данных, которые нам нужно исследовать.
Обозначение при использовании того же самого сервера Это довольно просто. Чтобы иметь доступ за пределами нашей БД, мы просто включаем БД в обозначение объекта, к которому обращаемся. При этом используется следующее соглашение: |
<имя_БД>.<владелец>.<имя_объекта>
Это подходит для таблиц, хранимых процедур, функций и т. д.
Безопасность на том же самом сервере Каждая БД имеет уникальное значение db_owner (владелец БД). Следовательно, один db_owner не может создать процедуру для доступа к данным в другой БД без того, чтобы не получить разрешение от db owner этой БД.
530
Расширенный доступ к данным и методы корректировки Если мы на сервере имеем две БД, в настоящее время нет никакого способа коррелировать этих двух владельцев и позволять им передавать права на объекты через хранимую процедуру. Например, рассмотрим следующую процедуру: CREATE PROCEDURE p r o c e d u r e $ t e s t C r o s s D a t a b a s e AS SELECT fieldl FROM database2.dbo.table GO
Пользователь будет нуждаться не только в правах доступа к процедуре p r o c e d u r e $ t e s t C r o s s D a t a b a s e , но и к БД database2, и мы будем должны предоставить ему возможность явного доступа к БД и объекту. Плохо также, что это касается и триггеров. Так как пользователь даже не будет, вероятно, и видеть данные, которые триггер контролирует, кажется опасно предоставлять ему полные права на таблицу в другой БД, только для того, чтобы проверить значение. По этим причинам было бы выгодно копировать данные, которые нужно контролировать, в вашу собственную БД, конечно же, в зависимости от фактического размера этих данных. Это защитит ваши данные от просмотра, но, конечно, обойдется дороже, чем выполнение взаимного поиска в БД.
Корректировка данных на том же самом сервере В первой части этой главы мы рассмотрели, как SQL Server использует транзакции и блокировки, чтобы обеспечить ситуацию, когда наши операции или полностью заканчиваются, или полностью не выполняются, и, что более важно, без прерывания от других процессов. Нет никакого различия в кодировании, когда мы обеспечиваем доступ к данным в различных БД. Мы просто используем тот же самый синтаксис и обращаемся к данным в обеих БД. Те же самые механизмы транзакций беспрепятственно работают в любом случае. Например, пусть мы имеем старую собственную заявочную систему и новую закупочную систему, которая была создана другой компанией. Когда мы создавали первую систему, то включили таблицу, которая содержала список товаров, которые покупатели могут приобрести, и которая показывала, откуда товары могут быть поставлены. Теперь, когда мы имеем новое третье лицо в закупочной системе, то хотим использовать ее функциональные возможности заказа товаров. Мы хотели бы автоматически создать заказ товаров, когда к заказам имеет доступ человек с полномочиями. Так, в хранимой процедуре r e q u i s i t i o n $ f i n a l i z e вызов INSERT INTO для вышеупомянутого списка будет заменен на INSERT INTO для таблицы заказа товаров: CREATE PROCEDURE requisition$finalize ... вспомогательный код удален для ясности ... BEGIN TRANSACTION INSERT INTO dbo.requisitionStatus (requisitionNumber, statusld) VALUES (@requisitionNumber, @statusld) -- обработка ошибок INSERT INTO purchasing.dbo.purchaseOrder(number, date)
Продолжение кода на следующей странице
531
Глава 12 VALUES (0purchaseOrderNumber, -- обработка ошибок
getdateO)
— и также поместить товары COMMIT TRANSACTION
Поскольку эти две БД находятся на одном и том же сервере, операция кажется пользователю неуязвимой. Однако так как теперь имеется несколько протоколов транзакции в смеси, SQL Server не может выполнять простую транзакцию. Он должен использовать очень важную концепцию, называемую завершением с двумя стадиями.
Завершение с двумя стадиями Завершение с двумя стадиями представляет собой протокол, который позволяет серверу связываться с БД, которой требуется выполнить задачу как транзакцию в двух различных логических областях. Тот же самый протокол используется и когда СОМ+ использует объекты СОМ для выполнения задач, не относящихся к БД, в пределах транзакции. Используя СОМ+, мы можем смешивать транзакции объектов БД с объектами файловой системы, используя завершение с двумя стадиями, чтобы в значительной степени гарантировать целостность транзакции (имеется некоторая возможность неудачного завершения транзакции на одном источнике просто из-за времени ожидания, но такая возможность очень маленькая). Имейте в виду, что диспетчер начинает транзакции так же, как это обычно делается. Транзакция начинается на каждой отдельной БД или ресурсе точно так же, как это было бы, если это была единственная транзакция. Затем, когда мы посылаем завершение, чтобы перевести счетчик транзакций в нуль, начинается завершение с двумя стадиями. •
Стадия подготовки — Посылает предварительное сообщение каждому из ресурсов и сообщает ему о завершении транзакции. В этом случае ресурс заботится обо всех протоколах и делает все необходимое, чтобы оставить действие транзакции неизменным. По завершению ресурс сообщает об зтом инициатору транзакции, и сообщает также, успешно ли все было выполнено.
•
Стадия завершения — Как только SQL Server получает сообщения от ресурсов об успешном завершении, каждому ресурсу передается заключительное сообщение о завершении. Если любой из ресурсов объявляет о неудаче, то вся транзакция откатывается.
Во время стадии завершения, если команда получена только одним из ресурсов диспетчера, который потерпел неудачу, ваша работа может нарушиться. Нет никакой возможности бороться с этим. Однако это чрезвычайно маловероятно, поскольку эти команды идут действительно быстро и в течение стадии завершения не должно быть никакой возможности возникновения блокировки — мы уже должны заблокировать элементы, и все, что осталось, — послать заключительную команду завершения.
Различные серверы (распределенные запросы) Я только хочу сделать краткое упоминание о распределенных запросах и представить функции, которые могут использоваться, чтобы установить отношения между двумя SQL Server или SQL Server и источником данных OLE DB или ODBC. Имеются два метода, которые можно использовать:
532
Расширенный доступ к данным и методы корректировки •
Связанные серверы — Мы можем строить связь между двумя серверами, регистрируя имя "server", которое нам затем позволит организовать доступ через имя, состоящее из четырех частей (<имя_сервера>.<БД>.<владелец>.<таблица>) или через интерфейс openquery (открыть запрос).
•
Нерегламентированные связи — Используя функции openrowset (открыть набор строк) или opendatasource (открыть источник данных), мы можем получать таблицу данных из любого источника OLE DB.
Так же как наши транзакции могут охватывать несколько БД, мы можем также иметь транзакции, которые охватывают различные серверы, использующие T-SQL. Если необходимо, мы можем обновить данные в нашем распределенном источнике данных с помощью сервиса DTC, который является частью SQL Server и СОМ+. Мы используем следующую команду: BEGIN DISTRIBUTED TRANSACTION UPDATE externalServer.database.owner.table
COMMIT TRANSACTION
Можно написать наш код почти точно так же, как будто все это было на том же самом сервере. Обратите внимание, что, если бы мы написали BEGIN TRANSACTION вместо BEGIN DISTRIBUTED TRANSACTION, транзакция была бы локальная для этого сервера, и операции, которые касаются других серверов, не будут частью транзакции. Мы не будем входить в детали этих команд, поскольку они могут быть весьма сложными и иметь много различных настроек для многих разных ситуаций. Я редко использовал их при создании OLTP-систем и предпочитаю, когда это возможно, импортировать/дублировать все данные в один и тот же SQL Server — главным образом, с целью увеличения скорости. Связанные серверы часто используются при установлении тиражирования отношений. Для получения большего количества информации относительно распределенных запросов и тиражирования см. SQL SERVER 2000 Books Online или "Professional SQL Server 2000 Programming" Роба Виейра (Wrox Press, ISBN 1-861004-48-6).
Учебный пример Мы теперь готовы начать строить доступ к структурам БД, которые создавали в прошлых двух главах. Для этой части учебного примера мы возьмем три конкретные задачи. Эти задачи позволят другим программистам, работающим с нашей БД, начать писать конкретный код для использования структур: •
Создание основных хранимых процедур корректировки (добавление, обновление, удаление), которые иногда называются CRUD-процедурами (Create, Read, Update, Delete — создание, чтение, обновление, удаление), а также процедур распечатки и заполнения доменов для наших таблиц доменов.
•
Построение специальных хранимых процедур, которые поддерживают важные задачи нашей системы и которые отмечены в диаграмме действий.
Q
Определение набора групп безопасности и определение, какие объекты они должны будут, вероятно, использовать на самом высоком уровне. 533
Глава 12
Основные хранимые процедуры Возвращаясь к нашей диаграмме из главы 10, мы создадим следующие четыре хранимые процедуры для четырех различных таблиц: •
transaction$ins
•
bank$upd
•
payee$del
Q
account$list
Мы создадим также процедуру transactionType$domainFill. Снова обратите внимание, что этот код может быть считан cwww.wrox.com.
Процедура размещения сделки Это, по существу, хранимая процедура размещения, которую мы разработали ранее в этой главе с небольшими дополнительными комментариями. В этой процедуре мы вставляем в таблицу каждое поле и включаем обработку ошибок, чтобы выдать дополнительную информацию, если возникает ошибка. Как обычно, мы должны помещать в скобки имя таблицы t r a n s a c t i o n (сделка), так как это зарезервированное слово. CREATE PROCEDURE t r a n s a c t i o n $ i n s ( @accountld i n t , Onumber v a r c h a r ( 2 0 ) , @date smalldatetime, @description varchar (1000), @amount money, k @signature varchar(100), @payeeld int, StransactionTypeld int, @new_transactionld int = NULL OUTPUT
-- содержит новое значение -- ключа для автоинкрементного -- первичного ключа
AS BEGIN -- параметры обработки ошибок DECLARE @msg varchar(255), -- сообщение об ошибке SmsgTag varchar (255), -- тег сообщения об ошибке @tranName sysname — имя точки сохранения — задание тега сообщения об ошибке и тега транзакции -- (nestlevel - уровень вложенности содержит уникальное значение — среди вызовов к процедуре, если они являются вложенными) SET @msgTag = ' <' + object_narne (@@procld) + ';type=P' SET @tranName - object_name(@@procld) + CAST(@@nestlevel AS varchar (2)) BEGIN TRANSACTION SAVE TRANSACTION StranName INSERT INTO [transaction] (accountld,. number, date, description, amount,
534
Расширенный доступ к данным и методы корректировки
signature, payeeld, transactionTypeld) VALUES(@accountld, @number, @date, @description, @amount, @signature, @payeeld, @transactionTypeId) -- проверка ошибки IF (@@error !- 0) BEGIN -- вначале завершение транзакции, чтобы минимизировать кэш транзакции, -- подвешенной в ожидании завершения сообщения ROLLBACK TRANSACTION SELECT @msg = 'Имеются проблемы с добавлением новой строки в ' + 'таблицу transaction.' + msgTag + ';call= (добавление в transaction)>' RAISERROR 50000 @msg RETURN -100 END — scope_identity ограждает нас от получения начальных одинаковых значений . SET @new transactionld = scope_identity() COMMIT TRANSACTION END
Процедура обновления таблицы bank В процедуре обновления мы просто корректируем поле name (название) таблицы bank (банк) передаваемым значением. Обратите внимание на параметр типа timestamp и на то, как мы используем его, чтобы реализовать оптимистическую блокировку для пользователей этой процедуры. CREATE PROCEDURE b a n k $ u p d ( @key bankld int,
@name v a r c h a r ( 3 8 4 ) , @ts timestamp timestamp
— --—
ключевой столбец, который мы будем использовать как ключ д л я обновления.Заметьте, что м ы не можем корректировать первичный ключ
-- оптимистическая
блокировка
Л5 -- объявление переменных, используемых д л я обработки ошибок в блоках DECLARE @rowcount int, число строк, возвращаемых вызовом Я М Д @error int, код ошибки после вызова Я М Д @msg v a r c h a r ( 2 5 5 ) , сообщение об ошибке @retval i n t , возвращаемое значение о т вызова хранимой процедуры @tranName sysname, имя транзакции @msgTag varchar (255) — тег сообщения об ошибке -- задание тега сообщения об ошибке SET @msgTag = ' < ' + object_name(@@procld) + ';type=P' + ';keyvalue=' + ' @key__bankld:' + CONVERT(varchar(10), @key_bankld)
Продолжение кода на следующей странице 535
Глава 12 SET @tranName = object_name(@@procld) + CAST(@§nestlevel AS varchar(2)) -- удостоверьтесь, что пользователь ввел значение timestamp, -- поскольку будет очень расточительно, если его не использовать IF @ts_timestamp IS NULL BEGIN SET @msg = 'Значение timestamp не должно равняться NULL' + @msgTag + '>' RAISERROR 500 00 @msg RETURN -100 END BEGIN TRANSACTION SAVE TRANSACTION @tranName UPDATE bank SET name = @name WHERE bankld «• @key_bankld AND autoTimestamp = @ts_timestamp — получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error - @@error IF @error != 0 — ошибка возникла за пределами этой процедуры BEGIN ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION SELECT @msg • 'Проблема возникла при корректировке записи в таблице bank.' + SmsgTag + ';call= (добавление в bank)>' RAISERROR 50000 @msg RETURN -100 END ELSE IF @rowcount <> 1 -- это не должно быть первичным ключом -- или запись не существует BEGIN IF (@rowcount = 0) BEGIN -- если запись существует без timestamp, -- она должна быть модифицирована другим пользователем IF EXISTS (SELECT * FROM bank WHERE bankld = @key_bankld) BEGIN SELECT @msg = 'Запись таблицы bank модифицирована' + ' другим пользователем.' END ELSE -- значение первичного ключа не существует BEGIN SELECT @msg = 'Запись в таблице bank не существует.' END END ELSE -- rowcount > 0, слишком много записей модифицировано BEGIN SELECT @rnsg = 'Слишком много записей было модифицировано.' END ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION
536
Расширенный доступ к данным и методы корректировки
SELECT @msg = @msg + @msgTag + ';call= (обновление bank)>' RAISERROR 50000 @msg RETURN -10 0 END COMMIT TRANSACTION RETURN 0
Процедура удаления получателя платежа Удаление получателя платежа с точки зрения кода во многом похоже на обновление. Эта процедура позволяет пользователю удалять запись о получателе платежа из таблицы. ;REATE PROCEDURE payee$del @key__payeeld int, @ts timestamp timestamp = NULL
—
оптимистическая блокировка
AS -- объявление переменных, используемых для обработки ошибок в блоках DECLARE @rowcount int, -- число строк, возвращаемых вызовом ЯМД @error int, — код ошибки после вызова ЯМД @msg varchar (255), -- сообщение об ошибке @retval int, -- возвращаемое значение от вызова -- хранимой процедуры (StranName sysname, — имя транзакции QmsgTag varchar(255) -- тег сообщения об ошибке -- задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld) + ';type=P' + ';keyvalue=' + '@key_payeeld:' + CONVERT(varchar(10), @key_payeeld) SET @tranName = object_name(@@procld) + CAST(@@nestlevel AS varchar(2)) BEGIN TRANSACTION SAVE TRANSACTION @tranName DELETE payee WHERE payeeld = @key_payeeld AND @ts_timestamp = autoTimestamp -- получение числа строк и уровня ошибки для кода управления ошибкой SELECT @rowcount = @@rowcount, @error = @@error IF @error != 0 — ошибка возникла за пределами этой процедуры BEGIN SELECT @msg = 'Проблема возникла при удалении записи из таблицы payee.' + @msgTag + ';call=(удаление из payee)>' ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RAISERROR 50000 @msg RETURN -100 END
Продолжение кода на следующей странице
537
Глава 12 ELSE IF @rowcount > 1
— —
это не должно быть первичным ключом или запись не существует '
BEGIN SELECT @msg = 'Слишком много получателей платежей было удалено. ' + @msgTag + ';call=(удаление из payee)>' ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RAISERROR 50000 @msg RETURN -100 END ELSE IF @rowcount = 0 BEGIN IF EXISTS (SELECT * FROM payee WHERE payeeld = @key_payeeld) BEGIN SELECT @msg = 'Запись таблицы payee модифицирована' + ' другим пользователем.' + ';call=(удаление из payee)>' ROLLBACK TRASACTION @tranName COMMIT TRANSACTION RAISERROR 50000 @msg RETURN -100 END ELSE BEGIN SELECT @msg = 'Запись в таблице payee, которую вы пытаетесь удалить' ' не существует.' + @msgTag + ';call=(удаление из payee)>' RAISERROR 50000 @msg -- Это зависит от потребностей системы, действительно ли вы -- должны фактически реализовать эту ошибку или мы должны -- здесь завершить и вернуть отрицательное значение. Если вы -- пробовали что-то удалить, а оно не существует, - это плохо? END END COMMIT TRANSACTION RETURN 0
Процедура формирования списка счетов Чтобы создать эту процедуру, мы должны включить информацию о счетах и несколько возможных фильтров — в данном случае, number (номер), bankid (идентификатор банка) и bankName (название банка). Когда мы видим список счетов, то можем пожелать видеть также и информацию о банке, поэтому ее и добавляем. CREATE PROCEDURE account$list @accountld int = NULL, @number varchar(20) - M Qbankld int = NULL, SbankName varchar(20) •
538
-- первичный ключ, чтобы получить -- конкретную строку -- так же, как и у account.name
Расширенный доступ к данным и методы корректировки
AS -- так как известное число сообщений будет проблемой для клиентов SET NOCOUNT ON -- значение по умолчанию параметра @number -- величина - NULL IF @number IS NULL SELECT @nuraber = '%'
если переданная
— выделение всех полей (кроме timestamp) из таблицы для просмотра SELECT account.accountld, account.bankld, account.number, bank.name AS bankName FROM dbo.account AS account JOIN dbo.bank AS bank ON account.bankld = bank.bankld WHERE (account.accountld - Oaccountld OR Saccountld IS NULL) AND (account.number LIKE @number) AND (account.bankld = @bankld OR @bankld IS NULL) AND (bank.name LIKE @bankName) ORDER BY account.name RETURN 0
Процедура заполнения типов доменов сделок Процедура заполнения доменов используется для заполнения комбинированного окна типов сделок наподобие формы сделок. Обратите внимание, что мы размещаем название в поле описания даже если в таблице уже имеется поле описания. Это сделано для того, чтобы даже если в таблице нет названия или описания, мы все же возвратили описание, чтобы упростить пользователю кодирование. CREATE PROCEDURE t r a n s a c t i o n T y p e $ d o m a i n F i l l AS BEGIN
-- все процедуры заполнения доменов возвращают одни и те же имена -- полей, так что пользователь может кодировать их единообразно, -- используя общую модель SELECT transactionTypeld AS Id, name AS description FROM transactionType ORDER BY transactionType.name END
Специальные хранимые процедуры Мы сосредоточимся на двух из имеющихся действий и рассмотрим, как можно создатьхранимые процедуры, чтобы поддержать их. Мы рассмотрим: •
баланс счета;
•
получение информации о счете. 539
Глава 12 Действие по выполнению баланса счета Вернувшись к нашей диаграмме действий в главе 5, мы выбрали действие Balance (баланс) и описали его следующим образом: "Позволяет пользователю урегулировать элементы, которые были сообщены банком". Мы не будем писать никакого конкретного кода SQL для этого действия, но рассмотрим эту довольно сложную ситуацию и покажем, как мы решим ее в нашей БД. Чтобы выполнить операции баланса, мы должны будем, вероятно, закодировать следующие хранимые процедуры:
540
•
s t a t e m e n t $ i n s e r t (размещение отчета), s t a t m e n t l t e m $ i n s e r t (размещение элемента отчета) и, возможно, s t a t e m e n t $ i n s e r t l t e m s (размещение элемента в отчете). Так как мы можем загрузить записи s t a t e m e n t Item (элемент отчета) из Интернета, можно бы использовать последнюю процедуру, чтобы загрузить значения во временную таблицу и затем вставлять весь набор сразу. Можно добавить триггер INSTEAD OF, чтобы поместить все неправильные строки в таблицу обработки исключений, когда они восстанавливаются из загруженного файла.
•
t r a n s a c t i o n $ l i s t P o s s i b l e M a t c h e s T o S t a t e m e n t I t e m s (список возможных типов элементов отчета) — эта процедура предназначена для добавления несогласованных сделок к s t a t e m e n t l t e m и дает все точные сделки, основанные на равенстве [ t r a n s a c t i o n ] .number и statementltem.number для чеков и, если возможно, t r a n s a c t i o n T y p e . Мы должны быть уверены, что объединение дает отношение "один к одному" для t r a n s a c t i o n и s t a t e m e n t l t e m .
•
t r a n s a c t i o n $ r e c o n c i l e (согласование со сделкой) — это позволит нам согласовывать t r a n s a c t i o n со s t a t e m e n t l t e m .
•
Пакет хранимых процедур, которые должны скоординировать информацию с экрана и используют t r a n s a c t i o n $ l i s t P o s s i b l e M a t c h e s T o S t a t e m e n t I t e m s , чтобы выполнить запрос.
•
Нам потребовалась бы процедура, наподобие нашей общей процедуры t r a n s a c t i o n $ l i s t , для конкретного счета с единственной целью показать несогласованные элементы.
•
s t a t e m e n t I t e m $ l i s t (список элементов отчета) — это дало бы нам список элементов, которые не должны быть связаны с данной сделкой. Эта и предыдущая процедуры позволили бы нам иметь форму соответствия (снова пользовательский интерфейс), которую мы могли бы использовать, чтобы связать элементы.
•
s t a t e m e n t $ f i n a l i z e R e c o n c i l i a t i o n (завершение согласования отчета) — она размещала бы запись в таблице A c c o u n t R e c o n c i l i a t i o n (согласование счета), чтобы сообщить отчету, что согласование закончено. Нужно бы добавить триггер к таблице отчета, который проверяет, что все кредиты и все дебеты (положительные и отрицательные сделки) в таблице t r a n s a c t i o n соответствуют общей сумме в отчете. Ужесточение требований к системе могло бы позволить автоматически добавлять изменения сделки в таблицу, чтобы балансировать отчет без каких-либо ошибок.
Расширенный доступ к данным и методы корректировки Имейте в виду, что в последнем пункте я добавил новые требования к системе. Эти функциональные возможности должны рассматриваться, как дополнения, и быть обсуждены с менеджером проекта и пользователем. Это может быть особенностью, которая клиенту не нужна, или, возможно, изменит его мнение. В течение процесса реализации системы вы будете думать о новом и охлаждать стремления делать добавления к проекту. Архитектура, которую вы создаете, должна быть достаточно хороша, чтобы поддержать новые особенности, но никогда не добавляйте новые особенности к изделию, которых нет в спецификациях, без того, чтобы не получить согласие клиента (и увеличение вашей оплаты). Как вы можете видеть, это весь список процедур, которые нужно написать. В зависимости от штата кодировщиков, следующий шаг должен был бы, вероятно, дать версии-заглушки этих процедур, чтобы кодировщики пользовательского интерфейса могли продолжить свою работу. Мы также обнаружили несколько дополнительных триггеров, которые должны быть написаны, чтобы охватить таблицы s t a t e m e n t и accountReconcile. Возникновение дополнительных триггеров и требований не есть что-то невероятное — никакой процесс проектирования не совершенен.
Действие по получению информации о счете Мы должны быть способны обеспечить это действие с помощью довольно простой хранимой процедуры, так что напишем ее здесь. Будем считать, что пользовательский интерфейс был определен так, чтобы дать выпадающий список счетов и иметь возможность просматривать текущую информацию, включая: •
текущий баланс (полный дебет, полный кредит);
•
общее количество несогласованных сделок;
•
общее количество сделок;
•
последняя дата отчета;
•
последняя дата согласования.
Итак, м ы напишем следующую процедуру: CREATE PROCEDURE account$getAccountInformation ( @accountld int ) AS -- Отметим, что так как мы создаем таблицу transaction, используя -- зарезервированное слово, мы должны использовать квадратные скобки -- для имени в нашем запросе SELECT account.accountNumber, statement.date, — если эта величина NULL, то отчет -- никогда не был получен accountReconcile.date, -- если эта величина NULL, то отчет -- никогда не был согласован SUM([transaction].amount) AS accountBalance, SUM(CASE WHEN [transaction].amount > 0
Продолжение кода на следующей странице
541
Глава 12 THEN [transaction].amount ELSE 0 END) AS totalCredits, SUM(CASE WHEN [transaction].amount < 0 THEN [transaction].amount ELSE 0 END) AS totalDebits, SUM(CASE WHEN transactionReconcile.transactionReconcileld IS NOT NULL THEN transactionAmount ELSE 0 END) AS unreconciledTotal FROM dbo.account AS account -- счет может не иметь сделок LEFT OUTER JOIN dbo. [transaction] AS [transaction] -- сделка может быть не согласована LEFT OUTER JOIN dbo.transactionReconcile AS transactionReconcile ON [transaction].transactionld = transactionReconcile.transactionld ON account.accountld = [transaction].accountld -- отчет может быть никогда не получен для счета LEFT OUTER JOIN dbo.statement AS statement LEFT OUTER JOIN dbo.accountReconcile AS accountReconcile ON statement.statementld • accountReconcile.statementld ON account.accountld • [transaction].accountld WHERE accountld = @accountld GROUP BY account.accountNumber, statement.date, accountReconcile.date
Безопасность в учебном примере Наконец, мы должны определить нашу безопасность. Наш пример имеет две роли безопасности, и мы, вероятно, будем желать добавить другую роль для целей администрирования: •
AccountUser (основной пользователь счета) — основанная на диаграмме действий, эта роль будет только способна рассматривать информацию о счете. Конечно, потребуется специальная хранимая процедура account$getAccountInformation наряду с любыми поддерживающими хранимыми процедурами для построения формы.
•
AccountingClerk (клерк бухгалтерского учета) — как видно из диаграммы, эта роль будет иметь права на любую определенную в системе процедуру.
•
UserAdministrator (администратор пользователей) — эта роль является членом стандартных ролей БД db_securityadmin и db_accessadmin.
Имеется некоторая двусмысленность в спецификациях, но мы хотим реализовать пользователя, который может просматривать всю информацию о счете —AccountUser. Мы, конечно, должны спросить менеджера проекта и клиента, должны ли некоторые счета быть недоступны некоторым пользователям. Если это так, то мы могли бы добавить систему безопасности на уровне строк, чтобы реализовать это.
542
Расширенный доступ к данным и методы корректировки
Резюме В этой главе мы рассмотрели многие из наиболее важных проблем, которые должны знать при реализации доступа к данным и корректировках в наших приложениях. Мы обсудили все "за" и "против" использования хранимых процедур, нерегламентированных SQL-запросов и представлений. Особое внимание было обращено на то, что можно сделать, чтобы перехватить ошибки — обработка ошибок является одним из слабых мест SQL Server — и посмотрели на реализацию безопасности с помощью ролей пользователей и хранимых процедур. Мы детально рассмотрели транзакции и блокировки, временные таблицы, значения NULL и курсоры. Наконец, сформировали некоторые из основных хранимых процедур, требуемых, чтобы реализовать наш учебный пример БД. Хотелось бы надеяться, что эта глава ответила на многие из ваших вопросов относительно доступа к данным и их корректировки и улучшила ваше понимание важных проблем. Было бы интересно видеть, как эти идеи затрагивают ваши собственные проекты. В следующей главе мы рассмотрим одну из наиболее трудных задач для проектировщика БД — перевод требований к системе в фактические требования к аппаратным средствам.
543
1 mJT
•"
1
J
Определение требований к аппаратным средствам Введение Эта глава включает в себя задачи, которые не играют большую роль в проектировании БД, и поэтому обычно не рассматриваются как сфера деятельности архитектора данных. Однако теперь, когда физически реализован нагл проект, мы имеем некоторые идеи относительно объема данных, аппаратных средств, которые требуются для работы. При рассмотрении этих требований данная глава не представляет собой полное руководство по проектированию аппаратных средств, но скорее как учебник для начинающих по таким вопросам, так как мы не всегда будем иметь роскошь в виде штата администраторов БД, который занимается этим. Конфигурация аппаратных средств, на которых запускается SQL Server, может быть как допустимой, так и недопустимой. Как вы думаете, какие аппаратные средства действительно необходимы вашим приложениям? Как вы думаете, могут ли ваши приложения вырасти до размеров, когда они перегрузят вашу систему? Эта глава даст вам информацию, которая нужна при выборе аппаратных средств, на которых будет размещена БД. Более конкретно, мы посмотрим, сколько места потребуется вашей БД (тема, известная как объемный анализ), и рассмотрим реализацию вашего сервера. Объемный анализ — процесс, в котором мы исследуем типы размещаемых данных и ожидаемую скорость их роста. Анализ типов БД, с которыми вы столкнетесь, необходим. Следуя этому, мы займемся формальным анализом таблиц и постараемся упростить его для огромного большинства случаев. Важное значение имеет архивирование старых данных, которое часто увеличивает свободную область, улучшая функционирование. Реализация сервера включает анализ различных свойств сервера и СУБД.
Глава 13
Типы БД Очевидно, что имеются неисчислимые приложения с множеством БД, которые поддерживают их. Хотя всегда трудно выделить ясные различные направления, обычно БД делятся на два направления: OLTP (On-Line Transaction Processing — обработка транзакций в реальном масштабе времени) и OLAP (On-Line Analytical Processing — аналитическая обработка в реальном масштабе времени). Мы поочередно рассмотрим каждое из них. O L T P OLTP-БД — обычно высоко нормализованные таблицы, которые увеличиваются с помощью многочисленных коротких транзакций. БД кассовых терминалов или банкоматов (ATM — Automatic Telling Machine) банковских счетов — хорошие представители OLTP-БД. Из-за нормализованного характера таблиц размеры строк и использование индексов обычно минимизируются. Основные таблицы в этих БД обычно будут расти с постоянной скоростью X (вычисляемое значение) строк за заданный период времени. Этот период времени может быть часами или днями, или, даже, месяцами или годами. Так как вы хотите собрать требования к хранению данных для OLTP-БД, то должны будете измерить эту скорость роста, предсказывая ее настолько точно, насколько возможно. Некоторые OLTP-БД могут поддерживать больше одного приложения; хорошим примером могут быть приложения бухгалтерского учета, где вы можете иметь отдельную БД для каждой бухгалтерской книги, расчетов кредиторской задолженности и расчетов модулей счетов дебиторов. С другой стороны, по причинам безопасности SQL Server и инфраструктуры таблиц системы, вы будете часто встречать такие приложения, написанные с единственной БД для всех этих модулей. Таким образом, такая БД бухгалтерского учета может иметь поднаборы таблиц, которые поддерживают отдельные бухгалтерские книги, расчеты кредиторской задолженности и расчеты модулей счетов дебиторов. По существу, с точки зрения объемного увеличения эти поднаборы можно рассматривать как отдельные приложения для таблиц и анализировать их отдельно. O L A P OLAP-БД используются для подведения итогов и формирования отчетов на основе данных, формируемых OLTP-приложениями и нереляционными источниками. Возьмем, например, компанию страхования, которая собирает данные требований в одной из своих OLTP-БД. Они могли бы иметь отдельные OLAP-БД, которые суммируют эти данные с целью определения законности требований и формирования отчетов с анализом деловых решений. Некоторые из них будут вызывать эти БД, являющиеся хранилищами рабочих данных, и резервировать OLAP-аббревиатуру для специализированных звездных схем БД, которые обеспечивают дальнейшую детализацию хранилища рабочих данных. Для наших целей мы будем использовать общую аббревиатуру OLAP, когда будем говорить о формировании отчетов в БД, работающих на вершине механизма SQL Server. Данные обычно загружаются в эти БД с помощью дискретных загрузок пакетов во время наименее занятых периодов. Такое использование прежде всего ограничено тем, что подобные системы используются только для чтения, в то время как таблицы обычно денормализованы и часто будут иметь большие размеры строк в попытке уменьшить число соединений, используемых в запросах. Чтобы ускорить выполнение запросов, каждая таблица в OLAP-БД обычно обладает многими индексами. Эти БД увеличиваются на основе 546
Определение требований к аппаратным средствам ожидаемого роста подпитывающих их исходных БД. Однако индексы также должны всегда рассчитываться. Поскольку возникают новые требования к формированию отчетов, будут добавляться новые таблицы и/или новые индексы. Однако, так же как и в жизни, все не только черное и только белое. В то время как большинство БД обычно будет иметь или тип OLTP, или тип OLAP, некоторые будут поддерживать приложения, которые имеют как OLTP-, так и OLAP-компоненты. БД заработной платы, например, обеспечит программа OLTP деловых бухгалтерских записей, связанных с циклическим видом оплаты. В то же самое время, ежеквартальные и ежегодные таблицы отчета OLAP-типа будут часто формироваться из этих данных, чтобы обеспечить государственные налоговые требования. Снова в этом случае OLTP- и OLAP-части БД должны быть проанализированы отдельно.
Рост OLTP-таблиц Можно ожидать, что не все таблицы в OLTP-БД будут расти с одной и той же самой скоростью. Некоторые из них используют ежедневные данные (издательство может ожидать получать новые заказы ежедневно), в то время как для других будет ожидаться менее быстрый рост (мы надеемся получать новых заказчиков, но не с той же скоростью, с какой мы получаем заказы). Рассмотрим эти сценарии более подробно.
Быстрый рост Обычно большинство OLTP-приложений будет иметь от двух до пяти таблиц, которые содержат основную часть данных всей системы. Размер этих таблиц обычно определяет объем, постоянно увеличивающийся в течение времени. Приложение, связанное с закупками, например, будет обычно иметь таблицы PurchaseOrder (заказ закупок) и POLineltem (строка заказа закупки). Для каждого заказа закупок вы будете иметь одну или несколько строк в таблице POLineltem. Во многих приложениях вы могли бы иметь разнообразные самостоятельные части. Например, приложение кадровой системы имело бы набор таблиц, которые, прежде всего, поддерживают сторону приложения, связанную с трудовыми ресурсами, и набор таблиц, касающихся заработной платы. В первом случае таблицы, включающие историю параметров сотрудников, типа обучения, назначений, персональных данных и данных компании охватили бы большую часть данных. Во втором случае таблицы истории, включающие платежи по всему разнообразному списку начисления жалованья, налогов и отчислений от жалованья, внесли бы вклад в основной рост БД.
Медленный рост В дополнение к этим таблицам имеются вторичные таблицы, которые увеличиваются в течение времени, но менее быстро. В приложении, связанном с покупками, это включило бы таблицу доменов, которая охватывает список продавцов, покупателей и допустимых изделий. В приложении, связанном с кадровой системой, это включило бы персонал сотрудников и таблицы общего назначения. Эти таблицы увеличиваются во времени, хотя в более медленном темпе, чем главные операционные таблицы приложения. Часто эти таблицы относительно маленькие по сравнению с основными таблицами, и если грубо оценивать, можно их считать постоянными в период жизни приложения.
547
Глава 13
Отсутствие роста Очевидно, имеются некоторые таблицы доменов, которые обычно заполняются в самом начале и незначительно изменяются в период жизни приложения. Для приложения, связанного с покупками, это охватило бы характеристики заказа закупки, такие как "единица хранения". Для приложения кадровой системы это включало бы такие таблицы доменов как коды должностей и их описания. Эти таблицы могут вообще считаться постоянными через какое-то время после их начальной загрузки. Таким образом, когда вы вычисляете увеличение таблиц, то не должны учитывать влияние времени на размер этих таблиц. Фактически, если период времени для анализа достаточно велик, вы можете часто полностью игнорировать эти таблицы для целей вычисления размера, существенно упрощая задачу. Суммируя все, можно сказать, что отдельное OLTP-приложение будет часто иметь от двадцати до сорока таблиц. От двух до пяти основных таблиц будут расти с постоянной скоростью и будут занимать основную часть требуемого пространства. Некоторые поддерживаемые таблицы доменов будут расти с более медленной скоростью. Другие таблицы доменов первоначально загружаются, и их рост (а часто и сам размер) через какое-то время становится незначительным.
Рост OLAP-таблиц OLAP-таблицы также, как ожидается, будут расти. Этому может быть много причин — будет влиять количество данных, поступаемых от OLTP-БД, или может меняться число предварительно вычисляемых запросов. Рассмотрим эти типы роста.
Рост пакетов OLAP-БД, из-за их часто денормализованного характера, будут иметь меньшее количество таблиц доменов и большее количество итоговых таблиц, которые подвержены росту в зависимости от увеличения пакетов программ. Как правило, OLAP-БД будут иметь одну из двух форм. Большое оперативное хранилище данных будет иметь ряд больших итоговых таблиц с некоторыми поддерживающими таблицами доменов. Если не используется специализированный механизм OLAP, некоторые OLAP-БД будут иметь схему звезды, одну большую итоговую таблицу с лучами, направленными к ряду таблиц доменов. Объемный рост OLAP-БД имеет множество факторов, такие как основные данные и их рост, степень денормализации, необходимая для ускорения запросов, и частота, с которой данные вводятся в БД из источника или источников. Почти весь рост может быть определен как дискретные события. Наиболее очевиден рост БД из-за обычных пакетов программ, поступающих из одного или большего количества источников. Каждый источник должен быть проанализирован на предмет периодичности загрузки пакетов и величины роста при их трансляции в итоговые таблицы OLAP-БД. Не следует это путать с вводом данных в программу из различных источников, обеспечивая денормализацию и формирование итоговых данных, которые часто используются в OLAP-БД. В дополнение к тому, что данные могут быть просто загружены, они будут также получены в итоге выполнения "временных пакетов". Например, таблицы с различными уровнями детализации могли бы быть загружены для периодов, охватывающих квартал, год, предшествующий год и период до предыдущего года. Следует осторожно относиться к оценке фактора потенциального геометрического роста, который может влиять на рост OLAP-БД. 548
Определение требований к аппаратным средствам OLAP-БД широко используются для объединения неоднородных источников данных, чтобы выполнить анализ. Наряду с этими несоизмеримыми источниками, вы должны будете оценить частоты загрузки их несоизмеримых пакетов. Пакеты могут быть от ежедневных до ежеквартальных в зависимости от источника и допустимого времени ожидания в ваших требованиях по формированию отчетов.
Рост в связи с ростом компании Менее очевиден для роста OLAP-БД рост компании. Так как OLAP-БД часто используются для управленческих сообщений, БД должна иметь размер, чтобы не только учесть рост ее первоначальных источников, но также и рост из-за слияния компаний и приобретений родительской компании. Например, медицинская компания страхования могла бы построить OLAP-БД, предусматривающую прибыль и потери передач, учитывая требования от конкретных географических областей. Когда приобретается другая компания страхования, охватывающая отдельную географическую область, сразу же исходная БД должна приспособить данные от этого приобретения, чтобы дать точный отчет о прибыли и потерях для новой, расширенной компании. Здесь, в соответствии с нашими представлениями относительно роста из-за слияния компаний и приобретений, вы могли бы получить хороший результат, рассматривая проектирование структур OLAP-таблиц в довольно общей манере с таблицами, содержащими переменные столбцы. SQL 2000 предоставляет тип данных SQL_variant, и это именно то место, где вы можете воспользоваться преимуществами данного типа. Безусловно, при использовании столбцов переменного размера и столбцов TnnaSQL_variant затрачивается большее количество места, чем в случае использования простого целого числа или большого целого числа для того же самого представления. Кроме того, некоторые функции (AVG, SUM и т. д.) не могут использоваться для столбцов типа SQL v a r i a n t .
"Нам нужно больше отчетов!" Помните также, что OLAP-БД играют жизненно важную роль в получении своевременного и полезного анализа информации основными лицами, принимающими решение в организации. Поскольку данные меняются и принимаются деловые решения, здесь, как нигде, возникает потребность в новых отчетах и информации, которая их обеспечивает. Обеспечение этой потребности часто означает новую обработку данных или новый столбец и/или индекс для существующей таблицы.
Не забывайте об индексах В OLAP-БД влияние индексов на объем не может не учитываться. Отдельная таблица с десятками гигабайт данных может иметь соизмеримое количество данных, необходимых для индексов. Таким образом, поскольку обсуждается вычисление объема таблицы, одинаково важно обсудить и вычисление размеров индекса.
Вычисление полного размера таблицы Как вы можете ожидать, размер таблицы грубо равен произведению числа строк на размер строки. Однако множество факторов увеличивает сложность вычисления. Они включают число и тип индексов, индексные ключевые столбцы, число и процент использования столбцов переменной длины, а также размер текста или рисунка BLOB-данных. 549
Глава 13 Можно использовать следующие шаги, чтобы оценить количество места, требуемого для хранения данных таблицы. 1.
Вычислить область, используемую данными в таблице.
2.
Вычислить область, используемую кластерным индексом, если он существует.
3.
Вычислить область, используемую каждым некластерным индексом, если таковые существуют.
4.
Объединить эти значения вместе.
SQL Server Books Online хорошо объясняет, как таблицы и индексы сортируются по размеру на основе этих параметров. Что сюда не включено, однако, так это программный метод для выполнения утомительной работы по сбору информации относительно размеров столбцов, характеристик столбцов и индексных ключевых столбцов, а затем выполнения вычислений. Мы включили в эту книгу набор из трех хранимых процедур для выполнения данной работы, и вскоре будем с ними работать. Эти три процедуры (объясненные подробно ниже), которые нужны для создания мастера БД, следующие: • • •
sp_table$calcDataSpace (хранимая процедура расчета объема данных таблицы); sp_index$getKeyData (хранимая процедура получения индексных ключевых данных); sp_table$indexSpace (хранимая процедура получения размера индекса таблицы).
Чтобы использовать процедуры, вы сначала должны фактически создать пустые таблицы и индексы с помощью мастера БД. Дополнительно вы должны подумать об ожидаемом размере строк и проценте заполнения столбцов переменной длины. Эти процедуры были разработаны для SQL 7 и SQL 2000 и не будут иметь силу для SQL 6.5.
Вычисление размера данных Сначала мы должны вычислить область, используемую данными таблицы. Для наших целей мы предположим, что вы уже создали таблицу с ее соответствующими столбцами и индексами. Альтернативно вы можете работать, используя действия, как показано в конце главы. Если вы имеете столбцы переменной длины типа varchar, nvarchar и varbinary, то для наших целей вы должны будете сделать оценку процента, на который эти столбцы будут заполнены. Для процедуры sp_table$calcDataSpace, как мы увидим ниже, это — единственное значение для всех столбцов. Это единственное значение для всей таблицы мы будем хранить в переменной Q v a r i F i l l P e r c e n t (процент заполнения столбцов переменной длины).
С добавлением определенных пользователем расширенных свойств (обсужденных в главе 10) в SQL 2000, вы можете теперь создавать свойство p e r c e n t F i l l E x p e c t a t i o n для каждого из ваших столбцов переменной длины, которое программно доступно. Это — только одна из многих идей, когда разработчики и администраторы БД могут использовать эти определяемые пользователем расширенные свойства.
550
Определение требований к аппаратным средствам Размер таблицы определяется числом столбцов, длиной этих столбцов и числом строк. Вначале нам нужна некоторая исходная информация. В этой секции мы рассмотрим подготовительную работу для использования процедуры sp_table$calcDataSpace. Предполагается, что объявления переменных заданы. Размер страницы по умолчанию для SQL 2000 — 8192 байта. Вы можете получать это программным путем, используя следующий запрос: §§
SELECT B p a g e s i z e
= l o w FROM m a s t e r . d b o . s p t _ v a l u e s
WHERE n u m b e r
= 1 AND t y p e
= 'E'
Для целей планирования вам нужно оценить число столбцов, которые ожидаются в таблице. Если вы ходите использовать по умолчанию число уже имеющихся в таблице столбцов, то можете получить это программным путем с помощью следующего запроса: SELECT @rowcount = rows
FROM s y s i n d e x e s
WHERE o b j e c t _ n a m e ( i d ) = @tablename AND i n d i d IN (0, 1)
Далее нам нужно число столбцов переменной длины, размер этих столбцов переменной длины (который является функцией их максимального размера, умноженного на фактор ожидаемого заполнения — @variFillPercent), общее число столбцов и размер фиксированных столбцов. Следующие два запроса возвращают эти результаты, основное различие в запросах заключается в значении переменных столбцов из таблицы s y s t y p e s : SELECT @ v a r i c o l c o u n t = COUNT(с.name), @maxvaricolsize - ROUND((@variFillPercent + ISNULL(SUM(c.length) , 0) FROM syscolumns с JOIN s y s t y p e s t ON c . x u s e r t y p e = t . x u s e r t y p e JOIN s y s o b j e c t s о on c . i d • » o . i d WHERE t . v a r i a b l e
= 1 AND o.name = @tablename
GROUP BY o.name SELECT @columncount = COUNT(c.name) + @ v a r i c o l c o u n t , @ f i x c o l s i z e = ISNULL(SUM(CASE WHEN с l e n g t h IS NULL THEN t . l e n g t h ELSE с l e n g t h END), 0) FROM syscolumns с JOIN s y s t y p e s t ON c . x u s e r t y p e = t . x u s e r t y p e JOIN s y s o b j e c t s
о ON c . i d - o . i d WHERE t . v a r i a b l e
= 0 AND o.name = @tablename
Часть каждой строки резервируется для определения возможности использования NULL в каждом столбце. Она содержит по биту для каждого столбца плюс некоторая дополнительная величина. Эта часть строки называется битовым массивом значений NULL. Нам нужна целая часть рассчитанной величины, и запрос для расчета следующий: S E L E C T
Щ
@ n u l l B i t m a p
= 2 + F L O O R ( ( @ c o l u m n c o u n t
+ 7 )
/ 8 )
Если имеются какие-либо столбцы переменной длины, мы подрегулируем максимальный размер столбца переменной длины для строки, добавляя два байта для таблицы и два байта для каждого столбца переменной длины: Щ
SELECT @maxvaricolsize = @maxvaricolsize + 2 + @varicolcount * 2
Полный размер строки — сумма размера фиксированных столбцов, размера битового массива значений NULL, максимального размера переменных столбцов и заголовка строки данных в четыре байта: |
SELECT @maxrowsize = @fixcolsize
+ @nullbitmap + @maxvaricolsize + 4
551
Глава 13 Затем мы должны разобраться, представляет ли таблица "кучу" строк или организована в соответствии с кластерным индексом. Если последнее верно, то мы должны вычислить количество свободного места между строками из-за того, что фактор заполнения в этом случае меньше чем 100. Каждая таблица всегда имеет строку в параметре sysindexes с индексом Id, равным 0, для таблицы, представляющей собой "кучу" без кластерного индекса, или индексом Id, равным 1, для таблицы, физически сортируемой в соответствии с кластерным индексным ключом. Очевидно, если таблица представляет собой "кучу" без фактора заполнения, нам не нужно определять фактор заполнения и вычислять свободное пространство. Мы получаем фактор заполнения с помощью следующего запроса: IF ((SELECT indid FROM sysindexes WHERE object_name(id) = @tablename AND indid IN (0, 1)) = 1 ) SELECT @fillfactor = origFillFactor FROM sysindexes WHERE object_name(id) = @tablename AND indid = 1
Значение по умолчанию для фактора заполнения — 0; допустимые значения находятся в диапазоне от 0 до 100. Значение фактора заполнения, равное 0, не означает, что страницы заполняются на 0 процентов. Это трактуется подобно фактору заполнения, равному 100, при котором SQL Server создает кластерные индексы с полными страницами данных и некластерные индексы с полными страницами-листьями. Это отличается от 100, так как SQL Server оставляет некоторое свободное место на верхних уровнях дерева индексов. Для целей вычисления свободного пространства данных в таблице фактор заполнения 0 будет идентичен фактору заполнения 100. Если мы имеем таблицу "кучи", то продолжим формулу для вычисления количества свободных строк на страницу, регулируя фактор заполнения до 100. Если мы имеем фактор заполнения таблицы, организованной на основе кластерного индекса, то вычисляем количество свободного места, помещенного в каждую страницу: Jfj SELECT GfreeRowsPerPage = (@pagesize * (100 - @fillfactor) / 100 / (@maxrowsize + 2)) Заметьте, что, если фактор заполнения — 100, вышеупомянутый запрос будет всегда иметь нулевое значение для числа свободных строк на страницу. Это истинно независимо от того, является ли таблица таблицей "кучи" или организованной на основе кластерного индекса с фактором заполнения 0 или 100. Прежде, чем мы рассмотрим следующий запрос для вычисления полного размера данных в таблице, необходимо небольшое разъяснение. Встроенная в этот запрос формула определяет число строк на странице. Оно программно представлено выражением FLOOR ( (@pagesize 96) / (@maxrowsize + 2) ) . Это — место на странице, не используемое указателями на строки заголовками строк — @pagesize — 96, поделенное на размер строки плюс два байта между строками — @maxrowsize + 2. Далее мы используем функцию FLOOR, чтобы округлить частное до ближайшего целого числа, так как строки не могут перекрывать страницы, и полная строка должна находиться на одной странице. Из этого результата мы вычитаем число свободных строк на страницу. На полученное число полезных строк на страницу затем делится оцененное или фактическое число строк, полученное ранее, чтобы получить общее количество страниц, которые нужны для наших данных. Округление, использующее функцию CEILING, необходимо для получения полных страниц. Вы могли бы остановиться в этот момент, если вас интересует только число нужных страниц. Умножение на фактический размер страницы дает число байт, которые нужны для данных.
I 552
SELECT @ d a t a _ s p a c e _ u s e d = @ p a g e s i z e * C E I L I N G ( @ r o w c o u n t / ( ( F L O O R ( ( @ p a g e s i z e - 96) / ( @ m a x r o w s i z e + 2 ) ) ) @freeRowsPerPage))
-
Определение требований к аппаратным средствам Повторное рассмотрение индексируемых структур в виде В-дерева Прежде, чем мы начнем вычислять область для индекса, полезно рассмотреть, как индексы организуются. Индексы, по существу, представляют сокращенные указатели на строки в БД. Все индексы содержат, по крайней мере, один уровень, уровень корневого узла. Это — единственная страница, где начинаются запросы индекса. Если таблица не настолько маленькая, что страница корневого узла является также страницей узла-листа нижнего уровня основания некластерного индекса, корневой узел укажет на следующий уровень в индексе. Это может быть страница уровня узлов индекса или страница одного и только одного уровня узлов-листьев. Каждый уровень узлов индекса указывает на более низкий уровень узлов индекса, пока не будет достигнут уровень узлов-листьев. Как вы можете видеть из рисунка ниже, этот пример индекса имеет три уровня: уровень корневых узлов, единственный уровень узлов индекса и уровень узлов-листьев. Уровень узлов-листьев может принимать одну из трех форм. Если индекс — кластерный индекс, уровень узлов-листьев — фактические данные, отсортированные в соответствии с кластерным индексным ключом. Оставшиеся две формы узлов-листьев — для некластерных индексов и зависят от того, имеет ли таблица кластерный индекс или нет. Если индекс — некластерный индекс таблицы с кластерным индексом, каждая строка уровня узлов-листьев будет содержать указатели на соответствующий кластерный индесный ключ для этой строки. Таким образом, размер каждой строки уровня строк-листьев в этом случае — по существу размер кластерного индексного ключа. Здесь следует сделать краткое упоминание, что это — основная причина для хранения кластерных индексированных ключей, очень коротких. Когда запрос проходит через некластерный индекс такой формы, он должен затем пройти кластерный индекс, чтобы найти фактическую строку. Помните, что чем короче ваш индексированный ключ, тем меньшее количество уровней будет иметь ваш индекс. Третья форма узла-листа — для некластерного индекса у таблицы, не имеющей кластерный индекс (известной также как таблица "кучи"). Каждая строка на уровне строк-листьев является указателем с восемью байтами (четыре байта для указателя страницы и четыре байта для смещения строки на странице) на фактическую строку таблицы.
Страница корневого узла
/ Страницы узлов индекса
/ Страница узла-листа J Страницы узлов-листьев Страницы узлов-листьев
Страницы узлов индекса
Страница узла-листа Страницы узлов-листьев Страницы узлов-листьев
5 5 3
Глава 13 Подытоживая этот раздел, можно сказать: •
Для кластерных индексов страницы уз лов-листьев являются страницами данных.
•
Для некластерных индексов в таблице с кластерным индексом, страницы узлов-листьев содержат указатели на кластерный индексированный ключ.
•
Для некластерных индексов в таблице, не имеющей кластерного индекса, страницы узла-листа содержат указатели на фактические строки таблицы.
Когда индекс имеет меньшее количество уровней узлов, он называется "выгодным". Вы можете видеть, что выгодные индексы означают, что меньшее количество страниц читается при запросе, так как каждый уровень узлов представляет читаемую страницу. Меньшее количество читаемых страниц означает меньшее количество дорогостоящих дисковых операций и меньшее количество циклов ЦП. В целом, выгодные индексы лучше, когда вы должны добраться к данным таблицы, чтобы получить нужную информацию. Концепция рассмотренных индексов дает компромисс, упомянутый в другом месте этой книги.
Вычисление размера индекса Область, занимаемая данными в таблице, — только половина уравнения для получения общей области, используемой таблицей. Другая половина — область, занимаемая индексами. В OLAP-БД неудивительно, когда область, занимаемая индексами, равна или превышает область, используемую фактическими данными. Мы рассмотрим еще две хранимых процедуры, sp_table$indexSpace (хранимая процедура определения области, занимаемой индексом таблицы) и sp_index$getKeyData (хранимая процедура получения ключевых данных индекса). Первая, sp_table$indexSpace, является главной процедурой, в то время как sp_index$getKeyData — вспомогательная процедура, вызываемая первой. Как было отмечено выше при вычислении размера таблицы, мы делаем упрощение, оценивая процент, который будут занимать столбцы переменной длины. Как и выше, предполагается, что переменные объявлены.
Когда вы запускаете процедуру sp_table$indexSpace, в дополнение к полному размеру таблицы вы получите информацию относительно размера одних данных в таблице, размере каждого индекса, и сколько уровней узлов В-деревьев необходимо для данного числа записей в таблице. Мы начнем наш анализ с помощью sp_table$indexSpace, выполнив некоторую вспомогательную работу. Нам понадобятся размер страницы для сервера, число строк таблицы, если оно не задано в качестве входного параметра, и полная область, занимаемая только данными. Запросы получения размера страницы и числа строк были рассмотрены ранее в разделе Вычисление размера данных. Чтобы получить область, используемую фактическими данными, мы вызовем хранимую процедуру sp_table$calcDataSpace, которая рассмотрена выше. Затем мы создаем временную таблицу #keyColumns (ключевые столбцы) со столбцами, содержащими информацию для идентификатора индекса, названия столбца, длины столбца, имеет ли столбец переменную длину, и фактора заполнения индекса с заданным идентификатором. Мы передаем имя этой таблицы хранимой процедуре sp_index$getKeyData, чьей единственной задачей является заполнение таблицы #keyColumns. 554
Определение требований к аппаратным средствам В sp_index$getKoyData курсор определяется идентификатором индекса для каждого индекса независимо от того, является ли он в таблице кластерным или некластерным. Если нет никаких индексов, @@fetchstatus (состояние выборки) для курсора будет меньше нуля, и мы вернемся к главной процедуре, иначе мы пропустим курсор через все идентификаторы индекса. Каждый индекс будет всегда иметь по крайней мере один столбец. Чтобы получить данные этого столбца и поместить их в таблицу #keyColumns (имя которой было передано в процедуру как переменная StempTableName (имя временной таблицы)), мы динамически формируем строку символов и обрабатываем ее. Обработка динамической строки символов — большое достоинство мастера, так что стоит рассмотреть выполнение запроса ниже поподробнее. В переменную @exct_stmt (выполняемый оператор) помещается оператор USE для определения имени БД. Затем мы формируем оператор INSERT для временной таблицы #keyColumns (передаваемой в переменной QtempTableName) и ее столбцов i n d i d (идентификатор индекса), colname (имя столбца), v a r i a b l e (является ли столбцом переменной длины), c o l l e n g t h (длина столбца) и f i l l _ f a c t o r (фактор заполнения). Данные, загруженные в эти столбцы, являются идентификатором индекса в месте расположения курсора (@indid), именем столбца, формируемым функцией INDEX_COL, значением переменной для столбца d a t a t y p e (тип данных) из таблицы systypes (системные типы), длиной столбца из таблицы syscolumns (системные столбцы) и значением фактора заполнения, который также является частью ключа курсора. После того, как мы загружаем строку в переменную @exct_stmt, мы просто выполняем ее внутри набора круглых скобок. SELECT @ e x c t _ s t m t = 'USE ' + quotename(@dbName, ' [ ' ) + ' INSERT ' + quotename(@tempTableName, ' ] ' ) + ( o n d i d , c o l n a m e , v a r i a b l e , collength, fill_factor) SELECT ' + c o n v e r t ( v a r c h a r ( 1 0 ) , @indid) + ' , INDEX_COL(' + q u o t e n a m e ( @ t a b l e n a m e , ' ' " ) c o n v e r t ( v a r c h a r ( 1 0 ) , @indid) + ' , 1),
+ ',
'
+
t.variable, c.length, ' + c o n v e r t ( v a r c h a r (3), @fill_factor) + ' FROM ' + @dbname + '.dbo.syscolumns с JOIN ' + @dbname + ' . d b o . s y s t y p e s t ON c . x u s e r t y p e = t . x u s e r t y p e WHERE c . i d = ' + c o n v e r t ( v a r c h a r (16), @objid) + ' AND c.name = INDEX_COL (' + quotename (@tablename, ' ' " ) + ' , ' + c o n v e r t ( v a r c h a r ( 1 0 ) , @indid) + ' , 1 ) ' EXEC (@exct_stmt)
Если вы не понимаете, как выполнить строки символов, будет полезно рассмотреть эту процедуру более подробно. Откройте код для процедуры в SQL 2000 Query Analyzer. Цветное оформление кода в Query Analyzer будет очень полезно для понимания, что мы делаем. Заметьте, что, так как мы используем функцию INDEX_COL, мы не должны делать запрос к таблице SYSINDEXES. Поверьте нам, что нет нужды вызывать SYSINDEXES, чтобы получить имена столбцов. Функция INDEX_COL более чем достаточная. После получения имени первого столбца мы используем для INDEX_COL поле идентификатора инкрементного ключа, чтобы получить имя следующего столбца. Если имя столбца, возвращаемое функцией INDEX_COL, имеет значение NULL, это означает, что мы закончили рассмотрение столбцов для конкретного индекса, и переходим к следующему индексу в курсоре, если он имеется. Если имя столбца, возвращаемое функцией INDEX_COL, не NULL, мы организуем цикл WHILE, обрабатывая строку символов подобно описанному выше, чтобы поместить следующую строку в #keyColunms. 555
Глава 13 Когда мы используем последний столбец для последнего индекса, то возвращаем управление процедуре sp_table$indexSpace с теперь уже заполненной таблицей #keyColumns. Сначала мы проверяем #keyColumns, чтобы узнать, имеет ли таблица кластерный индекс. Если он есть, мы устанавливаем в нашей процедуре флаг, указывающий, что имеется кластерный индекс. Это важно по двум причинам. Вспомните, что из рассмотрения выше структуры индекса, уровень узлов-листьев для кластерного индекса представляет данные. Мы уже имеем эту информацию из процедуры sp_table$calcDataSpace. Во-вторых, если мы имеем кластерный индекс, каждая строка на уровне листьев любых некластерных индексов будет иметь размер кластерного индексного ключа индекса, а не указателя строки в 8 байтов. Вот как мы проверяем наличие кластерного индекса, чей идентификатор индекса всегда равен 1: Щ IF EXISTS (SELECT 1 FROM #keyColumns WHERE i n d i d = 1) SELECT @clus_flag = 1
Далее мы начинаем перемещаться по дереву в зависимости от условий, вычисляя для уровней, не представляющих листья, размеры кластерных индексов или сразу же продолжаем вычислять размер каждого некластерного индекса. Так как мы можем иметь до 249 некластерных индексов, то устанавливаем курсор для работы с каждым некластерным индексом на основе идентификаторов индексов, размещенных в таблице#keyColumns. За исключением уровня листьев вычисление размеров других уровней для всех индексов одинаково. Мы начинаем вычисление с размера ключа для индекса. В странице индекса длина ключа дает размер строки. Это очень похоже на вычисление выше размера строки с помощью процедуры s p _ t a b l e $ c a l c D a t a S p a c e , и мы не будем рассматривать конкретные детали, однако отметим, что данные в этом случае поступают из сформированной ними таблицы #keyColumns, а не из фактических таблиц системы, как в случае процедуры s p _ t a b l e $ c a l c D a t a S p a c e . Имеются три вида подсчета в процедуре s p _ t a b l e $ i n d e x S p a c e для соответствующих вариантов размера строк кластерного индекса и некластерного индекса в случае таблицы с кластерным индексом и таблицы "кучи" без кластерного индекса. @Fixed_CKey_Size
+ @Variable_CKey_Size
+ @CIndex_Null_Bitmap + 1 + 8
@Fixed_Key_Size
+ @Variable_Key_Size
+ @Index_Null_Bitmap + 1 + @CIndex_Row_Size
@Fixed_Key_Size
+ @Variable_Key_Size
+ @ I n d e x _ N u l l _ B i t m a p+ 1 + 8
Заметьте, что в последних двух случаях для размера строки некластерного индекса различие в суммах определяется различием между размером строки кластерного индекса @CIndex_Row_Size и значением указателя в таблице "кучи" в 8 байтов. Кроме того, заметьте, что в первом случае размер строки кластерного индекса, @CIndex_Row_Size будет всегда, по крайней мере, 10 байтов и, что более вероятно, по крайней мере, 13 байтов в размере. 13 байтов имеют место, когда ваш кластерный индекс — единственный столбец целых чисел в 4 байта. Некоторые думают, что могло бы быть лучше, если не иметь кластерный индекс. Вовсе нет! Когда сделано разумно, кластерный индекс может дать существенное улучшение работы. Что мы должны сделать, так это обеспечить размер ключа кластерного индекса и размер строк настолько малыми, насколько получится, благодаря эффекту влияния размера строки некластерного индекса.
556
Определение требований к аппаратным средствам
Всегда думайте об этом. Уменьшение длины ключа вашего кластерного индекса на один байт приведет к сокращению одного байта для каждой отдельной строки каждой отдельной страницы каждого отдельного уровня каждого отдельного индекса вашей таблицы вне уровня узлов-листьев кластерного индекса. Все складывается. Это означает более простые индексы и более быстрые запросы. После того, как мы имеем длину ключа для индекса, нужно сначала вычислить размер уровня листьев (или в случае кластерного индекса уровня индексов непосредственно выше уровня листьев). Если этот уровень имеет только одну страницу, мы на этом завершаем. Если здесь размер больше, чем одна страница, мы должны постепенно вычислять следующие уровни, пока не получим ситуацию, когда уровень представляет только одну страницу в размере. Этот уровень по определению является уровнем корневых узлов. Для организации цикла расчета страниц уровня индексов здесь используется код цикла WHILE, проверяющий, занимает ли конкретный уровень только одну страницу в размере: SELECT @this_Num_Pages_Level = CEILING((@Num_Rows / @Index_Rows_Per_Page - @Free_Index_Rows_Per_Page)) SELECT @Num_Index_Pages = @this_Num_Pages_Level WHILE @this_Num_Pages_Level > 1 BEGIN SELECT @level_cnt - @level_cnt + 1 SELECT @this_Num_Pages_Level = CEILING(@this_Num_Pages_Level / @NL_Index_Rows_Per_Page) SELECT @Num_Index_Pages = @Num_Index_Pages + @this_Num_Pages_Level END
Сначала мы вычисляем число страниц для текущего уровня —@this_Num_Pages_Level. Мы уже имеем инициализированную переменную @level_cnt со значением 1. Добавляем @this_Num_Pages_Level к полному значению, которое получаем для числа страниц в индексе — @Num_Index_Pages. Если @this_Num_Pages_Level больше единицы, организуем цикл и определяем число страниц для каждого последовательного более высокого уровня индекса, пока не получим корневой уровень, состоящий из единственной страницы. Вычисления для кластерного индекса аналогичны. В данный момент мы определили число страниц, необходимых для каждого индекса, и число уровней В-дерева для каждого индекса. Далее выводим эту информацию и добавляем ее к полному числу страниц, которое получаем для всей таблицы. После завершения работы с индексами добавляем число страниц индексов к числу страниц данных, чтобы установить полный размер таблицы. Очевидно, что через какое-то время в таблицах и индексах данные увеличиваются. Имеется ли какой-либо способ разумно уменьшить число записей в таблице и число уровней в ваших индексах? Мы обсудим это в разделе ниже, посвященном архивированию данных.
Размер загрузки транзакции После вычисления оценок для размеров всех ваших таблиц, нужно также определить размеры загружаемых частей транзакций. Обычно загружаемые части транзакций первоначально задаются в виде процента от размера БД. Этот процент прежде всего определяется ожидаемым уровнем транзакции и скоростью, с которой загруженные части транзакции сбрасываются на диск или на ленту. 557
Глава 13 Для OLTP-БД, где транзакции — хлеб и масло БД, неудивительно видеть размеры загрузки транзакций, превышающие 50 процентов от размера БД. OLAP-БД, с другой стороны, обычно работают с 10 или менее процентами заданной загрузки транзакций. Это вызывает проблему восстановления загруженных частей транзакции, так как возможность восстановления будет влиять на их размер. Для больших загрузок пакетов в SQL SERVER 7.0 часто разрабатывается процедура, куда помещается загруженная часть транзакции в режиме t r u n c a t e log on checkpoint (усеченная загрузка в соответствии с контрольными точками), когда загружается пакет. Разработчики SQL Server 2000 учли этот процесс и добавили опцию Bulk-Logged (массовая загрузка) для процесса загрузки транзакции. Northwind Properties General] Data Files] Transaction Log j Filegroups Options J Permissions] Г* Restrict a c c e s s "•'•• :
M e r g e r s of d b . c w n e : . d b c e a i o t , orsys&*?«rr
: : ' •: •;
Г Sirrgic f p e :
Г"
Read-only
Recovery
••
;
•
Simple
Settings - - •••-•
Bulk-Logged Full
Г ANSI NULL defaul Г~ Recursive triggers W Auto update statistic*
Г" Auto shrink
{• Torn gage detection
Г" U s e g u o t e d identifiers
Compatibility Levei:
—
fs
A,uto c r e a t e statistics
—~~ I Database compatibility level 80
Cancel
Help
Модель восстановления Full (полная) означает, что конкретная транзакция, включая массовые загрузки и операторы SELECT INTO, может быть восстановлена отдельной транзакцией. Она устанавливается для больших загрузок транзакций, особенно, для OLAP-БД, которые имеют большие пакеты транзакций. Модель Bulk-Logged (массовая загрузка) — компромиссная, обычно резервируемая для OLAP-БД. Здесь для массовой загрузки, операторов SELECT INTO и программ массового копирования записываются пакеты загрузки вместо отдельных транзакций. Это может означать, что в случае отказа целые пакеты транзакций должны быть повторены вручную, но достоинством является меньшее число загрузок транзакций. Модель восстановления Simple (простая) аналогична установке БД в t r u n c a t e log on checkpoint (усеченная загрузка в соответствии с контрольными точками) в SQL 6.5 и 7.0. По существу, БД, восстанавливаемая к последней контрольной точке, используется для полной совместимости с предыдущими версиями и не применяется больше в этой модели. Она обычно рекомендуется для малоэффективного оборудования. 558
Определение требований к аппаратным средствам
Так какие же загрузки транзакций достаточны? Обычно начинают с небольших, а диспетчер автоматически увеличивает файлы восстановления транзакций. Мы рекомендуем начать с 20 процентов от ожидаемого размера данных для OLTP-БД и с 5 процентов для OLAP-БД в режиме восстановления Bulk-Logged.
Архивирование данных в случае необходимости Взяв все эти источники данных, можно быстро заметить, что даже в компании умеренного размера объем данных может стать огромным. Это особенно характерно д л я денормализованных OLAP-систем. На ранних стадиях объемного анализа размеров должно быть сделано решение, нужно л и и когда нужно архивировать данные отдельно от основной БД в контейнере хранения, характеризуемом общим термином "автономный": лента, накопитель с однократной записью и многократным считыванием (Write-Once, Read Many — WORM), CD-ROM, DVD ... Хотя данные могут больше и не быть необходимы д л я сообщений или обработки с помощью т р а н з а к ц и й , может быть юридическая и л и распорядительная причина сохранять данные. Вы д о л ж н ы помнить, что в то время к а к эти инструкции очень часто определяют требование относительно времени хранения данных, они редко определяют средства хранения (бумажная твердая к о п и я , хранилище только д л я чтения, в р а щ а ю щ и й с я диск, г л и н я н а я и л и к а м е н н а я табличка...), на которых размещаются данные. Может оказаться очень дорого держать массивные количества данных, в р а щ а ю щ и х с я в течение многих лет, когда они могут быть просто "заскладированы" на носителях, используемых только д л я ч т е н и я . Даже если данные не огромны, и они могут все еще оставаться в Б Д на SQL Server, все-таки может иметь смысл архивировать и х отдельно от ваших основных таблиц. К а к мы отметили ранее в книге, самые большие затраты, которые м ы всегда стремимся минимизировать п р и использовании Б Д , с в я з а н ы с чтением с диска. Ч е м большее количество д а н н ы х , которые м ы д о л ж н ы читать с диска, тем медленнее п р и л о ж е н и е будет казаться конечному пользователю. Если чтение с диска считается медленным, чтение архивированных, автономно х р а н я щ и х с я данных может быть на порядок (или более) медленнее. Общее эмпирическое правило заключается в том, что н и к а к и е данные не д о л ж н ы быть архивированы с диска, если имеется обычная эксплуатационная потребность в доступе к этим д а н н ы м . С другой стороны, если данные больше не являются оперативно необходимы, настоятельно рекомендуется и х заархивировать и л и в другую таблицу, и л и в другую Б Д , и л и н а другой сервер, и л и даже в другую среду. В ы и г р ы ш от запросов с меньшими индексами и (что менее желательно) м е н ь ш и м и таблицами может быть огромным. Но что означает "обычная эксплуатационная потребность"? Акцент здесь должен быть на слове " о б ы ч н а я " . Где к данным обращаются ежемесячно, ежеквартально или даже ежегодно, может оказаться лучше держать их н а жестком диске. Необычные события подпадали бы под рубрику запросов информационно-поисковой системы, юридических исков и приобретений компании. Такие необычные события возникают редко. Когда они происходят, расход времени на загрузку данных н а жесткий диск и л и в ОЗУ с автономного устройства хранения и настройка запросов, которая может понадобиться, — обычно приемлемые расходы СУБД и А Б Д (администратора Б Д ) .
19 1868
559
Глава 13
Стоимость архивирования Подобно всем усовершенствованиям в работе каждая оптимизация должна быть проанализирована с точки зрения выгоды стоимости. В то время как выгоды включают увеличенную производительность, затраты на архивирование в основном тройные. Во-первых, это стоимость подсистемы архивирования и ее физическая реализация. Прежде всего она включает непосредственную стоимость, например, специализированного накопителя с однократной записью и многократным считыванием. Дополнительно, инженеры, обслуживающие сервер, должны получить, а затем поддерживать навыки для его реализации и обслуживания. Мы не должны превратно истолковывать слово "обслуживать" в предыдущем предложении. Все мы знаем, что быстрый темп изменений в программном обеспечении определяется темпом усовершенствования аппаратных средств ЭВМ. Если вы программист с большим стажем вроде меня, то можете помнить, что чуть больше десятилетия назад мы крутились вокруг процессоров TRS-80 и 51/4-Д1°ймовых (или еще хуже, 8-дюймовых) дискет. Подумайте, что требуется, чтобы загрузить и прочитать 8-дюймовые дискеты сегодня. Это даже не входит в программное обеспечение форматирования дисков современных ОС, с помощью которого оно было сохранено. Вообразите, что требуется хранить данные в течение 25 лет. Вы должны посчитать "затраты", связанные с возможностью прочитать данные древнего формата или с периодическим переформатированием данных со старых носителей на более современные. Когда физическое устройство хранения архива существует, дополнительные и существенные затраты могут быть в связи с аккуратным удалением данных из активных БД реального времени. Как никогда, необходим некоторый детальный анализ. Боже упаси архивировать деловые данные, которые являются оперативно необходимыми! Стоимость и время этого анализа должны быть учтены в вашем проекте.
Архивирование данных не означает, что мы их просто удаляем; вы все еще должны быть способны использовать их.
Наконец, имеется стоимость проектирования кода, необходимого для доступа к архивным данным. Может потребоваться необычная, особенная потребность в данных. Проектировщик архива должен учесть и подготовить все для таких необычных потребностей.
Детали архивирования Теперь, когда вы полностью убеждены в необходимости архивирования, то должны разобраться с деталями. Очевидно, что каждая БД в зависимости от параметров должна иметь разные критерии, которые я ранее сформулировал. Однако обычно нужно начинать думать относительно архивирования, рассматривая следующие моменты.
560
•
Период архивирования.
•
Архивирование таблиц фактов с разделением по датам.
•
Архивирование при завершении сделок.
•
Доступ к архивированным данным.
Определение требований к аппаратным средствам
Период архивирования Вкратце, мы должны начать с обсуждения концепции таблиц фактов. Таблицы фактов обычно представляют собой самые большие таблицы в БД, потому что они содержат подробные данные для основных транзакций. Наиболее очевидный метод архивирования заключается в удалении старых по времени данных из таблиц фактов и размещении в параллельную структуру таблиц с архивированными данными. Хотя обычно непосредственно ясно, как, например, в случае "Мы архивируем все данные трехлетней давности", некоторые моменты могут быть не столь очевидными. Иногда таблицы фактов могут иметь несколько связанных с ними дат. Например, запись деталей заказа закупки может иметь дату заказа закупки, когда он был начат, многократные даты одобрения на различных уровнях согласующих инстанций, дату, когда заказ закупки был представлен продавцу, дату подтверждения продавцом получения заказа закупки, дату или даты, когда заказ был выполнен, и дату, что сделка закупки была, наконец, закончена. Неудивительно, если проходит шесть месяцев или год, прежде чем некоторые заказы закупки будут наконец выполнены, особенно при организации контрактов. Так как же вы хотите архивировать — на основе даты, когда заказу закупки был назначен номер, или на основе даты, когда заказ закупки был наконец выполнен? Это — работа квалифицированного аналитика данных, чья работа хорошо оплачивается. И этим специалистом могли бы быть вы. А что сказать относительно таблиц доменов, которые поддерживают таблицы фактов? Возьмем таблицу vendor (продавец), которая дает столбец vendor_id (идентификатор продавца) для записи деталей заказа закупки. Обычно вы будете иметь внешний ключ в записи заказа закупки, ссылающейся на первичный ключ в столбце таблицы vendor. Возьмем случай, когда продавец ушел из бизнеса. Теперь, тремя годами позже, вы должны заархивировать все строки, которые этот ключ записи продавца поддерживался вашими строками заказов закупок. Совершенно неожиданно, вы больше не можете программно обеспечить наличие первичных ключей с помощью строки, содержащей внешний ключ. Вы должны думать о таких вещах, поскольку собираетесь осуществлять архивирование. Как все мы знаем, хорошие программисты заставят это работать: лучшие программисты выполнят это удаление корректно.
Архивирование таблиц фактов с разделением по датам Многие из данных, которые мы помещаем в наши БД, легко делятся по значениям дат. Например, сделки в банкоматах имеют очень точную дату, когда они были проведены. Далее, как только они были зарегистрированы, данные по существу используются только для чтения. Вместо возвращения и удаления спорной сделки, позже вводится другая сделка для корректировки. Таким образом, легко видеть, как все сделки за конкретный месяц вводились в течение месяца в конкретную таблицу. Например, таблица АТМ_200104 (или фактически подмножество датированных таблиц со ссылочной целостностью, которые на самом деле охватили бы все сделки банкоматов) могла бы содержать все сделки банкоматов в течение апреля в 2001 году. Как только апрель закончится, больше сделки не будут поступать в эту таблицу. После формирования счетов за апрель 2001 года и итоговых таблиц для OLAP-анализа эта таблица — явный кандидат на архивирование на носителе только для чтения. Такие огромные таблицы фактов обычно сгружаются на ленту, CD-ROM или DVD для нечастых обращений. Теперь, зная это, вы можете понять, почему банк запрашивает у вас изрядную плату за копию предыдущего ежемесячного отчета. В некоторых случаях компании просто откажутся формировать дубликаты записей с деталями из этих таблиц фактов (за исключением требований суда). 561
Глава 13
Архивирование таблиц фактов в соответствии с характеристиками Подобно архивированию таблиц фактов с разделением по датам конкретные строки могут быть также архивированы из текущих таблиц в соответствии с конкретным набором условий для строки. В этом случае мы не отключаем всю таблицу. Скорее, мы отбираем конкретные строки и группы строк из таблиц на основе заданных критериев. Хорошим примером могла бы быть сделка, завершенная больше чем два года назад. Очевидно, когда сделка открыта и в нее помещаются записи деталей, вы вряд ли захотите ее архивировать. Даже после того, как она недавно закрыта, вы вряд ли захотели бы архивировать сделку, потому что она все еще используется для формирования отчетов и анализа на данный год и за весь год. В зависимости от ваших бизнес-правил, тем не менее, сделки более чем двухгодичной давности могли бы быть главной целью архивирования. Другим примером строки, которая могла бы быть архивирована на основе заданного критерия, будет строка, представляющая потенциального заказчика, который никогда не был реализован. Вы могли бы иметь таблицу заказчиков, внося в список всех ваших текущих, прошлых и предполагаемых заказчиков. Вы используете эту таблицу не только, чтобы записывать деятельность заказчика, но также и как инструмент формирования продаж для отправок по почте и прогнозов. Обычно потенциальные заказчики помещаются в таблицы в виде списков, которые ваша компания покупает у брокера списков. Многие из этих потенциальных заказчиков станут заказчиками. Многие не станут. Периодически вы хотели бы сокращать этот список, чтобы делать ваши запросы более легкими. Вы определяете критерии для архивирования потенциального заказчика как "негодный материал". Такими критериями могли бы быть "никогда не был активным заказчиком" и "мы вносили его в список в течение трех лет без каких-либо действий с его стороны". Однако помните, что вы заплатили хорошие деньги за этот список. Вы не просто удаляете этих потенциальных заказчиков. Они помещаются в расширяющуюся архивную таблицу негодного материала или таблицы на носителях, используемых только для чтения. Кто знает? Может понадобиться вернуть к жизни этот негодный материал и вернуть строку назад в категорию активных заказчиков. Или вы можете создать такой список, который можете продать другой компании и возместить часть стоимости первоначального списка. Если бы вы удалили данные, то вынуждены были бы выполнить дорогой процесс ручного восстановления данных. Может быть намного дешевле сделать относительно медленный, но дешевый запрос к вашей архивной таблице. При проектировании БД тот момент, когда вы начинаете думать относительно размещения большого количества данных в таблицу, является одновременно и моментом, когда вы должны начать думать относительно архивирования данных из таблицы. Важно заметить следующее: архивирование не должно быть запоздалой мыслью при проектировании БД и приложения. Архивирование может представлять существенную область преимуществ во время функционирования системы. Если, однако, ваша структура таблиц не позволяет поддерживать архивирование, или приложения не были разработаны так, чтобы иметь доступ к архивным данным, вы можете этого и не получить. Будет очень трудно продать заказчику идею относительно удаления данных из его БД, если требуется оплатить стоимость перепроектирования приложения или БД и дать вам большее количество денег, чтобы сделать это, как только исходное приложение начнет работать. Еще хуже, когда ваше приложение начинает задыхаться от данных, которые в противном случае должны быть архивированы, и заказчик в этом обвинит вас. 562
Определение требований к аппаратным средствам
Доступ к архивированным данным Итак, вы теперь тщательно рассмотрели и забрали данные из ваших больших таблиц фактов или удалили таблицы фактов полностью от активного использования в случае таблиц фактов с разделением по датам. Два вопроса приходят на ум: как вы вернетесь к этим данным, когда они вам понадобятся? И если вы сгрузили данные на альтернативные носители, что является сроком их годности? Относительно первого вопроса можно сказать, что приложение часто будет создано так, что оно будет знать о наличии архива, вставляя маленькую строку в активную таблицу, являющуюся ключом к архивированной строке. Когда пользователь запрашивает эту архивную строку, процесс заменяет ее в запросе. Это может включать простой запрос к другой таблице, дисковод, загружающий CD-ROM, сообщение, посланное дежурному компьютерной комнаты, чтобы установить ленту, или сообщение, посланное назад исходному пользователю, запрашивающее его, если он реальный пользователь, действительно ли ему нужны эти данные. Многое в этом процессе зависит от того, как данные архивированы. Если архивированные данные все еще в БД, но только перемещены в другое место (так, чтобы запросы основной таблицы фактов были быстрее и индексы проще), то получение данных будет почти незаметно для пользователей. Другой крайностью является случай, когда архивированные данные размещены на магнитных лентах, которые должны быть установлены вручную, и конечный пользователь может решить, что они на самом деле ему не нужны. Второй вопрос, который нужно иметь в виду при архивировании данных, ставит задачу хранения данных на альтернативных носителях и их срока годности. Все чаще при архивировании данных они помещаются или на CD-ROM или на DVD-ROM. А что, если они помещены на магнитную ленту? Мы недавно натолкнулись на историю, связанную с Национальной воздухоплавательной и космической администрацией (NASA) в Соединенных Штатах. Спутники NASA обычно разгружают информацию на ленту для последующего изучения и анализа учеными в финансируемых NASA лабораториях. Количество данных потрясающее. Оказывается, что эта лента имеет срок годности 10 лет. Также оказывается, что, если взять лентопротяжные устройства NASA, которые сегодня являются пригодными к эксплуатации, то чтобы начать перекопирование этих данных на более надежные и более долговечные средства информации, потребуется значительно больше 10 лет. Короче говоря, будьте осторожны в выборе, где вы будете хранить ваши архивированные данные, если вам потребуется использовать их позже.
Характеристики сервера Мы могли бы представить сложный анализ поломок памяти, чтобы точно определить, какие части ОЗУ занимаются какими частями SQL Server. Эта тема, однако, исчерпана другими томами, особенно "Inside Microsoft SQL Server 2000" (Microsoft Press, ISBN 0735609985). Мы ограничим наш анализ кратким обзором, подчеркивая, где вы можете получить самый большой выигрыш в характеристиках.
Подсистемы памяти Короче говоря, если имеются какие-либо деньги, оставленные в бюджете для ваших аппаратных средств, первое правило в настройке характеристик SQL Server — использовать эти деньги на ОЗУ. SQL Server процветает на хорошем ОЗУ. Если мы углубимся в возможности улучшения функционирования дисковых подсистем, то можно сделать вывод, что они определяются величиной ОЗУ, которое дает максимальный эффект больше, чем у дисковых подсистем. 563
Глава 13 По этой причине вы должны серьезно рассматривать получение максимального объема ОЗУ, которое может обеспечивать ваша материнская плата. Если вы не делаете так, удостоверьтесь, что покупаете чипы ОЗУ, которые являются максимальными для каждого разъема для установки ОЗУ на материнской плате, получая самый большой возможный размер модуля памяти для каждого разъема, поскольку это позволит более легко обновить любую будущую память. Например, вы покупаете сервер, который имеет 8 гнезд для подключения ОЗУ с возможностью до 512 Мб для каждого разъема при общем максимуме в 4 Гб. Вы проверили бюджет и можете обосновать, по мнению вашего босса, только 2 Гб. Имеются два разных пути, которыми ваш продавец сервера может выполнить это требование. Более дешевый путь — с 8 микросхемами ОЗУ по 256 Мб каждая. Лучший путь и цена, которую вы хотите представить вашему боссу, — 4 микросхемы ОЗУ по 512 Мб каждая, оставляя четыре пустых разъема. Небольшая переплата, которую вы делаете с этой конфигурацией, будет меньше, чем то, сколько нужно будет уплатить, когда требования ваших данных расширятся, и вы должны будете покупать эти дополнительные 2 Гб.
Подсистемы памяти на сервере Windows NT Говоря о 4 Гб ОЗУ, мы имеем в виду некоторые ограничения для ОС, на которых работает SQL Server. Хотя SQL Server будет работать на системах Windows 9x/ME, мы ограничимся только рассмотрением SQL Server на серии Windows NT/2000. Важно обратить внимание, что в семействе ОС Windows 9x ОС Windows NT и Windows 2000 имеют различные проекты виртуальной памяти, поэтому алгоритмы, используемые для оптимизации SQL Server, различны. Сервер Windows NT имеет две разновидности — Standard edition (базовая версия) и Enterprise edition (версия для предприятий). В то время как обе версии Windows NT поддерживают максимум 4 Гб ОЗУ, они делят эти 4 Гб по-разному. Standard edition поддерживает максимум 2 Гб памяти, отведенной любому процессу. Эта отведенная память включает и ОЗУ, и виртуальную память, где размещена часть жесткого диска, чтобы действовать подобно ОЗУ, хотя очень, очень медленно. Таким образом, даже при том, что вы могли бы иметь 4 Гб ОЗУ на вашем сервере, процесс обслуживания, который начинается в программе начального запуска ОС, в которой работает SQL Server, будет способен использовать только 2 Гб этой памяти. Другие 2 Гб памяти будут отложены для ОС. Enterprise edition поднимает этот уровень до 3 Гб, обеспечивая по существу 50-процентную надбавку к характеристике. Это сделано с помощью переключателя BOOT.INI.
Подсистемы памяти на сервере Windows 2000 Архитектура памяти Windows 2000 немного более сложная. Для начала отметим, что теперь имеются три разновидности ОС: Standard (стандартная), Advanced (расширенная) и Data Center (информационный центр). Standard во многом подобна стандартной версии Windows NT. Она будет поддерживать 4 Гб ОЗУ с максимумом до 2 Гб для любого отдельного процесса. Windows 2000 Advanced повышает объем ОЗУ до 8 Гб. Это обеспечивается двумя различными способами. Первый называется настройкой памяти приложения (Application Memory Tuning) или 4-гигабайтовой настройкой (4-gigabyte-tuning — 4GT). Настройка 4GT очень похожа на то, как NT Enterprise обращается с памятью. Она используется для приложений, которые запускаются на сервере и требуют от 2 до 4 Гб ОЗУ, и имеет максимальное ограничение процесса в 3 Гб. 564
Определение требований к аппаратным средствам Чтобы обеспечить полное использование 8 Гб ОЗУ на сервере Windows 2000 Advanced, вы должны будете использовать расширение физического адреса (Physical Address Extension — РАЕ) Х86. Требования к аппаратным средствам для РАЕ Х86 более строгие, чем для 4GT. Чтобы определить, поддерживают ли это ваши аппаратные средства, следует проконсультироваться в Windows 2000 Hardware Compatibility List (список совместимости аппаратных средств Windows 2000) в http://www.microsoft.conn/winclows2000/upgrade/compat/, разыскивая ключевые слова Large Memory (большая память). Технология
Требования к аппаратным средствам
Расширение физического адреса (РАЕ) Х86
•
Процессор Pentium Pro или более поздние
•
4 гигабайта или больше ОЗУ
•
450 NX или совместимый набор микросхем и их поддержка или более поздний
• •
Intel-совместимый процессор
Настройка памяти приложения, также называемая 4-гигабайтовой настройкой (4GT)
2 гигабайта или больше ОЗУ
При реализации РАЕ Х86 оба SQL, Server 7 и SQL Server 2000 Enterprise Editions используют Microsoft Windows 2000 Address Windowing Extensions (AWE — адресные оконные расширения), чтобы адресовать приблизительно 8 Гб памяти. В SQL 2000 это относится к одному экземпляру, так как он поддерживает больше чем один экземпляр SQL Server. Однако для каждого экземпляра, использующего эту расширенную память, нужно разместить его память вручную вместо того, чтобы разрешить SQL Server управлять этим, автоматически используя свои динамические программы управления памятью. Windows 2000 Data Center является наиболее амбициозной ОС, которую фирма Microsoft когда-либо поставляла на рынок. В дополнение к поддержке до 32 процессоров, использованию РАЕ Х86, она может поддерживать 64 Гб ОЗУ. Может быть установлена только SQL 2000 Enterprise Edition, чтобы работать в этом оборудовании. Дополнительно сервер ОС Windows 2000 Data Server будет загружать только допустимую конфигурацию аппаратных средств. Допустимые конфигурации аппаратных средств подходят для программы Windows Data Center и поставляются основными продавцами типа Compaq, Dell, Unisys, IBM и Hewlett-Packard. Unisys, например, поставляет на рынок ES7000 с 32-м ЦП и до 64 Гб ОЗУ. Ее дисковая подсистема ввода/вывода поддерживает до 96 каналов.
Управление работой памяти Теперь, когда вы знаете ограничения на память в SQL Server, наложенные ОС, что вы можете сделать, чтобы оптимизировать эту память и определить, нужно ли ее еще? То, чем мы будем сейчас заниматься, обычно называется искусством управления работой.
565
Глава 13 Обычно мы будем сосредотачивать наши усилия на настройке механизмов SQL Server 7 и SQL Server 2000. Эти механизмы имеют принципиальные усовершенствования по сравнению с SQL Server 6.5 в алгоритмах управления памятью. В отличие от SQL Server 6.5, они динамически выделяют и освобождают память в случае необходимости. Обычно нет необходимости определять администратору, сколько памяти следует разместить для серверов SQL Server 7 или SQL Server 2000, хотя опция все еще существует и требуется в некоторых конфигурациях. Из-за динамической природы памяти в SQL Server 7 и SQL Server 2000, фирма Microsoft удалила поддержку для одного из наиболее полезных средств, доступного в SQL Server 6.5 — DBCC MEMUSAGE. Теперь, чтобы получить эту информацию, вы должны контролировать разнообразные измерители характеристик в объекте Buffer Manager (менеджер буфера) и объекте Cach Manager (менеджер кэша), так как информация больше не отображается статически в таблице, наподобие sysconf i g u r e s (системные конфигурации). Хотя этот удобный инструмент теперь внесен в список как "Не поддерживаемый" и больше не возвращает разбиение использования памяти, попытайтесь запустить его. Вы увидите, что этот "выстрел навскидку" продолжает возвращать верхние 20 элементов списка буферированных таблиц и индексов. Это может быть очень удобный список. При анализе характеристик конкретного приложения в испытательной конфигурации это может быть неоценимо.
Искусство управления работой Управление работой является искусством, и в этом смысле является комбинацией таланта, опыта, знания, и иногда только простой удачей. Как вы думаете, можете ли вы это сделать? Вы должны пробовать, пробовать, и снова пробовать. Настойчиво делайте это, специально изучайте это. Держите монитор постоянно включенным для обслуживания вашего сервера. Имеются некоторые хорошие руководящие принципы для начала.
566
1.
Удостоверьтесь, что вы запустили ваш типичный процесс (SQL Server) и загрузили его работой (запросы и хранимые процедуры) в течение вашего управления.
2.
Только не осуществляйте управление вашими серверами в реальном масштабе времени. Выделите долго протекающие загрузки. В Windows NT установите сервис Datalog/Monitor из NT Resource Kit; эти функциональные возможности являются новыми в Windows 2000.
3.
Всегда держите счетчики диска включенными, запускаемыми от команды, подтверждающей команду DISKPERF — У, и затем перезагружайте их. Когда в производственном оборудовании непроизводительные издержки минимальны, последнее, что вам нужно сделать в середине коллизии, когда необходимы счетчики логических и физических дисков — необходимо их перегрузить.
4.
Для ежедневного контроля программы на рабочем столе выводите окно с диаграммой с интервалом в 18 секунд. И в Windows NT PerfMon, и в Windows 2000 ММС SysMon, это даст вам окно в течение 30 минут. По моему мнению, это является идеальным интервалом и с точки зрения наблюдения прошлого, и с точки зрения уменьшения воздействия на сервер.
5.
Используйте SQL Profiler для отдельных запросов и процессов в сочетании с PerfMon или SysMon, чтобы получить хорошую картину влияния отдельных запросов.
Определение требований к аппаратным средствам
6.
Знайте терминологию управления работой. Объекты — это списки конкретной доступной статистики. Примером может быть объект Processor (процессор). Счетчик — отдельная статистика, которая находится под управлением объекта. Примером является счетчик % Processor Time (процент времени, отведенного процессу) в объекте Processor. Экземпляр — дальнейшее разбиение статистики счетчиков на отдельные компоненты. Не все счетчики будут иметь отдельные экземпляры. Счетчик % Processor Time имеет экземпляры для каждого процессора, а экземпляр a_Total (общее время) содержит суммарную активность всех процессоров.
7.
Знайте ваши средства. Хотя вы можете и знать, как установить диаграмму в PerfMon, изучите, как задать загрузку с Datalog или Performance Log. Другие средства, с которыми нужно познакомиться — DBCC MEMUSAGE, Task Manager и SQL Enterprise Manager Current Activity.
о.
Не бойтесь экспериментировать. Back Office Resource Kit имеет средства для организации проверки данных (DataSim), создания испытательных БД (DBGen) и моделирования загрузки от многих клиентов (SqlLS).
Управление работой SQL и узкие места Узкие места возникают, когда ресурсы аппаратных средств не могут поддерживать требования программного обеспечения. Например, когда процесс программного обеспечения или комбинация процессов требуют больший объем операций ввода/вывода с диска, чем диск может физически обеспечить, на диске возникает узкое место. Когда подсистема ЦП становится слишком насыщенной и процессы ожидают своей очереди, возникает узкое место. Узкие места обычно фиксируются одним из двух способов. Первый должен определить ограничивающие аппаратные средства и увеличить их способности. Другими словами, возьмите более быстрый накопитель на жестких дисках или увеличьте скорость ЦП. Второй путь состоит в том, чтобы вынудить процессы программного обеспечения использовать аппаратные средства более эффективно. Это могло бы быть сделано, помещая индекс таблицы так, чтобы или необходимость ввода/вывода с диска для обслуживания запроса была уменьшена, или число элементов ЦП, необходимых для обработки объединения, уменьшилось. Следующие пять ключевых областей следует контролировать при прослеживании работы сервера и определении узких мест. Каждый кандидат на узкое место будет изменять работу рассматриваемых контролируемых объектов и счетчиков. •
Q
Использование памяти — SQL Server нуждается в памяти и для себя, и для ОС. Если SQL Server имеет достаточно памяти, но ОС испытывает недостаток памяти так, что она вынуждена часто перекачивать страницы файла на диск, вся работа будет существенно страдать от этого. Использование ЦП — Большая загрузка ЦП указывает, что подсистема обладает недостаточной мощностью. Решением могли бы быть модернизация ЦП или увеличение числа процессоров.
•
Организация операций ввода/вывода с диском — Неспособность диска или контроллера диска удовлетворять требованиям чтения или записи надлежащим образом сильно воздействует на функционирование.
•
Соединения пользователя — Ненадлежащим образом задание ряда соединений может отобрать память, в противном случае доступную для SQL Server.
567
Глава 13 Q
Блокировки — Один процесс препятствует другому процессу вызывать или обновлять данные. Это особенно важно для пользователей и может быть причиной некоторых из ваших наиболее серьезных проблем функционирования с точки зрения пользователя.
Настройка памяти: ОС и SQL Server Начните ваш интенсивный анализ памяти, рассматривая два противоположных счетчика: •
Memory: A v a i l a b l e Bytes (память: доступные байты);
•
Memory: Pages F a u l t s / s e c (память: число ошибок в секунду, связанных с отсутствием страниц).
Счетчик A v a i l a b l e Bytes сообщает, сколько памяти доступно для использования процессами. Счетчик Pages F a u l t s / s e c сообщает нам число ошибок обращения к страницам на жестком диске, которые должны быть восстановлены с жесткого диска, так как они не были в рабочей памяти. Он также включает число страниц, записываемых на жесткий диск, чтобы освободить место в рабочей области для поддержания ошибок страниц на жестком диске. Малое число A v a i l a b l e Bytes указывает, что может не быть достаточной доступной памяти или что процессы, включенные в SQL Server, не могут освободить память. Большое число Pages F a u l t s / s e c указывает на чрезмерную страничную подкачку информации. Рассматривая глубже отдельные экземпляры Process : Page F a u l t s / s e c , можно сделать заключение, что процесс SQL Server, например, имеет чрезмерное разбиение на страницы. Низкая скорость Pages F a u l t s / s e c (обычно 5-И0 в секунду) нормальна, поскольку ОС продолжит делать свое дело, выполняя набор работ. Как раньше было отмечено, начиная с SQL Server 7, память по умолчанию настраивается автоматически. Тем не менее, обычно вы хотите дать SQL столько выделенной памяти, сколько возможно. Это главным образом зависит того, как другие приложения могут запускаться на сервере. Используя хранимую процедуру sp_c on f i g u r e (хранимая процедура конфигурирования), вы можете устанавливать нужные значения в MIN SERVER MEMORY (минимальная память сервера) и MAX SERVER MEMORY (максимальная память сервера). Если SQL Server — единственное приложение на сервере, задайте параметрам MIN SERVER MEMORY и MAX SERVER MEMORY одно и то же значение. Если SQL Server сосуществует с одним или большим количеством приложений, уменьшайте MIN SERVER MEMORY до значений, обеспечивающих требования других приложений. Если другое приложение не в состоянии начать работать должным образом, это может произойти потому, что SQL Server вынужден работать вблизи установки MAX SERVER MEMORY и медленно освобождает память для нового приложения, испытывающего недостаток памяти. В этом случае понизьте значение МАХ SERVER MEMORY. Очевидно, значение MAX SERVER MEMORY всегда должно быть больше или равняться MIN SERVER MEMORY.
568
Определение требований к аппаратным средствам
Если вы установили и запускаете поддержку Full-Text Search — полнотекстовый поиск (Microsoft Search service — сервис поиска фирмы Microsoft — также известный как MS Search), то вы должны установить опцию MAX SERVER MEMORY, оставив достаточно памяти для запуска сервиса MSSearch. Microsoft дает здесь удобную формулу: полная виртуальная память — (максимальная память для SQL Server + виртуальная память для других процессов) = 1.5 * физическая память сервера. Как только мы настроили память SQL Server, хорошо решить, хотите ли вы, чтобы SQL Server 7/2000 настраивал память процессов автоматически или имел набор значений для конфигурации. Для лучшего функционирования можно заблокировать количество рабочей памяти, которую зарезервировал SQL Server. Компромисс здесь заключается в том, что можно получить сообщение "недостаток памяти" для других приложений на том же самом сервере. Если вы решаете зафиксировать количество рабочей памяти, необходимы две настройки конфигурации. Сначала уравняйте настройки MIN SERVER MEMORY и MAX SERVER MEMORY. Затем включите флаг конфигурации SET WORKING SET SIZE (включение установки размера), использующий sp_conf i g u r e . Значение MAX SERVER MEMORY не должно вообще превышать ОЗУ, доступного для сервера.
Динамическая настройка памяти в SQL Server 2000 Так как же работает динамическая настройка? Приведенный ниже кадр экрана показывает настройки свойств памяти для экземпляра SQL Server 2000. Данный экземпляр называется CD1XMS8D1. Это означает сервер CD1 и второй или следующий из экземпляров, так как первый задается по умолчанию просто именем сервера. Если посмотреть глубже, экземпляр запускается с процессом, идентифицированным как MSSQL$InstanceName, в данном случае, MSSQL$MS8D1. Экземпляр по умолчанию, как в SQL Server 6.5, так и в SQL Server 7.0, имеет имя процесса MSSQLServer. Экземпляр CD1\MS8D1 имеет настройку минимума, равную 60, и максимума, равную 512, который является полным ОЗУ, установленным на этом конкретном сервере. С этими настройками вы считаете, что когда запускаете экземпляр CD1XMS8D1, сразу требуется 60 Мб ОЗУ, и он может потенциально использовать все 512 Мб. Это не является работой с динамической памятью. Когда вы первый раз запускаете экземпляр на SQL Server 2000, первоначально требуется 8-4-12 Мб ОЗУ. Он получает большее количество ОЗУ для своего процесса только тогда, когда подключаются пользователи и увеличивается рабочая нагрузка. В случае CD1\MS8D1 он постепенно увеличит свою область памяти, назначенную процессу MSSQL$MS8D1. Как только память достигает сконфигурированного порога значения в 60 Мб, она не будет опускаться ниже этой уставки. В то же самое время, когда рабочая нагрузка увеличивается, экземпляр будет постепенно получать память. Память, однако, не достигнет 512 Мб — значения, установленного ОЗУ на сервере. Вместо этого, фирма Microsoft разработала ряд алгоритмов, которые добавляют память. Эти алгоритмы имеют две конкретные цели: •
обеспечить свободную память ОС в пределах 4*40 Мб;
•
распределить память между экземплярами SQL Server с учетом их относительных рабочих нагрузок.
569
Глава 13
SQL Server Properties (Configure) - CD1\MS8D1 Server Setings J Database Setn i gs | Replication j Active Driectory • Мегшу
j
Processor : ;:• •••:
•• M e m o r y
Security ;
:
I :
Connections :
:
S Q L S e r v e i c a n b e c o n f i g u r e d t o u s e a fixed a m o u n t of
— д memory or la dynamically adjust the amount of memory used ВЙЙН based upon demand.
{Dynamically configure SQL Server memory Minimum (MB): OMB
80
— }
511MB
Maximum (MB): 4MB
511
—
511 MB
Ц$е a fixed memory size(M8):
Г" FSeserve physical memoiy for SQL Server Minimum query memory (KB): Configured values
11024
Running values OK
; Cancel
Help
Целевая свободная память Как было раньше отмечено, установка максимума для экземпляра CD1AMS8.D1 в вышеупомянутом кадре экрана была 512 Мб — значение установленного ОЗУ на сервере. Эксперименты Microsoft показали, что поддержание от 4 до 10 Мб памяти на сервере Windows минимизирует обмен страниц файла. Конкретное значение, которое SQL Server ищет, чтобы обеспечить значение в этом диапазоне 4+10 Мб, называется целевой свободной памятью. Алгоритмы были разработаны так, что когда SQL Server потребляет все больше памяти для кэша данных, количество свободной памяти для ОС уменьшается. Когда достигнут порог 4+10 Мб, SQL Server прекратит добавление памяти. Целевая свободная память — около 10 Мб для слабо загруженного SQL Server и постепенно уменьшается до 4 Мб для сильно загруженного SQL Server. Эта целевая свободная память — отражение ожидаемой жизни страницы в буфере-накопителе SQL Server. Когда сервер сильно загружен, страницы более часто загружаются в кэш буфера, а старые страницы удаляются, уменьшая продолжительность жизни страницы. Когда продолжительность жизни страницы уменьшается, SQL Server поддерживает свободную память ОС у нижнего края (4 Мб) диапазона 4+10 Мб. Когда сервер менее занят и продолжительность жизни страницы возрастает, он поддерживает целевую свободную память ближе к 10 Мб. Таким образом, возвращаясь к нашему экземпляру CD1\MS8D1, максимальная память никогда не будет приближаться к 512 Мб ОЗУ. Скорее, это, вероятно, будет где-нибудь в диапазоне от 460 до 470 Мб, так как сама ОС забирает 3&-40 Мб ОЗУ, и целевая свободная память в 4+10 Мб ОЗУ всегда поддерживается свободной для ОС, чтобы избежать страничную подкачку. 570
Определение требований к аппаратным средствам
Так что происходит, когда вы запускаете SQL Server, и он поддерживает целевую свободную память 4-^10 Мб ОЗУ, доступную для ОС, и вы внезапно начинаете большой процесс, который занимает 20-^25 Мб ОЗУ? Оказывается, что SQL Server динамически уменьшит свою память в кэше данных, чтобы обеспечить 4н-10 Мб свободной памяти ОС. Он может делать это очень быстро, порядка нескольких Мб в секунду.
Настройка памяти для нескольких экземпляров Когда SQL Server должен разделить ОС с другим процессом, например с сервисом Microsoft Search, максимальная память SQL Server должна быть сконфигурирована вручную. Как вы, вероятно, теперь уже знаете, SQL 2000 позволяет реализовать несколько экземпляров SQL Server на одной и той же ОС. Это верно, независимо от того, используется ли ОС Windows NT или Windows 2000. Как же осуществляется работа в ситуации, когда два или более экземпляров SQL Server установлены на одном и том же сервере? В таком случае SQL Server имеет достаточно средств, чтобы балансировать нагрузки между этими двумя экземплярами. Вспомните из предыдущего раздела 3K3eMnjiHpCDl\MS8Dl. Очевидно, экземпляр по умолчанию называется CD 1. Мы рассмотрим два таких экземпляра, но теория будет той же самой для трех или более экземпляров.
Сколько экземпляров вы можете запускать на отдельном сервере? Microsoft рекомендует один — экземпляр, используемый по умолчанию, для работающего сервера. Известно об отдельных испытаниях, успешно управляющих 10 и 16 экземплярами. Мы, конечно, не рекомендуем больше 10 экземпляров. Вообще, вы можете захотеть запустить несколько экземпляров на одном сервере, чтобы поддержать, например, формирование конфигурации на одном или нескольких экземплярах и обеспечение качества или проверку оборудования на другом экземпляре. Все это, конечно, возможно при полных характеристиках аппаратных средств используемых экземпляров.
При запуске системы, каждый из экземпляров, CD1 и CD1\MS8D1, немедленно получит 8-^12 Мб ОЗУ. Они останутся на этом уровне, пока соединения пользователя и рабочие нагрузки не начнут увеличиваться на одном или обоих экземплярах. Тогда они будут получать память независимо друг от друга, пока ОС не станет иметь от 4 до 10 Мб свободной доступной памяти, как отмечено в предыдущем разделе. В этот момент они начинают конкурировать друг с другом за использование памяти на основе своих значений целевой свободной памяти. Как пример, если экземпляр по умолчанию имеет большую рабочую нагрузку, его целевая свободная память будет около 4 Мб. Пусть экземпляр CD1XMS8D1 менее загружен и имеет целевую свободную память около 10 Мб. Предположим, что только 7 Мб свободной памяти доступно для ОС. Как вы и ожидали бы, сильно загруженный экземпляр CD1 со значением 4 Мб продолжит получение памяти. Так как мало загруженный CD1XMS8DL не видит свободных 10 Мб, он будет освобождать память ОС, пытаясь достигнуть ее целевого значения свободных 10 Мб. Не думайте относительно того, сколько памяти используется в каждом процессе. Лучше думайте относительно продолжительности жизни страницы и значения соответствующей целевой свободной памяти для этого экземпляра. 571
Глава 13 Таким образом формируется контур отрицательной обратной связи, чтобы регулировать количество памяти, которую каждый экземпляр получает в зависимости от его рабочей нагрузки. Так как CD1, сильно нагруженный экземпляр, получает большее количество памяти, он может сохранять страницы в памяти дольше, увеличивая их продолжительность жизни. Поскольку продолжительность жизни страницы увеличивается, значение целевой свободной памяти, которое было равно 4 Мб, увеличивается. На слабо загруженном экземпляре CD1\MS8D1 память освобождена, так как SQL Server пытался увеличить целевую свободную память ОС до 10 Мб. Когда память освобождена, страницы сохраняются в памяти в течение более короткого периода, уменьшая свою продолжительность жизни. Поскольку продолжительность жизни страницы уменьшается, значение целевой свободной памяти удаляется от 10 Мб. В некоторый момент слабо нагруженный экземпляр освободит достаточно памяти (по крайней мере, до ее минимального значения) так, что значения целевой свободной памяти из этих двух экземпляров сравняются. Каков заключительный результат? Значения целевой свободной памяти (и соответствующая продолжительность жизни страницы) этих двух экземпляров сравняются. Сильно загруженный экземпляр все еще сильно загружается. Кроме того, учитывая теперь приблизительно 460 Мб ОЗУ, доступных всем экземплярам SQL Server, сильно загруженный экземпляр будет иметь, например, 430 Мб ОЗУ, выделенного для него. Слабо загруженный экземпляр будет иметь около 100 Мб ОЗУ. Равновесие продолжается, пока относительные рабочие нагрузки экземпляров остаются неизменными. Как только рабочая нагрузка на одном или другом экземпляре увеличится или уменьшится, продолжительность жизни страницы и соответствующая целевая свободная память отреагируют и переведут экземпляры в новое состояние равновесия.
Настройка памяти процесса в SQL Server Как только вы получили полностью настроенную память ОС и SQL Server, поподробнее рассмотрим использование памяти в SQL Server. Здесь желательно использовать четыре счетчика: •
P r o c e s s : Working S e t : s q l s e r v r (процесс: количество рабочей памяти: SQL Server);
•
SQL S e r v e r : Buffer Manager: Buffer Cache Hit R a t i o (SQL Server: диспетчер буфера: результативность кэша буфера);
•
SQL S e r v e r : Buffer Manager: Free Buffers (SQL Server: диспетчер буфера: свободные буферы); SQL S e r v e r : Memory Manager: Total Server Memory (KB) (SQLServer: диспетчер памяти: полная память сервера (K6))L
•
Экземпляр Working S e t : s q l s e r v r показывает количество памяти, которое использует SQL Server. Если это число постоянно ниже, чем количество, сконфигурированное для использования в SQL Server с помощью опций MIN SERVER MEMORY и MAX SERVER MEMORY, то SQL Server сконфигурирован для слишком большой памяти. В противном случае вам нужно увеличить ОЗУ и MAX SERVER MEMORY. Значение Buffer Cache Hit R a t i o должно быть постоянно больше 90. Это указывает, что кэш данных обеспечивает 90 процентов от запросов данных. Если данное значение постоянно низкое, это является очень хорошим индикатором, что для SQL Server необходимо большее количество памяти. Если Available Bytes (доступные байты) мало, это означает, что мы должны добавить большее количество ОЗУ. 572
Определение требований к аппаратным средствам Когда значение Free Buffer мало, это означает, что нет достаточной оперативной памяти, чтобы обеспечить постоянную величину кэша данных. Это также указывает, что нужно больше памяти. Если значение T o t a l Server Memory для SQL Server постоянно больше, чем полная память сервера, это указывает, что нет достаточного ОЗУ.
Настройка работы сервера Если этот сервер предназначен для SQL Server или если SQL Server считается наиболее важным приложением на машине, имеется возможность изменения конфигурации, которое вы при желании можете выполнить, чтобы увеличить производительность. Вы можете управлять относительными количествами процессорного времени, которые будут предоставляться высокоприоритетным приложениям по сравнению с фоновыми приложениями. Значение по умолчанию для Windows NT должно дать высокоприоритетным приложениям самый высокий приоритет и, следовательно, лучшее время отклика. Чтобы сконфигурировать эти настройки, откройте Control Panel (панель управления) из меню папки Settings (настройка) меню Start (пуск). Откройте апплет System (система) в Control Panel и выберите страницу Performance (выполнение), как в кадре экрана, приведенном ниже. Используйте управляющий движок на этой странице для регулировки высоких и фоновых приоритетов. В Windows NT Workstation планировщик ОС будет всегда давать фоновые потоки в два временных тика. Временные тики могут равняться 7f-15 мс в зависимости от Hardware Abstraction Layer (HAL — уровень аппаратных абстракций). В зависимости от положения движка высокоприоритетные приложения получат 2, 4 или 6 временных тиков, соответствующих значениям от None (нет) до Maximum (максимум). Небольшой известный факт — в сервере Windows NT это не имеет абсолютно никакого воздействия. Изображение графического интерфейса работает, но от этого ничего не изменяется. Все процессы получают период в 12 временных тиков.
Vfauei Memory ТйШ рщщ Me sizetorat!
13? ME
брр'У
j
5 7 3
Глава 13 В отличие от сервера Windows NT, сервер Windows 2000 действительно работает. Но это несколько более просто. И в варианте сервера, и в варианте рабочей станции, называемом Windows 2000 Professional, вы получаете диалоговое окно, показанное на следующем кадре экрана, щелкнув кнопкой Performance Option (опции выполнения) на странице Advanced (расширенный) апплета Control Panel | System (панель управления | система).
Application response Optimize performance for: • MppkjtijTi; ' Background services Virtual memory Total paging file size for all drives:
Change... Cancel Когда вы оптимизируете работу для варианта Applications в Windows 2000, то получаете отношение высокоприоритетного потока к фоновому потоку как 2 временных тика к 6 тикам, которые рабочая станция Windows NT использует, когда она оптимизирована для высокоприоритетных приложений. Когда вы выбираете Background services для Windows 2000, то получаете режим сервера Windows NT — 12 временных тиков для всех потоков. Рекомендуется выбирать Background services на всех серверах Windows 2000, запускающих SQL Server, чтобы обеспечить одинаковое процессорное время как для высокоприоритетных, так и для фоновых приложений. Это даст сервисам Microsoft SQL Server больший процент производительности, так как они почти всегда управляются как фоновые сервисы.
Подсистемы ЦП Подсистема ЦП — сердце и душа вашего сервера. Будучи таковой, она является основным кандидатом на настройку функционирования. В недавние годы превалировала симметричная мультипроцессорная (symmetric multi-processor — SMP) конфигурация. Сервер Windows NT поддерживает четыре новых процессора, не имеющих специальных механизмов уровня аппаратных абстракций (hardware abstraction layers — HAL). Сервер Windows 2000 обеспечивает поддержку четырех процессоров, в то время как сервер Windows 2000 Advanced поднимает эту планку до восьми процессоров. Сервер Windows 2000 Data Center дальше увеличивает значение до 32 процессоров. Вы можете производить на этом уровне существенные вычисления. Что вам следует использовать — Windows 2000 или Windows NT? Определенно, Windows 2000. Для четырехпроцессорного сервера Windows 2000 дает на 68 процентов улучшение работы по сравнению с подобным сервером Windows NT. Windows NT даже не поддерживает что-либо более четырех процессоров, за исключением специализированной аппаратуры. Windows 2000 также поддерживает родственность процессоров. Это похоже на то, когда процесс типа SQL Server установлен на одном или нескольких процессорах. Это эффективно заменяет мультипроцессорный параллелизм, который был переписан для SQL Server как опция конфигурации после SQL 6.5. Пожалуйста, обратите внимание также, что как и в SQL 6.5 с мультипроцессорным параллелизмом, вы должны быть осторожны при использовании родственности процессоров, так как это будет часто связано с хранением процесса типа SQL Server на менее используемом процессоре. 574
Определение требований к аппаратным средствам В настоящее время появляется экономически эффективное "свежее пятно" на уровне четырех процессоров. Так как вы должны конфигурировать SQL Server, чтобы максимизировать использование ЦП? Для SQL Server 7 и SQL Server 2000, по-видимому, потребуется пассивное участие! Хотя некоторые аргументы и могут быть сказаны в пользу настройки, подсистемы ЦП в SQL Server 6.5, системы SQL Server 7 и SQL Server 2000 хорошо настроены, чтобы обеспечить максимальное преимущество любой организации ЦП, которую вы используете.
Контроль работы ЦП При организации контроля работы ЦП мы собираемся использовать несколько счетчиков: •
Processor:
% Processor Time (процессор: процент процессорного времени);
•
Processor: времени);
% P r i v i l e g e d Time (процессор: процент привилегированного
•
Processor:
% User Time (процессор: процент пользовательского времени);
Q
System: % T o t a l Processor Time (система: процент полного процессорного времени).
Обычно контроль работы ЦП осуществляется непосредственно. Вы должны начать с контроля Processor: % Processor Time. Если вы имеете более одного процессора, то должны контролировать каждый экземпляр этого счетчика, а также контролировать System: % Total Processor Time, чтобы определить среднее значение для всех процессоров. Постоянные скорости использования более 80т-90 процентов могут указывать на плохую настройку или проектирование приложения. С другой стороны, если вы использовали все другие рекомендации этой книги, то это может указывать на потребность в более мощной подсистеме ЦП. Вообще говоря, я потратил бы немного времени для анализа приложений перед тем, как немедленно пойти и купить еще три процессора. Потратив это время на экспериментирование, чтобы определить проблемы в работе ЦП, и устранение их через усовершенствования программного обеспечения часто оградит вас от расхода денег на более мощный ЦП, который только "закроет" плохо написанное программное обеспечение в течение маленького или никакого периода времени. Если вы наблюдаете интенсивное использование ЦП, то далее захотите проконтролировать Processor:% P r i v i l e g e d Time. Это — потраченное время на операции уровня ядра, типа дисковых операций ввода/вывода. Если этот счетчик постоянно имеет скорость использования более 80*90 процентов и соответствует большим значениям счетчика работы диска, скорее всего, это связано с узким местом диска, а не ЦП. А что касается SQL Server? Счетчик P r o c e s s o r : % User Time измеряет величину процессорного времени, используемого приложениями, не относящимся к уровню ядра. SQL — именно такое приложение. Если эта величина велика, и имеется несколько процессов, запущенных на сервере, вы можете захотеть посмотреть глубже на конкретные экземпляры процессов с помощью экземпляров счетчика Processor : % User Time. Это может быть очень полезно для случаев типа того, когда наши инженеры ОС установили новое антивирусное программное обеспечение на всех наших серверах. Это временно принесло бы им удовлетворение, пока мы не можем определить виновника с помощью анализа P r o c e s s o r : % User Time для экземпляра антивирусного программного обеспечения.
20-1868
5 7 5
Глава 13
Волоконное управление На языке использования ЦП потоки — единственные процессы, управляемые процессором. Потоки управляются ОС в режиме ядра. SQL Server может поддерживать до 255 открытых потоков, каждый из которых используется для обработки пакета. С появлением SQL Server 7 был введен новый термин — волокна (fibers). В то время как потоки обрабатываются ОС в режиме ядра, волокна обрабатываются в режиме пользователя самим приложением. В OLTP-обрудовании высокой производительности число пакетов и, следовательно, потоков, переданное подсистеме ЦП, может быть очень велико, обычно приближаясь к сконфигурированным по умолчанию 255 потокам. Каждый раз, когда контекст потока переключается на ЦП, ЦП должен выйти из режима пользователя, перейти через переключатель контекста в режим ядра, взять новый поток и затем переключить контекст назад в режим пользователя. Для ЦП переключатель контекста относительно дорог. Используя волокна, SQL Server будет порождать несколько потоков и затем несколько волокон к каждому потоку. Волоконное управление связано с кодом уровня приложения на SQL Server, а не ядра ОС. Так как волокна запускаются в режиме пользователя, когда ЦП переключается с одного волокна на другое в одном и том же потоке, то для ЦП нет переключателей контекста. Два цикла ЦП — один, когда ЦП захватывает новый поток, далее через переключатель контекста попадает в режим ядра и затем возвращается обратно, и другой, когда ЦП использует новое волокно, не требующее перечисленных операций, отличаются по времени на порядок. Волокна годятся не для каждой ситуации. Они ограничены единственным потоком, который всегда назначается единственному ЦП. Таким образом, когда поток процесса может обслуживаться одним ЦП, а затем другим, волокно процесса будет ограничено единственным ЦП. OLTP-приложения выделяются особо, так как они обычно имеют очень короткие, определенные транзакции, запускаемые в процессе, содержащемся в потоке или волокне. Используйте следующий контрольный список, чтобы определить, могут ли волокна быть вами использованы: 1.
Ваш сервер должен быть полностью связан с использованием SQL Server и обычно применяется для OLTP-оборудования.
2.
Вы имеете очень высокую скорость использования ЦП (больше 90 процентов).
3.
Вы наблюдаете чрезмерно высокую скорость переключений контекста, используя счетчик System: Context Switches/sec (система: число переключений контекста в секунду). •
Если эти параметры соответствуют вашей системе, то можете попробовать поэкспериментировать с использованием волокон, переключая настройку конфигурации "облегченного пула" от 0 до 1.
Дисковые подсистемы Вторая наиболее важная характеристика системы, определяющая хорошее функционирование вашего приложения на SQL Server, — дисковая подсистема. Помните, что функционирование главным образом определяется максимизацией числа чтений данных из быстродействующей памяти. Обмен этими данными с быстродействующей памятью и хранение их — ответственность дисковой подсистемы. 576
Определение требований к аппаратным средствам
Основная дисковая подсистема Основная дисковая подсистема включает жесткий диск и контроллер жесткого диска, взаимодействующий с материнской платой. Жесткие диски имеют различные эксплуатационные характеристики. Мы можем рассматривать их с точки зрения чтения и записи, с точки зрения чтения данных с диска последовательно или случайным образом и с точки зрения скорости вращения диска. Характеристики чтения, естественно, отличаются от характеристик записи на диск. Как вы могли бы ожидать, запись относительно медленнее по сравнению с чтением. Таким образом, мы хотим минимизировать время ожидания при записи на диск. SQL Server, естественно, оптимизирует это, используя комбинацию потоков конкретного рабочего процессора и систему отложенных записей. Эти две системы объединяются для просмотра кэша буфера в активной памяти и уменьшения счетчика, указывающего относительную занятость страницы в кэше буфера. Обращение к странице вызывает увеличение его счетчика. Таким образом, частое обращение к страницам сохраняет их к кэше буфера. Если счетчик страницы, расположенной в кэше буфера, достигает нуля, и флаг изменения страницы установлен, поток конкретного рабочего процессора или, что менее часто, система отложенных записей запланирует сбрасывание страницы на диск. Ключевой момент здесь заключается в том, что это делается асинхронно с операциями чтения в потоке, так что время ожидания записи минимизируется. Чтение данных с диска — это та операция, где можно получить наибольший выигрыш в характеристиках. Намного более эффективно читать с диска данные последовательно (нежели беспорядочным образом). Таким образом, использование кластерных индексов для загрузки большого объема данных в кэш данных для анализа всегда оптимально. Наконец, если вы можете выбирать между дисками, которые имеют скорость вращения 7200 оборотов в минуту и 15000 оборотов в минуту, естественно выбрать последний. Диски, которые вращаются быстрее, получают данные под головкой чтения быстрее и помещают их в кэш данных быстрее. Конечно, они более дорогие. Однако вы можете иметь самый быстрый жесткий диск в мире и тем не менее иметь слабую исполнительную систему, если у вас нет хорошего контроллера, связывающего жесткий диск и материнскую плату. Работа контроллера характеризуется пропускной способностью в битах. 64 бита, очевидно, лучше, чем 32. Что еще важно для контроллеров, так это их способность кэширования. Современные контроллеры обладают как кэшированием опережающего чтения, так и кэшированием опережающей записи или обратной записи. Когда контроллер читает в опережающем режиме, он использует сложные алгоритмы, чтобы дождаться потребности в данных, и размещает эти данные в быстродействующем кэше, пока центральный процессор не будет готов их принять. Кэширование опережающей записи или обратной вынуждает контроллер посылать данные в быстродействующий кэш и возвратить в ОС статус завершения. Затем данные фактически записываются на диск контроллером независимо от ОС. Важно, что способность кэширования определяется объемом сверхбыстрого ОЗУ, используемого для кэширования этой информации. Снова, чем больше, тем лучше, но также и дороже.
Кэширование записи и SQL Server Многие контроллеры обладают особенностью, называемой кэшированием записи (Write Caching). Это когда диспетчер получает данные от ОС и по существу сообщает ОС, что данные записаны на диск, хотя фактически они помещены в высокоскоростное ОЗУ для возможной записи на диск. 577
Глава 13 Это может быть высокопроблематично. Все схемы обеспечения целостности данных SQL Server в случае неправильной работы сервера зависят от загрузок транзакций, записанных на диск до того, как основные таблицы будут фактически изменены на диске. С кэширующим записи контроллером SQL Server может ошибочно сообщить, что загрузка транзакции надежно находится на жестком диске, когда фактически она все еще находится в высокоскоростном ОЗУ кэшированного контроллера. Если в этот момент работа сервера нарушается, целостность SQL при восстановлении совершенных транзакций и возвращении запорченных транзакций потенциально будет нарушена. Большинство средних и высококачественных серверов сегодня имеют контроллеры с кэшированием записей и резервной батареей на плате, используемой только для контроллеров и их дисковых матриц. При наличии этой резервной батареи вы должны быть осторожны при включении контроллеров с кэшированием записи. Однако, что произойдет, если батарея испортится? Инженеры сервера посоветуют вам не волноваться. Они получат сообщение alarms/paged (тревога/оповещение). Однако из нашего личного опыта даже с очень компетентным штатом инженеров сервера и высококачественным сервером от одного из ведущих и наиболее известных изготовителей серверов, мы только могли выяснить, что первый из двух резервных источников энергии для сервера отказал, когда второй также отказал и сбил весь сервер. Даже при том, что оповещение было включено, по некоторым причинам это не сработало. Короче говоря, за очень, очень немногими исключениями Microsoft почти всегда рекомендует, чтобы кэширование записи у контроллеров, через которые SQL пишет информацию на диск, было выключено. Исходя из нашего опыта, мы целиком с этим соглашаемся. За исключением очень небольшого числа OLTP-приложений это маленькое преимущество выполнения полностью возмещается дополнительными часами сна, которые получит ваш администратор базы данных. Задумайтесь об этом. Вспомните, как первый раз (и надеемся единственный) вы печатали десять страниц курсовой работы в текстовом процессоре без частого сохранения, и при этом мигала световая сигнализация? Если вы собираетесь включить кэширование записи, то действительно должны иметь очень серьезную причину для этого и очень высокое доверие к аппаратным средствам, инженерам и системам, поддерживающим и контролирующим их.
Подсистема RAID Диски и контроллеры могут быть соединены вместе в матрицу недорогих устройств с избыточностью (Redundant Array of Inexpensive Disk — RAID). Основная цель таких комбинаций состоит в том, чтобы обеспечить устойчивость к ошибкам (другими словами избыточность), такую, что отказ любого компонента не будет сбивать всю систему. Комбинации RAID могут быть или программно-организованы, когда ОС Windows NT или Windows 2000 обслуживает матрицу, или аппаратно-организованы, когда специализированный контроллер обслуживает конфигурацию RAID. Вообще имеется три плюс один путь рассмотрения конфигурации RAID. Эти три пути называются RAID 0, RAID 1 и RAID 5. Дополнительный называется или RAID 10 или RAID 0+1. RAID 0, RAID 1 и RAID 5 могут быть как программно-, так и аппаратно-организованы. RAID 10 имеет только аппаратное решение.
RAID О RAID 0 — это когда данные разделены на дисках на блоки, обычно размером около 64 Кб каждый. Единственный контроллер или ОС записывают участки блоками в 64 Кб на один диск, а затем продолжает писать блок на следующий диск. Запись и чтение таких матриц могут быть чрезвычайно быстрыми. Чтение и запись оказываются быстрыми, потому что имеется много головок, которые могут читать блоки данных одновременно.
578
Определение требований к аппаратным средствам
Многие скажут вам, что RAID 0 не является на самом деле структурой RAID, так как она не избыточна. Мы лично согласны. Если любой из дисков отказывает, то ваша система со скрипом остановится, и вы можете (удивительно!) потерять данные. Имеется преимущество RAID 0 в смысле скорости. Так как никакая дублирующая информация не размещается на каком-либо другом диске, данные пишутся быстро участками или блоками данных на каждый диск. Точно так же и при чтении (и конкретном последовательном чтении) работа может быть чрезвычайно хороша. В дополнение к скорости есть другое преимущество конфигурации RAID 0 — то, что она может поддерживать очень большие массивы. Даже при том, что это на самом деле не "избыточная" матрица, важно детализировать ее, так как ее характеристики означают, что она представляет одну из двух конфигураций RAID, которые будут использованы позже, когда мы будем обсуждать RAID 10. Следующей является RAID 1. RAID
1
RAID 1 называется зеркальным отображением, когда один контроллер зеркально отображает два диска, или дублированием, когда каждый из двух контроллеров управляет отдельным диском, чтобы поддерживать зеркальные копии данных. Часто термин "зеркальное отображение" используется и тогда, когда термин "дублирование" был бы более технически правилен. Обычно достаточно знать, что упоминаются идентичные подсистемы. По существу, задаются два идентичных тома диска — А и В. Когда данные записываются на А, они одновременно записываются и на В. Тома находятся в вечной синхронизации. Когда А или В отказывает, дисковая подсистема не теряет работоспособность. В этом случае • возникают (будем надеяться) все виды аварийных сигналов, чтобы сообщить вам, что больше нет избыточности. Чтобы все восстановить, вы должны "разорвать зеркальное отображение", воспользоваться новым двигателем и заново синхронизировать зеркальное отображение. Большинство подсистем, которые поддерживают зеркальное отображение, позволяют вам сделать это без какой-либо остановки сервера. Что при этом страдает? Помните " I " в аббревиатуре RAID? Она означает "недорогой". В случае RAID 1 вы должны купить два байта хранения для каждого байта данных. В дополнение к затратам неудобством конфигурации RAID 1 является то, что она может быть только размером в один жесткий диск.
-
21
-1868
• • • • • • •
579
Глава 13
RAID 5 RAID 5 — наиболее общая конфигурация RAID и подобно RAID 0 данные записываются на ряд дисков в виде блоков. Однако в то время как RAID 0 требует только два диска, RAID 5 требует по крайней мере три или большее количество дисков. Каждый участок, тем не менее, будет теперь иметь дублирующий блок, записанный на другой диск. Например, если вы имеете четыре диска в матрице RAID 5, каждый участок будет записываться на один из трех блоков данных и один блок дублирующей информации. Дублирующая информация будет перемещаться от диска к диску, так что никакой диск не содержит всю дублирующую информацию.
F
K
O .
Контроллер данных
Когда отдельный диск отказывает в матрице RAID 5, другие диски примут нагрузку на себя. Когда нужно читать с отказавшего диска, контроллер или ОС будут читать дублирующую информацию и сравнивать ее с данными от всех других неповрежденных дисков, вычисляя данные, которые в противном случае находились бы на отказавшем диске.
Конфигурация RAID 3 очень похожа на RAID 5 за исключением того, что вместо распределения дублирования на все диски в матрице для хранения резервных данных используется единственный диск, а все другие диски используются для хранения данных. 580
Определение требований к аппаратным средствам Совершенно очевидно, что отказ диска в конфигурации RAID 5 означает, что чтение данных становится существенно медленнее, чем в случае отказа системы RAID 1, где чтение данных фактически не затрагивается. Также очевидно, что издержки, связанные с поддержкой избыточности — только 1/п от вашей области для размещения данных, где п — число дисков, входящих в матрицу. Другое преимущество матрицы RAID 5 — то, что массив может быть очень большой размерности. Я видел массив RAID 5 с сотнями гигабайт. Для огромного большинства серверов и приложений среднего размера аппаратно-организованные структуры RAID 5 являются предпочтительными дисковыми подсистемами.
RAID 10 — где О + 1 не равняется 1 Как вы можете видеть, RAID 0 имеет лучшую производительность и расширяемость всех массивов, но не имеет никакой избыточности. RAID 1 имеет лучшую избыточность, но очень ограничена ее расширяемость. Таким образом, лучшие матрицы RAID сегодня на рынке те, которые определяются как RAID 0 + 1 или RAID 10. Чтобы построить матрицу RAID 10, вы сначала делаете две отдельных, но идентичных матрицы RAID 0, где данные разделяются на блоки на двух или больше дисках. Эти две идентичные матрицы RAID 0 затем зеркально отображают друг друга. Данное решение реализуется только на высококачественных контроллерах RAID.
Контроллер данных
В то время как RAID 10 часто рассматривается как одно из лучших решений RAID, следует отметить, что RAID 5 и RAID 1 часто используются из-за их более низкой стоимости и равной избыточности.
Решения для нескольких контроллеров/каналов До сих пор мы адресовали только один канал данных. Многие контроллеры могут иметь двойные каналы, и, конечно, вы можете устанавливать на сервере дополнительные контроллеры. Вспомните, что SQL Server — существенно многоуровневая система. Когда отдельная запись пишется на диск, этому предшествует запись загрузки транзакции и очень часто запись одного или большего количества индексов. Когда SQL записывает информацию, параллельно этому ОС Windows NT или Windows 2000 непрерывно сокращает память ОС, постранично помещая на диск неактивную память. Если эти записи могут быть записаны различными контроллерами на различные подсистемы матриц дисков, то производительность транзакций будет умножена. 581
Глава 13 Чтение данных с диска в кэш данных также может извлечь существенные выгоды из нескольких каналов. Хорошо оптимизированная БД будет иметь дополнительные таблицы, которые чаще всего соединены. Помещая данные из одной таблицы с помощью контроллера А, индексы этой таблицы — контроллера В, данные из другой таблицы — контроллера С, индексы этой таблицы — контроллера D и TempDB (временная таблица БД) — с помощью контроллера Е, чтение данных может выполняться одновременно. И быстро. В целом, в то время как структура RAID 0 + 1 была бы, вероятно, самой быстрой единственной дисковой подсистемой для одного и того же бюджета, лучшим решением с точки зрения стоимости для большинства приложений будет покупка трех или четырех подсистем RAID 5. Тогда загрузкам транзакций будет отведена одна подсистема, данным от некоторых таблиц и индексам от других — другая подсистема, и остающимся таблицам и индексам от предыдущих таблиц — третья подсистема. Файл страниц и другие компоненты ОС могут быть на четвертой подсистеме, если она доступна. Другое жизнеспособное решение, которое часто используется, состоит в том, чтобы поместить интенсивно записывающую загрузку транзакции на зеркально отображающую конфигурацию RAID 1. Записи RAID 1 быстрее, так как зеркально отображающие драйверы выполняют одновременную запись и не имеют издержек на вычисление и запись дублирующих частей. Так как данные требуют последующего расширения, одна или большее количество подсистем RAID 5 включаются для данных и индексов.
Настройка диска и контроль работы В одном из предыдущих разделов Искусство управления работой упоминалось о введении счетчиков работы диска. Счетчики издержек работы диска, связанных с использованием ЦП, очень малы. На заре Windows NT было определено, что счетчики издержек работы диска были в худшем случае равны половине процента от использования процессора на одном сервере CPU 486DX66 Server. С Pentium 3, работающим с десятикратным увеличением тактовой частоты в симметричных мультипроцессорных структурах, значения счетчиков работы диска пренебрежимо малы. Самый серьезный риск узкого места после памяти одинаково распределен между дисковой подсистемой и процессором(ами). Просмотр большой таблицы может буквально поставить сервер на колени. Еще хуже опрометчивое декартово соединение двух больших таблиц — смертельный приговор для работы диска. Вы будете хотеть управлять работой диска, когда такие события просачиваются в ваши системы (и они будут в самые неподходящие времена). Вообразите руководителя людскими ресурсами, пытающегося запустить итоговый запрос во время процесса формирования платежной ведомости. Это случается. Начните управление работой диска, просматривая следующие счетчики: •
PhysicalDisk:
% Disc Time (физический диск: процент дискового времени);
•
P h y s i c a l D i s k : Current Disc Queue Length (физический диск: текущая длина очереди к диску);
•
P h y s i c a l D i s k : Avg. очереди к диску).
Disc Queue Length (физический диск: средняя длина
Приложения и системы, связанные с вводом/выводом, могут постоянно держать диск активным. Это называется перегрузкой диска.
582
Определение требований к аппаратным средствам
Вы должны всегда знать, сколько каналов, какие типы матриц, сколько дисков находятся в каждой матрице и в каких матрицах/каналах расположены ваши данные и загрузки транзакций перед тем, как вы решили заняться настройкой работы диска.
Счетчик P h y s i c a l D i s k : % Disk Time контролирует процент времени, которое диск тратит на проверку счетчика P h y s i c a l D i s k : Current Disk Queue Length, чтобы посмотреть число запросов в очереди, которые ожидают доступ к диску. Важно при этом быть знакомым с вашей дисковой подсистемой. Если число запросов, ожидающих ввода/вывода, непрерывно составляет величину, в 1.5*-2 раза больше числа дисководов, составляющих физический диск, вы имеете на диске узкое место. Например, конфигурация RAID 5 с семью дисководами/дисками была бы кандидатом на настройку работы диска, если Current Disk Queue Length будет постоянно держаться на значениях более чем 12+14. Чтобы улучшить работу в этой ситуации, рассмотрите добавление более быстрых приводов диска, перемещение некоторых процессов на дополнительную подсистему контроллера-диска или добавление дополнительных дисков к матрице RAID 5. Большинство дисков имеют один дисковод, хотя устройства RAID обычно имеют больше. Аппаратные средства устройства RAID 5 в Windows NT PerfMon или Windows 2000 SysMon рассматриваются как один физический диск. Устройства RAID, созданные с помощью программного обеспечения, ведут себя как несколько дисков. ПРЕДУПРЕЖДЕНИЕ: Счетчик % Disk Time может указывать значение, большее чем 100 процентов, если вы используете аппаратно-организованную конфигурацию RAID. Если это так, используйте счетчик PhysicalDisk: Avg. Disk Queue Length, чтобы определить среднее число запросов системы, ожидающих доступ к диску. Он опять-таки указывает на проблемы в работе, если постоянно его значение в 1.5+2 раза больше числа дисководов в матрице.
Соединения пользователя В SQL Server 6.5 соединения пользователя были важным местом настройки. SQL Server 7 и SQL Server 2000 уменьшили эту важность. Соединения пользователя в этих версиях являются динамическим, самоконфигурируемым выбором. В большинстве случаев вам не нужно регулировать назначения соединений пользователя, потому что если необходимо 100 связей, то и будет размещено 100 связей. Запуск следующих команд даст текущие назначения соединений пользователя и максимум допустимого числа соединений пользователя:
I
EXEC sp_configure "user connections" SELECT @@MAX_CONNECTIONS
Вы можете использовать sp_conf i g u r e , чтобы настроить число соединений пользователя, но мы настоятельно рекомендуем избегать это в SQL Server 7 и SQL Server 2000. Каждое соединение пользователя потребляет приблизительно 40 Кб памяти. Когда вы начинаете определять максимальное число соединений пользователя вместо разрешения SQL Server обслуживать нужное их число, распределение памяти больше не будет динамическим. 583
Глава 13 Вместо этого будет сразу же использовано 40 Кб памяти, умноженное на максимальное число соединений. Вообще, вы можете захотеть конфигурировать число соединений пользователя в оборудовании только тогда, когда незаконные пользователи могут породить большое количество соединений.
Многие администраторы и менеджеры путают соединения пользователя (User Connections) и лицензии доступа клиента (Client Access Licenses — CAL). Это все равно что яблоки и апельсины. В SQL Server 2000 CAL — лицензия для единственного устройства (обычно компьютера, но это может быть и ваша телефонная ячейка), чтобы соединиться с SQL Server. Люди, которые управляют вашими лицензиями, должны явно назначить ее устройству. Как только это устройство залицензировано единственной CAL, оно может соединяться и открывать столько соединений пользователя, сколько позволит сервер. Если способ лицензирования для вашей CAL — "На место" (PER SEAT), можно открыть дополнительные соединения пользователя с другим SQL Server. Неудивительно видеть, когда отдельный компьютер с единственной CAL "На место" открывает 20 соединений пользователя к одному серверу и пять — к другому. Короче говоря, нет никакой корреляции между соединениями пользователя и CAL. Пожалуйста, не думайте, что это короткое объяснение решает все ваши юридические проблемы (которые существенны для CAL). Структура лицензирования Microsoft более сложна, чем это короткое объяснение различия между соединениями пользователя и CAL. Профессиональный вывод, который следует сделать, — ЧИТАЙТЕ ВАШИ ЛИЦЕНЗИИ.
Важно контролировать соединения пользователя при вашей настройке работы. Использемый счетчик — SQL S e r v e r : G e n e r a l : User Connections (SQLServer: общие: соединения пользователя). При поиске неисправностей или проверке оборудования вы можете захотеть проконтролировать SQL S e r v e r : Databases: Active T r a n s a c t i o n s : d a t a b a s e (SQL Server: базы данных: активные транзакции: база данных). Число соединений пользователя основано на требованиях вашего приложения и пользователей. Приложения OLE DB нуждаются в соединении для каждого открытого объекта связи. ODBC-соединения порождаются для обработки каждого активного соединения в приложении. Библиотека БД использует соединение для каждого использования функции dbopen. n-ярусные приложения, которые используют объединение соединений, стремятся минимизировать число открытых соединений. Так как SQL Server является системой, загружающей транзакции, все, что вы можете пожелать знать — это присутствие длинных работающих транзакций. Обычно нужно сбрасывать загрузки транзакций каждые час или два. Загрузка транзакции остается открытой, пока открыта самая длинная транзакция. Если транзакция остается открытой на больший период времени, загрузка транзакции будет расти и расти, с каждым сбросом становясь все больше и больше. По умолчанию SQL Server будет автоматически увеличивать загрузку транзакции. Она может стать действительно огромной, и как мы можем видеть, достигнуть емкости диска. Мы проследили приложения третьих лиц, которые начинают сеанс с запроса функции dbopen библиотеки БД, немедленно следуемой за оператором BEGIN TRANSACTION. Контролируйте размеры дампов ваших загрузок транзакций. Если они получают постепенно больше информации в течение дня для конкретной БД, вы можете определить это условие. Можно наблюдать длительную работу открытых транзакций, выполняя команду T-SQL: DBCC OPENTRAN <база данных>
584
Определение требований к аппаратным средствам Вы можете даже рассмотреть запись на языке T-SQL, которая обеспечивает вам наличие длинных работающих открытых транзакций.
Запирание и блокировка Запирания — сами по себе очень хорошая вещь. Они гарантируют, что данные, которые вы читаете, — фактические данные в БД. Запирания, сохраняющиеся слишком долго, — проблема. Это приводит к тому, что мы называем блокировкой.
Демонстрация блокировки Запустим Query Analyzer (анализатор запросов). Откроем два соединения к одному и тому же серверу. Затем состыкуем окна вертикально, используя опцию меню Windows | Tile Vertically. В первом окне напишем и выполним следующий код:
I
USE p u b s GO SELECT au_fname FROM authors WHERE au lname = 'Green'
—
(NOLOCK)
Вам следует вернуть значение Marjorie.
Во втором окне напишем и выполним следующий код: USE pubs GO BEGIN TRAN UPDATE authors SET au_fname = 'Harry' WHERE au_lname = 'Green' — ROLLBACK TRANSACTION
:
' .
Убедитесь, что имеются две черты перед оператором ROLLBACK TRANSACTION. Вы должны получить сообщение. (1 row(s) affected) — воздействует на 1 строку Теперь заново выполним первый запрос. Откройте третье окно (набрав Ctrl-N) и запустите команду: Щ sp_who2 Просмотрите список и обратите внимание на столбец blkbу. Он показывает, что ваша транзакция SELECT блокирована вашей транзакцией UPDATE. Закройте третье окно. Если необходимо, перестройте ваши окна, снова используя опцию меню Windows | Tile Vertically. В вашем окне транзакции UPDATE выделите фразу ROLLBACK TRANSACTION и выполните ее. Заметьте, что произойдет с запросом SELECT. Повторите эксперимент снова, только на этот раз не помещайте в комментарий оператор (NOLOCK), удаляя две черты перед ним. Так как вы теперь используете NOLOCK, вы не увидите ваш оператор SELECT, на сей раз блокированный в sp_who2. Это — хороший пример испорченного чтения.
585
Глава 13
Контроль блокировок Иногда что-то нельзя сделать лучше, чем есть. В SQL Server 6.5 имелся очень удобный счетчик SQLServer-Locks: Users Blocked (блокировки SQL Server: блокированные пользователи), который мы всегда использовали, чтобы показать активные блокировки наших серверов. Это был такой важный счетчик, что мы увеличили масштаб счетчика в 10 раз и увеличили ширину счетчика на очень толстую линию. Таким образом, когда даже единственная блокировка возникнет в наших серверах, в диаграммах контроля работы появится большая толстая линия. К сожалению, этот счетчик ушел из SQL Server 7 и не используется в SQL Server 2000. Что делать? Используйте User Definable Counters (счетчики, определяемые пользователем). Большинство людей никогда не используют эти счетчики и не знают, как они работают. В SQL Server 6.5, когда вызывался счетчик, он автоматически обновлялся. В SQL Server 7 и SQL Server 2000 это не так. Каждый определяемый пользователем счетчик должен быть явно обновлен значением целого числа, запустив хранимую процедуру sp_user_counterX Y, где X — номер определяемого пользователем счетчика и Y »— целое число, задаваемое счетчику. В SQL Enterprise Manager для SQL Server перемещайтесь по дереву управлений к Management | SQL Server Agent | Jobs (Управление | Агент SQL Server | Виды работ). Щелкните правой клавишей по Jobs и выберите New Job (новая работа). Затем введите название и описание для User Defined Performance Counter 1 (Счетчик 1 с характеристиками, определяемыми пользователем). New Job Properties - CD1\MS8O1 General j Steps | Schedules J Notifications jUser Definied Performance Counter 1 Created: Category
(Not yet created)
W £nabled
(local)
(* Targetjocal server Г Taroamufeiesetv
[Uncategorized (Local)]
Owner: Description:
Source:
d
Executes every minute to reset the user defined performance counter 1 for Performance Monitor
Last modeled: (Not applicable)
OK
Cancel
Help
Щелкните по закладке Steps (шаги) и затем по кнопке New (новая), чтобы добавить шаг. В диалоговом окне New Job Step (новый шаг работы) создайте в этой работе единственный шаг с именем Reset Counter (исходное состояние счетчика) со следующим фрагментом на T-SQL, выполняемым в БД master:
586
Определение требований к аппаратным средствам
SET NOCOUNT ON DECLARE @blocks int SELECT @blocks = COUNT(blocked) FROM master.dbo.sysprocesses(NOLOCK) WHERE blocked > 0 EXEC sp_user_counterl Qblocks GO
Окно будет выглядеть следующим образом: New Job Step - CD1\MS8D1 General Advanced! Step name j Reset Counteij JLvpe:
i
1T ransact-SQL Script (TSQL)
Database: [master Command
Open» 1
SET NOCOUNT ON DECLARE ©blocks int SELECT ©blocks = coyrAfblocked) FROM masterdbo. sytptocesse* Indockl WHERE blocked > 0 EXEC sp_u$ei_counter1 ©blocks GO
Parse I
•••:.
•
—
\±г
«I
Go to:
Help
Cancel
Щелкните по кноке OK, чтобы закрыть диалоговое окно New Job Step. На закладке Schedules (расписания) щелкните по кнопке New Schedule (новое расписание). Установите выключатель Start whenever the CPU(s) become idle (запускать всякий раз, когда ЦП станет свободным), так как мы хотим, чтобы эта работа выполнялась в фоновом режиме и не сталкивалась с более важными. New ЗоЬ Schedule - C0i\MS8Dl
Name:Jwhend le l r Schedue l Type—™ -• • - ____— ..,., j f* S.tati automac tiayl «hen SQL Server Agent starts I (* '$i ;PU(sj become i5e] " 3 ^;Qnetm i e . On date: | 1/1/2001
P Enabled
•
С Recurring
Cancel
Help
5 8 7
Глава 13 Щелкните по кнопке ОК, чтобы сохранить это расписание, и по кнопке ОК в окне New Job Properties, чтобы сохранить работу. Теперь или в Windows NT PerfMon, или Windows 2000 SysMon добавьте экземпляр SQLServer: User Settable: Query: User counteri (SQL Server: установка в начальное состояние: запрос: счетчик 1 пользователя). Запустите проверку блокировки, приведенную в разделе Демонстрация блокировки, чтобы проверить вашу работу. Если вы затем решите добавить другие определяемые пользователем счетчики характеристик, вам нужно будет только добавить их во фрагмент на T-SQL на шаге Reset Counter, который вы определили ранее, и активизировать счетчик в Windows NT PerfMon или Windows 2000 SysMon.
Учебный пример В этом разделе нашего учебного примера мы рассмотрим получение объемных значений, которые собрали в главе 8, и в конце превращение их в реальные числа. В следующей таблице мы взяли таблицу из главы 8 и теперь проэкстраполируем, какие в ней появятся новые значения, поскольку у нас теперь есть несколько новых таблиц. Наиболее примечательны в них следующие три строки: Название таблицы
Начальное число строк
Увеличение строк за месяц
Максимальное число строк
Deposit
0
5
4000
Check
0
245
10000
DirectWithdrawal
0
50
1000
В наших таблицах мы взяли таблицы подтипов транзакции и перевели их в таблицу t r a n s a c t i o n . Добавим их все вместе в единственную строку:
300
transaction
15000
Мы делаем то же самое для каждой из таблиц и получаем следующую таблицу начального количества строк, увеличения строк и максимального ожидаемого числа строк:
588
Название таблицы
Начальное число строк
Увеличение строк за месяц
Максимальное число строк
account
1
0
20
accountReconcile
0
1
36
directWithdrawal
0
50
1000
address
50
30
600
addressType
5
0
10
Определение требований к аппаратным средствам
Название таблицы
Начальное число строк
Увеличение строк за месяц
Максимальное число строк
bank.
1
0
5
city
450
20
25000
payee
100
30
300
payeeAddress
100
30
600
payeePhoneNumber
100
30
600
phoneNumber
50
30
600
phoneNumberType
5
0
10
state
50
0
70
statement
0
1
36
statementItem
0
300
15000
statementType
3
0
5
transactionAllocation
75
400
12000
transaction
0
300
15000
transactionAllocationType
20
0
40
transactionType
3
0
5
user
10
2
50
zipCode
1000
10
99999
zipCodeCityReference
1100
10
99999
Затем мы должны вычислить размеры таблиц. Используя процедуру sp_table$indexSpace, вычислим значения для числа начальных строк, роста строк в месяц и максимального числа строк. Для каждой таблицы должно быть сделано предположение относительно процента, до которого заполнены столбцы переменной длины. Для этого учебного примера мы примем 50 процентов. Типовой запрос для максимального числа строк в таблице t r a n s a c t i o n показан ниже. Имеются три столбца переменной длины в таблице t r a n s a c t i o n : number (номер) типа v a r c h a r (20), s i g n a t u r e (подпись) типа v a r c h a r (20) и d e s c r i p t i o n (описание) типа v a r c h a r (1000). Нужно оценить значение предполагаемых 50 процентов для них в памяти.
I
EXEC sp_table$indexSpace @tableName = 'transaction' @varFillPercent = 50, @num rows = 16000
Результаты для всех таблиц и всех условий, которые мы рассмотрели, представлены в таблице ниже. Когда вы посмотрите на результаты для каждой таблицы, можно заметить, что увеличение в месяц не кажется нарастающим. Это потому, что мы использовали строки 589
Глава 13 в месяц как входной параметр строк для хранимой процедуры sp_table$indexSpace. Будет ли одна строка или несколько, выходная величина процедуры будет всегда кратна 8 Кб. Таким образом, размер особенно уместен для начального количества строк и максимального числа строк. Также помните, что при росте индекса уровни узлов постепенно заполняются. В некоторый момент они потребуют "вырастить" дополнительный уровень узлов-листьев. Таким образом, рост индекса вообще-то будет нелинеен, особенно для больших таблиц. Размер кластерного индекса относительно гладкий (единственная страница корневого узла в 8 Кб) фактически для всех условий. Это потому, что кластерный индекс, о котором мы говорим здесь, является единственной страницей корневого узла, которая указывает на страницу фактических данных. Все наши кластерные индексы — ключи из одного столбца типа 4-байтовых целых чисел, и 455 указателей страниц будет находиться на странице корневого узла. Это будет почти всегда давать самый гладкий реальный кластерный индекс, который вы можете получить. Как только страницы данных превысят 455 страниц, у кластерного индекса увеличится уровень и индекс включит дополнительный уровень узлов индекса. Это видно только в таблице t r a n s a c t i o n для максимального числа строк. Из-за относительно большого размера строк в таблице t r a n s a c t i o n (помните, что столбец d e s c r i p t i o n — v a r c h a r (1000), и мы предполагаем, что 50 процентов заполнено), число страниц превышает порог в 455 страниц.
рного
Таблица
Условие
Строки
account
Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц
1
8
8
1
8
1
0
0
0
0
0
0
20
8
8
1
8
1
0
0
0
0
0
0
1
8
8
1
8
1
36
16
8
1
24
2
50
24
8
1
32
2
30
16
8
1
24
2
ft В
Разм* даннь в Кб Размс класт индек account account accountReconcile accountReconcile accountReconcile address address
590
ряого
[ОГО
гого
Так как все наши таблицы имеют кластерные индексы, более точно было бы сказать, что размер кластерного индекса равен сумме столбца "Размер кластерного индекса" и столбца "Размер данных в Кб", который является фактически узлом-листом кластерного индекса. Мы получаем их в хранимой процедуре и помещаем ниже, чтобы их можно было проанализировать.
ей 0) U
111III 111
S
Определение требований к аппаратным средствам
Таблица
Условие
Строки
о и о
ай
2S 3Я
ft Is 2 н 8 я ft
addressType addressType bank bank bank city city city payee payee payee payeeAddress payeeAddress payeeAddress payeePhoneNumber payeePhoneNumber
о
X
к
ф
о ф
се у ф
аФ s
н
5* ft s •*ь S3
600
272
8
1
29е
3
5
8
8
1
8
1
0
0
0
0
0
0
10
8
8
1
8
1
1
8
8
1
8
1
0
0
0
0
0
0
5
8
8
1
8
1
450
32
8
1
40
2
00
addressType
Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц
(н
о
2 * И о к S ч о Фщ.ft ч к и и се К И>* 3 К Рн се М Ф SS Ьч
ю S » И 54 м Рч fc[ Н Он address
№
о
2
о и о
8
1
8
1
20
25000
1456
8
1
1496
3
100
8
8
1
8
1
30
8
8
1
8
1
300
24
8
1
32
2
100
8
8
1
8
1
30
8
8
1
8
1
600
24
8
1
32
2
100
8
8
1
8
1
30
8
8
1
8
1
се о Ф
« S и
Продолжение таблицы на следующих страницах
591
Таблица
Условие
Строки
payeePhoneNumber
Макс, число строк Начальное число строк Увеличение за месяц
600
24
8
1
32
2
50
8
8
1
8
1
30
8
8
1
8
1
Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк
600
32
8
1
40
2
5
8
8
1
8
1
0
0
0
0
0
0
10
8
8
1
8
1
50
8
8
1
8
1
state
Увеличение за месяц
0
0
0
0
0
0
state
Макс, число строк Начальное число строк Увеличение за месяц
70
8
8
1
8
1
0
0
0
0
0
0
1
8
8
1
8
1
phoneNumber phoneNumber phoneNumber phoneNumberType phoneNumberType phoneNumberType state
statement statement statement
Макс, число строк
36
8
8
1
8
1
statementltem
Начальное число строк Увеличение за месяц Макс, число строк Начальное число строк Увеличение за месяц Макс, число строк
0
0
0
0
0
0
300
32
8
1
24
2
15000
1584
8
1
600
2
75
8
8
1
8
1
400
48
8
1
32
2
12000
1264
8
1
672
2
statementItem statementltem statementType statementType statementType
592
Размер данных в Кб Размер кластерного индекса Уровни кластерного индекса Размер некластерного индекса Уровни некластерного индекса
Глава 13
Размер данных в Кб Размер кластерного индекса Уровни кластерного индекса Размер некластерного индекса Уровни некластерного индекса
Определение требований к аппаратным средствам
Таблица
Условие
Строки
transaction
Начальное число строк
0
0
0
0
0
0
transaction
Увеличение за месяц
295
176
8
1
24
2
transaction
Макс, число строк
15000
9144
32
2
640
2
transactionAllocation
Начальное число строк
1
8
8
1
8
1
transactionAllocation
Увеличение за месяц
0
0
0
0
0
0
transactionAllocation
Макс, число строк
20
8
8
1
8
1
transactionType
Начальное число строк
3
8
8
1
8
1
transactionType
Увеличение за месяц
0
0
0
0
0
0
transactionType
Макс, число строк
5
8
8
1
8
1
user
Начальное число строк
10
8
8
1
8
1
user
Увеличение за месяц
2
8
8
1
8
1
user
Макс, число строк
50
16
8
1
8
1
zipCode
Начальное число строк
1000
32
8
1
40
2
zipCode
Увеличение за месяц
10
8
8
1
8
1
zipCode
Макс, число строк
99999
2544
8
1
2656
2
zipCodeCityReference
Начальное число строк
1000
32
8
1
40
2
zipCodeCityReference
Увеличение за месяц
10
8
8
1
8
1
zipCodeCityReference
Макс, число строк
99999
2936
8
1
2944
2
593
Глава 13 Полное пространство для таблицы таким образом будет равно размеру данных плюс размер кластерных и некластерных индексов. Наши таблицы имеют только по одному некластерному индексу для вторичного ключа. Очевидно, что когда вы добавляете больше некластерных индексов для улучшения функционирования, то нужно дополнительно рассмотреть результаты процедуры sp_table$indexSpace для каждого дополнительного некластерного индекса, которые нужно добавить в общие количества строк, перечисленные выше.
Резюме В этой главе мы рассмотрели, как можно оценить требования, предъявляемые к вашей БД не только во время запуска, но также и через год или больший промежуток времени. Оценка размеров данных и индексов и оценка строк тесно связаны с будущей работой. Архивирование — один из наиболее эффективных методов для получения существенного улучшения функционирования зрелой БД. Мы рассмотрели наиболее важные требования к аппаратным средствам с точки зрения места хранения данных, производительности обработки и оперативной памяти. С этим связано требование, чтобы вы потратили время на ознакомление непосредственно со средствами анализа функционирования, чтобы, когда возникнет необходимость, вы имели бы навыки устранения причин, порождающих проблемы функционирования. Более конкретно мы увидели, какие возможны конфигурации накопителей на жестких дисках и как они влияют на работу и избыточность данных. Очевидно, чем более высокая скорость процессора, чем больше количество приобретенной памяти, тем лучше. Но мы также увидели, насколько важно правильно сконфигурировать SQL Server, чтобы воспользоваться преимуществами процессора и памяти, особенно когда сервер может бороться за ресурсы с другими приложениями. Мы теперь на стадии завершения проектирования нашей системы, которую продолжим в следующей главе.
•
5
9
4
Завершение проектирования Введение На протяжении всех тринадцати глав мы рассматривали нашу СУБД от концепции до рабочей БД. Мы собрали данные относительно того, что хочет пользователь, построили модели этих данных, создали таблицы, рассмотрели различные механизмы защиты и, наконец, добавили код, который действует как своего рода связующее звено между пользователем и данными. Так что, наша работа подошла к концу? Ну, хотя OLTP-часть системы теперь и завершена, полна, мы все еще имеем несколько моментов, которые нужно привести в порядок. •
Настройка функционирования — Хотя мы коснулись выполнения настройки в предыдущих главах, но главным образом обращали внимание на то, как транзакции могут воздействовать на систему.
•
Отчеты — Теперь, когда наша OLTP-БД укрепилась и мы в состоянии работать с ней, можно оптимизировать ее с точки зрения требований отчетов, которые определены на стадии логического проектирования. Для больших специализированных проектов (особенно включающих большие количества данных, или даже механических клиентов, использующих данные двадцать четыре часа в сутки) нам может потребоваться сделать копии (или частичные копии) данных только для чтения, чтобы использовать их для отчетов. Это уменьшит конфликтные ситуации с данными в наших первичных БД. Мы рассмотрим проектирование БД только для чтения, чтобы выполнить такие требования. Отметим также несколько видов использования выделенных структур только для чтения, отличных от простого формирования отчетов, типа формирования оперативного хранилища данных (Operational Data Store — ODS).
Глава 14 Q
Моделирование предприятия — Как у архитекторов общих данных, одной из наших задач должно быть построение модели, которая не только охватывает работу, которую мы сделали для одного проекта, но и всех проектов, которые были до этого, и даже новейшие пакеты, которые были созданы различными поставщиками. Цель моделирования предприятия состоит в том, чтобы иметь запись всех данных в нашей организации и увеличить полную нормализацию на общем уровне.
•
Реализация с тремя стадиями — Означает цикл проектирования и производства, включающие стадии разработки, проверки харктеристик и реализации. Мы обсудим, как строить такую конфигурацию. Будут представлены несколько руководящих принципов, как обезопасить себя от системных проблем на стадии функционирования, типа перебоев в электропитании и общих отказов аппаратных средств. Хотя настройка системы на стадии функционирования, строго говоря, не является обязанностью архитектора данных, на моем опыте архитектор данных часто вынужден включаться и может быть ответственным, если соответствующее дублирование и план при аварийных ситуациях не будут определены в структуре системы.
Настройка функционирования Процесс настройки функционирования сложен и полностью отделен от проектирования БД. Мы должны также иметь в виду, что это — бесконечный процесс. Разработчикам может потребоваться повышение эффективности функционирования, и испытатели (особенно выполняющие напряженные испытания) с надеждой будут использовать почти каждую дополнительную возможность настройки функционирования. Как только реальная система создана и запущена, мы снова будем настраивать функционирование. Настройка функционирования — ответственность администратора БД, а не архитектора данных. Тем не менее, при проектировании БД мы должны знать критические моменты, которые затронут функции администратора БД, и должны регулярно с ним консультироваться. Следующий перечень содержит причины, почему мы обходили стороной серьезное обсуждение настройки функционирования во время нашего проектирования: Настройка функционирования не должна основываться на догадках — Функционирование должно рассматриваться для всей системы, а формирование ваших соображений насчет функционирования на основе предыдущих экспериментов не всегда оптимально. SQL Server обеспечивает некоторые чрезвычайно мощные средства для выявления мест, где существуют проблемы в наших системах. Используя SQL Server Profiler, мы можем определить, какие запросы требуют длительное время на выполнение, и посмотреть, как долго будут протекать процессы до и после изменения. Это, вместе с хорошими напряженными испытаниями, поможет определить, являются ли индексы, усовершенствования аппаратных средств или даже денормализация лучшим вариантом для полного пакета системы. Следует мимоходом отметить, что проверка функционирования не означает изменения модели БД отменой нормализации и, разумеется, денормализации ради этого, так как она может просто привести к деградации функционирования при попытке управлять таблицами БД. Хотя денормализация может быть желательна в некоторых случаях, вы начнете заново вводить различные виды аномалий корректировки, которые мы старались удалить в процессе нормализации. Нужно или жить с этими аномалиями, или написать дополнительный код, чтобы бороться с ними. Умелый администратор БД, который может настраивать БД без необходимости выполнять денормализацию, стоит на вес золота.
596
Завершение проектирования Настройка функционирования изменяется в различных версиях SQL Server — В то время как более ранние версии требовали, чтобы проектировщик выполнял большую часть оптимизации вручную, последние версии (7.0 и 2000) добавили богатый спектр функциональных возможностей настройки функционирования. Вообще, я теперь пришел к выводу, что часто лучше всего отдать управление оптимизацией системе SQL Server. Дальнейшие детали по этому вопросу могут быть найдены в "Professional SQL Server 2000 Programming" (Wrox Press, ISBN 1861004486). Лишние индексы очень дорого стоят — В интенсивно записывающей системе типа OLTP-БД слишком много индексов могут вызвать проблемы функционирования. Это возникает вследствие того, что когда в основной структуре сделаны изменения, мы существенно увеличиваем стоимость выполнения добавления, обновления или удаления в связи с тем, что приходится манипулировать и структурами таблиц, и индексами. При создании такой БД использование индексов может быть привлекательным решением, но это может воздействовать на полное функционирование системы, особенно, если данные непрерывно добавляются. С другой стороны, в случае БД или даже таблицы, которая часто не изменяется, соответствующее использование индексов может привести к реальному улучшению функционирования. Когда возникают проблемы функционирования, мы можем использовать SQL Server Profiler, чтобы пронаблюдать точные вызовы, которые делают наши клиенты. Query Analyzer тогда позволит нам рассмотреть следующее: •
План запроса — Просматривая, как SQL Server планирует выполнять запрос, мы можем видеть, делает ли он какие-либо нелогичные предположения, которые мы должны улучшить, или переписывая наши запросы (подобно удалению из запроса данных, которые мы никогда не используем), используя подсказки оптимизатора относительно улучшения запроса, или, возможно, добавляя индексы, но с соответствующим предостережением, как упомянуто выше.
•
Временная статистика и статистика ввода/вывода — Мы можем установить Query Analyzer, чтобы он возвращал информацию относительно времени, занятого запросом, или сколько и какого типа работы диска требуется для запроса.
Q
"Мастер" настройки индекса — Использование этого средства определит, используете ли вы индексы, являющиеся оптимальными в вашей БД.
•
Команды DBCC — DBCC (Database Consistency Checker — модуль контроля непротиворечивости БД) устанавливается для контроля непротиворечивости БД, и имеется весьма немного очень полезных команд DBCC, которые можно использовать для настройки функционирования. Они включают SHOW_STATISTICS, которая показывает страницы статистики для индексов, чтобы помочь определить, почему они используются/не используются, и SHOWCONTIG, которая показывает, как фрагментирована таблица или индекс.
Эти средства, однако, только скользят по поверхности. Имеется много других методов, которые мы можем использовать для настройки запросов, БД и серверов, чтобы получить максимальную эффективность. Создание индексов всегда имеет смысл делать для улучшения функционирования, но они могут дать слишком много реальных проблем. Можно предпринять ограниченную денормализацию типа отмены разбиения на уровне конкретных таблиц, где имеются проблемы (обычно связанные с отменой разбиения 4НФ или выше), но только после того, как был выполнен некоторый уровень испытаний.
597
Глава 14 Для удачного начала настройки БД в SQL Server можно рассмотреть следующие книги: •
"Inside SQL Server 2000" (Microsoft Press, ISBN
•
"Professional SQL Server 2000 Programming" (Wrox Press, ISBN
•
"Professional ASP Data Access" (Wrox Press, ISBN
0735609985) 1861004486)
1861003927)
Поддержка БД только для чтения Если наши потребности формирования отчетов не в режиме реального времени, то есть мы можем позволить некоторое время ожидания между текущими данными и данными, с которыми пользователи имеют дело, то мы можем построить БД только для чтения, которая является зеркальным отображением наших активных БД. Пользователи, которым нужно запускать дорогостоящие запросы относительно данных, могли бы тогда использовать эту БД только для чтения. В зависимости от того, как БД используется (и после того, как мы загрузили данные в БД), можно установить БД только для чтения, используя следующий оператор: EXEC sp_dboption '<имя_БД>', 'read only', 'true'
Мы можем затем переключиться назад в режим записи всегда, когда нам нужно загрузить дополнительные данные в БД (так как нельзя использовать режим только для чтения, когда вы желаете освежить БД). Задание БД режима только для чтения имеет очень большие преимущества, сообщая SQL Server, что он фактически не должен использовать блокировку ресурсов БД, так как никто не может делать изменения данных или структур БД. Обычно мы стараемся выполнить все записи в базах, используемых только для чтения, когда никакие пользователи не хотят читать данные. Прежде чем пойти дальше, нужно ответить на вопрос: "Почему нужно создавать БД только для чтения?" Ответом на этот вопрос может быть следующее: I.
Максимизировать параллелизм в наших пользовательских системах БД.
2.
Максимизировать функционирование на наборе запросов.
3.
Объединить данные из нескольких источников.
Суть заключается в том, что хотя все наши данные должны иметь свой оригинал в соответствующей нормализованной OLTP-БД, имеются ситуации, где это определенно не будет иметь место: •
598
Наследуемые БД — универсальные ЭВМ обычно упоминаются как наследуемые системы, но поскольку прошло только несколько лет, как мини- и даже PC-платформы начали развивать платформы сервера, мы можем получить наследуемые БД, основанные на любой платформе.
Завершение проектирования •
Пользовательские БД — к сожалению, не все БД построены на сервере БД. Существует много таких, как Access, Foxpro или другие платформы БД.
•
Другие — категория "другие" названа так потому, что имеется слишком много вариантов, чтобы их упоминать. Электронные таблицы, текстовые файлы и много других способов хранения данных могут быть использованы в конкретной организации и могут потребоваться для ввода в системы, используемые только для чтения.
Как только мы преобразовали данные из наших БД в систему, используемую только для чтения, мы получим следующие преимущества: •
Ограниченное число конфликтных ситуаций — так как все пользователи БД будут неспособны изменить данные, имеется очень небольшой шанс, что параллельные пользователи будут блокировать одни и те же части данных друг от друга. Всякий раз, когда мы можем гарантировать, что никакие процессы не будут записывать данные в БД, мы можем задать для БД режим только для чтения, и никакие блокировки не будут работать. Если мы имеем непрерывно обновляемую систему, то могли бы запустить отчеты на повторяемом читаемом уровне изоляции, который будет означать, что мы не должны волноваться относительно внешнего процесса, изменяющего результаты наших отчетов, когда мы их запускаем. Заметьте, конечно, что мы не можем исключить конфликтную ситуацию в пределах нашей подсистемы аппаратных средств или методов загрузки, так как когда порция данных копируется из нашей OLTP-системы, она будет блокирована для пользователей, пока идет передача данных.
•
Быстрое функционирование — в зависимости от ситуации конкретные запросы в нашей пользовательской БД могут быть оптимизированы, или, если желательно, мы можем удалить все запросы из пользовательской БД. Имейте в виду, что большие преимущества функционирования будут достигнуты, когда БД, используемые только для чтения, не находятся на том же сервере, что и пользовательские БД.
В следующем разделе мы рассмотрим несколько возможностей для структурирования нашей БД только для чтения, обсудим влияние на функционирование, дадим несколько советов реализации решения и, наконец, несколько примеров того, как использовать такие БД на практике. Следует отметить, что в наших обсуждениях мы не переходим на уровень хранилища данных или витрины данных (реализованных в OLAP). Вместо этого мы рассматриваем более общие решения непосредственных проблем, как мы обсуждали в главе 9, типа интенсивного использования клиентов, нерегламентированного формирования отчетов для текущих данных и ODS. Активность чтения — очень важная часть разрешения проблемы для нашей OLTP-системы, чтобы остаться настолько нормализованной, насколько возможно, забрав нагрузку от наших серверов и особенно сокращая состязание между читающими и пишущими пользователями.
22-1868
5 9 9
Глава 14
Моделирование изменений В этом разделе мы будем использовать следующий набор из четырех простых таблиц для иллюстративных целей: product productld number name price
nvoice invoiceld • customerld (FK) date number
customer customerld name
nvoiceLineltem invoiceLineltemld • productld (FK) invoiceld (FK) quantityPurchased
Здесь product — таблица "изделие"; productld — идентификатор изделия; number — номер (изделия); name — название (изделия); price — цена; customer — таблица "заказчик"; customerld — идентификатор заказчика; name — имя (заказчика); invoice— таблица "счет-фактура"; invoiceld — идентификатор счета-фактуры; date— дата; number — номер (счета-фактуры); invoiceLineltem — таблица "элемент счета-фактуры"; invoiceLineltemld — идентификатор элемента счета-фактуры; quantityPurchased — проданное количество. Очевидно, что это является упрощенной моделью системы счета-фактуры, и что мы, например, игнорировали факт, что значение цены изменится через какое-то время. Однако этот набор таблиц будет служить иллюстрацией общей системы, откуда должны быть получены отчеты. В действительности, мы можем иметь намного более сложную ситуацию, наподобие следующей: <
^
Универсальная ЭВМ ИУС
БД Access \
f d
H^i Выборка только для чтения ч -~-— —^
Oracle
/ i d
"
X
H^ SQL Server 6.5 ——-*'
Здесь ИУС — информационно-управляющая система. 600
Электронные таблицы Excel
i d
I^3 SQL Server 2000 — • — - • '
Завершение проектирования Это означает, что ваше хранилище данных может получать данные более чем от одной БД. Обратите внимание, что я включил в диаграмму электронные таблицы Excel — это не ошибка! Это не исключительный случай, когда электронные таблицы являются "домом" части данных, особенно, когда нет технического анализа при создании и управлении БД. Заметьте, что мы не защищаем, конечно, использование электронных таблиц как хранилища данных, просто допускаем, что это часто случается. К счастью, мы можем импортировать данные почти отовсюду, используя сервисы преобразования данных, создавая ADO-клиенты или используя некоторые из функций в SQL Server, которые мы обсудили. Моделирование систем, используемых только для чтения, похоже на задачу моделирования OLTP-БД, за исключением того, что мы можем в значительной степени игнорировать правила нормализации. Имеются два метода, которые я нашел полезными для выполнения этой задачи. Один яваляется очень прагматичным, а другой — открытым решением, которое позволяет в более свободной форме просматривать данные: •
Ориентируемый на таблицы — Общедоступный метод создания БД для отчетов, где представлена каждая важная таблица, которая не содержит лишь данные домена.
•
Ориентируемый на решение — Если мы должны решить очень конкретную проблему, то обычно не нужно использовать столь радикальный подход, как предыдущий метод. Мы обсудим, почему мы могли бы пожелать включить только то, что необходимо для обеспечения конкретных нужд.
Имейте в виду, что как всегда я не утверждаю, что они являются единственными методами для выполнения этого вида действия. Перечисленные здесь методы являются пищей для размышления, так как они являются точными решениями конкретных проблем. Так как мы рассматриваем БД только для чтения, единственное требование — то, что данные в БД соответствуют исходным данным для некоторого конкретного момента времени. Кроме того, структура вызывает меньше беспокойства, потому что данные не изменяются (за исключением системных процессов, изолированных от пользователя).
Метод, ориентируемый на таблицы Что мы должны сделать, так это взять копию нашей OLTP-БД и расширить каждую сущность так, чтобы она включала столько данных из таблиц-предков, сколько возможно. В нашем сценарии примера таблица i n v o i c e (счет-фактура) имеет в качестве предка таблицу customer (заказчик), а таблица invoiceLineltem (элемент счета-фактуры) имеет i n v o i c e и customer с одной стороны и product (изделие) — с другой. В этом случае сущность и определение таблицы invoiceLineltem включает определение того, что было куплено, счет-фактуру, заказчика и изделие. В таком подходе есть преимущества, так как когда пользователь хочет посмотреть invoiceLineltem, он обычно будет хотеть знать, какой заказчик купил конкретное изделие. Следовательно, мы включаем ссылку на заказчика в таблицу invoiceLineltem. Одна концепция в этом сценарии не изменяется — каждый атрибут объекта должен описывать объект хотя бы только косвенно. Так как мы будем включать информацию о заказчике как элемент строки счета-фактуры, мы явно нарушаем правила нормализации, хотя значения в таблице фактически описывают информацию строки счета-фактуры, а именно, заказчика, который купил изделие.
601
Глава 14 Мы можем также включить дополнительные столбцы для итоговых данных, которые характеризуют объект. Если мы хотим иметь общую сумму всех счетов-фактур, то поместим это в таблицу i n v o i c e , а не в invoiceLineltem. Цель не в том, чтобы полностью устранить соединения, а чтобы иметь возможность организовать разумную логическую структуру для оптимизации всего нашего функционирования. Следует упомянуть об одной дополнительной проблеме. Дисковое пространство. Наши потребности в дисковом пространстве, расширенные до окончательного формата, будут большими и в некоторых случаях станут фактическим препятствием в зависимости от размера исходных данных. Очевидно, вся система не должна формироваться таким образом, если требуются только небольшие ее части для обеспечения запросов. Первым шагом должно быть перемещение по нашим деревьям отношений и получение атрибутов. Из диаграммы нашего примера мы спускаемся сверху вниз, начиная с invoice и добавляя поля предка следующим образом:
customer | customerld I name
invoice invoiceld -• customerld (FK) date number customer_name
Здесь customer — таблица "заказчик"; customerld — идентификатор заказчика; name имя (заказчика); invoice — таблица "счет-фактура"; invoiceld — идентификатор счета-фактуры; date — дата; number — номер; customer_name — имя заказчика. customer_name теперь является частью таблицы i n v o i c e , так как счет-фактура предназначен для заказчика. Столбец customerld уже присутствует в таблице customer, так что не нужно ничего предпринимать. Следующий шаг — посмотреть на таблицу invoiceLineltem. Мы добавляем сюда атрибуты таблиц i n v o i c e и product и получаем следующее: product productld number name price
customer customerld name
invoice invoiceld customerld (FK) date number
invoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased invoice_customerld invoice_date •• invoice_number i customer_name product_number product_name : product_price
Здесь product — таблица "изделие"; productld — идентификатор изделия; number — номер (изделия); name — имя (изделия); price — цена (изделия); customer — таблица "заказчик"; customerld — идентификатор заказчика; name — имя (заказчика); 602
Завершение проектирования invoice— таблица "счет-фактура"; invoiceld — идентификатор счета-фактуры; date— дата; number — номер (счета-фактуры); InvoiceLineltem — таблица "элемент счета-фактуры"; invoiceLineltemld — идентификатор элемента счета-фактуры; quantityPurchased — купленное количество; invoice_customerld — идентификатор счета-фактуры-заказчика; invoice_date — дата счета-фактуры; invoice_number — номер счета-фактуры; customer_name — имя заказчика; product_number — номер изделия; product_name — имя изделия; product_price — цена изделия. На следующем шаге следует взять любые величины, с которыми нам нужно выполнить вычисления или операции объединения строк, и добавить поле, содержащее результат. В таблице invoiceLineltem мы имеем столбец quantityPurchased, а также столбец product_price, и мы хотели бы перемножить значения этих полей, чтобы найти значение общей суммы для элемента таблицы, для чего мы добавим столбец, чтобы поместить это значение: invoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased invoice customerld invoice date invoice number customer_name product_number product_name product_price invoiceLineltenvvalue Здесь дополнительно invoiceLineItem_value
— сумма элемента счета-фактуры.
Последний шаг включает возвращение к цепочке и суммирование всех потомков. В этом случае мы можем взять отношение i n v o i c e к i n v o i c e L i n e l t e m и просуммировать все изделия в счете-фактуре: nvoice invoiceld customerld (FK) date number _ customer name invoiceUneltertvvalueTotal invoiceUneltem_nurnberLineltems invoiceLJneltem_productCount invoiceLinettem totalProductSoldValue
nvoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased — __ — —Ш i n v o i c e _ c u s t o m e r l d invoice_date invoice_number customer_name product_number product_name product_price
Здесь дополнительно invoiceLineItem_valueTotal — общая сумма элементов счета-фактуры; invoiceLineItem_numberLineIterns — число элементов счета-фактуры; invoiceLineItem_productCount — общее число изделий элементов счета-фактуры; invoiceLineItem_totalProductSoldValue — общая стоимость элементов счета-фактуры. 603
Глава 14 Возьмем, например, столбец invoiceLineItem_valueTotal. Он будет заполнен суммированием всех столбцов invoiceLineItem_value (quantityPurchased * p r o d u c t P r i c e ) для данного счета-фактуры. Значение invoiceLineItem_numberLineItems было бы полным числом элементов счета-фактуры. Мы должны сделать то же самое для product, а затем customer, получив следующую диаграмму: product productld number name price invoiceLineltem_totalProductSoldCount invoiceLineltem_totalProductSoldValue
customer customerld name invoice_totalValue invoice totalCount
invoice invoiceld customerld (FK) date number customer_name invoiceLineltem_valueTotal invoiceLineltem_numberl_ineltems invoiceLineltem_productCount invoiceLineltem totalProductSoldValue
invoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased invoice_customerld invoice_date invoice_number customer_name product_number product_name product_price invoiceLineltem value
Здесь дополнительно invoiceLineItem_totalProductSoldCount — общее число проданных изделий в счете-фактуре; invoice_totalValue — общая сумма счета-фактуры; invoice_totalCount — общее количество счета-фактуры. Имеется ряд достоинств этого подхода.
604
•
Простота использования — Так как все связанные данные находятся в одной и той же таблице, построение отчета пользователем или программистом с помощью стандартного средства типа Seagate Crystal Reports относительно просто.
•
Распознаваемые данные — Пользователи будут знать имена таблиц и полей в пределах таблицы, так как мы перешли к большим длинам, чтобы дать нашим объектам имена, которые имели бы смысл для пользователей.
•
Полезные итоговые данные — целью является просто попробовать переместить данные вниз по дереву отношений, чтобы устранить объединения. Это дает размещение всех данных, в которых мы будем нуждаться, на одном уровне в реляционной иерархии БД.
•
Относительно легкая реализация — В большинстве случаев мы выполняем простые запросы, которые включают один или большее количество потомков и один или большее количество предков. Это, конечно, не приведет ко всеобщему упрощению, поскольку все будет зависеть во многом от того, насколько сложны ваши структуры БД.
Завершение проектирования Например, чтобы загрузить таблицу product, м ы могли бы запустить следующий код: INSERT orderReport.dbo.product(productld, number, name, price, invoiceLineItem_totalProductSoldCount, invoiceLineItem_totalProductSoldValue) SELECT product.productld, product.number, product.name, product.price, SUM(invoiceLineltem.quantityPurchased) AS invoiceLineItem_totalProductSoldCount, SUM(product.price * invoiceLineltem.quantityPurchased) AS invoiceLineItem_totalProductSoldValue FROM dbo.product AS product JOIN dbo.invoiceLineltem AS invoiceLineltem ON product.productld = invoiceLineltem.productld GROUP BY product.productld, product.number, product.name, product.price
He каждый ответ будет легко доступен, но м ы в основном стараемся обеспечить формирование нерегламентированных отчетов, делая выходные данные более легкими для понимания пользователем. Это возможно, потому что м ы удалили всю иерархию из модели данных и скорректировали данные так, как требуется пользователю. Например, если м ы хотим знать общее количество изделий 'XYY43233', на которое был выписан счет-фактура в январе 2001 г. заказчику 10, м ы могли бы просто закодировать оператор SELECT из единственной таблицы: SELECT SUM(invoiceLineItem_value) FROM invoiceLineltem WHERE invoice_customerld = 10 AND i n v o i c e _ d a t e < 'Feb 1 2001' AND i n v o i c e _ d a t e >= ' J a n 1 2001' AND product_number = 'XYY43233'
С другой стороны, мы могли бы преобразовать данные, предварительно объединив сложные величины, и (раз мы помещаем данные в БД только для чтения) рассмотреть использование представлений, чтобы выполнить любые относительно простые вычисления. Это, конечно, уменьшит кодирование, необходимое, чтобы сформировать такую БД только для чтения, а также ослабит требования к области данных. Использование представлений таким образом может оказаться более эффективным, чем кодирование, в то время как одновременно допускает большую гибкость, если требования к формированию отчетов изменятся в будущем.
Метод, ориентируемый на решение Метод формирования БД, ориентируемый на решение, более легок для объяснения, чем метод, ориентируемый на таблицы. В некоторых случаях нам просто нужен способ получения некоторых данных быстрее, чем мы можем получить их из нашей OLTP-БД, и мы можем сформировать решение, ориентируемое на извлечение данных, необходимых для этого. Вместо создания полной модели БД, как мы делали ранее, мы могли бы решить обеспечить ответы на очень конкретный набор вопросов. Возьмем, например, отчет о заказчиках и изделиях, купленных ими на прошлых неделе, месяце или годе. Используя ту же самую иллюстрацию БД из предыдущего примера, мы могли бы построить запрос, используя только таблицу i n v o i c e L i n e l t e m , и если мы решим построить полную БД, основанную, как и прежде, на таблице, мы могли бы создать дополнительную таблицу, чтобы разобраться с этой ситуацией. 605
Глава 14 Однако рассмотрим ситуацию, где запрос занимает сорок минут, чтобы получить данные из исходной OLTP-БД, а коммерческий штат нуждается в этих данных непосредственно с клавиатуры быстро и эффективно. В этом случае мы можем просто построить таблицу, подобную следующей: customerProduct customerld productld customer_name product_name product_price product_number week_quantityPurchased week_valuePurchased month_quanityPurchased montti_valuePurchased year_quanityPurchased year_valuePurchased build date
Здесь customerProduct — таблица "заказчик-изделие"; customerld — идентификатор заказчика; productld — идентификатор изделия; customer_name — имя заказчика; product_name — название изделия; product_price — цена изделия; product_number — номер изделия; week_quantityPurchased — проданное количество за неделю; week_valuePurchased — стоимость проданного за неделю; month_quantityPurchased — проданное количество за месяц; month_valuePurchased — стоимость проданного за месяц; year_quantityPurchased — проданное количество за год; year_valuePurchased — стоимость проданного за год; build_date — дата формирования. В самом общем случае таблица будет содержать ответ на их запрос. Здесь мы организовали пересечение таблиц customer и product и обеспечили данные о продажах изделия в течение прошлых недели, месяца и года. Эта модель является гибкой в том, что если требование будет таким, что пользователь захочет просуммировать типы изделий, мы могли бы добавить поле для типа, но если мы имеем дело с десятками тысяч изделий, вероятно, хорошо будет суммировать изделия по их типам. Мы включаем столбцы идентификаторов (Id) как первичный ключ, чтобы эта таблица могла бы быть связана с исходной OLTP. В дополнение к этой итоговой таблице пользователю могла бы потребоваться и более детальная информация о заказчике; в этом случае мы могли бы добавить таблицу-потомок, чтобы иметь эту информацию. Место, где я наиболее часто использую этот вид итоговых данных, не имеет никакого отношения к продажам, а скорее к содержанию Web-сайта, наподобие обслуживания набора страниц, персонифицированных по типу пользователей. Нормализованные свойства БД, содержащих данные сети, могут быть чрезвычайно уродливыми, однако мы должны обеспечить очень высокий уровень параллелизма при представлении страниц, а также разрешить пользователю делать с OLTP-данными то, что он хочет. Как только содержимое подготовлено, скажем, для общего титульного листа сайта, можно построить версию данных только для чтения, чтобы обеспечить Web-сайт. Здесь мы в результате получаем сильно денормализованную БД, возможно, содержащую одну единственную таблицу. Хотя это и похоже на решение, основанное на таблицах, в этом случае мы имеем меньшее количество таблиц (и также меньший поднабор данных), и каждая из этих таблиц ориентируется на конкретный запрос, а не на описываемый объект. 606
Завершение проектирования Реализация Как и с OLTP-системой, мы снова должны рассмотреть реализацию создания нашей БД только для чтения, но на сей раз мы хотим сделать упреждающее действие, когда не только учитываем функционирование, но и исключительные ситуации и охватываем все и каждые сценарии использования БД. Так как непротиворечивость БД только для чтения управляется подсистемами преобразования, все, о чем нам нужно заботиться — сделать формирование отчетов быстрым. В этом разделе мы рассмотрим две из наиболее важных проблем для систем только для чтения — аппаратные средства и индексы. Аппаратные средства Поскольку эта тема была рассмотрена в предыдущей главе, мы только напомним вам о важности не пренебрегать аппаратными средствами при создании системы только для чтения. Может даже быть выгодно иметь отдельный сервер для размещения данных, в зависимости от объема ваших операций. Если мы физически располагаем данные только для чтения на том же самом сервере, на котором размещается текущая OLTP-БД, одновременно уменьшая содержимое в пределах БД, мы все же можем разобраться с содержимым аппаратных средств. Даже если вы построили лучшую из возможных подсистем дисков, вам, вероятно, все же придется купить дополнительный сервер из-за ограничений процессора при одновременной обработке пользователей, запускающих большие отчеты, и других пользователей, пытающихся модернизировать основную OLTP-БД. Индексы При создании БД только для чтения индексы являются неотъемлемой частью проекта, особенно, когда мы допускаем использование нерегламентированных запросов относительно данных. Так как БД только для чтения будет обновляться нечасто, включение в нее слишком большого числа индексов вряд ли будет главной проблемой. Это, конечно, зависит от фактического размера БД, так как если вы имеете терабайты в вашей системе только для чтения, то, что я здесь предлагаю, не может быть реализовано. В среде, предназначенной для нерегламентированных запросов, единственной стратегией, которая даст большие преимущества, является создание индексов для каждого конкретного столбца вашей таблицы. Рассмотрим наш пример таблицы invoiceLineltem, которую мы построили для поиска, ориентированного на таблицы: invoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased invoice customerld invoice date invoice number customerjname product_number product_name product_price invoiceLineltem_value (Обозначения см. выше)
607
Глава 14 У этой таблицы мы построили бы индексы для всех столбцов индивидуально. Это позволит оптимизатору выбрать любой или несколько любых индексов таблицы для выполнения запроса, используя неявный запрос. Неявный запрос — это запрос, когда SQL Server не должен просматривать фактические таблицы, чтобы получить требуемые данные. Так как узлы индекса содержат данные, которые необходимы, чтобы построить индекс, то если мы ссылаемся только на столбцы индекса или индексов, SQL Server никогда не должен будет читать фактические данные. Обратите внимание, что мы можем даже включать индексы в столбцы, которые в противном случае могло бы показаться глупо индексировать. Возьмем случай столбца, который содержит род ("М" — мужской, " F " — женский). Он обычно считается ужасным индексом для обнаружения значений. Однако если мы хотим видеть, сколько мужских или женских значений находится в таблице, то можем выполнить неявный запрос с помощью индекса рода, и таким образом осуществить доступ только к столбцу рода. Это будет экономить большое количество порядков, потому что такой индекс будет очень небольшим и, конечно, более эффективным, чем использование всех страниц данных, особенно в расширениях наших таблиц только для чтения. Возьмем, например, запрос доступа к таблице invoiceLineltem: SELECT SUM(invoiceLineItem_value) FROM invoiceLineltem WHERE invoice_customerld = 10 AND invoice__date < ' Feb 1 2001' AND i n v o i c e _ d a t e >= ' J a n 1 2001' AND product_number - 'XYY43233'
В таблице i n v o i c e L i n e l t e m мы обратились только к столбцам invoiceLineItem_value, invoice_customerld, i n v o i c e _ d a t e и product_number. При наличии индекса для каждого столбца мы дали SQL Server возможность самому решить, каков лучший первый шаг, используя статистику индексов, чтобы задать несколько простых вопросов: •
Сколько строк я получу от invoice_customerld = 10?
•
Сколько строк возвратится от i n v o i c e _ d a t e < ' Feb I 2 001 ' и invoice_date > = ' J a n 1 2001 ' ?
•
Сколько строк я получу от product_number = 'XYY43233'?
•
Насколько накладнее будет получить invoiceLineItem_yalue из таблицы, а не с помощью индексов?
Оптимизатор будет не только выбирать лучший план, но также и определит, имеем ли мы один счет-фактуру или миллион между указанными датами, а также и вероятность строк с указанным product_number. Мы не делаем это в нашей OLTP-БД из-за больших издержек, связанных с поддержанием индексов, но мы считаем, что в большинстве случаев будем делать записи в нашу БД только для чтения, когда никакие пользователи не читают данные.
608
Завершение проектирования Время ожидания Время ожидания в этом случае представляет количество времени, которое требуется для части данных, созданной до того, как будет доступна в нашей БД. В этом разделе мы рассмотрим факторы, с которыми связано время ожидания нашей БД только для чтения.
Определение частоты обновления Определение, как часто следует обновлять БД только для чтения, зависит от нескольких факторов, которые объясняются в следующем списке. Нам должно быть ясно, что так как имеются ограничения финансов и ресурсов, на практике вы никогда не достигнете идеала мгновенных корректировок. •
• *
Как часто пользователь нуждается в этом — если данные нужны немедленно, то нам, вероятно, придется использовать все остановки, чтобы получить корректировку настолько быстро, насколько возможно. Насколько фактически занят OLTP-Server — если сервер используется, чтобы хранить измерения от промышленных роботов или обращаться с заказами в реальном масштабе времени, не удастся анализировать данные немедленно из-за нагрузки на сервер БД.
•
Свойства соединений — в зависимости от скорости соединения, конкуренции требований к нашей полосе пропускания и относительной близости OLTP-системы к версии только для чтения, может быть неразумно перемещать массивные количества данных по сети в течение рабочего времени.
•
Состояние соединения — когда имеется несколько клиентов, которые получают данные, они могут даже и не иметь возможность соединяться с сервером, кроме как по требованию. Это относится к карманным и портативным ПК пользователей, которые получают копию данных от сервера. Размер данных — количество важных данных в системе изменяется от нескольких записей до терабайт данных. Если мы рассмотрим OLTP-систему с 200 Гб данных, то в этом случае мы не в состоянии извлечь всю БД, но сможем, конечно, извлекать и объединять некоторое количество данных для более быстрого использования. Размер данных приводит к неприятному выводу, что чем они больше, тем мы должны выполнить больше дополнительных шагов для построения БД только для чтения и тем труднее это фактически реализовать. В некоторых случаях мы сможем обеспечить увеличивающуюся корректировку, так как редко требуется каждый раз передавать всю БД. Пригодность ресурсов — если вы можете позволить себе только сервер основного уровня, то затрата времени на преобразование OLTP-данных в БД только для чтения может быть менее эффективной, чем реализация некоторой денормализации OLTP-данных в той же самой БД и создание по мере необходимости хранимых процедур/представлений.
•
•
Я хотел бы построить для вас матрицу факторов, связанных со временем ожидания, но не сделал этого. Не существует никакой отдельной функции или набора функций, которые мы можем использовать, чтобы решить, что делать и когда. Взаимодействие между этими факторами сложно, и с каждым проектом нужно обращаться индивидуально при определении частоты обновления, хотя потребности конкретного пользователя должны учитываться всюду, где это возможно, в первую очередь.
609
Глава 14 Реализация Реализация БД только для чтения намного более легкая, чем в случае OLTP-БД. Мы имеем три средства обслуживания, полностью доступные нам для преобразования данных из OLTP-БД. •
Сервисы преобразования данных — мощное средство для преобразования данных из одной формы в другую. Оно позволяет использовать в преобразованиях графическое отображение данных, наряду с использованием языков скриптов (VBScript, JavaScript).
•
Хранимые процедуры — мы можем написать запросы, которые берут данные в одной форме и помещают их как строки в новую БД. Для многих случаев это самый простой и самый быстрый способ строить преобразования, особенно, когда они работают между БД в одном и том же сервере.
•
Тиражирование — используется как инструмент, чтобы копировать данные из БД в БД; действительно удобный способ осуществления преобразования — с помощью хранимой процедуры тиражирования. Таким образом мы можем публиковать одну БД в другую, обходя стандартные средства добавления, обновления и удаления в SQL и используя вместо этого хранимые процедуры. В БД только для чтения мы можем строить хранимые процедуры, чтобы работать в каждой ситуации, когда изменяется запись или таблица, и соответственно изменять данные в итоговых таблицах. Недостаток этого плана состоит в том, что мы должны реализовать инициализацию хранимых процедур, которая может быть довольно хитрой по сравнению с чистыми преобразованиями, когда мы должны добавить или удалить данные из итоговых результатов. В качестве примера рассмотрим текущее общее количество всех сделанных закупок в таблице customer (заказчик). Нам нужно будет закодировать хранимую процедуру INSERT, которая увеличивает значение текущего общего количества. Для процедуры DELETE нам нужно уменьшить значение, а для процедуры обновления UPDATE мы должны будем вычесть предыдущее значение и добавить новое значение. Мы можем также использовать тиражирование в качестве нашего средства распределения, чтобы перераспределить полную БД всем пользователям. С дополнительной информацией относительно сервисов преобразования данных и тиражирования читатель может познакомиться в "Professional SQL Server 2000 DTS " (Wrox Press, ISBN 1861004419) и "Professional SQL Server 2000 Programming (WroxPress, ISBN 1861004486).
С каждым из этих средств вы можете сформировать получение данных, удовлетворяющее разнообразным потребностям. Давайте рассмотрим ситуацию, когда мы имеем организацию по обработке заказов в Нэшвилле, а штаб-квартиру в Нью-Йорке. В то время как заказы поступают и на них выписываются счета-фактуры в Нэшвилле, общая штаб-квартира, куда поступают отчеты, суммирующие продажи, расположена в Нью-Йорке. Так как штаб-квартира в Нью-Йорке не имеет достаточно места, чтобы разместить любой из первичных общих компьютеров, разработан следующий проект:
610
Завершение проектирования
Штаб-квартира Нью-Йорк, штат Нью-Йорк Обработка заказов, Нэшвилл, штат Теннесси
Выписывание счетов-фактур [получение счетов;
Коммерческие пользователи в США Передвижная копия продаж Передвижная копия продаж Передвижная копия продаж Передвижная копия продаж
В свою очередь мы имеем три различных проблемы, с которыми приходится иметь дело: •
Преобразование данных (А) — Мы должны взять нормализованные данные и сформировать из них нужные данные.
•
Размещение данных на легких клиентах (В) — Это вряд ли разместит все данные на клиенте; скорее всего это небольшой, очень определенный набор данных, который даст возможность пользователю делать его работу лучше.
•
Загрузка данных с одного физического объекта на другой (С) — С нашим бизнесом, становящимся все более глобализованным, мы должны быть способны транспортировать данные по сети.
Теперь рассмотрим каждую из этих проблем отдельно.
Преобразование данных Преобразование данных из исходного нормализованного формата имеет два возможных направления: •
Очистить и начать снова — Всегда, когда создается БД только для чтения, мы должны создать метод первоначальной загрузки БД. Если требуются преобразования, мы должны будем синхронизировать БД с текущим состоянием OLTP-БД. Если размер данных разумно невелик, и имеется период времени, когда использование БД несущественно, мы можем просто удалить данные и начать снова. Это — самый легкий метод, когда вы должны обеспечивать ежедневное получение информации.
•
Использование в транзакции процедур тиражирования — Если нам требуется обновлять данные часто, или данные слишком большие, чтобы работать, очищая их и синхронизируя, мы можем написать процедуры тиражирования, чтобы поддерживать данные в БД.
611
Глава 14 В качестве примера закодируем процедуру из нашего примера с четырьмя таблицами и посмотрим, что мы должны будем делать, когда помещена новая запись в таблицу invoiceLineltem. Мы должны будем не только поместить новую строку в invoiceLineltem, но также и скорректировать таблицу product, как и другие таблицы в цепи отношений: CREATE PROCEDURE invoiceLineItem$replInsert ( QinvoiceLineltemld int, @productld int, @invoiceld int, @quantityPurchased int ) AS —
транзакция и обработка ошибок не помещены для ясности
INSERT INTO invoiceLineltem (invoiceLineltemld, productld, invoiceld, quantityPurchased, invoice_customerld ! , invoice_date, invoice__number, customer_name, product_number, product_name, product_price, invoiceLineItem_value) SELECT @invoiceLineItemID, @productID, @invoiceID, @quantityPurchased, invoice.customerlD, invoice.date, invoice.number, customer.name, product.number, product.name, product.price, invoice.quantityPurchased * @quantityPurchased FROM invoice JOIN customer ON customer.customerld • invoice.customerld ON @invoiceld = invoice.invoiceld — CROSS JOIN используется, потому что в обычном запросе мы бы объединяли -- эти два набора для таблицы invoiceLineltem, которая представлена — переменными величинами, гарантирующими декартово произведение двух — отдельных строк, которые возвращают одну строку CROSS JOIN product ON product.productld - @productld UPDATE product — добавление купленного количества к количеству проданных изделий SET invoiceLineItem_totalProductSoldCount = invoiceLineItem_totalProductSoldCount + @quantityPurchased, — добавление купленного количества, умноженного на цену для значения общей стоимости invoiceLineItem_totalProductSoldValue = invoiceLineItem_totalProductSoldValue + price * SquantityPurchased WHERE productld = @productld —
аналогичные типы операций должны быть для таблиц invoice и customer
GO
Обратите внимание, что мы не будем делать никакого суммирования, так как в это время только вставляем строку. Однако нужно учесть количества, которые были добавлены. Обычно это выполняется довольно быстро. К сожалению, все это может быть более
612
Завершение проектирования дорогостоящим, чем удаление всех данных и получение их снова для набора данных, подверженных постоянному изменению (они обычно меньше по своей природе). Может быть лучше попробовать просто удалить данные и добавить их снова, так как это требует намного меньше усилий от нас.
Размещение данных на мобильных клиентах Мобильные клиенты — работающие или с портативными, или карманными ПК. Лучший метод реализации — "по требованию" клиента при работе с разъединенными клиентами состоит в том, чтобы реализовать подписку тиражирования (так как мы не можем контролировать, когда и где они будут подключаться). Это позволяет клиентам подключаться каждый раз, когда они захотят, и почти так же, как почтовая программа обращается к серверу для получения электронной почты; SQL Server на клиенте вызывает главный сервер и запрашивает любые изменения. Этот тип тиражирования может также использоваться в случаях, когда мы должны обновить строки в других таблицах.
Загрузка данных с одного физического объекта на другой В этом случае тиражирование с помощью транзакции (где каждая транзакция, которая применяется к одной БД, будет использоваться для тиражирования) было бы способом решения с таким большим временем ожидания, которое может позволить клиент, или, по крайней мере, настроенное прежде всего на выполнение, когда нагрузка на сервере самая низкая.
у
Обратите внимание, что любой тип тиражирования использует загрузку с помощью транзакций, чтобы обеспечить синхронизацию, так что если вы должны сделать много изменений в любой из БД и хотите скопировать их, загрузка транзакции будет продолжать выполняться, пока агент по распределению или объединению не будет способен использовать все загрузки для нужной БД.
Использование Мы имеем здесь удобный случай, чтобы кратко перечислить несколько возможных использований БД только для чтения: •
Простые потребности формирования отчетов — здесь просто обращаются к отчетам для отдельных БД.
•
Оперативное хранилище данных — БД, где мы объединяем данные из всех наших внешних источников. Web-сайты — так как большинство содержимого, поступаемого от Web-сайта, используется только для чтения, любое содержимое, в котором нуждаются пользователи, может быть преобразовано из нормализованных структур в формат, который организован в соответствии с любым конкретным сайтом.
•
•
Портативные компьютеры — мы могли бы пожелать учесть пользователей портативных компьютеров (которые могут иметь копии данных о продаже или даже экспертные таблицы) так, чтобы они имели мгновенный доступ к самым последним данным.
Очевидно, здесь нет границ. Отделяя серьезных пользователей, которые читают данные, от серьезных пользователей, которые записывают данные, мы даем обеим группам пользователей огромное преимущество в работе. Теперь поглубже посмотрим на пару использований БД только для чтения. 613
Глава 14 Задачи простого формирования отчетов Обратите внимание, что когда я говорю "простой", то не отношу это ни к запросам, которые мы можем поддерживать, ни к сложности отчетов, а скорее к работе, которую нужно выполнить для настройки формирования отчетов. Обычно мы создаем наши БД для формирования отчетов из единственной БД. Это довольно общее использование БД только для чтения, так как, выполнив лишь небольшую работу, мы можем дать пользователю то, что ему нужно для ответа на вопросы без опасения случайного изменения чего-нибудь в исходной системе. Обычно в этом случае мы можем иметь два пути. Мы можем обеспечивать конкретные отчеты, которые нужны пользователю, подобно тому, как мы ранее поступали с таблицей customerProduct: invoiceLineltem invoiceLineltemld productld (FK) invoiceld (FK) quantityPurchased invoice customerld invoice date invoice number customer_name product_number product_name product_price invoiceLineltem_value (Обозначения см. выше) Мы делаем это, удаляя все соединения и давая пользователям отчет, у которого они могут настраивать параметры. Так как большинство отчетов основано на общем результирующем наборе информации, такой путь на самом деле работает хорошо для мало осведомленных клиентов с очень конкретными потребностями. Однако если мы имеем более осведомленных клиентов, то мы могли бы реализовать что-то более близкое к решению, ориентируемому на таблицы, которое мы раньше разработали. English
Query
Средство для формирования нерегламентированных отчетов, о котором, я уверен, вы слышали, называется English Query (запрос на английском языке). Это — продукт, который поставляется с SQL Server (начиная с версии 6.5) и используется, чтобы применить интерфейс на естественном языке для БД на SQL, переводя вопрос типа: Каково было общее количество стираемого ППЗУ (программируемое постоянное запоминающее устройство), на которое были выписаны счета-фактуры для компании "Bob's electronics" в прошлом году? ... в привычный для SQL вызов данных из нашей таблицы выше: SELECT S U M ( i n v o i c e L i n e I t e m _ v a l u e ) FROM i n v o i c e L i n e l t e m WHERE p r o d u c t _ n a m e = ' X ' AND c u s t o m e r _ n a m e = ' B o b ' ' s e l e c t r o n i c s ' AND i n v o i c e _ d a t e >= ' j a n 1, 2 0 0 1 ' AND i n v o i c e _ d a t e < ' j a n 1, 2 0 0 2 ' 614
Завершение проектирования Конечно, это может также работать и в OLTP-БД, и создаст следующий запрос: SELECT SUM(invoice.quantityPurchased * @quantityPurchased) FROM invoiceLineltem JOIN invoice JOIN customer ON customer.customerld = invoice.customerld ON invoiceLineltem/invoiceld = i n v o i c e . i n v o i c e l d JOIN product ON p r o d u c t . p r o d u c t l d = invoiceLineltem.productld WHERE product.name = 'X' AND customer.name = ' B o b ' ' s e l e c t r o n i c s ' AND i n v o i c e . d a t e >= ' j a n 1, 2001' AND i n v o i c e . d a t e < ' j a n 1, 2002' Если это выглядит как волшебство, то это не так. Имеется очень детальный семантический формат моделирования (который является XML-языком), который мы должны использовать для размещения "модели" БД с описателями для каждой таблицы, атрибута и отношений, так что общее количество и были выписаны счета-фактуры в нашем вопросе могут быть преобразованы в оба из вышеупомянутых запросов в различных БД. Мы были бы, конечно, должны смоделировать всю БД, для которой хотели бы распознавать запрос на английском языке, чтобы можно было выполнить эти запросы. Дополнительную информацию относительно English Query читатель может получить в "Professional SQL Server 2000 Programming" (Wrox Press, ISBN 1861004486).
Оперативное хранилище данных Это — БД, где мы берем все данные из любых (и, возможно, всех) наших БД, как показано на следующей диаграмме:
Универсальная ЭВМ ИУС
БД Access
Электронные таблицы Excel
SQL Server 6.5
SQL Server 2000
6 1 5
Глава 14 Это — БД, где мы объединяем данные из всех наших внешних источников данных. В оперативном хранилище данных мы будем использовать многие методы нормализации (а для формирования отчетов — некоторую денормализацию), которые мы обычно использовали при создании нашей OLTP-БД, чтобы сформировать довольно современный набор данных. Это позволит нам принимать решения, которые дадут возможность обеспечивать всю деловую организацию более эффективно. Оперативное хранилище данных будет содержать модель, которая является в значительной степени подобной нашей OLTP-БД, с некоторой степенью денормализации. Цель не состоит в том, чтобы построить одно оперативное хранилище данных для каждой OLTP-БД, скорее оно должно объединить организационные данные из всех общих источников в общее хранилище, полностью очищенное и с общими данными, согласованными со всеми другими данными. Например, пусть мы имеем две БД, одну — внешнюю и одну — собственной разработки, причем каждая из БД используется своей организационной единицей. Неудивительно, что каждая БД заполняет таблицу vendor (продавец) продавцами, работающими в каждой организационной единице. Первый шаг, который следует сделать, — смоделировать каждую из исходных БД, используя общие термины. Например, мы можем иметь две БД, которые моделируют продавцов совершенно по-другому. Или: vendor vendorNumber vendorName addressi address2 city
state zip
Здесь vendor — таблица "продавец"; vendorNumber — номер продавца; vendorName — имя продавца; addressi — первый адрес; address2 — второй адрес; city — город; state — штат; zip — почтовый индекс. Или:
vendor vendorld name j
vendorAddress faddressld (FK) ,.* vendorld (FK) [addressTypeld (FK) V •
addressType addressTypeld name description
address addr sssld « cityld (FK) stateld (FK) zipcodeld (FK) t 9
zipcode ! zipcodeld zipcode
addressLine 'addressld (FK) • addressLineld address ^sortOrderld
city L,._. 'stateld (FK) cityld i nanie
^ J
state I stateld code
Здесь vendor — таблица "продавец"; vendorld — идентификатор продавца; name — имя; vendorAddress — таблица "адрес продавца"; addressTypeld — идентификатор типа 616
Завершение проектирования адреса; address — таблица "адрес"; addressld — идентификатор адреса; addressLine — таблица "строка адреса"; addressLineld — идентификатор строки адреса; sortOrderld— идентификатор порядка сортировки; addressType — таблица "тип адреса"; name — название (типа адреса); description — описание типа адреса; zipCode — таблица "почтовый индекс"; zipCodeld — идентификатор почтового индекса; city — таблица "город"; city Id — идентификатор города; name — название (города); state — таблица "штат"; stateld — идентификатор штата; code — код штата. В случае этих таблиц выбранным путем было бы, вероятно, построение нашей модели оперативного хранилища данных, используя нормализованные структуры и преобразовывая данные первой структуры во вторую. Мучительно трудная часть создания оперативного хранилища данных — слияние существующих БД. Возвращаясь к первой главе, мы получим следующую диаграмму:
Извлечение, преобразование и загрузка
Все потоки данных через этот очищающий цикл извлечения, преобразования и загрузки, чтобы получить данные от каждой БД, должны совпадать с соответствующими данными от других БД. В заключительном оперативном хранилище данных мы хотим хранить данные в одной и той же манере для каждого их варианта; так, если мы имеем десять таблиц продавцов в десяти различных БД (или электронных таблицах, или любых используемых хранилищах данных) и мы имеем пять различных записей названия 'Joe's Fish Market' (рыбный рынок Джо) — 'Joe"s Fish Market', 'joes phish markt', 'joe fish' и 'joe market' — которые все представляют один и тот же рыбный рынок, мы объединяем содержимое всех этих строк в одну и ту же строку в нашем оперативном хранилище данных. Очевидно, я не дал вам достаточной информации, чтобы построить полное оперативное хранилище данных; скорее я попробовал дать достаточную информацию для читателя, чтобы начать размышлять о том, как начать процесс создания хранилища.
617
Глава 14
Модели данных предприятия Концепция — относительно понятная. Модель данных предприятия — это модель, которая покажет нам каждую таблицу и все отношения в каждой БД, с которой мы имеем дело. Таким образом, она позволяет архитектору иметь полное представление всех данных, которые использует корпорация. Звучит это хорошо. К сожалению, было бы удивительно, если можно было бы получить совершенную модель данных предприятия. В нашем учебном примере, который мы строили на протяжении книги, имеется почти двадцать таблиц для того, чтобы выполнить очень небольшое число функций. Теперь рассмотрим корпорацию практически любого размера и более сложной банковской системы, содержащую, например, около двух сотен таблиц. Затем заметим, что большинство корпораций имеет больше одной БД, которую они создали, и еще несколько систем управления, которые они приобрели и которые во многих случаях требуют интерфейсов от других систем. Это может привести нас к системе, показанной схематично в следующей диаграмме:
Снабжение
^
\
гX\ А
Контакты /
Бухгалтерский учет
Отдел кадров
\
Товарный склад
Затемненные прямоугольники обозначают системы, которые были куплены, а линии представляют имеющийся интерфейс. Каждая из этих систем будет требовать сотен таблиц для своих целей, и мы, вероятно, создадим несколько таблиц, чтобы поддержать все связи между ними. Цель модели предприятия состоит в том, чтобы соединить все модели в одну "великую модель", где мы можем видеть все взаимодействия и получить ясную картину того, что мы имеем и как они взаимодействуют. Это в любом случае непростая задача по нескольким причинам:
618
•
Абсолютный объем данных — Предположим, что наш пример выше содержит одну тысячу таблиц.
•
Закрытый характер систем третьих лиц — Даже в ряде лучших средств третьих лиц разработчики стараются скрыть детали реализации своих систем от любопытных глаз. Деловые обстоятельства таких схем скрыты, это делает понимание/использование данных почти невозможным.
•
Разные типы серверов БД — сочетание SQL Server 4.2, 6.5, 7.0, 2000; Oracle 6, 7, 8; Informix и т. д. затрудняет нас разобраться, что происходит на сервере.
•
Изменения в модели — Сохранение модели данных, синхронизированной с единственной БД, — привлекательная задача, не требующая строгих правил того, кто может изменять модель.
•
Стоимость — учет всех этих пунктов может быть весьма дорог.
Завершение проектирования Мы можем избежать некоторых из этих проблем, решая просто реализовать логическую модель предприятия, весьма возможно, удаляя из рассмотрения закрытость системы, и включая только таблицы интерфейса (или те, которые мы создаем для взаимодействия с процессами, или фактические таблицы системы, которые нас интересуют). Это приводит нас к совершенно другой проблеме, а именно, обеспечить, чтобы логическая модель БД соответствовала ее физической модели. Следующая сложность состоит в том, что некоторые из систем, которые мы создадим, могут включать многие "модули" исходной системы, типа системы бухгалтерского учета:
Бухгалтерский учет
\г Счета, подлежащие оплате
Главная бухгалтерская книга
Счета к получению
Цель любого раздела этой книги состоит в том, чтобы выделить ситуацию, описать проблему и затем предложить, что делать в этой ситуации. В данном случае ответ не прост. Единственный способ обеспечить синхронность логической модели с физической моделью и, что более важно, с физической реализацией, — быть дисциплинированным. Единственный путь — это взять меньшие модели и построить модель предприятия — снова подчиняясь дисциплине. Установленные на теоретической стадии процессы должны быть заменены кодом на стадии построения. Изменения в схеме БД должны протекать в определенном порядке, типа: •
Создать или изменить логическую модель, чтобы она содержала все данные, которые в настоящее время требуется хранить.
•
Модернизировать физическую модель, чтобы отразить эти изменения принятым образом.
•
Изменить физическую структуру.
•
Модернизировать модель предприятия, чтобы отразить текущее состояние системы каждый раз, когда что-нибудь изменяется.
Как фактически преобразовать цель модели предприятия в действительность, будет во многом зависеть от того, какие средства вы используете для моделирования ваших данных и предприятия. Важно подобрать правильные средства и хранилища, которые позволят вам построить модель и поддерживать ее через какое-то время, когда изменятся ваши условия.
Переход от опытного варианта При разработке программного обеспечения мы должны быть реалистичными. Наш проект никогда не будет совершенен и никогда не будет окончательно закодирован. Если бы было так, мы могли бы сначала разработать новое программное обеспечение, затем вручить его пользователям и перейти к следующему проекту. Вторая действительность заключается в 619
Глава 14 том, что как только пользователи будут иметь возможность работать с программным обеспечением, им потребуется внесение изменений. Обычно мы используем для развития итерационный процесс, включающий три основных стадии: •
разработка;
Q
проверка характеристик;
Q
реализация.
Каждая среда играет фундаментальную роль в процессе создания кода, его проверки и использования персоналом. Цель состоит в том, чтобы иметь процесс, где наш код проходит через ряд стадий, которые выглядят следующим образом:
Разработка
Проверка характеристик
Кодировщики считают, что код готов
Протестированный и допустимый код
Реализация
Кодирование начинается на стадии разработки БД, затем перемещается на стадию проверки характеристик и, наконец, на стадию реализации. Хотя мы и стремимся к такой ситуации, она, конечно, не достижима. Практически невозможно разработать достаточно сложную систему и не сделать каких-либо ошибок. Обычно, как минимум, процесс выглядит так: Выявленные ошибки при тестировании
\
Разработка
к Кодировщики считают, что код готов
Проверка характеристик
Протестированный и допустимый код
Реализация
А на самом деле мы могли бы нарисовать линии от каждой стадии БД к каждой другой стадии. Однако когда дефекты выявлены на стадии реализации, мы должны опять пройти этот же цикл разрабатываемого кода, проверяя его качество и, наконец, возвращаясь на стадию реализации. В дополнение к этой модели с тремя стадиями, обычно имеется стадия опытного образца между стадией проверки характеристик и стадией реализации. Мы можем также иметь две или больше БД на стадии проверки характеристик для различных типов испытания (испытание нагрузки, проверка требований пользователя, проверка определения правильности всех алгоритмов), которые опять-таки основаны на разрабатываемой системе и требованиях к ней. Некоторым организациям могут потребоваться различные варианты разработки, чтобы обеспечить различные версии кода для поддержания системы на стадии реализации, одну из которых пользователи будут применять в настоящее время, и другую — для новой версии. 620
Завершение проектирования Однако мы рассмотрим все это в минимальном объеме, так как независимо от того, сколько вы фактически имеете систем, будут одни и те же проблемы.
Разработка Она представляет собой среду, где мы будем создавать наши БД, с которыми начнется работа (и что мы до сих пор предполагали в тексте книге). На этой стадии мы не должны иметь никаких пользователей или испытателей, а только архитекторов, администраторов БД, программистов пользовательского интерфейса, кодировщиков БД и т. д., анализирующих и создающих новый код.
Аппаратные средства разработки Аппаратные средства для разработки должны быть минимальны, но разумны. Если ваш испытательный сервер столь же мощен, как ваши серверы БД на стадии реализации, разработчики будут стараться использовать каждую его часть, чтобы решить задачу, и будет очень трудно обнаружить, какие у вас будут проблемы с настройками данных, когда перейдете к стадии реализации. То же самое можно сказать относительно полосы пропускания сети. Если вы создаете приложение для глобальной сети с 2-х мегабитовым соединением между клиентом и сервером, а строите ваши приложения на 100-мегабитовой сети, может быть трудно выбрать надлежащее решение. Имейте в виду, однако, что сервер не должен быть столь медленным, чтобы препятствовать процессу. Выбор надлежащего баланса для среды разработки — трудная задача, и, вероятно, лучше допустить ошибку на стороне слишком больших аппаратных средств. Это решение должно также быть умерено знанием того, что большинство испытательных серверов обычно не используется только для одной СУБД, а используется для полного обеспечения БД, включая разработку нового программного обеспечения и испытания новых версий коммерческого программного обеспечения.
Проверка характеристик Как только программное обеспечение (или части программного обеспечения) будет доведено до уровня, когда можно считать, что оно готово для передачи пользователям, оно должно быть проверено. Среда проверки характеристик используется для доказательства выполнения основных требований несколькими способами: •
Испытание характеристик — Позволяет проверить, что программное обеспечение (БД и код для доступа к БД, наподобие пользовательского интерфейса) выполняет то, что рекламируют программисты, а также проверить, что система выполняет требования пользователей. Оно будет включать проверку правильности работы системы (что все алгоритмы правильны), проверку безопасности и, особенно, что каждая часть ее работает.
•
Испытание нагрузки — Используется, чтобы поместить программное обеспечение в реальные (а во многих случаях и сверх реальных) условия загрузки, чтобы посмотреть, может ли оно работать с нагрузками, с которыми будет работать на стадии реализации.
•
Приемочные испытания — Прежде, чем пользователь сможет начать использовать новую БД или потребуется внести некоторые изменения в нее, пользователи, как правило, должны одобрить написанное программное обеспечение, чтобы согласиться с тем, что оно делает все, что было в исходной спецификации. 621
Глава 14 Аппаратные средства проверки характеристик Ключ к правильному уровню аппаратных средств при проверке характеристик — масштабирование. Не каждая организация будет способна полностью дублировать аппаратные средства своих серверов, поскольку сервер(ы) может (могут) составлять большой процент от полного бюджета разрабатываемой системы. Однако рассмотрим это здесь. Если вся система проверки характеристик составляет 10 процентов мощности от стадии реализации, мы будем в состоянии работать в 10 раз активнее с системой на стадии реализации. Обратите внимание, что, конечно, мы редко будем способны масштабировать каждую часть нашей системы на одном уровне, но хороший администратор должен быть способен аппроксимировать процесс и получать приемлемый коэффициент масштабирования для испытания. Минимальные аппаратные средства требуются для другой задачи проверки характеристик, — удостовериться, что все разработанное выполняет заданные спецификации. Во время таких проверок ускоренные испытания аппаратных средств будут полностью удовлетворять текущим сценариям испытаний, которые определяют входы для процессов/процедур и осуществляют передачу надлежащих значений, а также испытаниям приложения.
Реализация Среда реализации — это то, когда пользователь фактически использует БД. Как только мы пропустили наш код со стадии разработки через процесс проверки характеристик, теоретически он должен быть свободен от ошибок (конечно, на практике обычно требуется несколько циклов разработки и проверки характеристик, чтобы получить результат, практически свободный от ошибок). Хотя формирование БД на стадии реализации не является задачей архитектора данных, исходя из моего опыта, архитектор данных часто привлекается для определения того, как эта система должна использоваться и поддерживаться (особенно в небольших организациях), и может в большой мере отвечать за недостатки в БД и аппаратных средствах. В главе 13 мы дали основное представление о некоторых проблемах аппаратных средств, с которыми вы столкнетесь, а также о некоторых формулах/хранимых процедурах, которые мы можем использовать, чтобы помочь с проблемами калибровки БД. Мы также вычислили размеры (и проекты будущих размеров) данных, которые будут размещены в нашей БД. В большинстве случаев это будет полностью работой администратора БД, но важно, чтобы вы понимали основы, так как не все администраторы БД полностью разбираются в аппаратных средствах, а во многих случаях может даже и не быть специального администратора БД, так что вам, вероятно, придется участвовать в формировании требований к аппаратным средствам для СУБД.
Обслуживание и планы восстановления поломок Хотя мы и не будем рассматривать, как поддерживать БД, мы должны обсудить несколько важных моментов, чтобы рассмотреть, как система переходит к стадии реализации. Обычно этот вид информации, вероятнее всего, определяется, когда архитектор данных почти закончил свою работу. Здесь имеются два важных момента: обслуживание и восстановление поломок, которые отличаются друг от друга.
622
Завершение проектирования И планируя обслуживание, и планируя восстановления поломок, мы ограждаем себя от простых проблем, связанных с программными средствами и программным обеспечением, наподобие отказов системы бесперебойного питания (обычно, резервная батарея) и аварии жесткого диска. Это довольно ясные проблемы, если администратор БД понимает некоторые моменты: •
Размер данных — Используя формулы, рассмотренные в предыдущей главе, мы можем получить прогноз размера данных на основе их ожидаемого роста, который был оценен в конце логической стадии проектирования.
•
Приемлемая потеря данных — Это легко! Никаких потерь. Конечно, это — не подходящий ответ, поскольку мы должны принять, что будем всегда иметь некоторый промежуток времени, когда имеется потеря данных из-за массового отказа системы. Однако когда время допустимого риска становится меньше, стоимость этого увеличивается по экспоненте. Если мы можем потерять данные дня, мы просто должны обеспечить каждую ночь некоторое сохранение данных на другой машине. Если час — приемлемая потеря, мы можем делать ту же самую вещь с загрузками транзакций, в то время как пользователи используют данные, и мы можем устранить почти любую потерю данных, используя кластерные методы (всегда имеется некоторое минимальное время ожидания, которого нельзя избежать). Конечно, выделение кластеров гораздо более дорогое, чем привод ленты, и с выделением кластеров вы все еще нуждаетесь в приводе ленты. Стоимость, конечно, является основной характеристикой для клиента и менеджера проекта.
•
Приемлемый уровень обслуживания — Это было бы вообще некоторым признаком ожидаемой нагрузки в разное время в течение дня. Он также детализировал бы ожидаемое/обговоренное время работы, когда БД должна быть активна. Конечно, это звучит наподобие пункта Приемлемая потеря данных, поскольку ожидаемый уровень обслуживания — всеобъемлющий, как всегда хочет клиент; но это — нереально и никогда не будет оговорено таким образом. Чтобы полностью обеспечить это требование, мы должны также знать, как долго будет восстанавливаться сервер.
Для плана восстановления поломок мы должны учесть "реальные" проблемы, типа ураганов, пожаров, торнадо, и даже унесенного трансформатора из соседнего здания. Полный план был бы просто расширением плана обслуживания, но он будет зависеть также от следующего: •
Отказы питания — Отказы питания — очень серьезная ситуация. Сколько раз мы слышали истории относительно подключенного розничного продавца, который отключился на несколько часов? Это нетрудно распознать. Наличие генератора, который будет запущен задолго до отказа бесперебойного питания, является лучшим способом быть уверенными, что вы не будете иметь никаких перерывов в работе, связанных с электропитанием.
•
Природные бедствия — Что, если все отключится из-за урагана? Или торнадо? Или даже наводнения? В глобальной экономике, если ваш сервер отключится на неделю, вы попадете, вероятно, в ночные новости (или хуже — в финансовые новости) и, возможно, поставите в затруднительное положение вашу компанию. Вы будете, вероятно, должны компенсировать это использованием городских серверов.
623
Глава 14 •
Другие бедствия — Бедствия принимают различные формы и размеры, и мы даже не можем их все перечислить. Ошибки в электропроводке, плохо настроенные системы, мыши, насекомые, извержения вулканов, или (к сожалению) даже неудачно запланированная модернизация программного обеспечения — все может привести к нарушению работы системы в течение недопустимого промежутка времени.
Последняя и, конечно, наиболее важная проблема, связанная с обслуживанием и планированием восстановления поломок — тестирование системы. Лучшие разработанные планы — от мышей и администраторов БД ... или кое-что вроде этого. Вы можете строить наиболее внушительный план восстановления поломок с наиболее впечатляющим генератором и бесперебойным питанием, но если вы запасли топлива, чтобы управлять генераторами только в течение дня; а если впоследствии возникнет ледяной шторм, который отключит энергию на два дня, и у вас нет возможности получить топливо, то ваш план будет в какой-то мере неэффективен. Таким же образом, если вы выдернете штепсель основного бесперебойного питания только для того, чтобы обнаружить, что не можете запустить генератор перед тем, как ваше бесперебойное питание будет отключено от энергии, то, короче говоря, ох, что произойдет!
Учебный пример В этом заключительном учебном примере мы столкнемся с решением одной последней оставшейся задачи создания БД, формирующей отчеты. Если бы мы действительно строили эту систему, то, вероятно, на самом деле не создавали БД, формирующую отчеты, поскольку наша система маленькая с невысокими требованиями параллелизма. Однако для законченности мы создадим таблицу, чтобы поддержать конкретный отчет, который мы выбрали в разделе учебного примера из главы 8. Он было описан следующим образом:
Активность пользователя — Список всех сделок, связанных с данным пользователем, или просто сгруппированный по пользователям. Также нужно иметь возможность показать любые сделки, не связанные с пользователем, который фактически потратил деньги. Следовательно, мы должны взять таблицы, связанные со сделкой, а именно: • •
624
transaction (сделка) — непосредственно сделка; account (счет) — счет, от которого поступил чек;
•
bank (банк) — банк, где находится счет;
•
payee (получатель платежа) — для чеков и, возможно, других операций, основанных на таблице t r a n s a c t i o n T y p e ;
•
user (пользователь) — список пользователей, известных системе, которая выписывает чеки;
•
transactionType (тип сделки) — классифицирует тип сделки;
•
transactionReconcile (согласование счета) — если запись существует в этой таблице для сделки, она должна быть согласована.
Завершение проектирования Отношение между ними, полученное в итоге, имеет следующий вид:
bank bankld: int IDENTITY autoTimestamp: smallDateTime NOT NULL
transaction Reconcile transactionReconcileld: int IDENTITY statementld: int NOT NULL (FK) transactionld: int NOT NULL (FK) autoTimestamp: smallDateTime NOT NULL
transactionType transactionTypeld: int IDENTITY name: varchar(20) NOT NULL description: varchar( 100) NOT NULL requiresSignatureFI: bit NOT NULL requiresFayeeFI: bit NOT NULL allowsPayeeFI: bit NOT NULL autoTimestamp: smallDateTime NOT NULL
отмечен как согласованный со statementltem с помощью transaction transactionld: int IDENTITY
предлагает
accountld: int IDENTITY bankld: int NOT NULL (FK) (AK1.1) number: varchar(20) NOT NULL (AK1.2) autoTimestamp: timestamp NOT NULL
userld: int NULL (FK) accountld: int NOT NULL (FK) (AK1.1) date: smallDateTime NOT NULL number: varchar(20) NOT NULL (AK1.2) description: varchar(1000) NOT NULL amount: money NOT NULL signature: varchar(20) NOT NULL payeeld: int NULL (FK) statementltemld: int NULL (FK) transactionTypeld: int NOT NULL (FK) autoTimestamp: smallDateTime NOT NULL
классифицирует
передает фонды, формируя
получает оплат с помощью
userid: int IDENTITY username: sysname NOT NULL firstName: varchar(60) NOT NULL middleName: varchar(60) NOT NULL lastName: varchar(60) NOT NULL
payee payeeld: int IDENTITY name: varchar(20) NOT NULL (AK1.1) disableDate: smalldatetime NULL autoTimestamp: smalldatetime NOT NULL
Мы сведем многие из полей этих таблиц в единственную таблицу следующим образом: transactionRollup transactionld: int IDENTITY date: smallDateTime NOT NULL number: varchar(20) NOT NULL description: varchar(1000) NOT NULL amount: money NOT NULL signature: varchar(100) NOT NULL reconciledFI: bit NOT NULL payee_name: varchar(20) NOT NULL user_userName: sysname NOT NULL userJirstName: varchar(20) NOT NULL usermiddlename: varchar(20) not null userJastName: varchar(20) NOT NULL bank_name: varchar(20) NOT NULL accountjiumber: varchar(20) NOT NULL transactionType_name: varchar(20) NOT NULL
Здесь transactionRollup — таблица "объединенная сделка"; transactionld — идентификатор сделки; date — дата (сделки); number — номер (сделки); description — описание; amount — сумма; signature — подпись; reconciledFI — флаг согласования; payee_name — имя получателя платежа; user_userName — системное имя пользователя; user_firstName — имя пользователя; user_middleName — отчество пользователя; user_lastName — фамилия пользователя; bank_name — название банка; account_number— номер счета; transactionType_name — имя типа транзакции.
625
Глава 14 Так как нашей системе не требуется сложный доступ, мы просто создадим хранимую процедуру, чтобы усечь данные в таблице t r a n s a c t i o n R o l l u p и заменить их надлежащими значениями. Вместо перемещения этой таблицы к другой БД мы поместим ее в ту же самую БД и никогда не будем позволять пользователям изменять ее. Для меньших систем это позволит вам разрешить пользователям получение данных только для чтения без необходимости создавать дополнительную БД. В следующем коде мы создадим процедуру преобразования. Хотя она и довольно велика, но, с другой стороны, и довольно проста. Процедура обеспечивает все соединения с таблицами-предками таблицы t r a n s a c t i o n : CREATE PROCEDURE transactionRollup$transform AS — объявление переменных, используемых для управления ошибками в блоках DECLARE @rowcount int, -- содержит число строк, возвращаемых вызовом ЯМД @error int, — используется для размещения кода ошибки — после вызова ЯМД -- используется для предварительного @msg varchar (255), — форматирования сообщений об ошибках @retval int, — общая переменная для восстановления — возвращаемых значений от вызовов -- хранимых процедур — используется для размещения имени транзакции @tranName sysname, SmsgTag varchar(255) -- используется для размещения тега сообщения об ошибке — задание тега сообщения об ошибке SET @msgTag = '<' + object_name(@@procld) + ';keyvalue=' + '@key_bankld:' + convert (varchar (10), @key_bankld)
';type=P'
SET @tranName - object_name(@@procld) + CAST(@@nestlevel AS varchar(2)) BEGIN TRANSACTION SAVE TRANSACTION @tranName TRUNCATE TABLE transactionRollup IF @@error != 0 — ошибка возникла за пределами этой процедуры BEGIN SELECT @msg - 'Проблема возникла при отсекании таблицы transactionRollup' + SmsgTag + 'call=(отсекание transactionRollup)>' ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RAISERROR 50000 @msg RETURN -100 END INSERT INTO transactionRollup(transactionld, date, number, description, amount, signature, reconciledFl, payee_name, user_userName, user_firstName, user_middleName, user_lastName, bank_name, account_number, transactionType_name) SELECT xaction.transactionld, xaction.date, xaction.number, xaction.description, xaction.amount, xaction.signature,
626
Завершение проектирования — если внешнее соединение с таблицей transactionReconcile — допускает значение NULL, соединение тоже будет NULL. — Следовательно, если мы проверяем на NULL это поле, — то можем узнать, согласована ли запись CASE WHEN transactionReconcile.transactionReconcileld IS NULL THEN 1 ELSE 0 END AS reconciledFl, payee.name, user.userName, user.firstName, user.middleName, bank.name, account.number, transactionType.name FROM dbo.[transaction] AS xaction — получение счета и банка JOIN dbo.account AS account JOIN dbo.bank AS bank ON bank.bankld = account.bankld ON xaction.accountld = account.accountld — получение типа сделки JOIN dbo.transactionType AS transactionType ON transactionType.transactionTypeld = xaction.transactionTypeld — не все сделки будут иметь получателя платежа LEFT OUTER JOIN dbo.payee AS payee ON payee.payeeld = xaction.payeeld — не все сделки имеют связанных с ними пользователей LEFT OUTER JOIN dbo.user AS user ON user.userld - xaction.userld -- левое внешнее соединение в случае, если значение не согласовано LEFT OUTER JOIN dbo.transactionReconcile AS transactionReconcile ON transactionReconcile.transactionld = xaction.transactionld IF @@error != 0 — ошибка возникла за пределами этой процедуры BEGIN SELECT @msg = 'Проблема возникла при отсекании таблицы transactionRollup' + @msgTag + 'call=(отсекание transactionRollup)>' ROLLBACK TRANSACTION @tranName COMMIT TRANSACTION RAISERROR 50000 @msg RETURN -100 END COMMIT TRANSACTION
Конечно, мы должны перечислить и другие задачи типа установки среды реализации, настройки функционирования и т. д. Однако эти задачи за пределами возможностей данного учебного примера и были упомянуты в большей мере для того, чтобы обратить внимание на их существование для случаев, когда мы вызываем помощь для определения аппаратных средств, типа случая, когда штат администраторов БД всего земного шара запускает новые операции в действующем Интернете. Понимание того, как все работает, даже если мы никогда не должны нажимать кнопки или движки SQL Server, является очень важным.
627
Глава 14
Резюме В этой главе мы рассмотрели некоторые из проблем, которые могут возникнуть в конце проектирования, а также связанные с приемлемым представлением результатов и переходом к следующей работе. Мы начали с обсуждения потребности в настройке функционирования, и рекомендовали строить запросы и настраивать работу после проектирования. SQL Server — хороший исполнитель, так что практика профилактической настройки запросов SQL Server до того, как они были выполнены хотя бы раз, может наделать больше вреда, чем пользы. Затем мы обсудили основы создания наших БД только для чтения. Легкая часть этой задачи — моделировать то, как мы хотим видеть данные, но другие задачи, типа задания тиражирования, использование сервисов преобразования данных и т. д. — намного более сложные и вовлекают для решения других сотрудников типа администратора БД. В обсуждение была включена идея относительно создания модели предприятия, объединяющей все наши ресурсы БД. Наиболее важная причина заниматься этим состоит в том, чтобы гарантировать, что мы храним любую часть данных только однажды в БД. Наконец, мы тщательно рассмотрели, как организовать нашу среду разработки, используя итерационный процесс с тремя стадиями, включающий разработку, проверку характеристик и реализацию. Такой цикл требует рассмотрения проблем типа аппаратных средств, размера данных и характера тестирования того, что мы должны реализовать. Мы также рассмотрели потребность разработки планов обслуживания и восстановления поломок. Рассмотрев все эти проблемы, мы должны затем послать БД пользователям и позволить им начать использование нашего создания, в то время как сами отправляемся после завершения к следующей большой проблеме создания новой БД.
628
: ...-i." ??<.r "•!• s*iV-\^>. ;..; ' .-Ot-'b;! qi' ,;-:"U ., • J.
12 правил Кодда для СУРБД В то время как большинство из нас думает, что любая БД, которая поддерживает SQL, является автоматически реляционной БД, это не всегда так, по крайней мере, не полностью. В главе 3 мы обсудили основы и принципы реляционной теории, но никакое обсуждение по этому предмету не будет успешным без рассмотрения правил, которые были сформулированы в 1985 г. в статье из двух частей, опубликованной издательством Computer World (Codd, Е. F., «Is Your DBMS Really Relational?» и «Does Your DBMS Run By the Rules?» Computer World, 14 октября и 21 октября 1985 г.). Они также отмечены в http://luna.pepperdine.edu/gsbm/class/ckettennborough/Codd12R.html и в 'Fundamentals of Database Systems' (The Benjamin Cummings Publishing Co., ISBN 0805317538). Эти правила шире реляционной теории и определяют более точный набор критериев, которые должны быть реализованы в СУРБД, если она должна действительно быть реляционной. Это может походить на устаревшие новости, но данные критерии все еще используются и сегодня, чтобы оценить, насколько реляционной является БД. Данные правила часто поднимаются при обсуждении, насколько хорошо реализован конкретный сервер БД. Мы представляем правила наряду с краткими комментариями относительно того, как SQL Server 2000 реализует каждое из них.
Правило 1 : Информационное правило Вся информация в реляционной БД должна быть представлена явно на логическом уровне лишь одним способом — значениями в таблицах. Это правило — в некотором роде неформальное определение реляционной БД и указывает, что каждая часть данных, которые мы постоянно храним в БД, расположена в таблице.
Приложение А Обычно SQL Server 2000 выполняет это правило, поскольку мы не можем хранить какую-либо информацию в чем-нибудь другом, кроме таблиц. Переменные, используемые в этом коде, не могут использоваться для хранения каких-либо данных, и поэтому они ограничены отдельным пакетом.
Правило 2: Правило гарантированного доступа Любая и каждая величина (элементарное значение) в реляционной БД должна быть гарантированно логически доступна, обращаясь к названию таблицы, значению первичного ключа и имени столбца. Это правило в основном подчеркивает важность первичных ключей для размещения данных в БД. Название таблицы задает нужную таблицу, имя столбца задает нужный столбец, а значение первичного ключа определяет строку, содержащую конкретный экземпляр интересующих данных. Другими словами, каждая (элементарная) часть данных доступна с помощью комбинации названия таблицы, значения первичного ключа и имени столбца. Это правило точно определяет, как мы добираемся до данных в SQL Server. В таблице мы можем искать данные по значению первичного ключа (который гарантированно является уникальным на основе реляционной теории), и как только мы найдем строку, к данным можно обратиться с помощью имени столбца.
Правило 3 : Систематическая обработка значений NULL Значения NULL (отличающиеся от пустой строки символов или строки пробелов и отличные от нуля) поддерживаются в СУРБД для представления систематическим способом отсутствующей информации, независимо от типа данных. Это правило требует, чтобы СУРБД поддерживала различные представления NULL независимо от типа данных. Значения NULL отличаются от пустой строки символов или любого другого числа и интерпретируются согласно настройке ANSI_NULL. Когда задана настройка ANSI_NULL, с NULL обращаются естественным образом, так что NULL + 'Значение строки' = NULL. Если настройка ANSI_NULL выключена, это позволяет осуществить совместимость с ранними версиями SQL Server; значения NULL представляются нестандартным способом, так что NULL + 'Значение строки' = 'Значение строки'.
Правило 4: Динамический оперативный каталог, основанный на реляционной модели Описание БД представлено на логическом уровне таким же образом, как обычные данные, так что зарегистрированные пользователи могут применять тот же самый реляционный язык для запросов к ней, который они используют для обращения с регулярными данными. Это правило требует, чтобы реляционная БД была самоописываемой. Другими словами, БД должна содержать некоторые таблицы системы, чьи столбцы непосредственно описывают структуру БД, или другими словами, описание БД содержится в доступных для пользователей таблицах. 630
12 правил Кодда для СУРБД Это правило все больше реализуется с каждой новой версией SQL Server, используя системные представления INFORMATION_SCHEMA (информационная схема). Однако это не совсем точно, потому что наше представление системного каталога не имеет нормализованную форму; если бы это было так, то мы могли бы осуществить доступ к данным таким же образом, как к таблицам пользователя. Следовательно, мы должны найти информацию о схеме БД, используя системные функции SQL Server, чтобы получить дополнительную информацию из каталога.
Правило 5: Правило общего языка манипулирования данными (ЯМД) Реляционная система может поддерживать несколько языков и различные методы терминального использования (например, способ заполнения бланка). Однако должен существовать, по крайней мере, один язык, операторы которого могут быть выражены с использованием некоторого четкого синтаксиса в виде строк символов и который может явно поддерживать следующие операции: определение данных, определение представлений, обработка данных (диалоговая и программным образом), ограничения, связанные с целостностью данных, и задание границ транзакций (начало, фиксация и откат). Это правило разрешает использование реляционного языка БД, типа SQL, хотя SQL требуется не обязательно. Язык должен быть способен поддерживать все основные функции СУРБД — создание БД, извлечение и ввод данных, обеспечение безопасности БД и т. д. Язык Transact-SQL выполняет эту функцию для SQL Server и выполняет все определения данных и задачи управления, требуемые для доступа к данным.
Правило 6: Правило корректировки представления Все представления, которые являются теоретически корректируемыми, также корректируемы и системой. Это правило относится к представлениям, которые являются виртуальными таблицами, используемыми, чтобы дать различным пользователям БД различные представления ее структуры. Это — одно из наиболее перспективных правил для практической реализации, и сегодня никакой коммерческий продукт не удовлетворяет его полностью. Представление теоретически корректируемо, когда оно составлено из столбцов, которые непосредственно соответствуют реальным полям таблиц. В SQL Server представления являются корректируемыми, если вы корректируете не более одной таблицы в операторе; при этом вы не можете модернизировать вычисляемое или постоянное поле. SQL Server 2000 также реализует триггеры INSTEAD OF, которые вы можете использовать для представления (глава 11). Поэтому это правило фактически выполняется, используя триггеры INSTEAD OF, хотя это и является косвенным способом.
23 1868
631
Приложение А
Правило 7 : Операции добавления, корректировки и удаления высокого уровня Способность обработки основных или производных отношений в виде единственного операнда применяется не только к получению данных, но также и к добавлению, корректировке и удалению данных. Это правило подчеркивает ориентируемый на наборы характер реляционной БД. Оно требует, чтобы со строками обращались как с наборами в операциях добавления, удаления и корректировки. Правило предназначено для того, чтобы запретить реализации, которые поддерживают только навигационную модификацию БД, связанную с конкретной строкой. Оно реализуется в языке SQL с помощью операторов INSERT, UPDATE и DELETE.
Правило 8: Физическая независимость данных Прикладные программы и терминальные операции остаются логически незатронутыми всякий раз, когда производятся любые изменения в размещении данных или методах доступа. Приложения должны продолжать работать, используя тот же самый синтаксис, даже когда сделаны изменения в способе, которым БД внутренне реализует хранение данных, и в методах доступа. Это правило более жесткое для реализации, чем другие правила, и подразумевает, что способ, которым данные физически размещены, должен быть независим от логического способа, которым к ним обращаются. Это означает, что пользователи не должны беспокоиться относительно того, как данные размещены или как к ним обращаются. Например: •
Добавление индексов — индексы определяют, как данные фактически размещены, однако пользователь никогда не узнает через SQL, что индексы используются.
•
Модификации механизма размещения — время от времени фирма Microsoft будет, очевидно, должна изменять работу SQL Server. Однако операторы SQL должны иметь доступ к данным точно так же, как они делали в любой предыдущей версии.
Microsoft затратила много усилий в этой области, так как обладает собственным реляционным механизмом и механизмом размещения, a OLE DB используется, чтобы передать данные между ними. Дальнейшее чтение на эту тему возможно в SQL Server 2000 Books Online, раздел Database Engine Components (компоненты механизма БД).
Правило 9 : Логическая независимость данных Прикладные программы и терминальные операции остаются логически незатронутыми, когда над основными таблицами произведены любые изменения в хранении информации, теоретически не вызывающие ухудшение. Наряду с правилом 8, это правило изолирует пользователя или прикладную программу от реализации низкого уровня в БД. Вместе они определяют, что конкретный доступ или методы размещения, используемые в СУРБД, даже при изменении структуры таблиц в БД, не должны затронуть способность пользователя работать с данными. Это правило весьма 632
12 правил Кодда для СУРБД трудно реализовать, так как цель состоит в том, чтобы оградить структуры таблиц от фактических пользователей таблицы. Таким образом, если я добавлю столбец к таблице, или если таблицы разбиты так, что не добавляются и не удаляются столбцы, прикладные программы, которые вызывают БД, должны быть незатронутыми. Например, пусть мы имеем следующую таблицу: • •
••
:
:
/••••: . ц
: ' Щ I
baseTableld c o l u m n l c o l u m n 2
Здесь baseTable — таблица «основная таблица»; baseTablelD — идентификатор основной таблицы; columnl — столбец 1; column2 — столбец 2. ... и вертикально разобьем ее на две таблицы: baseTableA . baseTableld | columnl
baseTableB baseTableld column2 |
... а теперь создадим следующее представление: CREATE VIEW baseTable AS SELECT baseTablelD, FROM baseTableA
columnl, column2
JOIN baseTableB ON baseTableA.baseTablelD
= baseTableB.baseTablelD
... пользователь должен быть незатронут. Если мы должны реализовать триггеры INSTEAD OF для представления, которое имеет то же самое число столбцов с теми же самыми именами, то можем встретиться с этой потребностью. Обратите внимание, что обработка столбцов идентичности может быть в представлениях довольно хитрая, поскольку они требуют, чтобы данные были введены, даже когда они не будут использоваться. Детали можно посмотреть в главе 11. В главе 12 мы рассмотрели использование распределенных разделенных представлений, чтобы горизонтально разбить таблицы на тождественно структурированные таблицы. К этим новым таблицам мы затем обращаемся, используя представление, которое снова объединяет таблицы вместе, позволяя таким образом обращаться к ним, как к единой таблице. Это фактически выполняет критерии правила 9. Одно замечание: мы можем выполнять требования этого правила, реализуя промежуточный слой между пользователем и данными, используя хранимые процедуры; хранимые процедуры тогда становятся «пользователями» данных. 633
Приложение А
Правило 10: Независимость целостности данных Ограничения, связанные с целостностью данных, характерные для конкретной реляционной БД, должны быть определяемы на языке реляционных данных и размещаемы в каталоге, а не в прикладных программах. Должны быть минимум два следующих ограничения, связанных с целостностью данных: •
Целостность сущности: никакой компонент первичного ключа не должен иметь значение NULL.
•
Ссылочная целостность: для каждого значения внешнего ключа, отличного от NULL, в реляционной БД должно существовать соответствующее значение первичного ключа того же самого домена.
Это правило означает, что язык БД должен поддерживать ограничения, связанные с целостностью, сортирующие данные, которые могут быть введены в БД, и корректировки БД, которые могут быть сделаны. Другими словами, СУРБД должна поддерживать определение и выполнение целостности сущности (первичных ключей) и ссылочной целостности (внешних ключей). Это правило требует, чтобы БД была способна обеспечить ограничения для защиты данных от недопустимых значений, и что необходим аккуратный проект БД, чтобы гарантировать ссылочную целостность. SQL Server 2000 выполняет большую работу, обладая средствами, чтобы сделать это правило действительностью. Мы можем защитить наши данные от недопустимых значений еще больше, используя различные ограничения и триггеры. Большая часть главы 11 посвящена рассмотрению методов, которые мы можем использовать в SQL Server, чтобы реализовать независимость целостности.
Правило 11: Независимость распределения СУРБД обладает независимостью распределения. Независимость распределения подразумевает, что пользователям не придется знать, распределена ли БД. Это правило говорит, что язык БД должен быть способен управлять данными, расположенными на других компьютерных системах. В сущности, мы должны быть способны разбить данные СУРБД на несколько физических систем без участия пользователя. Это становится действительностью, начиная с SQL Server 2000. С SQL Server 2000 понятие объединенных серверов БД с полным разделением нагрузки становится реальностью. Дополнительную информацию на эту тему можно найти в SQL Server 2000 Books Online, раздел Federated SQL Server 2000 Servers (объединенные серверы SQL Server 2000).
Правило 12: Правило единственности Если СУРБД имеет язык низкого уровня (единственная запись одновременно), то этот язык низкого уровня не может использоваться, чтобы нарушить или обойти правила целостности или ограничения, выраженные на реляционном языке более высокого уровня (несколько записей одновременно). 634
12 правил Кодда для СУРБД Это правило в основном требует, чтобы дополнительные методы доступа к данным не могли обойти ограничения, связанные с целостностью, означающие, что пользователи не смогут нарушить правила БД никаким способом. Для большинства приложений SQL Server 2000 это правило выполняется, так как там нет никаких методов получения данных строк и изменения значений, отличных от методов, предписанных БД. Однако имеются два места, где в SQL Server 2000 это правило нарушается: •
Массовые копии — по умолчанию программы массового копирования могут использоваться, чтобы вставить данные прямо в таблицу, минуя проверки сервера БД.
•
Запрещение ограничений и триггеров — имеется синтаксис запрещения ограничений и триггеров, таким образом, нарушая это правило.
Важно удостовериться, что вы используете эти две особенности внимательно. Они оставляют зияющие бреши в целостности ваших данных, так как позволяют вставить любые значения в любое поле. Так как вы ожидаете, что данные защищены ограничением, которое вы применили, в значениях данных могут возникнуть ошибки в программах, которые используют данные без их повторной проверки.
Заключение 12 правил Кодда для реляционных БД могут использоваться, чтобы объяснить многое относительно того, как SQL Server работает сегодня. Эти правила были главным шагом вперед в определении, может ли продавец БД называть свою систему «реляционной», и предъявили жесткие условия реализации для разработчиков БД. Через пятнадцать лет даже реализация наиболее сложных из этих правил становится достижимой, хотя SQL Server (и другие СУРБД) все еще не достигает всех их целей.
635
Предметный указатель Основные принципы предметного указателя Этот предметный указатель охватывает все пронумерованные главы и приложение. Он организован в алфавитном порядке, пословно, с символами и цифрами, предшествующими букве А. (Вначале перечисляются английские слова и словосочетания, а затем русские. Прим. перев.) Там, где основная тема имеет ссылки на страницы и подтемы, ссылки на страницы включают основные описания темы, в то время как подтемы указывают страницы, где упомянуты только частные аспекты. Предпочтение отдано аббревиатурам по сравнению с их расшифровкой, на том основании, что незнакомые аббревиатуры легче построить, чем их расшифровать.
Символы # префикс, локальные временные таблицы, 267,454 ## префикс, глобальные временные таблицы, 454 $ (знак доллара) разделитель, 321 @@еггог переменная доступ к номерам ошибок в SQL, 384 @@rowcount переменная запрос после выражений SET, 384 @@trancount переменная, 440 хранимые процедуры должны оставлять неизменными, 495 @name и другие параметры стандартных процедур расширенных свойств, 331 @parmname = синтаксис функции, определяемые пользователем, 362 [ ] (квадратные скобки) заключение имен объектов SQL Server, 267 заключение имени таблицы с зарезервированным словом, 338 "" (двойные кавычки) заключение имен объектов SQL Server, 267 <условия_поиска>значение ограничения-проверки, 376 1НФ (Первая нормальная форма), 152 одинаковое число значений во всех экземплярах сущности, 155 признаки, что существующие данные не удовлетворяют 1НФ, 158 примеры нарушений, 153
тип данных datetime фактически является нарушением, 158 требование, чтобы все записи были различны, 155 учебный пример, 172 2НФ (Вторая нормальная форма), 159 определение отношений и нарушений, 161 признаки, что существующие данные не удовлетворяют 2НФ, 162 применяется к составным ключам, 159 ЗНФ (Третья нормальная форма), 163 необходимость дельнейшей нормализации, 181 признаки, что существующие данные не удовлетворяют ЗНФ, 167 4GT (4 гигабайтовая настройка), 564 4НФ (Четвертая нормальная форма), 182 троичные отношения, 182 учебный пример, 197 5НФ (Пятая нормальная форма), 195
ACID (Atomicity, Consistency, Isolation and Durability), свойства, 438 блокировки оставляют транзакции работоспособными, 443 ActiveX (группа технологий программирования на основе модели СОМ) технология доступа к данным см. ADO
ADO (ActiveX Data O b j e c t s )
ADO (ActiveX Data Objects)
API-курсоры, 459 могут посылать транзакции в несколько пакетов, 453 AFTER триггеры, 400 AFTER триггеры обновления, 311 несколько триггеров AFTER, 401
определение, 360
организация принудительных отношений, 404 проверка диапазона, 405 проверка распределения сделки, 434 соответствие почтовых индексов и городов, 432 ALTER TABLE, оператор добавление ограничений, 319 определение, 320 ANSI_NULLS, опция, 471 ANSI_PADDINQ, опция, 283 API (Application Programming Interface), 459 API курсоры, 459 Application Memory Tuning см. настройка памяти приложения ASP (Active Server Page), 514 ATM (Automatic Teller Machine) см. банкомат. Available Bytes, счетчик, 568 AWE (Address Windowing Extensions), 565
В Back Office Resource Kit
средства настройки характеристик, 567 BCP (Bulk Copy Program), 418 BEGIN DISTRIBUTED TRANSACTION, оператор, 533 BEGIN TRANSACTION оператор, 438 BETWEEN, оператор в операторе WHERE, 270 BLOB (Binary Large Objects), 283 размещение в SQL Server, 283 таблица доменов, учебный пример, 145 Books Online (электронный справочник), 45, 267 PERMISSIONS, функция, 527 SQL-DMO, 248 блокировки, 444 выполнение транзакций в ADO, 453 детерминированные и недетерминированные функции, 370 компоненты механизма БД, 632 объединенные серверы SQL Server 2000, 634 определение значений NULL, 468 определение размеров таблиц и индексов, 550 распределенные запросы и тиражирование, 533 расширенные свойства, 335 системные хранимые процедуры, 330 создание индекса, 308 создание индексируемых представлений, 480 тиражирование из других БД, 245 Buffer Manager: счетчик, 572 В-деревья использование для расчета области индекса, 553 использование реализованных индексов, 308
CAL (Client Access Licences) отличие от соединения пользователей, 584 CASE, ключевое слово использование для возможных значений NULL, 473
6 3 8
CEILINQ, функция, 552 CHECK, опция ограничение-проверка, 375 CHECK_CONSTRAINTS, список информационная схема, 328 Chen ERD, методология, 133 COALESCE, функция использование для возможных значений NULL, 473 проверки-ограничения, 377 COLLATE, оператор, 305 COLUMN_DOMAIN_USAGE, список информационная схема, 329 COLUMN_PRIVILEGES, список информационная схема, 328 COLUMNS, список информационная схема, 327 оператор INSERT, 389 columns_updated(), функция, 391 COM (Component Object Model) СОМ+, завершение с двумя стадиями, 532 реализация СОМ-объектов, 248 COMMIT TRANSACTION оператор определение, 440 CONSTRAINT_COLUMN_USAGE, список информационная схема, 328 CONSTRAINT_TABLE_USAGE, список информационная схема, 328 CREATE FUNCTION, оператор, 360 CREATE TABLE, оператор, 266 использование для определения столбцов, 270 CREATE TRIGGER, оператор, 387 CREATE VIEW, оператор, 472,475,477 CROSS JOIN, оператор пример оператора «Произведение», 56 CRUD (Create, Read, Update, Delete), процедуры, 533 CURRENT OF, ключевое слово, 462
Datalog/Monitor, сервис, 566, 567 DBCC (Database Consistency Checker) см. модуль контроля непротиворечивости БД DBCC MEMUSAGE, 566, 567 DBCC OPENTRAN определение длительной работы транзакций, 584 dbo (database owner) см. владелец БД. DDL (Data Definition Language) см. ЯОД. DEADLOCK_PRIORITY свойство, 451 DELETE,оператор триггеры INSTEAD OF могут потребовать повторения, 409 хранимая процедура, альтернатива, 511 DELETED, таблица временная таблица, используемая триггерами, 388 DENY, команда, 525 DISKPERF -Y, команда, 566 DISTINCT, ключевое слово пример оператора «Набор», 55 DML (Data Manipulation Language) см. ЯМД. DMO (Data Management Objects), 248 код для реализации и связи, 248 отображение ролей безопасности БД, 526 DOMAIN_CONSTRAINTS, список информационная схема, 329 DOMAINS, список информационная схема, 329
NO ACTION, опция DROP, команды вместе со связыванием схемы, 366 DTC (Distributed Transaction Controller), 246 см. контроллер распределенных транзакций DTS (Data Transformation Services) см. сервисы преобразования данных DW (Data Warehouse) см. хранилище данных
email адреса нарушают 1НФ, 153 возможности почты SQL, 248 English Query, 614 Enterprise Manager задание определяемых пользователем счетчиков, 586 окно запроса как пример «тонкого» клиента, 250 создание ролей безопасности, 524 средства представления диаграмм, 134-135 ERwin, средство моделирования представление вторичных ключей, 114 EXEC, оператор определяемые пользователем функции, 361
FLOOR, функция, 552 fnjistextendedproperty, системная функция, 331, 335 FTP (File Transfer Protocol) см. протокол передачи файлов
предпочтительней системных хранимых процедур, 326 представление parameters, 462 сообщения о структурах таблиц, 325 INSERT, операторы не следует кодировать без списка полей, 300 список столбцов, используемых триггером, 389 триггеры INSTEAD OF могут потребовать повторения, 409 хранимая процедура, альтернатива, 506 INSERTED, таблица временная таблица, используемая триггерами, 388 пример использования триггера INSTEAD OF, 407 INSTEAD OF, триггер, 407 игнорирует каскадированные модификации, 411 использование для модификации представлений, 415,631 определение, 386 проблемы, 379 сложно проконтролировать границы для нескольких строк, 410 создание таблицы исключений, 413 удаление времени из величин типа datetime, 431 IP (Internet Protocol), адрес 1НФ и целые числа без знака, 154 размещение в SQL Server требует тип bigint, 273 IS NULL, оператор, 471 IS_MEMBER, функция, 526 ISBN (International Standard Book Number) 48 ISNULL, функция использование для возможных значений NULL, 473
JOIN,оператор, 56
GO, оператор пакетирование транзакций, 443 GOTO, операторы, 517 GRANT, команда, 525 GROUP BY, оператор использование кластерных индексов, 310 столбец типа bit, использующий тип данных tinyint, 274 GUID (Globally Unique Identifiers) см. глобальный уникальный идентификатор I IDEF1X (Integration Definition for Information Modeling), методология, 105 домены и наследование типов данных, 115 обозначение отношений, 130 определение, 104 представление отношений, 118 представление сущностей, 110 IE (Information Engineering), 132 IEEE стандарт для величин с плавающей точкой, 277 IGNORE_DUP_KEY, опция, 314 Information Engineering, методология необязательные отношения, 132 представление отношений, 132 Information Schema (информационная схема)
определение, 52
К KEY_COLUMNJJSAGE, список информационная схема, 328
LIKE, маска проверка-ограничение на основе функции, 380 LOCK-TIMEOUT свойство, 451
м Maybe (сравнения с NULL) логический эквивалент Unknown, 469 MDS (MetaData Services), 325 MDX (Multidimensional Expressions) доступ к витринам данных не зависит от MDX, 15 memory см. память. MS_Description, таблица, 332
N newld(), функция, 290 NO ACTION, опция каскадирование удалений, 323
6 3 9
NOCHECK, опция NOCHECK, опция проверка-ограничение, 375, 378, 382 NOCOUNT, опция, 294 Northwind, пример БД Enterprise Manager, средство построения диаграмм, 134 NOT IN, оператор оператор "Разность", 58 NULL-ограничения используемые правила должны быть на уровне БД, 418 не являются фактическими ограничениями, 371 определение, 45 NULL, значения, 468 использование значений NULL в логических доменах, 346 использование ключевого слова CASE, 473 использование типов данных, определенных пользователем, противоречиво, 279 используется для необязательной величины, 52 может вызвать проблемы с вариантными данными, 287 определение в Books Online, 468 определение столбцов, допускающих значение NULL, 296 ошибки, связанные с использованием, 471 преобразование запроса, иллюстрирующее проблемы, 491 сравнения, 469 ссылки внешних ключей таблиц-потомков, 407 третье правило Кодда для СУРБД, 630 уменьшение с помощью нормализации, 150 NUMERIC_ROUNDABORT, опция, 275 n-уровневая конфигурация, 254 проблемы с защитой данных, 417 пул соединений, 243, 254
ODBC (Open Database Connectivity) API-курсоры, 459 источники данных и распределенные запросы, 532 ODS (Operational Data Store') см. оперативное хранилище данных. OLAP (Online Analytical Programming) данные должны включать индексы, 546, 549 как технология формирования запросов в витрине данных, 20 рост таблиц и факторы роста, 548-549 характеристики БД, 546 OLEDB API-курсоры, 459 DTS и DTC требуют подходящие источники, 246 источники данных и распределенные запросы, 532 OLTP (Online Transaction Processing) банк в качестве иллюстрации, 11 нормализация структуры, 11 оптимизированная для ускорения транзакций, а не для чтения, 9,12 проблемы, связанные с ODS, 12 противоположность хранилищу данных, 9 скорости роста таблиц, 547 характеристики БД, 546 Operational Data Stores см. оперативное хранилище данных. OS (Operating System) см. ОС
6 4 0
РАЕ (Physical Address Extension) 565 Pages Faults/sec, счетчик, 568 PARAMETERS, список информационная схема, 327 PDF (Package Definition File), 355 PerfMon (Window NT), 566 проверка блокировки с помощью определяемых пользователем счетчиков, 587 PERMISSIONS, функция, 526 PhysicalDisk: счетчики, 582 Processor:% счетчики контроль работы ЦП, 575
Query Analyzer выполнение динамической строки индексов, 555 роль в настройке функционирования, 597 сообщения об ошибках, 383 QUOTEDJDENTIFIER, опция, 267
RAID (Redundant Array of Inexpensive Disks), 578 RAID 0, 578 RAID 1,579 RAID 5 как подсистема выбора, 580 RAID 10, 581 преимущества нескольких систем RAID 5, 582 RAISERROR, оператор выполняемый после выражений SET, 384 хранимые процедуры, 486 RAM см. память. RDBMS (Relational Database Management Systems) см. СУРБД RECOMPILE, опция хранимые процедуры,504, 521 REFERENTIAL_CONSTRAINTS, список информационная схема, 328 RETURNS, оператор требования в ОПФ, 360 REVOKE, команда, 525 RFP (Request For Proposal) см. запрос
предложений
RFQ (Request For Quote) см. запрос о цене ROLLBACK TRANSACTION оператор, 440 ROUTINE_COLUMNS, список информационная схема, 327 ROUTINES, список информационная схема, 327 rowguidcol, свойство, 290, 317
SAVE TRANSACTION оператор, 441 SCHEMATA, список информационная схема, 326 SELECT, операторы хранимая процедура, 500 определяемые пользователем функции, 361
U M L (Unified M o d e l i n g L a n g u a g e )
SET, команда для курсоров, 460 SMP (Symmetric Mufti-Processor) конфигурация, 574 sp_index$getKeyData, хранимая процедура вспомогательная роль, 554 определение, 550 sp_table$calcDataSpace, хранимая процедура, 550,551 sp_table$indexSpace, хранимая процедура, 554 определение, 550 учебный пример, 589 sp_addextendedproperty, хранимая процедура, 331 sp_adduser, хранимая процедура, 464 sp_column_privileges, хранимая процедура, 328 sp_columns, хранимая процедура, 327 sp_configure, хранимая процедура, 394 настройка распределения памяти сервера, 568 ограничение числа соединений пользователей, 583 sp databases, хранимая процедура, 326 sp_datatypes_info, хранимая процедура, 329 sp_dboption stored, процедура, 297, 394 задание БД только для чтения, 598 sp_dropextendedproperty, хранимая процедура, 331 sp_fkeys, хранимая процедура, 328 spjielp stored, процедура, 298 sp_helpconstraint, хранимая процедура, 318,328 sp_helptext, хранимая процедура, 327 sp-pkeys, хранимая процедура, 328 sp_server_info, хранимая процедура, 326 sp_speclal_columns, хранимая процедура, 328 sp_sproc_columns, хранимая процедура, 327 sp_statlstlcs, хранимая процедура, 328 sp_stored_procedures, хранимая процедура, 327 sp_table_privileges, хранимая процедура, 329 sp_tableoption, хранимая процедура, 284 sp_tables, хранимая процедура, 326 sp_updateextendedproperty, хранимая процедура, 331 sp_user_counterX x, хранимая процедура, 586 SQL (Structured Query Language) см. также T-SQL. T-SQL— версия Microsoft SQL, 17 все интерфейсы могут быть построены на хранимых процедурах, 493 необходимость архитектору данных поддерживать навыки кодирования, 438 операторы в реляционной алгебре, 54 операторы обновления данных подчиняются седьмому правилу Кодда, 632 сравнение компилируемого и нерегламентированного, 518 требует элементарные поля, 155 триггеры, операторы SQL, 387 SQL Profiler см. SQL Server Profiler. SQL Server оценка значения NULL,470 реляционная модель БД, 38 средства построения диаграмм для версии 7.0, 134 SQL Server 2000 динамическая настройка памяти, 569 источник информации о SQL Server 2000, 261 соответствие правилам Кодда для СУРБД, 629 SQL Server 2000, новые возможности гибкое задание сопоставления, 305 индексированные и разделенные представления, 479 индексированные представления, Indexed views, 238 каскадирование обновлений в объявленной ссылочной целостности, 323 массовая загрузка транзакции, 558
ОПФ (определяемые пользователем функции), 358 полностью поддерживает вычисляемые столбцы, 298 размещение вариантных данных, 285 расширенные свойства описательных объектов, 330 связывание схемы, 331 SQLServer Agent использование для планирования работы, 249 SQL Server Books Online см. Books Online. SQL Server Profiler, 566 используется для проверки перепроектируемых приложений, 166 как средство формирования сценария, 332 роль в настройке функционирования, 597 SQL-DMO см. DMO. SQL-код компилируемый, 480,482 см. также процедуры хранимые; триггеры. сравнение с нерегламентированными запросами, 478, 480 SQL-почта, 238 SSN (Social Security Number) пример типа данных, определяемого пользователем, 293 substrtng(), функция, 156 sysmessages, таблица, 486 SysMon (Windows 2000), 566 проверка блокировки с помощью определяемых пользователем счетчиков, 587 System:% Total Processor Time, счетчик контроль работы ЦП, 575
TABLE_CONSTRAINTS, список информационная схема, 328 TABLE_PRIVILEGES, список информационная схема, 329 TABLES, список информационная схема, 326 timestamp, столбцы использование хранимой процедуры обновления, 510 использование для оптимистической блокировки, 301 использование типа данных datetime, 280 тип данных timestamp, 288 trancount переменная, 447 хранимые процедуры не должны изменять, 495 truncate log on checkpoint, режим 558 TRUNCATE TABLE, оператор, 401 T-SQL (Transact-SQL) см. также SQL. вторичные таблицы, 456 использование курсоров часто медленнее, чем операции с набором данных, 460 общий язык манипулирования данными, 631
определение,9
список команд, недопустимых в триггерах, 386
и UDF (User Defined Functions), 360 см. ОПФ. UDT (User defined Data Types) см. типы данных, определяемые пользователем UML (Unified Modeling Language) 104 объектно-ориентированный предок моделей Use Case, 104
6 4 1
Unicode, набор символов Unicode, набор символов допустимые символы в именах без ограничителей, 267 изменение сопоставления для сортировки, 305 типы данных nchar, nvarchar и ntext, 285 UNION оператор, 57 альтернатива использованию временных таблиц, 456 межстрочное тестирование с помощью триггеров, 393 распределенные секционированные представления, 480 Unknown "Maybe" как логический эквивалент, 469 UPDATE, оператор выражение SET, используемое триггерами, 389 обновление курсоров, 461 хранимая процедура, альтернатива, 508 URL (Uniform Resource Locator), 75 Use Case модели, 105 включение информации о безопасности, 211 классы отношений, 107 описательные таблицы для Use Case, 108 определение, 103 специальные хранимые процедуры, 539, 540 учебный пример, 138, 228 USER, функция, 526
VBS (Visual Basic Script), 514 VIEWS, список информационная схема, 326 Visio, методология моделирования, 105, 134 Visual Basic логические типы и использование типа данных smallint, 274
W WAN (Wide Area Networks) см. глобальная сеть. Web-приложения БД только для чтения, 613 копии данных только для чтения, 244, 256 решения, ориентированные на БД только для чтения, 606 скорость соединения и доступ к данным, 240 WHERE, оператор использование всех столбцов для оптимистической блокировки, 301 оператор BETWEEN, 280 пример оператора «Ограничение», 55 Windows 2000 Data Center, ОС, 565 Windows Authentication, 523 Working Set:sqlserver, счетчик, 572
Zip коды, 200 проверка соответствия городам, 432
6 4 2
АБД- администратор базы данных, 559 абстрактные сущности, 69 администратор БД выбор аппаратных средств, 545, 622 настройка функционирования, 596 отличие от проектировщика БД, 7, 22 адреса примеры атрибутов указателей на расположение, 74 примеры нарушений 1НФ, 154, 155 требования нормализации, учебный пример, 174, 176,199,338 уникальность определения разных сущностей, 176 активная страница сервера см. ASP. алфавитное упорядочивание см. также сортировка. сопоставление в учебном примере, 353 типы сопоставления, 304 языки, отличные от английского, 305 американский почтовый индекс см. ZIP. анализатор запроса см. Query Analyser, аналитическая обработка в реальном масштабе времени см. OLAP. аппаратные средства см. также дисковая подсистема; память. определение требований, 545 память, необходимая для соединения с сервером, 244 потребности в адекватном обеспечении, 243 работа подсистемы ЦП, 574 реализация БД только для чтения, 607 системы с большим объемом данных, 236 требования на стадии проверки характеристик, 622 требования на стадии разработки, 621 архивирование, 559 доступ к архивным данным, 563 маркирование архивных данных, 563 не следует запаздывать, 562 разделение по датам таблицы фактов, 561 старение аппаратных и программных средств, 560 стоимость архивных данных, 560 архивирование данных, стоимость, 560 архитектор данных, роль, 22 архитектурные ограничения требования, накладываемые на БД, 212 атрибуты 1НФ, должны быть элементарными, 153 2НФ должна описывать весь ключ, 159 Enterprise Manager, средство представления диаграмм, 134 атрибуты атрибутов, 43 добавление описательной информации, 136 завершение определений, 350 и домены, определение в данных, полученных от заказчиков, 66 история, нарушение 4НФ, 192 многозначные и 4НФ, 182, 189 НФБК должна полностью зависеть от ключа, 168 определение дополнительных данных, 85 определение столбцов, 41 определения, учебный пример,146 определяющие сущности, 72 отличие от столбцов, 74,112 отношение к ключам и нормализация, 158 перемещение при создании БД только для чтения, 602 полученные из образцов документов, 89, 91, порядок не важен, 42
блокировка представление в стандарте IDEF1X, 112 пример таблицы, содержащей сущности и домены, 75 проверка нарушений ЗНФ, 165 соглашения об обозначениях, 117 учебный пример, 89 формирование стандарта, использующего домены IDEF1X, 115 атрибуты неключевые взаимная зависимость нарушает ЗНФ, 163 атрибуты стандартные создание, используя домены IDEF1X, 115
балансирование нагрузки обеспечивается кластеризацией сервера, 244 переключатель для сложного использования Web-серверов, 255 банкомат, 546 БД (базы данных) 1 см. также БД, копия только для чтения. OLTP и OLAP, 546 конечный пользователь и исходные источники данных, 28, 598 определение, 38 перевод B O D S , 616 правила определения, действительно ли является реляционной, 629 программирование имеет аналогии с ООП, 41 рекурсивные триггеры, 394 реляционная модель, 38 реорганизация для работы с большим объемом данных, 236 рост в связи с ростом компании, 549 средства генерации БД, 260 теория, 37 техника доступа и корректировки, 437 БД, копия только для чтения, 244,598 Web-приложения, использующие методы, ориентируемые на решение, 606 возвращение в режим записи, 598 возможное использование группировки, 613 выбор модели в зависимости от пользователя, 614 использование для защиты первичной БД, 598 конфигурация сложного использования Web-серверов, 255 обоснование, 599 пример корпоративного решения, 612 реализация, 610 учебный пример,624 частота обновления, 609 БД на основе OLE-технологии см. OLE DB. БД, отличающиеся от SQL Server доступ как к связанным серверам, 246 тиражирование, 245 БД, проектирование важность постепенного подхода, 66 выделение различных ролей, 22 история, 8 как искусство и наука, 7 перечисление стадий проектирования, 19 различие логического и физического проектирования,19 разработка замедлялась технологией, 8, 163 следование стандартам и нормализации, 149
следует добавить план архивирования, 562 сравнение с реализацией, 229 средства, обеспечиваемые SQL Server, 245 стадия сбора информации, 21 схема типичной большой системы, 16 БД, разработка не использовать деловые данные, 260 БД, структура описывается внутри самой БД, 630 отказ от структуры на стадии разработки, 66 БД таблицы см. таблицы. БД, уровень блокировки, 443 БД формирования отчетов см. БД, копия только для чтения. безопасности проблемы см. также доступ к данным. доступ к данным на другой БД, 530 защита от нежелательного доступа или модификации, 211 представления, скрывающие основные детали таблиц,474, 476 роли, создаваемые Enterprise Manager, 524 ситуационный контроль доступа к таблице, 495 учебный пример, 542 хранимые процедуры, работающие на объектном уровне, 494 хранимые процедуры, скрывающие основные детали таблиц, 494 безопасность SQL Server, 524 безопасность на уровне строк и на уровне столбцов, 527 безопасность на уровне столбцов, 478, 529 безопасность на уровне строк, 478, 529 бизнес-правила важность полного определения, 81 использование и отношение к предикатам, 47 кодирование на функциональном языке, а не на SQL, 255 лучшая организация на сервере, 256 необходимо внимательно формулировать, 82 необязательные правила в учебном примере, 435 обеспечение с помощью триггеров, 386 ограниченное использование триггера INSTEAD OF, 411
определение, 81
определение в структурах данных, 195 получение из данных, взятых от заказчика, 66, 81,98 реализация через сервисы среднего уровня, 417 распределение между средним уровнем и БД, 418 таблица новых процессов в примере банка, 99 бизнес-правила необязательные учебный пример, 435 бизнес-решения см. поддержка решений. бизнес-слой обеспечение бизнес-правил, 254 формирование пула соединений посредника, 254 битовые маски columns_updated() возвращает, 391 битовый массив значений NULL, 551 блокировка доступ, вызывающий слишком длительные блокировки, 585 доступ, вызывающий усиление блокировки, 238 причины возникновения узких мест, 567 проверка с определяемыми пользователем счетчиками, 587 соединения, повторяемые чтения, connections, 447
6 4 3
блокировка ключа или диапазона ключа блокировка ключа или диапазона ключа, 443 блокировка оптимистическая, 301 использование хранимой процедуры обновления, 510 учебный пример, 342 блокировка страницы, 443 блокировка строк, 443 блокировка таблицы, 444 блокировка экстента, 444 блокировки массовой модернизации, 444 блокировки модернизации, 444 блокировки направленные направленные совместные, направленные эксклюзивные и совместные с направленными эксклюзивными, 444 блокировки совместные, 444 блокировки совместные с направленными эксклюзивными, 444 блокировки схемы, 444 блокировки эксклюзивные, 444 бумажные носители возможность размещения исходных данных, 28 бюджет влияние выбора характеристик оптимизации, 243
В важная информация маскировка в образцах документов, 34 вариант использования см. Use Case модели, вариантные данные см. тип данных sql_variant. величины с плавающей точкой, 277 невозможность представить некоторые величины точно, 277 сравнение с помощью операции • сомнительно, 279 тип данных float, 278 величины с плавающей точкой, мантисса, 278 величины с плавающей точкой, показатель, 278 величины в плавающей точкой, смещение, 278 венгерская нотация, 271 вертикальное разделение данных, 478 безопасность на уровне столбцов, альтернатива, 529 верхний регистр см. запись в строке слов прописными буквами; чувствительность к регистру, витрины данных доступные через средства, не использующие кодирование, 15 отличаются от OLAP и кубов, 20 сегменты хранилища данных, 14 владелец БД, 266, 268 владелец БД, роль, 266,268 db_owner и другие встроенные роли, 525 определяемые пользователем функции, 362 внешние ключи задание NULL в таблицах-потомках, 407 каскадированные удаления и отношения между БД, 319 определение, 51 отсутствуют для первичных ключей независимых сущностей, 109 первичный ключ мигрирует в отношении "один к п", 60 подверженность ошибкам в ненормализованных БД, 150 представление в стандарте IDEF1X, 114,118
6 4 4
присутствуют в первичных ключах зависимых сущностей, 110 ссылочная целостность, 634 функциональные имена, используемые для атрибутов, 124 внешние системы импортирующие прослойки для уменьшения связывания,216 требования взаимодействия, 213 учебный пример, 224 внешние соединения как источники значений NULL, 468, 472 возвращаемые значения хранимые процедуры, протокол, 482 возраст данных период архивирования, 561 волокна вместо потоков используют приложения, не входящие в ядро, 576 когда нужно использовать, 576 "волоконное управление", 576 вопросы предлагаемые для начального интервью с заказчиком, 27 восприятие заказчика прототип может исказить, 25 восстановление записей большие передачи данных могут быть разбиты на части, 241 из списков, пример хранимой процедуры, 500 восстановление поломок, 622 временная статистика, 597 временные таблицы # как идентификатор, 267 UNION [ALL], альтернативный оператор, 456 вторичные таблицы как альтернатива, 456 замена функциями с несколькими операторами, 369 использование отображения рекурсивного дерева, 422 используются менее часто в SQL Server 2000, 421 локальные и глобальные различаются префиксами, 456 таблицы DELETED и INSERTED, 388 время ожидания поддержка БД только для чтения, 598, 609 системы принятия решений, 238 формирование отчетов в реальном времени, 475 встроенные функции формирования таблицы выполнение в команде, использующей таблицу, 366 тип ОПФ, 365 вторичные ключи нормальная форма Бойса-Кодда, 171 определение, 49 представление в стандарте IDEF1X, 113 реализация для обеспечения уникальности, 317 роль в поддержании уникальности, 47 уникальность определяется ограничениями, а не индексами, 313 вторичные таблицы, T-SQL альтернатива временным таблицам, 456 выборочный откат, 441 выходные параметры хранимые процедуры, 484
документация
глагольные конструкции можно читать в любом направлении, 132 обозначение отношений, 130 глобальная сеть практические вопросы редактирования и передачи сообщений, 255 увеличение скорости соединения и доступ к данным, 240 глобальные временные таблицы видимы любым соединением или пользователем, 454 глобальный уникальный идентификатор, 115 пример определения домена в IDEF1X, 116 тип данных uniqueidentifier, 289 горизонтальное разделение данных, 477 графическое представление см. моделирование данных; Use Case модели, группировка данных по секундам, 298
Д дальнейшие требования документирование, 220 данные, дублирование нормализация позволяет минимизировать, 150 данные избыточные нормализация позволяет минимизировать, 150 данные исходные, 28 см. также наследование данных. определение ошибок с проверками-ограничениями, 382 перевод в ODS, 616 следует проверить ограничения перед использованием триггеров, 403 данные, объем см. также объемный анализ. источник проблем формирования отчетов, 235 оценка для аппаратных средств, 545 оценка для столбцов переменной длины, 551 последствия удаления записей из таблиц-предков, 243 факторы, учитываемые при расчете, 549 данные переменной длины, 282 получение фактора заполнения с помощью ОПФ, 551 данные, подсчет размера использование хранимых процедур, 550 данные предварительно агрегатированные проблемы с целостностью данных, 237 данные, преобразование пример корпоративного решения, 612 данные числовые тип данных numeric, 275 точные и приближенные типы данных, 272 данные числовые точные, 272 данных зависимости три определенных типа, 52 данных защита см. данных целостность, данных потери допустимые уровни для планов поддержания, 623 данных целостность см. также последовательность. безопасность, четыре метода, 357 главная цель OLTP, 11 кэширование данных для проверок могло бы быть компромиссом, 419
кэширование записи может быть компромиссом, 578 массовое копирование и запрещение ограничений и триггеров, 635 проблемы при использовании электронных таблиц, 28 проблемы с предварительным агрегатированием данных, 237 проблемы, связанные с импортом данных, 418 связывание схем и защита данных, 366 средний уровень и включение БД, 418 триггеры, обеспечивающие значения, 418 управление параллелизмом при блокировке, 300,443 уровни изоляции блокировок, 448 даты будущие проверка с помощью ОПФ, 428 двоичное дерево, пример, 456 двухфазная фиксация, 246 действие по получению информации о счете специальные хранимые процедуры, 541 действия по балансированию счета специальная хранимая процедура, 539 декларативные ограничения ключей, 307 декомпозиция тройные отношения, 189 декомпозиция с потерями, 186 "Деление", оператор, 58 деловой аналитик, роль, 22 денормализация, 196 добавление итоговых данных, 266 кодирование в связи с денормализацией данных, 163 копии данных только для чтения, 256 обработка подтипов может использовать, 264 последнее обращение при настройке функционирования, 596, 597 проблемы из-за денормализованных внешних систем, 215 сложные ситуации формирования отчетов могут оправдать, 237 требуется в ODS, 616 хранимые процедуры обновления, 510 детализация типы блокировок, 443 детерминанты все должны быть ключами в НФБК, 170 диаграммы состояния нарушения 4НФ, 192 динамическая память, размещение нарушение заданием соединений пользователей, 583 настройка SQL Server 2000, 569 дисковая подсистема важность знания, 583 несколько контроллеров диска, 581 настройка работы, 577 счетчики работы диска, 582 дискриминаторы отношения классификации, 127 учебный пример, 336 документация дальнейшие требования, 220 завершение задач формирования документов с данными, 84 маскировка значащей информации в образцах, 34 моделирование данных как альтернатива неуправляемому списку, 97 необходимо постоянно обновлять, 66 окончательный обзор, 218, 229 определение элементов информации, 66 учебный пример, 32 чрезвычайно важна для проекта БД, 23,209,219
6 4 5
документы документы как сущности в БД, 69 домены и атрибуты, получение из данных, полученных от заказчиков, 66 использование как шаблон столбца, 272 использование, объясняемое примерами, 46 логические, преобразование в типы данных, 346 логические, учебный пример, 145 названия, таблицы с эквивалентными типами данных, 346 наследование зависит от средств моделирования, 115 определение доменов столбцов, 43 определяются одновременно с атрибутами, 72 получение домена, подходящего для атрибута, 73 получение ограничений-проверок из списка доменов, 425 представление в IDEF1X, 115-116 пример таблицы с сущностями и атрибутами, 75 реализация триггерами и хранимыми процедурами, 46 создание во время нормализации, 174,191,198,200 соответствующие типам данных, 272 формирование не учитывает реализацию, 116 домены данных см. домены. домены столбцов см. домены. допустимые значения перечисление методов ограничения, 43 доступ диалоговый см. мобильные клиенты. доступ к архивным данным, 563 доступ к данным см. также безопасности проблемы. доступ к распределенным данным, 477 задание прав ролям безопасности, 525 защита от нежелательных пользователей, 524 из разных БД, 530 ограничение доступа к представлениям вместо таблиц, 476 ограничение с помощью хранимых процедур, 520 формирование систем с большим количеством данных, 236 через таблицу, столбец и первичный ключ, 630 доступ к данным между БД, 530 БД на разных серверах, 532 БД на том же сервере, 530 доступ к сети реализация SQL требуется для эффективности, 18 дублирование (RAID 1), 579
Ж живучесть свойства транзакции, 438
завершение важность нормального завершения, 86 пересмотр окончательной документации, 229 проблемы, которых можно избежать, 220 зависимости с несколькими значениями, 64 4НФ допускает только одну, 183 модель проектирования сущности contact (партнер), 191 загрузка транзакции размеры в OLTP-БД и OLAP-БД, 558
6 4 6
задание флагов архивированные данные, 563 записи, которые невозможно удалить, 243 запирание, 411 блокировка доступа вызывается расширением блокировки, 238 избежание с помощью представлений, 476 оптимизатор запросов выбирает типы, 444 оптимистические и пессимистические блокировки, 301 риск от использования курсоров T-SQL, 460 способы блокировок, 444 типы блокировок в порядке уменьшения детализации, 443 записи действий как сущности в БД, 70 запись в строке слов прописными буквами, 369 см. также чувствительность к регистру. использование триггера INSTEAD OF, 411 определяемая пользователем функция, 362 запись на диск оптимизация системы отложенных записей, 577 запрещение ограничений и триггеров нарушает двенадцатое правило Кодда для СУРБД, 635 запрос о цене источник правил данных, 29 запрос предложений источник правил данных, 29 запросы выгодные индексы и реализация, 554,557 добавление определяемых пользователем функций, 360 максимизация функционирования с БД только для чтения, 599 написание в случае значений NULL, 473 настройка запросов клиента, 519 реализация неявных запросов, 608 факторы, влияющие на эффективность, 438 запросы динамические хранимые процедуры, 504 запросы неявные, 608 запросы, преобразования пример, иллюстрирующий проблемы, связанные с NULL, 472 зарезервированные ключевые слова, 267, 338 защита данных см. данных целостность, зеркальное отображение (RAID), 579, 581 значение <условия_поиска> проверки-ограничения, 376 значения данных определение для проекта БД, 28 значения по умолчанию обеспечение с помощью триггера AFTER, 405 значения полей добавление внутренних ограничений по умолчанию, 375
И идентификаторы подходящие атрибуты, 72 идентификационные номера использование типа данных char, 284 изменчивые правила кандидаты на использование хранимой процедуры, 419 примеры, которые кажутся фиксированными, 422
итоговые данные изображения проблемы с размещением, 283 тип данных image, 282 изоляция свойства транзакции, 438 имена полей с одинаковыми ключевыми префиксами признаки, что существующие данные не удовлетворяют 2НФ, 162 имена полей с одинаковыми префиксами признак, что существующие данные не в ЗНФ, 167 учебный пример, 175 имена полей с числовыми суффиксами признак, что существующие данные не удовлетворяют 1НФ, 158 имена полные примеры нарушения 1НФ, 153 имена функциональные атрибуты используемые в качестве внешних ключей, 124 именованные отношения 40,54 импортирование данных проблемы целостности данных, 418 импортирующие прослойки взаимодействие с внешними системами, 216 индексы БД только для чтения может иметь много, 607 выгодные и формирование запросов, 554 вычисление размера, используя специальные хранимые процедуры, 554 должны включаться в OLAP-БД, 546,549 используются для организации уникальности, 308 команды DBCC, 597 литеральные значения по умолчанию, 373 максимальное число байт на вход индекса, 269 максимальное число кластерных и некластерных индексов, 269 "мастер" настройки индекса, 597 нелинейный рост, 590 нормализация уменьшает потребность, 151 обеспечение уникальных значений, 313 обозначение необязательно, 318 оставить реализацию до стадии настройки, 319 программный подход к рассмотрению узких мест, 568 расчет объемов данных, 550 слишком много проблем дают при функционировании, 597 индексы выгодные, 554,557 индексы кластерные, 310 вычисления, определяющие наличие индекса, 556 задание первичных ключей по умолчанию, 317 нормализация и использование индексов, 151 причина иметь короткие индексные ключи, 553,556 причина иметь короткий размер строк, 556 свободная область из фактора заполнения, 552 учебный пример, 590 факторы, определяющие выбор, 310 индексы уникальные использование с вторичными ключами, 317 обеспечение уникальных значений, 308,313 инкапсуляция код внутри ОПФ, 378 код внутри хранимой процедуры, 491,522 соединения и т. д. для маскировки от пользователя, 475 типы данных, определяемые пользователем и средства, 292
интегральное определение для информационного моделирования см. IDEF1X. интеграция одиннадцатое правило Кодда для СУРБД, 634 интернационализация сложности с размещением номеров телефонов и адресов, 153 интерфейс внешние системы, 213 гибкость, используя нерегламентированный SQL-код, 518 интерфейс на естественном языке English Query, 614 интерфейс пользователя не зависит от изменений хранимой процедуры, 494 интерфейс прикладного программирования см. API. информационная разработка см. IE. информационное правило первое правило Кодда для СУРБД, 629 информация о времени см. также информация о дате. размещение и манипулирование, 280 информация о дате datePart, системная функция, 476 использование дат в ключах не рекомендуется, 345 период архивирования, 561 проверка, является ли дата будущей, 428 размещение и работа с датой, 280 информация о состоянии нарушения 4НФ, 192 искусственные ключи использование для идентификации строк, 49 исполнители, модели Use Cases, 106 включение информации о безопасности, 211 учебный пример, 138 исполняемый код клиента защита целостности данных, 358 использование данных, 206, 210 учебный пример, 220 используемость уменьшается в случае «тонких» клиентов, 251 испорченное чтение, 585 испытание нагрузки, 621 истинности таблицы сравнения, включающие NULL, 469 историческая информация размещение в хранилище данных, 13 история проектирования БД, 8 источники гетерогенных данных, 246 источники данных объединение в БД только для чтения, 598 исходные системы см. также наследование данных. опасность копирования, 30 получение бизнес-правил из старого кода, 81 преобразование данных может быть неудачным, 217 проблемы, возникающие из потребностей взаимодействия с ними, 213 итоговые данные добавление в БД только для чтения, 602,604,606 построение с индексируемыми представлениями, а не триггерами, 479 признак нарушения ЗНФ, 167 причины отхода от логического проекта, 266 суммирование данных сделок в ODS, 12 учебный пример, 175
6 4 7
каскадирование модификаций К каскадирование модификаций внешние ключи и каскадированные удаления, 319 задание ссылок ключей потомков равными NULL, 323 каскадирование добавлений, используя триггеры AFTER, 404 каскадирование обновлений и удалений, 322 каскадирование удалений с помощью триггера AFTER, 407 необязательное каскадирование удалений, 420 обработка ошибок от внутренних триггеров, 398 триггеры INSTEAD OF игнорируют каскадированные обновления, 411 каталог динамический оперативный четвертое правило Кодда для СУРБД, 630 каталог системный четвертое правило Кодда для СУРБД, 630 кластерные серверы, 244, 623 кластерные таблицы с некластерными индексами, 312 клиент, программное обеспечение инкапсуляция защищает от изменений SQL, 494 ключ интеллектуальный, 143 ключи атрибуты и отношение с нормализацией, 158 в качестве детерминантов в НФБК, 170 использование дат нецелесообразно, 345 "ключ, весь ключ и ничего кроме ключа", 159 "ничего, корме ключа", 165 реализация, 307 соглашения обозначений для первичных и вторичных ключей, 318 ключи естественные должны быть уникальными идентификаторами, 73 учебный пример, 142 ключи первичные внешние ключи в отношениях «один к п», 60 выбор при логическом моделировании, 170 задание по умолчанию кластерного индекса, 317 имя сущности повторяется в имени атрибута, 117 использование в хранимых процедурах обновления, 510 логические изменения влияют на реализацию ключей, 343 миграция, обозначение, 117 миграция различает идентифицирующие и неидентифицирующие отношения, 119 мигрирующие указатели не подходят для реализации, 315 не могут иметь необязательные атрибуты, 122 размещение данных внутри БД, 630 реализация таблиц с незначащими ключами, 315 роль в поддержании уникальности, 47 служебные столбцы, 342 сущности IDEF1X должны иметь ключи, 112 требуются для сущностей в 1НФ, 155 учебный пример, 144 целостность сущности, 634 ключи потенциальные избежание дублирования информации, 47 НФБК, 170,176 ключи разумные, 73,143 ключи составные 138 не-внешние ключи предполагают нарушение 2НФ, 163 2НФ применима к составным ключам, 159
6 4 8
Кодд, Эдгар Ф, правила БД, 50,51,629 родоначальник реляционной БД, 8,152 сформулированные реляционные оператры, 54 кодирование см. программирование. коды ошибок, определяемые пользователем, 384 коллеги получить оценку проекта, 206 константные выражения ограничения по умолчанию, 373 консультация с заказчиком должна предшествовать моделированию данных, 21 определение будущих источников, 33 определение требований к отчетам, 208 основные принципы организации интервью, 25 пересмотр первого варианта документа, 87 предлагаемые вопросы для интервью, 27 разметка записей интервью, 87,89,98 контекста переключение использование волокон, чтобы избежать, 576 контракты и порядок работы источник правил данных, 29 контроллер распределенных транзакций, 246 пример кода, использующего связанные серверы, 247 транзакции, охватывающие различные серверы, 533 копия БД см. БД, копия только для чтения. корректировка данных, 437 SQL-операторы подчиняются седьмому правилу Кодда, 651 включение сообщений об ошибках, 487 гибкость использования нерегламентируемого SQL-кода, 519 делает кластерные индексы менее привлекательными, 310 допускается для хранимых процедур, но не для ОПФ, 481 доступ к данным между БД, 531 доступ к измененным строкам с помощью триггеров, 388 корректировка представлений с помощью триггеров INSTEAD OF, 416 недостатки в корректировке представлений, 415 отдельные строки, с хранимыми процедурами, 505 примеры использования триггеров INSTEAD OF, 411 проблемы, вызываемые нарушением 2НФ, 161 проблемы, вызываемые нарушением ЗНФ, 163 проблемы, вызываемые тройными отношениями, 184 седьмое правило Кодда для СУРБД, 632 типичная модель операции, 505 триггеры INSTEAD OF удовлетворяют общим расширениям, 413 корректировка списков проблемы, вызываемые нарушением 1НФ, 155 корректируемость представлений шестое правило Кодда для СУРБД, 631 кортежи, 41 как строки таблицы, 41 круглые скобки всегда требуются в ОПФ, 360 кубы данных как технология запросов в витринах данных, 20 определение, 14 курсоры, 459 использование хранимых процедур, оценивающих объемы, 555 направление, 461 не допускается как тип данных столбца, 291
моделирования методологии обновляемость, 461 условия использования, 464 "куча" таблицы без кластерных индексов, 311,552 кэширование вызовы БД через умеренные соединения, 240 контроллером диска, 577 контроль данных может противоречить целостности данных, 419 кэширование записи, 577 планы запросов, 520 страниц для ускорения доступа к данным, 577 кэширование записи, 57 компромисс с целостностью данных, 578
Л "легкие" клиенты см. мобильные клиенты, литеральные значения по умолчанию, 373 лицензии, важность внимательного изучения, 584 лицензии программного обеспечения, 243 логика распределение в трехуровневой и п-уровневой системах, 254 трехзначная логика, включающая NULL, 468 логическая независимость данных девятое правило Кодда для СУРБД, 632 обеспечивается использованием представлений, 51 логические домены см. домены, логические чтения параметры работы, 502 логическое проектирование завершающие стадии, 205 использует общие типы данных, 44 причины отхода во время реализации, 262,266 локальные временные таблицы используются для соединений с SQL Server, 454 локальные сети, нарушение работы минимизация влияния пакетированием транзакций, 452 локаторы строк, 311 "кучи", включающие страницу и смещение, 312 люди как сущности в БД, 87
м маски см. также битовые маски. использование для проверок данных, 380 массив использование как временные таблицы, 457 массовая загрузка транзакций, 558 массовое копирование нарушает двенадцатое правило Кодда для СУРБД, 635 промежуточные таблицы для уменьшения издержек, 451 масштаб тип данных decimal, 275 масштабируемость см. также пользователи, число. необходимость знать до реализации, 213 матрица недорогих устройств с избыточностью см. RAID.
менеджер проекта, роль, 22 (1022) места как сущности в БД, 68 межтабличные проверки использование правил должно быть на уровне БД, 418 метаданные процедуры, представления информационной схемы и системные стандартные процедуры, 325 элементы отношения (столбец), 52 метаданные сервисов см. MDS метод водопада, 100 механизм БД различие реляционного механизма и механизма размещения, 632 миграция первичных ключей идентифицирующие и неидентифицирующие отношения, 119 как внешних ключей в отношении «один к п», 59 обозначение первичных ключей, 117 мигрирующие атрибуты, мигрирующие ключи см. внешние ключи, многоуровневый проект см. п-уровневая конфигурация мобильные клиенты см. также перемежающиеся соединения. БД только для чтения,613-614 использование ODS для передвигающегося штата, 13 тиражирование слиянием наиболее подходит, 245 удаленный штат может использовать диалоговый доступ, 240 модели данных предприятия, 68 логическая модель предприятия, 618 модели, ориентируемые на решение БД для формирования отчетов, 601 модели, ориентируемые на таблицы БД для формирования отчетов, 601 преимущества подхода, 604 модели процессов см. Use Case модели, моделирование данных, 103 см. также моделирование логическое; моделирования методология. альтернатива неуправляемому списку, 97 идентификация соответствующих процессов, 83 определение моделей данных, 103 различие логических и физических моделей, 108 система счета-фактуры для преобразования в БД только для чтения, 600 учебный пример, 140,179 моделирование логическое денормализация нежелательна, 196 должны быть рассмотрены нормальные формы, 152 итоговые данные абсолютно запрещены, 167 коррекция ошибок, 340 логическая модель предприятия, 618 модели должны быть живыми документами, 111 независимость от реализации, 109 первичные ключи, выбор, 170 сущности как концептуальная версия таблиц, 41 учебный пример, 179,203 моделирования методологии см. также Enterprise Manager; IDEF1X; Information Engineering; Use Case модели; моделирование данных. ключ к моделированию данных в версии Enterprise, 619 определение, 104 представление отношений, 132
6 4 9
моделирования средства моделирования средства см. также моделирования методологии. обеспечиваемые SQL Server, 245 модель проекта contact (партнер) пример атрибутов с несколькими величинами, 190 модификация данных высокого уровня седьмое правило Кодда для СУРБД, 632 модуль контроля непротиворечивости БД, 464, 597 мощность используется для обозначения неоднозначного числа строк, 50 отношений, 59 правила, как использовать для триггеров AFTER, 405 таблица шести возможных мощностей, 123
н "Набор", оператор DISTINCT, ключевое слово, 55 наборы результатов определяемые как неименованные отношения (таблицы), 40, 53 тип table используется для хранения данных, 291 хранимые процедуры, возвращающие результат оператора SELECT, 484 наследование использование доменов зависит от средств моделирования, 115 типы данных, использующие домены IDEF1X, 115 наследование данных см. также данные исходные. объединение в хранилища данных, 13 наследуемые системы см. также исходные системы. возможность размещения исходных данных, 28 источник денормализованных данных, 598 проблемы взаимодействия с денормализованными системами, 215 настольные БД возможность размещения исходных данных, 28 настройка памяти приложения, 564 настройка памяти процесса счетчики, 572 настройка функционирования, 596 Windows 2000 Performance Options, 574 дисковая подсистема, 577 дисковая подсистема, счетчики, 582 подсистема ЦП, 574 пять ключевых мест, где возможны узкие места, 567 роль Query Analyzer, 597 неалфавитные символы признак, что существующие данные не в 1НФ, 158 "невидимые строки", 445,447 независимости целостности правило, 50,51 десятое правило Кодда для СУРБД, 634 независимость распределения одиннадцатое правило Кодда для СУРБД, 634 некластерные индексы, 311 учебный пример, 594 необязательные значения см. NULL, значения, неправильные данные см. проверка данных, нерегламентированный SQL-код, 518,521 нерегламентированные запросы БД только для чтения, ориентируемые на таблицы, 605 несколько каналов данных, 581
6 5 0
несколько ключей НФБК учитывает, 168 несколько контроллеров дисков, 581 несколько соединений, 56 номер социального страхования см. SSN. нотация "верблюда", 111 нормализация, 150 см. также денормализация. игнорирование правил с БД только для чтения, 601 определение,8 отработка подтипов может являться компромиссом, 264 причины нормализации структур данных, 150 расширенная, 181 регулирование эксплуатационных характеристик, сохраняя нормализацию, 235 семь типов нормальных форм, 152 таблица, суммирующая требования к нормализованной форме,203 учебный пример после модификации, 203 НФ (нормальные формы) см. 1НФ, 2НФ, ЗНФ, 4НФ, 5НФ, НФБК, НФДК НФБК (Нормальная форма Бойса-Кодда), 168 заменяет 2НФ и ЗНФ, 168 признаки нарушений 2НФ или ЗНФ, 172 учебный пример, 174,179 НФДК (Нормальная форма домен-ключ), 195, 252
О обзор первый проект документа, 86 стадия проектирования, которой часто пренебрегают, 19 область действия транзакций пакетирование транзакций, 443 обновление критерии для обновления строк, 311 обновление отдельных строк, 508 поддержка обновления БД, 598 частота в БД только для чтения, 609 обозначение в квадратных скобках, 267, 338 обозначения, соглашения см. также терминология. атрибуты,117 доступ к данным между БД, 530 иллюстрация различных типов именования таблиц, 268 ключи и индексы, 318 объекты БД, 111,117,267 ограничения по умолчанию, 372 определяемые пользователем функции, 361 ориентированные на таблицы БД только для чтения, 604 отношения, 130,320 проблемы из-за неинформативных имен столбцов, 214 расширенные свойства, 331 столбцы таблицы, 270 сущности, 111 схема из четырех частей в SQL Server, 269 обработка ошибок см. также проверка данных. операторы, которые нарушают ограничения, 383 пакеты с несколькими операторами, 514,516 реализация с триггерами, 397 стороны клиента и сервера, 252
отношение "расширяет' хранимые процедуры, 485 хранимые процедуры для проверки возвращаемых значений,483 хранимые процедуры для проверки выходных параметров, 483 хранимые процедуры для проверки сообщений, 491 обработка транзакций в реальном масштабе времени см. OLTP. обслуживание планы обслуживания, 622 проблемы систем архивирования, 560 объединение разделенных представлений, 581 объекты см. СОМ; объекты БД. объекты БД иерархия, используемая в расширенных свойствах, 330 как сущности в БД, 68 средства формирования отчетов SQL Server 2000, 325 управление правами доступа, 524 объекты управления данными см. DMO. объемный анализ см. также данные, объем. определение, 545 планирование, 217 стратегия архивирования должна быть определена заранее, 559 учебный пример, 225,588 "Ограничение", оператор WHERE, оператор, 55 ограничения, 371 добавление с ALTER TABLE, 320 завершение сделки, не подходит для SQL Server 2000, 203 запрещение ограничений нарушает двенадцатое правило Кодда, 635 обеспечение целостности данных, 358 определение и классификация, 45 определение пяти типов, 371 ошибки от триггеров вызывают откат, 397 ошибки, отображающие дружественные сообщения, 488 сравнение с триггерами, 386,399 ограничения в виде внешних ключей используемые правила должны быть на уровне БД, 419 один из пяти типов ограничений, 371 ограничения между таблицами и между строками обеспечиваются триггерами, 386 ограничения первичного ключа, 371 ограничения по умолчанию, 372 учебный пример,423 ограничения таблиц тип проверки-ограничения, 375 ограничения столбцов тип проверки-ограничения, 375 ограничения целостности целостность сущности и ссылочная целостность, 634 ограничители в виде двойных кавычек, 267 ожидаемая жизнь страницы, 570,572 ОЗУ (оперативное запоминающее устройство) см. память. округление, 275 ООП (объектно-ориентированное программирование) 41 аналогия со строками и столбцами таблицы, 41 отношение «является» аналогично подклассу, 78 оперативное хранилище данных, 615 использование для передвигающегося штата, 13
объединение данных, включая определение общих объединенные данные для ежедневных отчетов, 12 терминов, 616 определение OLAP, 546 формат, ключ к преобразованиям хранилищ данных, 16 операторы восемь реляционных операторов Кодда, 54 модификации и поисковые, 58 отличие от ключевых слов SQL, 54 элементы реляционной алгебры, 54 операции побитовые двоичные типы данных не поддерживают, 282 операции с набором данных альтернативы курсорам, 460,463 операции со строками архивирование избранных строк таблицы фактов, 562 использование курсоров, 459,460 операции только для чтения ограничение определяемых пользователем функций, 360 операция "извлечение, преобразование и загрузка", 16,617 описательная информация добавление к графическим моделям данных, 135 описательные свойства связь с разработчиками, 330 определяемые пользователем счетчики, 586 оптимизатор запросов БД только для чтения содержит индексы всех столбцов, 608 может использовать ограничения, 371 расширение блокировки на уровне строки, 238 уникальные индексы помогают работать, 314 хранимые процедуры легче оптимизировать, 522 ОПФ (определяемая пользователем функция), 360 встроенные функции формирования таблиц, 365 доступ к данным столбца переменной длины, 551 задание владельца, 362 курсоры и нелокальные объекты, 370 нельзя включать некоторые встроенные функции, 370 нельзя передавать заданные пользователем параметры,362 описание версий, 360 пример проверки диапазона, 430 проверка пустых строк, 376 связывание схемы, 366 сравнение с хранимыми процедурами, 481 учебный пример, 424 функции со многими операторами, формирующими таблицу, 369 организация циклов SELECT для объединяемых таблиц эквивалентен циклу, 17 ОС (операционная система), 9 отказы источников питания, 623 открытый интерфейс доступа к базам данных см. ODBC отладка ошибки, связанные с использованием NULL, 471 отношение коммуникаций модели Use Case, 107 отношение "имеет" пример отношения "один к X", 78 отношение "использует" модели Use Case, 107,139 отношение "расширяет" модели Use Case, 107,139
6 5 1
отношение является отношение "является" пример отношения "один к п", 78 отношений деревья создание БД только для чтения, 602 отношений список, 79 отношений типы между таблицами БД, 78 описание менее важных типов, 125 отношения, 40 Enterprise Manager не отображает триггеры, 135 Enterprise Manager, средство построения диаграмм, 134 бинарные и небинарные, 59 все сущности должны быть отношениями и связанными с одной темой, 204 глагольные конструкции дают удобочитаемые предложения,130 добавление описательной информации, 135 идентифицирующие и неидентифицирующие, 119, 120 методологии, отличные от IDEF1X, 132 методология Chen ERD, 134 между БД требуют триггеры, 324 обеспечение с помощью триггеров AFTER, 403 обозначение, 320 однотемные, 204 определение в данных, полученных от заказчиков, 66 представление в соответствии с методологией моделирования данных, 118 происхождение реляционной БД, 39 реализация, 319 сущности, 118 таблица примера поставщиков офисного оборудования, 79 таблица различных мощностей, 123 таблица символов, используемых в Information Engineering, 132 таблица типов в IDEF1X, 118 учебный пример,89, 142 отношения двоичные, 59 отношения идентифицирующие, 119 Information Engineering, методология, 132 графическое представление, 118 нарушения 2НФ, 161 отношения иерархические, 61,125 графическое представление, 118 отношения классификации, 127 полные и неполные категории, 128 отношения между БД, 324 внешние ключи, 319 отношения "многие ко многим", 62,129 графическое представление, 119 разрешающие сущности, 129 отношения небинарные, 62 отношения неидентифицирующие, 120 графическое представление, 120 нарушения ЗНФ, 164 обязательные и необязательные, 121 отношения необязательные, 122 Information Engineering, 132 графическое представление, 118 должны быть неидентифицирующими, 122 отношения обязательные обеспечиваемые триггерами AFTER, 403 обязательные неидентифицирующие отношения, 121 отношения "один к п", 60 "один к одному" и "один ко многим", 77
6 5 2
отношения "один к X" таблица, показывающая возможные мощности, 123 отношения "один к одному", 60 отношения "один к одному или большему числу", 123 отношения "один ко многим", 60,119 Enterprise Manager, средство построения диаграмм, 134 отношения подтипов, 127 графическое представление, 119 учебный пример, 141 отношения рекурсивные, 61,125 графическое представление, 119 отношения само-ссылающиеся, 61,125 графическое представление, 119 отношения тройные, 62, 183 5НФ, 195 разрешение, 186 связанные с 4НФ, 182 отчет банка bank образец как источник информации об атрибутах, 91 отчеты, формирование, 207,235 ежедневные данные, основанные на ODS, 12 использование копии БД, чтобы улучшить характеристики, 236 ключевые проблемы в физическом проектировании, 235 объемы данных и реализация, 235 проблемы при сложном поиске, 237 простые требования для БД только для чтения, 614 реализация БД только для чтения, 607 сложность отчетов и реализация, 237 стандартные и специализированные отчеты, 208 требования для поддержания решения, 548 требования к данным должны быть определены на ранних стадиях, 27 требуются своевременные данные, 238 учебный пример, 222 формирование отчетов в реальном времени, используя представления, 474,475 хранилища данных для оптимизации формирования отчетов, 9 частота получения, 239 отчеты, формирование в реальном времени, 475 оценка штата специалисты, требуемые в больших проектах, 243 очистить и начать снова реализация БД только для чтения, 611 очистка данных, 246 ошибки см. также обработка ошибок, программирование. связанные с использованием NULL, 471 ошибки обновления строк, 514 ошибки ограничений от триггеров вызывают откат, 397 отображение дружественных сообщений, 488
п пакетирование SQL-операторы, выполняемые в группе, 513 вызовы БД через умеренные соединения, 240 рост пакетов OLAP-БД, 548,559 транзакции не должны выходить за границы пакета, 452 транзакции с оператором GO, 443
правило единственности пакеты с несколькими операторами, 514 определение, 513 ошибки обновления строк, 515 пакеты с одним оператором, 514 память важность максимизации ОЗУ, 563 динамическая настройка памяти, 569 настройка, доступные счетчики, 568 настройка, несколько экземпляров сервера, 571 настройка памяти процесса, 572 оптимизация сервера Windows 2000, 564 оптимизация сервера Windows NT, 564 работа с SQL Server 7 и 2000, 566 работа с AWE, 565 свободная память ОС, 570 требования к другим приложениям, 569 требования к соединениям сервера, 243 "паралич анализа", 225 параллелизм см. также состязание доступа; блокировка. конфликт с целостностью данных 448 максимизация с БД только для чтения, 598 управление, 300, 443 параметры, названные пользователем определяемые пользователем функции не допускают использование, 362 паритетная информация (RAID 5), 580 перегрузка диска, 582 перемежающиеся соединения см. также мобильные клиенты. подписка БД только для чтения, 613 частота обновления БД только для чтения, 608 переменные объявление как тип данных cursor, 461 типы данных, определяемые пользователем работают противоречиво, 295 переносные устройства см. мобильные клиенты, пересечение таблиц модели, ориентируемые на решение, 606 "Пересечение", оператор, 57 пессимистическая блокировка, 300 планирование проектирования, 218 важность всестороннего документирования, 24 стадия сбора информации, 21 учебный пример, 227 планировщик работы SQL Server Agent, 249 планы запросов повторное использование, 520,523 повторное использование хранимых процедур, 520 роль в настройке функционирования, 597 планы преобразования данных, 217 повторение групп данных признак нарушения ЗНФ, 167 признаки нарушения 2НФ, 162 повторение префиксов ключей в именах полей, признаки нарушения 2НФ, 162 поддержка БД см. БД, копия только для чтения. модели Use Case, 106 поддержка решений ежедневная, основанная на ODS, 13 основное использование хранилища данных, 13 требования к отчетам в OLAP-БД, 548 поддержка транзакций включающая более одного сервера с использованием DTC, 246 целостность данных OLTP, 11
подзапросы включение в соединения, 456 пример использование вложенных подзапросов вместо курсоров, 463 подзапросы вложенные пример работы без курсора, 463 подписка тиражирования, 613 подписки определение тиражирования, 245 подсистема ЦП контроль, но не настройка работы, 528 подтипы причины отхода от логического проекта, 262 учебный пример, 336 ПОИСК
данные, чувствительные к регистру, могут вызвать проблемы, 305 полнотекстовый поиск, 249 полоса пропускания уменьшение, используя "толстый" клиент, 250 пользователи см. также консультация с заказчиком. влияние многоуровневого проекта на разные типы, 417 важность ранней идентификации, 27 наблюдение важных соединений пользователя, 584 представление в моделях Use Case, 106 ряд проблем с классической конфигурацией клиент-сервер, 254 само-конфигурирующиеся соединения SQL Server 7, 583 транзакции воздействуют на все соединения пользователя, 443 управление доступом к данным с помощью хранимых процедур, 520 число и конкуренция, 211,212 число и оптимизация характеристик, 243 пользователи переносных компьютеров см. мобильные клиенты, пользовательские БД источник денормализованных данных, 599 поля с несколькими частями корректировка, 156 понижение типов курсоров, 462 портативные компьютеры см. мобильные клиенты, последовательность см. также данных целостность. противоречие с параллелизмом, 449 свойства транзакции, 438 потоки приложения, не входящие в ядро, используют волокна, 576 права см. доступ к данным, права доступа см. также права доступа к данным. задание для ОПФ, 362 права доступа к данным, 210 см. также владелец БД, роль, правила использование типов данных, определяемых пользователем, 293 правило гарантированного доступа второе правило Кодда для СУРБД, 630 правило единственности двенадцатое правило Кодда для СУРБД, 634
6 5 3
предварительная документация предварительная документация дополнительные вопросы после анализа, 71 определение сущностей во время анализа, 67 учебный пример, 34 предикаты, 47 правила ввода данных в таблицы, 53 предикаты доменов, 147 представления,474 информационная схема как набор представлений, 325 использование для маскирования, 474 корректировка, используя триггеры INSTEAD OF, 416 ограничение доступа к представлению, нежели к таблицам,476 ограничения единственной таблицей, 387 определение и обсуждение использования, 51 организация абстрактных прослоек, 217 ориентированные на таблицы БД только для чтения, 605 проблемы с модификацией, 415 разделение данных таблицы, 477 сравнение с хранимыми процедурами, 474 триггеры INSTEAD OF используются не только для отдельных таблиц, 417 шестое правило Кодда для СУРБД, 631 представления данных см. моделирование данных; Use Case модели, представления индексируемые, 479 используются для организации сложного поиска, 238 ограничения на исходное содержание, 480 преобразования неявные, 275 преобразование типа может вызвать проблемы с вариантными данными, 287 скаляры для бинарного сопоставления, 307 типа datetime в строку, 428 привилегии пользователя см. доступ к данным, привилегированность доступа см. доступ к данным приемочные испытания, 621 приложения,типы определение числа соединений пользователей, 584 пример БД записи артистов, 359 безопасность на уровне столбцов, 529 безопасность на уровне строк, 527 доступ, используя ОПФ, 364 пример БД ресторанов, 40 пример видеотеки обработка подтипов, 262 обязательные отношения, 122 отношения классификации, 128 пример двойного сопоставления, 305 пример издания книги модели Use Case, 107,211 нарушение ЗНФ, 164 нарушения 2НФ, 166,162 пример планирования презентаций разрешение тройных отношений, 183 пример продажи автомобилей использование итоговых данных, 167 пример проката фильмов см. пример видеотеки. пример работы банка см. учебный пример, приобретенные системы недоступность деталей реализации, 618 природные бедствия, 623 проблемы функционирования БД только для чтения, 607 выгодные индексы, 554,556 добавление индексов на стадии настройки, 319
6 5 4
индексируемые представления, 479 курсоры, 462 максимизация запросов в БД только для чтения, 599 настройка, не жертвуя нормализацией, 235 нерегламентированные запросы SQL-кода, 520 нерегламентированные запросы, устраняемые использованием представлений,476 ограничения структуры, 212 основные проблемы кодирования, 453 размещение изображений, 283 размещение полей типа text, 284 распределенные секционированные представления, 480 серверы,563 триггеры и проверки-ограничения, 383 удаления и устройства блокировки записей, 303 формирование отчетов, 235 число пользователей как фактор, 243 чтение с дисков и внешнее размещение, 559 проверка см. проверка данных. проверка границ использование триггеров AFTER, 405 проверка формата поля с помощью ОПФ, 378 трудно использовать триггеры INSTEAD OF, 410 учебный пример, 429 проверка данных включение межтабличных сравнений, 418 использование "толстых" и "тонких" клиентов, 250 код проверки одной строки, 382 многострочная проверка с помощью триггеров, 392 обработка ошибок от внутренних триггеров, 398 правила, 29 правила, которые не подходят для реализации клиента, 252 пример записи номеров каталогов, 378 проверка диапазона с помощью триггера AFTER, 405 проверки-ограничения, определение неправильных данных, 382 таблицы исключений, 413 проверка характеристик см. также настройка функционирования; тестирование. вторая стадия в разработке БД, 620 проверки-ограничения, 375 зависимости между столбцами, 52 значение <усповия_поиска>, 376 использование проверки диапазонов, 429 используемые правила должны быть на уровне БД, 418 на основе функции,379 нарушения, специальные сообщения об ошибках, 489 один из пяти типов ограничений, 371 определение, 45 получаемые из списков доменов, 425 триггеры могут работать лучше при проверках, 383 учебный пример, 424 программа массового копирования см. ВСР. программирование дополнительное в ненормализованных системах, 151 кодирование приложений, а не создание объектов, 438 кодирование триггеров, оператор CREATE TRIGGER, 387 логические ошибки из-за NULL, 471 проблемы, связанные с нарушением 1НФ, 155 проблемы, связанные с нарушением НФБК, 171 проблемы, связанные с нормализацией, 163 проблемы, устраняемые с помощью 2НФ, 160
регистр банка проблемы, устраняемые с помощью ЗНФ, 184 реляционное и процедурное, различия, 17 проектирование интегрированных БД четыре функциональных модуля, 10 "Произведение", оператор пример CROSS JOIN, 56 протокол передачи файлов, 75 прототип БД риск нарушить ожидания заказчика, 25 прототипы отчетов выбор данных для представления, 210 учебный пример,223 процедура преобразования учебный пример,626 процедуры фиктивные, 494 процедуры хранимые 481 см. также SQL-запросы компилируемые. RECOMPILE, опция, 504,521 безопасность обеспечивается на уровне объектов, 494 возвращаемые значения, 482 выполнение для каждой строки в наборе, 464 вычисление полного размера таблицы, 550 вычисление размера индекса, 554 инкапсуляция - основное преимущество, 523 использование бизнес-правил, 419 корректировка данных, 505 можно переписать, но продолжают работать, 493 наборы результатов, возвращаемые оператором SELECT, 484 не должны изменять число транзакций, 443,495 необязательное каскадирование удалений, 420 обеспечение бизнес-правил, 422 обеспечение целостности данных, 358 обновление отдельных строк, 508 обработка ошибок, 485 обработка ошибок при вызове из триггеров, 398 обычное использование, 500 ограничение путей доступа к данным, 5200 параметры работы, 502 повторное использование планов запросов, 520 реализация БД только для чтения, 610 реализация с доменами столбцов, 46 следует избегать взаимодействий с клиентом, 485 создание отдельных строк, 506 специальные, для учебного примера, 540 сравнение с ОПФ, 360,481 сравнение с пакетами, 481 сравнение с представлениями, 474 удаление отдельных строк, 511 усечение объединенной таблицы, 626 учебный пример, 533 процесс экспертной оценки, 206 процессора время настройка для высокоприоритетных и фоновых приложений, 573 процессы определение из документации заказчика, 84 получение в записях интервью с заказчиком, 98 таблица новых процессов в примере банка, 99 процессы основные, 83 процессоры, родственность, 574 псевдонимы типы данных, определяемые пользователем, 292 публикации определение тиражирования, 245 пустые строки проверка, использующая ОПФ, 378,424
пул соединении организация в бизнес-слое n-звенной системы, 254 техника для многопользовательских систем, 243
разгрузка данных большие передачи данных могут быть разбиты на части, 241 разделение дисков (RAID), 578, 581 разделители обозначение в квадратных скобках, 267,338 ограничения обозначений, 321 признак, что существующие данные не удовлетворяют 1НФ, 158 соглашение об обозначениях в SQL Server, 267 размер БД см. данные, объем; объемный анализ, размерности ограничения для таблиц БД, 269 размеров ограничения компоненты таблиц БД, 269 "Разность", оператор NOT IN, оператор SQL, 58 разработка БД итерационный процесс, 620 разработка и логическое проектирование определение процесса разработки, 66 стратегия разработки отчетов, 208 разработчики связь с разработчиками, 325 скорость, используя нерегламентированный SQL-код, 520 разреженные таблицы, 286 разрешение конфликтов см. состязание доступа, распределенное вычисление кластерные серверы, 244 обычно включается в приложения БД, 18 распределенные системы, 256 распределенные запросы, 532 распределенные секционированные представления,480 расширенные свойства определение и добавление программным способом, 330 реализация задачи, которые нужно решить перед началом реализации, 206 отличие от логического проектирования, 234 реализация, проблемы денормализация, 196 использование итоговых данных, 167 конфигурация сети, 239 ненормализованные системы приводят к излишнему программированию, 151 разбиение страниц, 310 устранение использованием представлений, 474 реальные данные разработка БД не должна содержать, 260 получение отчетов, используя представления, 474 ревизии перенести формирование протоколов ревизий на стадию физического проектирования, 70 требования, 30 регистр банка атрибуты, получаемые из электронного формата, 92
6 5 5
регистрация регистрация перенести разработку на стадию физического проектирования, 70 реконфигурация серверов проверка вложенных и рекурсивных триггеров, 395 рекурсивное дерево, пример, 456 рекурсивные триггеры, 394 реляционные БД см. БД. реляционные операторы операторы поиска, сформулированные после Кодда, 658 реорганизация БД копирование с большим объемом данных, 236 роли (безопасности), 524
сбалансированные деревья см. В-деревья. свободная память ОС, 570 своевременность системы принятия решений, 238 сворачивание подтипов, 264 связанные серверы, 246,533 пример кода, использующего DTC, 247 связь между членами команды проектировщиков, 2 связывание взаимодействие с внешними системами, 216 связывание схемы, 366 желательно использовать в окончательных системах, 367 необходимо, чтобы представления были индексированы, 479 определение, 331 сервер Windows 2000 оптимизация памяти, 564 преимущество над NT, 574 сервер Windows NT оптимизация памяти, 564 хуже, чем 2000, 574 серверы Web-приложения с копиями БД только для чтения, 255 важность передачи обработки на сервер, 18 два или большее число, необходимые для разработки, 243 кластеризация, балансировка нагрузки и разбиение информации, 244 набор вложенных триггеров на уровне сервера, 394 настройка работы сервера, 573 несколько вариантов сервера.-настройка памяти, 571 проблемы реализации, 563 распределенные секционированные представления из нескольких частей, 481 реорганизация для работы с большими объемами данных, 236 сервисы преобразования данных, 246 проблемы целостности при импорте данных, 419 БД только для чтения, 610 планирование задач с помощью SQL Server Agent, 249 сервисы промежуточного уровня перемещение реализации бизнес-правил, 417 символьные данные задание Unicode в строках, 285 недостатки использования для хранения других данных, 284 способы определения динамической строки символов, 554 6 5 6
тип данных char, 283 символы иностранные см. языки, отличные от английского, символы "птичьей лапки", 132 система отложенных записей, 577 система счета-фактуры модель для преобразования в БД только для чтения, 600 системный архитектор, роль, 22 системы клиент-сервер большие системы имеют непростую структуру, 241 влияние скорости соединения, 240 идеальные системы - между "тонким" и "толстым" клиентом,252 простейшая конфигурация использования SQL Server, 253 "толстый" и "тонкий" клиенты, 250 учебный пример,256 системы третьих лиц недоступность деталей реализации, 618 скалярные величины возвращаемые выходные параметры, 484 скалярные выражения могут использовать ОПФ, но не стандартные процедуры, 482 скалярные функции, вариант ОПФ, 361 скорость соединения глобальные сети и Web-приложения, 240 скрытые атрибуты с несколькими величинами, 189,198 сложное использование Web-серверов, 255 служебные столбцы, 300 учебный пример,342 события как сущности в БД, 70 соединение объектов, ADO может вызвать проблемы блокировки, 453 соединение, разрыв минимизация последствий пакетированием транзакций, 452 соединения включение подзапросов, 456 иллюстрация отличия SQL от процедурного программирования, 17 инкапсуляция, используя представления, 474,475 использование кластерных индексов, 310 минимизация в БД только для чтения, 602 несколько каналов могут увеличить скорость чтения данных, 581 объединение вместе, 56 соединения по требованию подписка БД только для чтения, 613 частота обновления БД только для чтения, 609 соединения с другими системами могут потребоваться исходные данные, 29 соединения таблиц см. соединения, создание отдельных строк, 506 сокращения в названиях атрибутов следует избегать, 117 в названиях сущностей следует избегать, 112 исключения из правил обозначения, 270 сообщения об ошибках, 383 включение после оператора модификации, 487 использование правил типов данных, определяемых пользователем, 293 коды ошибок, 383 специальное использование таблицы для размещения информации, 488
сущности тупиковая ситуация, заданная номером ошибки 1205,451 хранимые процедуры возвращают значения и сообщения об ошибках, 481 сообщения об ошибках информативные, 488-489 сообщения об ошибках специальные, 489 нарушения проверок-ограничений, 489 тег значений для включения в сообщение, 490 сопоставление типов, 304 см. также сортировка. учебный пример, 353 сортировка см. также сопоставление типов. использование курсора для искусственной сортировки, 464 состязание см. состязание доступа, состязание доступа см. также параллелизм. минимизация БД только для чтения, 599,607 нерегламентированные запросы пользователя как источники, 238 следует избегать в сильно распределенных системах, 256 специализированные отчеты,208 список объектов учебный пример, 93 список элементов управления заполнение, пример хранимой процедуры, 500 среда хранения стратегия архивирования и среда размещения, 560, 563 средства формирования отчетов объекты БД, 325 подходы, ориентируемые на таблицы, позволяют легче использовать, 604 средства хранения стратегия архивирования, 563 ссылочная целостность внешние ключи, 634 между БД с помощью триггеров, 386 транзакции OLTP, 12 стадия разработки требования к аппаратным средствам, 621 стандартные отчеты, 208 стандартный международный номер книги см. ISBN, старый код см. наследуемые системы; исходные системы, статистика ввода/вывода, 597 использование для тестирования, 501 степень отношения, 42 столбец типа bit оператор GROUP BY, использующий тип данных tinyint, 274 столбцы атрибуты, 42 заголовки и тела, 52 индексирование всех в БД только для чтения, 607 которые дорого обновлять, 510 максимально допустимое число, 269 нормализация уменьшает число, 152 обладают порядком, 42 ограничение видимости с помощью представлений, 477 определение в SQL Server, 42 определение, используя CREATE TABLE, 270 отличие от атрибутов, 74,112 физические и вычисляемые, 270
столбцы виртуальные см. столбцы вычисляемые, столбцы вычисляемые, 270, 298 построение БД только для чтения, 603 столбцы идентификации CREATE TABLE, оператор, 276 столбцы идентификации, начальное значение, 276 столбцы идентификации, приращение, 276 страниц передача число зависит от индексов, 151 страниц разбиение использование кластерных индексов, 310 страница, размер программный доступ, 551 строк число @@rowcount переменная после выражений SET, 384 использование термина "мощность", если число неоднозначно, 50 строки доступ к конкретным строкам с помощью курсора, 464 максимальное число байт на строку, 269 ограничение видимости с помощью представлений, 476 определение в SQL Server, 41 строки, идентификаторы избежание двойных строк, 47 искусственные ключи, 49 строки текста см. запись в строке слов прописными буквами; символьные данные, структура корпоративных данных четыре функциональных модуля, 10 структура организации пример рекурсивного отношения, 126 структура системы требования, предъявляемые к БД, 212 часто сложная, с различными скоростями соединения, 242 СУБД (система управления базой данных), 5 СУРБД (система управления реляционной базой данных), 52 сущности, 41 атрибуты как идентификаторы, 72 все атрибуты должны быть описаны в 2НФ и ЗНФ, 169 добавление описательной информации, 136 должны быть отношениями и посвящены одной теме, 204 идеи как абстрактные сущности, 69 концептуальные понятия таблиц, 40 обеспечение целостности сущности, 634 обозначение, 111 определение данных о заказчиках, 67 получение из документации заказчика, 67 потомки и предки, определение,118 представление в стандарте IDEF1X, 110, 112 примеры таблиц с атрибутами и доменами, 75, 93 пример таблицы для поставщика офисного оборудования, 70, 79, 9 расширение для БД, формирующих отчеты, 601 связь с отношениями, 66, 118 средство построения диаграмм Enterprise Manager, 134-135 строки в 1НФ должны иметь одно и то же число значений, 155 таблицы БД должны представлять отдельные темы, 152 удаление несущественных перед реализацией, 340 учебный пример, 87, 140
6 5 7
сущности доменов сущности доменов см. таблицы доменов сущности зависимые, 109 внешние ключи представлены в первичном ключе, 109 сущности идентификаторо-зависимые, 109 сущности независимые, 109 внешние ключи отсутствуют для первичных ключей, 109 сущности общие отношения классификации, 127 сущности разрешающие, 129 сущности специфические отношения классификации, 127 сущность person (человек) пример НФБК, 169 схема "звезда", 14, 546, 548 схема "снежинка" 14 сценарии иллюстрация возможных физических реализаций, 233
таблицы архивирование с разделением по датам, 561 доступ внутри ОПФ, 364 запросы могут иметь доступ к ним, 260, 456 концептуальная версия сущностей, 41 математически описываются как отношения, 39 объединение с различными типами операторов, 54 оператор CREATE TABLE, 266 обработка исключений, 413 отношения между таблицами, типы, 78 постоянные данные должны быть размещены внутри таблиц, 629 разделение структуры с помощью представлений, 477 реляционная теория БД, 39 рост OLAP-таблиц, 548 рост OLTP-таблиц, 547, 548 рост таблиц в учебном примере, 588 соглашения об обозначениях, 267 создание таблиц БД, 259 терминология, иллюстрируемая примером, 40 факторы, учитываемые в расчете объема, 549 таблицы доменов, 121 изменения для поддержания таблиц фактов, 561 проблемы, вызываемые взаимодействием с внешними системами, 214 учебный пример, 339, 534 характеризуются медленным ростом или его отсутствием, 547 таблицы измерений и таблицы фактов, 14 таблицы исключений, 413 таблицы "тонкие", 152 таблицы фактов архивирование характеристик строк, 562 и таблицы измерений,14 период архивирования, 561 таблицы-потомки задание внешним ключам значений NULL, 407 проблемы удаления записей-предков, 243 тег, формат расширенные сообщения об ошибках, 488, 489 телефонов номера обновление кодов региона, элементарность и телефонные номера, 155 примеры нарушения 1НФ, 153 проблемы, связанные с различными форматами, 78
6 5 8
учебный пример, 176,424 терминология см. также обозначения, соглашения. SQL-терминология предпочтительнее реляционной, 63 таблицы БД и реляционная теория, 39 таблица соответствия SQL-терминов и терминов реляционной теории, 64 управление работой, 566 тестирование см. также настройка функционирования; проверка характеристик. защита данных тестированием триггерами, 392 работа хранимых процедур, 501-502 типы тестирования, 620,621 техника интервью, 26 тип данных bigint, 273 тип данных bit, 273 тип данных boolean использование типа smallint мало подходит, 274 тип bit как лучшая аппроксимация в SQL Server, 273 тип данных datetime, 279 случаи, когда определяемые пользователем типы данных предпочтительны, 280 удаление времени с помощью ОПФ, 428, 476 удаление времени с помощью триггера INSTEAD OF, 431 удаление часов, используя системную функцию datePart, 476 фактически нарушает 1НФ, 158 тип данных decimal, 275 тип данных money, 276 тип данных real, 278 тип данных rowversion см. timestamp, столбцы, тип данных smalldatetime, 279 тип данных smallint, 273 тип данных smallmoney, 276 тип данных sql_variant, 285 определение типа данных с помощью хранимой процедуры,287 размещение расширенной описательной информации, 330 тип данных string общий тип данных в проверках-ограничениях, 376 тип данных sysname, 335 тип данных table не допускается как тип данных столбца, 292 тип данных text, 284 тип данных tinyint, 274 тип данных uniqueidentifier, 317 задание автогенерации с помощью rowguidcol, 290 тип данных varbinary, 282 тип данных varchar, 284 типы величин нельзя представить типом float, 278 типы данных выбор, соответствующий логическим доменам, 272 заполнитель <общий_тип_данных>, 372 курсор, объявленный как переменная,460 наследование в ЮЕР1Х,использование доменов, 115 обеспечение безопасности данных, 358 общие классы важны для логического проектирования, 44 определяемые пользователем, 45 преобразование логических доменов, 346 примеры доменов в SQL Server, 44 таблица "примитивных" типов данных, 44 тип cursor, передаваемый как выходной параметр, 484
унифицированный указатель информационного ресурса типы данных nchar, nvarchar и ntext, 285 типы данных встроенные, 272 типы данных двоичные, 282 типы данных общие, 44 строки в проверках-ограничениях, 376 типы данных, определяемые пользователем, 45, 292 нельзя изменять, когда используются, 296 работа с датами и временем, 280 работа с переменными противоречива, 295 типы курсоров, 461 важность точного задания, 468 влияние на сортировку, пример, 467 понижение типа, 462 тиражирование, 245 альтернатива доступу к данным между БД, 531 может потребоваться для диалогового доступа, 240 распределенные секционированные представления, 480 реализация БД только для чтения, 610, 611 тиражирование копии,245 тиражирование с помощью транзакции, 245 подходит для удаленных БД, 613 тиражирование слиянием,245 распределенные системы, 256 "толстый" и "тонкий" клиенты, 250-251 топологии см. топологии сетей, топологии сетей, 239 см. также n-уровневая конфигурация,- системы клиент-сервер. клиент и конфигурация данных, 253 предназначены для минимизации проблем функционирования, 239 скорость соединения для доступа клиент-сервер, 240 трехуровневые и n-уровневые системы, 254 точки сохранения, 442 задание с помощью хранимой процедуры, 497 точность задание требований для числовых типов данных, 275 тип данных decimal, 275 транзакции, 438 блокировка как механизм управления, 443 использование завершения с двумя стадиями, 532 лучше иметь короткие и в одном пакете, 449,452 лучше делать короткие, чтобы минимизировать тупиковые ситуации, 451 модифицирующие операторы должны быть внутри, 384 определение, 12 определение длительной работы, 584 откат воздействует на все триггеры, 397 откат после кода тестирования, 392 ошибки хранимых процедур, связанные с числом транзакций, 495 пакеты с несколькими операторами, 514 последовательности завершения пакетов в случае ошибок, 516 частичное завершение, 500 транзакции длинные минимизация воздействия, 452 требования поиска проблемы формирования отчетов из-за сложного поиска, 238 трехзначная логика, 468 трехуровневые конфигурации, 254 трехфазная модель разработка БД, проверка характеристик и реализация, 620
триггеры, 386 см. также SQL-код компилируемый. доступ к данным между БД, 531 запрещение триггеров нарушает двенадцатое правило Кодда, 635 значения, обеспечивающие целостность данных, 418 индексируемые представления как альтернатива, 479 использование в оптимистической блокировке, 301 использование обработки ошибки, 397 исходные данные должны удовлетворять ограничениям, 403 корректировка каскадирования, которая требует триггера, 320 многострочная проверка, 392 могут работать лучше, чем проверки-ограничения, 383 необходимо проверять, вложив в транзакцию, 392 несколько триггеров для одной операции, 394 несколько триггеров следует использовать осторожно, 402 обеспечение целостности данных, 358 определение последующих требований в процессе проектирования, 540 основной формат, 403 осторожно включать операторы, возвращающие наборы результатов, 386 отношения между БД, 324 порядок выполнения, 402 проверка вложенных и рекурсивных триггеров, 395 проверка соответствия ZIP-кодов и городов, 432 реализация с доменами столбцов, 46 реализованные отношения, 135 сравнение с ограничениями, 371,399 удвоение времени из величин типа datetime, 431 учет межтабличных зависимостей, 53 учет межстрочных зависимостей, 53 цикл хранимых процедур как альтернатива, 464 триггеры вложенные, 394 триггеры, порядок выполнения, 402 тупиковые ситуации, 450
удаление в таблице-предке установление внешних ключей потомка в NULL, 407 узкие места возможные причины, 567 диск и ЦП, 575 работа диска, 582 узлы-листья, 309 кластерные и некластерные индексы, 553 указания оптимизатору выбор типов блокировки, 444 избежание блокировок, используя представления, 476 использование конкретных уровней изоляции, 449 перечень, 449 указатели на расположение адреса как примеры атрибутов, 74 указатели, страницы и строки, 553 уникальные идентификаторы unique identifiers см. также GUID. timestamps как уникальные величины, 288 в качестве ключей в НФБК, 1628 уникальные ограничения, 371 использование со вторичными ключами, 313,317 унифицированный язык моделирования см. UML. унифицированный указатель информационного ресурса, 75 см. URL. 6 5 9
управление работой (памяти) управление работой (памяти), 565 как искусство, 566 уровень изоляции по умолчанию "фиксированное чтение", 445,448 уровень изоляции ^Повторяемое чтение#, 445,447 уровень изоляции "Свободное чтение", 445 уровень изоляции "Упорядочиваемый", 446,448, уровень изоляции #Фиксированное чтение#, 445 уровень корневого узла определение при вычислении размера индекса, 557 уровни изоляции продолжительность блокировки, 445,448 задание указаний оптимизатору, 449 уровни обслуживания планирование, 623 соглашения об уровне обслуживания, 30 уровни серьезности, сообщения об ошибках, 383 условные добавления использование триггеров INSTEAD OF, 413 устойчивость к ошибкам обеспечиваемая комбинациями RAID, 578 устройства блокировки записей, 303 учебный пример (проверка регистра) внешние системы, 224 использование бизнес-правил, 423 использование данных и формирование отчетов, 220 моделирование в Use Case, 138 моделирование данных, 140 модель данных после НФБК, 179 модель данных после полной нормализации, 203 нарушения 1НФ, 172 нарушения 4НФ, 197 необязательные бизнес-правила, 435 НФБК, 174 ограничения по умолчанию, 423 определение,16 определение сущностей, 85 объемный анализ, 225, 588 подтип сделки, 336 пометка атрибутов документа, 89 построение структур доступа к БД, 533 построение физической реализации, 335 проблемы безопасности, 542 проверка диапазонов, 429 проверки-ограничения и ОПФ, 424 простая топология клиент-сервер, 256 процедура заполнения доменов, 539 процедура формирования списка, 538 служебные столбцы, 342 создание БД для формирования отчетов, 624 список объектов, 93 стадия сбора информации, 31 таблица доменов, 145 таблица сущностей, атрибутов и доменов, 93 типы данных, 346 физическая модель, 353 формирование отчетов, 222 хранимая процедура обновления, 535 хранимая процедура размещения, 534 хранимая процедура удаления, 537
Ф файл определения пакета, 355 см. PDF фактор заполнения, 552
6 6 0
факты корректировка записей с переменным числом фактов, 157 физическая модель учебный пример, 353 физическая независимость данных восьмое правило Кодда для СУРБД, 632 физическая реализация иллюстрирующие сценарии, 233 физическое чтение параметры работы, 502 формат поля формирование проверок с ОПФ, 378 формат семантического моделирования English Query, 615 формирование сценария использование SQL Profiler, 332 формы разрозненные источники данных, 31 функции использование в ограничениях по умолчанию, 373 функции встроенные ОПФ не могут некоторые из них включать, 370 функции недетерминированные, 370 функции с несколькими операторами, формирующие таблицу, 360 функции с одним оператором, формирующие таблицу, 365-366 функциональные зависимости, 63 2НФ, 160 более высокие нормализованные формы, 158,161 неключевые атрибуты в ЗНФ, 164 функция update, триггеры, 389
"характеристики ползут" роль документации, чтобы избежать, 24 хранилище данных 13 альтернатива БД только для чтения, 599 диаграмма типичной структуры, 16 как средство поддержания решения, 13 оптимизированное размещение для организации отчетов, 9,13 противоположность к OLTP, 9 хранимые процедуры, специальные вычисление полного размера таблиц, 550 вычисление размера индекса, 554 хранилище данных в противоположность OLTP, 9 как средство поддержки решений, 15 поддержка БД только для чтения является альтернативой, 599 размещение, оптимизированное для формирования отчетов, 9,13 схема типичной структуры, 16 хранимая процедура тиражирования реализация БД только для чтения, 610 хранимая процедура удаления, 511 ограничения обозначений, 321 хранимые процедуры обновления столбцы, дорогие для обновления, 510 хранимые процедуры системные см. также sp_; процедуры хранимые. информационная схема является альтернативой, 326 условия использования курсоров для выполнения, 464
ЯОД (язык определения данных)
ц целевая свободная память, 570-571 целостность сущности 634 целые числа тип данных int, 273 увеличение скалярной ОПФ, 361 ЦП (центральный процессор), 236
чеки (банковская система) образец как источник информации об атрибутах, 89 числовые данные приближенные, 277 чтение с дисков можно определить допустимую работу, 559 "что-то в промежутке" идеальная система клиент-сервер, 252 чувствительность к регистру см. также запись в строке слов прописными буквами. изменение сопоставления для задания, 305
ш шаблоны использование доменов для столбцов, 272 шестнадцатеричные целые представление битовой маски, 391
электронные таблицы обычно используются как хранилища данных, 601 проблемы целостности данных, 28 элементарность атрибуты, требование 1НФ, 153 доступ к внешним таблицам, 418 корректировка телефонных кодов регионов, 156 необязательные каскадирование удалений, 420 свойства транзакции, 438 транзакции OLTP, 12,438 учебный пример,172
язык данных пятое правило Кодда для СУРБД, 631 язык сценариев на основе Visual Basic см. VBS. языки, отличные от английского использовать сопоставление,чтобы правильно поставить в алфавитном порядке, 305 поддержка в операторе RAISERROR, 486 ЯМД (язык манипулирования данными), 58 команды, вытекающие из основных операторов отношения, 59 ЯОД (язык определения данных), 59 нужен реляционный язык для построения DML-объектов, 59
6 6 1
Справочное издание Дэвидсон Луис Проектирование баз данных на SQL Server 2000 Редактор Б. Копылов Художественный редактор О. Лапко Компьютерная верстка Л. Катуркиной Лицензия на издательскую деятельность № 06331 от 21 ноября 2001 г. Подписано в печать 30.04.03. Формат 74x100 /jg. Гарнитура Школьная. Бумага офсетная. Печать офсетная. Усл. печ. л. 58. Тираж 3000 экз. Заказ 1868 Издательство «БИНОМ. Лаборатория знаний». Адрес для переписки: 119071, Москва а/я 32. Телефон (095)955-0398. E-mail: [email protected] Отпечатано с готовых диапозитивов в полиграфической фирме «Полиграфист» 160001, г. Вологда, ул. Челюскинцев, 3.