/
Автор: Ульман Дж.
Теги: языки программирования трансляторы программирование базы данных язык программирования паскаль
ISBN: 5-217-00628-5
Год: 1990
Текст
JULIAN ULLMANN fi- dxL ДЖ. УЛЬМАН ДАНН
ДЖ. УЛЬМ АН БАЗЫ ДАННЫХ НА ПАСКАЛЕ
JULIAN ULLMANN University of Sheffield A Pascal database book CLARENDON PRESS • OXFORD 1985
ДЖ.УЛЬМАН БАЗЫ ДАННЫХ ПАСКАЛЕ Перевод с английского канд. техн, наук М.В. Сергиевского, канд. техн, наук А.В. Шалашова Под редакцией д-ра техн, наук Ю.И. Толчеева Москва • Машиностроение • 1990
ББК 32.973.2-01 У51 УДК [[681.3.016 : 519.682] + 681.3-181.411 03 20 Ульман Дж. У51 Базы данных на Паскале/Пер. с англ. М. В. Сергиев- ского, А. В. Шалашова; Под ред. Ю. И. Топчеева. —М.- Машиностроение, 1990.—368.: ил. ISBN 5-217-00628-5 В книге английского автора процесс создания баз данных впервые описывается с позиций инженера-программиста. Многочисленные примеры структур данных и запросов дают возможность читателю быстро овладеть мощными языковыми средствами и могут быть легко обобщены на ситуа- циях, возникающих в технических областях. Основное внимание автор уделяет широко распространенным реляционным базам данных, реали- зуемых на мини- и микроЭВМ, в частности на персональных компьютерах. Для инженеров-разработчиков и пользователей баз данных во всех областях техники. 2404090000—614 пл лл У - 038(01)—90 КБ ,9-20-90 ББК 32.973.2-01 Оригинал книги опубликован на английском языке издательством «Оксфорд Юниверсити Пресс», г. Оксфорд, Англия ПРОИЗВОДСТВЕННОЕ ИЗДАНИЕ ДЖУЛИАН УЛЬМАН БАЗЫ ДАННЫХ НА ПАСКАЛЕ Редактор Д. П. Бут Переплет художника Н. А. Игнатьева Художественный редактор С. Н. Голубев Технический редактор Н. М. Харитонова Корректор Л. Я. Шабашова ИБ № 5836 Сдано в набор 18.04.90. Подписано в печать 11.07.90. Формат 60х90?/хв. Бумага офсетная № 2. Гарнитура литературная. Печать офсетная. Усл. печ. л. 23,0. Усл. кр.-отт. 23,0. Уч.-изд. л. 24,85. Тираж 30 000 экз. Заказ 72. Цена 5 р. Ордена Трудового Красного Знамени издательство «Машиностроение» 107076, Москва, Стромынский пер., 4 Типография № 6 ордена Трудового Красного Знамени издательства «Машиностроение» при Государственном комитете СССР по печати. 193144, Ленинград, ул. Моисеенко, 10. ISBN 5-217-00628-5 (СССР) © Julian Ullmann, 1985 ISBN 0-19-859642-1 (Велико- © Перевод на русский язык и британия) ответы к упражнениям, М. В. Сергиевский, А. В. Ша- лашов, 1990
ОГЛАВЛЕНИЕ Введение 9 Глава 1. ВВЕДЕНИЕ В ПРОБЛЕМЫ ОБРАБОТКИ ДАННЫХ 1.1. Информация как объект обработки........................ 12 1.2. Прием заказов.......................................... 12 1.2.1. Заказ от клиента................................ 12 1.2.2. Файлы и их компоненты........................... 13 1.2.3. Файлы, используемые при составлении заказа на товары................................................. 14 1.2.4. Процедура оформления наряда..................... 17 1.3. Доставка товаров и оформление счетов................... 18 1.3.1. Комплектование заказанных товаров............... 18 1.3.2. Счета на оплату товаров......................... 20 1.3.3. Файл архива нарядов на продажу . ............... 22 1.4. Операции с поставщиками................................ 22 1.4.1. Поставщики...................................... 22 1.4.2. Оформление заказов на закупку товаров. ... 23 1.4.3. Прием товаров................................... 24 1.5. Проблемы бухгалтерского учета.......................... 25 1.5.1. Расчетный счет как файл......................... 25 1.5.2. Счета дебиторов................................ 25 1.6. Счета кредиторов....................................... 27 1.6.1. Прием счетов на оплату ......................... 27 1.6.2. Выплаты поставщикам............................. 28 L7. Главная бухгалтерская книга.......................... 28 1.7.1. План бухгалтерских счетов................... 28 1.7.2. Контрольный поиск........................... 31 L8. Файл служащих и платежная ведомость.................. 31 1.8.1. Файл служащих .............................. 31 1.8.2. Обработка платежной ведомости............... 33 1.8.3. Файл архива служащих ........................ 34 1.8.4. Файл штатного расписания..................... 35 1.9. Упражнения.......................................... 35 Глава 2. РЕЛЯЦИОННАЯ АЛГЕБРА ................................. 37 2.1. Введение ............................................. 37 2.2. Музыкальные файлы..................................... 37 2.3. Операции и операнды .................................. 41 2.4. Проектирование...........,............................ 41 2.5. Выбор................................................. 42 2.6. Простейшие запросы.................................... 43 2.7. Соединение ........................................... 44 5
2.8. 2.9. 2.10. 2.11* 2.12. 2.13. 2.14. Гл Гл 2.7.1. Соединение по одному полю................... 2.7.2. Использование соединения при ответах на запросы 2.7.3. Соединение по нескольким полям.............. Упражнения.......................................... Объединение......................................... Пересечение......................................... Вычитание........................................... Деление ............................................ Умножение........................................... Упражнения....................................... . . 2.15. Оптимизация запросов..................... 2.16. Языки запросов и вычисления ...................... 2,17. Дополнительные возможности языков запросов........ в а 3. ПРИНЦИПЫ НОРМАЛИЗАЦИИ ........................... 3.1. Распределение полей по файлам ..................... 3.2. Полная декомпозиция ............................ . 3*3* Дублирование информации............................ 3.3.1. Примеры дублирования........................ 3.3.2. Ключи-кандидаты . .......................... 3.4. Присоединенные записи.............................. 3.5. Нормализация....................................... 3.5.1. Пятая нормальная форма............... . . . 3.5.2. Упражнения.............................. . . 3.6. Функциональная зависимость.................. . . . 3.6.1. Определение функциональной зависимости...... 3.6.2. Теорема Хита................................ 3.7* Нормализация на основе функциональной зависимости. . . 3.7.1. Первая нормальная форма . .................. 3.7.2. Вторая нормальная форма ................ . 3.7.3. Третья нормальная форма ............. . . . . 3.7.4. Нормальная форма Бойса—Кодда ............... Упражнения ........................................ Четвертая нормальная форма......................... 3.8. 3.9. 3.10. Объекты и атрибуты ......................... 3.10.1. Функциональная зависимость атрибутов от объектов 3.10.2. Построение набора файлов по эмпирическим данным 3.10.3. Преобразование набора файлов............. 3.11* Упражнения..................................... в а 4. ФАЙЛЫ, ЗАПИСИ И УКАЗАТЕЛИ ЯЗЫКА ПАСКАЛЬ 4.1. Введение ......................................... 4.2. Текстовые файлы................................... 4.3. Объявления типов ................................. 4.4. Комбинированные типы и переменные................. 4.4.1. Объявления комбинированных типов*......... 4.4.2. Инструкция WITH........................... 4.4.3. Записи с вариантами....................... 4.5. Двоичные файлы.................................... Файловые буфера.................................... Упражнения......................................... Указатели.......................................... 4.8.1. Ссылочные типы............................ 4.8.2. Связанные списки *........................ Упражнения......................................... в а 5. ПРОГРАММИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ ПЕРЕ- МЕННЫХ, СОХРАНЯЮЩИХ СВОИ ЗНАЧЕНИЯ . . . 5.1. Высокий и низкий уровни программирования.......... 5.2. Описание баз данных на Паскале.................... 4.6. 4.7. 4.8. 44 45 47 48 49 50 51 51 52 54 55 56 57 57 57 57 58 58 60 61 64 64 65 67 67 70 71 71 72 73 74 75 78 78 81 84 86 92 92 93 97 103 103 106 W8 ill 113 118 119 119 122 134 138 139 Гл 4.9. 6
5.3. Программы, обращающиеся и базе данных .......... U0. 5.4. Указатели, сохраняющие свои значения............ 144 5.5. О дополнительных возможностях Паскаля по работе с базами данных............................................... 153 5.6. Упражнения...................................... 153 Глава 6. ВВЕДЕНИЕ В ОРГАНИЗАЦИЮ ФАЙЛОВ.............. 157 6.1. Организация файлов и доступ к ним...................... 6.2. Сортировка ............................................ 6.3. Простые последовательные файлы......................... 6.3.1. Доступ к записям в простых последовательный файлах 6.3.2. Групповая обработка............................ 6.3.3. Буфер файлового блока.......................... 6.3.4. Блоки, связанные в цепь........................ 6.4. Коэффициент активности файла и эффективности доступа . . . 6.5. Определение адреса .................................... 6.5.1. Прямая адресация............................... 6.5.2. Методы хэширования............................. 6.5.3. Непригодность файлов с хэш-адресацией к групповой обработке............................................ 6.5.4. Упражнения..................................... 6.6. Индексно-последовательные файлы........................ 6.6. L Введение в индексно-последовательную организацию 6.6.2. Включения и удаления (переполнение отсутствует). 6.6.3. Переполнение .................................. 6.6.4. Иерархические индексы для индексно-последователь- ных файлов ........................................... 6.6.5. Упражнения..................................... 6.7. Сопровождение файлов.................................. 6.8. В-деревья............................................. 6.8.1. Основная терминология.......................... 6.8.2. Поиски в В-деревьях............................ 6.8.3. Основные свойства В-деревьев................... 6.8.4. Высота В-дерева................................ 6.8.5. Включение записи в В-дерево.................... 6.8.6. Удаление записи из В-дерева.................... 6.8.7. Объявление В-деревьев.......................... 6.8.8. Процедуры, предназначенные для работы с В-де- ревьями............................................... 6<8.9. Упражнения..................................... 157 157 158 158 159 161 164 166 167 167 169 176 176 179 188 189 194 196 197 198 199 201 203 207 Глава 7. РЕАЛИЗАЦИЯ ОПЕРАЦИЙ ВЫБОРА............ 210 7.1. Введение ............................................ 7.1.1. Выбор по первичному ключу..................... 7.1.2. Владельцы и члены индексов.................... 7.2. Категории индексов................................... 7.2.1. Первичные и вторичные индексы................. 7.2.2. Плотные и разреженные индексы................. 7.2.3. Прямые и косвенные индексы.........*.......... 7.3. Реализация доступа к членам.......................... 7.3.1. Хранение членов индекса вне файлов данных. . . . 7.3.2. Использование указателей в записях файла данных 7.4. Реализация доступа к владельцам записей.............. 7.5. Операции выбора по нескольким ключам................. 7.5.1. Условие И..................................... 7.5.2. Условие ИЛИ................................... 7.6. Косвенная адресация.................................. 7.6.1. Плотные первичные индексы .................... 7.6.2. Многоступенчатая косвенная адресация.......... 7.7. Упражнения........................................... 210 210 210 211 211 213 213 214 214 219 227 231 231 233 234 234 236 238 7
Глава 8. РЕАЛИЗАЦИЯ СОЕДИНЕНИЙ.......................... 8.L Иерархические межфайловые индексы............. 8.2. Двунаправленные межфайловые индексы........... 8.2.1. Наборы................................. 8.2.2. Реализация операций соединения в случае отношения «многие ко многим»............................. 8.3. Упражнения.................................... 8.4. Рабочая группа по базам данных КОДАСИЛ........ 8.4.1. Введение............................... 8.4.2. Схемы КОДАСИЛ.......................... 8.4.3. ЯМД КОДАСИЛ............................ 8.5. СУБД TOTAL.................................... 8.6. СУБД ADABAS................................... 8.7. Реализация соединений с помощью вторичных индексов. . . 8.8. Упражнения.................................... Глава 9. ОРГАНИЗАЦИОННЫЕ ПРОБЛЕМЫ....................... 242 242 247 247 250 267 268 268 270 275 278 280 287 288 291 9.1. Целостность данных................................... 291 9.1.1. Контроль типов ............................... 291 9.1.2. Контроль изменений............................ 291 9.1.3. Дублирование ключей ............................ 292 9.1.4. Связи, объединяющие различные поля.............. 292 9.1.5. Предварительный контроль данных................. 293 9.2. Восстановление данных.................................. 294 9.2.1. Типы отказов ................................... 294 9.2.2. Отказы транзакций............................... 295 9.2.3. Дубликаты записей............................... 296 9.2.4. Образы скорректированных записей................ 296 9.2.5. Дампы........................................... 297 9.2.6. Развертывание базы данных....................... 298 9.3. Управление параллельным доступом....................... 298 9.3.1. Понятие параллелизма........................... 298 9.3.2. Блокировки...................................... 300 9.3.3. Зависания и тупиковые ситуации................. 301. 9.3.4. Упорядоченность................................. 303 9.3.5. Флажки.......................................... 303 9.4. Управление доступом.................................... 304 9.4.1. Средства защиты баз данных...................... 309 9.4.2. Таблицы доступа................................. 304 9.4.3. Пароли ......................................... 305 9.4.4. Пропускная система.............................. 306 9.4.5. Шифрование...................................... 306 9.5. Модели и подсхемы ..................................... 308 9.6. Независимость данных................................... 310 9.7. Администратор базы данных.............................. 311 9.8. Справочники данных..................................... 312 Ответы к упражнениям.............................................. 314
ВВЕДЕНИЕ Обширный предмет исследований и разработок, объе- диняемых под общим названием «Технология проектирования баз данных», принадлежит к числу важнейших разделов совре- менной информатики. Этим определяется и место данного пред- мета в профессиональной подготовке будущих специалистов по вычислительной технике. Настоящая книга адресована в первую очередь студентам университетов и технических вузов, изучающим Паскаль в каче- стве базового языка программирования. Автор считал одной из своих главных задач продемонстрировать связь технологии про- ектирования баз данных и методологии Паскаля. По его мнению, такой подход расширяет кругозор студентов, стимулирует фор- мирование у них представления о вычислительной технике как единой области знаний. Гораздо менее этому способствует тра- диционное изложение предмета в виде набора изолированных дисциплин. Автор стремился построить книгу таким образом, чтобы изу- чение технологии проектирования баз данных могло вестись на более раннем этапе цикла, посвященного вычислительной тех- нике, чем это принято в настоящее время. Опыт преподавания показывает, что такие понятия, как пятая нормальная форма или структуры данных КОДАСИЛ, вполне доступны хорошо успеваю- щему первокурснику. Между тем, раннее знакомство с техноло- гией проектирования баз данных представляется желательным по многим соображениям. а. Очень важно, чтобы уже в самом начале обучения ос- новам вычислительной техники студент приобщался к ее практи- ческому применению, и базы данных предоставляют для этого са- мый разнообразный материал. б. Технология баз данных включает ряд весьма изящных ма- тематических построений, особенно в теории нормализации. Зна- комство с ним может послужить стимулом для углубленного изу- чения проблем вычислительной техники. 9
в. Внутренние механизмы функционирования современных систем управления базами данных отличаются значительной слож- ностью, и разобраться в них бывает непросто даже старшекурс- нику. Переход к более ранней форме обучения позволяет сосредо- точить внимание студента прежде всего на общих принципах по- строения таких систем, а не на деталях, представляющих интерес с точки зрения профессионала. г. Курс по базам данных, прочитанный на одном из первых семестров, может послужить хорошей основой для практикума по программированию, ориентированного на применение файлов, записей, указателей, методов косвенной адресации. Подобный курс будет также способствовать более осознанному усвоению общих понятий вычислительной техники, таких, например, как иерархия уровней программирования. д. Следует, наконец, упомянуть и о тех трудностях, с ко- торыми сталкивается преподаватель, читающий на одном из по- следних семестров курс по архитектуре баз данных, если техно- логия проектирования баз данных изучается в параллель с этим курсом или даже позже. Важной особенностью книги является то, что в ней сосед- ствуют, дополняя друг друга, материалы по технологии проекти- рования баз данных и элементы обработки информации. Так, например, гл. 9 в основном посвящена проблемам целостности баз данных, однако в ней упоминаются и некоторые методы кон- троля информации. В гл. 6 обсуждение вопросов групповой обра- ботки сочетается с описанием В-деревьев, используемых впо- следствии для организации доступа к данным. Здравый смысл подсказывает, что изучению основ технологии баз данных должно предшествовать хотя бы минимальное зна- комство с практическими приложениями файлов. По этой при- чине гл. 1 начинается с описания типичного предприятия, исполь- зующего в своей деятельности различные виды данных. В этой же главе приводятся наглядные примеры файлов, записей, полей и ключей. Операции с данными первоначально исследуются в книге с позиций реляционной теории. Такая методика дает возможность получить более абстрактные и вместе с тем более простые опи- сания по сравнению с описаниями на уровне анализа путей до- ступа к данным. Знакомство с элементами указанной теории зна- чительно облегчает студенту последующее усвоение таких поня- тий, как пути доступа, уяснение их роли в реализации действий на более высоком уровне реляционных соотношений. Между тем по-прежнему бытует традиционная и, по мнению автора, совер- шенно ошибочная практика изучения методов организации фай- лов до реляционной теории. Подобный подход является лишь отражением того, как исторически складывается развитие этой области знаний, и не может рассматриваться как результат поиска наиболее эффективных методов преподавания данного предмета. 10
Для иллюстрации излагаемых принципов в книге было введено всего два языка — один язык запросов реляционной алгебры и один язык более низкого уровня, являющийся, по существу, дополнением Паскаля. Из всего многообразия реляционных язы- ков запросов был выбран язык реляционной алгебры, поскольку его операции весьма удобны при выводе нормальных форм, опи- сываемых в гл. 3. Кроме того, эти операции широко используются в гл. 7 и 8. Реляционная алгебра представлена как самостоятель- ный язык, не погруженный в среду языка более высокого уровня. На примере языка нижнего уровня предполагалось продемон- стрировать студентам соотношение между средствами программи- рования путей доступа к базе данных и стандартными средствами Паскаля. С этой целью в гл. 5 и 6 Паскаль был дополнен неболь- шим набором процедур, в том числе INSERT, FIND и REMOVE. Подобное расширение в какой-то мере приближает Паскаль к фирменным языкам манипулирования данными. Разумеется, это расширение является минимальным и не идет ни в какое сравнение с другими дополнительными средствами, за счет кото- рых Паскаль приобретает некоторые возможности, свойственные лишь реляционным языкам запросов. Опыт показывает, что целесообразно размещать материал в том порядке, в котором он лучше усваивается студентом, даже если при этом приходится нарушать традиционную логику, идя от частного к общему. Именно поэтому книга начинается с почти элементарных практических примеров, а завершается в гл. 9 анализом достаточно абстрактных для начинающего проблем ор- ганизационного характера. Вряд ли гл. 9 пришлась бы по вкусу читателю, будь она в самом начале книги, хотя во многих учеб- ных пособиях с этого примерно и начинается изложение предмета. В завершение автору хотелось бы поблагодарить Спобана Норта, Хью Лэфферти, Джима Мак-Грегора и Лоренса Эткинсона за их конструктивные замечания по содержанию некоторых раз- делов рукописи. Дополнительные средства Паскаля, описанные в книге, были полностью реализованы Нортом на ЭВМ PRIMOS 750, оснащенной системой PRIMOS (см. A Pascal database mana- gement system, Journal of Pascal, Ada and Modula 2, Vol. 3, No. 6, 1984, pp 15—22). Шеффилд। декабрь 1984 Дою. УЛЬМАН
ГЛАВА 1 ВВЕДЕНИЕ В ПРОБЛЕМЫ ОБРАБОТКИ ДАННЫХ 1.1. ИНФОРМАЦИЯ КАК ОБЪЕКТ ОБРАБОТКИ В промышленности и торговле информация — это за- частую столь же необходимый элемент развития, как и оборотный капитал. Эффективное владение информацией повышает конку- рентоспособность, но само в значительной мере зависит от сте- пени оснащенности компьютерной техникой. Ознакомление чи- тателей с некоторыми подходами к решению задач компьютерной обработки информации и является основной целью этой книги. В ней, как правило, рассматриваются примеры из сферы тор- говли, однако описываемые методы носят общий характер и на- ходят применение в самых разных областях. Оставив на время проблемы компьютерной обработки, сосредо- точимся вначале на особенностях информации. Обсудим, что она собой представляет, откуда берется и, наконец, как, когда и с какой целью используется. Уяснив природу объекта предстоя- щей компьютеризации, проще разобраться в том, как ее можно реально осуществить. В первых вводных примерах рассмотрим (разумеется, весьма упрощенно), как протекает деятельность некой воображаемой, но вполне типичной фирмы, которую и назовем «Типико». Это торговая фирма, приобретающая у своих поставщиков разнооб- разные товары по сравнительно низким ценам, поскольку объемы сделок достаточно велики. Прибыль у «Типико» образуется за счет перепродажи тех же товаров розничным торговцам по более высоким расценкам, хотя и меньшими партиями. Эти розничные торговцы являются клиентами «Типико». В частности, один из таких клиентов — фирма «Смитсон электрик». 1.2. ПРИЕМ ЗАКАЗОВ 1.2,1. ВАКАЗ ОТ КЛИЕНТА Начнем с обсуждения процедуры подачи заказов и вы- писки счетов. «Типико» выпускает каталог, где содержатся шифры всех товаров, которые фирма поставляет своим заказчикам. Без 12
этих шифров было бы трудно обойтись. Действительно, «Типико» может, например, продавать электрические чайники нескольких типов, причем выпускаемые различными предприятиями. Каж- дый из этих типов идентифицируется отдельным справочным но- мером, указанным в фирменном каталоге. Изучив каталог, клиент по почте или по телефону может заказать у «Типико» набор кон- кретных товаров. Вариант оформления подобного заказа иллю- стрируется примером 1.1. Пример 1.1 ЗАКАЗ НА ТОВАРЫ № НАШЕГО ЗАКАЗА: 58311 № НАШЕГО ПОСТАВЩИКА: 214 ОТ кого? СМИТСОН ЭЛЕКТРИК ХАЙ-СТРИТ, 17, ПИМ АРШ, ЭЙВОН Дата: 3.5.91 ИМЯ ПОКУПАТЕЛЯ? № ТЕЛЕФОНА? 0691-322178 М-Р ДЖ. БЛАДУОРТ КОМУ: РАСПОРЯЖЕНИЯ ПО ДОСТАВКЕ? типико Отправить по указанному выше адресу ИНДАСТРИАЛ ИСТЕЙТ ГЕЙТСХЕД ТАЙН ЭНД УЭАР КОЛИЧЕСТВО ШИФР ОПИСАНИЕ ТОВАРА ЦЕНА ЗА ЕДИНИЦ ЕДИНИЦУ 2 6PK9F1 Электрический чайник 10.17 100 F8431P Комплект из трех предо- 0.30 1 J496B5 хранителей на 5 А Электрический паяльник 2.90 150 775Y94 Штепсель на 13 А 0.46 4 8KJ652 Тостер 8.62 1.2.2. ФАЙЛЫ И ИХ КОМПОНЕНТЫ Список товаров в заказе фирмы «Смитсон электрик» представляет пример простейшего файла. В первом приближении всякую упорядоченную совокупность записей можно считать фай- лом. В заказе фирмы «Смитсон Электрик» каждая строка образует отдельную запись. Вообще записью называют упорядоченную совокупность полей, содержащих некоторые значения. Напри- мер, в том же заказе значения полей представлены элементами строки, относящимися к различным колонкам. В частности, третья запись файла состоит из четырех полей, имеющих следую- щие значения: «1», «J496B5», «Электрический паяльник» и «2.90». Обратим внимание на то, что значением может служить не только число «2.90», но и цепочка символов «Электрический паяльник». Заголовок колонки, например ЦЕНА_ЗА_ ЕДИНИЦУ, на- зывают именем поля. В заказе на товары поле ЦЕНА_ЗА_ЕДИ- НИЦУ последовательно заполняется следующими значениями: 10.17 о.зо 2.90 0.46 8.62 13
Значения любого поля заключена в пределах некоторого множества величин, определяющего типа поля. Например, КО- ЛИЧЕСТВО ЕДИНИЦ является полем целого типа, поскольку в него могут заноситься только целые числа, а ЦЕНА“ЗА“ЕДИ- НИЦУ —действительного типа, поскольку его значения не обязательно должны быть целыми. Значениями поля ОПИСА- НИЕ. ТОВАРА служат строки символов, и для краткости гово- рят, что подобные поля являются полями строчного типа. Простоты ради во вводной части всегда будет считаться, что все записи любого из рассматриваемых файлов включают поля о одинаковыми именами и типами. 1.2.3. ФАЙЛЫ, ИСПОЛЬЗУЕМЫЕ ПРИ СОСТАВЛЕНИИ ЗАКАЗА НА ТОВАРЫ Файл клиентов. Имена, адреса и другая информация о заказчиках фирмы «Типико» хранятся в файле, именуемом КЛИЕНТЫ. Один из первых шагов, предпринимаемых фирмой по получении заказа от «Смитсон Электрик», состоит в просмотре файла КЛИЕНТЫ на предмет выяснения, правилен ли указанный почтовый адрес и не превышен ли предельный размер кредита. Ниже приведен перечень имен полей, содержащихся в записях файла КЛИЕНТЫ. Рядом с именами в фигурных скобках по- мещены краткие пояснения, которые, разумеется, не входят в состав записей. Имена полей файла КЛИЕНТЫ НОМЕР. КЛИЕНТА {Уникальный номер, который «Типико» НАЗВАНИЕ присваивает каждому своему клиенту) {Название фирмы-клиента} АДРЕС. ДЛЯ- РАСЧЕТОВ {Адрес, по которому «Типико» от- правляет счета на оплату и другие ПРЕДЕЛЬНЫЙ_РАЗМЕР_КРЕ- ДИТА ТОРГОВАЯ-. ЗОНА документы) {Вся страна разделена у «Типико» на семь зон; в этом поле указывается, в которой из зон располагается фирма НОМЕР. ПРОДАВЦА заказчика) {Номер, присвоенный работнику фир- мы «Типико», отвечающему за оформ- ление сделок с данным клиентом) На каждого клиента в файле заведена одна запись. В частности, запись, относящаяся к фирме «Смитсон Электрик», может содер- жать следующую информацию: 6391 СМИТСОН-ЭЛЕКТРИК ХАЙ-СТРИТ,. 17,_ ПИМАРШ, .ЭЙВОН 5000 ЮЗ 78631 НОМЕР-КЛИЕНТА НАЗВАНИЕ АДРЕС- ДЛЯ- РАСЧЕТОВ ПРЕДЕЛЬНЫЙ. РАЗМЕР. КРЕДИТА ТОРГОВАЯ-ЗОНА НОМЕР. ПРОДАВЦА 14
Можно привести еще один пример записи из файла КЛИЕНТЫ: 6433 НОМЕР-КЛИЕНТА БЛАНДЕЛЗ НАЗВАНИЕ КУИНЗ-РОУД,-302,-БРАЙТОН АДРЕС. ДЛЯ-РАСЧЕТОВ 10000 ' ПРЕДЕЛЬНЫЙ- РАЗМЕР. КРЕДИТА ЮВ ТОРГОВАЯ-ЗОНА 78631 НОМЕР. ПРОДАВЦА Назовем ключом-кандидатом такой минимальный набор полей, по значениям которых можно однозначно найти в файле требуе- мую запись. Минимальным этот набор (в него может входить одно или более полей) является в том смысле, что при изъятии из него любого поля оставшиеся уже не могут идентифицировать запись. Для файла КЛИЕНТЫ роль ключа-кандидата играет поле НОМЕР "КЛИЕНТА, поскольку в фирме «Типико» каждый заказчик получает отдельный номер. В обсуждаемом примере НОМЕР. КЛИЕНТА со значением 6391 позволяет выделить среди всех запись, относящуюся к «Смитсон Электрик». Может случиться, что у нескольких фирм-клиентов одинако- вые названия. Не исключено, например, что наряду со «Смитсон Электрик» из Пимарша в Брайтоне действует компания, также именуемая «Смитсон Электрик». Из этого следует, что поле НАЗ- ВАНИЕ нельзя использовать в качестве ключа-кандидата, по- скольку оно однозначно не определяет конкретного клиента. В то же время крайне маловероятно, чтобы у «Типико» нашлись заказчики не только с одинаковыми названиями, но и располага- ющиеся по одному адресу. Поэтому пара полей, НАЗВАНИЕ и АДРЕС-ДЛЯ.РАСЧЕТОВ, совместно могут служить ключом- кандидатом. Совокупность значений этих двух полей позволяет не только выявить нужную запись, но и отыскать при необходи- мости по файлу КЛИЕНТЫ значение поля НОМЕР. КЛИЕНТА. Итак, у файла КЛИЕНТЫ имеется два ключа-кандидата. Один из них — поле НОМЕР. КЛИЕНТА, другой — пара НАЗ- ВАНИЕ и АДРЕС-ДЛЯ-РАСЧЕТОВ. Действительно, файл мо- жет обладать более чем одним ключом-кандидатом. На практике для постоянного применения выбирают один из ключей-кандида- тов, который именуется первичным ключом файла. Например, в «Типико» в качестве первичного ключа файла КЛИЕНТЫ ис- пользуется поле НОМЕР. КЛИЕНТА. Файл товарных запасов. В «Типико» ведется файл ТОВАР- НЫЕ. ЗАПАСЫ, содержащий по одной записи на каждый вид товара, который фирма может предложить своим клиентам (или израсходовать на иные нужды). Имена полей файла ТОВАРНЫЕ ЗАПАСЫ ШИФР. ТОВАРА {Уникальный справочный номер, ис- пользуемый как первичный ключ, иден- тифицирующий данный товар) ШИФР. ХРАНЕНИЯ {Шифр, указывающий на котором из складов «Типико» и в каком именно хранилище располагается товар} 15
ОПИСАНИЕ КОЛИЧЕСТВО. НА. СКЛАДЕ РАСПРЕДЕЛЕННОЕ. КОЛИ- ЧЕСТВО УРОВЕНЬ. ПОВТОРНОГО. ЗАКАЗА ЗАКАЗЫВАЕМОЕ. КОЛИЧЕСТВО ЗАКАЗ АННОЕ. КОЛИЧЕСТВО СТАНДАРТНАЯ- ЦЕНА. ЗА. ЕДИНИЦУ (Краткое словесное описание товара} {Наличное количество товара на те- кущий момент} {Товар, подлежащий отгрузке кли- ентам, но еще не вывезенный со склада} {Фирма вновь заказывает товар у по- ставщиков, когда его количество складе за вычетом уже распределен- ного плюс ранее заказанное, но еще не завезенное на склад оказывается ниже установленного уровня} {В поле указывается, какое коли- чество товара надлежит приобрести у поставщика в случае повторного заказа} {Количество заказанного у постав- щика, но еще не доставленного на ?1Ирму «Типико» товара} Цена, взимаемая фирмой с клиентов} Файлы заголовков и содержания нарядов на продажу. Когда на фирму «Типико» поступает заказ от клиента (например, от «Смитсон Электрик»), прежде всего осуществляется его проверка (как это делается, будет рассказано ниже). Если фирма соглашается принять заказ и, следовательно, доставить перечисленные в нем товары, оформляется наряд на продажу, предназначенный для внутрифирменных расчетов. В нем указывается, что за товары и какому клиенту намеревается по- ставить «Типико», заключив данную сделку. Фирма может сохранить наряд на продажу, оформив его в виде отдельной записи файла нарядов. В таком файле число полей в записях должно, очевидно, варьироваться, поскольку записи могут содержать различные перечни разнообразных то- варов. Проблемы, связанные с переменностью числа полей, пре- одолимы, однако, как правило, за счет существенного усложне- ния работы с файлом, о чем еще будет говориться ниже. На прак- тике для упрощения дела вся информация, включаемая в наряд, заносится обычно в два файла—ЗАГОЛОВКИ.НАРЯДОВ и СОДЕРЖАНИЕ.НАРЯДОВ. В первом из них «Типико» хранит такую информацию, которая не зависит от содержания наряда, например, название фирмы-клиента, подавшей заказ, дата его подачи и т. п. Информация, относящаяся к заказанным товарам, заносится во второй файл. Ниже приведены имена полей, исполь- зуемых в этих файлах. Имена полей файла ЗАГ0Л0ВКИ.НАРЯД0В НОМЕР. НАРЯДА НОМЕР. КЛИЕНТА НОМЕР. ЗАКАЗА ИМЯ. ПОКУПАТЕЛЯ {Значение поля служит первичным ключом, по которому в «Типико» идентифицируется конкретный наряд} {То же, что в файле КЛИЕНТЫ} {Имя служащего фирмы-клиента, от- вечающего за данный заказ} 16
НОМЕР. ТЕЛЕФОНА. ПОКУПА- ТЕЛЯ АДРЕС.ДОСТАВКИ {Адрес, указанный клиентом для до- ДАТА. ЗАКАЗА ставки товаров} {Дата оформления наряда на продажу ОТМЕТКА. ПОДТВЕРЖДЕНИЯ в фирме «Типико»} {В поле проставляется «да», если «Типико» отправила клиенту подтвер- ждение приема поданного заказа, и «нет» — в противном случае} Имева полей файла СОДЕРЖАНИЕ.НАРЯДОВ НОМЕР. НАРЯДА {То же, что в файле ЗАГОЛОВКИ. Н ADCT НОМЕР. СТРОКИ {Для каждого вида товара в файле отводится отдельная запись. Каждая такая запись именуется строкой на- ряда, и ей присваивается номер ее позиции в наряде. Поля НОМЕР. НАРЯДА и НОМЕР.СТРОКИ сов- местно образуют первичный ключ фай- ШИФР. ТОВАРА ла СОДЕРЖАНИЕ. НАРЯДОВ} {То же, что в файле ТОВАРНЫЕ. ОПИСАНИЕ {Краткое словесное описание товара, к которому можно обратиться в случае ЗАКАЗАННОЕ. КОЛИЧЕСТВО ЕЩЕ. НЕ. ДОСТАВЛЕННОЕ. КОЛИЧЕСТВО ЦЕНА. ЗА. ЕДИНИЦУ возникновения ошибок) {Поле используется, если доставка выполняется не за один прием} 1.2.4. ПРОЦЕДУРА ОФОРМЛЕНИЯ НАРЯДА В фирме «Типико» оформление заказа — это процедура, в ходе которой поданный клиентом заказ подвергается проверке, и если он может быть удовлетворен, то составляется соответст- вующий наряд на продажу. В этой процедуре можно выделить три основных этапа. Идентификация клиента. Для этого просматривается файл КЛИЕНТЫ, причем ключом поиска служит либо значение поля НОМЕР. КЛИЕНТА, либо пара значений полей НАЗВАНИЕ и АДРЕС.ДЛЯ. РАСЧЕТОВ. Если данный клиент ранее не обслу- живался, в файле КЛИЕНТЫ формируется новая запись, вклю- чающая предельный размер кредита, назначаемый руководством «Типико». Обратившись к файлу КЛИЕНТЫ, фирма а) выясняет, совпадает ли хранящийся в файле адрес для расчетов с тем, что указан в заказе, б) определяет НОМЕР. КЛИЕНТА, который впоследствии будет помещен в файл ЗАГОЛОВКИ. НАРЯДОВ (остальная информация, входящая в запись этого файла, берется из заказа, присланного клиентом), и в) считывает из записи файла КЛИЕНТЫ предельный размер кредита, установленный для данного клиента. 17
Идентификация товара. По каждому виду товара, включен- ного в заказ клиента, осуществляется просмотр файла ТОВАР- НЫЕ. ЗАПАСЫ. Если имеется некоторое расхождение между описаниями товара в заказе и в соответствующей записи этого файла или цена, указанная клиентом, отличается от хранящейся в файле, фирма «Типико» связывается с клиентом (например, по телефону). Если же все параметры совпадают, то информация заносится в запись файла СОДЕРЖАНИЕ.НАРЯДОВ, а цена товара добавляется к сумме выплат по данному заказу. Проверка состояния кредита. В тех случаях, когда итоговая сумма выплат по заказу плюс непогашенные текущие задолжен- ности клиента фирме «Типико» превышают установленный для него размер кредита, фирма возвращает ему заказ, уведомляя, что последний может быть подан повторно только после оплаты ранее выставленных счетов. Если же клиент уложился в предель- ный размер кредита, «Типико» формирует наряд на продажу, для чего в файл ЗАГОЛОВКИ.НАРЯДОВ заносится запись его за- головка, а в файл СОДЕРЖАНИЕ.НАРЯДОВ — строки, от- носящиеся к заказанным товарам. Описанную процедуру оформления наряда проводит служащий фирмы «Типико», пользуясь терминалом, подключенным к компь- ютеру. Пока не будем обсуждать, какая часть этой работы выпа- дает на долю компьютера и какая достается служащему. 1.3. ДОСТАВКА ТОВАРОВ И ОФОРМЛЕНИЕ СЧЕТОВ 1.3.1. КОМПЛЕКТОВАНИЕ ЗАКАЗАННЫХ ТОВАРОВ «Типико» доставляет товары клиентам только собст- венным автотранспортом, не прибегая к услугам почты или дру- гих транспортных фирм. Каждый день от ворот «Типико» отправ- ляются автофургоны, везущие товары в определенные торговые зоны страны, однако не во все зоны сразу. Никогда фургоны «Типико» не едут в одну и ту же зону два дня подряд. Кроме того, дело организовано так, чтобы за одну ездку фургона товары до- ставлялись по возможности не одному, а нескольким клиентам. Накануне вечером, перед тем, как фургоны отправляются в назначенные им торговые зоны, в «Типико» осуществляется просмотр еще не отработанных нарядов по заказам клиентов, расположенных в каждой из этих зон. Строки в файле нарядов, относящиеся к каждому из таких клиентов, сопоставляются с за- писями файла товарных запасов, после чего младшему управляю- щему фирмы подается докладная, уведомляющая его о том, какие товары из числа заказанных и по каким ценам «Типико» может отправить со своих складов (без учета ранее распределенных). Если соответствующих товаров на складе недостает, фирма вы- сылает клиенту по почте подтверждение приема заказа (если оно 18
не было отослано ранее), однако отправку товаров не оформляет. Одновременно с отсылкой данного сообщения в поле ОТМЕТКА_ ПОДТВЕРЖДЕНИЯ записи файла ЗАГОЛОВКИ.НАРЯДОВ заносится значение «да». Это гарантирует, что подтверждение приема каждого конкретного заказа клиент получает не более одного раза. Если младший управляющий решает, что отправка товаров данному клиенту может состояться, в «Типико» подготавливают три отдельных документа: товарную ведомость, транспортную накладную и счет на оплату. В процессе оформления этих доку- ментов просматривается файл ТОВАРНЫЕ.ЗАПАСЫ, и для каждого товара, подлежащего отправке, в поле РАСПРЕДЕ- ЛЕННОЕ. КОЛИЧЕСТВО добавляется количество отгружаемых единиц. Ниже приведен типичный пример простейшей товарной ведомости. Руководствуясь товарной ведомостью, работники склада от- бирают соответствующие товары, готовя партию к отгрузке. Товары в ведомости перечислены именно в том порядке, в котором их можно обнаружить, последовательно обходя хранилища склада. Подобная упорядоченность позволяет существенно сэкономить время, затрачиваемое складским персоналом на комплектование товаров, особенно если их список достаточно обширен. После того как отправляемая партия скомплектована, для каждого из вхо- дящих в нее товаров в записи файла ТОВАРНЫЕ.ЗАПАСЫ соответствующим образом изменяются значения полей КОЛИ- ЧЕСТВО. НА.СКЛАДЕ и РАСПРЕДЕЛЕННОЕ. КОЛИЧЕСТВО. Если в результате какой-то ошибки на складе не оказалось того количества товара, которое было указано в ведомости, вносятся необходимые изменения в накладную и в счет, а о самой ошибке сообщается в отчете, направляемом руководству фирмы «Типико» Пример 1.2 ТОВАРНАЯ- ВЕДОМОСТЬ. ТИПИКО АДРЕС ПОЛУЧАТЕЛЯ: Смитсон Электрик, Хай-стрит, 17, Пимарш, Эйвон ДАТА ДОСТАВКИ: 7 : 5 : 91 НОМЕР. ХРА- ШИФР. НИЛ ИЩА ТОВАРА 2 F8431P 2 775Y94 5 8КИ43 8 6NF31K 9 6PK9F1 Групповые ведомости ОПИСАНИЕ. ТОВАРА КОЛИЧЕСТВО. ЕДИНИЦ Комплект из трех предохра- 50 нителей на 5 А Штепсель на 13 А 150 Лампа с прозрачной колбой 100 мощностью 100 Вт Электрический вентилятор 5 Электрический чайник 2 одновременно для нескольких клиентов могут составляться заранее, до того, как каждому из них склад- ской персонал подготовит к отгрузке заказанные товары. Для того чтобы одни и те же единицы товара не были направлены раз- ным клиентам, и было введено поле РАСПРЕДЕЛЕННОЕ. КО- ЛИЧЕСТВО в файле ТОВАРНЫЕ.ЗАПАСЫ. Например, на складе в данный момент имеется четыре электрических чайника, 19
и если три из них распределены для фирмы «Бланделз» из Бри- столя, то выделение еще двух фирме «Смитсон Электрик» может произойти лишь вследствие ошибки. Может возникнуть вопрос, почему бы «Типико» не упростить процедуру и не составлять товарную ведомость, накладную и счет на оплату одновременно с оформлением наряда на продажу, исходя непосредственно из заказа, присланного клиентом. Од- нако «Типико» действует по-иному, и объясняется это, в частно- сти, тем, что автофургоны фирмы могут выехать к конкретному клиенту лишь в течение одного или двух дней каждой недели. В результате оказывается выгоднее распределять товары не в по- рядке очередности подачи заказов, а выделять их тем клиентам, отправка которым может состояться ранее других. Кроме того, в промежутке между приемом заказа и днем возможной отправки фирма «Типико» может получить от своих поставщиков некоторые из требуемых товаров. 1.3.2. СЧЕТА НА ОПЛАТУ ТОВАРОВ В «Типико» счет составляется одновременно с товарной ведомостью. Возможная форма счета, согласно которому клиент производит оплату полученных товаров, приведена в примере 1.3. Наряду со счетом фирма оформляет также накладную ве- домость, очень похожую на счет, но в отличие от последнего она снабжается заголовком НАКЛАДНАЯ-ВЕДОМОСТЬ или БЛАНК»ОТГРУЗКИ и адресом доставки, который может не совпадать с адресом для расчетов. Забирая в «Типико» партию товаров, водитель автофургона подписывает копию накладной ведомости. При получении то- варов клиент также подписывает копию накладной, после чего водитель отвозит эту копию обратно на фирму, которая лишь после этого высылает по почте счет на оплату. Предвидя разного рода случайности, например пожар, дотла уничтоживший фирму клиента, или его отказ от привезенных товаров, вызванный иными причинами, или исчезновение автофургона по дороге к клиенту, «Типико» никогда не отсылает счет на оплату, не получив прежде от клиента накладную ведомость с его подписью. Пример 1.3. типико ИНДАСТРИАЛ ИСТЕИТ, ГЕЙТСХЕД, ТАЙН ЭНД УЭАР СЧЕТ ЗАКАЗЧИК № СЧЕТА: 61192 СМИТСОН ЭЛЕКТРИК ХАЙ-СТРИТ, 17 ПИМАРШ ДАТА: 10 : 5 : 91 № РЕГИСТРАЦИИ НАЛОГА: ЭЙВОН ШИФР ОПИСАНИЕ 86872194 КОЛИ- № ВАШЕГО ЦЕНА ТОВАРА ТОВАРА ЧЕСТВО ЗАКАЗА ТОВАРА 6NF31K Электрический ЕДИНИЦ 5 76132 26.25 вентилятор 20
8КИ43 Лампа с прозрач- 100 76131 33.50 6PK9F1 ной колбой мощ- ностью 100 Вт Электрический 2 58311 20.34 F8431P чайник Комплект предо- 50 58311 12.00 775Y94 хранителей на 5 А Штепсель на 13 А 150 58311 69.30 ИТОГ ПО ТОВАРАМ 161.39 ПЛАТА ЗА ДОСТАВ- 20.00 КУ Ю-% НАЛОГ 18.14 ВСЕГО К ОПЛАТЕ 199.53 В процессе оформления счета и накладной служащие «Типико» заносят финансовую информацию в два файла — ЗАГОЛОВКИ- СЧЕТОВ и СОДЕРЖАНИЕ.СЧЕТОВ. Причина использования двух файлов вместо одного та же, что и в случае нарядов на про- дажу: сложно работать с файлом, записи которого по длине могут сильно варьироваться. Ниже приведен перечень имен полей этих двух файлов. Имена полей файла ЗАГОЛОВКИ.СЧЕТОВ НОМЕР. СЧЕТА НОМЕР. КЛИЕНТА ПЛАТА. ЗА. ДОСТАВКУ НАЛОГ ОБЩАЯ-СУММА ДАТА-ОТГРУЗКИ ДАТА- ПРЕДЪЯВЛЕНИЯ- СЧЕТА ОТМЕТКА. ОТСЫЛКИ. СЧЕТА (Первичный ключ} {То же, <то в файле КЛИЕНТЫ} {Общая сумма выплат по счету} {Обычно счет высылается на сле- дующий день после доставки товаров} {В поле проставляется значение «да», если счет выслан клиенту по почте, или «нет» в противном случае} Имена полей файла СОДЕРЖАНИЕ.СЧЕТОВ НОМЕР. СЧЕТА {Совпадает с одноименным полем в файле ЗАГОЛОВКИ.СЧЕТОВ} НОМЕР. СТРОКИ. СЧЕТА {Записям содержания счета, относя- щимся к счету с данным номером, присвоены последовательные номера строк, т. е. 1, 2, 3, ...; благодаря этому поля НОМЕР. СТРОКИ. СЧЕ- ТА и НОМЕР.СЧЕТА совместно обра- зуют первичный ключ рассматривае- мого файла} НОМЕР. НАРЯДА {Отдельные виды товаров, перечис- ленные в счете «Типико», могут содер- • жаться в нарядах с неодинаковыми номерами} ШИФР. ТОВАРА {Для каждого товара или комплекта одинаковых товаров в файле СОДЕР- ЖАНИЕ. СЧЕТОВ предусмотрена од- на запись} ОПИСАНИЕ. ТОВАРА КОЛИЧЕСТВО. ЕДИНИЦ ЦЕНА. ЗА_ ЕДИНИЦУ 21
Как уже говорилось, по возвращении автофургона, доставив- шего товары, клиенту (но лишь при условии, что товары приняты) по почте высылается счет. Одновременно в файле ЗАГОЛОВКИ. СЧЕТОВ заполняется поле ДАТА, ПРЕДЪЯВЛЕНИЯ.СЧЕТА, а в поле ОТМЕТКА.ОТСЫЛКИ-СЧЕТА заносится значение «да». Если же партия товаров не принята, то все записи в файлах ЗАГОЛОВКИ.СЧЕТОВ и СОДЕРЖАНИЕ.СЧЕТОВ, относя- щиеся к этой партии, удаляются, а сами товары возвращаются обратно на склад, что сопровождается соответствующей коррек- тировкой файла ТОВАРНЫЕ “ЗАПАСЫ. В файлах ЗАГОЛОВКИ.СЧЕТОВ и СОДЕРЖАНИЕ.СЧЕТОВ имеется вся информация о доставке товаров, так что «Типико» нет нужды заводить отдельный файл отправок. 1.3.3. ФАЙЛ АРХИВА НАРЯДОВ НА ПРОДАЖУ После того как клиент принял партию товаров и наряд на их продажу был окончательно заполнен, соответствующие за- писи из файлов ЗАГОЛОВКИ.НАРЯДОВ и СОДЕРЖАНИЕ. НАРЯДОВ копируются в файл АРХИВ. НАРЯДОВ, а затем удаляются из первых двух файлов. Файл архива, а также ори- гиналы заказов, котсфые хранятся на протяжении нескольких лет, используются в качестве источника информации при раз- решении конфликтных ситуаций и при ответах на разного рода запросы. Если после доставки партии наряд на продажу заполняется не до конца, в файл АРХИВ. НАРЯДОВ копируются запись из файла ЗАГОЛОВКИ. НАРЯДОВ и строка файла СОДЕРЖАНИЕ. НАРЯДОВ, относящиеся к фактически отгруженным товарам. Записи же по тем товарам, что были отгружены лишь частично, корректируются так, чтобы в них значилось то количество еди- ниц товара, которое предстоит доставить в дальнейшем. Таким образом, записи, хранящиеся в файлах ЗАГОЛОВКИ.НАРЯДОВ и СОДЕРЖАНИЕ.НАРЯДОВ, позволяют служащим «Типико» выяснять, что было заказано и еще не доставлено клиентам. 1.4. ОПЕРАЦИИ С ПОСТАВЩИКАМИ 1.4.1. ПОСТАВЩИКИ Обсудим теперь, как «Типико» приобретает товары у своих поставщиков. В «Типико» ведется файл ПОСТАВЩИКИ, содержащий по одной записи на каждого поставщика. Ниже пере- числены имена полей этого файла. Имена полей файла ПОСТАВЩИКИ НОМЕР. ПОСТАВЩИКА {Первичный ключ) НАЗВАНИЕ. ПОСТ АВЩИКА АДРЕС. ДЛЯ. ПЛАТЕЖЕЙ. ПО- СТАВЩИКУ 22
НОМЕР. ПОСТАВЩИКА ИМЯ-ПРОДАВЦА НОМЕР. ТЕЛЕФОНА {Присваивается «Типикоэ фирмой-по- ставщиком} {Имя служащего фирмы-поставщика, отвечающего за операции с «Типико»} {Номер телефона продавца} Каждый конкретный поставщик может снабжать «Типико» несколькими видами товаров, и в то же время каждый товар может поступать от нескольких поставщиков. В «Типико» заве- ден файл ИСТОЧНИКИ, в котором указывается, от каких по- ставщиков и какие товары получает фирма. В файле использу- ются следующие поля. Имена полей файла ИСТОЧНИКИ ШИФР. ТОВАРА НОМЕР. ПОСТАВЩИКА ШИФР. ПРОДУКЦИИ. ПОСТАВ- ЩИКА ЦЕНА. ПРОДУКЦИИ. ЗА- ЕДИ- НИЦУ КОЛИЧЕСТВО. ЕДИНИЦ. ЗА. ПОСЛЕДНИЕ. ШЕСТЬ. МЕСЯЦЕВ КОЛИЧЕСТВО. ЕДИНИЦ. С. ДЕФЕКТАМИ. ЗА. ПОСЛЕДНИЕ. ШЕСТЬ. МЕСЯЦЕВ {Шифр, присвоенный фирмой «Ти- пико»} {Данный шифр однозначно связан с шифром по каталогу у «Типико»} {Количество, отгруженное данным по- ставщиком} 14.2. ОФОРМЛЕНИЕ ЗАКАЗОВ НА ЗАКУПКУ ТОВАРОВ Для выяснения того, какие товары необходимо приоб- рести у поставщиков, в «Типико» дважды в неделю просматри- вается файл ТОВАРНЫЕ ЗАПАСЫ. Товар заказывается в том случае, когда КОЛИЧЕСТВО, НА, СКЛАДЕ минус РАСПРЕДЕ- ЛЕННОЕ, КОЛИЧЕСТВО плюс ЗАКАЗАННОЕ, КОЛИЧЕСТВО оказывается меньше, чем УРОВЕНЬ, ПОВТОРНОГО, ЗАКАЗА. Служащий «Типико» (именуемый покупателем) находит товар, который пора приобретать, в файле ИСТОЧНИКИ и решает, у которого из поставщиков следует его заказать. Решение это определяется ценой за единицу товара, репутацией поставщика и наличием у «Типико» в данный момент возможности заказать достаточно большую партию, чтобы получить хорошую скидку. Для каждого поставщика, у которого покупатель решил закупить товары, оформляется заказ. Заказ «Типико» имеет примерно та- кой же вид, что и заказ, скажем, фирмы «Смитсон Электрик», только количества товаров в нем фигурируют, как правило, су- щественно большие. В «Типико» ведутся файлы ЗАГОЛОВКИ,ЗАКАЗОВ и СО- ДЕРЖАНИЕ, ЗАКАЗОВ, схожие с файлами ЗАГОЛОВКИ, НАРЯДОВ и СОДЕРЖАНИЕ,НАРЯДОВ. В процессе оформле- ния заказа в эти два файла заносятся соответствующие записи, имена полей которых перечислены ниже. 23
Имена полей файла ЗАГОЛОВКИ. ЗАКАЗОВ НОМЕР_ЗАКАЗА номер_поставщика ИМЯ-ПРОДАВЦА {Первичный ключ} {Номер поставщика, у которого «Ти- пико» размещает свой заказ} {Имя служащего фирмы-поставщика, отвечающего за выполнение данного заказа «Типико»} НОМЕР-ПОКУПАТЕЛЯ {Номер, идентифицирующий служа- щего «Типико», который отвечает за оформление данного заказа} ДАТА-ЗАКАЗА ’ Имена полей файла СОДЕРЖАВ ИЕ-ЗАКАЗО В НОМЕР.ЗАКАЗА НОМЕР.СТРОКИ-ЗАКАЗА ШИФР_ТОВАРА {Шифр товара в «Типико»} ШИФР_ТОВАРА-У_ПОСТАВ- {Шифр по каталогу поставщика} КОЛИЧЕСТВО_ТОВАРА СТАНДАРТНАЯ-ЦЕНА_ЗА- ЕДИНИЦУ СКИДКА _В,% % -НА-ЗАКАЗАН- НОЕ.КО ЛИЧЕСТВО После того как «Типико получила и оплатила все товары, значившиеся в заказе, соответствующие записи из файлов ЗАГО- ЛОВКИ, ЗАКАЗОВ и СОДЕРЖАНИЕ, ЗАКАЗОВ копируются в файл АРХИВ,ЗАКАЗОВ, по функции аналогичный файлу АРХИВ, НАРЯДОВ. 1.4.3. ПРИЕМ ТОВАРОВ Когда на фирму «Типико прибывают от поставщика заказанные товары, служащие проверяют их, выясняя, находя- тся ли они в удовлетворительном состоянии. На каждую партию однотипных товаров, приобретенных «Типико», заводится запись в файле ПОСТУПЛЕНИЕ,ТОВАРОВ, имена полей которого перечислены ниже. Имена полей файла ПОСТУПЛЕНИЕ-ТОВАРОВ НОМЕР,ЗАКАЗА HOMEP,CTPOKHL ЗАКАЗА НОМЕР_СЧЕТА_ НА_ ОПЛАТУ- {Это поле совместно с двумя пре- ЮТ-ПОСТАВЩИКА дыдущими образует первичный ключ. Следует учитывать, что товары с оди- наковыми номерами заказа и строки могут поступать в разные дни} ДАТА-ПОСТУПЛЕНИЯ ПРИНЯТОЕ-КОЛИЧЕСТВО Если поставщик за один прием доставляет партию однотипных товаров, но заказанных порознь, по каждому из этих заказов в файл ПОСТУПЛЕНИЕ, ТОВАРОВ заносится отдельная за- пись. Приняв товары, служащие «Типико» должным образом корректируют содержимое полей КОЛИЧЕСТВО, НА,СКЛАДЕ в соответствующих записях файла ТОВАРНЫЕ,ЗАПАСЫ. 24
1.5. ПРОБЛЕМЫ БУХГАЛТЕРСКОГО УЧЕТА 1.5.1. РАСЧЕТНЫЙ СЧЕТ КАК ФАЙЛ Стремясь к единообразию, будем полагать, что и рас- четный счет представляет собой файл, который может включать следующие поля: ДАТА АГЕНТ {Дата выполнения операции} {Имя дебитора или кредитора либо справочный номер} ШИФР-ОПЕРАЦИИ {Например, НОМЕР-СЧЕТА-НА- ОПЛАТУ) РАЗМЕР-ДЕБЕТА {Величина, на которую увеличивается РАЗМЕР-КРЕДИТА расчетный счет} {Величина, на которую уменьшается расчетный счет} Одно из последних двух полей (т. е. РАЗМЕР_ДЕБЕТА или РАЗМЕР. КРЕДИТА) в обычных ситуациях остается незапол- ненным. Согласно установившейся практике (хотя, возможно, с этими терминами и не все знакомы) РАЗМЕР.ДЕБЕТА — это то же самое, что приход, а РАЗМЕР. КРЕДИТА — то же, что расход. 1.5.2. СЧЕТА ДЕБИТОРОВ Прием платежей от клиентов. Клиенты оплачивают приобретенные у «Типико» товары чеками. «Типико», в свою очередь, собрав пачку чеков, пересылает их в банк (такая пачка может, например, включать все чеки, поступившие с полудня одного дня до полудня следующего). Каждая пачка чеков полу- чает свой порядковый номер, т. е. 1, 2, 3, .... На каждый принятый чек служащий «Типико» заводит от- дельную запись в файле ЖУРНАЛ.КАССОВЫХ.ПОСТУПЛЕ- НИЙ, поля которого имеют следующие имена. Имена полей файла ЖУРНАЛ. КАССОВЫХ.ПОСТУПЛЕНИИ НОМЕР-КЛИЕНТА НОМЕР-ЧЕКА ДАТА ВРЕМЯ НОМЕР-СЧЕТА {Номер, напечатанный на чеке} /Время поступления чека} {Номер счета, согласно которому про- изведена оплата} РАЗМЕР-ПЛАТЕЖА НОМЕР-СЛУЖАЩЕГО или ЛИЦО,. ОСУ ЩЕСТВИВШЕЕ- ВВОД_ ДАННОЙ. ЗАПИСИ {В случае возникновения недоразу- мений в «Типико» известно, с кого можно спросить} Когда пачка чеков собрана и готова к отправке в банк, для записей из файла ЖУРНАЛ.КАССОВЫХ.ПОСТУПЛЕНИЙ, относящихся к этим чекам, подсчитывается сумма содержимого полей РАЗМЕР. ПЛАТЕЖА. Эту сумму именуют промежуточ- 25
ним итогом. Кассир банка, принимая пачку чеков, повторно суммирует размеры платежей, и, если результат не сходится с итогом, полученным в «Типико», банк совместно с фирмой пы- таются обнаружить причину несоответствия. Это классический пример использования промежуточного итога в качестве средства защиты от ошибок, допускаемых служащими при вводе данных в записи файла ЖУРНАЛ.КАССОВЫХ,ПОСТУПЛЕНИЙ. Файл счетов дебиторов. Для каждого клиента в «Типико» заведен лицевой счет, в котором отмечаются его платежи и за- долженности перед фирмой. Все эти счета собраны в одном файле, именуемом СЧЕТА.ДЕБИТОРОВ. Ниже перечислены имена полей этого файла. Имена полей файла СЧЕТА-ДЕБИТОРОВ НОМЕР. КЛИЕНТА ДАТА РАЗМЕР.ДЕБЕТА НОМЕР. ЧЕКА РАЗМЕР. КРЕДИТА НОМЕР. СЧЕТА. НА. ОПЛАТУ ОСТАТОК. НА. СЧЕТЕ {Приход, увеличивающий сумму на лицевом счете) {Номер чека, по которому деньги по- ступают на данный счет. Если запись отражает уменьшение суммы на счете, данное поле и РАЗМЕР.ДЕБЕТА не заполняются) {Расход, уменьшающий сумму на ли- цевом счете) {Если запись отражает увеличение суммы на счете, это поле и РАЗМЕР. КРЕДИТА не заполняются) {Значение поля имеет знак: «плюс», если клиент должен «Типико», и «минус», если «Типико» . задолжала клиенту) Каждой записи файла ЖУРНАЛ. КЛАССОВЫХ.ПОСТУП- ЛЕНИЙ ставится в соответствие запись, которая заносится в файл СЧЕТА-ДЕБИТОРОВ. Подобное копирование информации в файл СЧЕТА-ДЕБИТОРОВ называют проводкой данных, вво- димых в ходе выполнения операции, в файл СЧЕТА-ДЕБИТО- РОВ. Иногда оказывается удобным осуществлять такую проводку спустя некоторое время после формирования записи файла ЖУР- НАЛ. КАССОВ Ы X. ПОСТУПЛЕНИИ. Согласно каждому счету, отправленному по почте клиенту, в файле СЧЕТА-ДЕБИТОРОВ создается запись, в которой ука- зывается, какую сумму по данному счету клиент должен выпла- тить «Типико». Фактическое получение клиентом заказанных то- варов (о чем свидетельствует выставленный ему счет) рассматри- вается как расход, уменьшающий сумму на его лицевом счете. Ежемесячно осуществляется просмотр файла СЧЕТА-ДЕ- БИТОРОВ, из которого извлекается информация, необходимая для составления месячного баланса, направляемого клиентам. 26
В балансе содержатся сведения о всех платежах и задолженностях клиента фирме «ТИПИКО», имевших место на протяжении ме- сяца. Ниже приведен простой вариант подобного баланса. Пример 1.4 ТИПИКО Индастриал истейт Гейтсхед, Тайн энд Уэар КЛИЕНТ МЕСЯЧНЫЙ БАЛАНС СМИТСОН ЭЛЕКТРИК ХАЙ-СТРИТ, 17, ПИМАРШ, ЭЙВОН на месяц, истекший 30 июня 1991 г. ДАТА ОПИСАНИЕ СУММА ПЕРЕНЕСЕННОЕ САЛЬДО 210.31 2.6.91 11.6.91 15.6.91 20.6.91 28.6.91 СЧЕТ № 61924 395.60 СЧЕТ № 64382 184.72 ЧЕК 500.00 СЧЕТ № 65112 221.02 СЧЕТ № 68925 166.53 ДЕБЕТОВОЕ САЛЬДО 1678.18 В колонке СУММА минусом отмечена выплата, произведенная клиентом. Файл СЧЕТА,ДЕБИТОРОВ используется не только для со- ставления баланса. На его основе подготавливается также отчет, из которого можно узнать, какие клиенты задерживают выплаты, какие, напротив, платят раньше срока, кто из них согласно этим данным заслуживает большей скидки, а с кем вообще следовало бы прекратить отношения из-за хронических задолженностей. Эго пример так называемой выборочной отчетности. 1.6. СЧЕТА КРЕДИТОРОВ 1.6.1. ПРИЕМ СЧЕТОВ НА ОПЛАТУ Когда «Типико» получает от одного из своих постав- щиков счет на оплату его продукции, фирма предпринимает сле- дующие действия. а. В файле ЗАГОЛОВКИ.ЗАКАЗОВ отыскивается соответствую- щая запись (или записи). б. По НОМЕРУ.ПОСТАВЩИКА, извлеченному из записи файла ЗАГОЛОВКИ.ЗАКАЗОВ, в файле ПОСТАВЩИКИ находится название и адрес фирмы-поставщика, которые сверяются с наз- ванием и адресом, проставленными на счете. в. Для того чтобы удостовериться, что указанные в счете коли- чества товаров действительно были заказаны, доставлены и приняты «Типико», производится построчная сверка счета с записями в файлах СОДЕРЖАНИЕ. ЗАКАЗОВ и ПОСТУП- ЛЕНИЕ. ТОВАРОВ. 27
f. Выясняется, до какого числа необходимо оплатить счет, чтобы не потерять право на причитающуюся скидку. д. Если «Типико» принимает решение оплатить счет, формируется запись, которая заносится в файл НЕОПЛАЧЕННЫЕ.СЧЕТА, включающий следующие поля. Имена полей файла НЕОПЛАЧЕННЫЕ. СЧЕТА НОМЕР. ПОСТАВЩИКА НОМЕР.СЧЕТА {Номер, присвоенный поставщиком} НОМЕР. ЗАКАЗА {Номер заказа, поданного фирмой «Типико»} ДАТА.СЧЕТА {Дата выставления счета} СРОК-ОПЛАТЫ {Дата, не позднее которой «Типико» СУММА. ПЛАТЕЖА должна оплатить счет} 1.6.2. ВЫПЛАТЫ ПОСТАВЩИКАМ- Записи файла НЕОПЛАЧЕННЫЕ.СЧЕТА размеща- ются в порядке СРОКОВ.ОПЛАТЫ, так что всем счетам, которые должны быть оплачены к определенной дате, соответствует группа последовательных записей. Сразу вслед за ней располагается группа записей, относящихся к счетам, подлежащим оплате на следующий день, и так далее. Старший бухгалтер «Типико», ежедневно просматривая файл НЕОПЛАЧЕННЫЕ.СЧЕТА, выбирает те, для которых подошел срок оплаты. Руководствуясь суммой, которой в данный момент располагает фирма на выплаты по счетам, он решает, какие из них следует оплатить, а какие можно пока отложить. По каждому из оплачиваемых счетов поставщику высылается чек, и одновре- менно данный платеж фиксируется в записи файла ЖУРНАЛ. КАССОВЫ X. ВЫПЛАТ. 1.7. ГЛАВНАЯ БУХГАЛТЕРСКАЯ КНИГА 1.7.1. ПЛАН БУХГАЛТЕРСКИХ СЧЕТОВ Главная бухгалтерская книга представляет собой сово- купность счетов, по которым можно оценить общее финансовое положение фирмы (например, рассчитать ее рентабельность). Главная бухгалтерская книга может включать отдельные счета по каждому из перечисленных ниже разделов. Основной капитал Недвижимое имущество Счет в банке Счета кредиторов {Деньги, вложенные в фирму ее вла- дельцами} (Здания, мебель, автофургоны ит. д.} Обычно фирма имеет не один, а несколько счетов} {В главную бухгалтерскую книгу за- носятся только итоговые цифры} 28
Счета дебиторов {В главную бухгалтерскую книгу за- носятся только итоговые цифры} Арендная плата и налоги Плата за газ, электричество и воду Оклады и заработная плата Объем реализации {Суммы, взыскиваемые «Типико» с клиентов} Товарные запасы {Стоимость наличных товаров} Стоимость проданных товаров {Сумма, выплаченная фирмой «Ти- пико» за эти товары} Этот список может быть и более обширным. Спецификации счетов, входящих в главную бухгалтерскую книгу (а также всех остальных), объединены в документе, который называется планом бухгалтерских счетов. В этом плане содержатся описания всех операций бухгалтерского учета, выполняемых служащими фирмы, а также сведения о структуре каждого счета. Ряд действий такого рода, например прием фирмой «Типико» платежа от ее клиента, оплаты счета за электричество, приобре- тение нового автофургона, закупка в кредит товаров у поставщи- ков и т. п., называют операциями. Счета в главной бухгалтерской книге организованы таким образом, что любая операция привносит некоторые изменения в итоговые, цифры, которые отражаются по меньшей мере в двух разных счетах. В приведенном далее упрощенном примере ре- зультаты каждой операции оказывают не косвенное влияние на цифры, а непосредственно заносятся в два (в одном случае в че- тыре) счета. Если результат операции попадает ровно в два счета, то по отношению к одному из них он играет роль дебета, увеличиваю- щего сальдо этого счета, а по отношению к другому — роль кредита, уменьшающего сальдо. Принципиально необходимо, чтобы сумма дебета и кредита всегда равнялась нулю, т. е. доход должен совпадать с расходом. И в тех случаях, когда операция воздействует на большее количество счетов, сумма всех измене- ний должна оставаться равной нулю. Подобный метод бухгал- терского учета, при котором все записи в счетах, связанные с од- ной операций, дают нулевую сумму, получил название двойной итальянской бухгалтерии. Большим достоинством этого метода является простота выявления бухгалтерских ошибок: достаточно лишь проверить, равна ли нулю сумма сальдо соответствующих счетов. В примере 1.5 приведен перечень простейших операций, при выполнении каждой из которых производятся изменения в двух (лишь однажды в четырех) счетах. Знак «+» в этом примере обо- значает дебет, увеличивающий сумму счета, а «—» — кредит, уменьшающий эту сумму. Внесение записи в счет, связанное с вы- полнением некоторой операции, называют проводкой в счет. В примере 1.5 осуществляются, в частности, три проводки в счет товарных запасов, в результате чего он претерпевает определен- 29
ные изменения, детализированные в примере 1.6. Нетрудно скон- струировать и примерную эволюцию остальных счетов, учитывая, что все они начинались е нуля. Потер 1.5 Операция Сальдо Название счета +1 или-— Владельцы переводят капитал 10 000 Основной капитал на счет фирмы 10000 Счет в банке + «Типико» приобретает автофур- 3 000 Недвижимое имущество + гон 3 000 Счет в банке —. «Типико» уплачивает налоги 1 200 Арендная плата и налоги + 1 200 Счет в банке —— «Типико» вакупает товары в 5 000 Товарные запасы + кредит 5000 Счета кредиторов «Типико» выдает заработную 1500 Оклады и заработная + плату плата 1 500 Счет в банке МИЯ, «Типико» вакупает товары в 2 000 Товарные запасы + кредит 2 000 Счета кредиторов —- «Типико» продает товары, сто- 800 Товарные запасы ящие 800 денежных единиц. 800 Стоимость проданных + за 1100 в кредит товаров Отметим, что в данном случае 1 100 Объем реализации mown записи вносятся сразу в четыре 1 100 Счета дебиторов + счета «Типико» оплачивает приобре- 3 000 Счет в банке тенные товары 3 000 Счета кредиторов + «Типико» получает платежи от 1 000 Счета дебиторов —» клиентов 1000 Счет в банке + Ряд счетов, представленных в главной бухгалтерской книге под такими, например, заголовками, как «Счета кредиторов», «Счета дебиторов» или «Товарные запасы», не отражают деталей каждой операции. Эти детали внесены во вспомогательные книги. Такой вспомогательной книгой является, в частности, описанный ранее файл СЧЕТА.ДЕБИТОРОВ. В главной бухгалтерской книге «Типико» раздел «Счета дебиторов» включает лишь итоговые цифры по файлу СЧЕТА.ДЕБИТОРОВ, информирующие о том, Пример 1.6 Счет товарных запасов ДАТА АГЕНТ ДЕБЕТ б: 9:90 УИЛЬЯМС КЕЙБЛЗ 5000 10: 9:90 УИЗАРД ФИТТИНГЗ 2000 12 : 9 : 90 СМИТСОН ЭЛЕКТРИК КРЕДИТ 800 какие количества товаров поставляются ежемесячно в кредит каждому клиенту, а также объемы фактических помесячных вып- лат клиентов фирме «Типико». Это иллюстрирует иерархический принцип построения бухгалтерских книг: в главной содержатся обобщенные показатели, во вспомогательных — более подробные сведения. Этот принцип распространяется и далее. Так, файл СЧЕТА. ДЕ БИТОРОВ — это вспомогательная бухгалтерская 30
книга, включающая перечень счетов, выставленных клиентам, и соответствующих платежей, однако в нее не заносятся строки, •входящие в состав счетов. Эта еще более подробная информация вынесена в файл СОДЕРЖАНИЕ.СЧЕТОВ. Ввиду достаточной сложности здесь не описываются механизмы проводки в счета главной бухгалтерской книги. Следует отметить, что рассмотренный в примере 1.5 набор операций и связанных с ними проводок представляет собой весьма упрощенную модель и может служить лишь иллюстрацией изложенного принципа. 1.7.2. КОНТРОЛЬНЫЙ ПОИСК Желая предотвратить хищение или утерю товаров и денежных средств, «Типико» содержит контрольную службу, которая способна установить, каково происхождение каждой записи, сделанной в любом счете. Другими словами, контролеры располагают способами, позволяющими выяснить, по какой при- чине появилась та или иная запись. Для этого используются файлы журналов, такие, как упоминавшиеся ЖУРНАЛ.КАССОВЫХ. ПОСТУПЛЕНИЙ, ЖУРНАЛ. КАССОВЫХ.ВЫПЛАТ и ПОСТУПЛЕНИЯ-ТОВАРОВ. Выявление цепочки событий, ве- дущей от конкретного счета на оплату или чека к отметке в глав- ной бухгалтерской книге называют контрольным поиском. Это своего рода реконструкция последовательности обработки данных в ходе бухгалтерской операции. Для того чтобы контрольный поиск гарантировал получение результата по каждой операции, необходима определенная организация счетов и файлов системы. В «Типико» для проведения контрольных операций, а также для подготовки ответов на различные запросы заведены два специ- альных файла — АРХИВ. НАРЯДОВ. НА. ПРОДАЖУ и АР- ХИВ. ЗАКАЗОВ. НА. ПОСТАВКУ. 1.8. ФАЙЛ СЛУЖАЩИХ И ПЛАТЕЖНАЯ ВЕДОМОСТЬ 1.8.1, ФАЙЛ СЛУЖАЩИХ В «Типико» ведется файл, включающий по одной за- писи на каждого служащего фирмы. Поля файла имеют следующие имена. Имена полей файла СЛУЖАЩИЕ НОМЕР. СЛУЖАЩЕГО {Первичный ключ} ФАМИЛИЯ ИМЯ ДОМАШНИЙ. АДРЕС СЛУЖЕБНОЕ. ПОМЕЩЕНИЕ {Комната в одном из зданий, зани- маемых «Типико») {См. разд. 1.8.4} НОМЕР, должности ДАТА. НАЗНАЧЕНИЯ. НА. ДОЛЖНОСТЬ 31
РАБОЧИЙ. ДЕНЬ. В. % ® ДАТА. РОЖДЕНИЯ НАНЯТ. ВРЕМЕННО ДАТА. ИСТЕЧЕНИЯ- СРОКА ДОГОВОРА ИСПЫТАТЕЛЬНЫЙ. СРОК ДАТА. ИСТЕЧЕНИЯ- ИСПЫТА- ТЕЛЬНОГО. СРОКА ДАТА. ПОСЛЕДНЕЙ. АТТЕСТА- ЦИИ СПЕЦИАЛЬНОСТЬ УЧЕНАЯ-СТЕПЕНЬ СЕМЕЙНОЕ. ПОЛОЖЕНИЕ ПОЛ ЗАРАБОТНАЯ- ПЛАТА. ЗА- ДАННЫЙ. МЕСЯЦ ЕЖЕМЕСЯЧНЫЙ. ПЕНСИОН- НЫЙ. ВЗНОС ОБЛАГАЕМАЯ- НАЛОГОМ. ЗА- РАБОТНАЯ- ПЛАТА. ЗА_ ТЕКУ- ЩИЙ. ГОД КАТЕГОРИЯ- НАЛОГООБЛОЖЕ- НИЯ {Полный рабочий день — 100%} {«Да» или «Нет»} {Для постоянных работников поле не заполняется} {«Да» или «Нет»} {Для постоянных работников поле не заполняется} ПОДОХОДНЫЙ. НАЛОГ. ЗА_ ТЕКУЩИЙ. МЕСЯЦ ПОДОХОДНЫЙ. НАЛОГ. ВА_ ТЕКУЩИЙ. ГОД ГОСУДАРСТВЕННОЕ. СТРАХО- ВАНИЕ ГОСУДАРСТВЕННОЕ. СТРАХО- ВАНИЕ. ЗА_ ТЕКУЩИЙ. ГОД ПРИЧИНА. ДРУГИХ. ВЫЧЕТОВ СУММА. ДРУГИХ. ВЫЧЕТОВ ЗАДОЛЖЕ ННОСТЬ. ПО. ЗАРА- БОТНОЙ. ПЛАТЕ ЧИСТАЯ- ЗАРАБОТНАЯ- ПЛАТА. ЗА. ТЕКУЩИЙ. МЕСЯЦ НАЗВАНИЕ. БАНКА. СЛ УЖА- ЩЕГО НОМЕР. БАНКОВСКОГО. СЧЕТА- СЛУЖАЩЕГО {Например, счетовод, бухгалтер, шо- фер} {Например, бакалавр наук по вычис- лительной технике} {Холост, женат, разведен или вдовец} {М или Ж} {Заработная плата до всех вычетов} {Средства, выплачиваемые после увольнения по выслуге лет} {Заработная плата до всех вычетов минус суммарный пенсионный взнос} {Код, определяющий размер подоход- ного налога, который наниматель удер- живает из зарплаты и выплачивает затем непосредственно в управление внутренних бюджетных поступлений} {Сумма, которую наниматель удержи- вает из зарплаты служащего} {Суммарный подоходный налог за все месяцы, прошедшие с начала года} {Ежемесячный страховой взнос, вы- плачиваемый служащим} {Например» профсоюзные взносы, по- становление суда и т. п.} {Задолженность, связанная, например, с выходом приказа о повышении зарплаты задним числом} В «Типико» описанный файл может использоваться для под- готовки отчетов, содержащих списки служащих, которые отве- чают некоторым критериям (заданная совокупность критериев иногда именуется разрезом). Можно, в частности, получить фа- милии всех служащих в возрасте 60 лет и старше. А вот более сложный пример: предположим, фирме понадобился список ее служащих, принадлежащих следующему разрезу: 1) специальность — инженер-электрик; 2) место проживания — г. Ньюкасл; 32
3) годовая зарплата превышает 10 000 фунтов стерлингов. «Типико» может также воспользоваться файлом СЛУЖАЩИЕ, чтобы получить перечень работников, проходящих испытательный срок, до истечения которого осталось не более трех месяцев. Деловые качества этих служащих должны быть подвергнуты стро- гому анализу, прежде чем фирма сочтет возможным предложить им постоянную работу. 1.8.2. ОБРАБОТКА ПЛАТЕЖНОЙ ВЕДОМОСТИ Корректировка файла СЛУЖАЩИЕ. Во многих фирмах часть служащих получает зарплату понедельно, а остальным деньги переводятся с банковского счета фирмы на их банковские счета ежемесячно. Рассмотрим здесь только операции, связанные со второй категорией служащих. Для того чтобы определить, какую сумму должен получить каждый работник, в «Типико» ежемесячно производится обработка файла СЛУЖАЩИЕ. В качестве предварительного шага перед выполнением расчетов по начислению зарплаты, файл СЛУЖА- ЩИЕ подвергается корректировке. В него заносятся все изме- нения, случившиеся за месяц, например, в основной зарплате, категории налогообложения, размере профсоюзных взносов или взносов государственного страхования. Чтобы зарплата соответ- ствовала сроку работы, в файл вводятся также даты приема и увольнения — ведь не обязательно служащий поступает на ра- боту или уходит с нее в первый или в последний день месяца. Наконец, для работников, получивших прибавку, оформленную задним числом, в файле СЛУЖАЩИЕ отмечаются задолженности по зарплате. После завершения корректировки файла СЛУЖАЩИЕ все внесенные изменения вв!водятся на печать, чтобы работники бухгалтерии могли их проверить. Список изменений служит одним из документов, используемых при проведении контроль- ного поиска. Совокупность данных о выплатах, содержащаяся в файле СЛУЖАЩИЕ, образует вспомогательную бухгалтер- скую книгу. Расчет зарплаты. Чистая зарплата, т. е. та сумма, которую служащий получает на руки, определяется по следующей фор- муле: чистая зарплата = основная зарплата + задолженность по зарплате — пен- сионный взнос — подоходный налог — другие вычеты (например, отчисления в профсоюз) Точно так же, как ежемесячное начисление заработной платы предваряет фактический перевод денег служащему, суммарная зарплата и подоходный налог за текущий (финансовый) год рас- 2 Ульман Дж. 33
считываются в «Типике» по текущий месяц включительно. Это дает фирме возможность отчитываться в конце года перед финан- совым ведомством за зарплату своих служащих и удержания из нее. По завершении расчета платежной ведомости старший бух- галтер, чтобы застраховаться от возможных ошибок, еще раз про- веряет ее (обычно уже вручную). Фактические выплаты служащим. После того как все расчеты и проверки чистой заработной платы проведены, в последний день месяца она выплачивается служащему путем перевода соот- ветствующей суммы с банковского счета фирмы «Типико» на лич- ный счет служащего в его банке. Последняя операция осуществля- ется с помощью бесчековой электронной системы, функциониро- вание которой здесь не рассматривается. Одновременно с перево- дом денег каждому служащему высылается отпечатанная платеж- ная ведомость, где по отдельности указаны суммы основной зарплаты и чистой зарплаты, а также всех вычетов (подоходного налога и т. п.). Осуществляя выплаты, фирма отмечает в специальных отче- тах (о них еще не упоминалось) общие суммы, выдаваемые раз- личным категориям ее служащих, объединяющим, в частности, работников отдела продаж, склада, водителей автофургонов, работников бухгалтерии, управляющий персонал. Благодаря этому «Типико» имеет возможность контролировать, на какие статьи расходуются ее средства. Налоговая отчетность. Удерживаемый со служащих подоход- ный налог совместно с отчетом о выплатах и налогах каждого служащего «Типико» передает в налоговое управление. Кроме того, согласно законодательству, фирма обязана предоставить в конце финансового года каждому своему‘служащему официальное уве- домление о размерах заработной платы и подоходного налога, удержанного за этот год. «Типико» должна также отчитаться перед финансовыми ведомствами, занимающимися пенсионным обеспе- чением, и перечислить им пенсионные взносы служащих. Нако- нец, «Типико» высылает отчеты и передает денежные суммы проф- союзам и другим организациям, в пользу которых делаются вы- четы из зарплаты служащих. 1.8.3. ФАЙЛ АРХИВА СЛУЖАЩИХ В тех случаях, когда служащий уходит с фирмы, полу- чает повышение или просто меняет работу внутри «Типико», информация из относящейся к нему записи в файле СЛУЖАЩИЕ копируется во вновь формируемую запись файла АРХИВ_СЛУ- ЖАЩИХ. Если служащий покидает фирму, его запись удаляется из файла СЛУЖАЩИЕ. В остальных случаях запись лишь кор- ректируется: в нее, в частности, заносится номер новой долж- ности. 34
1.8.4. ФАЙЛ ШТАТНОГО РАСПИСАНИЯ Из соображений простоты ранее не упоминалоеь, что на фирме «Типико» имеется ряд отделов: продаж, закупок, склад- ского хранения, финансовый и транспортный. Для каждого отдела установлена структура должностей его служащих. Так, скажем, в структуре отдела продаж предусмотрено определенное число старших и младших продавцов, старших делопроизводителей и т. д. Совокупность этих должностей называется штатным рас- писанием отдела. В «Типико» ведется файл ШТАТНОЕ,РАСПИ- САНИЕ, включающий записи штатных расписаний, утвержденных руководством фирмы для каждого из ее отделов. Не исключено, разумеется, что в течение некоторого периода часть должностей, указанных в файле ШТАТНОЕ.РАСПЙСАНИЕ, остаются ва- кантными, в то время как на других число служащих больше положенного. Первичным ключом для файла ШТАТНОЕ.РАСПИСАНИЕ служит поле НОМЕР.ДОЛЖНОСТИ. Одно и то же значение номера должности может быть присвоено нескольким служащим, и у каждого из них в соответствующей записи файла СЛУЖАЩИЕ поле НОМЕР.ДОЛЖНОСТИ имеет это же значение. В записи файла ШТАТНОЕ. РАСПИСАНИЕ, помимо остального, указаны отдел фирмы и название должности — например ПОМОЩНИК- ДЕЛОПРОИЗВОДИТЕЛЯ • 1.9. УПРАЖНЕНИЯ 1. Сконструируйте вариант записи, которая может входить в файл ТОВАРНЫЕ.ЗАПАСЫ, ведущийся на фирме «Типико». 2. Сконструируйте одну запись файла ЗАГОЛОВКИ.НАРЯ- ДОВ и набор записей файла СОДЕРЖАНИЕ.НАРЯДОВ, сфор- мированных, например, согласно заказу на приобретение товаров, предоставленному фирмой «Смитсон Электрик» (см. разд. 1.2.1). 3. В принципе запись файла СОДЕРЖАНИЕ.НАРЯДОВ однозначно идентифицируется по значениям двух полей — НО- МЕР. НАРЯДА и ШИФР.ТОВАРА. В то же время записи этого файла снабжаются, как правило, и полем НОМЕР.СТРОКИ. Как Вы полагаете, с какой целью это делается? (Учтите, что един- ственной записи файла ЗАГОЛОВКИ. НАРЯДОВ может соот- ветствовать несколько сотен записей в файле СОДЕРЖАНИЕ. НАРЯДОВ). 4. Перечислите возможные ключи-кандидаты для файлов а) и б), считая, что в процессе эксплуатации их содержание остается неизменным. а) ПОЛЕ 1 ПОЛЕ 2 ПОЛЕ 3 ПОЛЕ 4 а 1 X Р b 3 Z Р с 3 у Р d 2 W q 35
б) ПОЛЕ 1 а b с d ПОЛЕ 2 ПОЛЕ 3 ПОЛЕ 4 1 х р 3 у р 3 z р 2 к q 5. Перечислите возможные ключи-кандидаты для каждого из следующих файлов, используемых на фирме «Типико»: а) ЗАГОЛОВКИ. НАРЯДОВ; б) СОДЕРЖАНИЕ. НАРЯДОВ; в) ПОСТАВЩИКИ; г) ИСТОЧНИКИ; д) ПОСТУПЛЕНИЕ.ТОВАРОВ; е) НЕОПЛАЧЕННЫЕ.СЧЕТА; ж) служащие. 6. Предложите имена полей для записей следующих двух файлов фирмы «Типико»: а) ЖУРНАЛ. КАССОВЫХ.ВЫПЛАТ (см. разд. 1.6.2); б) ШТАТНОЕ. РАСПИСАНИЕ (см. разд. 1.8.4).
ГЛАВА 2 РЕЛЯЦИОННАЯ АЛГЕБРА 2.1. ВВЕДЕНИЕ Если файлы хранятся в памяти компьютера, пользо- ватель не может сам взять и прочитать или исправить их содержи- мое, как в добрые старые времена, когда они хранились в папках с бумагами. Вместо этого он должен скомандовать машине, чтобы та проделала за него требуемые операции. Но иногда пользова- телю бывает нужно выполнить такие действия, которые крайне сложно, а зачастую и невозможно запрограммировать на обычном языке, в том числе на Паскале. В таких случаях приходится пе- реходить на специальный язык обработки данных, представляю- щий расширение стандартного языка программирования. Аль- тернативой может служить высокоуровневый язык запросов — специализированный язык для работы с файлами. Между язы- ками запросов и обработки данных трудно провести четкую границу, и некоторые специалисты предпочитают не различать их вовсе. Примером языков подобного типа является простейший язык запросов на основе реляционной алгебры. Существуют и другие, порой весьма изощренные и обладающие широкими возможно- стями языки, по форме напоминающие примитивный английский, и благодаря этому значительно более удобные в обращении. Нач- нем, однако, именно с реляционной алгебры. Этот сравнительно новый раздел математики послужил теоретической основой для описания действий, выполняемых над группами файлов. Опре- деленные в ней элементарные операции реализуются в любом языке запросов независимо от его внешнего оформления. 2.2. МУЗЫКАЛЬНЫЕ ФАЙЛЫ Файла, ведущиеся на фирме «Типико», характеризу- ются достаточно сложными взаимосвязями и поэтому мало под- ходят в качестве примера для первого знакомства с реляционной алгеброй. Вместо них воспользуемся набором намеренно упро- щенных файлов, которые в дальнейшем будут именоваться му- зыкальными. 37
Музыкальные файлы еодержат информацию о музыкантах, музыкальных произведениях и обстоятельствах их исполнения. Нескольких музыкантов, образующих единый коллектив, назо- вем ансамблем — это может быть классический оркестр, джазо- вая группа, квартет, квинтет и т. д. К музыкантам причислим исполнителей (играющих на одном или нескольких инструментах), композиторов, дирижеров и руководителей ансамблей. Все даты в файлах будут представлены восьмизначными чис- лами, в которых первая слева четверка цифр указывает год, следующая пара — номер месяца, а две последние цифры — чис- ло месяца. В частности, 19910507 обозначает «7 мая 1991 г.» Достоинство такой непривычной записи дат в том, что их можно использовать непосредственно как операнды в операциях сравне- ния. Если, например, d = 19891102, можно утверждать, что справедливо неравенство 19910506 > d. Дадим теперь краткую характеристику музыкальных файлов и входящих в них полей. МУЗЫКАНТЫ (MUSICIANS) {Имя файла, содержащего персональ- ную информацию о музыкантах. Ниже НОММУЗ (MNO) перечислены поля этого файла} {Номер музыканта. Поле служит пер- вичным ключом файла, а также ис- ИМЯМУЗ (MNAME) ДАТАРОЖ (BDATE) СТРАНАРОЖ (BCOUN TRY) СОЧИНЕНИЯ (COMPOSITIONS) пользуется как перекрестная ссылка} (Имя музыканта. Для простоты за- писывается только фамилия} {Дата рождения} (Страна происхождения} {Имя файла, содержащего краткую информацию о сочинениях. Ниже пе- номсоч (CNO) речислены поля файла} {Номер сочинения. Поле служит пер- вичным ключом файла, а также ис- НАЗВАНИЕ (TITLE) НОММУЗ (MNO) ДАТАСОЧ (CDATE) АНСАМБЛИ (ENSEMBLES) пользуется как перекрестная ссылка} {Название произведения} {Номер музыканта, присвоенный ком- позитору} {Дата создания произведения} {Имя файла, содержащего информа- цию об ансамблях. Ниже перечислены НОМАНС (ENO) поля файла} {Номер ансамбля. Поле служит пер- вичным ключом файла, а также ис- НАЗВАНС (ENAME) СТРАНАНС (ECOUNTRY) НОММУЗ (MNO) ИСПОЛНЕНИЯ пользуется как перекрестная ссылка} {Название ансамбля} {Страна, где был организован ан- самбль} {Номер музыканта, присвоенный ру- ководителю ансамбля) {Имя файла, содержащего информа- 38
(PERFORMANCES) НОМСОЧ (CNO) ДАТАИСП (PDATE) ГОРОД (TOWN) СТРАНА (COUNTRY) НОММУЗ (MNO) НОМАНС (ENO) ИСПОЛНИТЕЛИ (PERFORMERS) НОМИСП (PNO) НОММУЗ (MNO) ИНСТРУМЕНТ (INSTRUMENT) ОЦЕНКА (GRADE) УЧАСТНИКИ АНСАМБЛЕЙ (ENSEMBLES MEMBERS) НОМАНС (ENO) НОМИСП (PNO) цию об исполнениях сочинений. Ниже перечислены поля файла} {Номер исполненного сочинения) {Дата исполнения} {Место, где было исполнено сочи- нение} {Страна, где было исполнено сочине- ние} {Номер музыканта, присвоенный ди- рижеру ансамбля, исполнявшего со- чинение} { Номер ансамбля, исполнявшего со- чинение} { Имя файла, содержащего информа- цию о специальностях и квалификации исполнителей (некоторые из них мо- гут играть на нескольких инструмен- тах, а другие не умеют ни на одном). Ниже перечислены поля файла} {Номер исполнителя. Поле служит первичным ключом файла, а также используется как перекрестная ссылка} {Номер музыканта, присвоенный ис- полнителю) {Инструмент, на котором играет ис- полнитель) {Оценка качества исполнения} {Имя файла, содержащего информа- цию о том, в каких ансамблях играют исполнители. Ниже перечислены поля {Номер исполнителя} Приведем пример содержимого музыкальных файлов, запол- нив каждый из них всего несколькими записями. С его помощью будут наглядно проиллюстрированы описываемые далее операции реляционной алгебры. Вообще достоинства реляционной алгебры проявляются более наглядно, когда приходится манипулировать десятками тысяч записей, однако обилие данных затрудняет вос- приятие. Кроме того, соотношения реляционной алгебры одина- ково справедливы для файлов, содержащих и многие тысячи, и единицы записей, как в представленном примере. МУЗЫКАНТЫ НОММУЗ ИМЯМУЗ ДАТАРОЖ СТРАНАРОЖ Ml ЭЙБИЙСКИЙ 19410609 УАЙЛАНДИЯ М2 БИСИЙСКИЙ 18181211 ЗЕДЛАНДИЯ М3 М4 сидийский ЭЙБИЙСКИЙ 18980404 19360621 ЭКСЛАНДИЯ УАЙЛАНДИЯ М5 ЭЙЭФСКИЙ 19510919 ЗЕДЛАНДИЯ 39
Мб БИДИЙСКИЙ 19021025 ТИЛАНДИЯ М7 ЭЙСИЙСКИЙ 19471104 УАЙЛАНДИЯ М8 сидийский 18980404 ТИЛАНДИЯ М9 ливийский 19540130 УАЙЛАНДИЯ СОЧИНЕНИЯ номсоч НАЗВАНИЕ НОММУЗ ДАТАСОЧ С1 элпис М5 19830605 С2 эмтьюн М9 19851108 сз КЕЙСОНГ М3 19340120 С4 эйчтьюн М5 19840029 С5 элпис М3 19290530 С6 КЕЙСОНГ М5 19821204 АНСАМБЛИ НОМАНС НАЗВАНО СТРАНАНС НОММУЗ Е1 ЭЙЗ ЭКСЛАНДИЯ М7 Е2 виз ЭКСЛАНДИЯ М5 ЕЗ сиз ТИЛАНДИЯ М7 Е4 АЙЗ ЗЕДЛАНДИЯ М4 Е5 ДИЗ ЭКСЛАНДИЯ М5 Е6 ЭФС ЭКСЛАНДИЯ М5 Е7 виз ЗЕДЛАНДИЯ М4 ИСПОЛНЕНИЯ НОМСОЧ ДАТАИСП ГОРОД СТРАНА НОММУЗ НОМАНС С5 19860530 титон ТИЛАНДИЯ М9 ЕЗ С1 19860615 АРБИ ЭКСЛАНДИЯ Ml Е6 С5 19860622 ЭСТОН ЭКСЛАНДИЯ М9 Е5 СЗ 19860622 ЭСФИЛД ЭКСЛАНДИЯ Ml Е1 С5 19860703 ЭСТОН ТИЛАНДИЯ М9 Е5 С6 19860705 АРБИ ЗЕДЛАНДИЯ М7 Е4 С1 19860711 ТИБИ ЭКСЛАНДИЯ Ml Е1 С4 19860719 АРБИ УАЙЛАНДИЯ М4 Е2 ИСПОЛНИТЕЛИ номисп НОММУЗ ИНСТРУМЕНТ ОЦЕНКА Р1 Ml САКСОФОН ОТЛИЧНО Р2 Ml ФЛЕЙТА СРЕДНЕ РЗ Ml ФОРТЕПИАНО СКВЕРНО Р4 М4 ТРУБА ОТЛИЧНО Р5 М5 СКРИПКА ОТЛИЧНО Р6 М5 АЛЬТ плохо Р7 М9 КЛАРНЕТ отлично Р8 М9 САКСОФОН СКВЕРНО Р9 М7 ФОРТЕПИАНО СРЕДНЕ УЧАСТНИКИ АНСАМБЛЯ НОМАНС НОМИСП Е1 Р5 Е1 Р8 Е1 Р9 Е2 Р5 Е2 РЗ ЕЗ Р9 Е4 Р4 Е4 Р1 Е4 Р7 Е5 Р5 Е5 Р9 40
Е6 Pl Е6 Р7 . Е7 РЗ Е7 Р5 2.3. ОПЕРАЦИИ И ОПЕРАНДЫ Имена и значения полей, образующих запись, опреде- ляют ее тип. Неупорядоченную совокупность однотипных запи- сей называют отношением х. Понятие файла по сравнению с от- ношением является более общим — действительно, в файле могут содержаться записи разных типов (хотя в книге всегда подразу- мевается противоположное). Кроме того, существуют способы организации файлов, делающие возможной упорядоченность вхо- дящих в них записей и ее использующие. Предваряя обсуждение операндов реляционной алгебры, рассмотрим простейший опера- тор Паскаля: F: = (А + В)*С — D. Здесь А, В, С и D — операнды, а знаки +, * и — обозна- чают выполняемые над ними операции. Итоговый результат присваивается переменной F. В реляционной алгебре и операнды и результата всех дей- ствий являются отношениями. Несколько упрощая, будем в даль- нейшем отождествлять отношения реляционной алгебры с фай- лами. Порядок размещения записей в файлах-операндах никак не учитывается при выполнении операций, а в файлах-результатах их распределение считается произвольным. В работах по теории отношений и реляционной алгебре ана- логи записей иногда именуются кортежами, полей—доменами, однако в книге эта специальная терминология не употребляется. Познакомимся теперь с операциями, используемыми в реляци- онной алгебре. 2.4. ПРОЕКТИРОВАНИЕ В операции проектирования участвует единственный операнд, который обозначим как ИМЯФАЙЛА (FNAME). Это может быть и файл с исходными данными, и файл, созданный в ре- зультате преобразований реляционной алгебры. Дадим следующее определение операции проектирования proj список имен полей (ИМЯФАЙЛА) В списке имен, следующем за оператором proj, должны упо- минаться только те поля, что входят в состав записей ИМЯФАЙ- ЛА. Результат операции проектирования формируется следую- щим образом: а) Из файла ИМЯФАЙЛА удаляются все поля, имена которых не фигурируют в списке; 1 Термин «отношение» в оригинале — «реляция». Отсюда и произошло название раздела математики «реляционная алгебра». — Прим. пер. 41
ИНСТРУМЕНТЫ (ИСПОЛНИТЕЛИ) б) Из полученной в п. а) совокупности записей удаляются повторяющиеся, так что итог операции не содержит оди- наковых записей. Первый пример — proj САКСОФОН ФЛЕЙТА ФОРТЕПИАНО ТРУБА СКРИПКА АЛЬТ КЛАРНЕТ Как можно видеть, каждый инструмент назван лишь однажды. Второй приме] ЭЙБИЙСКИЙ БИСИЙСКИЙ СИДИЙСКИЙ ЭЙЭФСКИЙ БИДИЙСКИЙ ЭЙСИЙСКИЙ СИДИЙСКИЙ ЛИВИЙСКИЙ Второй пример — pro) ИМЯМУЗ, СТРАНАРОЖ (МУЗЫКАНТЫ): ---------- УАЙЛАНДИЯ ЗЕДЛАНДИЯ ЭКСЛАНДИЯ ЗЕДЛАНДИЯ ТИЛАНДИЯ УАЙЛАНДИЯ ТИЛАНДИЯ УАЙЛАНДИЯ 2.5. ВЫБОР Подобно проектированию, операция выбора имеет единственный операнд, который может быть файлом с исходными данными или файлом, полученным в результате преобразований реляционной алгебры. Синтаксис операции выбора таков: \ sei условие (ИМЯ ФАЙЛА) Итогом операции является совокупность записей ИМЯФАЙЛА, объединяющая лишь те из них, которые удовлетворяют заданному условию. Например, в результате операции sei СТРАНАРОЖ='УАЙЛАНДИЯ' (МУЗЫКАНТЫ) формируется следующий набор записей: Ml ЭЙБИЙСКИЙ 19410609 УАЙЛАНДИЯ М4 ЭЙБИЙСКИЙ 19360621 УАЙЛАНДИЯ М7 ЭЙСИЙСКИЙ 19471104 УАЙЛАНДИЯ М9 ЛИВИЙСКИЙ 19540130 УАЙЛАНДИЯ Обратим внимание на то, что в приведенном примере название страны заключено в апострофы. Они указывают, что эта строка символов, называемая также литералом, является значением поля СТРАНАРОЖ. В то же время СТРАНАРОЖ записано без апо- строфов, поскольку это имя поля, а не его значение. Данное пра- вило относится к любым одиночным символам и строкам, представ- ляющим литералы. Их всегда помещают в апострофы, чтобы отли- чать от имен полей, переменных и др., которые, напротив, нельзя записывать в апострофах. Числовые константы также записы- ваются без апострофов, за исключением тех случаев, когда они выступают как символьные строки. В частности, из файла МУ- 42
ЗЫКАНТЫ можно выбрать записи о музыкантах, родившихся не ранее 1946 г., с помощью следующей операции: sei ДАТАРОЖ > 19451231 (МУЗЫКАНТЫ). Здесь 19451231 понимается как целая константа, а не как строка символов, и поэтому не заключается в апострофы. Условие, определяющее результат выбора, представляет со- бой выражение, которое может включать операции сравнения <, =, >, <=, <> и >=, а также логические операции and («И»), or («ИЛИ») и not («НЕ»). В то же время запрещено исполь- зовать арифметические операции +, —, *, /. Любой операнд, входящий в условие, сам не может быть результатом преобразо- ваний реляционной алгебры. В остальном условия записываются согласно правилам стандартного Паскаля. Предположим, например, что из файла МУЗЫКАНТЫ тре- буется выбрать данные обо всех музыкантах, родившихся в Уай- ландии не позже 1946 г. Это можно вделать, воспользовавшись операцией sei (СТРАНАРОЖ='УАЙЛАНДИЯ') and (ДАТАРОЖ> 19451231) (МУЗЫКАНТЫ) Она дает следующие две записи: М7 ЭЙСИЙСКИЙ 19471104 УАЙЛАНДИЯ М9 ЛИВИЙСКИЙ 19540130 УАЙЛАНДИЯ 2.6. ПРОСТЕЙШИЕ ЗАПРОСЫ Применив операцию проектирования к результату ранее выполненного выбора, из файла можно извлечь информа- цию, содержащую ответ на вполне практический, хотя и очень простой вопрос. Рассмотрим следующий пример. Запрос: перечислить фамилии всех музыкантов, родившихся не ранее 1946 г. Задача решается с помощью операций proj ИМЯМУЗ (sei ДАТАРОЖ> 19451231 (МУЗЫКАНТЫ)). Приведем еще один пример. Запрос: перечислить номера всех музыкантов, играющих на фортепиано, кроме тех, игра которых оценивается как «скверно», Ответ дает следующая пара операций: proj НОММУЗ (sei (ИНСТРУМЕНТ='ФОРТЕПИАНО') and (ОЦЕНКА <> 'СКВЕРНО') (ИСПОЛНИТЕЛИ)). Результат включает единственный номер М7. Ответ, однако, может быть и «отрицательным», т. е. не содержать никакой ин- формации. Это демонстрирует следующий пример. Запрос: указать все страны и города, в которых любые произ- ведения исполнялись 22 июня 1986 г. Этот запрос следует записать таким образом: proj ГОРОД, СТРАНА (sei ДАТАИСП=19860622 (ИСПОЛНЕНИЯ)). Ни одной запиви, отвечающей заданному уеловию, не найдено. 43
2.7. СОЕДИНЕНИЕ 2.7.1. СОЕДИНЕНИЕ ПО ОДНОМУ ПОЛЮ В реляционной алгебре определен целый ряд операций соединения. Ниже будет рассматриваться лишь одна разновид- ность соединения, именуемого естественным. Поэтому далее тер- мином соединение будет обозначаться, если не оговорено иное, именно этот вид операции. Очевидно, любое поле записи, подобно переменной Паскаля, является объектом определенного типа — например, целым чис- лом (INTEGER), вещественным числом (REAL) или строкой сим- волов (STRING). В операции соединения участвуют два операнда, которые запишем как ИМЯФАЙЛА! и ИМЯФАЙЛА2. Здесь и далее считается, что файлы могут содержать как исходные данные, так и полученные в результате преобразований реляционной алгебры. Для простоты предположим сначала, что в состав ИМЯ- ФАЙЛА! и ИМЯФАЙЛА2 входит ровно одно общее для них поле, имеющее в обоих файлах одинаковые имя и тип. Например, у файлов МУЗЫКАНТЫ и СОЧИНЕНИЯ таким общим полем является НОММУЗ. Что же касается полей, содержащих даты, то у них разные имена: в первом файле ДАТАРОЖ, во втором — ДАТАСОЧ. Рассмотрим еще один простой пример. Допустим, в ИМЯ- ФАЙЛА! присутствуют поля G1 и F1, содержащие следующую информацию: G1 FI d 3 h 7 У 4 В ИМЯФАЙЛА2 имеются поля FI и G2, содержимое которых таково: F1 3 3 7 4 4 G2 А В А С В Результатом операции соединения ИМЯФАЙЛА1 join ИМЯФАЙЛА2 является конкатенация каждой записи ИМЯФАЙЛА! с каждой записью ИМЯФАЙЛА2, у которых совпадают данные в общем поле, причем само это поле в образующейся записи помещается лишь однажды. (Конкатенацией называется слияние двух или нескольких строк в одну). В обсуждаемом примере сливаются записи F1 3 3 G2 А В 44
из ИМЯ ФАЙ Л А2 в запивью G1 F1 d 3 из ИМЯФАЙЛА1. Действительно, у них одинаковые значения (равные 3) общего поля F1. После выполнения операции ИМЯФАЙЛА join ИМЯФАЙЛА2 из этих запивей формируютвя две новые: G1 Fl G2 d 3 А d 3 В Однако это лишь часть результата. В ИМЯФАЙЛА1 есть за- пись G1 F1 h 7, которую можно влить е запивью Fl G2 7 А из ИМЯФАЙЛА2, поскольку у них совпадают значения (равные 7) общего поля F1. Наконец, результат операции должен содержать конкатенацию пары записей Fl G2 4 С 4 В из ИМЯФАЙЛА2 с запивью G1 F1 У 4 из ИМЯФАЙЛА1. У них в общем поле F1 помещается одно и то же число 4. В итоге операции ИМЯФАЙЛА join ИМЯФАЙЛА2 даст следующую совокупность записей: G1 Fl G2 d 3 А d 3 В h 7 А у 4 С у 4 В 2.7.2. ИСПОЛЬЗОВАНИЕ СОЕДИНЕНИЯ ПРИ ОТВЕТАХ НА ЗАПРОСЫ С помощью операции proj НОММУЗ (СОЧИНЕНИЯ) можно получить перечень номеров, присвоенных всем компози- торам, т. е. музыкантам, воздавшим хотя ба по одному произве- дению. Но чтобы узнать имена всех композиторов, с полями НОММУЗ в записях файла СОЧИНЕНИЯ нужно связать соот- 45
ветствующие поля ИМЯМУЗ. Для этого достаточно произвести соединение файлов СОЧИНЕНИЯ и МУЗЫКАНТЫ, в резуль- тате которого произойдет слияние каждой записи файла СОЧИ- НЕНИЯ с записью файла МУЗЫКАНТЫ, содержащей имя автора. Таким образом, список имен композиторов сформирует пара опе- раций proj ИМЯМУЗ (СОЧИНЕНИЯ join МУЗЫКАНТЫ). Рассмотрим еще один пример. Запрос: указать имя и дату рождения каждого композитора, написавшего вещь под названием «Кейсонг». Ответ дает следующая последовательность операций: proj ИМЯМУЗ, ДАТАРОЖ (sei НАЗВАНИЕ='КЕЙСОНГ' (СОЧИНЕНИЯ) join МУЗЫКАНТЫ). С помощью операции выбора из файла СОЧИНЕНИЯ выделя- ются записи всех сочинений, названных «Кейсонг». Эти записи соединяются затем с теми записями файла МУЗЫКАНТЫ, у ко- торых совпадают значения поля НОММУЗ. Это означает, что каждая отобранная запись файла СОЧИНЕНИЯ дополняется информацией об авторе данного произведения. Окончательным итогом описанной последовательности операций реляционной алгебры являются следующие данные: СИДИЙСКИЙ 18980404 ЭЙЭФСКИЙ 19510919 Нетрудно убедиться, что это правильный ответ на запрос. Могут быть и более сложные запросы, требующие выполнения двух и более операций соединения. Приведем пример. Запрос: перечислить все инструменты, на которых играют в ансамбле «Эйз» из Зедландии. Вот выражение реляционной алгебры, которое позволит полу- чить ответ на этот запрос: proj ИНСТРУМЕНТ (proj НОМАНС (sei (НАЗВАНИЕ^ 'ЭЙЗ') and (СТРАНАНС= 'ЗЕДЛАНДИЯ') (АНСАМБЛИ)) join УЧАСТНИКИ АНСАМБЛЕЙ join ИСПОЛНИТЕЛИ). В результате первого (считая слева) соединения формируется набор номеров НОМИСП всех участников названного ансамбля. Второе соединение образует совокупность записей, содержащих названия инструментов, на которых играют исполнители. Операции выбора в преобразованиях реляционной алгебры могут выполняться более чем над одним файлом. Проиллюстри- руем это следующим примером. Запрос: указать, когда в Эксландии исполнялось произве- дение под названием «Эмтьюн». Ответ на запрос получим с помощью такого выражения: proj ДАТАИСЩзе! СТРАНАИСП='ЭКСЛАНДИЯ' (ИСПОЛНЕНИЯ) join proj НОМСОЧ (sei НАЗВАНИЕ= 'ЭМТЬЮН' (СОЧИНЕНИЯ))). Еще один пример развивает предыдущий. В нем потребуется вторая операция соединения. 46
Запрос: указать автора любого произведения, названного «Эмтьюн», которое хотя бы однажды исполнялось в Эксландии. На запрос отвечает следующее выражение: proj ИМЯМУЗ (proj НОМСОЧ (sei СТРАНА= 'ЭКСЛАНДИЯ' (ИСПОЛНЕНИЯ)) join sei НАЗВАНИЕ= 'ЭМТЬЮН' (СОЧИНЕНИЯ) join МУЗЫКАНТЫ). В таком множестве операций нетрудно и запутаться. Недаром многие предпочитают конструировать преобразования реляцион- ной алгебры, а не разбираться в них. 2.7.3. СОЕДИНЕНИЕ ПО НЕСКОЛЬКИМ ПОЛЯМ Соединением двух файлов, не имеющих ни одного общего поля, является пустое множество. Если же у файлов ИМЯФАЙЛА1 и ИМЯФАИЛА2, наоборот, не одно, а несколько общих полей, то соединение включает конкатенацию каждой записи ИМЯФАЙЛА1 с каждой записью ИМЯФАЙЛА2, у ко- торой совпадают с первой значения всех общих полей. Предположим, например, что файл ИМЯФАЙЛА1 содержит три записи: 01 Fl F2 d 3 Н h 7 N У 4 Н а файл ИМЯФАЙЛА2 — пять: Fl F2 G2 3 Н А 3 Н В 7 N А 4 Н С 4 Н В Операция соединения ИМЯФАЙЛА1 join ИМЯФАЙЛА2 формирует следующий набор данных: G1 Fl F2 G2 d 3 Н А d 3 Н В h 7 N А у 4 Н С У 4 Н В Как и прежде, в полученных записях имеется лишь по одному общему полю. Запрос: указать страну и название каждого ансамбля, руко- водитель которого выступает одновременно и как исполнитель. Ответ дает последовательность операций proj НАЗВАНО, СТРАНАНС (АНСАМБЛИ Join ИСПОЛНИ- ТЕЛИ join УЧАСТНИКИ АНСАМБЛЕЙ). 47
Поясним написанное. В результате первого соединения номер исполнителя НОМИСП связывается с номером НОМАНС, если этот исполнитель руководит ансамблем. Таким образом, все руководители, не являющиеся исполнителями, после этого соеди- нения удаляются из списка. Второе соединение осуществляется на основе двух общих полей, НОМИСП и НОМАНС. После него из результата предшествующего соединения исключаются записи, содержащие пары НОМИСП и НОМАНС, отсутствующие в запи- сях файла УЧАСТНИКИ. АНСАМБЛЕЙ. Тем самым отсеиваются ансамбли, в которых не играют их руководители. 2.8. УПРАЖНЕНИЯ 1. Составьте последовательность операций реляционной алгебры, с помощью которых можно получить ответы на следу- ющие запросы: а) назвать города Эксландии, в которых давались концерты любых ансамблей; б) указать дату создания каждого произведения под назва- нием «Кейсонг»; в) перечислить имена исполнителей, играющих на саксофоне; г) перечислить названия сочинений, написанных Эйэфским; д) указать даты исполнения произведений, созданных компо- зиторами — уроженцами Зедландии; е) перечислить руководителей всех ансамблей, в составе кото- рых есть саксофонисты. 2. Сконструируйте выражения, дающие ответы на запросы, касающиеся содержимого файлов фирмы «Типико»: а) найти описание товара, имеющего шифр 95РА42; б) указать номер клиента и предельный размер кредита фирмы «Смитсон электрик», расчеты с которой производятся по адресу Хай-стрит-17, Пимарш; в) перечислить номера всех клиентов, в нарядах на продажу которым значится товар с шифром 95РА42; г) указать названия фирм и адреса для расчетов тех клиентов, для которых составлялось хотя бы по одному наряду на продажу, включающему 10 единиц товара с шифром 95РА42. 3. Сконструируйте выражения, дающие ответы на запросы относительно содержимого музыкальных файлов: а) перечислить названия всех эксландских ансамблей, среди участников которых есть саксофонисты; б) назвать все города Эксландии,. в которых исполнялись любые вещи, созданные не ранее 1947 г.; в) перечислить страны, города и даты проведения концертов, на которых композиторы дирижировали исполнением собственных сочинений. 48
2.9. ОБЪЕДИНЕНИЕ Наряду с выбором, проектированием и соединением в реляционной алгебре определены операции объединения, пере- сечения, вычитания, деления и умножения. Объединение, пере- сечение и вычитание применяются только к файлам с одинаковыми именами и типами полей. Иными словами, у файлов-операндов должны совпадать типы записей. Результатом операции объединения ИМЯФАЙЛА1 union ИМЯФАЙЛА2 является'еовокупность записей, входящих хотя бы в один, а воз- можно, и в оба операнда. Пусть, например, ИМЯФАЙЛА1 вклю- чает записи Fl G1 а 4 b 5 с 6, а ЙМЯФАЙЛА2 — Fl G1 d 2 b 5 е 4 с 6. В этом случае операция ИМЯФАЙЛА1 anion ИМЯФАЙЛА2 дает следующий набор записей: Fl G1 ч а 4 b 5 с 6 d 2 е 4. При ответах на некоторые запросы вместо объединения пред- почтительнее пользоваться выбором, включая в его условие операцию «ИЛИ». Приведем пример подобной ситуации. Запрос: назвать имена всех музыкантов, родившихся в Уайлан- дии или Зедландии. Ответ на этот запрос получим с помощью выражения: proj ИМЯМУЗ (sei (СТРАНАРОЖ' = 'УАЙЛАНДИЯ') or (СТРАНАРОЖ - 'ЗЕДЛАНДИЯ') (МУЗЫКАНТЫ)). Операция объединения применяется в тех случаях, когда информацию нельзя извлечь из одного файла с помощью выбора. Еще один пример. Запрос: перечислить имена всех музыкантов, родившихся в Уайландии либо сочинивших вещь под названием «Кейсонг». Ответ на запрос даст следующая последовательность опе- раций: 49
proj ИМЯМУЗ (sei СТРАНАРОЖ = 'УАЙЛАНДИЯ' (МУ- ЗЫКАНТЫ)) union proj ИМЯМУЗ (МУЗЫКАНТЫ join sei НАЗВАНИЕ = 'КЕЙСОНГ' (СОЧИНЕНИЯ)). Первым операндом в операции объединения служит набор имен музыкантов, родившихся в Уайландии, вторым — набор, объединяющий имена композиторов, которые написали произ- ведения, названные «Кейсонг». 2.10. ПЕРЕСЕЧЕНИЕ В результате выполнения операции пересечения ИМЯФАЙЛА1 intersection ИМЯФАЙЛА 2 получается набор записей, входящих в состав обоих файлов. Если, например, в файле ИМЯФАЙЛА1 содержатся записи F1 а b с G1 4 5 6, а в ИМЯФАЙЛА2 — F1 d b е с G1 2 5 4 6, то их перевечением является следующая совокупность записей: Fl G1 b 5 С 6. Пример запроса, для ответа на который необходимо при- менить операцию пересечения. Запрос: назвать имена всех композиторов, являющихся авто- рами по меньшей мере двух произведений — «Кейсонг» и «Элпис». Ответ можно получить g помощью такого выражения: proj ИМЯМУЗ (МУЗЫКАНТЫ join (proj НОММУЗ (sei НА- ЗВАНИЕ = 'КЕЙСОНГ' (СОЧИНЕНИЯ)) intersection proj НОМ- МУЗ (sei НАЗВАНИЕ = 'ЭЛПИС' (СОЧИНЕНИЯ)))). А вот для ответа на следующий запрос операция пересечения не потребуется. Запрос: перечислить всех композиторов, родившихся в Уайлан- дии не позднее 1945 г. Вместо пересечения здесь проще воспользоваться логиче- ским «И» в условии операции выбора (этого не удалось бы сделать в предыдущем случае): proj ИМЯМУЗ (sei (СТРАНАРОЖ='УАЙЛАНДИЯ') and (ДА- ТАРОЖ < 19460101) (МУЗЫКАНТЫ)). 60
2.11. ВЫЧИТАНИЕ Результатом операции вычитания ИМЯ ФАЙЛА 1 difference ИМЯ ФАЙ Л А2 является совокупность записей, имеющихся в ИМЯФАЙЛА1, но отсутствующих в ИМЯФАИЛА2. Если, например, ИМЯ- ФАЙЛА! содержит записи F1 а b с f а ИМЯФАЙЛА2 — F1 d b е с G1 4 5 6 5, G1 1 5 4 6, то операция ИМЯФАЙЛА1 difference ИМЯФАЙЛА2 формирует набор записей Fl G1 а 4 f 5 Приведем пример запроса, для ответа на который приходится использовать вычитание. Запрос: назвать имена всех музыкантов, не сочиняющих произ- ведений. Ответ даст последовательность операций proj ИМЯМУЗ (proj НОММУЗ (МУЗЫКАНТЫ) difference proj НОММУЗ (СОЧИНЕНИЯ) join МУЗЫКАНТЫ). Интерес здесь представляет внутреннее выражение proj НОММУЗ (МУЗЫКАНТЫ) difference proj НОММУЗ (СОЧИНЕНИЯ), с помощью которого образуется совокупность НОММУЗ всех музыкантов, упомянутых в файле МУЗЫКАНТЫ, но не фигурирующих в файле СОЧИНЕНИЯ- 2.12. ДЕЛЕНИЕ При выполнении операции деления ИМЯФАИЛА1 division ИМЯФАЙЛА2 требуется, чтобы каждое поле ИМЯФАЙЛА2 имело те же имя и тип, что и одно из полей ИМЯФАЙЛА1. В результате деления получается файл, поля которого содержатся в ИМЯФАЙЛА1, но отсутствуют в ИМЯФАЙЛА2. Запись включается в результат только при том условии, что в ИМЯФАЙЛА1 она сцеплена с каж- дой записью из ИМЯФАЙЛА2. Если, например, ИМЯФАЙЛА1 содержит записи 51
G2 G1 Fl A d 3 C h 7 В h 7 B y 4 В d 3 С у 4, а ИМЯФАЙЛА2 — G1 Fl d 3 h 7 У 4, то результат деления ИМЯФАЙЛА! division ИМЯФАЙЛА2 представляет собой файл G2 А В. В этом файле лишь одно поле — G2, которое входит в ИМЯ- ФАЙЛА! и не входит в ИМЯФАЙЛА2. Значение А поля G2 включено в результат деления по той причине, что в ИМЯФАЙЛА1 оно сцеплено с каждой записью ИМЯФАЙЛА2. Действительно, в составе ИМЯФАЙЛА1 имеются все записи G2 G1 F1 A d 3 A h 7 А у 4. Значение С поля G2 не вошло в результат, поскольку в ИМЯФАЙЛА1 отсутствует запись С d 3. Рассмотрим пример, в котором используется операция де- ления. Запрос: перечислить номера ансамблей, в каждом из которых играют все исполнители, составляющие ансамбль Е6. Ответ сформируем с помощью соотношения УЧАСТНИКИ АНСАМБЛЕЙ division proj НОМИСП (sei НОМАНС = 'Е6' (УЧАСТНИКИ АНСАМБЛЕЙ)). Второй операнд операции деления дает здесь список номеров всех исполнителей из ансамбля Е6. 2.13. УМНОЖЕНИЕ Результатом операции умножения * * В некоторых работах эту операцию называют декартовым произведением.— Прим. ред. 52
ИМЯФАЙЛА1 prodact ИМЯФАЙЛА2 является совокупность записей, представляющих конкатенацию каждой записи ИМЯФАЙЛА1 с каждой записью ИМЯФАЙЛА2. Пусть, например, даны файлы-операнды ИМЯФАЙЛА1 Fl F2 b 4 d 7 и ИМЯФАЙЛА2 F3 F4 А 4 С R В этом случае результат содержит записи ИМЯФАЙЛА1.Р1 ИМЯФАЙЛА1.Р2 ИМЯФАЙЛА2.F3 ИМЯФАЙЛА2. F4 Ь 4 А 4 b 4 С R d 7 А 4 d 7 С R В образованном файле каждое поле сохраняет прежнее имя, но оно дополняется именем файла, из которого взято. Дополни- тельно имя предшествует имени поля и отделено от него точкой. Получающиеся при умножении составные имена, подобные ИМЯ- ФАЙЛА1.Е1, употребляются точно так же, как и простые. Имя полю присваивается для того, чтобы однозначно иденти- фицировать определенный фрагмент записи, и поэтому в ней не должно быть двух полей с одинаковыми именами. Введение дополнительных имен обеспечивает однозначность идентификации полей в таких случаях, например, как при выполнении умно- жения МУЗЫКАНТЫ prodact СОЧИНЕНИЯ. Если бы не использовались дополнительные имена файлов, результат операции содержал бы два поля с одинаковым именем НОММУЗ. Операция умножения находит практическое применение при составлении ответов на запросы, в которых требуется сравнивать значения полей различных файлов. Проиллюстрируем это сле- дующим примером. Запрос: перечислить названия всех произведений, созданных после того, как родился Эйэфский. Ответ получим с помощью выражения proj СОЧИНЕНИЯ.НАЗВАНИЕ (sei СОЧИНЕНИЯ.ДАТАСОЧ > МУЗЫКАНТЫ.ДАТАРОЖ (СОЧИНЕНИЯ prodact sei ИМЯМУЗ = 'ЭЙЭФСКИЙ' (МУЗЫКАНТЫ))). Еще один вариант использования операции умножения — формирование пар значений полей одного и того же файла, как это делается в приведенном ниже примере. 53
Запрос: назвать всевозможные пары имен музыкантов, родив- шихся в одной стране. Если просто умножить файл МУЗЫКАНТЫ на самое себя, в результат войдут пары полей с одинаковыми составными име- нами. Это запрещено, поскольку имена полей файла должны быть уникальными. Разрешить проблему поможет введение аль- тернативного имени для файла МУЗЫКАНТЫ, которое принято называть псевдонимом или алиасом. Чтобы задать это имя, перед последовательностью операций реляционной алгебры, форми- рующих ответ на запрос, необходимо поместить оператор aliases: НОВЗЫКАНТЫ aliases МУЗЫКАНТЫ; proj МУЗЫКАНТЫ.ИМЯМУЗ, НОВЗЫКАНТЫ.ИМЯМУЗ (sei МУЗЫКАНТЫ.СТРАНАРОЖ = НОВЗЫКАНТЫ.СТРА- НАРОЖ (МУЗЫКАНТЫ product НОВЗЫКАНТЫ)). Можно сделать и так, чтобы среди полученной совокупности пар имен не было повторяющихся, однако для этого выражение придется несколько усложнить: НОВЗЫКАНТЫ aliases МУЗЫКАНТЫ; proj МУЗЫКАНТЫ.ИМЯМУЗ, НОВЗЫКАНТЫ.ИМЯМУЗ (sei МУЗЫКАНТЫ.СТРАНАРОЖ = НОВЗЫКАНТЫ.СТРА- НАРОЖ) and (МУЗЫКАНТЫ.НОММУЗ < > НОВЗЫКАНТЫ.НОММУЗ) (МУЗЫКАНТЫ product НОВЗЫКАНТЫ)). 2.14. УПРАЖНЕНИЯ 1. Составьте выражения реляционной алгебры, с по- мощью которых можно получить ответы на следующие запросы относительно содержимого музыкальных файлов: а) дать перечень номеров всех ансамблей, среди участников которых есть кларнетист или саксофонист (либо тот и другой); б) дать перечень номеров всех ансамблей, среди участников которых есть и кларнетист, и саксофонист; в) дать перечень номеров всех ансамблей, среди участников которых есть саксофонист, но нет кларнетистов; г) перечислить имена всех музыкантов, не являющихся уро- женцами Уайландии и родившихся не ранее 1946 г.; д) перечислить имена всех музыкантов — уроженцев Уайлан- дии, не играющих на саксофоне; е) перечислить имена всех музыкантов, не являющихся уро- женцами Уайландии и не играющих на саксофоне; ж) перечислить имена всех музыкантов — уроженцев Уайлан- дии, играющих на саксофоне; з) назвать каждую страну, в которой исполнялись все сочи- нения Эйэфского; и) назвать каждую страну, в которой не было исполнено ни одного сочинения Эйэфского; 54
к) перечислить названия ансамблей, дававших концерты во всех странах, в каждой из которых выступал любой из них; л) перечислить названия ансамблей, среди участников кото- рых есть кларнетист или саксофонист, но не оба одновременно. 2. Сконструируйте последовательности операций реляционной алгебры, дающие ответы на запросы, касающиеся содержимого файлов фирмы «Типико»: а) указать шифр каждого товара, который, во-первых, хоть раз включался в наряд на продажу товаров клиенту, значащемуся под номером 6391; во-вторых, количество данного товара состав- ляет менее 5 единиц; б) указать шифр каждого товара, который минимум по разу включался в наряды на продажу товаров клиенту под номером 6391 и клиенту под номером 6752; в) указать шифр каждого товара, включенного по меньшей мере в один наряд на продажу, но ни разу не фигурировавшего в счете на оплату; г) указать номер каждого наряда на продажу товаров, в кото- ром значатся все товары, включенные в наряд на продажу под номером 1058. 2.15. ОПТИМИЗАЦИЯ ЗАПРОСОВ Вопрос о вычислительных затратах, связанных с вы- дачей ответов на конкретный вопрос, пока не затрагивался, и при составлении выражений на языке реляционной алгебры никак не учитывалось, насколько они эффективны с этой точки зрения. Между тем запросы необходимо оптимизировать, выбирая такую последовательность операций, которая сводила бы к минималь- ному объем вычислительной работы. Предположим, например, что V представляет одно из возможных значений поля F файла ИМЯФАЙЛА1. Если ответ на запрос формируется согласно соот- ношению (sei F = V (ИМЯФАЙЛА1)) join ИМЯФАЙЛА2, записи из ИМЯФАЙЛА1 выбираются до того, как будет выполнена операция соединения. Выражение sei F = V (ИМЯФАЙЛА1 join ИМЯФАЙЛА2) в принципе даст тот же самый ответ, но эффективность его может оказаться ниже, если результат соединения будет содержать больше записей, чем имеется в файле ИМЯФАЙЛА1. Выборка в этом случае будет идти дольше, поскольку придется просматри- вать большее число записей. Если запрос формализуется на специализированном языке типа языка реляционной алгебры, нетрудно сделать так, чтобы компьютер оптимизировал последовательность операций авто- матически. Однако при использовании расширенной версии Па- скаля, обсуждаемой в гл. 4 и 5, автоматизировать процедуру оптимизации ответов на запросы гораздо сложнее, и это еще один довод в пользу применения специализированных языков. 55
2.16. ЯЗЫКИ ЗАПРОСОВ И ВЫЧИСЛЕНИЯ По своим возможностям многие языки запросов суще- ственно превосходят язык реляционной алгебры. Достигается это благодаря включению в их состав ряда дополнительных средств. Арифметические расчеты. Начнем с примера. Запрос: определить суммарную задолженность фирмы «Смитсон электрик> по всем неоплаченным ею счетам, предъявленным фирмой «Типико». Пользуясь реляционной алгеброй, ответить на этот вопрос не удастся. Однако более совершенные языки запросов позволяют рассчитать многие экономические показатели, в том числе такие, как суммарный итог, среднее значение, общее количество и т. д. Упорядочение. Некоторые языки запросов предоставляют поль- зователю возможность упорядочивать ответы, т. е. указывать, в какой последовательности должны по его запросу выдаваться данные. Квоты. Вновь прибегнем к примеру. Запрос: перечислить номера последних четырех неоплаченных счетов, выставленных фирме «Смитсон электрик». В приведенном запросе роль квоты играет число четыре. Квота определяет максимальное количество записей, включаемых в ответ на запрос. Язык реляционной алгебры не позволяет за- давать квоты, но во многих других языках запросов такая воз- можность предусмотрена. 2.17. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ ЯЗЫКОВ ЗАПРОСОВ До сих пор о языках запросов говорилось лишь в связи с проблемами поиска и извлечения нужной информации. Однако весьма желательно, чтобы с помощью подобного языка можно было бы осуществлять также занесение записей в файл, их удале- ние и корректирование. Под корректированием понимается изме- нение содержимого некоторых или всех полей записи, не сопро- вождаемое ее запоминанием или удалением. В книге не рассматривается какой-либо конкретный язык запросов, позволяющий производить численные расчеты или обладающий другими возможностями помимо обычного поиска информации. Вместо этого в гл. 4 и 5 описывается расширенный вариант Паскаля, на примере которого показано, как можно дополнить стандартные средства языка новыми функциями, спе- циально предназначенными для получения ответов на запросы. 56
ГЛАВА 3 ПРИНЦИПЫ НОРМАЛИЗАЦИИ 3.1. РАСПРЕДЕЛЕНИЕ ПОЛЕЙ ПО ФАЙЛАМ В обсуждавшихся примерах все файлы (и музыкаль- ные, и принадлежащие фирме «Типико».) выступали до сих пор лишь как объекты, содержащие некоторые данные. Взглянем теперь на проблему с иной точки зрения и попытаемся найти способ, позволяющий объективно судить о том, какие нужны файлы и какие поля должны в них помещаться. Но сначала по- знакомимся с понятием полной декомпозиции. 3.2. ПОЛНАЯ ДЕКОМПОЗИЦИЯ Начнем с простейшего примера. Рассмотрим зоологи- ческий файл, который назовем ЗВЕРИ.В.НЕВОЛЕ (ZOOANI- MALS). В нем три поля: ЗООПАРК, ЖИВОТНОЕ и ЗОНА.ОБИ- ТАНИЯ- Предположим, что файл содержит ровно четыре записи: ЗООПАРК ЖИВОТНОЕ ЗОНА ОБИТАНИЯ ЭЙТОН КЕНГУРУ АВСТРАЛИЯ эйтой ВЕРБЛЮД АРАВИЯ БИТОН ЭМУ АВСТРАЛИЯ БИТОН ВЕРБЛЮД АРАВИЯ Нетрудно убедиться, что результат операций (proj ЗООПАРК, ЖИВОТНОЕ (ЗВЕРИ. В.НЕВОЛЕ)) (proj ЖИВОТНОЕ, ЗОНА.ОБИТАНИЯ (ЗВЕРИ. В.НЕ- ВОЛЕ)) полностью совпадает с исходным файлом ЗВЕРИ.В.НЕВОЛЕ. В общем случае полную декомпозицию файла определяют как совокупность произвольного числа его проекций, соединение которых идентично этому файлу. В частности, приведенный при- мер демонстрирует, как две проекции: proj ЗООПАРК, ЖИВОТ- НОЕ (ЗВЕРИ. В.НЕВОЛЕ) и proj ЖИВОТНОЕ, ЗОНА.ОБИ- ТАНИЯ (ЗВЕРИ.В.НЕВОЛЕ), соединяясь, образуют полную декомпозицию файла ЗВЕРИ. В. НЕВОЛЕ. 57
С другой стороны, соединение проекций proj ЗООПАРК, ЗОНА.ОБИТАНИЯ (ЗВЕРИ_В_НЕВОЛЕ) и proj ЖИВОТНОЕ, ЗОНА_ОБИТАНИЯ (ЗВЕРИ.В.НЕ- ВОЛЕ) дает следующий набор записей: ЗООПАРК ЖИВОТНОЕ ЗОНА, ОБИТАНИЯ ЭЙТОН КЕНГУРУ АВСТРАЛИЯ эйтон ЭМУ АВСТРАЛИЯ ЭЙТОН ВЕРБЛЮД АРАВИЯ БИТОН КЕНГУРУ АВСТРАЛИЯ БИТОН ЭМУ АВСТРАЛИЯ БИТОН ВЕРБЛЮД АРАВИЯ АВСТРАЛИЯ АВСТРАЛИЯ, В нем есть две записи эйтон ЭМУ БИТОН КЕНГУРУ которые отсутствуют в самом файле ЗВЕРИ. В. НЕВОЛЕ. Таким образом, этот набор не идентичен исходному файлу и, следова- тельно, соединением проекций proj ЗООПАРК, ЗОНА.ОБИ- ТАНИЯ (ЗВЕРИ.В.НЕВОЛЕ) и proj ЖИВОТНОЕ, ЗОНА.ОБИ- ТАНИЯ (ЗВЕРИ. В. НЕВОЛЕ) нельзя получить полную де- композицию файла ЗВЕРИ.В.НЕВОЛЕ. Материал последующих разделов в значительной мере посвя- щен проблемам реляционной теории нормализации. Уместно поэтому сформулировать здесь ее основные задачи. I. Создание методов анализа, позволяющих определить, обра- зует ли данная совокупность проекции файла полную декомпо- зицию этого файла. 2. Определение правил выбора одной из двух альтернативных возможностей: а) хранить файл в его непосредственном виде или б) хранить вместо самого файла набор проекций, составля- ющих его полную декомпозицию (при необходимости файл всегда можно восстановить по его проекциям). Обратимся сначала к задаче 2), т. е. решим вопрос о том, что хранить — файл или его полную декомпозицию, а задачей 1) займемся чуть позже. 3.3. ДУБЛИРОВАНИЕ ИНФОРМАЦИИ 3.3.1. ПРИМЕРЫ ДУБЛИРОВАНИЯ В некоторых случаях, заменив файл его полной де- композицией, можно избежать ненужного дублирования данных. Проиллюстрируем это на примере файла ПОСТАВЩИКИ. ТОВА- РОВ, включающего три поля: ШИФР, ТОВАРА {Уникальный номер, идентифицирующий товар} НАЗВАНИЕ {Название фирмы-поставщика} НОМЕР, ТЕЛЕФОНА {Телефон фирмы-поставщика} 58
Для простоты будем ечитать, что любой товар поетавляется не более чем одной фирмой, и все эти фирмы имеют разные назва- ния. Предположим, содержимое файла таково: ШИФР. ТОВАРА НАЗВАНИЕ НОМЕР. ТЕЛЕФОНА АА39 ДЖЕКСОН 3692 АС22 ДЖЕКСОН 3692 АН84 УИЛСОН 5948 АР83 РОБСОН 2514 AZ27 УИЛСОН 5948 DD68 УИЛСОН 5948 FS44 РОБСОН 2514 HS41 РОБСОН 2514 ММ72 УИЛСОН 5948. Можно показать (это будет сделано несколько позднее), что две проекции: proj ШИФР.ТОВАРА, НАЗВАНИЕ (ПОСТАВ- ЩИКИ. ТОВАРОВ) и proj НАЗВАНИЕ, НОМЕР. ТЕЛЕФОНА (ПОСТАВЩИКИ.ТОВАРОВ) совместно образует полную де- композицию файла ПОСТАВЩИКИ.ТОВАРОВ. Иными словами, соединение этих проекций дает файл, идентичный исходному. Проекция proj НАЗВАНИЕ, НОМЕР.ТЕЛЕФОНА (ПОСТАВ- ЩИКИ. ТОВАРОВ) содержит следующий набор записей: НАЗВАНИЕ НОМЕР-ТЕЛЕФОНА ДЖЕКСОН 3692 УИЛСОН 5948 РОБСОН 2514 Каждый из телефонных номеров упоминается в этой проекции лишь по одному разу, хотя, скажем, в файле ПОСТАВЩИКИ. ТОВАРОВ телефон фирмы «Уилсон» фигурирует в четырех запи- сях. Многократное повторение телефонных номеров — один из примеров дублирования данных. Чтобы обойтись без него, пред- лагается в данном случае вместо файла ПОСТАВЩИКИ.ТОВА- РОВ хранить его полную декомпозицию, в которой каждый номер (как и остальные данные) указан ровно один раз. Точно так же можно избежать дублирования информации, храня полную декомпозицию, а не сам исходный файл ЗВЕРИ. В. НЕВОЛЕ. Устранить дублирование важно по двум причинам. Во-пер- вых, можно добиться существенной экономии памяти. Во-вто- рых, если какой-то элемент дублируется п раз (например, теле- фон фирмы «Робсон» — трижды), то при корректировании данных необходимо менять содержимое всех п копий. В частности, если у фирмы «Робсон» сменится телефон, новый номер придется за- нести во все три записи файла ПОСТАВЩИКИ.ТОВАРОВ, где был указан старый. Очевидно, что я-кратное повторение одной и той же операции — излишняя трата времени. С другой сто- роны, не вносить изменения во все я копий нельзя, иначе будут возникать ошибки из-за несовпадения данных. Если же дублиро- 59
ванне отсутствует, то нет и причин для возникновения подобных ошибок. Например, в проекции proj НАЗВАНИЕ, НОМЕР.ТЕ- ЛЕФОНА (ПОСТАВЩИКИ. ТОВАРОВ) каждый номер упо- мянут лишь однократно, в силу чего несовпадение принципиально невозможно. 3.3.2. КЛЮЧИ-КАНДИДАТЫ Итак, в некоторых случаях дублирование устраняется за счет перехода от самого файла к его полной декомпозиции. Однако возможны и такие ситуации, когда подобная замена не дает эффекта. Очевидно, необходимо найти критерий, который поз- волил бы объективно судить о целесообразности использования полной декомпозиции вместо файла. Воспользуемся понятием ключа-кандидата, введенным в разд. 1.2.3. Ключ-кандидат определялся как минимальный набор полей (одного или нескольких), значения которых одно- значно идентифицируют запись файла. Минимальность набора понимается в том смысле, что при изъятии из него любого поля он перестает быть ключом-кандидатом. У файла может иметься несколько таких ключей. В приведенном ниже примере ключом-кандидатом файла АД- РЕСА-КЛИЕНТОВ служит поле НОМЕР. КЛИЕНТА: НОМЕР, КЛИЕНТА НАЗВАНИЕ АДРЕС 0391 УИЛКИНСОН 19 КОБЛ-СТРИТ 0403 СЭМСОН 31 ХАЙ-СТРИТ 0569 УИЛКИНСОН 31 ХАЙ-СТРИТ 0615 СЭМСОН 19 КОБЛ-СТРИТ Еще один ключ-кандидат образует пара полей НОМЕР. КЛИ- ЕНТА и АДРЕС. Соединение проекций proj НОМЕР. КЛИЕНТА, НАЗВАНИЕ (АДРЕСА. КЛИЕНТОВ) и proj НОМЕР. КЛИ- ЕНТА, АДРЕС (АДРЕСА. КЛИЕНТОВ) представляет собой полную декомпозицию, однако она не позволяет избавиться от , дублирования. Дело в том, что обе проекции содержат ключ- кандидат НОМЕР. КЛИЕНТА. Именно его присутствием и там и там объясняется нежелательный эффект. Заметим, что в об- суждавшемся выше примере полная декомпозиция файла ПОСТАВЩИКИ.ТОВАРОВ, с помощью которой устранялось дублирование данных, была сформирована как соединение двух проекций, общие поля которых не могли образовать ключ-кан- дидат. Можно доказать, что дублирование неизбежно, если проекции, порождающие полную декомпозицию, содержат общий для обеих ключ-кандидат исходного файла. Доказательство построим таким образом: покажем, что отсутствие дублирования противоречит предположению о наличии у проекций общего ключа-кандидата. 60
Рассмотрим файл ИМЯФАЙЛА с полями FX, FY и FZ. Пусть выполняется равенство ИМЯФАЙЛА = proj FX, FY (ИМЯ- ФАЙЛА) Join proj FY, FZ (ИМЯФАЙЛА), причем FY, являясь ключом-кандидатом, входит в состав обеих проекций, образу- ющих полную декомпозицию файла ИМЯФАЙЛА. В данном случае дублирование не исключено, поскольку файл ИМЯФАЙЛА может содержать такие записи, как FX FX FZ X Y Z X' Y Z. В этих записях пара значений Y и Z повторяется дважды. Налицо противоречие, поскольку предполагалось, что FY служит ключом-кандидатом, ввиду чего Y может встретиться максимум в одной записи. Этим и завершается доказательство. Оно практи- чески не изменилось бы, если FX, FY и FZ обозначали не одиноч- ные поля, а совокупность полей. В общем случае у файла может быть несколько полных де- композиций. Так, файл ПОСТАВЩИКИ. ТОВАРОВ имеет пол- ную декомпозицию, составленную из проекций proj ШИФР- ТОВАРА, НОМЕР.ТЕЛЕФОНА (ПОСТАВЩИКИ.ТОВАРОВ) и proj ШИФР.ТОВАРА, НАЗВАНИЕ (ПОСТАВЩИКИ.ТОВА- РОВ). Обе они содержат ключ-кандидат файла ПОСТАВЩИКИ. ТОВАРОВ, ввиду чего замена его приведенной декомпозицией не гарантирует отсутствия дублирования. Однако ранее для этого файла была составлена другая декомпозиция, в которой не было дублирования данных. В конечном итоге можно сделать следу- ющий вывод: если существует такая полная декомпозиция файла, которая образована проекциями, не имеющими общего ключа- кандидата, то замена исходного файла этой декомпозицией исклю- чает возможность дублирования информации. 3.4. ПРИСОЕДИНЕННЫЕ ЗАПИСИ Решая вопрос о том, что целесообразнее хранить — файл как таковой или его полную декомпозицию, следует при- нимать в расчет и проблему записей, получивших название «при- соединенных». Сначала рассмотрим пример подобной записи, а затем дадим точное определение. Вновь вернемся к файлу ПОСТАВЩИКИ.ТОВАРОВ, который обсуждался в разд. 3.3.1. Предположим, что в этот файл понадо- билось занести телефонный номер 6632 новой фирмы-поставщика, именуемой «Симеон», от которой не поступало еще никаких това- ров. Но поле ШИФР. ТОВАРА служит ключом, и его нельзя оставить пустым, иначе новую запись впоследствии невозможно будет идентифицировать. По этой причине номер телефона в файл ПОСТАВЩИКИ.ТОВАРОВ ввести не удастся. В то же время 61
его можно записать в проекцию proj НАЗВАНИЕ, НОМЕР. ТЕЛЕФОНА (ПОСТАВЩИКИ.ТОВАРОВ): НАЗВАНИЕ НОМЕР. ТЕЛЕФОНА ДЖЕКСОН 3692 УИЛСОН 5948 РОБСОН 2514 СИМСОН 6632 Записи, подобные последней, и называют присоединениями. Сформулируем теперь общее определение присоединенной за- писи. Пусть R и S —наборы полей файла ИМЯФАЙЛА, и вы- полняется условие ИМЯФАЙЛА = proj R (ИМЯФАЙЛА) join proj S (ИМЯ- ФАЙЛА). В качестве примера можно вновь взять файл ПОСТАВЩИКИ. ТОВАРОВ, отождествив с R пару полей (ШИФР.ТОВАРА, НАЗВАНИЕ), а с S — пару полей (НАЗВАНИЕ, НОМЕР.ТЕ- ЛЕФОНА). Запись является присоединенной, если она занесена в проекцию proj S (ИМЯФАЙЛА), но не входит в состав какой- либо записи proj R (ИМЯФАЙЛА) join proj S (ИМЯФАЙЛА), поскольку с ней невозможно образовать конкатенацию ни одной из записей проекции proj R (ИМЯФАЙЛА). В частности, proj ШИФР.ТОВАРА, НАЗВАНИЕ (ПОСТАВЩИКИ.ТОВАРОВ) не содержит записей, где фигурировало бы название «Симеон». По этой причине «Симеон» и номер телефона фирмы будут исклю- чены из результата соединения proj ШИФР.ТОВАРА, НАЗВАНИЕ (ПОСТАВЩИКИ. ТОВАРОВ) join proj НАЗВАНИЕ, НОМЕР.ТЕЛЕФОНА (ПОСТАВЩИКИ. ТОВАРОВ). Если присоединенная запись занесена в proj S (ИМЯФАЙЛА), последняя перестает быть точной проекцией ИМЯФАЙЛА. Тем не менее основное ее содержание не меняется, и поэтому будем обозначать ее по-прежнему proj S (ИМЯФАЙЛА). Вернемся к проблеме запоминания телефона фирмы «Симеон», которая еще не успела приступить к поставке товаров. В подоб- ных обстоятельствах проблема решается с помощью присоеди- ненных записей, которые можно хранить не в исходном файле, а в его полной декомпозиции. Возможны, однако, такие ситуации, когда использование полной декомпозиции для запоминания присоединенных записей лишено смысла. Как и ранее, все решает наличие или отсутствие общего ключа- кандидата. Если в состав обеих проекций, образующих полную декомпозицию ИМЯФАЙЛА, входит ключ-кандидат, проблемы присоединенных записей практически не существует. Предполо- жим, например, что в файл АДРЕСА. КЛИЕНТОВ (см. разд. 3.3.2) потребовалось поместить название новой фирмы — «Хобсон», 62
а также ее телефон — 0643, не указывая в то же время ее адреса. Эту информацию в виде записи 0643 ХОБСОН можно занести в проекцию proj НОМЕР. КЛИЕНТА, НАЗВАНИЕ (АДРЕСА.КЛИЕНТОВ). Можно поступить и по-иному, поместив в файл АДРЕСА. КЛИЕНТОВ запись 0643 ХОБСОН пусто. Словом «пусто» здесь обозначено поле, не содержащее ни- какого значения. Учитывая, что это поле не используется как ключ, в данном случае правила не нарушены. Таким образом, запись 0643 ХОБСОН по существу не является присоединенной, поскольку ее можно поместить, объединив с пустым полем, непосредственно в исход- ный файл. Чтобы обосновать правильность последней операции, доста- точно занести в проекцию proj НОМЕР. КЛИЕНТА, АДРЕС (АДРЕСА. КЛИЕНТОВ) вспомогательную запись 0643 пусто. Ее конкатенация с записью 0643 ХОБСОН из proj НОМЕР. КЛИЕНТА, НАЗВАНИЕ (АДРЕСА. КЛИЕН- ТОВ) дает запись 0643 ХОБСОН пусто в файле АДРЕСА. КЛИЕНТОВ, который восстанавливается в ре- зультате соединения двух указанных проекций. Итак, можно считать, что в рассмотренном примере присоединенные записи отсутствуют. Встанем теперь на формальную позицию и покажем более строго, что проблема присоединенных записей не существует, если в проекциях, образующих полную Декомпозицию некоторого файла, присутствуют поля, составляющие его ключ-кандидат. Пусть этими свойствами по отношению к файлу ИМЯ ФАЙЛА обладают две проекции, proj R (ИМЯФАЙЛА) и proj S (ИМЯ- ФАЙЛА). Дополним proj S (ИМЯФАЙЛА) еще одной записью, которую обозначим s, и предположим, что ни одно из полей s, входящих в ключ-кандидат, не является пустым. Проекцию proj R (ИМЯФАЙЛА) также дополним специально сформированной записью г. Все поля этой вспомогательной за- писи, присутствующие одновременно в наборах R и S, по своему содержанию идентичны полям записи s, в то время как остальные пусты. Введение г в проекцию proj R (ИМЯФАЙЛА) не противо- речит правилам, поскольку в силу своей структуры г включает ключ-кандидат, причем ни одно из его полей в этой записи не пусто. Очень важно также, что новая запись, не привнося ни- какой информации, помимо уже имеющейся в proj R (ИМЯФАЙ- ЛА), фактически не меняет содержимого последней. Завершим доказательство, осуществив соединение 63
proj R (ИМЯФАЙЛА) join proj S (ИМЯФАЙЛА). Результат этой операции включает конкатенацию г и s. Сле- довательно, s нельзя считать присоединенной записью, поскольку, согласно определению, при соединении проекций такая запись не может войти в результирующий файл. Таким образом, пока- зано, что присоединенные записи отсутствуют, если проекции, порождающие полную декомпозицию, содержат общий ключ- кандидат исходного файла. Ранее уже отмечалось, что файл может иметь не одну полную декомпозицию. Если во всех проекциях, из которых формируется каждая из таких декомпозиций, присутствует ключ-кандидат, бессмысленно хранить вместо исходного файла какую-то из его полных декомпозиций (речь, разумеется, идет лишь о проблеме присоединенных записей). Но если существует хотя бы одна полная декомпозиция, которую образуют проекции, не содержа- щие общего ключа-кандидата, то при переходе от нее к исходному файлу возможна (хотя и не обязательна) потеря присоединенных записей. 3.5. НОРМАЛИЗАЦИЯ 3.5.1. ПЯТАЯ НОРМАЛЬНАЯ ФОРМА Говорят, что файл находится в пятой нормальной форме, если не существует ни одной его полной декомпозиции, в которую входили бы проекции, не имеющие общего ключа- кандидата. Можно сформулировать это условие й более строго: файл представлен в пятой нормальной форме тогда и только тогда, когда в каждой его полной декомпозиции все проекции содержат ключ-кандидат. Считается, что файл, не обладающий ни одной полной декомпозицией, также имеет пятую нормаль- ную форму. Предшествующий анализ позволяет сделать вывод, что замена .файла в пятой нормальной форме любой его полной декомпози- цией не дает никаких преимуществ в смысле устранения дубли- рования информации и сохранения присоединенных записей. С другой стороны, если файл не находится в пятой нормальной форме, то имеется возможность избежать дублирования и потери присоединенных записей, перейдя от файла к такой его полной декомпозиции, которая образована проекциями, не содержащими общего ключа-кандидата. Если составляющие такую декомпози- цию проекции, в свою очередь, не являются файлами в пятой нормальной форме, то каждую из них также можно заменить полной декомпозицией, сформированной из проекций в пятой нормальной форме. Подобный процесс последовательного перехода к полным декомпозициям называют нормализацией. Основная цель нормализации — добиться того, чтобы не дублировались данные и не исчезали присоединенные записи. 64
3.5,2. УПРАЖНЕНИЯ В каждом из упражнений 1—6 совместно с исходным файлом дается одна или несколько его полных декомпозиций, а. Во всех упражнениях необходимо нормализовать файл. Возможно, для этого потребуется заменить его подходящей полной декомпозицией из числа приведенных. Достаточно указать, кото- рую из декомпозиций следует выбрать для этой цели. Если же файл предположительно уже находится в пятой нормальной форме, следует доказать, что это действительно так. Можно счи- тать, что, помимо представленных, других декомпозиций, способ- ных заместить файл, не существует. б. Предлагается во всех случаях, когда исходный файл не приведен еще к пятой нормальной форме, дать пример нормали- зации, сокращающей дублирование, а также найти нормализа- цию, которая позволила бы сохранить присоединенную запись, выпадающую из ненормализованного файла. 1. Представим себе подробный план парка, на котором от- дельно указано каждое растущее дерево. Все деревья снабжены индивидуальными номерами. Исходный файл в этом упражнении именуется ДЕРЕВЬЯ. В нем хранятся краткие сведения относи- тельно каждого дерева. Ниже приведены имена полей этого файла и четыре его записи. НОМЕР.ДЕРЕВА ПОРОДА ВЫСОТА ВЕЧНОЗЕЛЕНОЕ 1 БУК 21 НЕТ 2 ПАДУБ 9 ДА 3 БУК 23 НЕТ 4 ЯСЕНЬ 18 НЕТ Первая полная декомпозиция proj НОМЕР.ДЕРЕВА, ПОРОДА, ВЫСОТА (ДЕРЕВЬЯ), proj НОМЕР.ДЕРЕВА, ВЕЧНОЗЕЛЕНОЕ (ДЕРЕВЬЯ). Вторая полная декомпозиция proj НОМЕР.ДЕРЕВА, ПОРОДА (ДЕРЕВЬЯ), proj НОМЕР.ДЕРЕВА, ВЫСОТА (ДЕРЕВЬЯ), proj НОМЕР.ДЕРЕВА, ВЕЧНОЗЕЛЕНОЕ (ДЕРЕВЬЯ). Третья полная декомпозиция proj НОМЕР_ДЕРЕВА, ПОРОДА, ВЫСОТА (ДЕРЕВЬЯ), proj ПОРОДА, ВЕЧНОЗЕЛЕНОЕ (ДЕРЕВЬЯ). 2. Имя исходного файла — КОНФЕТЫ. Вот имена его полей и часть помещенных в нем записей: РЕЦЕПТ ИНГРЕДИЕНТ ГРАММЫ КАЛОРИИ. НА. ГРАММ ИРИС САХАР 450 3,7 ИРИС МАСЛО 225 7,8 ИРИС МУКА 5 3,5 ИРИС ПАТОКА 20 3,2 ТЯНУЧКА САХАР 450 3,7 ТЯНУЧКА МАСЛО 225 7,8 ТЯНУЧКА СГУЩЕННОЕ. МОЛОКО 400 4,5 3 Ульман Дж. 65
Первая полная декомпозиция proj РЕЦЕПТ, ИНГРЕДИЕНТ, ГРАММЫ (КОНФЕТЫ), proj ИНГРЕДИЕНТ, КАЛОРИИ. НА. ГРАММ (КОНФЕТЫ). Вторая полная декомпозиция proj РЕЦЕПТ, ИНГРЕДИЕНТ, ГРАММЫ (КОНФЕТЫ), proj РЕЦЕПТ, ИНГРЕДИЕНТ, КАЛОРИИ. НА. ГРАММ (КОНФЕТЫ). 3. В исходном файле, названном ВИЗИТЫ (VISITS), фикси- руются приезды людей в различные города и страны. Для про- стоты предполагается, что у всех визитеров разные фамилии и нет городов с одинаковыми названиями. Ниже указаны имена полей файла и приведено несколько его записей. ДАТА ФАМИ- ПРОФЕССИЯ ГОРОД СТРАНА (DATE) ЛИЯ (NAME) ДЖОНС (PROFESSION) (TOWN) (COUNTRY) 19920615 БУХГАЛТЕР ЭФТОН УАЙЛАНДИЯ 19920615 СМИТ ПРОГРАММИСТ СИТОН ЭКСЛАНДИЯ 19920617 СМИТ ПРОГРАММИСТ эйтон ЭКСЛАНДИЯ 19920620 СМИТ ПРОГРАММИСТ ЭФТОН УАЙЛАНДИЯ 19920620 НАЙТ ИНЖЕНЕР ДИТОН ЗЕДЛАНДИЯ 19920620 ЯНГ ИНЖЕНЕР СИТОН Первая полная декомпозиция ЭКСЛАНДИЯ proj ДАТА, ФАМИЛИЯ, ПРОФЕССИЯ, ГОРОД (ВИЗИТЫ), proj ГОРОД, СТРАНА (ВИЗИТЫ). Вторая полная декомпозиция proj ДАТА, ФАМИЛИЯ, ГОРОД, СТРАНА (ВИЗИТЫ), proj ФАМИЛИЯ, ПРОФЕССИЯ (ВИЗИТЫ). Третья полная декомпозиция proj ДАТА, ФАМИЛИЯ, ГОРОД (ВИЗИТЫ), proj ГОРОД, СТРАНА (ВИЗИТЫ), proj ФАМИЛИЯ, ПРОФЕССИЯ (ВИЗИТЫ). 4. Исходный файл, названный ПОЕЗДКИ, содержит записи о междугородных автобусных перевозках. Переезд из одного города в другой всегда совершается по неизменному маршруту, причем в день этим путем проезжает не более одного автобуса. В шестом поле файла отмечается продолжительность поездки. Имена полей и образцы записей приведены ниже. ОТКУДА УИНКЛБИ УИНКЛБИ коклтон КУДА РАССТОЯ- ДАТА НИЕ КОКЛТОН 62 19930305 КОКЛТОН 62 19930306 МАСЛГРОВ 62 19930306 Первая полная декомпозиция ВОДИТЕЛЬ МАРШАЛЛ АРНОЛЬД МАРШАЛЛ ВРЕМЯ 3,4 2,8 4,1 proj ОТКУДА, КУДА, ДАТА, ВОДИТЕЛЬ, ВРЕМЯ (ПОЕЗДКИ), proj ОТКУДА, КУДА, РАССТОЯНИЕ (ПОЕЗДКИ). Вторая полная декомпозиция proj ОТКУДА, КУДА, ДАТА, ВОДИТЕЛЬ (ПОЕЗДКИ), prrj ОТКУДА, КУДА, ДАТА, ВРЕМЯ (ПОЕЗДКИ), proj ОТКУДА, КУДА, РАССТОЯНИЕ (ПОЕЗДКИ). 5. Исходный файл ШАХМАТЫ содержит записи, в которых указаны дата встречи, фамилии участников и победитель, а также 66
продолжительность игры. Два конкретных шахматиста могут сыграть не более одной партии в день. Имена полей и часть запи- сей приведены ниже. ДАТА УЧАСТНИК-1 УЧАСТНИЮ ПОБЕДИТЕЛЬ ВРЕМЯ 19920502 ГРАМБИГ ПИВИЧ ПИВИЧ 3,6 19929592 ГРАМБИГ СМИТ СМИТ 2,5 19920503 ГРАМБИГ ПИВИЧ ПИВИЧ 1,4 19920503 СМИТ ПИВИЧ СМИТ 5,2 Полная декомпозиция proj ДАТА, УЧАСТНИК- 1, УЧАСТНИК- 2, ПОБЕДИТЕЛЬ (ШАХМАТЫ), proj ДАТА, УЧАСТНИК-1, УЧАСТНИК-2, ВРЕМЯ (ШАХМАТЫ). В данном случае имеется всего одна полная декомпозиция, которую и предстоит проанализировать. 6. В исходном файле ИСКИ содержится подробная информация об исках, представленных страховым компаниям. Названия этих компаний не повторяются. По данному страховому полису может быть возбуждено несколько исков, но не более одного в день. Любой клиент может иметь несколько полисов. Ниже приведен перечень полей файла ИСКИ. ФИРМА АДРЕС. ФИРМЫ НОМЕР. ПОЛИСА (Название страховой компании} {Адрес страховой компании) ДАТАСТРАХ КЛАСС СУММАСТРАХ {Вид покрытия} ДАТА. ИСКА СУММА. ИСКА НОМЕР. КЛИЕНТА ИМЯ-КЛИЕНТА {Фамилия страхователя или название фирмы-клиента} АДРЕС. КЛИЕНТА Первая полная декомпозиция proj ФИРМА, АДРЕС. ФИРМЫ, НОМЕР. ПОЛИСА, ДАТ АСТРАХ, НОМЕР.КЛИЕНТА,.КЛАСС, СУММАСТРАХ (ИСКИ), proj НОМЕР. ПОЛИСА, ИМЯ-КЛИЕНТА, АДРЕС. КЛИЕНТА, ДАТА. ИСКА, СУММА.ИСКА (ИСКИ). Вторая полная декомпозиция proj ФИРМА, АДРЕС.ФИРМЫ (ИСКИ), proj НОМЕР. КЛИЕНТА, ИМЯ-КЛИЕНТА, АДРЕС. КЛИЕНТА (ИСКИ), proj НОМЕР. ПОЛИСА, ФИРМА, НОМЕР. КЛИЕНТА, ДАТ АСТР АХ, КЛАСС, СУММАСТРАХ (ИСКИ), proj НОМЕР.ПОЛИСА, ДАТА.ИСКА, СУММА.ИСКА (ИСКИ). 3.6. ФУНКЦИОНАЛЬНАЯ ЗАВИСИМОСТЬ 3.6.1. ОПРЕДЕЛЕНИЕ ФУНКЦИОНАЛЬНОЙ ЗАВИСИМОСТИ До сих пор молчаливо предполагалось, что существует некоторый критерий, позволяющий определить, образует данный набор проекций полную декомпозицию или не образует. Теперь 3* 67
настала пора разобраться, как практически можно выявить подобную декомпозицию. Весьма полезно в связи с этим познако- миться с понятием функциональной зависимости. Пусть X и Y — наборы, каждый из которых объединяет одно или несколько полей файла ИМЯФАЙЛА. Y находится в функци- ональной зависимости от X тогда и только тогда, когда с каждым данным значением X связано не более одного значения Y. По отношению к ИМЯФАЙЛА функциональная зависимость Y от X создает ограничение, выражающееся в том, что любые две записи этого файла, содержащие одинаковые значения X, должны обя- зательно включать и совпадающие значения Y. Ограничение это действует не только на записи, уже имеющиеся в ИМЯФАЙЛА, но и на те, что потенциально могут попасть в него в будущем. В качестве примера возьмем файл АДРЕСА- КЛИЕНТОВ из разд. 3.3.2. Положив X = НОМЕР-КЛИЕНТА, a Y = АД- РЕС, сразу же можно убедиться, что Y функционально засисит от X. Действительно, АДРЕСА» КЛИЕНТОВ удовлетворяют дан- ному выше определению уже потому, что ни в одной из записей значения НОМЕР,КЛИЕНТА не повторяются. Рассмотрим еще один, намеренно упрощенный, пример. Пусть файл содержит следующие четыре записи: Fl F2 a h b ! Очевидно, F2 не связано функциональной зависимостью с F1, поскольку значению b поля F1 соответствуют два различных значения поля F2. В то же время нельзя утверждать, что F1 функционально зависит от F2, исходя лишь из имеющихся запи- сей. Это допустимо только при том условии, что на файл допол- нительно наложено ограничение, запрещающее вводить в него такие новые записи, как, например, Fl F2 с к которая нарушает условие однозначного соответствия значений F2 значениям F1. Если X состоит из нескольких полей, то говорят, что Y нахо- дится в полной функциональной зависимости от X тогда и только тогда, когда: a) Y функционально зависит от X; б) Y функционально не зависит от любого X', где X' — такое подмножество X, что по меньшей мере одно поле из X не при- надлежит X7. 68
Иными словами, утверждать, что Y связан в X полной функ- циональной зависимостью, можно в том и только в том случае, если Y функционально зависит от X и не зависит от любого его подмножества, не совпадающего с X. Примером может служить файл ЗВЕРИ. В. НЕВОЛЕ из разд. 3.2. В нем поле ЗОНА.ОБИТАНИЯ функционально зависит от пары ЗООПАРК ЖИВОТНОЕ. Действительно, любой паре значений, например, ЭЙТОН КЕНГУРУ ЭЙТОН ВЕРБЛЮД соответствует ровно по одному значению поля ЗОНА.ОБИТА- НИЯ. Легко, однако, убедиться и в том, что ЗОНА.ОБИТАНИЯ функционально зависит также от поля ЖИВОТНОЕ. Совместно это означает, что ЗОНА.ОБИТАНИЯ не обладает полной функ- циональной зависимостью от пары ЗООПАРК, ЖИВОТНОЕ. Завершим раздел еще одним простым примером. Предполо- жим, что дан файл ЗАКАЗЫ со следующими полями: НОМЕР. ЗАКАЗА ШИФР. ТОВАРА {Шифр, присвоенный заказанному то- , вару) ОПИСАНИЕ {Описание товара) КОЛИЧЕСТВО (Заказанное количество единиц то- вара) Первичный ключ (и единственный ключ-кандидат) этого файла состоит из двух полей — НОМЕР.ЗАКАЗА и ШИФР.ТОВАРА. В одиночку каждое из указанных полей не способно играть роль ключа-кандидата, поскольку файл может содержать по несколько записей с одинаковыми значениями НОМЕР.ЗАКАЗА и с оди- наковыми значениями ШИФР.ТОВАРА. В то же время пара зна- чений НОМЕР.ЗАКАЗА и ШИФР.ТОВАРА однозначно иден- тифицирует конкретную запись. Следовательно, этой паре соот- ветствует единственное значение поля КОЛИЧЕСТВО, из чего можно заключить, что КОЛИЧЕСТВО связано полной функци- ональной зависимостью с полем НОМЕР.ЗАКАЗА и ШИФР.ТО- ВАРА. Действительно, условие б) выполняется: не исключено появление записей с повторяющимися значениями полей НО- МЕР.ЗАКАЗА и ШИФР.ТОВАРА, вследствие чего поле КОЛИ- ЧЕСТВО не зависит функционально ни от того, ни от другого в отдельности. С другой стороны, поле ОПИСАНИЕ функционально зависит от поля ШИФР.ТОВАРА. Из этого следует, что ОПИСАНИЕ не находится в полной функциональной зависимости от ключа- кандидата НОМЕР.ЗАКАЗА, ШИФР.ТОВАРА. 69
3,6.2. ТЕОРЕМА ХИТА Посвятим этот раздел доказательству теоремы Хита, устанавливающей связь между функциональной зависимостью и полной декомпозицией файла. Рассмотрим файл ИМЯФАЙЛА, в котором выделим три набора полей: Н, J и К таких, что каждое поле ИМЯФАЙЛА принадлежит лишь какому-то одному из этих трех наборов. Теорема Хита. Если К функционально зависит от J, то спра- ведливо тождество ИМЯФАЙЛА = proj Н, J (ИМЯФАЙЛА) join proj J, К (ИМЯФАЙЛА). Это означает, что при наличии функциональной зависимости К от J проекции proj Н, J (ИМЯФАЙЛА) и proj J, К, (ИМЯ- ФАЙЛА) образуют полную декомпозицию исходного файла. Доказательство теоремы. Возможные значения Н, J и К будем обозначать соответственно h и h', j и j', к и к'. Каждая из ука- занных величин фактически может представлять собой совокуп- ность значений нескольких полей. Начнем доказательство с того, что введем вспомогательный файл ИМЯ Ф АЙ ЛА 1: ИМЯФАЙЛА1 = proj Н, J (ИМЯФАЙЛА) join proj J, К (ИМЯФАЙЛА). Возьмем произвольную запись hjk, входящую в ИМЯФАЙЛА. Очевидно, hj принадлежит проекции proj Н, J (ИМЯФАЙЛА), a jk — проекции proj J, К (ИМЯФАЙЛА). Исходя из определения ИМЯФАЙЛА1 и свойств операции соединения, можно заклю- чить, что запись hjk входит в файл ИМЯФАЙЛА1. Следовательно, каждая запись ИМЯФАЙЛА является одновременно и за- писью ИМЯ ФАЙЛА 1. Далее рассмотрим произвольную запись h'j'k' из файла ИМЯФАЙЛА1. Согласно определению файла ИМЯФАЙЛА1 вы- полняется равенство proj Н, J (ИМЯФАЙЛА1) = proj Н, J (ИМЯФАЙЛА). Отсюда можно сделать вывод, что в ИМЯФАЙЛА присут- ствует запись h'j'k'. Аналогичным образом равенство proj J, К (ИМЯФАЙЛА1) = proj J, К (ИМЯФАЙЛА) позволяет утверждать, что ИМЯФАЙЛА содержит запись hj'k'. Поскольку К функционально зависит от J, в записи h'j'k' и hj'k' входит одно и то же значение j'. Следовательно, к = к' и h'j'k — = h'j'k'. Последнее означает, что каждая запись ИМЯФАЙЛА1 присутствует в ИМЯФАЙЛА. Учитывая, что выполняется и обратное, можно сделать вывод, что эти два файла тождественны, т. е. ИМЯФАЙЛА = ИМЯФАЙЛА1. Этим и завершается дока- зательство теоремы Хита. 70
3.7. НОРМАЛИЗАЦИЯ НА ОСНОВЕ ФУНКЦИОНАЛЬНОЙ ЗАВИСИМОСТИ 3.7.1. ПЕРВАЯ НОРМАЛЬНАЯ ФОРМА Теорема Хита создает основу для построения различ- ных полных декомпозиций, которые образуются из проекций, не содержащих общего для всех них ключа-кандидата. Начнем с того, что проанализируем условия, определяющие принадлеж- ность файла к определенной нормальной форме, последовательно переходя от первой формы ко второй, затем к третьей и т. д. Наиболее общими свойствами среди нормальных форм обла- дает первая, с которой и начнем обсуждение. Файл находится в первой нормальной форме тогда и только тогда, когда ни одна из его записей не содержит в любом своем поле более одного значения и ни одно из ее ключевых полей не пусто. В качестве примера возьмем файл СОСТАВ.АНСАМБЛЯ. В нем два поля: НОМАНС УЧАСТНИКИ {Номер ансамбля) {Номера, присвоенные музыкантам, играющим в ансамбле) Предположим, дующие данные: что СОСТАВ-АНСАМБЛЯ включает сле- НОМАНС УЧАСТНИКИ Е1 Р1, Р8, Р9 Е2 РЗ, Р5 ЕЗ Р9 Е4 Р1, Р4, Р7 Е5 Р5, Р9 Е6 Pl, Р7 Е7 РЗ, Р5 Большинство записей файла содержат по несколько номеров в поле УЧАСТНИКИ — следовательно, это не первая нормальная форма. В то же время все музыкальные файлы из разд. 2.2 имеют первую нормальную форму, поскольку в каждом поле любой их записи помещается ровно по одному значению. Заметим, что строки символов, подобные записанным в поле АДРЕС файла АДРЕСА. КЛИЕНТОВ, рассматриваются как отдельные зна- чения. 3.7.2. ВТОРАЯ НОРМАЛЬНАЯ ФОРМА Как пояснялось в разд. 1.2.3, первичным ключом при- нято называть наиболее удобный для доступа к записям файла ключ-кандидат. Например, фирма «Типико» в качестве первичного ключа файла КЛИЕНТЫ использует поле НОМЕР. КЛИЕНТА. Файл считается представленным во второй нормальной форме в том и только в том случае, если все его поля, не входящие в пер- вичный ключ, связаны полной функциональной зависимостью с первичным ключом. 71
В частности, файл ЗВЕРИ_В_НЕВОЛЕ из разд. 3.2 не может быть отнесен ко второй нормальной форме, поскольку поле ЗОНА. ОБИТАНИЯ функционально зависит от поля ЖИВОТНОЕ и тем самым не находится в полной функциональной зависимости от первичного ключа, образуемого парой ЗООПАРК, ЖИВОТ- НОЕ. Описанный в разд. 3.3.1 файл ПОСТАВЩИКИ.ТОВАРОВ, напротив, удовлетворяет определению второй нормальной формы, так как его поля НАЗВАНИЕ, НОМЕР.ТЕЛЕФОНА функци- онально полностью зависят от первичного ключа, которым служит ШИФР.ТОВАРА. Первичный ключ файла ЗАКАЗЫ из разд. 3.6.1 состоит из двух полей: НОМЕР.ЗАКАЗА и ШИФР.ТОВАРА. В то время как поле КОЛИЧЕСТВО находится в полной функциональной зависимости от первичного ключа, поле ОПИСАНИЕ просто зависит от него, и поэтому файл ЗАКАЗЫ не представлен во второй нормальной форме. Легко показать, что любой файл, не отвечающий определению второй нормальной формы, не может находиться и в пятой нор- мальной форме. Предположим, что таким файлом является ИМЯ- ФАЙЛА. Набор из одного или нескольких неключевых полей ИМЯФАЙЛА, функционально зависящих от первичного ключа, обозначим К, а подмножество полей первичного ключа, от кото- рых К функционально зависит полностью, — J. Совокупность остальных полей ИМЯФАЙЛА, не входящих в К или J, назо- вем Н. Согласно теореме Хита, указанная функциональная зави- симость влечет тождество: ИМЯФАЙЛА = proj Н, J (ИМЯФАЙЛА join proj J, К (ИМЯ- ФАЙЛА). Проекции в правой части тождества не имеют общего ключа- кандидата. Следовательно, ИМЯФАЙЛА не находится в пятой нормальной форме и подлежит поэтому нормализации. Имеется в виду, что его следует заменить полной декомпозицией, объединя- ющей проекции в пятой нормальной форме. Для файла ЗВЕРИ. В.НЕВОЛЕ такую декомпозицию можно составить, в частности, из проекций proj ЗООПАРК, ЖИВОТНОЕ (ЗВЕРИ. В. НЕ- ВОЛЕ) и proj ЖИВОТНОЕ, ЗОНА.ОБИТАНИЯ (ЗВЕРИ. В.НЕВОЛЕ). Нормализация файла ЗАКАЗЫ осуществляется путем разложения на проекции proj НОМЕР.ЗАКАЗА, ШИФР. ТОВАРА, КОЛИЧЕСТВО (ЗАКАЗЫ) и proj ШИФР.ТОВАРА, ОПИСАНИЕ (ЗАКАЗЧ). 3.7.3. ТРЕТЬЯ НОРМАЛЬНАЯ ФОРМА Файл представлен в третьей нормальной форме, если и только если он удовлетворяет определению второй нормальной формы и ни одно из его неключевых полей не зависит функци- онально от любого другого неключевого поля. 72
Если ввести ограничение, запрещающее записывать в файл ПОСТАВЩИКИ-ТОВАРОВ из разд. 3.3.1 фирмы-поставщики с одинаковыми названиями, он потеряет третью нормальную форму, поскольку его поле НОМЕР.ТЕЛЕФОНА станет связано функциональной зависимостью с полем НАЗВАНИЕ, не явля- ющимся ключом. В то же время, благодаря тому, что поле АДРЕС файла АДРЕСА. КЛИЕНТОВ функционально не зависит от поля НАЗВАНИЕ, этот файл имеет третью нормальную форму. Можно доказать, что любой файл ИМЯФАЙЛА, который не представлен в третьей нормальной форме, не может находиться в пятой нормальной форме. Для этого определим К как набор неключевых полей, обладающих функциональной зависимостью от другого набора неключевых полей, которое обозначим J. Вве- дем также совокупность полей Н файла ИМЯФАЙЛА, не при- надлежащих J или К- Наличие указанной функциональной зави- симости согласно теореме Хита приводит к тождеству ИМЯФАЙЛА = proj Н, J (ИМЯФАЙЛА) join proj J, К, (ИМЯФАЙЛА). Проекции в правой части тождества не имеют общего ключа- кандидата. Следовательно, ИМЯФАЙЛА не находится в пятой нормальной форме и его следует нормализовать, заменив этими проекциями. Так, например, файл ПОСТАВЩИКИ-ТОВАРОВ (разд. 3.3.1) можно нормализовать, замещая его проекциями proj ШИФР. ТОВАРА, НАЗВАНИЕ (ПОСТАВЩИКИ-ТОВАРОВ) и proj НАЗВАНИЕ, НОМЕР. ТЕЛЕФОНА (ПОСТАВЩИКИ- ТОВАРОВ). 3.7.4. НОРМАЛЬНАЯ ФОРМА БОЙСА—КОДДА Следующим шагом по сравнению с третьей нормаль- ной формой является нормальная форма Бойса — Кодда (НФБК). По определению, файл находится в НФБК, если и только если любая функциональная зависимость между его полями сводится к полной функциональной зависимости от ключа-кандидата. Примером файла, который представлен в третьей нормальной форме, но не имеет НФБК, может служить файл СТОРОЖА. ЗООПАРКОВ: ЗООПАРК ЖИВОТНОЕ СТОРОЖ ЭЙТОН КЕНГУРУ ПОНСОНБИ эйтон ВЕРБЛЮД ПОНСОНБИ БИТСН ЭМУ КАРУЗЕРС БИТОН ВЕРБЛРЭД ГЕРДЛСТОН Пара ЗООПАРК, ЖИВОТНОЕ составляет ключ-кандидат, от которого поле СТОРОЖ находится в полной функциональной зависимости. Однако из-за того, что поле ЗООПАРК, входящее в ключ-кандидат, функционально зависит от поля СТОРОЖ, данный файл не нахсднтся в НФБК. 73
Для того чтобы доказать, что любой файл, не имеющий НФБК, не может находиться в пятой нормальной форме, рассмотрим произвольный файл ИМЯФАЙЛА. Пусть К — набор полей этого файла, связанный полной функциональной зависимостью с дру- гим набором полей J, который не является ключом-кандидатом. Как и в предыдущих случаях, введем совокупность полей Н, не принадлежащих J или К. В силу наличия функциональной зависимости из теоремы Хита следует, что ИМЯФАЙЛА = pro] Н, J (ИМЯФАЙЛА) join proj J, К, (ИМЯФАЙЛА). Две проекции в правой части тождества не содержат общего ключа-кандидата, ввиду чего можно утверждать, что ИМЯФАЙЛА не представлен в пятой нормальной форме и может быть нормали- зован путем разложения на свои проекции. Файл СТОРОЖА. ЗООПАРКОВ, например, можно нормализовать, заменив его проекциями proj ЖИВОТНОЕ, СТОРОЖ (СТОРОЖА.ЗОО- ПАРКОВ) и proj СТОРОЖ, ЗООПАРК (СТОРОЖА.ЗООПАР- КОВ). 3.8. УПРАЖНЕНИЯ Анализируя файлы, заданные в каждом из упражне- ний 1—5, необходимо решить следующие задачи: а) перечислить все имеющиеся функциональные зависимости между полями. (Чтобы составить такой перечень, следует сначала выяснить, не связано ли каждое поле функциональными зависи- мостями со всеми из оставшихся полей по отдельности, затем со всевозможными парами из числа оставшихся полей и т. д.); б) полагая, что множество функциональных зависимостей ис- черпывается найденными, нормализовать файл, т. е. заменить его проекциями в НФБК; в) привести один пример, в котором за счет нормализации сокращается дублирование, и еще один пример, показывающий, как благодаря нормализации удается сохранить присоединенную запись, которая была бы исключена из ненормализованного файла. 1. Дано содержимое файла, имеющего поля со следующими именами: ПОМЕСТЬЕ САДОВЫЕ. ЦВЕТЫ ГЕЙБЛЗ НАРЦИССЫ ГЕЙБЛЗ РОЗЫ козикот колокольчики КОЗИКОТ РОЗЫ СЕЗОН. ЦВЕТЕНИЯ ВЕСНА ЛЕТО ВЕСНА ЛЕТО 2. Дано содержимое файла, имеющего поля со следующими именами: 74
ВИД.СПОРТА ПОБЕДИТЕЛЬ ГОД-РОЖДЕНИЯ- ПОБЕДИТЕЛЯ ПРЫЖКИ В ДЛИНУ АРМСТРОНГ 1972 БЕГ НА 100 м МАРШАЛЛ 1969 100 М С БАРЬЕРАМИ МАРШАЛЛ 1969 ПРЫЖКИ С ШЕСТОМ УИЛЬЯМС 1969 3. Дано содержимое файла, имеющего поля со следующими именами: ФАМИЛИЯ НАПИТОК КОЛИЧЕСТВО. ЦЕНА. ЗА. ПОРЦИЮ ПОРЦИИ АРМСТРОНГ виски 3 40 АРМСТРОНГ ХЕРЕС 1 30 БЕК виски 1 40 НАЙТ ХЕРЕС 2 30 содержимое файла. имеющего поля со следующими 4. Дано именами: ВЛАДЕЛЕЦ. ДАТА. РОЖДЕ- №. РЕГИСТРАЦИИ ДАТА.РЕГИ- АВТОМОБИЛЯ НИЯ СТРАЦИИ АРМСТРОНГ июнь 1960 АНС134Т июнь 1979 АРМСТРОНГ июнь 1960 BCY529 май 1980 БЕК май 1959 AHD339H октябрь 1972 НАЙТ июль 1961 ОУУ796Р январь 1976 имеющего поля со 5. Дано содержимое файла, именами: следующими НОМЕР. ДОРОГИ ПРОТЯЖЕННОСТЬ ГОРОД НАСЕЛЕНИЕ АЗ 352 АРБИ 25632 АЗ 352 ТИТОН 62310 А4 219 АРБИ 25632 А4 219 ЭСФИЛД 25632 Для простоты можно считать, что в файле нет двух городов е одинаковыми названиями. 3.9. ЧЕТВЕРТАЯ НОРМАЛЬНАЯ ФОРМА Для того чтобы перейти от НФБК к следующей нор- мальной форме, необходимо несколько расширить условия тео- ремы Хита. Конструктивное определение четвертой нормальной формы, занимающей промежуточное положение между НФБК и пятой, основывается на понятии обобщенной функциональной зависимости. Здесь это понятие не вводится, поскольку требует привлечения достаточно объемистого материала. Сформулиро- ванный ниже вариант определения нельзя считать полноценным, так как в нем не содержится ответа на вопрос, образует ли данная пара проекций полную декомпозицию файла. Будем говорить, что файл представлен в четвертой нормальной форме, если и только если каждая его полная декомпозиция из 75
двух проекций такова, что обе проекции не содержат общего ключа-кандидата. Сконструируем пример файла, который, будучи в НФБК, не находится в четвертой нормальной форме. Для этого несколько преобразуем структуру музыкального файла ИСПОЛНЕНИЯ так, чтобы в него можно было записывать номера всех сочинений (НОМСОЧ), исполнявшихся на каждом концерте. Кроме того, заменим поле НОМАНС, в которое будут заноситься номера всех исполнителей, выступавших на каждом концерте. Сразу же можно отметить, что модернизированный файл не отвечает требованиям, предъявляемым к первой нормальной форме, поскольку его поля НОМСОЧ и НОМИСП могут содержать по нескольку значений. ДАТА ГОРОД СТРАНА НОММУЗ НОМСОЧ НОМИСП 19860530 титон ТИЛАНДИЯ М9 С1, СЗ, С5 Р8, Р9 19860615 АРБИ ЭКСЛАНДИЯ Ml СЗ, С4, С6 Р1, Р4, Р7 и т. д. Можно получить эквивалентную нормальную форму (по край- ней мере, первую), соединив два файла, которые назовем НА- БОР. СОЧ и НАБОР.ИСП. Первый из них должен содержать следующую информацию: ДАТА ГОРОД СТРАНА НОМСОЧ 19860530 титон ТИЛАНДИЯ С1 19860530 титон ТИЛАНДИЯ СЗ 19860530 титон ТИЛАНДИЯ С5 19860615 АРБИ ЭКСЛАНДИЯ СЗ 19860615 АРБИ ЭКСЛАНДИЯ С4 19860615 АРБИ ЭКСЛАНДИЯ С6 и т. д. В этом файле (он имеет первую нормальную форму) указаны номера всех произведений, исполненных на каждом концерте. Файл НАБОР. ИСП, также представленный в первой нормальной форме, позволяет выяснить, что за исполнители участвовали в каж- дом концерте. Его содержимое таково: ДАТА ГОРОД СТРАНА НОММУЗ НОМИСП 19860530 титон ТИЛАНДИЯ М9 Р8 19860530 титон ТИЛАНДИЯ М9 Р9 19860615 АРБИ ЭКСЛАНДИЯ Ml Р1 19860615 АРБИ ЭКСЛАНДИЯ Ml Р4 19860615 АРБИ ЭКСЛАНДИЯ Ml Р7 И т. д. Соединению файлов НАБОР.СОЧ и НАБОР. ИСП присвоим имя НОВИСПОЛНЕНИЯ- Операция соединения осуществляется по трем полям — ДАТА, ГОРОД и СТРАНА. Ключевыми полями файла НОВИСПОЛНЕНИЯ являются ДАТА, ГОРОД, СТРАНА, 76
НОМИСП и НОМСОЧ, так что он, очевидно, представлен в НФБК- Однако НОВИСПОЛНЕНИЯ не находится в четвертой нормальной форме, хотя его проекции НАБОР,СОЧ и НАБОР. ИСП образуют полную декомпозицию. Эти проекции имеют общие поля ДАТА, ГОРОД и СТРАНА, которые составляют лишь часть ключа-кандидата, и поэтому любое сочетание значений этих трех полей может присутствовать в файле НОВИСПОЛНЕ- НИЯ произвольное число раз. Файл, не представленный в четвертой нормальной форме, не имеет и пятой нормальной формы, ввиду чего может быть норма- лизован. По отношению к файлу НОВИСПОЛНЕНИЯ, например, это означает, что его следует заменить проекциями НАБОР.СОЧ и НАБОР.ИСП. Для того чтобы можно было выявить принадлежность конкрет- ного файла к четвертой нормальной форме, необходимо иметь способ определения полных декомпозиций. В тех случаях, когда файл, как в примере с НОВИСПОЛНЕНИЯ, либо уже записан, либо наверное может быть записан в виде соединения двух про- екций, распознавание полной декомпозиции не составляет труда. В определении пятой нормальной формы, данном в разд. 3.5.1. число проекций, образующих полную декомпозицию файла и со- держащих общий ключ-кандидат, не лимитируется. Очевидно, четвертая нормальная форма представляет специальный случай пятой, когда полная декомпозиция должна быть соединением ровно двух проекций. Весьма не просто подобрать реальный файл, который нахо- дился бы в четвертой, но не был бы в пятой нормальной форме. Как уже говорилось, нормализация, в сущности, сводится к полу- чению файлов в пятой нормальной форме. На практике, приведя все файлы к НФБК, с большой гарантией можно утверждать, что они имеют и пятую нормальную форму. Разумеется, этот факт нуждается в проверке, и при возможности ее всегда следует вы- полнять. Наиболее просто проводить нормализацию поэтапно. Сначала все неприведенные файлы заменяются соответствующими проек- циями в третьей нормальной форме. Затем полученные файлы подвергаются проверке на предмет выполнения требований, предъ- являемых к НФБК- Те файлы, что не находятся в этой форме, приводятся к ней. После этого осуществляется проверка на нали- чие четвертой нормальной формы. Как правило, обнаружить, что условия существования четвертой формы не выполняются, бы- вает очень нетрудно. Далее следовало бы удостовериться, не оказались ли уже все файлы в пятой нормальной форме. К со- жалению, эффективного алгоритма проверки для этого этапа нормализации пока не предложено. 77
3.10. ОБЪЕКТЫ И АТРИБУТЫ 3.10.1. ФУНКЦИОНАЛЬНАЯ ЗАВИСИМОСТЬ АТРИБУТОВ ОТ ОБЪЕКТОВ Вполне естественно возникает вопрос — как опреде- лить, какие файлы необходимы и какие поля должны в них по- мещаться? Пытаясь ответить на него, можно начать с перечисле- ния имен полей в том порядке, в котором они располагаются внутри каждого отдельного файла. Далее можно проверить, не связано ли каждое поле функциональной зависимостью с наборами других полей и, последовательно применяя теорему Хита, сфор- мировать в конечном итоге совокупность проекций в НФБК. Отправным пунктом может, например, служить такой перечень полей: НОМЕР, СЧЕТА ДАТА, ПРЕДЪЯВЛЕНИЯ- СЧЕТА НОМЕР, НАРЯДА ШИФР, ТОВАРА КОЛИЧЕСТВО. ЕДИНИЦ ПЛАТА, ЗА, ДОСТАВКУ ОБЩАЯ-СУММА НОМЕР, КЛИЕНТА ИМЯ-ПОКУПАТЕЛЯ АДРЕС. ДЛЯ- РАСЧЕТОВ ПРЕДЕЛЬНЫЙ- РАЗМЕР. КРЕДИТА ТОРГОВАЯ, ЗОНА Выявление всех функциональных зависимостей даже в этом, сравнительно небольшом, списке представляет весьма трудоемкую задачу, не говоря уж о более протяженных. Предпочтительнее иной подход, в основе которого лежит следующее свойство первичного ключа файла: либо он идентифи- цирует объект, либо определяет соотношение между объектами. Под объектом понимается нечто материальное, например товар, хранящийся на складе, или некоторый признак, например цвет предмета. Чаще всего файлы используются как носители ин- формации об объектах и их атрибутах. В таких файлах неключевые поля являются атрибутами объектов, идентифицируемых первич- ными ключами. Примером могут служить поля файла АДРЕСА- КЛИЕНТОВ, обсуждавшегося в разд. 3.3.2: НАЗВАНИЕ и АДРЕС являются атрибутами клиентов, которые однозначно определяются значениями первичного ключа НОМЕР-КЛИЕНТА. Файлы могут применяться и как описания соотношений между объектами в тех случаях, когда необходимо хранить атрибут подобных соотношений и/или когда эти файлы служат связками при выполнении операций соединения файлов, содержащих ин- формацию об объектах. Проиллюстрируем эти положения на примере музыкальных файлов. 78
Файл АНСАМБЛИ содержит такие атрибуты каждого ан- самбля, как его название, страна, где он был организован, его руководитель. Сами ансамбли представляют объекты, идентифи- цируемые первичным ключом НОМАНС. В файле МУЗЫКАНТЫ имя музыканта, дата его рождения и страна, где он родился, представляют атрибуты музыканта, однозначно определяемого по первичному ключу НОММУЗ. Файл ИСПОЛНИТЕЛИ описывает соотношения между двумя объектами, которые идентифицируются полями НОММУЗ и ИНСТРУМЕНТ. Поле ОЦЕНКА — это атрибут соотношения между музыкантом и его инструментом, принимающий значения от СКВЕРНО до ОТЛИЧНО. Совместно поля НОММУЗ и ИН- СТРУМЕНТ образуют ключ-кандидат файла ИСПОЛНИТЕЛИ. Желательно иметь еще один ключ-кандидат для перекрестных ссылок, в связи с чем файл ИСПОЛНИТЕЛИ дополнен полем НОММУЗ, которое используется как первичный ключ. Файл УЧАСТНИКИ, АНСАМБЛЕЙ также описывает соот- ношение между двумя типами объектов — исполнителями и ан- самблями. Как можно выяснить из гл. 2, без этого файла трудно обойтись, хотя в нем и не содержится никаких атрибутов ука- занного соотношения. Их отсутствие означает, что оба поля файла УЧАСТНИКИ,АНСАМБЛЕЙ являются ключевыми. Инструмент в музыкальных файлах выступает как объект, однако никаких атрибутов этого объекта хранить не предполага- лось, в связи с чем специальный файл ИНСТРУМЕНТЫ не заводился. Воспользуемся всеми этими сведениями при анализе списка полей, помещенного в начале раздела. Попытаемся выяснить, какие понадобятся файлы и как распределятся поля по этим фай- лам. Начнем с того, что определим объекты, атрибуты которых должны храниться в файлах. Не вызывает сомнений, что такими объектами являются счета и клиенты. Очевидно, поле НОМЕР,СЧЕТА может служить первичным ключом и использоваться для идентификации счетов. Поля ДАТА, ПРЕДЪЯВЛЕНИЯ-СЧЕТА, ПЛАТА, ЗА, ДОСТАВ КУ, НО- МЕР-КЛИЕНТА и ОБЩАЯ-СУММА следует рассматривать как атрибуты счетов. В приведенном выше списке других атрибу- тов, связанных со счетами, не имеется. Отсюда можно сделать вывод, что первый из планируемых файлов должен обладать сле- дующей структурой: ЗАГОЛОВ КИ_ СЧЕТОВ НОМЕР, СЧЕТА ДАТА, ПРЕДЪЯВЛЕНИЯ- СЧЕТА НОМЕР» КЛИЕНТА ПЛАТА, ЗА- ПОСТАВКУ ОБЩАЯ-СУММА Имя файла} Первичный ключ файла) {Суммарная плата за доставку всея товаров} {Общая сумма выплат по счету) 79
Второй тип объектов представляют клиенты, идентифици- руемые первичным ключом НОМЕР. КЛИЕНТА. Данным объ- ектам в списке соответствуют атрибуты ИМЯ-ПОКУПАТЕЛЯ, АДРЕС. ДЛЯ-РАСЧЕТОВ, ПРЕДЕЛЬНЫЙ. РАЗМЕР- КРЕ- ДИТА и ТОРГОВАЯ-ЗОНА. Учитывая это, логично предло- жить следующую структуру для второго файла: КЛИЕНТЫ {Имя файла} НОМЕР. КЛИЕНТА ИМЯ-ПОКУПАТЕЛЯ АДРЕС. ДЛЯ. РАСЧЕТОВ ПРЕДЕЛЬНЫЙ. РАЗМЕР. КРЕДИТА ТОРГОВАЯ-ЗОНА Можно заметить, что ни в один из файлов не вошло поле КО- ЛИЧЕСТВО. ЕДИНИЦ, которое должно играть роль атрибута. Не исключено, что любой из товаров фигурирует в нескольких счетах, причем всякий раз КОЛИЧЕСТВО. ЕДИНИЦ может принимать различные значения. Следовательно, это поле функ- ционально не зависит от поля ШИФР.ТОВАРА и вообще оно не может рассматриваться как атрибут какого-либо из объектов. Однако оно все же является атрибутом, но не объекта, а соот- ношения между двумя объектами — счетом и товаром. Еще одним атрибутом того же соотношения служит НОМЕР. НАРЯДА. Это поле идентифицирует наряд на продажу, в котором записан данный товар, вследствие чего последний и был включен в счет. Принимая во внимание эти атрибуты, необходимо сформировать еще один файл следующей структуры: СОДЕРЖАНИЕ-СЧЕТОВ {Имя файла} НОМЕР. СЧЕТА ШИФР. ТОВАРА КОЛИЧЕСТВО. ЕДИНИЦ НОМЕР. НАРЯДА Поскольку в рассматриваемом примере нет ни одного атри- бута, характеризующего продаваемые товары, нет нужды заво- дить такой файл, как ТОВАРНЫЕ.ЗАПАСЫ. Вообще, помимо трех сформированных файлов, включивших все поля из приведен- ного в начале раздела списка, очевидно, никаких других не тре- буется. Отметим, что эти три файла находятся в НФБК и, более того, в пятой нормальной форме. Если бы поля НОМЕР.СЧЕТА и НОМЕР. КЛИЕНТА от- сутствовали в первоначальном списке, их в любом случае при- шлось бы ввести, чтобы обеспечить с помощью перекрестных ссы- лок связь между файлами. (Напомним, что с той же целью музы- кальный файл ИСПОЛНИТЕЛИ был дополнен полем НОМИСП. Приобретя опыт работы с файлами, можно оценить выигрыш, получаемый от введения дополнительных ссылок типа номера НОМИСП). 80
Итак, в результате анализа объектов, атрибутов и соотноше- ний между ними определены структуры файлов. Далее необходимо проверить, соблюдаются ли следующие требования: а) все файлы имеют НФБК (разумеется, предпочтительнее — пятую нормальную форму); б) соединение сформированных файлов включает все поля, подлежащие хранению. Если обнаружится, что указанные требования не выполняются, файлы нуждаются в перекомпоновке. Например, могло случиться так, что поле НОМЕР. КЛИЕНТА не было включено в файл ЗАГОТОВКИ.СЧЕТОВ. В этом случае из соединения трех файлов выпало бы поле ИМЯ-ПОКУПАТЕЛЯ- В подобных ситуациях весь анализ необходимо проводить заново. 3.10.2. ПОСТРОЕНИЕ НАБОРА ФАЙЛОВ ПО ЭМПИРИЧЕСКИМ ДАННЫМ В этом разделе обсуждаются несколько достаточно простых примеров, иллюстрирующих, каким образом производится формирование файлов и закрепление за ними полей в тех случаях, когда постановка задачи носит эмпирический характер. Футбольные состязания. Требуется подобрать выразительные имена файлов и полей этих файлов, которые предполагается ис- пользовать для хранения данных, касающихся графика проведе- ния игр, запланированных для клубной футбольной команды г. Бигчестер. Нужно запоминать следующую информацию: дату встречи, название стадиона, где она должна произойти, вмести- мость стадиона, ожидаемое количество зрителей, а также расстоя- ние от Бигчестера до места, где расположен стадион. Начнем с определения объектов, которым будут приписаны запоминаемые в файлах атрибуты. Назовем их МАТЧИ (MAT- CHES) и СТАДИОНЫ (GROUNDS). Для каждого из этих двух объектов заведем отдельный файл со следующими полями: МАТЧИ {Имя файла} ДАТА. ВСТРЕЧИ (MATCH DATE) НАЗВАНИЕ. СТАДИОНА (NAME OF GROUND) ЗРИТЕЛИ (EXPECTED GROUND ATTENDANCE) СТАДИОНЫ {Имя файла) НАЗВАНИЕ.СТАДИОНА (NAME OF GROUND) РАССТОЯНИЕ.ОТ.БИГЧЕСТЕРА (DISTANCE FROM BIGCHESTER ТО THIS GROUND) ВМЕСТИМОСТЬ (GROUND CAPACITY) Заметим, что ни одно из неключевых полей файла СТАДИОНЫ не связано функциональной зависимостью с полем НАЗВАНИЕ. СТАДИОНА. И еще одно замечание: если бы вместо двух файлов, МАТЧИ и СТАДИОНЫ, был сформирован лишь один, пред- ставляющий соединение МАТЧИ join СТАДИОНЫ, он имел бы третью нормальную форму. 81
Библиотечное дело. Этот пример несколько сложнее. Вновь предстоит найти имена для файлов и полей, которые войдут в базу данных городской библиотеки Бигчестера. Кроме цен- трального отделения, эта библиотека имеет также ряд филиалов в отдаленных районах города. Каждому абоненту при регистрации вручается читательский билет с уникальным номером. Читателю разрешается брать книги и в центральной библиотеке и в любом из ее филиалов, причем все они пользуются единым каталогом. Каждая книга идентифицируется стандартным международным библиотечным шифром ISBN (естественно, у всех экземпляров книги одинаковый ISBN; переиздания той же книги имеют дру- гие шифры ISBN). Одно или большее число отделений библиотеки может располагать несколькими экземплярами книги, а также разными ее переизданиями. Когда книга поступает в библйотеку Бигчестера, ей при- сваивается отдельный инвентарный номер, посредством которого идентифицируется конкретный экземпляр данного издания. Ни у одной из книг (даже если они находятся в разных хранилищах) инвентарные номера не совпадают — все экземпляры с одинако- выми шифрами ISBN имеют разные инвентарные номера. Каждый экземпляр книги должен иметь своего рода паспорт, включаю- щий такие данные, как название, имена авторов, издание (на- пример, второе), страна и город опубликования, название изда- тельства, дата выхода книги, число страниц, инвентарный номер, номер отделения и номер стеллажа. Номер отделения указывает, в каком филиале (или в центральной библиотеке) постоянно на- ходится книга, а по номеру стеллажа можно определить, где именно она стоит. В файлах необходимо также хранить адреса и названия отделений библиотеки и регистрационные данные каж- дого абонента. Наконец, должны фиксироваться даты получения и возврата книг. Как и в предыдущем случае, начнём с того, что выделим из множества перечисленных данных объекты и относящиеся к ним атрибуты. Кроме того, определим те соотношения, с которыми связаны некоторые из запоминаемых атрибутов, а также те, что будут использоваться при соединении файлов. Анализ показы- вает, что к числу объектов и соотношений, для которых потре- буются отдельные файлы, должны быть отнесены следующие: АБОНЕНТЫ КНИГИ ВЫДАЧА. КНИГ ПУБЛИКАЦИИ ОТДЕЛЕНИЯ Объект) Объект) Соотношение) Объект (файл каталога)) ’Объект) Сформировав такой перечень, довольно нетрудно выяснить, какие атрибуты следует приписать каждому объекту или соотно- шению. 82
АБОНЕНТЫ НОМЕР. ЧИТАТЕЛЬСКОГО - БИЛЕТА ИМЯ-АБОНЕНТА АДРЕС. АБОНЕНТА КНИГИ ИНВЕНТАРНЫЙ- НОМЕР ISBN НОМЕР. ОТДЕЛЕНИЯ НОМЕР. СТЕЛЛАЖА {Имя файла. Ниже приведены имена его полей} {Первичный ключ, идентифицирующий абонента библиотеки} {Имя файла. Ниже перечислены имена его полей} (Первичный ключ, идентифицирующий конкретный экземпляр книги} {Используется для доступа к инфор- мации из каталога} Структура последнего файла несколько упрощена за счет того, что вместо двух полей, НАЗВАНИЕ.ОТДЕЛЕНИЯ и АДРЕС.ОТДЕЛЕНИЯ, в него включено лишь одно — НОМЕР- ОТДЕЛ ЕНИЯ. Связь между полем НОМЕР. ОТДЕЛЕНИЯ и парой НАЗВАНИЕ.ОТДЕЛЕНИЯ, АДРЕС.ОТДЕЛЕНИЯ уста- навливается с помощью дополнительного файла ОТДЕЛЕНИЯ * ОТДЕЛЕНИЯ НОМЕР. ОТДЕЛЕНИЯ НАЗВАНИЕ, ОТДЕЛЕНИЯ АДРЕС. ОТДЕЛЕНИЯ {Имя файла. Ниже перечислены имена его полей} {Первичный ключ} Получение книги можно рассматривать как соотношение, связывающее конкретного читателя с конкретной книгой. Атри- бутом этого соотношения является ДАТА. ВОЗВРАТА. ВЫДАЧА КНИГ {Имя файла. Ниже перечислены имена его полей} НОМЕР. ЧИТАТЕЛЬСКОГО. {Поле, идентифицирующее абонента БИЛЕТА библиотеки} ИНВЕНТАРНЫЙ-НОМЕР {Поле, идентифицирующее данный эк- земпляр книги} ДАТА-ВОЗВРАТА Первичный ключ этого файла образуют два первых поля. Учитывая, что в библиотеке могут храниться несколько эк- земпляров книги с одним и тем же шифром ISBN, последний не может служить ключом файла КНИГИ. Такие атрибуты, как ИМЕНА-АВТОРОВ и НАЗВАНИЕ, функционально зависят от ISBN, и поэтому вместо файла КНИГИ они должны быть вклю- чены в файл каталога. КАТАЛОГ ISBN НАЗВАНИЕ ИЗДАНИЕ ИМЕНА. АВТОРОВ НАЗВАНИЕ. ИЗДАТЕЛЬСТВА ГОРОД СТРАНА {Имя файла. Ниже перечислены его поля} (Первичный ключ} (Заголовок книги} {Например, второе} 83
Этим необходимый набор файлов исчерпывается. Может пока- заться, что этот набор следовало бы дополнить файлом авторов. Однако для них не имеет смысла заводить отдельный файл, по- скольку авторам в данном случае не приписано ни одного атри- бута. Еще раз подчеркнем, что файлы нужны только для тех объ- ектов, с которыми связаны подлежащие хранению атрибуты. Примером может служить объект «издательства». Если бы такие атрибуты, как город и страна опубликования книги, функцио- нально зависели от него (что выполняется далеко не всегда), можно было бы создать специальный файл издательств. ~ 3.10.3. ПРЕОБРАЗОВАНИЕ НАБОРА ФАЙЛОВ Не всегда требуется формировать файлы заново, опи- раясь лишь на данные эмпирического анализа. В некоторых слу- чаях задача состоит в нормализации уже имеющейся совокупности файлов, не все из которых представлены в первой нормальной форме. Скачки (пример 1). В этом примере речь идет о скаковых лошадях и их владельцах. Предстоит нормализовать два файла, названия которых соответствуют их содержанию. Первый файл находится по меньшей мере в первой нормальной форме: ЛОШАДИ (HORSES) {Имя файла. Ниже приведены имена его полей и часть записей) НОМЕР. ЛО- КЛИЧКА. ЛО- ЦВЕТ ВЫСОТА ДАТА. РОЖДЕНИЯ ШАДИ ШАДИ (COLOUR) (HEIGHT) (DATE OF BIRTH) (HORSE (HORSE NUMBER) NAME) НЗ ТРАНКОЛ ГНЕДАЯ 165 19890630 Н5 ХОТПОТЕЙТО ВОРОНАЯ 170 19890212 Н6 СТРОГОУЛД ГНЕДАЯ 164 19881105 ВЛАДЕЛЬЦЫ (OWNERS) {Имя файла. Ниже приведены имена его полей и часть записей) ИМЯ-ВЛАДЕЛЬЦА АДРЕС ПРИНАДЛЕЖАЩИЕ. ЛОШАДИ (OWNER NAME) (ADDRESS) (HORSES OWNED) ЭЙСОН КЛАМБЕРУИК Н7, НЮ, Н18 БИЙСОН УИГХЭМСТЕД НЗ, Н8 джийсон ПЛАМХЕЙВН Н5, Н6, Н9, Н14 Этот файл не имеет первой нормальной формы, поскольку в каждой его записи поле ПРИНАДЛЕЖАЩИЕ.. ЛОШАДИ содержится по несколько значений. В рассматриваемом примере только лошади и владельцы являются объектами, атрибуты которых нуждаются в хранении. Следовательно, других файлов, кроме приведенных, не требуется, и для их нормализации нужно лишь несколько изменить состав полей. Если лошадь не может 84
принадлежать более чем одному владельцу (в данный момент), достаточно ввести в оба файла новое поле НОМЕР ВЛАДЕЛЬЦА (OWNER NUMBER) и удалить из файла ВЛАДЕЛЬЦЫ поле ПРИНАДЛЕЖАЩИЕ.ЛОШАДИ. Структура модифицированных файлов такова: ВЛАДЕЛЬЦЫ НОМЕР. ВЛАДЕЛЬЦА ИМЯ-ВЛАДЕЛЬЦА АДРЕС ЛОШАДИ НОМЕР. ЛОШАДИ КЛИЧКА, ЛОШАДИ ЦВЕТ ВЫСОТА ДАТА. РОЖДЕНИЯ НОМЕР. ВЛАДЕЛЬЦА {Имя файла} {Номер, присвоенный владельцу} {Имя файла} Нормализацию удалось осуществить благодаря тому, что каждая лошадь имеет единственного хозяина, и поэтому в поле НОМЕР. ВЛАДЕЛЬЦА всегда заносится ровно одно значение. В первоначальном варианте соответствие между лошадьми и их владельцами устанавливалось посредством поля ПРИНАДЛЕ- ЖАЩИЕ. ЛОШАДИ. Поскольку любому владельцу может при- надлежать несколько лошадей, в это поле приходилось заносить все их номера. Скачки (пример 2). Дополним предыдущий пример сведениями о жокеях и о тех лошадях, на которых они выступали как мини- мум в одном состязании. Эта информация помещается в ненор- мализованном файле ЖОКЕИ (JOCKEYS). ЖОКЕИ (Имя файла. Ниже приведены имена его полей и НОМЕР, ЖОКЕЯ некоторые ИМЯ-ЖОКЕЯ записи} АДРЕС ЛОШАДЬ. ЖОКЕЯ (JOCKEY (JOCKEY (ADDRESS) (HORSES NUMBER) NAME) RIDDEN) J2 УИЛСОН УИГЛСВИК Н4, Н9, Н18 J4 ЭНДРЮС ЭЛФРИСТОН НЗ, Н4 J5 ХОБСОН ЭКЛСФИЛД НЗ, Н4, Н9, НЮ В поле ЛОШАДЬ.ЖОКЕЯ может помещаться несколько раз- личных значений, поэтому файл не имеет первой нормальной формы. Два других файла, ЛОШАДИ и ВЛАДЕЛЬЦЫ, нормали- зованы описанным выше способом. Если бы на каждой лошади выезжал единственный жокей, для приведения файла ЖОКЕИ к нормальной форме достаточно было бы исключить из него поле ЛОШАДЬ. ЖОКЕЯ и одновре- менно ввести в файл ЛОШАДИ поле НОМЕР.ЖОКЕЯ. К сожа- лению, так сделать не удастся, поскольку лошадь может уча- ствовать в скачках с разными жокеями. В данном случае пред- лагается удалить из файла ЖОКЕИ поле ЛОШАДЬ.ЖОКЕЯ и создать дополнительный файл, назвав его, скажем, ЛОШАДИ. НА.СКАЧКАХ (HORSERIDDEN). 85
ЛОШАДИ. НА. С КАЧКАХ НОМЕР. ЖОКЕЯ J2 J2 J2 J4 J4 J5 J5 J5 J5 {Имя файла. Ниже приводятся имена его полей и часть записей} НОМЕР. ЛОШАДИ Н4 Н9 Н18 НЗ Н4 НЗ Н4 Н9 НЮ Файлы ЛОШАДИ и ВЛАДЕЛЬЦЫ уже приведены к нормаль- ной форме, так что никаких изменений вносить в них не требуется. В итоге получен набор из четырех файлов, причем все они нор- мализованы. Остается выяснить, обязательно ли было заводить новый файл ЛОШАДИ. НА.СКАЧКАХ. Дело в том, что поле ЛОШАДЬ. ЖО- КЕЯ исходного файла ЖОКЕИ представляет соотношение между жокеями и лошадьми. Учитывая, что жокеи могут менять лоша- дей, а лошади — наездников, нельзя обеспечить однозначность этого соотношения в любом из файлов, будь то ЛОШАДИ или ЖОКЕИ. Единственный выход — создать новый файл ЛОШАДИ. НА-СКАЧКАХ, описывающий данное соотношение в нормализо- ванной форме. Подчеркнем объективность полученного резуль- тата — четыре файла, ВЛАДЕЛЬЦЫ, ЛОШАДИ, ЖОКЕИ и ЛОШАДИ. НА.СКАЧКАХ—это минимально необходимый на- бор, позволяющий хранить атрибуты объектов «владельцы», «лошади» и «жокеи», а также соотношения «лошади—жокеи». 3.11. УПРАЖНЕНИЯ 1. Усложним пример с лошадьми, владельцами и жо- кеями, разобранный в разд. 3.10.3. Предлагается удалить файл ЛОШАДИ. НА.СКАЧКАХ и вместо него завести другие файлы, в которых хранилась бы следующая информация относительно каждого состязания: дата, время и место проведения скачек, их название (если таковое имеется), кличка лошади, пришедшей первой, имя ее жокея, список лошадей и жокеев, занявших вто- рое и последующие места. Подберите имена, отражающие содер- жание сформированной системы файлов и их полей. 2. В альпинистском клубе ведется хроника восхождений. Записываются даты начала и завершения каждого восхождения, имена и адреса участвовавших в нем альпинистов, название и высота горы, страна и район, где она расположена. Дайте выра- зительные имена файлам и полям, в которые могла бы заноситься указанная информация. Попытайтесь продемонстрировать на при- мере этих файлов, как благодаря их нормализации удается ре- 86
шить проблемы дублирования данных и сохранения присоединен- ных записей. 3. Четыре практикующих врача-терапевта решили организо- вать своего рода кооператив. Они завели файлы, в которых зане- сены имя, пол, дата рождения и домашний адрес каждого их пациента. Всякий раз, когда врач осматривает больного, явивше- гося к нему на прием, или сам приходит к нему на дом, он запи- сывает дату и место, где проводится осмотр, симптомы, диагноз и предписания больному, проставляет имя пациента, а также свое. Если врач прописывает больному какое-либо лекарство, в файл заносится его название, способ приема, словесное описа- ние предполагаемого действия и возможных побочных эффектов. Подыщите выразительные имена для файлов и полей, содержащих всю перечисленную информацию. 4. В базе данных бигчестерского муниципалитета хранятся имена, адреса, домашние и служебные телефоны всех членов городского совета. Он объединяет порядка сорока комиссий, все участники которых состоят в совете. Каждая комиссия имеет свой профиль — одна, например, занимается вопросами образо- вания, другая решает проблемы, связанные с жильем и т. д. В муниципальной базе данных записаны данные по каждой из комиссий: ее нынешний состав и председатель, прежние председа- тели и члены этой комиссии, участвовавшие в ее работе за про- шедшие 10 лет, даты включения и выхода из состава комиссии, избрания ее председателей. Многие члены совета заседают в не- скольких комиссиях. В файлы заносятся время и место проведения каждого заседания комиссии с указанием служащих муниципа- литета, которые будут участвовать в его организации. Протокол заседания и список присутствующих на нем членов комиссии пи- шутся на бумаге и в файлах не хранятся. Предлагается подобрать выразительные имена полей и файлов для базы данных городского совета Бигчестера. 5. Даны три файла. Первый содержит информацию о рейсах пассажи р ски х судов: НОМЕР. РЕЙСА ДАТА. ОТПЛЫТИЯ (CRUISE NUMBER) (DEPARTURE DATE) 1 2 3 4 5 6 7 8 840407 840412 840529 840529 840603 840630 840630 840719 ЧИСЛО. ПАССАЖИРОВ (NUMBER OF PASSENGERS) 380 560 830 790 600 780 400 810 Суда, совершающие эти рейсы, перечислены во втором файле, не имеющем первой нормальной формы. 87
НАЗВАНИЕ. СУДНА КАСЛ КУИН СИ ПРИНСЕС МЭРИ РОУЗ БРИСТОЛ БЕЛЛ ВМЕСТИМОСТЬ. СУДНА 820 460 850 630 НОМЕРА. РЕЙСОВ 5, 6 1, 7 3, 8 2, 4 Третий файл также не нормализован. В нем указаны все порты, в которые заходит судно, выполняющее конкретный рейс. ПОРТ (PORT) ГИБРАЛТАР МАРСЕЛЬ АЛЖИР АЛЕКСАНДРИЯ ПАЛЕРМО АФИНЫ СТАМБУЛ ПЛАТА. ЗА_ СТОЯН КУ (HARBOUR CHARGE PER DAY) 100 250 150 150 200 250 150 НОМЕРА. РЕЙСОВ (CRUISE NUMBERS) 2, 3, 5, 7, 8 2, 3, 6 2, 3, 7 1, 2, 3, 5, 6 4, 6, 7, 8 1, 3, 5, 8 1, 4, 8 Приведите эти файлы к пятой нормальной форме и выберите подходящие имена для новых файлов и их полей. 6. Фирме «Слиппери фишерз» принадлежит небольшая фло- тилия рыболовных катеров. Каждый катер имеет «паспорт», куда занесены его название, тип, водоизмещение и дата постройки. Фирма регистрирует каждый выход на лов, записывая название катера, имена и адреса членов команды с указанием их должностей (капитан, боцман и т. д.), даты выхода и возвращения, а также вес пойманной рыбы отдельно по сортам (например, трески). За время одного рейса катер может посетить несколько банок. Фиксируется дата прихода на каждую банку и дата отплытия, качество выловленной рыбы (отличное, хорошее, плохое). На борту улов не взвешивается. Подыщите имена полей и файлов, в которых можно было бы хранить всю эту информацию. 7. Фирма «Уиллоби» занимается продажей с аукциона анти- кварных изделий и произведений искусства. Владельцы вещей, выставляемых на аукционах «Уиллоби», юридически являются продавцами. Лица, приобретающие эти вещи, именуются покупа- телями. Получив от продавцов партию предметов, фирма решает, на котором из аукционов выгоднее представить конкретный пред- мет. Перед проведением очередного аукциона каждой из выстав- ляемых на нем вещей присваивается отдельный номер лота, играю- щий ту же роль, что и введенный ранее шифр товара. Две вещи, продаваемые на различных аукционах, могут иметь одинаковые номера лотов. В книгах «Уиллоби» делается запись о каждом аукционе. Там отмечаются дата, место и время его проведения, а также специ- фика (например, выставляются картины, написанные маслом и не ранее 1900 г.). Заносятся также сведения о каждом продаваемом предмете: аукцион, на который он заявлен, номер лота, прода- 88
вец, отправная цена и краткое словесное описание. Продавцу разрешается выставлять любое количество вещей, а покупатель имеет право приобретать сколько ему угодно. Одно и то же лицо или фирма может выступать и как продавец, и как покупатель. После аукциона служащие «Уиллоби» записывают фактическую цену, уплаченную за проданный предмет и фиксируют данные покупателя. Придумайте имена полей и файлов для базы данных «Уиллоби», которая позволит фирме усовершенствовать ее дело- производство. 8. Владелец магазина граммофонных пластинок пользуется набором файлов, содержащих те же данные, что и музыкальные файлы, описанные в разд. 2.2. Кроме того, у него хранится ин- формация о пластинках, которыми он торгует. Каждая пла- стинка, а точнее, ее наклейка, идентифицируется отдельным но- мером, так что всем копиям, отпечатанным с матрицы в разное время, присвоены одинаковые номера. На пластинке может быть записано несколько исполнений одной и той же вещи — для каж- дого из них в магазине заведена отдельная запись типа содержа- щихся в музыкальном файле ИСПОЛНЕНИЯ. Когда выходит новая пластинка, регистрируется название выпустившей ее ком- пании (например, EMI), а также адрес оптовой фирмы, у которой магазин может приобрести эту пластинку. Не исключено, что ком- пания-производитель занимается и оптовой продажей своих пла- стинок. Магазин фиксирует текущие оптовые и розничные цены на каждую пластинку, дату ее выпуска, количество экземпля- ров, проданных за прошлый год и в нынешнем году, а также число еще не распроданных пластинок. Попробуйте подобрать имена для полей и файлов, которые могут понадобиться владельцу магазина граммофонных пластинок для хранения нужной ему информации в дополнение к музыкаль- ным файлам из разд. 2.2. Не стоит особенно сосредоточиваться на том, как модернизировать эти исходные носители данных — хотелось бы, чтобы изменения в них вносились лишь в случае явной необходимости. 9. Примерно посередине воображаемого великого океана ле- жит воображаемый остров Санта Белинда. Вот уже триста лет ведется подробная летопись острова. В летопись заносятся и дан- ные обо всех людях, которые хоть какое-то время жили на Санта Белинде. Записываются их имена, пол, даты рождения и смерти. Хранятся там и имена их родителей, если известно, кто они. У некоторых отсутствуют сведения об отце, у некоторых — о ма- тери, а часть людей, судя по записям, — круглые сироты. Из ле- тописи можно узнать, когда был построен каждый дом, стоящий на острове, а если сейчас его уже нет, то когда он был снесен, точ- ный адрес и подробный план этого дома, кто и когда в нем жил. Точно так же, как и столетия назад, на Санта Белинде дей- ствуют предприниматели, занимающиеся, в частности, ловлей рыбы, заготовкой сахарного тростника и табака. Большинство 89
из них делают все сами, а некоторые нанимают работников, за- ключая о ними контракты разной продолжительности. Имеются записи и о том, кто кого нанимал, на какую работу, когда начался и закончился контракт. Собственно, круг занятий жителей Санта Белинды крайне невелик и не меняется веками. Неудивительно поэтому, что в летописи подробно описывается каждое дело, будь то рыбная ловля или выпечка хлеба. Все предпринима- тели — уроженцы острова. Некоторые объединяются- в коопе- ративы, и по записям можно установить, кто участвовал в деле, когда вступил и когда вышел из него, каким паем владел. Имеются краткие описания деятельности каждого частного предпринима- теля или кооператива, сообщающие, в том числе, когда было начато дело, когда и почему прекращено. Предлагается сформировать систему нормализованных файлов, в которых можно было бы хранить всю эту многообразную ин- формацию. Подыщите выразительные имена для файлов и полей, снабдив их при необходимости соответствующими поясне- ниями. 10. Фирма «Типико» отказалась от приобретения некоторых товаров у своих поставщиков, решив самостоятельно наладить их производство. С этой целью она организовала сеть специализи- рованных цехов, каждый из которых принимает определенное участие в технологическом процессе. Каждому виду продукции, выпускаемой «Типико», присваивается, как обычно, свой шифр товара, под которым он значится в файле товарных запасов. Этот же номер служит и шифром продукта. В записи с этим шифром ука- зывается, когда была изготовлена последняя партия этого про- дукта, какова ее стоимость, сколько операций потребовалось. Операцией считается законченная часть процесса производ- ства, которая целиком выполняется силами одного цеха в соответ- ствии с техническими требованиями, перечисленными на отдель- ном чертеже. Для каждого продукта и для каждой операции в «Типико» заведена запись, содержащая описание операции, ее среднюю продолжительность и номер чертежа, по которому можно отыскать требуемый чертеж. Кроме того, указывается номер цеха, обычно производящего данную операцию. В запись, связанную с конкретной операцией, заносятся по- требные количества расходуемых материалов, а также присвоен- ные им шифры товара. Расходуемыми называют такие материалы, как, например, электрический кабель, который нельзя использо- вать повторно. Когда, готовясь к выполнению операции, расхо- дуемый материал забирают со склада, регистрируется фактически выданное количество, соответствующий шифр товара, номер служащего, ответственного за выдачу, дата и время выдачи, номер операции и номер наряда на проведение работ, который будет обсуждаться ниже. Реально затраченное количество материала может не совпадать с потребным, из-за того, например, что часть изготовленной продукции бракуется. 90
Каждый из цехов располагает многочисленными инструмен- тами и приспособлениями. При выполнении некоторых операций их все же не хватает, и цех вынужден обращаться в центральную инструментальную за недостающими. В «Типико» каждый тип инструмента снабжен отдельным номером и на него заведена за- пись со словесным описанием. Кроме того, там отмечено, какое количество инструментов этого типа выделено цехам и какое оста- лось в инструментальной. Экземпляры инструмента конкретного типа, например гаечные ключи одного размера, различаются по своим индивидуальным номерам. На фирме для каждого типа инструмента имеется запись, содержащая перечень всех индиви- дуальных номеров. Кроме того, указаны даты их поступления на склад. По каждой операции в «Типико» отмечают типы и количества инструментов этих типов, которые должны использоваться при ее выполнении. Когда инструменты действительно берутся со склада, фиксируется индивидуальный номер каждого экземпляра, указываются номер заказавшего их цеха и номер наряда на про- ведение работ. И в этом случае потребное количество не всегда совпадает с заказанным. Наряд на проведение работ по форме напоминает заказ на приобретение товаров, но, в отличие от последнего, он направ- ляется не поставщику, а в один из цехов. Оформляется этот наряд после того, как руководство «Типико» сочтет необходимым вы- пустить партию некоторого продукта. В наряд заносятся шифр продукта, дата оформления наряда, срок, к которому должен быть выполнен заказ, а также требуемое количество продукта. Подберите имена полей и файлов, в которых могла бы раз- меститься вся эта информация. Разработанная система файлов должна обеспечивать возможность получения разнообразных спра- вок в ответ на запросы, например, следующего характера. а. Каковы количества и шифры всех расходуемых материалов, фактически потраченных при выполнении работ по наряду номер 6531? б. Располагает ли фирма всеми инструментами, необходимыми для выпуска партии продукта номер 421729? в. Где находится (на складе или в цехе с таким-то номером) каждый инструмент типа 321? В каких операциях и при изготов- лении какой продукции он применяется?
ГЛАВА 4 ФАЙЛЫ, ЗАПИСИ И УКАЗАТЕЛИ ЯЗЫКА ПАСКАЛЬ 4.1. ВВЕДЕНИЕ Для того чтобы разобраться в материале гл. 4—7, необходимо обладать знаниями о файлах, записях и указателях языка Паскаль. Такие знания читатель может получить из этой главы. Гл. 9 является дополнительной по отношению к остальному материалу книги, поскольку в ней описаны средства самого языка Паскаль, не связанные непосредственно ни с базами данных, ни о вопросами обработки данных. Хорошее знание этих возможно- стей языка является необходимым условием для понимания ма- териала второй половины книги. Предполагается, что читатель уже знаком с организацией ввода-вывода на терминальное устройство и что ему известны инструкции READ, WRITE, READLN, WRITELN и стандарт- ная функция EOLN. Кроме того, от читателя требуется знание возможностей операционной системы ЭВМ, связанных с созда- нием, редактированием и уничтожением текстовых файлов, в част- ности файлов, содержащих программы на Паскале. Например, считается, что читатель способен создать текстовый файл с именем ROUTE1, содержащий пять строк: AYTON DEEFIELD EMBY GREATHAM BIGCHESTER и файл ROUTE2, содержащий шесть строк: AYTON BEEHAMPTON GIDDLEWICK HOLLYFIELD GREATHAM BIGCH ESTER В приведенных выше файлах содержатся названия городов, встречающихся при движении по двум разным маршрутам от AYTON до BIGCHESTER. 92
4.2. ТЕКСТОВЫЕ ФАЙЛЫ Предположим, что СН объявлено переменной типа CHAR. Тогда о помощью инструкции Паскаля READ (СН) с терминала считывается символ и присваивается переменной СН. Тогда с помощью инструкции READ (ROUTE1, СН) очередной символ считывается из текстового файла ROUTE1 и присваивается переменной СН. Аналогично, по инструкции WRITE (FNAME, СН) значение переменной СН записывается в текстовый файл FNAME, а само значение СН не меняется. Таким образом, Паскаль позво- ляет программисту составлять программы считывания и записи данных в текстовый файл, используя простые расширения ин- струкций READ и WRITE, чаще всего применяемые для связи с терминальным устройством. Расширение заключается в том, что в качестве первого пара- метра в инструкции READ и WRITE добавляется имя файла. Если СН1 и СН2 являются переменными типа CHAR, то с по- мощью инструкции READ (CHI, СН2) о терминального устройства считываются два очередных символа и назначаются переменным СН1 и СН2 соответственно. Аналогично, инструкция READ (ROUTE2, CHI, СН2) позволяет извлечь два очередных символа из файла ROUTE2 и назначить их переменным СН1 и СН2 соответственно. Таким образом, при использовании в качестве аргументов имен файлов инструкции READ и WRITE, так же как и в случае обмена данными с терминальным устройством, могут иметь про- извольное число параметров. Две инструкции READ (CHI); READ (СН2) эквивалентны следующей одной: READ (CHI, СН2). Предположим, что входные данные размещены на одной строке терминального устройства и имеют вид AYTON. По инструкции READ (СН1) первый символ строки А присваивается переменной СН1. После того как символ А считан, специальный маркер будет указывать, что для ввода подготовлен следующий символ строки Y. Если вслед за инструкцией READ (СН1) будет следовать инструкция READ (СН2) переменной СН2 будет присвоено значение Y, поскольку маркер указывает на этот символ. После этого маркер передвигается 93
к символу Т i Т таким образом подготовлен для считывания. Итак, использование выполняющихся одна за другой инструкций READ позволяет последовательно передвигаться по входной строке, переходя от одного символа к другому. Та же идея лежит в основе использования инструкций WRITE для вывода на терми- нальное устройство и применения инструкций READ и WRITE для текстовых файлов. Транслятору с языка Паскаль должно быть известно, чем, именем файла или переменной, является первый аргумент такого оператора, как READ (ROUTE1, CHI). По этой и другим причинам программист должен объявлять тек- стовые файлы. Например, в программе, которая работает с пере- менными CHI, СН2 и текстовым файлом ROUTE 1, необходимо предусмотреть следующие объявления: var CHI, СН2: CHAR; ROUTE Г. TEXT. Заметим, что TEXT — это ключевое слово, служащее для объяв- ления текстового файла. Перед тем как программа первый раз обратится к файлу для записи в него данных, транслятор должен получить информацию о том, что какие-то данные будут переданы в файл. Для этого программист должен задать инструкцию REWRITE и указать в качестве аргумента имя файла. Например, инструкция REWRITE (THYFILE) служит для того, чтобы информировать транслятор о том, что в программе будут использованы инструкции, предназначенные для записи данных в файл THYFILE. Аналогично, программист должен использовать инструкцию RESET с именем файла в качестве параметра для того, чтобы «предупредить» транслятор о присутствии в программе инструк- ций, предназначенных для считывания данных из этого файла. Например, инструкция RESET (MYFILE) нужна для того, чтобы сообщить, что ожидается считывание из файла MYFILE. Эта инструкция должна предшествовать инструк- циям, осуществляющим чтение из файла MYFILE. После выпол- нения инструкции RESET первый символ текстового файла будет подготовлен для считывания. Если после чтения информации из файла требуется снова начать чтение файла с первого символа, программист должен еще раз использовать инструкцию RESET. Программа, написанная на стандартном Паскале, не может начать чтение файла с того места, где она окончила запись. Про- грамма всегда начинает чтение с первого символа файла. Анало- гично, программа не может записать данные в то место файла, 94
где она закончила считывание. Запись информации также всегда осуществляется с начала файла. В некотором смысле запись и чтение являются взаимоисключающими операциями. Приводимая ниже программа производит подсчет числа строк в текстовом файле ROUTE1. Эта программа служит для целей демонстрации описанных в этой главе средств языка Паскаль. Считается, что текст был введен в файл ROUTE1 с помощью средств операционной системы. program COUNTEX1 (ROUTE1, OUTPUT); var COUNT: INTEGER; ROUTE 1: TEXT; begin COUNT: =0; RESET (ROUTE1); while not EOF (ROUTE1) do begin READLN (ROUTE1); COUNT: = COUNT +1 end; WRITELN (’number of lines = COUNT) end. Здесь EOF — стандартная функция Паскаля, сигнализирую- щая о конце файла. EOF является булевой функцией; она при- нимает значение TRUE, если достигнут конец файла или если файл пуст. Если файл не пуст, EOF принимает значение TRUE в том случае, когда последним прочитанным символом был маркер конца файла. С помощью инструкции READLN (ROUTE1) считывается строка текстового файла ROUTE1 вместе с марке- ром конца строки. Тогда очередным подготовленным для чтения символом будет первый символ следующей строки, если, конечно, эта следующая строка существует в файле. Если же больше строк в файле нет, то следующим символом, подготовленным для чте- ния, будет специальный маркер конца файла, и функция EOF примет значение TRUE. Поскольку единственным параметром READLN является ROUTE1, считанная строка файла ROUTE1 не назначается какой-либо переменной, т. е. действие оператора READLN (ROUTE 1) заключается в пропуске одной строки файла ROUTE 1. Рас- смотрим пример программы, вычисляющей среднее число символов в строке текстового файла ROUTE2. program COUNTEXTE2 (ROUTE2, OUTPUT); var LINECOUNT, CHARCOUNT: INTEGER; 95
R0UTE2: TEXT; begin LINECOUNT : = 0; CHARCOUNT j 0; RESET (ROUTE2); while not EOF (ROUTE2) do begin while not EOLN (ROUTE2) do begin READ (ROUTE2); CHARCOUNT: = CHARCOUNT + 1 end; READLN (ROUTE2); (* переход к началу следующей строки *) LINECOUNT : = LINECOUNT + 1 end; if LINECOUNT = 0 then WRITELN ('Empty file') else WRITELN ('average number of characters per line=', CHARCOUNT/LINECOUNT:6:2) end Стандартная функция Паскаля EOLN имеет один параметр — имя текстового файла. Эта функция принимает значение TRUE, если очередным подготовленным для считывания символом тексто- вого файла является маркер конца строки. Иногда бывает необходимо в одной программе сначала сфор- мировать файл, используя инструкцию WRITE, а затем просмо- треть его, применяя инструкцию READ. Если до начала выпол- нения программы файла не существует и после завершения ее работы он не сохраняется, то такой файл называется внутренним. Таким образом, внутренний файл существует только во время выполнения программы. Файл, который существует во внешней памяти до и/или после завершения работы программы, называется внешним. Программист обязан включать в заголовок программы имена всех внешних файлов, которые использует программа. Например, имя ROUTE2 включено в список параметров про- граммы COUNTEX2, поскольку ROUTE2 — внешний файл, су- ществующий как до, так и после выполнения программы. Имена внутренних файлов в заголовок программы не вклю- чаются. Ниже приводится пример программы, которая копирует со- держимое текстового файла MYFILE в текстовый файл THYFILE. Хотя такого рода операции обычно проводятся с помощью стан- дартных средств операционной системы, эта программа служит хорошей иллюстрацией возможностей инструкции WRITE. В данном случае необходима именно инструкция WRITELN, поскольку с ее помощью в файл THYFILE заносится маркер конца строки. Заметим, что в список параметров программы не 96
program COPY (MYFILE, THYFILE); var CH: CHAR; MYFILE, THYFILE: TEXT; begin RESET (MYFILE); REWRITE (THYFILE); while not EOF (MYFILE) do begin while not EOLN (MYFILE) do begin READ (MYFILE, CH); WRITE (THYFILE, CH) end; READLN (MYFILE); {to skip to first character on next line}* WRITELN (THYFILE) {to output end-of-line marker}1 2 end end. вписаны файлы INPUT и OUTPUT. Это сделано потому, что про- грамма не обменивается данными с терминальным устройством. 4.3. ОБЪЯВЛЕНИЯ ТИПОВ Единственным способом, которым программист может воспользоваться для включения новых данных в стандартный файл Паскаля, является чтение старого файла и формирование с по- мощью инструкций записи его новой версии, содержащей новые данные. Причем новая версия должна иметь имя, отличное от имени старой, поскольку программа не может работать с двумя файлами, имеющими одинаковые имена. После создания новой версии файла старая может быть уничтожена, если она больше не нужна для других целей. Аналогично, для того чтобы уничтожить некоторые данные, входящие в состав стандартного файла Па- скаля, программисту сначала необходимо создать новый файл и скопировать в него те данные из старого файла, которые необ- ходимо сохранить, и только после этого он может уничтожить старый файл. Анализ следующей программы позволяет понять, во-первых, как осуществляется удаление ненужных данных, и, во-вторых, как вводятся объявления типов данных. В ней проводятся считывание названия города с терминального устройства и формирование из файла ROUTE2 нового файла ROUTE3. Файл ROUTE3 отличается от файла ROUTE2 только тем, что в нем не присутствует название города, введенное с терминального устройства. Если программа не обнаруживает названия города в файле ROUTE2, она должна сообщить об этом и скопировать содержимое ROUTE2 в файл ROUTE3. 1 (* переход к первому символу следующей строки *) а (* запись в файл маркера конца строки •) 4 Ульман Дж. 97
program CHOPTOWN (INPUT, OUTPUT, ROUTE2, ROUTE3); var I, J: INTEGER; ROUTE2, ROUTE3. TEXT; DELETED, NAMEFOUND: BOOLEAN; WHICHTOWN, THISTOWN: array (1 ..10] of CHAR; begin WRITELN (’please type town name with not more than ten letters’); I: = 0; while not EOLN (INPUT) and (I < 10) do begin I: = I+1; READ (WHICHTOWN [I]) end; {WHICHTOWN now contains the name to be deleted}* {The following loop puts in spaces if necessary to make WHICHTOWN contain exactly ten characters. Such spaces are known as padding} 1 2 * for J: = I +1 to 10 do WHICHTOWN [J]: = ’ ’; RESET (ROUTE2); REWRITE (ROUTE3); DELETED: = FALSE; {to signify no deletion so far}** while not EOF (ROUTE2) do begin I: = 0; NAMEFOUND: = TRUE; while not EOLN (ROUTE2) and (I < 10) do begin I: = I+1; READ (ROUTE2, THISTOWN [I]); if THISTOWN [I] < > WHICHTOWN [I] then NAMEFOUND: = FALSE end; READLN (ROUTE2); if NAMEFOUND and (I >0) then DELETED: = TRUE else begin for J: = 1 to I do WRITE (ROUTE3, THISTOWN [J]); WRITELN (ROUTE3) {to output- end-of-line marker after the name}4 end end; if DELETED then WRITELN (’name deleted ) else WRITELN (’name not deleted’) end. 1 (* WHICHTOWN содержит подлежащее удалению имя ♦) 2 (* в следующем цикле к значению переменной WHICHTOWN добав- ляются справа пробелы для того, чтобы она содержала точно десять символов ♦) 8 (* означает, что удаление не нужно ♦) 4 (♦ запись в файл маркера конца строки после названия города *) 98
Чтобы быть надежной, программа CHOPTOWN должна про- верять, содержат ли названия городов, вводимые е терминаль- ного устройства и находящиеся в файле ROUTE2, не менее одного и не более десяти символов. Такая проверка опущена только ради того, чтобы не перегружать программу деталями. Вместо объявления var WHICHTOWN, THISTOWN: array I1..10J of CHAR; программист может задать имя типа, записав объявление типа следующим образом: type NAMEARRAY = array [1..101 of CHAR; После этого программист может объявить var WHICHTOWN, THISTOWN: NAMEARRAY; Теперь переменные WHICHTOWN и THISTOWN можно исполь- зовать в программе так же, как и раньше. Действие последнего объявления заключается в создании массивов WHICHTOWN и THISTOWN, которые содержат по 10 элементов типа CHAR. Необходимо отметить, что новый способ объявления в данном конкретном случае не имеет преимуществ, но во многих других случаях, таких, например, как объявления структурированных типов параметров процедуры, он приносит пользу. Стандартными типами Паскаля являются INTEGER, REAL, CHAR и BOOLEAN. Они могут быть использованы без дополни- тельных объявлений. Остальные типы называются типами, опре- деленными пользователем. Для того чтобы ввести другие типы, укажем, что в программе CHOPTOWN значение переменной I должно быть заключено в пределах от 0 до 10, а значение пере- менной J — в пределах от 1 до 10. Чтобы быть уверенным в том, что значения переменных I и J не выйдут за пределы указанных интервалов, программист может объявить интервальные типы следующим образом: type BOUNDS = 1..10; CURSOR = 0..10; После этого можно объявить var I: CURSOR; J: BOUNDS; Теперь I и J готовы для использования в программе. Преиму- щество такого способа объявления заключается в том, что о любой попытке присвоить I значение, не лежащее в пределах от 0 до 10, будет немедленно сообщено, и это поможет программисту понять, что он сделал неправильно. После того как BOUNDS объявлено интервальным типом, программист может использовать это объявление для описания типа NAMEARRAY: 4* 99
type BOUNDS =1.10; CURSOR = 0.. 10; NAMEARRAY = array (BOUNDS] of CHAR; var I: CURSOR; J: BOUNDS; WHICHTOWN, THISTOWN: NAMEARRAY; DELETED, NAMEFOUND: BOOLEAN; Эти объявления включаются в раздел описаний приведенной выше программы, а тело этой программы остается без изменений. Заметим, что в объявлениях типов имя типа предшествует символу '=', а в объявлениях переменных имя типа следует за симво- лом *:*. Кроме того, в программе, написанной на Паскале, объ- явления типов всегда должны предшествовать объявлениям пере- менных. Если в программе есть объявления констант, то объяв- ления типов располагаются сразу же после этих объявлений. Константы можно использовать в объявлениях как типов, так и переменных. Например: const MAXCHARS == 10; type BOUNDS = 1..MAXCHARS; CURSOR = == 0..MAXCHARS; Элементы любого массива, имеющего тип NAME AR RAY, разме- щены в памяти так, что к ним возможен сравнительно быстрый доступ. Правда, память, отводимая для массива, не всегда мини- мизируется. Если программист хочет, чтобы для массива отводи- лась минимально необходимая память, и готов пожертвовать ради этого скоростью доступа, он должен 'упаковать* элементы мас- сива, используя при объявлении атрибут PACKED. С точки зре- ния программиста никаких отличий при использовании упако- ванных и обычных массивов не существует. Но все же следует обратить внимание читателей на ряд моментов. Упакованный массив символов называется строкой *. На- пример, можно объявить type STRING10 = packed array [1..101 of CHAR; var WHICHTOWN, THISTOWN: STRING10; Для строк допустимо использование операций сравнения: >, <» < >, = , >= и < —. Строки могут содержать любые символы, а не только буквы. Результаты операций сравнения зависят от того, как закодированы * Строки, как упакованные массивы символов, следует отличать от строк, являющихся компонентами текстовых файлов. — Прим. пер. 100
символы *. Символы, упорядоченные в еоответетвии а их ко- дами, образуют лексикографическую последовательность. Можно сформировать следующую инструкцию IF: « WHICHTOWN < THISTOWN then STATEMENTl else STATEMENT2 Анализируя ее, можно сказать, что STATEMENTl будет выполняться в том случае, если содержимое строки WHICHTOWN больше содержимого строки THISTOWN. Еще одно важное свойство строк заключается в том, что всю строку можно передавать на внешнее устройство с помощью инструкций WRITE и WRITELN. Так, одна инструкция WRITE (WHICHTOWN); заменяет цикл, в котором вывод строки WHICHTOWN осуще- ствлялся бы последовательно символ за символом. Ниже приведена одна версия программы CHOPTOWN, в ко- торой показано, как можно использовать свойства строк. В про- program CHOPTOWN2 (INPUT, OUTPUT, ROUTE2, ROUTE3); const MAXCHARS =10; type BOUNDS = 1.MAXCHARS; CURSOR = 0.. MAXCHARS; STRING 10 = packed array [BOUNDS] of CHAR; var I: CURSOR; J: BOUNDS; DELETED: BOOLEAN; ROUTE2, ROUTE3: TEXT;' WHICHTOWN, THISTOWN: STRING 10; begin WRITELN (’please type town name with not more than ten letters’); WHICHTOWN: = ’ ’; {this fills WHICHTOWN with 10 spaces}* I: = 0; while not (EOLN (INPUT)) and (I < 10) do begin I: = I+1; READ (WHICHTOWN [1]) end; {WHICHTOWN now contains the name to be deleted}^ RESET (ROUTE2); REWRITE (ROUTE3); DELETED: = FALSE; {to signify no deletion so far}’? while not EOF (ROUTE2) do begin * Правила кодирования различны для разных ЭВМ, но для всех ЭВМ справедливо 'А’ < 'В' ... «С '1' и 'О' '1' ...<С 9'. — Прим. пер. 1 (* строка WHICHTOWN заполняется десятью пробелами *) 2 (* WHICHTOWN содержит имя, которое должно быть удалено *) 3 а означает, что удаление не нужно *) 101
THISTOWN: = ’ I: = 0; while not EOLN (ROUTE2) and (I < 10) do begin I: = 1-4-1; READ (ROUTE2, THISTOWN [I]) end; READLN (ROUTE2); if THISTOWN = WHICHTOWN then DELETED: = TRUE else WRITELN (ROUTE3, THISTOWN:!) {:I causes only the first I characters to be output} 4 end; if DELETED then WRITELN ( name deleted ) else WRITELN (’name not deleted’) end. грамме с помощью одной инструкции WRITELN осуществляется вывод всей строки THISTOWN. В стандартном Паскале не раз- решается. используя одну инструкцию READ, вводить строку целиком; вместо этого программист должен предусмотреть для ввода цикл, как это было сделано в программе CHOPTOWN. Такое программирование имеет свои преимущества: в этом случае программист должен знать, как работать с разделяющими данные пробелами и другими специальными символами. Однако существует довольно много вариантов языка, а сле- довательно, и компиляторов, допускающих чтение всей строки с помощью одной инструкции READ. В последней версии про- граммы CHOPTOWN используется такая возможность. Отметим, что программа CHOPTOWN3 будет работать правильно только тогда, когда строки, вводимые с терминала и считываемые из файда ROUTE2, содержат точно по 10 символов, включая, где необходимо, разрешающие пробелы. Выходной файл ROUTE3, так же как и входной файл ROUTE2, будет содержать в каждой строке ровно 10 символов, не считая маркера конца строки. program CHOPTOWN3 (INPUT, OUTPUT, ROUTE2, ROUTE3); type STRING10 = packed array [1..10] of CHAR; var DELETED: BOOLEAN; ROUTE2, ROUTE3: TEXT; WHICHTOWN, THISTOWN: STRING 10; begin WRITELN (’please type town name with exactly ten characters’); READ (WHICHTOWN); {WHICHTOWN now contains the name to _____be deleted} * 4 (♦ вывод только первых символов строки THISTOWN *) 1 (* WHICHTOWN теперь содержит имя, которое должно быть удалено *) 102
RESET (ROUTE2); REWRITE (R0UTE3); DELETED: = FALSE; {to signify no deletion so far}2- white not EOF (ROUTE2) do begin READ (ROUTE2, THISTOWN); if THISTOWN = WHICHTOWN then DELETED. = TRUF else WRITELN (ROUTE3, THISTOWN); READLN (ROUTE2) end; if DELETED then WRITELN ( name deleted ) else WRITELN (’name not deleted’) end. 4.4. КОМБИНИРОВАННЫЕ ТИПЫ И ПЕРЕМЕННЫЕ 4.4.1. ОБЪЯВЛЕНИЯ КОМБИНИРОВАННЫХ ТИПОВ* * Рассмотрим объявление type STRING6 = packed array [1..6] of CHAR; SOURCEREC = record ITEMREFNO: STRING6; SUPPLIERNO: 0..99999; PRICEPERUNIT: REAL end; var CURRENT, CHEAPEST: SOURCEREC; SOURCEREC — это комбинированный тип, объектами кото- рого являются записи, состоящие из трех полей: ITEMREFNO, SUPPLIERNO и PRICEPERUNIT. ITEMREFNO относится к типу STRING6, SUPPLIERNO — к интервальному типу 0..99999, и PRICEPERUNIT — к типу REAL. Вообще, любой комбиниро- ванный тип определяется списком пар (имя поля, тип поля) заключенных между ключевыми словами RECORD и END. Пары в списке разделяются точкой с запятой, но перед ключевым словом END точка с запятой не ставится. Элементы каждой пары отделяются друг от друга двоеточием. В рассматриваемом примере CURRENT объявлено переменной типа SOURCEREC. Это означает, что CURRENT имеет три поля. К первому полю можно обратиться по имени CURRENT.ITEM- REFNO. Тип этого поля STRING6, поскольку именно STRING6 2 (* означает, что удаление не нужно *) * Записи являются объектами комбинированного типа. — Прим. пер. 103
появляется рядом с полем ITEMREFNO в объявлении типа SOURCEREC. Имя CURRENT.ITEMREFNO состоит из имени первого поля комбинированного типа SOURCEREC, перед кото- рым стоит CURRENT, являющееся именем переменной, относя- щейся н типу SOURCEREC. Другими словами, имя поля перемен- ной комбинированного типа есть имя соответствующего поля этого типа, квалифицируемое именем переменной. Термин «квалифици- руемое» означает, что для обращения используется имя поля, которому предшествует точка, которой, в свою очередь, предше- ствует имя комбинированного типа. Попробуем проиллюстрировать понятие квалификации на при- мере второго по порядку поля переменной CURRENT. Имя этого поля CURRENT.SUPPLIERNO, оно относится к интер- вальному шипу 0..99999, поскольку это тип второго поля комби- нированного типа SOURCEREC. Аналогично именем третьего поля переменной CURRENT является CURRENT.SUPPLIERNO, а относится это поле к типу REAL. Поле CURRENT.PRICEPERUNIT можно использовать также, как и любую переменную типа REAL, поле CURRENT.SUP- PLIERNO — как переменную интервального типа 0..99999, а поле CURRENT.1TEMREFNO — как переменную типа STRING6. Таким образом, переменная комбинированного типа представ- ляет собой, по существу,совокупность переменных (полей), при- надлежащих различным типам. Здесь имеет смысл напомнить, что переменная регулярного типа * является совокупностью переменных (элементов массива), принадлежащих одному и тому же типу. Для обращения к различным полям записей используются разные имена, а для обращения к разным элементам массива — отличающиеся'друг от друга индексы. В начале этого раздела была объявлена еще одна переменная типа SOURCEREC — CHEAPEST. Тремя ее полями являются: CHEAPEST.ITEMREFNO, CHEAPEST.SUPPLIERNO и CHEA- PEST .PRICEPERUNIT. Используемое при квалификации имя CHEAPEST позволяет отличить поле ITEMREFNO переменной CHEAPEST от поля ITEMREFNO переменной CURRENT. С помощью одной инструкции присваивания можно присвоить переменной комбинированного типа, состоящей из нескольких полей, значение, равное значению другой переменной того же самого комбинированного типа. Например, одна инструкция CHEAPEST: = CURRENT эквивалентна трем следующим инструкциям: CHEAPEST.ITEMREFNO: = CURRENT. ITEMREFNO; CHEAPEST.SUPPLIERNO: = CURRENT.SUPPLIERNO; CHEAPEST.PRICEPERUNIT: = CURRENT.PRICEPE- RUNIT; Объектами регулярного типа являются массивы. — Прим. пер. 104
Кроме того, можно сравнивать между собой различные записи, являющиеся значениями переменных комбинированного типа if CHEAPEST = CURRENT then DOSOMETHING Пользуясь такими сравнениями, надо помнить, что последова- тельно сравниваются одноименные поля переменных. Пример поиска выгодного поставщика товаров. Приведем те- перь пример программы обработки записей типа SOURCE R ЕС, составляющих текстовой файл SOURCES (источники). В каждой записи содержится информация о том, какой товар и по какой цене поставляет определенный поставщик. Каждая строка файла со- держит точно одну запись. Первые шесть символов каждой строки составляют шифр товара. Далее следует по крайней мере один пробел, за которым идет номер поставщика, еще по крайней мере один пробел и цена. Промежуток между номером поставщика и ценой необходим по той же самой причине, что и промежуток или конец строки между следующими друг за другом числами при вводе их с терминального устройства. С другой стороны, не обя- зательно отделять шифр товара от следующей за ним информа- ции. Ниже размещено несколько следующих друг за другом запи- сей файла SOURCES: 6PK9F1 733 86.14 F8431P 733 152.18 J496B5 218 9.95 Далее следует программа, предназначенная для считывания с терминального устройства шифра товара и поиска в файле SOURCES поставщика, продающего этот товар по наиболее деше- вой цене. На терминальное устройство выдается минимальная цена товара и число поставщиков этого товара. program SEEKCHEAP (SOURCES, INPUT, OUTPUT); type STRING6 = packed array [ 1. 6] of CHAR; SOURCEREC = record ITEMREFNO: STRING6; SUPPLIERNO: 0..99999; PRICEPERUNIT: REAL end; var SOUGHTREFNO: STRING6; CURRENT, CHEAPEST: SOURCEREC; HOWMANY: INTEGER; SOURCES: TEXT; begin ty'RITELN (’please type item ref no: exactly six characters’); READLN (SOUGHTREFNO); 105
HOWMANY: = 0; {no suppliers of this item have been found yet} * RESET (SOURCES); while not EOF (SOURCES) do begin READLN (SOURCES, CURRENT.ITEMREFNO, CURRENT.SUPPLIERNO, CURRENT.PRICEPERUNIT); if SOUGHTREFNO = CURRENT.ITEMREFNO then begin HOWMANY: = HOWMANY +1; {a supplier of the sought item has been found} 1 2 if HOWMANY = 1 then CHEAPEST: = CURRENT {if this is the first it must be the cheapest so far}3 else if CHEAPEST.PRICEPERUNIT> CURRENT.PRICEPERUNIT then CHEAPEST: = CURRENT end end; WRITELN (’number of supplier of SOUGHTREFNO, ’is’, HOWMANY); if HOWMANY >0 then begin WRITELN (’cheapest supplier is ’, CHEAPEST.SUPPLIERNO); WRITELN (’cheapest price is ’, CHEAPEST.PRICEPERUNIT:10:2) end end.. 4.4.2. ИНСТРУКЦИЯ WITH Повторения квалификации при обращении к различным полям делают программу неоправданно длинной. Например, если NEXTINV — переменная комбинированного типа, структура объ- ектов которого аналогична структуре записей файла товарных запасов, то желательно было бы не писать такие инструкции, как if NEXTINV.QUANTITYINWAREHOUSE - NEXTINV.QUANTITYALLOCATED + NEXTINV.QUANTITY- ONORDER < NEXTINV.REORDERLEVEL then WRITELN ( reorder \ NEXTINV.ITEMREFNO, quantity \ NEXTINV.QUANTITYTOORDER) Для того чтобы избежать повторений квалификации, в Паскале предусмотрена инструкция WITH. 1 (* поставщиков товара не обнаружено *) 8 (* найден очередной поставщик товара *) 8 (♦ если этот элемент первый, то он становится минимальным «) 106
with (имя переменной комбинированного типа) do В инструкции (возможно и составной), которая следует за «do», считается, что имена всех полей квалифицируются именем переменной комбинированного типа, которое присутствует в ин- струкции WITH. Использование инструкции WITH позволяет более компактно записать вышеприведенную конструкцию with NEXTINV do if QU ANTITYINWA REHOUSE - QUANTITY ALLOCATED + QUANTITYONORDER < REORDERLEVEL then WRITELN (’reorder ITEMREFNO, 'quantity QUANTITYTOORDER) Некоторые из полей комбинированного типа сами могут быть записями. Например: type COMPLEX = record REALPART, IMAGPART: REAL end; ELECTROMAG = record WINDINGS: INTEGER; GAPWIDTH: REAL2; ELECTRIC, MAGNETIC: COMPLEX end; var LEFTCOIL, RIGHTCOIL: ELECTROMAG В этом примере ELECTRIC и MAGNETIC представляют собой поля, которые являются записями. В Паскале допустима такая конструкция: with LEFTCOIL do with ELECTRIC do begin REALPART: =REALPART*2; IMAGPART: = IMAGPART*2 end To же самое можно запивать и короче. with LEFTCOIL, ELECTRIC do begin REALPART: = REALPART*2; IMAGPART: = IMAGPART*2 end В заключение представим еще один вариант: with LEFTCOIL, ELECTRIC do begin REALPART: = REALPART*2; IMAGPART: = IMAGPART*2 end 107
Этот последний вариант для данного примера оптимален, по- скольку обеспечивает квалификацию только для тех полей, которые в ней нуждаются. 4.4.3. ЗАПИСИ С ВАРИАНТАМИ До сих пор рассматривались только такие комбини- рованные типы, для которых список полей был жестко зафикси- рован. На самом деле в Паскале можно объявлять комбинирован- ные типы, включающие общую часть и несколько альтернативных списков полей. Выбор из этих списков одного осуществляется с помощью специального поля — поля признака. Это поле исполь- зуется примерно так же, как и селектор в инструкции CASE. Запись, которая включает поле признака и альтернативные списки полей, называется записью с вариантами. Чтобы продемонстрировать приемы работы с такими записями, рассмотрим сильно упрощенную версию комбинированного типа EMPLOYEES для фирмы «Типико». Служащие фирмы «Типико» могут быть постоянными, временными или проходящими испыта- тельный срок. Временные служащие работают только в течение какого-то определенного периода времени, например года, без обязательства по отношению к фирме продолжить работу после окончания этого срока. Лица, проходящие испытательный срок, могут, если хорошо себя зарекомендуют, стать постоянными служащими, или фирма «Типико» может прервать испытания и отказаться от их услуг. Работа служащих, проходящих испы- тательный срок, проверяется через определенные промежутки времени. Приводимая ниже версия комбинированного типа EMPLOYEES не содержит альтернативных вариантов ЕМ PLORECTYPE = record EMPLOYEENUMBER: 100000..999999; SURNAME: STRING 12; FORENAMES: STRING24; DATEOFENTRYTOTHISAPPOINTMENT: DATETYPE; TEMPORARY: BOOLEAN; DATEOFEXPIRYOFTEMPORARY APPOINTMENT: DATETYPE; PROBATIONARY. BOOLEAN; DATEOFENDOFPROBATION, DATEOFLASTREVIEWOFPERFORMANCE. DATEOFNEXTREVIEWOFPERFORMANCE: DATETYPE; EMPLOYEENUMBEROFSUPERVISOR: 100000..999999 end Такая структура данных приводит к дополнительному расходу памяти, поскольку, например, для постоянных служащих инфор- 108
мация, хранящаяся в поле DATEOFEXPIRYOFTEMPORARY- APPOINTMENT, не нужна. Для постоянных и временных слу- жащих лишними являются также поля DATEOFLASTREVI- EWOFPERFORMANCE и DATEOFNEXTREVIEWOFPER- FORMANCE. Избавиться от лишней информации можно, исполь- зуя записи с вариантами. Перед тем как объявить новый комби- нированный тип для служащих, введем тип EMPSTATUS. К этому типу будет относиться поле признака. Любая переменная или поле типа EMPSTATUS может иметь одно из трех возможных значений: PERMANENT, TEMPORARY или PROBATIONARY. type EMPSTATUS = (PERMANENT, PROBATIONARY, TEMPORARY); EMPLORECTYPE = record EMPLOYEENUMBER: 100000..999999; SURNAME: STRING12; FORENAMES: STRING24; DATEOFENTRYTOTHISAPPOINTMENT: DATETYPE; case STATUS: EMPSTATUS of TEMPORARY: (DATEOFEXPIRYOFAPPOINTMENT: DATETYPE) PROBATIONARY: (DATEOFLASTREVIEWOFPERFORMANCE, DATEOFNEXTREVIEWOFPERFORMANCE, DATEOFENDOFPROBATION: DATETYPE; EMPLOYEENUMBEROFSUPERyiSOR: 100000.999999); PERMANENT: ( ) end Поле, расположенное между ключевыми словами „case” и ,,of” является полем признака. Имя поля признака STATUS, его тип EMPSTATUS. Если значением поля признака записи является TEMPORARY, программист может использовать эту запись так, как если бы поля этой записи были объявлены следующим об- разом: EMPLORECTYPE = record EMPLOYEENUMBER: 100000..999999; SURNAME: STRING12; FORENAMES: STRING24; DATEOFENTRYTOTHISAPPOINTMENT: DATETYPE; STATUS: EMPSTATUS; {which has the value TEMPORARY} * DATEOFEXPIRYOFAPPOINTMENT: DATETYPE _______ end 1 (* это поле имеет значение TEMPORARY х) 109
В этом примере после STATUS появляется только одно поле — DATEOFEXPIRYOF APPOINTMENT, поскольку именно это поле стоит в скобках после TEMPORARY в записи с вариантами. В случае, когда STATUS = TEMPORARY, поля, относящиеся к PROBATIONARY, не включаются в запись. Если значением поля признака записи является PROBATI- ONARY, программист как бы работает g комбинированным типом, объявленным так: EMPLORECTYPE = record EMPLOYEENUMBER: 100000..999999; SURNAME: STRING12; FORENAMES: STRING24; DATEOFENTRYTOTHISAPPOINTMENT. DATETYPE; STATUS: EMPSTATUS; {which has the value PROBATIONARY)1 DATEOFLASTREVIEWOFPERFORMANCE. DATEOF.NEXTREVIEWOFPERFORMANCE, DATEOFENDOFPROBATION: DATETYPE; EMPLOYEENUMBEROFSUPERVJSOR: 100000..999999; end Здесь поля, следующие за STATUS, в точности повторяют поле, стоящее после PROBATIONARY в записи с вариантами * *. Поле, относящееся к TEMPORARY, не нужно, когда STATUS = = PROBATIONARY.* В том случае, когда STATUS = PERMANENT, комбиниро- ванный тип выглядит следующим образом: EMPLORECTYPE = record EMPLOYEENUMBER: 100000..999999; SURNAME: STRING 12; FORENAMES: STRING24; DATEOFENTRYTOTHISAPPOINTMENT: DATETYPE; STATUS: EMPSTATUS {which has the value PERMANENT}1 end Здесь после STATUS полей нет, поскольку нет их и в скобках после слова PERMANENT *. Та часть записи с вариантами, которая следует после ,,case”, называется вариантной частью. Вариантная часть заканчивается 1 (♦ это поле имеет значение PROBATIONARY *) * См. объявление типа EMPLORECTYPE. — Прим. ред. 1 (* это поле имеет значение PERMANENT *) 110
ключевым еловом «end», завершающим объявление комбинирован- ного типа, т. е. в Паскале вариантная часть всегда является последней составляющей записи. Комбинированный тип может иметь только одну вариантную часть, но эта вариантная часть может, в свою очередь, иметь собственную вариантную часть и т. д. на любую глубину. Заметим, что все записи, встретившиеся в тексте, не являются записями с вариантами, исключая те случаи, где явно указано обратное. 4.5. ДВОИЧНЫЕ ФАЙЛЫ Текстовый файл представляет собой последовательность символов и обычно размещается во внешней памяти. Далее вводится понятие двоичного файла, который представ- ляет собой последовательность значений произвольного, но отлич- ного от CHAR типа. Все двоичные файлы, так же как и все тексто- вые файлы, которые используются в программе, должны быть объявлены. Например, можно объявить файл, состоящий из целых чисел, так type FTYPE = file of INTEGER; var AYFILE: FTYPE; или так var * AYFILE: file of INTEGER; Тогда AYFILE будет состоять из последовательности целых чисел, представленных не в символьной форме, а в обычном двоич- ном виде. Вот почему все файлы, элементы которых не принад- лежат типу CHAR, называются двоичными. Каждое значение, записанное в файл AYFILE или считанное из него, является целым числом. Целое не может быть записано или считано не целиком, цифра за цифрой. Все цифры, составляющие целое число, считываются или записываются сразу. Программисту не надо знать, как отделяются друг от друга целые, входящие в двоичный файл. ' В Паскале разрешено строить двоичные файлы из записей. Например: type RECTYPE = record X: INTEGER; Y: REAL; Z: CHAR end; SOURCEREC = record ITEMREFNO; STRING6; SUPPLIERNO: 0..99999; PRICEPERUNIT: REAL end; 111
var EXFILE: file of RECTYPE; BINSOURCES: file of SOURCEREC; CURRENT: SOURCEREC; EXREC: RECTYPE Инструкции READ и WRITE используются с двоичными файлами так же, как и с текстовыми. Правда, надо помнить, что информацию из двоичных файлов нельзя считывать и записывать посимвольно. На самом деле одной инструкции WRITE (EXFILE, EXREC) достаточно, чтобы поместить значение EXREC, включающее несколько полей, в файл EXFILE. Поскольку EXFILE является двоичным (а не текстовым), значение EXREC не может быть записано в EXFILE последовательно поле за полем. Все поля считываются или записываются вместе. Аналогично с помощью одной инструкции READ (BINSOURCES, CURRENT) все поля очередной записи файла BINSOURCES считываются и назначаются переменной CURRENT. В программе SEEKCHEAP (см. разд. 4.4.1) запись читается из текстового файла SOURCES последовательно поле за полем: READ (SOURCES, CURRENT.ITEMREFNO, CURRENT.SUPPLIERNO, CURRENT.PRICEPERUNIT) В файле SOURCES могут находиться те же самые данные, что и в файле BINSOURCES. Но в файле SOURCES они будут пред- ставлены в символьной форме, в то время как в файле BINSOUR- CES — в обычной двоичной форме. Отличие использования двоичных файлов от текстовых за- ключается в том, что для первых осуществляется чтение (или запись) целиком всей записи, а для вторых — раздельно по полям, т. е. первая операция проще. Причем это преимущество становится более значительным по мере возрастания числа подей в записи. Кроме того, операции с двоичными файлами выпол- няются быстрее, так как не нужно преобразовывать данные из символьной формы в двоичную при чтении и из двоичной формы в символьную при записи чисел. С помощью средств операционной системы можно создавать только текстовые файлы, поскольку информация, набираемая на терминале, представляет собой последовательность символов* Аналогично данные только из текстового файла можно напеча- тать на алфавитно-цифровом печатающем устройстве (АЦПУ) или отобразить на экране дисплея, так как на эти устройства можно вывести тоже только последовательность символов. Однако довольно легко написать как программу считывания записей текстового файла и помещения их в двоичный файл, так и про- 112
грамму считывания данных из двоичного файла и вывода этих данных в текстовом виде. Отметим, что в двоичных файлах не содержится маркеров конца строки. Поэтому при работе с двоичными данными нельзя пользоваться инструкциями READLN и WRITELN, а также стандартной функцией EOLN. Инструкции RESET, REWRITE и стандартная функция EOF действуют для двоичных файлов так же, как и для текстовых. Например, перед тем как первый раз считать информацию из файла BINSOURCES или начать чтение этого файла снова, необ- ходимо выполнить инструкцию RESET (BINSOURCES). Разница между внутренними и внешними двоичными файлами аналогична разнице между соответствующими текстовыми фай- лами. В список параметров инструкции program обязательно надо включать имена внешних двоичных файлов. 4.6. ФАЙЛОВЫЕ БУФЕРА Буфером называется область памяти, предназначенная для временного хранения данных при передаче их от источника к потребителю. При объявлении переменной файлового типа авто- матически создается буфер, который используется при дальнейшей работе с файлом. Например, объявление var FNAME: file of TYPENAME приводит к образованию буфера для файла FNAME. Можно ска- зать, что этот буфер является переменной типа TYPENAME и имя этой переменной FNAME f . В общем, буфер — это пере- менная, имеющая тот же тип, что и все компоненты файла, а ее имя образуется приписыванием слева к имени файла стрелки. Если файл используется только для чтения, его буфер содер- жит значение, которое будет считано в процессе выполнения следующей инструкции READ. Таким образом, значения, содер- жащиеся в файле, последовательно копируются в буфер. Иногда программисту может понадобиться, чтобы следующее значение попадало в файл без выполнения инструкции READ. В Паскале этого можно добиться с помощью процедуры GET, которая в ка- честве параметра использует имя файла. Таким образом, записав инструкцию GET (FNAME), можно скопировать следующую компоненту файла FNAME, име- ющую тип TYPENAME, в буфер FNAME | . При этом предыду- щее значение FNAME f будет потеряно. Таким способом, т. е. используя GET дважды, трижды и т. д., можно копировать в бу- фер расположенные одна за другой компоненты файла FNAME. 113
Если V — переменная типа TYPENAME, то следующие две инструкции: V: = FNAME f ; GET (FNAME) эквивалентны инструкции READ (FNAME, V) Для текстового файла буфер является переменной типа CHAR. Целые и вещественные числа при обмене данными с текстовым файлом проходят последовательно через буфер символ за симво- лом, причем программисту не обязательно знать, как это проис- ходит. Если V, W и X переменные типа TYPENAME, то в результате выполнения трех следующих инструкций V: = FNAME f ; W: = FNAME f ; READ (FNAME, X) этим переменным будет присвоено одно и то же значение. Присваивание значений переменным V и W не приведет к счи- тыванию в буфер очередной компоненты файла FNAME: ведь известно, что переход к следующей компоненте файла произво- дится только с помощью процедуры GET и инструкции READ. Подобно стандартной процедуре GET действует другая стан- дартная процедура PUT. Она также использует в качестве пара- метра имя файла. В результате выполнения инструкции PUT (FNAME) очередной номпоненте файла FNAME присваивается значение, находящееся в файловом буфере FNAME f , при этом значение FNAME остается неизменным. Таким образом, две следующие инструкции: FNAME: = V; PUT (FNAME) эквивалентны одной WRITE (FNAME, V) всегда, за исключением одного случая, когда FNAME текстовый файл, а V не типа CHAR. Программа определения суммы денег. Программа предназна- чена для того, чтобы показать, как используется буфер текстового файла. Текстовый файл AMOUNTS содержит вещественные числа, размещенные по одному на строке. Каждое из этих чисел пред- ставляет собой сумму денег, причем пробелов перед этими чис- лами нет. Если первым символом строки является '$*, то сумма дается в долларах; если же первый символ не *$*, то им обяза- тельно является первая цифра суммы в фунтах стерлингов. Про- грамма позволяет определить общую сумму денег в фунтах стер- лингов, считая, что в одном фунте стерлингов 1,6 долл. program SUMS (AMOUNTS, OUTPUT); const DOLLARSPERPOUND= 1.5; 114
var AMOUNTS: TEXT; DOLLARS, POUNDS, SUM: REAL; begin SUM: = 0; RESET (AMOUNTS); while not EOF (AMOUNTS) do begin if AMOUNTS! = ’S’ then {amount is in dollars}* begin GET (AMOUNTS); {so that the next character to be read will be the first digit of the amount} READLN (AMOUNTS, DOLLARS); POUNDS: = DOLLARS I DOLLARSPERPOUND end else READLN (AMOUNTS, POUNDS); SUM: = SUM + POUNDS end; WRITELN (’Total = ’, SUM:8:2) end. Использование файлового буфера дает возможность проверить, является ли первый символ $. При этом от числа не отщепляется первый символ, а все число целиком может быть считано с по- мощью инструкции READLN. Если же первый символ строки будет считываться с помощью инструкции READ (AMOUNTS, СН) а сумма с помощью такой инструкции, как READ (AMOUNTS, POUNDS), то первая значащая цифра числа в случае, когда сумма задана в фунтах стерлингов, будет потеряна. Это произойдет потому, что выполнение первой инструкции READ приведет к продвижению ко второму символу строки, который станет первым считыва- емым символом для второй инструкции READ. Программа слияния файлов. Эта программа является хорошим примером, показывающим, как используются файловые буфера, а также инструкции GET и PUT. В программе производится слияние двух входных двоичных файлов в один выходной файл. Оба входных файла состоят из компонент типа STUDENTCOUR- SES; они отсортированы по возрастанию значения поля STU- DENTIDENTINO. Можно сказать, что во входном файле значение STUDENTIDENTINO любой записи больше значений этого же поля для всех предшествующих записей. Выходной файл также должен быть отсортирован по возрастанию значения поля STU- DENTIDENTINO. --------- » 1 (« сумма в долларах •) 2 (• следующим символом будет первая цифра суммы в долларах ») 115
program EXMERGE (OLDMASTER, TRANSAC, NEWMASTER); type STRING 12 = packed array [I .12] of CHAR; STUDENTCOURSES = record STUDENTIDENTINO: 1 .9999; COURSES: array [1. 3} of STRING 12 end; var OLDMASTER, TRANSAC, NEWMASTER: file of STUDENTCOURSES; STOPLOOP: BOOLEAN; begin RESET (OLDMASTER); RESET (TRANSAC); REWRITE (NEWMASTER); STOPLOOP: = EOF (OLDMASTER) or EOF (TRANSAC); while not STOPLOOP do begin if OLDMASTER} STUDENTIDENTINO<TRANSAC}. STUDENTIDENTINO then begin NEWMASTER}: = OLDMASTER}; GET (OLDMASTER); STOPLOOP: = EOF (OLDMASTER) end else begin NEWMASTER}: = TRANSAC}; GET (TRANSAC); STOPLOOP: = EOF (TRANSAC) end; PUT (NEWMASTER) end: while not EOF (OLDMASTER) do begin NEWMASTERf: = OLDMASTERf; PUT (NEWMASTER); GET (OLDMASTER) end; while not EOF (TRANSAC) do begin NEWMASTERf: = TRANSACT; PUT (NEWMASTER); GET (TRANSAC) end end. В этой программе слияние осуществляется в первом цикле WHILE. После завершения этого цикла, остаток того из двух файлов OLDMASTER или TRANSAC, в котором еще есть эле- менты, переписывается в файл NEW-MASTER. Это производится 116
с помощью одного из двух последних циклов WHILE. Удобство использования буферов заключается в том, что имеется возмож- ность сравнивать значения STUDENTIDENTINO очередных за- писей файлов OLDMASTER и TRANSAC, не считывая самой записи. Для того чтобы убедить читателя, что это сильно упро- щает программирование, советуем ему попробовать написать ту же самую программу без использования буферов. 4.7. УПРАЖНЕНИЯ Все приведенные в этом разделе упражнения связаны с использованием упрощенной версии файла INVOICE, содержа- щей следующие восемь записей: INVOICENO CUSTMERNO DATE AMOUNT (номер счета) (номер клиента) (дата) (сумма) 1020 12 910621 302.61 1023 15 910621 57.81 1027 . 11 910622 652.34 1034 12 910622 81.72 1036 15 910622 24.20 1045 11 910623 775.00 1049 11 910624 61.55 1051 15 910624 84.17 Комбинированный тип, объектами которого являются эти записи, считается объявленным так record INVOICENO: 1000 .. 9999; CUSTOMERNO: 10 .. 99; DATE: 900101 .. 991231; AMOUNT: REAL end 1: а. Создайте текстовый файл TINVOICE, содержащий восемь приведенных выше записей. Используйте для этого только сред- ства операционной системы. б. Напишите программу, которая, используя файл TIN- VOICE, формирует двоичный файл INVOICE, содержащий те же самые записи, что и INVOICE. в. Напишите программу считывания данных из двоичного файла INVOICE и вывода их на экран дисплея. г. С помощью средств операционной системы уничтожьте файл TINVOICE. 2.: а. Напишите программу вывода на экран дисплея резуль- тата операции proj INVOICENO, DATE, AMOUNT (sei CUSTOMERNO = CX (INVOICE)) Здесь CX—номер клиента, набранный на экране дисплея. Ваша программа должна сначала считать значение СХ, а затем, просмотрев файл INVOICE, вывести данные из каждой записи, 117
значение поля CUSTOMERNO которой совпадает с СХ. Если таких записей нет, должно быть сформировано соответствующее сообщение. б. Модифицируйте Вашу программу так, чтобы по номеру клиента, введенному с дисплея, выводилась только сумма всех значений AMOUNT счетов данного клиента. Обратите внимание, что построить выражение на языке реляционной алгебры для этой цели нельзя. 3: а. Создайте копию файла INVOICE, используя средства операционной системы. Дайте этой копии какое-нибудь новое имя. б. Напишите программу ввода номера счета с экрана дис- плея и удаления из файла INVOICE записи, значение поля INVOICENO которой совпадает с введенным. Если такой записи не существует, программа должна выдать соответствующее сооб- щение. Удаление может быть проведено так: последовательно считывайте записи файла INVOICE, формируя новую версию этого файла, в которую не включается искомая запись. Выведите на экран дисплея новую версию файла INVOICE с помощью программы из 1, в. Сохраните только что написанную программу для использования в 4, б. в. С помощью средств операционной системы восстановите содержимое файла INVOICE. Для этого используйте копию файла, созданную в 3, а. 4: а. Составьте программу, выполняющую, во-первых, чтение с терминального устройства новой записи счета и, во-вторых, включение этой записи в файл INVOICE так, чтобы файл остался упорядоченным по возрастанию значения поля INVOICENO. Для этого создайте новую версию файла INVOICE, сформировав ее из записей старой версии файла и вновь введенной записи. В том случае, если значение поля INVOICENO новой записи совпадает со значением этого же поля одной из записей файла INVOICE, включение не производится и выдается соответству- ющее сообщение. После того как включение проведено, выведите на экран дисплея новую версию файла INVOICE. б. Используя программу из 3, б, удалите из файла IN- VOICE запись, включенную в файл в результате'выполнения пре- дыдущего упражнения. Выведите на экран дисплея восстановлен- ную версию файла INVOICE. 5. Напишите программу считывания с терминального устрой- ства номера счета INVOICENO и следующей за ним латинской буквы: А или D. Если вводится буква D, то необходимо, чтобы программа считывала с терминала дату и помещала ее в поле DATE записи файла INVOICE, значение поля INVOICENO которой совпадает с введенным номером счета. Старое значение поля DATE записи естественно уничтожается. Если вводится буква А, то программа должна считать с терминального устрой- сева сумму и поместить ее на место старого значения в поле AMOUNT записи, определяемой введенным номером счета. Если 118
же в файле нет записи е требуемым значением поля INVOICENO либо введенная буква не является ни А, ни D, необходимо, чтобы программа сформировала соответствующее сообщение и закончила работу. 6: а. Как и в 3, а вделайте копию файла INVOICE. б. Создайте текстовый файл DTRANSAC, содержащий числа 1023, 1034, 1036 и 1049. в. Напишите программу удаления из файла INVOICE записей, значение поля INVOICENO которых совпадает со зна- чениями, включенными в файл DTRANSAC. г. Используя операцию копирования, сохраните перво- начальную версию файла INVOICE. Модифицируйте файл DTRAN- SAC таким образом, чтобы он содержал 1023, 1024, 1036, 1038 и 1049. д. Выполните 6, в, но теперь предусмотрите выдачу на экран дисплея сообщения каждый раз, когда искомая запись не найдена. 7. Условия те же, что и в 5, только считается, что изменено может быть несколько записей файла INVOICE. Информация об изменениях содержится в текстовом файле VTRANS, содержащем записи типа record INVOICENO: 1000.. 9999; case TAG: CHAR of ’A’: (AMOUNT: REAL); ’D’: (DATE: 900101..991231) end. Сформируйте с помощью средств операционной системы файл VTRANS. Записи в нем должны быть расположены в порядке увеличения значения поля INVOICENO. 4.8. УКАЗАТЕЛИ 4.8.1. ССЫЛОЧНЫЕ ТИПЫ Указатель — это адрес, который определяет место пере- менной в оперативной памяти. Указатели могут сами быть значе- ниями переменных. На рис. 4.1 показана переменная типа Т, ее имя Р f . Р — переменная, значением которой является указа- тель, ссылающийся на Р f . На рисунке указатель представлен в виде стрелки, начало которой связано с переменной Р, а конец указывает на Р f . Говорят, что Р f — это значение, на которое указывает Р. Переменную Р | можно использовать так же, как р р| Рис. 4.1. Уиаеатель Р, ссылающийся на перемен- |~~| — ную Р f 119
а любую другую переменную, имеющую тот же самый тип. Каждая переменная ссылочного типа может указывать только на пере- менные одного типа. Переменная ссылочного типа, ссылающаяся на переменные типа Т, должна быть объявлена f Т. Это означает, что f Т указывает на переменную типа Т. Например, если Pf относится к типу INTEGER, то Р необ- ходимо объявить имеющей тип f INTEGER. Объявление var I: INTEGER; приводит к созданию переменной с именем I, начальное значение которой не определено. Аналогично, объявление var Р: f INTEGER; приводит к созданию переменной Р, значениями которой могут быть указатели, ссылающиеся на переменные типа INTEGER. Начальное значение Р также не определено. Последнее объявление не приводит к созданию переменной типа INTEGER с именем Р f . Программист может создать такую переменную с помощью стан- дартной процедуры NEW. Использование инструкции NEW (Р); приводит, во-первых, к созданию переменной целого типа с име- нем Pf и, во-вторых, к присваиванию переменной Р значения, ссылающегося на Pf . После выполнения этого оператора значе- ние Р f считается неопределенным. Р f может быть использовано так же, как любая другая переменная целого типа. Рассмотрим следующий фрагмент программы: program PEXAMPLE (INPUT, OUTPUT); type PCATCHTYPE = tCATCHTYPE; {it is legitimate for this declaration to precede the declaration of CATCHTYPE}' CATCHTYPE = record WEIGHT: REAL; FISHCOUNT: INTEGER end; var P, Q: ^INTEGER; TRIP1, TRIP2: PCATCHTYPE; begin NEW (P); NEW (TRIP1); READLN (Pf, TRIPIf.WEIGHT, TRIP1T.FISHCOUNT); Здесь TRIP1 f .WEIGHT и TRIP1 f .FISHCOUNT представ- ляют собой имена полей комбинированного типа, на который ссылается TRIP1. Выполнение инструкции READLN приводит 1 (♦ это объявление должно TYPE ♦) предшествовать объявлению типа CATCH- 120
Рис. 4.2. Пример возможных значений переменный к присваиванию трем переменным значений, приведенных на рис. 4.2. Отметим, что неверна инструкция Р: = TRIP1; поскольку Р и TRIP1 принадлежат разным типам. Инструкция же Q: = Р; верна. В результате ее выполнения Q будет ссылаться на ту же переменную целого типа, что и Р (рис. 4.3). Qf теперь одна из возможных форм обращения к переменной Р f . Нельзя исполь- зовать и инструкцию. TRIP2f : = TRIP1 f ; поскольку в данный момент значение указателя TRIP2 не опре- делено и переменная TRIP2f не размещена. Можно разместить переменную TRIP2 f , выполнив инструкцию NEW (TRIP2); Выполнение двух инструкций • NEW (TRIP2); TRIP2f : = TRIP1 f приведет к размещению переменной комбинированного типа TRIP21 и к присваиванию переменной TRIP2f значения, рав- ного TRIP1 f (рис. 4.4). В Паскале разрешается назначать переменным ссылочного типа специальное решение «nil». Если значением переменной ссылочного типа является «nil», то это значит, что переменная не указывает на конкретный объект. Например, можно назначить переменной TRIP2 значение «nil» перед выполнением инструкции NEW (TRIP2) для того, чтобы показать, что переменная TRIP2 до выполнения этой инструкции не была связана с конкретным объектом. Иногда возникает такая ситуация, что переменная, которая была создана с помощью процедуры NEW, больше не нужна. В этом случае можно уничтожить переменную и сделать занима- емую ей область памяти пригодной для других целей. Для этого Рис. 4.3. Пример двух указателей, ссылающихся на одну и ту же пере- менную TRIP2 ТИР?!_________ Q:------------------»! 17.93 | б WEIGHT FISHCOUNT Рис. 4.4. Пример значений перемен- ных TR1P2 и TRlP2f 121
предназначена процедура DISPOSE. Например, DISPOSE (TPIP1) уничтожает сделанное с помощью NEW (TRIP1). Но перед выполнением процедуры DISPOSE программист должен убедиться, что на TRIP1 нет ссылок с помощью других указа- телей. 4.8.2. СВЯЗАННЫЕ СПИСКИ Связанный список — это такая структура, в которой первая запись ссылается на вторую, вторая на третью и т. д. (рис. 4.5). Поле-указатель последней записи имеет значение «nib. Так обозначается конец списка. Достоинством связанного списка является то, что любая его запись может быть легко удалена простым изменением указателя предшествующей записи (рис. 4.6). Более того, на рис. 4.7 показано, что помещение в спи- сок новой записи связано лишь с изменением указателя записи, предшествующей вставляемой. Важно, что на стадии составления декларативной части программы программисту не нужно знать числа записей в связанном списке. Если же совокупность записей одного и того же типа хранится в массиве, программист при объявлении массива должен точно знать максимальное число его элементов. Задача об автомобильной стоянке. Читателю предлагается внимательно, шаг за шагом проследить за процессом составления программы, управляющей состоянием списка, который отражает текущую ситуацию, сложившуюся на автомобильной стоянке. Программа дает возможность проверить, находится ли в дан- ный момент на стоянке автомобиль с любым заданным регистра- ционным номером. С помощью этой же программы можно, если потребуется, получить список регистрационных номеров авто- мобилей, которые находятся на стоянке. Этот список упорядочен по возрастанию регистрационных номеров, что облегчает процесс поиска нужного номера. Разбор этой программы дает возможность читателю понять, как выполняются основные операции со связан- Рис. 4.5. Структура связанного списка Рис. 4.6. Пример, иллюстрирующий операцию удаления элемента из связан- ного списка 122
Рис. 4.7. Пример, иллюстрирующий операцию вставки элемента в связанный список ными списками. Но все же отметим, что связанный список в дан- ном случае не является наиболее подходящей структурой данных. В программе сначала предусмотрен ввод символа, определя- ющего характер дальнейших действий. Если вводится символ I, то необходимо осуществить вставку в список регистрационного номера автомобиля, въезжающего на стоянку; если вводится символ D, то необходимо удалить из списка регистрационный номер автомобиля, покидающего стоянку, и т. д. program CARWATCH (INPUT, OUTPUT); type STRING? = packed array [1..7] of CHAR; LISTPTR = f REGRECTYPE; REGRECTYPE — record REGNUMBER: STRING?; NEXTREG. LISTPTR end; var LISTHEAD: LISTPTR; SELECTOR: CHAR; procedure INSERT; begin {declaration omitted/end; procedure DELETE; begin {declaration omitted /end; procedure LOOKUP; begin {declaration omitied/end; procedure OUTPUTLIST; begin {declaration omitted/end; begin {body of program}1 2 LISTHEAD: = nil; {to signify that list is initially empty}^ repeat WRITELN (’if you wish to insert please type Г); WRITELN (’if you wish to delete please type D’); WRITELN (’if you wish to look up please type R’); WRITELN (’if you wish to output the list type L’); 1 (* объявление опущено «) 2 ( ♦ тело программы *) 8 ( ♦ показывает, что список сначала пуст *) 123
WRITELN (’if you wish to terminate the program type X); READLN (SELECTOR); case SELECTOR of ’Г: INSERT; D’: DELETE; R’: LOOKUP; L': OUTPUTLIST; ’X’: end until SELECTOR = ’X’ end. Ниже приведены тексты всех пропущенных процедур про- граммы, начиная с простейшей OUTPUTLIST: procedure OUTPUTLIST; CURRPTR: LISTPTR; begin CURRPTR; = LISTHEAD; if CURRPTR = nil then WRITELN (’list empty ) else repeat WRITELN (CURRPTRf.REGNUMBER); CURRPTR. = CURRPTRf.NEXTREG until CURRPTR = nil end; Для того чтобы продемонстрировать работу процедуры OUT- PUTLIST, предположим, что список содержит четыре регистра- ционных номера: 'А156РТК', 'A783AFC','В276РРС' и'В304РТС'. Перед первым выполнением тела цикла CURRPTR равен LISTHEAD и поэтому указывает на то же самое значение, что и LISTHEAD (рис. 4.8). Следовательно, первое выполнение инструкции WRITELN (CURRPTR f . REGNUMBER) приводит к выдаче регистрационного номера 'А136РТК'. В ре- зультате присваивания CURRPTR: - CURRPTR f .NEXTREG указатель CURRPTR будет ссылаться на то же значение, что и запись с номером 'А136РТК'. Ситуация, сложившаяся перед вторым выполнением тела цикла, показана на рис. 4.9. Теперь выполнение инструкции WRITELN приведет к выдаче 'A783AFC'. На рис. 4.10 представлена ситуация, сложившаяся перед третьей CURRPTR I I 'A783AFC ;В276РРС9 8304РТС’ nil Рис. 4.8. Значения указателей перед первой итерацией 124
LISTHEAD Рис. 4.9. Значения указателей перед второй итерацией LISTHEAD Рис. 4.10. Значения указателей перед третьей итерацией итерацией. В конце концов CURRPTR примет значение «nib: на терминал выведены номера всех автомобилей стоянки. Процедура INSERT сложнее процедуры OUTPUTLIST, по- этому сначала приводится лишь ее часть procedure INSERT; var NEWREGPTR: LISTPTR; REGISTRATION: STRING7; begin READLN (REGISTRATION); {assuming that exactly seven characters are typed} 7 NEW (NEWREGPTR); {NEWREGPTR now points to a newly created record}1 2 * 4 * 6 7 NEWREGPTR].REGNUMBER: = REGISTRATION; {to put the input registration number into the REGNUMBER field of the newly created record} * if LISTHEAD = nil then begin {in this case the list is empty} LISTHEAD: = NEWREGPTR; {to make LISTHEAD point to the newly created record}^ LISTHEAD].NEXTREG: = nil {to signify that the newly created record does not yet have a successor}® end else {the list is not empty and we next check whether the newly created record should be inserted as the first record in the list}7 1 (* предполагается, что вводится ровно семь символов *) 2 (* NEWREGPTR теперь указывает на вновь созданную запись *) 8 (* передача введенного регистрационного номера в поле REGNUMBER вновь созданной записи *) 4 (* в этом случае список пуст *) 6 (* LISTHEAD теперь ссылается на вновь созданную запись *) 6 (* вновь созданная запись является последней в списке *) 7 (* список не пуст, и с помощью следующей инструкции проверяется, не надо ли поместить вновь созданную запись в начало списка *) 125
if REGISTRA'r ON< =LISTHEADf.REGNUMBER then begin {the input registration number alphabetically precedes the first registration number in the list}4 NEWREGPTR}.NEXTREG: = LISTHEAD; {explained in the text below}8 9 LISTHEAD: = NEWREGPTR end else {continued later}10 Предположим, что введенный регистрационный номер 'A136PTR', а первый регистрационный номер, уже помещенный в список, 'A783AFC'. Ситуация, сложившаяся в этом случае перед выполнением инструкции NEWREGPTR f .NEXTREG: = LISTHEAD приведена на рис. 4.11. После выполнения этой инструкции ситу- ация изменится (рис. 4.12). Изменится она и после выполнения инструкции LISTHEAD: = NEWREGPTR (рис. 4.13). Отметим, что присвоение переменной LISTHEAD значения переменной NEWREGPTR приведет к следующей ситу- ации: LISTHEAD будет ссыпаться к тому же значению, что и NEWREGPTR, но LISTHEAD f и NEWREGPTR f будут отли- чаться, т. е. значение записи NEWREGPTR f не будет присвоено записи LISTHEAD f , и обе эти записи останутся в списке. NEWRE6PTR LISTHEAD NEWRE5PTR f ‘Л136РТК’ неопределено LISTHEAD I ‘A783AFC’ указатель к следующему элементу Рис. 4.11. Значения указателей перед выполнением составной инструкции NEWRE6PTR NEWREGPTR f Рис. 4.12. Значения указателей после выполнения составной инструкции 8 (* введенный регистрационный 'номер предшествует первому регистра- ционному номеру списка ♦) 9 (* пояснения приведены ниже *) 10 (* продолжение следует ♦) 126
NEWRE6PTR NEWRE6PTR f Рис. 4.13. Значения указателей после присваивания значения переменной LISTHEAD LISTHEAD В276 РР С * | неопределено Рис. 4.14. Значения указателей после однократного выполнения тела цикла NEWRE6PTR LISTHEAD NEWRE6PTR Рис. 4.15. Значения указателей после второй итерации Продолжим составление процедуры INSERT. Рассмотрим по- дробнее случай, когда новая запись не является первой записью списка, — возможно новая запись даже становится последней в списке. Для движения по списку теперь нужны два указателя; CURRPTR и PREVPTR. С помощью цикла «repeat» постоянно обновляется значение указателя CURRPTR, который исполь- зуется для ссылки на следующую за анализируемой запись. Ниже приведен упрощенный вариант цикла, не предусматрива- ющий случай, когда вводимая запись становится последней. CURRPTR: = LISTHEAD; repeat PREVPTR: = CURRPTR; CURRPTR: = CURRPTRf .NEXTREG until CURRPTR|.REGNO > REGISTRATION; PREVPTR f .NEXTREG: = NEWREGPTR; NEWREGPTRj .NEXTREG. = CURRPTR; 127
LISTHEAD NEWREGPTR Рис. 4.16. Значения указателей после окончания процедуры INSERT Предположим, что первыми тремя регистрационными номерами списка являются 'А136РТК', 'A783AFC' и 'В304РТС' и что вводится номер 'В276РРС'. На рис. 4.14 и 4.15 показаны ситуации, сложившиеся после того, как тело цикла выполнилось соответ- ственно один и два раза. Вторая итерация оказывается последней итерацией цикла, так как CURRPTR f .REGNO становится больше REGISTRATION. После этого принимается решение о том, что введенную запись следует поместить перед третьей записью списка. На рис. 4.16 показаны значения указателей после вы- полнения двух следующих инструкций: PREVPTR 1 .NEXTREG: = NEWREGPTR; NEWREGPTR f .NEXTREG: = CURRPTR Анализ рисунка показывает, что регистрационный номер 'В276РРС' помещен в список правильно. Ниже приведен полный текст процедуры INSERT, учитыва- ющий возможность вставки новой записи в конец списка. Логи- ческая переменная DONE служит для определения условия завершения цикла procedure INSERT; var NEWREGPTR, CURRPTR, PREVPTR: LISTPTR; REGISTRATION: STRING7; DONE: BOOLEAN; begin READLN (REGISTRATION); NEW (NEWREGPTR); NEWREGPTRf.REGNUMBER: = REGISTRATION; if LISTHEAD = nil then begin LISTHEAD: = NEWREGPTR; LISTHEADf. NEXTREG: = nil end else if REGISTRATION < = LISTHEADf.REGNUMBER then begin NEWREGPTRf .NEXTREG: = LISTHEAD; LISTHEAD: = NEWREGPTR end else 128
begin CURRPTR: = LISTHEAD; repeat PREVPTR: = CURRPTR; CURRPTR: = CURRPTRf .NEXTREG; if CURRPTR = nil then DONE: = TRUE else DONE: = CURRPTRf.REGNO> = REGISTRATION until DONE; PREVPTRf .NEXTREG: = NEWREGPTR; NEWREGPTRf .NEXTREG: = CURRPTR end end {of procedure INSERT}/ Использование переменной DONE позволяет избежать про- верки условия CURRPTR f .REGNO > = REGISTRATION в том случае, когда CURRPTR ==; nil. Ниже приведен текст процедуры DELETE: procedure DELETE; var CURRPTR, PREVPTR: LISTPTR; REGISTRATION: STRING7; begin if LISTHEAD = nil then WRITELN (’the list is empty’) else begin READLN (REGISTRATION); {the registration number to be deleted/ CURRPTR: = LISTHEAD; if CURRPTRf .REGNUMBER = REGISTRATION then LISTHEAD: = CURRPTRf.NEXTREG {this makes LISTHEAD point to the second record in the list, thus missing out and thereby deleting the first record}1 2 else repeat PREVPTR: = CURRPTR; CURRPTR: = CURRPTRf.NEXTREG until (CURRPTRf .REGNO = REGISTRATION) or (CURRPTRf .NEXTREG = nil); if CURRPTRf REGNO = REGISTRATION then begin PREVPTRf.NEXTREG: = CURRPTRf .NEXTREG; DISPOSE (CURRPTR) {to release memory space that was occupied by the unwanted record}** end else WRITELN (REGISTRATION, ’is not in the list’) end end; 1 (♦ конец процедуры INSERT ♦) 1 (♦ ввод регистрационного номера, подлежащего удалению из списка *) 2 (♦ теперь LISTHEAD указывает на вторую вались списка; после этого первая вались оказывается удаленной из списка ♦ ) 8 (♦ освобождение памяти, занимаемой записью, удаленной из списка *) 5 Удьнав Дж. 129
USTHEAD CURRPTR PREVPTR В30ЧРТС* nil Рис. 4.17. Значения указателей после завершения выполнения цикла PREVPTR ‘A783AFC’ Рис. 4.18.’ Значения указателей перед вызовом DISPOSE CURRPTR В276РРС ‘В304РТС* Предположим, что список содержит следующие регистрацион- ные номера: 'А136РТК', 'A783AFC', 'В276РРС' и 'В304РТС'. Необходимо удалить номер ,В276РРС'. На рис. 4.17 показана ситуация, сложившаяся после окончания выполнения цикла, а на рис. 4.18 — перед выполнением инструкции DISPOSE. Читателям, наверное, не составит труда самим разобраться в ра- боте процедуры LOOKUP: procedare LOOKUP; var CURRPTR: LISTPTR; REGISTRATION: STRING7; FOUND: BOOLEAN; begin if LISTHEAD = nil then WRITELN (’the list is empty’) ebe begin READLN (REGISTRATION); {registration number to be looked “РГ FOUND: = FALSE; {registration number not yet found in the list}z CURRPTR: = LISTHEAD; while (CURRPTR < > ail) and not FOUND do begin FOUND: = (CURRPTRf. REGNUMBER=REGISTRATION); CURRPTR:=CURRPTR NEXTREG end; if FOUND then WRITELN (‘FOUND’) ebe WRITELN (‘NOT FOUND’) end end; * (♦ ввод регистрационного номера *) 4 (* FOUND— логическая переменная, сигнализирующая о том, что регистрационный номер найден в списке ♦) 130
4.1. Пример заполненной таблице счетов INVOICE- NUMBER (Номер счета) CUSTOMER- NUMBER (Номер клиента) AMOUNT (Сумма) 10 14 65—73 11 12 78-10 12 14 19—50 13 15 35-33 14 12 61-80 15 10 69—90 16 14 12—30 17 15 30—00 18 12 13—19 19 14 50—42 20 14 88-00 4.2. Индекс по номеру счета (для табл. 4.1) CUSTOMER- NUMBER (Номер счета) ARRAY SUBSCRIPTS (Индекс массива) 10 15 11 nil 12 18, 14, 11 13 nil 14 20,19, 16,12 15 17,23 Индексированная таблица счетов. Для того чтобы еще раз продемонстрировать ис- пользование указателей и свя- занных списков, рассмотрим программу, предназначенную для считывания упрощенной версии файла INVOICE (СЧЕТ) в оперативную память, ввода с терминала номера клиента и вывода всех значений INVOICENUMBER и AMOUNT, относящихся к этому клиенту. Будем использовать индекс, который для каждого клиента ука- зывает все связанные с ним номера счетов. Для простоты в про- грамме не предусмотрен контроль входных данных. Ниже при- ведено объявление комбинированного типа, описывающего счета INVRECTYPE = record INVOICENUMBER: 10.20; CUSTOMERNUMBER: 10.. 15; AMOUNT: REAL end В начале программы производится передача данных из двоич- ного файла, состоящего из записей типа INVRECTYPE, в массив INVOICETABLE, объявленный так: INVOICETABLE: array 10 .. 20 of INVRECTYPE Причем каждая запись попадает в тот элемент массива, индекс которого совпадает со значением INVOICENUMBER. Массив, состоящий из записей, часто называют таблицей. Ниже показано возможное содержание таблицы (табл. 4.1). Имена полей, зани- мающих первую строку, на самом деле не присутствуют в таблице. В первом столбце таблицы хранятся индексы массива, которые, в принципе, можно было в нее ве включать. Содержимое индекса * по CUSTOMERNUMBER представлено в табл. 4.2. В первом ее столбце расположены значения CUSTOMERNUMBER, а во вто- * Не следует путать этот индекс, определяемый в разд. 6.1.2, с индексами массива. — Прим. пер. 5* 131
Рис. 4.19. Реализация связанны» списков индексов, приведенные в табл. 4.2 ром — множества, состоящие из всех относящихся к клиенту с заданным CUSTOMERNUMBER индексов массива INVOICE- TABLE. Напомним, что имена полей в памяти не хранятся. Каж- дому CUSTOMER NUMBER соответствуют несколько индексов массива INVOICETABLE. Их удобно хранить в связанных спи- сках; указатели к этим спискам содержатся в массиве CUSTIN- DEX, который использует CUSTOMER NUMBER, как свой соб- ственный индекс (рис. 4.19). Далее следует программа, процедуры которой пока пропущены: program CUSTIND (INVOICE, INPUT, OUTPUT); type CUSTOMERRANGE = 10.15; INVRECTYPE = record INVOICENUMBER: 10..20; CUSTOMERNUMBER: CUSTOMERRANGE; AMOUNT: REAL end; MEMPTRTYPE = f MEMBERTYPE; MEMBERTYPE = record INVOICENUMBER: 10..20; NEXT: MEMPTRTYPE end; var INVOICE: file of INVRECTYPE; INVOICETABLE: array [10 .20] of INVRECTYPE; CUSTINDEX: array [CUSTOMERRANGE] of MRMPTRTYPE; CUSTOMER: CUSTOMERRANGE; CH: CHAR; procedure LOADTABLEFROMFILE; begin {declaration omitted} end; procedure FINDINVOICESOF (CUSTOMER: CUSTOMERRANGE); begin {declaration omitted} * end; begin {body of program}^ LOADTABLEFROMFILE; repeat WRITELN (’please type a customer number in the range 10.. 15’); READLN (CUSTOMER); 1 (* объяснение опущено *) a (* тело программы #) 132
FINDINVOICESOF (CUSTOMER); WRITELN (’type C for another customer’, ’anything else to terminate the program’); READLN (CH) until CH < > *C’ end. Процедура LOADTABLEFROMFILE предназначена для заполнения таблицы INVOICETABLE и для формирования свя- занных списков значений INVOICENUMBER, каждый из которых относится к одному клиенту. Сначала всем элементам массива CUSTINDEX присваивается значение «nil», т. е. считается, что сначала все списки пусты. В том, как заполняется таблица INVOICETABLE, читатель может разобраться самостоятельно. procedure LOADTABLEFROMFILE; var CUSTOMER: CUSTOMERRANGE; INVNO: 10..20; ENTRY: MEMPTRTYPE; INVRECORD: INVRECTYPE; begin for CUSTOMER: = 10 to 15 do CUSTINDEX [CUSTOMER]: = nil; RESET (INVOICE); while not EOF (INVOICE) do begin READ (INVOICE, INVRECORD); INVOICETABLE [INVRECORD.INVOICENUMBER]: = INVRECORD; NEW (ENTRY); {to be inserted into the index, at the head of the list because this is easiest}1 ENTRYf .INVOICENUMBER: = INVRECORD.INVOICENUMBER; {this identifies the record in the INVOICETABLE}1 2 ENTRY}.NEXT: = CUSTINDEX [INVRECORD.CUSTOMERNUMBERJ; CUSTINDEX [INVRECORD.CUSTOMERNUMBERJ: = ENTRY end end; Если функции трех последних инструкций цикла «while» не совсем понятны, разберите работу этих инструкций с помощью дополнительного примера. iB процедуре FINDINVOICESOF предусмотрен один пара- метр — номер клиента CUSTOMER. Он используется как индеко 1 (♦ новый элемент включается в список значений типа MEMBERTYPEj он становится первым в списке *) 2 (# этот элемент определяет положение ваписи в таблице INVOICE- TABLE ♦ ) 133
для обращения к элементам массива CUSTINDEX, а это гаранти- рует доступ к списку индексов таблицы INVOICETABLE. procedure FINDINVOICESOF (CUSTOMER); var CURRENT.MEMPTRTYPE; begin CURRENT: = CUSTINDEX [CUSTOMER); white CURRENT < > nil do begin with INVOICETABLE [CURRENT].INVOICENUMBER] do WRITELN (INVOICENUMBER, AMOUNT); CURRENT: = CURRENT] .NEXT end end; Результат работы процедуры FINDINVOICESOF можно запи- сать на языке реляционной алгебры следующим образом: proj INVOICENUMBER, AMOUNT (sei) CUSTOMERNUMBER = CUSTOMER (INVOICE)) Использование индекса CUSTOMERNUMBER позволяет уменьшить время выполнения операции выбора. В данном случае при поиске всех записей, относящихся к одному и тому же кли- енту, не надо просматривать всю таблицу INVOICETABLE. 4.9. УПРАЖНЕНИЯ 1: а. Используя средства операционной системы, создайте текстовый файл MONTHS, содержащий 12 записей, каж- дая из которых состоит из номера и названия месяца. Например, 1 JANUARY 2 FEBRUARY 3 MARCH и т. д. Сохраните этот файл для использования в дальнейшем. б. Ниже приведена программа, которая сначала считывает данные из файла MONTHS в связанный список записей типа MONTHRECTYPE, а затем вводит номер месяца с терминального устройства и выводит соответствующее номеру название месяца; причем ввод и вывод повторяются до тех пор, пока пользователем не будет введен 0: program MONTHEX (INPUT. OUTPUT, MONTHS); type MONTHRANGE= 1 .12; STR I NG9 = packed array [L.9] of CHAR; MONTHPTR = T MONTHRECTYPE; 134
MONTH RECTYPE = record MONTHNUMBER: MONTHRANGE; MONTHNAME: STRING9; NEXTMONTH: MONTHPTR end; var LISTHEAD, CURRMONTH: MONTHPTR; NUMBER: MONTHRANGE; NUMIN: INTEGER; MONTHS: TEXT; procedure MINSERT; {to be written as an exercise/ procedure OUTPUTNAMEFORMONTHNO (N: INTEGER); {to be written as an exercise/ begin {program body}* LISTHEAD: = nil; {the list is initially empty}"* RESET (MONTHS); for NUMBER: =1 to 12 do begin NEW (CURRMONTH); with CURRMONTHf do READLN (MONTHS, MONTHNUMBER, MONTHNAME); MINSERT {inserts record pointed to by CURRMONTH at head of list because this is the easiest place for insertion/ end; repeat WRITELN (’type month number or 0 to stop program’); READLN (NUM IN); if NUMIN in [L.12] then OUTPUTNAMEOFMONTHNO (NUMIN) until NUMIN = 0 end. Разработайте опущенные в программе MONTHEX процедура MINSERT и OUTPUTNAMEFORMONTHNO. 2. С помощью приводимой ниже программы можно вводить последовательность слов и выводить их на терминальное устрой- ство, можно вставлять и удалять слова из введенной последова- тельности. Каждое слово состоит точно из десяти букв. Последо- вательность слов хранится в связанном списке, который сначала включает одно слово 'ZZZZZZZZZZ'. Последняя запись списка должна быть связана с первой: 1 (♦опущена*) 2 ( *тело программы*) 5 ( *список сначала пуст*) 1 ( ^включение записи, на которую ссылается указатель CURRMONTH, в список; запись становится первой в списке*) 135
program TOYEDITOR (INPUT, OUTPUT); type WORDTYPE = packed array [1..10] of CHAR; WORDPTR = t WORDRECTYPE; WORDRECTYPE = record WORDrWORDTYPE; NEXTWORD: WORDPTR end; var PREVIOUSWORD, CURRENTWORD: WORDPTR; SELECTOR: CHAR; procedure INSERT (CURRENTWORD: WORDPTR); {to be written as an exercise/ procedure DELETE (PREVIOUSWORD: WORDPTR; var CURRENTWORD: WORDPTR); {to be written as an exercise/ begin {program body/ NEW (CURRENTWORD); CURRENTWORDf WORD: = ZZZZZZZZZZ’; CURRENTWORDf. NEXTWORD: = CURRENTWORD; PREVIOUSWORD: = CURRENTWORD; repeat WRITELN (’to find next word type N, to insert a new word type Г); WRITELN (’to delete a word type D, to stop program type X’); READLN (SELECTOR); if SELECTOR in [’N’.’I’,’D’,’X’J then . case SELECTOR of ’N’: begin PREVIOUSWORD: = CURRENTWORD; CURRENTWORD: = CURRENTWORDf. NEXTWORD; WRITELN (CURRENTWORDf WORD) end; T: begin WRITELN (’type the new word, then press the return key’); INSERT (CURRENTWORD) end; D’: DELETE (PREVIOUSWORD, CURRENTWORD); ’X’: WRITELN (’program terminating’) end; until SELECTOR = ’X’ end. На рис. 4.20 показана структура списка после ввода в него двух слов. Список, конец которого связан с началом, называется кольцом. а. Разработайте процедуру INSERT (CURRENTWORD), с по- мощью которой осуществляется ввод нового слова с терминаль- 1 ( * * опущена*) * ( *тело программы*) 136
zzzzzzzzzz always tomorrow Рис. 4.20. Пример кольцевой структуры данных ного устройства и вставка его в кольцо сразу за словом, на которое ссылается CURRENTWORD. Значение CURRENTWORD изме- нять не надо. б. Разработайте процедуру DELETE (PREVIOUSWORD, CURRENTWORD) для удаления слова, на которое ссылается CURRENTWORD. Необходимо, чтобы CURRENTWORD после завершения процедуры указывало на следующее за удаленным слово. в. Используйте программу для ввода слов какого-нибудь предложения, удаления слов и вставки новых слов. Убедитесь, что программа работает правильно. р. Модифицируйте программу таким образом, чтобы в случае ввода пользователем символа Т она обеспечивала вывод на одну строку всех слов, начиная со слова, следующего за 'ZZZZZZZZZZ , и оставляя между словами точно один пробел. 3. Предусмотрите хранение записей в счетах из разд. 4.8.2 в связанном списке, а не в таблице. Если данные о счетах хранить в таблице (массиве), то увеличение множества возможных значе- ний номеров счетов приведет к чрезмерному росту памяти, от- водимой для хранения массива. Если же хранить их в связанном списке, то можно безболезненно расширять диапазон допустимых номеров, например, от 1000 до 9000.
ГЛАВА б ПРОГРАММИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ ПЕРЕМЕННЫХ, СОХРАНЯЮЩИХ СВОИ ЗНАЧЕНИЯ 5.1. высокий и низкий УРОВНИ ПРОГРАММИРОВАНИЯ Сравним два языка программирования. Пользуясь пер- вым, программист просто задает операцию, не учитывая, как эта операция реально будет выполняться. Использование второго языка требует от программиста описания деталей реализации операции. Тогда первый язык считается языком более высокого уровня, чем второй. Рассмотрим следующий оператор языка Паскалы F: = (А + В) *С — D В данном случае применен язык более высокого уровня, чем язык, пользуясь которым программист должен определить все те мелкие шаги, которые выполняются при вычислении приведен- ного выше выражения, например G: = А 4- В; G: « G*C; F: = G — D. Аналогично, когда для считывания с терминала целого числа используется оператор Паскаля READ (X) уровень программирования выше, чем в том случае, когда исполь- зуется язык, который предполагает считывание значения X после- довательно символ за символом. Переходя на язык более низкого уровня, можно выиграть во времени выполнения, но расплачи- ваться за это придется составлением более сложной программы. Гл. 2 была ориентирована на высокий уровень программиро- вания. Например, при описании операций выбора не уточнялось, какие именно действия будет производить ЭВМ, выполняя ту или иную операцию. В гл. 5—7 будут детально рассмотрены шаги, которые выполняет ЭВМ, осуществляя как операцию выбора, так и другие операции, связанные с файлами. Кроме того, методы управления этими шагами будут описаны подробнее, чем это сделано в гл. 2. Программирование на таком более низком, чем раньше, уровне будет иногда давать возможность достичь большей эффективности. Кроме того, появится возможность делать с дан- ными все, что угодно, без ограничений, о которых было упомянуто в разд. 2.16 и 2.17. 138
Одна из основных целей, которую пытается достичь автор в гл. 6 и 7, заключается в том, чтобы дать возможность читателю увидеть, как соотносится программирование баз данных с про- граммированием на языке Паскаль. Для этого, а также для того, чтобы можно было выполнить упражнения гл. 6, в этой главе вводится простое нестандартное расширение языка Паскаль. Предполагается, что читатели знакомы с комбинированным и ссы- лочным типами языка Паскаль. Если это не так, то сначала им следует разобрать материал гл. 4. 5.2. ОПИСАНИЕ БАЗ ДАННЫХ НА ПАСКАЛЕ Если в обычной программе на Паскале объявляется такая структура данных, как связанный список или таблица, то после завершения работы программы эта структура исчезает. В этой главе в Паскаль вводятся дополнительные средства, поз- воляющие определенным переменным сохранять значения после завершения работы программы *. Эти переменные, использовать которые в обычном Паскале нельзя, будем называть переменными, сохраняющими свои значения. Используя такие переменные, можно, например, создавать и пользоваться различными индек- сами, которые будут сохраняться и после завершения работы программы. Переменные, сохраняющие свои значения, описываются в спе- циальном объявлении базы данных. Такое объявление похоже на декларативную часть обычной программы на Паскале, правда в нем нет описаний процедур и функций. Однако в объявления баз данных можно включать описания констант и типов по прави- лам стандартного Паскаля. Обычная программа на языке Паскаль начинается с заголовка. Присутствие заголовка говорит о том, что далее следует сама программа. Объявление базы данных начинается с зарезервированного слова DBDNAME, за которым следует задаваемое программистом имя. Описание базы данных всегда завершается словом «finish». В дальнейшем аббревиатура DBD будет использоваться вместо ’database declaration’. Ниже приведен очень простой пример: DBDNAME АР; const PI = 3.142875; var RADIUS: INTEGER; finish Это объявление позволяет использовать константу PI и пере- менную RADIUS в нескольких программах. Значение, присвоен- ное переменной RADIUS в одной программе, будет сохранено * Значения этих переменных сохраняются во внешней памяти. — Прим, пер. 139
после завершения работы этой программы и будет доступно дру- гим программам, начавшим работу после первой. В следующем разделе будет приведен пример, иллюстрирующий это правило. Значения описанным в DBD переменным не присваиваются не- посредственно в DBD, поскольку присваивание значений пере- менным не может быть произведено в декларативной части про- грамм, написанных на Паскале. 5.3. ПРОГРАММЫ, ОБРАЩАЮЩИЕСЯ К БАЗЕ ДАННЫХ В любой программе, содержащей обращение к базе данных, после заголовка программы должна следовать инструкция invoke (имя); В этой инструкции (имя) является именем того DBD, к кото- рому программа обращается. Каждая программа может обра- щаться только к одному DBD, но сразу несколько программ могут обращаться к одному и тому же DBD. Объявления, содер- жащиеся в вызываемом DBD, считаются как бы присутствующими в вызывающей программе. Более подробно этот вопрос будет рассматриваться позднее. Переменные, сохраняющие свои значе- ния, можно использовать так же, как и любые другие переменные Паскаля, но надо помнить, что их значения после завершения выполнения программы не теряются. Перед тем как использовать переменную, сохраняющую свое значение, необходимо вызвать процедуру STARTTRANSACTION. Эта процедура имеет один параметр. Он может быть либо UN- RESTRICTED либо READONLY. Если в программе предусмо- трена инициализация, удаление или изменение значения пере- менной, необходимо использовать параметр UNRESRICTED. В том же случае, когда никаких изменений переменных, сохра- няющих свои значения, производить не планируется, нужен параметр READONLY. После завершения обращения к пере- менным, сохраняющим свои значения, необходимо вызвать про- цедуру с именем ENDTRANSACTION. Таким образом, все обра- щения к переменным, сохраняющим свои значения, должны на- ходиться между вызовами процедур STARTTRANSACTION и ENDTRANSACTION. Если между вызовами процедур STARTTRANSACTION и ENDTRANSACTION что-то, по мнению программиста, выпол- няется не так как надо, то можно предусмотреть вызов специаль- ной процедуры ABORTTRANSACTION. Эта процедура аннули- рует все те изменения, которые произошли с переменными, сохра- няющими свои значения, после вызова STARTTRANSACTION, и присваивает им старые значения. Далее будет показано, почему необходимо использовать STARTTRANSACTION, END TRAN- SACTION и ABORTTRANSACTION. 140
Ниже приводится пример использования DBD с именем АР из разд. 8.2. Программа, работающая первой, обычно вводит значения переменной RADIUS с терминала. Такая программа может быть использована как для инициализации значения пере- менной RADIUS, так и для присваивания переменной RADIUS нового значения. program RUPDATE (INPUT, OUTPUT); invoke AP; {this tells the computer that this program will refer to DBD AP}* var SELECTOR: CHAR; begin STARTTRANSACTION (UNRESTRICTED); WRITELN (’please type the integer value of RADIUS’); READLN (RADIUS); WRITE (’if you are happy with what you have typed type Y ’); WRITELN (’else type N’); READLN (SELECTOR); if SELECTOR < > ’Y* then begin ABORTTRANSACTION; WRITELN (’value of RADIUS has not been changed’) end else ENDTRANSACTION end. Основная цель этого примера заключается в том, чтобы пока- зать, что значение переменной RADIUS сохраняется после завер- шения работы программы и, следовательно, может быть исполь- зовано другими программами, которые вызывают то же самое DBD, например, программой, приведенной ниже: program ENQUIRE (INPUT, OUTPUT); invoke AP; var SELECTOR: CHAR; begin STARTTRANSACTION (READONLY); repeat WRITE ( to get RADIUS type R, CIRCUMFERENCE type C,’); WRITELN (’ AREA type A, terminate program type X’); READLN (SELECTOR); if not (SELECTOR in [ R’,’C’,’A’,’X ]) then SELECTOR: = F’; case SELECTOR of R’: WRITELN (’RADIUS = ’, RADIUS); ® (* эта инструкция показывает, что программа будет обращаться к DBD АР ♦ ) 141
’C’: WRITELN ( CIRCUMFERENCE = 2*PI*RADIUS); ’A’: WRITELN (’AREA= PI*sqr (RADIUS)); ’X’.’F’: end until SELECTOR =’X’; ENDTRANSACTION end. В этой программе непосредственно используются значения PI и RADIUS. Это правомерно, поскольку как PI, так и RADIUS описаны в вызываемом программой DBD АР. Значение переменной RADIUS, которое в этой программе дей- ствительно используется, можно получить, предварительно вы- полнив программу RUPDATE. Программу ENQUIRE можно выполнить несколько раз, не изменяя при этом значения перемен- ной RADIUS. Приведенный выше пример демонстрирует технику использования переменных, сохраняющих свои значения, но он слишком прост, чтобы иметь практический смысл. Следующий пример значительно ближе к реальности. Простая таблица товарных запасов. Таблица товарных запасов описывается о помощью переменной, сохраняющей свое значение. Каждая строка или элемент таблицы характеризует конкретный товар. В нее могут, например, входить цена, количество име- ющегося в наличии товара, номер поставщика. Для простоты предположим, что каждому элементу таблицы поставлено в соот- ветствие значение переменной ITEMREFNO из интервала 100. 140, и что каждый товар имеет только одного поставщика и одну цену. Тогда DBD для этого примера будет иметь вид «уре REFRANGE= 100.. 140; INVTYPE=record QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: 10.. 99; NORECORD: BOOLEAN end; var INVTABLE: array [REFRANGE] of INVTYPE; finish В записи INVTYPE не предусмотрено поле ITEMREFNO, поскольку ITEMREFNO впоследствии будет играть роль индекса INVTABLE. (Подобная схема применялась выше для индекси- рованной таблицы счетов в разд. 4.8.2). Первая программа, кото- рая будет использовать DBD IX, присваивает значение TRUE полям NORECORD всех записей таблицы. Это говорит о том, что таблица первоначально пуста. 142
\ program INITIALIX (OUTPUT); invoke IX; var I: REFRANGE; begin STARTTRANSACTION (UNRESTRICTED); for I: =100 to 140 do INVTABLE [IJ.NORECORD: = TRUE; ENDTRANSACTION; WRITELN (’initialization completed’) end. Анализ этой программы показывает, что после вызова DBD можно использовать описанные в DBD типы точно так же, как если бы ойи были описаны в декларативной части программы. Например, в программе INITIALIX используется тип REF- RANGE. Следующая программа считывает информацию с терминаль- ного устройства и помещает ее в таблицу. Можно сначала ввести несколько записей в таблицу и закончить выполнение программы, а затем, запустив эту же программу снова, закончить ввод новых записей или, если необходимо, переопределить те значения, которые были введены ранее. program ENTERDATA (INPUT, OUTPUT); invoke IX; var ITEMREFNO: REFRANGE; SELECTOR: CHAR; begin STARTTRANSACTION (UNRESTRICTED); repeat WRITELN ('if you wish to enter another record type Y else type N'); READLN (SELECTOR); if SELECTOR ='Y' then begin WRITE ( type ITEMREFNO. QUANTITYINSTOCK, '); WRITELN ('PRICEPERUNIT, SUPPLIERNO); READ (ITEMREFNO); with INVTABLE [ITEMREFNO] do begin READLN (QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); NORECORD: = FALSE end end until SELECTOR < > Y’; ENDTRANSACTION end. 143
Желательно иметь возможность проверить, лежат ли значения всех полей в пределах заданных интервалов, например ITEM- REFNO в пределах от 100 до 140. Такая проверка не была пред- усмотрена, поскольку она сделала бы программу слишком слож- ной и запутанной. Следующая программа по значению ITEMREFNO осуще- ствляет поиск и выводит на терминал соответствующие значения QUANTITYINSTOCK, PRICEPERUNIT и SUPPLIERNO. program LOOKUPIX (INPUT, OUTPUT); invoke IX; var ITEMREFNO: REFRANGE; begin STARTTRANSACTION (READONLY); WRITELN (’please type ITEMREFNO ); READLN (ITEMREFNO); with INVTABLE (ITEMREFNO] do if NORECORD then WRITELN (’no record available in the table’) else begin WRITE (’QUANTITY IN STOCK = ’, QUANTITYINSTOCK); WRITE ( PRICEPERUNIT = ’, PRICEPERUNIT: 10:2); WRITELN (’SUPPLIER NUMBER = ’, SUPPLIERNO) end: ENDTRANSACTION end. Проанализировав программу, читатель легко разберется, для чего введено и как используется поле NORECORD. Если необ- ходимо просмотреть данные для нескольких значений ITEM- REFNO, следует несколько раз запустить программу LOOKU- PIX. Правда, то же самое можно сделать, используя цикл «ге- реаЬ и запуская программу только один раз (см. программу ENTERDATA). 5.4. УКАЗАТЕЛИ, СОХРАНЯЮЩИЕ СВОИ ЗНАЧЕНИЯ В принципе указатели, сохраняющие свои значения, это обычные указатели Паскаля. Основные отличия заключаются в том, что если Р переменная ссылочного типа, описанная в DBD, то, во-первых, необходимо задавать CREATE (Р) вместо NEW (Р), которая применяется в том случае, если Р было объявлено в про- грамме, а не в DBD; н, во-вторых, необходимо использовать DELETE (Р) вместо DISPOSE (Р). Вне зависимости от того, где описано Р, в программе или в DBD, переменная, создаваемая с помощью инструкции CRE- ATE (Р), сохраняет свое значение после завершения выполнения 144
программн до тех пор, пока не будет выполнена инструкция DELETE (Р). Указатель Р сохраняет свое значение после окон- чания программы только, если Р объявлен в DBD. Для того чтобы показать, как можно использовать указатели, сохраняющие свои значения, расширим описание таблицы товар- ных запасов из разд. 5.3. Для этого определим индекс SUPPLI- ERNO. Индекс будет использоваться для поиска всех товаров, поставляемых данным поставщиком. Такой способ использования индекса анализируется в разд. 4.8.2, где описана индексированная таблица счетов. Правда, в данном случае в отличие от примера, разобранного в разд. 4.8.2, индекс будет сохраняться после завершения выполнения программы, присвоившей ему значение. В связи с этим не нужно будет заново формировать индекс каждый раз, когда он понадобится. Ниже приведено DBD, являющееся логическим расширением DBD IX, рассмотренного ранее: DBDNAME JX; ‘УРе REFRANGE =100.140; SUPPRANGE = 10.. 99; INVTYPE = record QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: SUPPRANGE; NORECORD: BOOLEAN end; MEMPTRTYPE = | MEMBERTYPE; MEMBERTYPE = record ITEMREFNO: REFRANGE; NEXT: MEMPTRTYPE end; var INVTABLE: array [REFRANGE] of INVTYPE; SUPPINDEX: array [SUPPRANGE] of MEMPTRTYPE; finish Для каждого поставщика переменная SUPPINDEX указывает на связанный список значений ITEMREFNO, являющихся ссыл- ками на товары, поставляемые данным поставщиком. Перед тем, как работать с таким DBD, необходимо всем значениям массива SUPPINDEX присвоить значение «nil». Эго делается с помощью следующей программы: program INITKX (OUTPUT); invoke КХ; var I: REFRANGE; 145
begin ! STARTTRANSACTION (UNRESTRICTED); z for I: = 100 to 140 do INVTABLE [IJ.LIST: = nil; ' ENDTRANSACTION end; Приведенная ниже программа предназначена для ввода одной записи типа INVTYPE в массив* INVTABLE и модификации индекса, указывающего на первый элемент списка, состоящего из записей типа MEMBERTYPE и относящегося к одному/постав- щику. В том случае, если необходимо ввести более одноц записи, программу следует запустить несколько раз. program ENTERJX (INPUT, OUTPUT); invoke JX; var ITEMREFNO: REFRANGE; SUPPNO: SUPPRANGE; ENTRY: MEMPTRTYPE; begin STARTTRANSACTION (UNRESTRICTED); WRITE (’please type ITEMREFNO, QUANTITYINSTOCK,’); WRITELN (’PRICEPERUNIT, SUPPLIERNO ); READ (ITEMREFNO); with INVTABLE (ITEMREFNO] do begin READLN (QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); SUPPNO: = SUPPLIERNO; NORECORD: = FALSE end; CREATE (ENTRY); ENTRYf .ITEMREFNO: = ITEMREFNO; ENTRY|.NEXT: = SUPPINDEX [SUPPNO]; SUPPINDEX [SUPPNO]: • ENTRY; {this makes SUPPINDEX [SUPPNO] point to the newly created record, which now points to the record that was previously first in the list}1 ENDTRANSACTION end. ' Поскольку MEMBERTYPE описано в DBD, ENTRY является указателем к переменной, сохраняющей свое значение после завершения выполнения ENTERJX. Заметим, что ENTRY не является переменной, сохраняющей свое значение, поскольку она объявлена не в DBD. Следовательно, значение ENTRY после завершения выполнения программы ENTERJX не сохранится. 1 (* SUPPINDEX [SUPPNO] указывает на вновь созданную запись, со- держащую в свою очередь ссылку на запись, которая была первой ранее ♦) 146
Но сохраняется значение элемента массива SUPPINDEX [SUP- NO L которое равно ENTRY. Кроме этого SUPPINDEX пред- ставляет собой массив указателей к связанному списку перемен- ных типа MEMBERTYPE, сохраняющих свои значения после завершения работы программы ENTERJX. Для того чтобы показать, как можно использовать индекс, вниманию читателей предлагается программа, которая считывает с терминального устройства номер поставщика и выводит все записи из таблицы товарных запасов, характеризующие товары, поставляемые данным поставщиком. program SUPPQUERY (INPUT, OUTPUT); invoke JX; var SUPPNO: SUPPRANGE; CURRPTR: MEMPTRTYPE; begin WRITELN (’please type a supplier number’); READLN (SUPPNO); STARTTRANSACTION (READONLY); CURRPTR: = SUPPINDEX [SUPPNO]; if CURRPTR = nil then WRITELN (’no items are supplied by supplier ’, SUPPNO) else repeat WRITE (’ITEMREFNO = ’, CURRPTRf .ITEMREFNO); with INVTABLE [CURRPTRj.ITEMREFNO] do begin WRITE (’QUANTITY IN STOCK = ’, QUANTITYINSTOCK); WRITELN (’PRICE PER UNIT== ’, PRICEPERUNIT) end; CURRPTR: = CURRPTR} .NEXT until CURRPTR = nil; ENDTRANSACTION end. Чтобы найти любой элемент таблицы товарных запасов по его номеру, можно использовать приведенную выше программу LOOKUPIX, заменив в ней только вызов IX на вызов JX. Следующая программа предназначена для ввода номера эле- мента таблицы товарных запасов и' уничтожения этого элемента. Кроме того, с ее помощью удаляется и ссылка на этот элемент из списка поставщиков. program DELJX (INPUT, OUTPUT); invoke JX; var SUPPNO: SUPPRANGE; ITEMREFNO; REFRANGE; CURRPTR, PREVPTR: MEMPTRTYPE; 147
begin STARTTRANSACTION (UNRESTRICTED); WRITELN (’type item reference number of record to be deleted’); READLN (ITEMREFNO); SUPPNO: = INVTABLE [ITEMREFNO].SUPPLIERNO; INVTABLE [ITEMREFNO].NORECORD: = TRUE; {this effectively deletes the record from the table/ CURRPTR: = SUPPINDEX [SUPPNO]; if CURRPTRf.ITEMREFNO = ITEMREFNO then {delete the first record from this supplier's linked list in the index}2 SUPPINDEX [SUPPNO]: = CURRPTRf.NEXT {to make the index point to the second record in the linked list}^ else {make CURRPTR point to the record to be deletedr begin repeat PR EVPTR: = CURRPTR; CURRPTR: = CURRPTRf .NEXT until CURRPTRf.ITEMREFNO= ITEMREFNO; PREVPTRf NEXT: = CURRPTRf NEXT end; DELETE (CURRPTR); ENDTRANSACTION end; Удаление элемента из списка поставщиков основано на том же методе, который используется и в разд. 4.8.2 в процедуре DELETE. Этот метод подробно разбирается в том же разделе с помощью диаграмм. Единственное отличие состоит в том, что в рассмотренном примере предполагается, что ссылка на любой элемент таблицы товарных запасов обязательно присутствует в связанном списке поставщиков. Приведем еще один пример. Учет динамики цен. В примере, связанном с таблицей товар- ных запасов, считалось, что каждый товар имеет одну строго опре- деленную цену. Здесь же предполагается, что цена одного и того же товара может меняться. В связи с этим необходимо хра- нить информацию о ценах и датах их изменения. Тогда в допол- нение к каждой строке таблицы товарных запасов, соответствую- щей одному товару, необходимо хранить^.связанный список, каждый элемент которого включает цену и дату. Причем элемент, содержащий максимальную цену, помещается в начало списка. На рис. 5.1 показано, как может выглядеть структура данных для нескольких произвольно выбранных строк таблицы. Анализ этой структуры показывает, что цена товара, соответствующего 8 (* удаление записи из таблицы ♦) 2 (* удаление первой записи из связанного списка поставщиков *) 3 (* формирование указателя, ссылающегося на вторую запись в свя- занном списке поставщиков *) 4 (* формирование такого значения CURRPTR, которое указывало бы на запись, подлежащую удалению «) 148
QUANTITY INSTOCK (количество на складе) SUPPLIER LIST PRICE w lm) m'*a) поставщика) BATE NEXT PRICE DATE NEXT (дата) (указатель к (цела) (дата) (указатель к следующему следующему _______элементу) элементу) 18 23 55 9 Рис. 5.1. Структура данных для примера о товарных запасах первой строке таблицы, выросла с 10.90 долл. 9 января 1992 р. до 12.30 долл. 12 июня 1993 р. Цена товара, соответствующего второй строке таблицы, менялась три раза, и поэтому соответ- ствующий список содержит четыре элемента. Товар, данные о котором помещены в третьей строке таблицы, имеет одну неиз- менную цену. Информацию о ценах, относящихся к одному товару, удобно размещать в связанном списке, поскольку разные товары могут иметь различное число отличающихся друг от друга цен. Альтернативный вариант заключается во включении в запись о товаре массива, содержащего данные о ценах array Ц..101 of record PRICE : REAL; DATE : 880101..980101 end. Верхняя граница массива произвольно взята равной 10. Если верхняя граница выбрана достаточно большой для того, чтобы разместить наибольшее число различных цен, относящихся к од- ному товару, то многие элементы массива для товаров с одной или двумя ценами останутся незаполненными. Этот вопрос яв- ляется частным случаем проблемы эффективности размещения данных переменной длины. В рассматриваемом примере вместо массивов используются списки, поскольку нежелательно иметь неиспользуемые (или пустые) элементы массива. В случае применения списков необходимо вводить указатели, которые тоже занимают память, правда небольшую. Очевидно, что указатели не нужны, если вместо списков использовать мас- сивы. Для простоты в этом примере не предусмотрено индекса по номеру поставщика SUPPLIERNO. Поэтому рассматриваемый пример используется лишь для того, чтобы показать, как в таб- лицу товарных запасов можно включить структуру для размеще- ния произвольного числа цен. Кроме того, в записи INVTYPE 149
отсутствует поле NORECORD. Это правомерно, поскольку, если значением поля LIST является «nib, то считается, что NORE- CORD = TRUE. DBDNAME КХ; type SUPPRANGE = 10..99; REFRANGE = 100.. 140; DPTRTYPE = J DPRECTYPE; DPRECTYPE = record PRICE: REAL; DATE: 880101..980101; DNEXT:DPTRTYPE end; INVTYPE = record QUANTITYINSTOCK: INTEGER; SUPPLIERNO: SUPPRANGE; LIST: DPTRTYPE end; var INVTABLE: array [REFRANGE] of INVTYPE; finish Программа инициализации для DBD КХ будет иметь вид program INITJX (OUTPUT); invoke JX; var I: REFRANGE; J: SUPPRANGE; begin STARTTRANSACTION (UNRESTRICTED); for I: -100 to 140 do INVTABLE[I].NORECORD: = TRUE; for J: = 10 to 99 do SUPPINDEX [I]: = nil; ENDTRANSACTION; WRITELN (’initialization completed’); end. В разд. 5.3 приведена программа ENTERDATA, предназна- ченная для ввода нового элемента в таблицу INVTABLE. В том случае, если нужно модифицировать одну строку таблицы, на- пример, изменить значение поля QUANTITYINSTOCK, можно использовать программу ENTERDATA, в которой предусмотрено заполнение новыми значениями всех полей, даже тех, которые в данном случае менять не требуется. Для того чтобы не прово- дить лишних действий по изменению значений полей, и, в част- ности, для того, чтобы не устанавливать указатель, когда изме- нять цену нет необходимости, предлагается применять различные программы: одну — для ввода новых строк, другую — для из- менения старых. Следующая программа предназначена только для ввода новых строк. Изменять с ее помощью старые строки нельзя. 150
program ENTERKX (INPUT, OUTPUT); invoke KX; var ITEMREFNO: REFRANGE; PDENTRY. DPTRTYPE; begin STARTTRANSACTION (UNRESTRICTED); WRITELN (’please type ITEMREFNO’); READLN (ITEMREFNO); if INVTABLE [ITEMREFNO].LIST < > nil then WRITELN (’program terminating because this is not a new ITEM ) else begin CREATE (PDENTRY); WRITE (’type QUANTITY IN STOCK, SUPPLIER NO, ’); WRITELN ( DATE and PRICE ); with INVTABLE [ITEMREFNO] do READ (QUANTITYINSTOCK, SUPPLIERNO); with PDENTRY] do READLN (DATE, PRICE); PDENTRYf.DNEXT: =nil; INVTABLE [ITEMREFNO],LIST: = PDENTRY end; ENDTRANSACTION end. Теперь рассмотрим программу, предназначенную для модифи- кации уже существующей строки таблицы товарных запасов. Эта программа помещает на место старого значения поля QUAN- TITYINSTOCK новое значение. Кроме того, она также позволяет провести включение новой цены и даты в начало связанного списка, не уничтожая старых значений. program UPDAKX (INPUT, OUTPUT); invoke KX; var PDENTRY: DPTRTYPE; ITEMREFNO: REFRANGE; SELECTOR: CHAR; begin STARTTRANSACTION (UNRESTRICTED); WRITELN (’Please type ITEM REF NO’); READLN (ITEMREFNO); if INVTABLE [ITEMREFNO].LIST = nil then WRITELN (’program terminating because item unknown’) else with INVTABLE [ITEMREFNO] do begin WRITELN ( to update QUANTITY IN STOCK type Y else N ); READLN (SELECTOR); if SELECTOR = Y’ then 151
begin WRITELN (’type QUANTITY IN STOCK’); READLN (QUANTITYINSTOCK) end; WRITELN (’if you wish to enter new price type Y else type N’); READLN (SELECTOR); if SELECTOR = ’Y’ then begin WRITELN (’type new price and date’); CREATE (PDENTRY); with PDENTRYT do READLN (PRICE,DATE); PDENTRYf DNEXT: = LIST; LIST: = PDENTRY end end; ENDTRANSACTION end. Во всех приведенных выше программах в принципе необхо- димо проверять, находятся ли вводимые данные в пределах вы- , деленных им диапазонов. Например, в программе UPDAKX * дата должна лежать в интервале 880101, ...,980101. Чтобы не усложнять логику программ, такого рода контроль в уже разра- ботанных программах не предусматривался. Следующая программа считывает номер товара и дату о тер- минального устройства и выводит цену, которую этот товар имел в период времени, включающий эту дату. program GETPRICEATDATE (INPUT, OUTPUT); invoke KX; var PRICEPTR: DPTRTYPE; ITEMREFNO: REFRANGE; SEEKDATE: 880101 ..980101; FOUND: BOOLEAN; begin STARTTRANSACTION (READONLY); WRITELN ('please type the required ITEM REF NO and DATE’); READLN (ITEMREFNO, SEEKDATE); PRICEPTR: = INVTABLE [ITEMREFNO].LIST; FOUND: = FALSE; while (PRICEPTR < > nil) and not FOUND do if PRICEPTRJ.DATE < = SEEKDATE then FOUND: = TRUE else PRICEPTR: = PRICEPTRf.DNEXT; if FOUND then WRITELN (’price at that date was ’, PRICEPTRJ.PRICE:8:2) else 1 WRITELN (’the item was not in the inventory at that date’); ENDTRANSACTION end. 152
Программа просматривает соответствующий введенному зна- чению ITEMREFNO список, состоящий из пар цена—дата, до тех пор, пока не обнаружит первую дату, которая меньше или равна введенной дате. В том случае, если поиск успешно завершен, на печать выводится цена, связанная с найденной датой. Все новые даты помещаются в начало списка с помощью программы UPDAKX. 5.5. О ДОПОЛНИТЕЛЬНЫХ ВОЗМОЖНОСТЯХ ПАСКАЛЯ ПО РАБОТЕ С БАЗАМИ ДАННЫХ Тех средств, которые были описаны в этой главе, не- достаточно для решения задач, которые будут поставлены в гл. 7 и 8. Для того чтобы сделать работу с базами данных на Паскале более эффективной, в разд. 6.8 будут введены дополнительные средства. 5.6. УПРАЖНЕНИЯ 1. В этом упражнении используется следующее объяв- ление базы данных: DBDNAME El: const LENGTH = 24; type ’ RANGE = I ..LENGTH: CHARSTRING = packed array [RANGE] of CHAR; var TEXTLINE: CHARSTRING: finish а. Напишите программу считывания о терминального устрой- ства в TEXTLINE последовательности символов, длина которой не больше, чем LENGTH. Значение переменной TEXTLINE должно сохраняться после завершения выполнения программы. б. Напишите программу считывания с терминального устрой- ства двух целых чисел, принадлежащих интервальному типу 1..LENGTH. Предположите, что эти целые числа имеют имена L и U, причем L не больше, чем U. Необходимо, чтобы Ваша про- грамма выводила на терминальное устройство символы TEXT- LINE [Ll, TEXTLINE IL + 1], ..., TEXTLINE [U] и разме- щала их на одной строке. в. Введите текстовую строку с помощью программы, которую Вы написали, выполнив 1, а, и затем выведите часть строки с по- мощью программы, которую Вы написали, выполнив I, б. Прого- ните программу из 1,6 несколько раз для вывода различных частей строки. 153
2. Задана матрица, строки и столбца которой помечена ла. тинскими буквами А, В, ...» F: А В G D Е F А 0 В 12 0 G 15 6 0 D 10 30 14 0 Е 8 23 22 40 0 F 12 6 22 12 45 0 Буква соответствуют городам, а элемента матрица — рас- стояниям между городами. а. Напишите DBD, которое можно использовать в програм- мах из 2, б и 2, в. б. Напишите программу для считывания значений элементов матрицы с терминального устройства в двухмерный массив, опи- санный в DBD из 2, а. Матрица должна содержать только значе- ния расстояний. Необходимо, чтобы значения элементов матрицы после завершения выполнения программы сохранялись: они будут использоваться в следующей программе. в. Напишите программу для считывания последовательности из трех букв, каждая из которых принадлежит интервалу А, ..., F. Определите общую длину пути, начинающегося в городе, соответ- ствующем первой букве, кончающегося в городе, соответствую- щем третьей букве, и проходящего через город, обозначаемый второй буквой. Например, предположим, что введена последова- тельность BED. В результате работы программы на терминальное устройство должно быть выведено число 63, которое является суммой расстояний между В и Е и между Е и D. 3. а. Составьте DBD, которое включает следующие объяв- ления: var DIGITWORDS: array [1..40] of CHAR; WHEREITSTARTS: array ГО'./9'l of I..40; Используйте это DBD для выполнения 3, б, в и г. б. Напишите программу для считывания с терминального устройства в массив DIGITWORDS строки символов ZERO- ONETWOTHREEFOURFIVESIXSEVENEIGHTNINE. Обратите внимание на то, что эта строка состоит из наименований цифр ZERO, ONE и т. д., следующих друг за другом без разделяю- щих пробелов. в. Напишите программу считывания с терминального устрой- ства в массив WHEREITSTARTS десяти целых чисел 1, 5, 8, II, 16, 20, 24, 27, 32, 37. Смысл этих целых чисел определяется 154
гак: подстрока ZERO в строке DIGITWORDS размещается, начиная с 1-й позиции, подстрока ONE в строке DIGITWORDS размещается, начиная с 5-й позиции, подстрока THREE — а 11-й позиции и т. д. V. Напишите программу для считывания о терминального устройства символа. Если этот символ — цифра, то Ваша про- грамма должна выводить на терминал наименование цифры, используя информацию из массивов WHEREITSTARTS и DI- GITWORDS. Например, если вводится символ 5, то выводиться должна строка FIVE. 4. В этом упражнении простая таблица товарных запасов из разд. 5.3 модифицирована таким образом, что строки таблицы размещаются не в массиве, а в связанном списке. Элементы списка упорядочены по возрастанию значения ITEMREFNO. Все зна- чения этого поля лежат теперь в пределах от 1000 до 9000, но надо помнить, что обычно список содержит только несколько эле- ментов. Если использовать для хранения массив, то многие его элементы останутся незаполненными, т. е. память будет расходо- ваться впустую. Ниже следует DBD: DBDNAME JY: type REFRANGE= 1000.9000; INVPTRTYPE = JINVTYPE; INVTYPE = record ITEMREFNO: REFRANGE; QUANTITYINSTOCK: INTEGER: PRICEPERUNIT: REAL; SUPPLIERNO: 10.99; NEXTITEM: INVPTRTYPE end; var STARTPOINTER: INVPTRTYPE; {this will point to the first record in the list: finish Далее приводится программа инициализации: program INITIALIZEJY (OUTPUT);* invoke JY; begin STARTTRANSACTION (UNRESTRICTED); STARTPOINTER: = nil; WRITELN ('initialization completed’); ENDTRANSACTION end. 1 (♦ этот указатель ссылается на первый элемент в списке *) 155
а. Напишите программу, которая считывает строку таблицы товарных запасов с терминального устройства и вводит ее в свя- занный список таким образом, что список остается упорядочен- ным в порядке возрастания значения ITEMREFNO. Перед вы- полнением этого упражнения еще раз внимательно просмотрите процедуру INSERT в разд. 4.8.2 и программу ENTERDATA в разд. 5.3. б. Напишите программу, которая считывает значение ITEMREFNO с терминального устройства и выводит соответ- ствующие значения QUANTITYINSTOCK, PRICEPERUNIT и SUPPLIERNO. В программе также необходимо предусмотреть специальное сообщение в том случае, если введенное значение ITEMREFNO не найдено в списке.
ГЛАВА в ВВЕДЕНИЕ В ОРГАНИЗАЦИЮ ФАЙЛОВ 6.1. ОРГАНИЗАЦИЯ ФАЙЛОВ И ДОСТУП К НИМ В первых трех главах файлы рассматривались как наборы записей, сохраняющиеся во внешней памяти после завер- шения работы программы. Вопросы, связанные со структурой файлов и размещением файлов в памяти, в тех главах не затра- гивались. В этой главе описываются традиционные методы орга- низации файлов, обеспечивающие эффективный доступ к файлам. 6.2. СОРТИРОВКА В разных приложениях часто встречаются наборы записей, порядок расположения которых не случаен. В качестве первого шага к введению в методы организации файлов остано- вимся на терминологии, описывающей порядок сортировки. Пред- положим, что имеется файл, состоящий из восьми записей; каждая запись делится на четыре поля Fl, F2, F3 и F4: F1 ас ап bd ck de dw da ea F2 А А С А N Р В D F3 62 74 13 21 93 18 22 26 F4 J J J К К К J J Файл отсортирован в порядке возрастания файл: F1 de ап ас еа da ck dw bd по F1, если записи файла расположены значения поля F1. Например, следующий F2 F3 F4 N 93 К А 74 J А 62 J D 26 J В 22 J А 21 К Р 18 К С 13 J упорядочен по убыванию F3. 157
Поле или несколько полей, которые полностью определяют последовательность расположения записей, называются ключом сортировки. В предыдущих примерах ключ сортировки состоял из одного поля; в приводимом ниже примере два поля F1 и F2 составляют ключ сортировки: F1 F2 F3 F4 2 А 62 J 2 G 74 и 2 N 62 J 3 В 62 К 4 С 21 К 4 н 21 к 5 р 74 J 5 X 62 J Этот файл отсортирован по F2 внутри F1. Записи файла рас- положены в порядке возрастания значения поля F1, а если зна- чение этого поля у нескольких записей одинаково, то они располо- жены в порядке возрастания значения поля F2. Таким образом F2 меняется быстрее, чем FL В этом случае F1 называется главным ключом сортировки, a F2 дополнительным ключом сортировки. Если вспомнить файл НЕОПЛАЧЕННЫЕ.СЧЕТА, то для него СРОК-ОПЛАТЫ и ДАТА.СЧЕТА соответственно главный и до- полнительный ключи сортировки. Файл НЕОПЛАЧЕННЫЕ. СЧЕТА отсортирован по полю ДАТА.СЧЕТА внутри поля НО- МЕР. ПОСТАВЩИКА и по полю НОМЕР. ПОСТАВЩИКА вну- три поля СРОК-ОПЛАТЫ, т. е. записи, имеющие одинаковые значения полей СРОК-ОПЛАТЫ и НОМЕР.ПОСТАВЩИКА, отсортированы по полю ДАТА.СЧЕТА. 6.3. ПРОСТЫЕ ПОСЛЕДОВАТЕЛЬНЫЕ ФАЙЛЫ 6.3.1. ДОСТУП К ЗАПИСЯМ В ПРОСТЫХ ПОСЛЕДОВАТЕЛЬНЫХ ФАЙЛАХ Материал о последовательных файлах может быть по- ложен в основу разговора о методах организации файлов. Стан- дартные файлы Паскаля — это простые последовательные файлы. Для того чтобы сохранить данные после завершения работы про- граммы на Паскале, необходимо сформировать стандартный файл Паскаля и записать в него эти данные. Программа, которая за- пускается после этого, может использовать сохраненные данные, считывая их из файла. Записи в простом последовательном файле доступны только последовательно одна за другой. Например, можно обратиться к п-й записи только после обращения к 1, 2, ...»п — 1 записям. Для того чтобы удалить запись из файла, необходимо создать копию файла, в которой эта запись отсутствует; для того чтобы 158
поместить запись в файл, также необходимо создать копию файла, в которую эта новая запись включена. И наконец, чтобы изменить хотя бы одно поле в записи, необходимо создать копию файла, содержащую модифицированную запись. В гл. 4 более подробно освещаются методы работы с файлами на языке Паскаль. Один из недостатков, свойственных последовательным фай- лам, заключается в том, что для доступа к n-й записи обязательно надо «пробраться» через (п — 1) предшествующих ей записей. Но, с другой стороны, возможность пользоваться для работы с последовательными файлами теми же инструкциями READ и WRITE, что и для ввода и вывода на терминальное устройство, является их несомненным преимуществом. В заключение стоит заметить, что если файл располагается на магнитной ленте, то никакая другая организация, кроме последовательной, невоз- можна. 6.3.2. ГРУППОВАЯ ОБРАБОТКА Линейный поиск — это метод, заключающийся в про- смотре одна за другой последовательности значений до тех пор, пока искомое значение не будет найдено. Если метод линейного поиска используется для поиска записи с заданным значением первичного ключа в последовательном файле» состоящем из N за- писей, то для того, чтобы обнаружить ее, необходимо просмотреть в среднем N/2 записей. Эго можно объяснить следующим образом. Предположим, что проводится достаточно много испытаний, свя- занных с поиском в последовательном файле случайно выбранных значений ключа. Причем каждый раз поиск начинается с первой записи файла. Тогда для обнаружения искомой записи надо про- смотреть в среднем N/2 записей. Если необходимо организовать доступ к М записям последо- вательного файла, состоящего из N записей, то обычно стараются не прибегать к методу линейного поиска. Ведь тогда придется прибегать к этому методу М раз, начиная каждый раз просмотр файла с первой записи. Всего потребуется проанализировать MN/2 записей; очевидно, что При большом N на это будет затра- чено значительное время. Более эффективный метод позволяет просмотреть файл только один раз. Для того чтобы использовать этот метод, необходимо все значения первичных ключей, которые ищутся, включить в новый файл. Этот файл обычно называют файлом сообщений, а тот файл, в котором производится поиск, главным файлом. Главный файл и файл сообщений должны быть упорядочены по одному и тому же ключу. Теперь перейдем к описанию метода. Сначала из файла со- общений считывается первое значение первичного класса и по методу линейного поиска в главном файле ищется соответствую- щая ему запись. Затем считывается второе значение первичного 159
ключа, и поиск в главном файле продолжается, начиная о той записи, на которой он был остановлен. Этот процесс последо- вательно повторяется для всех значений первичного ключа из файла сообщений. Рассмотрим, например, упрощенный файл товарных запасов TOYINVEN, состоящий из 72 записей типа TOYINVREC = record ITEMREFNO: STRING4; QUANTITYINSTOCK: INTEGER end Предположим, что необходимо получить из файла TOYINVEN значения поля QUANTITYINSTOCK (КОЛИЧЕСТВО ТОВАРА) для каждой из 10 записей, имеющих следующие значения поля ITEMREFNO : VF62, DS33, SC84, FS52, TL81, AQ01, НН50, ВС59, RT06 и АН84. Сформируем последовательный файл TRANSINVEN таким образом, чтобы составляющие его 10 пере- численных выше значений ITEMREFNO были упорядочены по возрастанию ITEMREFNO. В результате получим файл сообще- ний TRANSINVEN и главный файл TOYINVEN. Ниже приве- дена программа поиска значений поля QUANTITYINSTOCK: program BATCHLOOKUP (TOYINVEN, TRANSINVEN, OUTPUT); type STRING4=packed array [1..4) of CHAR; TOYINVREC = record ITEMREFNO: STRING4; QUANTITYINSTOCK: INTEGER end; var TOYINVEN: file of TOYINVREC; TRANSINVEN: TEXT; SOUGHTITEMREFNO: STRING4; VTOYINVEN: TOYINVREC; begin RESET (TOYINVEN); RESET (TRANSINVEN); while not EOF (TRANSINVEN) do begin READ (TRANSINVEN, SOUGHTITEMREFNO); repeat READ (TOYINVEN, VTOYINVEN) until SOUGHTITEM REFNO=VTOYINVEN.ITEM REFNO; with VTOYINVEN 4» WRITELN (ITEMREFNO, ’ ’, QUANTITYINSTOCK) end end. Для простоты считается, что все значения ITEMREFNO, нахо- дящиеся в файле TRANSINVEN, присутствуют и в файле 160
TOYINVEN. Результат работы программы описывается с по- мощью следующего выражения: TOYINVEN join TRANSINVEN Повторим, что в том случае, если файлы TOYINVEN и TRAN- SINVEN отсортированы одинаково, вычисление значения объеди- нения может быть проведено довольно быстро. Групповая обработка — это процедура поиска в главном файле записей, определенных с помощью файла сообщений, в ко- тором записи упорядочены так же, как и в главном файле. Только что рассмотренная программа как раз и дает пример групповой обработки, заключающейся в просмотре записей главного файла с целью получения необходимой информации. Групповая обра- ботка может быть также использована для модификации М за- писей в главном файле, включающем N записей, для удаления М записей из файла и для включения М новых записей в главный файл. Программа слияния двух файлов из разд. 2.6 относится к программам групповой обработки. Она осуществляет включение записей из файла сообщений в главный файл, формируя в резуль- тате новый главный файл. Примеры групповой обработки также могут быть взяты из упражнений 6 и 7 разд. 2.7. 6.3.3. БУФЕР ФАЙЛОВОГО БЛОКА Программисту, работающему на Паскале, не обяза- тельно знать, как данные, составляющие стандартный последова- тельный файл, размещаются на устройствах внешней памяти, таких как магнитные диски. Кроме того, он может не знать и все те шаги, которые составляют процесс передачи данных между внешней и оперативной памятью. Читателям этой книги пред- стоит спуститься на более низкий уровень, чем тот, на котором работает программист, и познакомиться с процессом передачи данных. Сначала объясним, чем вызвана необходимость передачи дан- ных. Одна из причин заключается в том, что значения обычных (не сохраняющих свои значения) переменных Паскаля хранятся в оперативной памяти и после завершения работы программы теряются. Это дает возможность использовать ту же оперативную память для хранения значений переменных другой программы. Если программист, работающий на Паскале, захочет сохранить значения переменных после завершения выполнения программы, он должен будет скопировать их во внешнюю память. В стандарт- ном Паскале такое копирование во внешнюю память выполняется с помощью процедуры записи в последовательный файл. Кроме того, внешняя память обычно дешевле оперативной (за показатель стоимости может быть принята цена одного бита или байта). Если программа обрабатывает достаточно много дан- ных, можно хранить часть этих данных во внешней памяти и при необходимости осуществлять передачу данных между оперативной 6 Ульман Дм. 161
и внешней памятью. Это позволит уменьшить стоимость занимае- мой данными памяти. Обычно передача данных между оперативной и внешней па- мятью осуществляется так быстро, что обычный центральный процессор не успевает управлять ею на уровне символов. Вместо центрального процессора функции управления на этом уровне берет на себя специальное управляющее устройство. Централь- ный процессор просто сообщает этому устройству, сколько сим- волов (или байтов) необходимо передать, где взять эти символы и куда поместить. Передаваемая таким путем последовательность символов (или байтов) называется блоком. Блок может содержать, например, 512 символов. Блок считается наименьшей совокуп- ностью данных, которая может передаваться между оперативной и внешней памятью. Важно понять, что передача данных между оперативной и внешней памятью производится блоками, т. е. либо передается один блок, либо последовательно один за другим передаются несколько блоков. В некоторых случаях блок может содержать точно одну за- пись; еще реже запись разбивается на части, хранящиеся в раз- ных блоках. Обычно длина записи значительно короче длины блока, и поэтому в блок можно поместить сразу несколько за- писей. В этом случае записи называют сблокированными. Число символов (или байтов) в блоке либо жестко фиксиро- вано, либо может быть установлено программистом. Для памяти на магнитных дисках первый вариант проще в реализации и по- этому предпочтительнее. Для памяти на магнитных лентах чаще используется второй вариант. Преимущество второго варианта перед первым заключается в том, что позволяет избежать незапол- ненных данными пространств в блоках. Для этого программист должен выбрать длину блока кратной длине записи. Существует еще одна возможность организации блоков, соче- тающая в себе преимущества как первого, так и второго вариантов, хотя в этом случае длина блока является постоянной, програм- мисту разрешается объединять два, четыре или восемь блоков, образуя как бы один большой блок. Такой блок называется бакетом. Если запись слишком велика и не помещается в блоке, ее можно поместить в бакет. В этом случае можно рассматривать бакет как минимальную совокупность данных, которая может передаваться между внешней и оперативной памятью. Далее бакет будет считаться просто большим блоком и термин бакет использо- ваться не будет. Кроме того, предполагается, что записи, состав- ляющие стандартный файл Паскаля, размещаются в блоках оди- накового размера. Когда с помощью программы, написанной на Паскале, осуще- ствляется запись данных в файл, передача данных между устрой- ствами производится блоками, а не записями. Процедура WRITE формирует из записей блоки; этот процесс называется блокирова- 162
нием. В свою очередь, процедура READ должна извлечь записи из блоков; этот процесс называется деблокированием. Обновление файла приводит к созданию буфера файлового блока, который необходим при выполнении операций блокирова- ния и деблокирования. Отметим, что программист с этим буфером не работает и может вообще не знать о его существовании. Буфер файлового блока — это область оперативной памяти, размер которой совпадает с размерами блока этого файла. Его не следует путать с файловым буфером (см. разд. 4.6), который предназначен для хранения одной записи файла. С файловым буфером про- граммист может работать, а с буфером файлового блока — нет. Для каждого файла, объявленного в программе, создается его собственный файловый буфер и его собственный буфер файлового блока. Пользоваться одним и тем же буфером два файла не могут. Предположим, что FNAME — двоичный файл, а V — пере- менная того же типа, что и записи файла. Приведенный ниже фрагмент программы, содержащий цикл, не выполняет никаких полезных действий, но в данном случае более сложный пример не нужен: RESET (FNAME); repeat READ (FNAME, V) ntil EOF (FNAME) Попробуем хотя бы приблизительно описать ход выполнения этой программы: первый блок файла FNAME считывается из внешней памяти в буфер файлового блока; файловый буфер : = первая запись в буфере файлового блока; repeat V : = файловый буфер; if это была последняя запись в буфере файлового блока then из внешней памяти в буфер файлового блока считывается следующий блок файла FNAME; файловый буфер : = следующая запись из буфера фай- лового блока; until файловый буфер содержит маркер конца файла. В разд. 4.6 включены программы, на примере которых хорошо видно, что использование файлового буфера имеет преимущество перед считыванием в память, отведенную для какой-нибудь пе- ременной, например V. Приведем еще один аргумент в пользу применения файлового буфера: маркер конца файла не может быть значением переменной, но в файловый буфер его поместить можно. Ниже для иллюстрации процесса блокирования приведен еще один пример for COUNT : = 1 to 100 do WRITE (FNAME, V) 6* 163
Опишем, что же происходит при выполнении этого цикла: for COUNT t m 1 to 100 do begin файловый буфер i — V; if места в буфере файлового блока недостаточно для раз- мещения очередной записи then сформировать блок во внешней памяти из информации, содержащейся в буфере файлового блока; переписать содержимое файлового буфера в свободное пространство буфера файлового блока; end. Когда цикл завершается, буфер файлового блока может быть заполнен не до конца, т. е. часть блока, который последним за- писывается во внешнюю память, может оказаться пустой. Отме- тим, что для завершения формирования файла необходимо еще записать маркер конца файла. На деталях этого процесса оста- навливаться нет необходимости. Специальные программы, выполняющие блокирование и де- блокирование, работают, считая, что блоки записываются во внешнюю память и считываются из внешней памяти в определен- ной последовательности. Если устройством внешней памяти яв- ляется магнитная лента, то следующие друг за другом блоки файла будут последовательно расположенными на магнитной ленте физическими блоками. 6.3.4. БЛОКИ, СВЯЗАННЫЕ В ЦЕПОЧКУ Если файл хранится на диске, следующие друг за другом блоки могут быть расположены так, как показано на рис. 6.1. На этом рисунке изображена одна дорожка, разделен- ная на восемь блоков; блоки, принадлежащие файлу, обозна- чены цифрами 1, ..., 6, а оставшиеся на дорожке два свободных блока заштрихованы. Блоки, расположенные таким образом, называются смежными. Это означает, что следующие друг за другом блоки файла физически расположены рядом. Если в файле так много блоков, что они не помещаются на одной дорожке, О часть блоков можно расположить на следующей дорожке (желательно того же самого цилиндра) так, чтобы они оказа- лись смежными. Организация файла, при которой блоки расположены так, как было описано выше, называется смежной. На практике часто бывает, что до того, как блоки связываются с конкретным файлом, часть дорожек диска уже занята блоками других файлов. На рис. 6.2 Рис. 6.1. дорожка, вме- заполненные данными блоки дорожек тающая восемь блоков заштрихованы. Отметим, что попытки 164
Рис. 6.2. Примеры заполнения данными произвольно расположенных блоков и блоков, связанных в цепь разместить файл так, чтобы он удовлетворял условию смежно- сти, часто бывают неудачными, поскольку не всегда имеется достаточное число свободных смежных блоков. Обычно блоки последовательных файлов расположены в памяти в случайном порядке, хотя чаще всего в пределах одного цилиндра. Когда для файла отводится память, на диске ищутся свободные блоки и связываются с файлом. Связывание достигается включением в каждый блок адреса следующего за ним блока. Адрес дает возможность ЭВМ найти место на диске, где находится блок. Адрес включает номер цилиндра, номер поверхности и номер сектора. Операционная система запоминает адрес первого из последовательности блоков. Все блоки соединены в цепь, т. е. каждый блок содержит адрес, указывающий на следующий за ним блок. Блоки стандартного файла Паскаля могут быть расположены так, как показано на рис. 6.2, б. Адреса связи на этом рисунке заменены стрелками. Если требуется, чтобы файл размещался в смежных блоках, то необходимо знать, сколько блоков этот файл займет, поскольку надо заранее найти на диске достаточный участок свободной памяти. Если же блоки файла должны быть связаны в цепь, то не обязательно знать, сколько всего блоков требуется для раз- мещения этого файла, поскольку новые блоки проста присоеди- няются к концу файла. Легкость присоединения новых блоков является достоинством файла, состоящего из блоков, связанных в цепочку. 165
6.4. КОЭФФИЦИЕНТ АКТИВНОСТИ ФАЙЛА И ЭФФЕКТИВНОСТИ ДОСТУПА Когда программа находит в файле искомое значение ключа, считается, что получен ответ. В процессе групповой обра- ботки одной программе требуется найти несколько различных значений ключа, т. е. получить как бы несколько ответов. Для программ групповой обработки определим коэффициент актив- ности. „ , , число ответов Коэффициент активности =--------------=------------ число записей в файле Групповая обработка, проводимая с главным файлом, включаю- щим N записей, и файлом сообщений, состоящим из М записей, имеет коэффициент активности M/N до тех пор, пока не найдена ни одна из требуемых записей главного файла. Типичным при- мером, в котором коэффициент активности близок к единице, является вычисление зарплаты при обработке платежных ведо- мостей: каждая (или почти каждая) запись в файле EMPLOYEES (СЛУЖАЩИЕ) обрабатывается. Будем считать, что имеет место произвольный доступ, когда программе нужно найти только одну запись файла. В случае произвольного доступа коэффициент активности близок к нулю. Произвольный доступ возникает, например, тогда, когда фирма «Типико> принимает заказы клиентов по теле- фону и поодиночке в произвольном порядке просматривает эле- менты файла товарных запасов. Это делается для того, чтобы проверить, согласуется ли заданный клиентом шифр товара с шиф- ром и описанием этого товара в файле товарных запасов. Если существуют какие-то расхождения, фирма предпочитает немед- ленно получить дополнительную информацию от клиента и только после этого перейти к следующему элементу. Эффективность произвольного доступа обратно пропорцио- нальна общему времени, затрачиваемому на доступ к произвольно взятой записи, хранящейся во внешней памяти. Производя оценку факторов, вносящих вклад в общее время доступа к записи, необходимо отметить, что время, затрачиваемое на передачу блоков, является преобладающим. Время передачи складывается из времени поиска, задержки и времени копирования найденного блока. Все эти величины определяются скоростью выполнения электромеханических операций. Известно, что скорость выполне- ния электромеханических операций ниже, чем скорость выпол- нения команд центральным процессором. Таким образом эффек- тивность доступа к записи, хранящейся во внешней памяти, обратно пропорциональна числу обращений к блокам, которые потребуются в процессе реализации доступа к записи *. * Это хотя и принципиально верная, но довольно грубая оценка. Более точные оценки приведены в [3]. — Прим. пер. 166
Произвольный доступ к последовательному файлу, как пра- вило, неэффективен, поскольку требует просмотра файла с самого начала, и передача следующих друг за другом блоков продол- жается до тех пор, пока не будет найден блок, содержащий иско- мую запись. В следующих разделах будут введены другие методы организации файлов, которые позволяют повысить эффективность произвольного доступа. Их использование даст возможность орга- низовать доступ к блоку без передачи в оперативную память всех предшествующих ему блоков. Отметим, что все отличные от после- довательного методы организации в стандартном Паскале не предусмотрены. 6.5. ОПРЕДЕЛЕНИЕ АДРЕСА 6.5.1. ПРЯМАЯ АДРЕСАЦИЯ Файл с прямой адресацией напоминает массив записей, где роль индекса записи играет функция, аргументом которой является значение первичного ключа записи файла. Например, значение функции может быть равно значению первичного ключа. В разд. 5.3 в примере, описывающем упрощенную таблицу товар- ных запасов, массив записей рассматривался как файл с прямой адресацией, в котором значением первичного ключа являлся индекс массива. Преимущество такого подхода по сравнению со стандартной последовательной организацией файла в Паскале заключается в том, что удается получить запись по заданному значению ключа (в да) чом случае ключом является ITEMREFNO) без предварительного просмотра всех предшествующих ей запи- сей файла. Рассмотрим, что происходит в этом случае на физическом уровне. Предположим, что записи массива объединены в смежные блоки и задан начальный адрес размещения блоков в памяти. Тогда, зная значение поля ITEMREFNO, ЭВМ может сразу опре- делить адрес блока, который содержит запись с этим значением. Естественно, что в этом случае для того, чтобы обратиться к за- писи, потребуется передать только один блок. Для определенности предположим, что массив хранится в смежных блоках с номерами от 0 до 10 и что в каждом блоке содержится по четыре записи. Тогда для того, чтобы организовать доступ к записи с ITEM- REFNO = 135, ЭВМ необходимо обратиться к блоку, номер которого равен (135 — 100) div 4 = 8. Передав блок в оператив- ную память, ЭВМ может не просматривать по очереди все входя- щие в него записи; ей достаточно прямо обратиться к четвертой записи' блока. Объясним, почему это так. Значение поля ITEMREFNO первой записи блока с номером восемь равно 132, второй — 133, третьей — 134, четвертой — 135. Функция, с по- мощью которой определяется порядковый номер записи в блоке, имеет вид (ITEMREFNO—100) mod 4 + 1 167
В разд. 6.3.4 было показано, что для размещения файла не всегда удается найти достаточное число свободных смежных блоков. Чаще свободные блоки располагаются на дорожках вперемешку с блоками, занятыми данными. Поэтому обычно файлы с прямой адресацией хранятся в несмежных блоках. Как и раньше, пронумеруем эти блоки 0, 1, 2..Но теперь для того, чтобы определить физический адрес блока, понадобится специаль- ная таблица или индекс. Для файла товарных запасов этот индекс будет иметь вид таблицы. Номер блока Физический адрес цилиндр поверх- ность сектор О 1 2 3 4 5 6 7 8 9 10 3 3 2 2 2 2 2 2 2 3 3 2 1 1 1 1 1 2 2 2 1 2 4 3 7 3 4 5 3 6 7 7 1 Например, пользуясь ин- дексом, можно установить, что блок с номером восемь имеет следующий физический адрес: номер цилиндра = 2, номер поверхности = 2, номер секто- ра = 7. Анализируя содержи- мое индекса, можно легко уста- новить, что не все блоки явля- ются смежными (впрочем, есть и смежные блоки, например 3, 4 и 5). Сам индекс обычно хра- нится по крайней мере в одном блоке. Тогда для организации доступа по заданному ключу требуется не менее двух обра- щений к блокам: одно — для получения индекса, второе — для получения блока, содержащего искомую запись. Таким образом, введение индекса, с одной стороны, позволило избавиться от не- обходимости искать свободные смежные блоки, но, с другой стороны, уменьшило общую эффективность доступа. Удалить запись из последовательного файла можно, только сформировав новую версию этого файла, которая не содержит удаляемой записи. Важно отметить, что удаление записи из файла с прямой адресацией не приводит к созданию новой версии всего файла. Файл с прямой адресацией строится так, что память резервируется для всех возможных записей, даже для тех, кото- рые либо должны быть удалены, либо не имеют конкретных зна- чений. В разд. 5.3 был описан способ, позволяющий отделить истинные записи от, во-первых, тех записей, которые подлежат удалению, и, во-вторых, тех записей, полям которых не присвоены значения. Этот метод заключается в создании специального поля со значением булевого типа. В разд. 5.3 это поле имёло имя NORECORD и принимало значение FALSE только тогда, когда запись существовала и была нужна. Для удаления записи доста- точно было просто присвоить NORECORD значение TRUE. Таким образом, если в файле с прямой адресацией имеется всего несколько записей, то большая часть памяти, отведенная для 168
размещения этого файла, тратится впустую. В этом случае часто бывает целесообразным использовать при организации файла методы хэширования. 6.5.2. МЕТОДЫ ХЭШИРОВАНИЯ На практике, как правило, число возможных значений первичного ключа намного превосходит число реально присут- ствующих в любой момент значений этого ключа. В этом случае прямая адресация неудобна, поскольку слишком много памяти отводится для записей, которых нет и никогда не будет в файле. В качестве примера снова рассмотрим сильно упрощенный файл товарных запасов TOYINVEN, состоящий из записей типа record ITEMREFNO: STRING4; QUANTITYINSTOCK: 0. .99999 end. Для этого файла диапазон возможных значений первичного ключа ITEMREFNO очень велик: от AA00 до ZZ99, т. е. 26х X 36x100 значений. Если файл состоит из 72 записей, значения первичных ключей которых лежат в интервале AA00, ..., ZZ99, то при использовании прямой адресации память, отведенная для 22 x 26x100—72 записей, будут расходоваться впустую. Метод хэширования позволит избежать этого и в то же время во многом сохранить эффективность, присущую прямой адресации. При использовании хэширования запись помещается в первый свободный участок блока, номер которого является функцией значения первичного ключа записи. Эта функция называется функцией хэширования. Наличие свободных участков устанавли- вается с помощью булевой переменной, являющейся обязательным полем каждой записи. Если в блоке нет свободных участков, за- пись помещается в специальный блок, называемый блоком пере- полнения. Рассмотрим пример. Пусть функция хэширования имеет сле- дующий вид: h (ITEMREFNO) « (последние две цифры ITEMREFNO) mod 13 Например, h (АА39) = 39 mod 13 = 0; h (AA62) «62 mod 13 = 10. Поле ITEMREFNO здесь используется в качестве первичного ключа для записей файла TOYINVEN. Функция хэширования позволяет получить значения в диапазоне от 0 до 12, идентифицируя тем самым 13 блоков. В каждый блок можно поместить восемь записей. Тогда, если файл содержит 72 записи, пространство, выделенное для его хранения, будет на две трети заполнено данными. Пометим все 13 блоков номерами от 0 до 12. Ниже показано состояние блоков после помещения в файл 72 записей. Читатель может самостоятельно убедиться, что номера блоков совпадают со значениями функции хэширования для тех записей, которые в них находятся. 169
0: АА91 00350 1. AQ01 00247 2: FD80 00731 РК39 00041 AZ27 00065 HS41 00059 AS65 00184 АН01 00322 FN02 00317 АА39 00061 AS80 00001 FS52 00114 FX80 00051 3: ХВОЗ 00315 4: АС56 00664 5: AV18 01075 LX94 00608 YD30 00000 AD37 00059 DD68 НА16 01642 00579 АВ82 00196 АВ83 АС05 00000 01006 TL81 00034 АР83 00914 МС55 00082 АЕ31 07154 FS44 00321 6: АН84 00621 7: AL72 00007 RT05 8: FH08 00108 00629 ВА32 00002 ММ72 00006 SY73 00094 ММ71 00008 WG33 00016 VR34 00788 АМ19 SX19 00034 00060 ВС59 00031 RB99 00775 RT06 00061 SC84 00112 9: YH61 00070 10: D.T62 00095 И: FA37 00000 АС22 00020 АА62 00008 АР76 00015 MQ09 00014 DD62 00315 DD63 00579 AJ09 00000 AL62 00085 АР50 00034 SY75 00008 МТ50 00078 VF62 00315 РАИ 00314 GB75 00338 НН50 00002 12: AF77 00011 V; ХХ44 04000 VK89 00456 HL12 00400 PY05 00073 СЕ64 00087 WN77 00019 РС25» 00080 Блок переполнения помечается буквой V. В нем содержатся записи, для которых значение функции хэширования равно 5 и которые не попали в блок 5 потому, что он к тому времени был уже заполнен. г»ттлм^тТ^т^бК^0 заДанномУ значению ITEMREFNO найти 4UAN11TYINSTOCK, необходимо вычислить h (ITEMREFNO) и затем организовать линейный поиск в блоке, номер которого равен h (ITEMREFNO). Этот поиск будет продолжаться до тех пор, пока не будет найдена запись, значение первичного ключа которой совпадает с заданным. Например, пусть требуется определить QUANTITYINSTOCK записи со значением ITEMREFNO, равным МТ50. Сначала вы- числяем значение функции хэширования: оно равно 11. Затем, используя метод линейного поиска, ищем в блоке 11 запись со Тиким образом, требуемое значение поля уиАгчШ Y INSTOCK может быть найдено с помощью обращения к записям только одного блока. В результате удалось сохранить быстроту метода прямой адресации и избежать свойственных этому методу больших затрат памяти. Следует отметить, что при- менение методов хэширования возможно только тогда, когда 170
в состав записей включены значения первичного ключа. В отли- чие от методов хэширования при использовании методов прямой адресации, в которых первичный ключ используется так же, как индекс массива, не обязательно включать в состав записей зна- чения первичного ключа. Важно, что в файлах с хэш-адресацией записи внутри блоков не упорядочены. Чтобы включить в файл новую запись, нужно с помощью метода линейного поиска в блоке, номер которого определяется значением h (ITEMREFNO), найти запись с NORECORD = = TRUE. После этого на место найденной записи надо поместить новую. Например, новая запись PQ56 00012 помещается в первое свободное пространство блока, номер которого равен h (PQ56) = яи 4. Теперь блок 4 содержит следующие четыре записи: АС56 00664 YD30 00000 АВ82 00196 PQ56 00012 В том случае, если при попытке поместить новую запись в файл обнаруживается, что в найденном блоке нет записи с NORECORD = TRUE, говорят, что блок переполнен. Не- сколько позже будет описана реакция на переполнение. Для удаления записи из файла надо, используя метод линей- ного поиска, попытаться обнаружить ее в блоке, номер которого равен h (значение первичного ключа). Если запись будет най- дена, то надо установить NORECORD = TRUE; если — нет, то очевидно, что блок был переполнен и необходимо продолжить поиск (об этом будет идти разговор ниже). Отметим, что перед тем как заполнить файл записями, необходимо установить RECORD = TRUE для всех записей всех блоков. Если требуется модифицировать запись, например изменить значение ее поля QUANTITYINSTOCK, то надо найти эту запись и заменить старое значение поля QUANTITYINSTOCK на новое. Никаких других изменений не требуется. Переполнение. Для борьбы с переполнением могут быть ис- пользованы три метода: блоков, связанных в цепь; записей, свя- занных в цепь, и прогрессирующего переполнения. Метод блоков, связанных в цепь, состоит в следующем. Когда блок переполняется, в нем размещается указатель, ссылающийся на вновь созданный блок, предназначенный исключительно для записей, не поместившихся в первый блок. Желательно этот вновь созданный блок разместить в том же цилиндре. Если же вновь созданный блок, в свою очередь, переполнен, в нем размещается указатель, ссылающийся на еще один вновь созданный блок, и т. д. В результате будет сформирован связанный список блоков, для всех записей которого значение функции хэширования оди- наково. 171
Основной недостаток метода блоков, связанных в цепь, за- ключается в том, что блоки переполнения часто оказываются за- полненными только несколькими записями, а оставшаяся в них память тратится впустую. Для устранения этого недостатка можно использовать один блок переполнения для всех записей, не обращая внимание на то, в какой блок они не попали. Когда блок переполнения, в свою очередь, переполняется, используется еще один блок переполнения, и т. д. Записи, имеющие одинаковые значения функции хэширования и попавшие в блоки переполне- ния, объединяются в связанный список. Тот блок, который изна- чально предназначен для хранения записей с одинаковыми зна- чениями функции хэширования, в случае переполнения должен содержать указатель, ссылающийся на начало связанного списка записей, находящихся в блоках переполнения. Для поиска записи в файле с такой организацией необходимо сначала произвести ли- нейный поиск в блоке, номер которого определяется значением функции хэширования для этой записи; если искомая запись не обнаружена, необходимо, воспользовавшись указателем, на- ходящимся в блоке, просмотреть связанный список записей в блоках переполнения. Если необходимо включить в файл новую запись, а в блоке, предназначенном для этого, места нет, эта запись помещается в первый блок переполнения, в котором есть свободное место. После этого надо еще включить эту запись в связанный список записей с тем же самым значением функции хэширования. Такой метод получил название метода записей, связанных в цепь. Хотя во втором из указанных методов часто используется меньше блоков переполнения, чем в первом, поиск записи, попав- шей в область переполнения, может потребовать для второго ме- тода больше времени. Предположим, например, что каждый блок предназначен для размещения восьми записей и что требуется найти запись, которая является седьмой в связанном списке за- писей, попавших в область переполнения. При использовании первого метода потребуется организовать доступ к двум блокам: во-первых, к основному блоку, номер которого определяется значением функции хэширования и, во-вторых, к блоку перепол- нения, который и содержит искомую запись. Если же использо- вать второй метод, то необходимо сначала организовать доступ к основному блоку, определенному с помощью функции хэширо- вания, а затем просмотреть связанный список записей, попавших в область переполнения. Так как эти последние записи, возможно, принадлежат разным блокам, то весьма вероятно, что седьмая запись связанного списка находится не в первом блоке перепол- нения. Тогда необходимо будет, следуя по связанному списку, организовать доступ к блокам до тех пор, пока не будет найден блок, содержащий искомую запись. Этот пример позволяет по- нять, почему для второго метода часто надо организовывать до- ступ к большему числу блоков, чем для первого метода. Отметим 172
еще раз, что в первом методе записи в блоках не объединяются в списки. Метод прогрессирующего переполнения во многом похож на второй метод. Единственное отличие состоит в том, что записи в блоках переполнения не объединяются в связанные списки. В том случае, если в блоке, номер которого определяется значе- нием функции хэширования, больше нет места, запись помещается в первое свободное пространство блока переполнения. В этот блок помещаются записи с различными значениями функции хэширования. Для поиска записи надо сначала организовать доступ к основ- ному блоку, номер которого определяется значением функции хэширования. Если искомой записи там нет, необходимо, исполь- зуя метод линейного поиска, просматривать блоки переполнения до тех пор, пока запись не будет обнаружена. В этом случае будут просматриваться даже те записи, которые имеют другие значения функции хэширования. Отметим, что второй метод обладает более высоким быстродействием, чем третий, поскольку ограни- чивает поиск только теми записями, которые имеют то же значение функции хэширования, что и искомая. Но третий метод требует памяти меньше, чем второй, поскольку нет необходимости хра- нить указатели. Кроме того, третий метод проще в реализации. Функции хэширования. При прямой адресации по заданному значению первичного ключа вычисляется номер блока. Число возможных значений первичного ключа, которые будут преобра- зовываться в один и тот же номер блока, равно максимальному числу записей, которые могут быть помещены в блок. При исполь- зовании хэширования вычисляются номер блока для заданного значения первичного ключа, но число возможных значений пер- вичного ключа, которые соответствуют этому же блоку, теперь превышает максимальное число записей, которые можно поме- стить в блок. Для файла TOYINVEN и выбранной ранее функции хэширования число возможных значений ключа, преобразуемых в один и тот же номер блока, равно 26х26х(100 div 13 + 1). Коэффициент 26x26 появляется из-за того, что в первичном ключе первые два символа могут быть произвольными буквами латинского алфавита, которые не принимаются во внимание при вычислении функции хэширования. Выбор функции хэширования осуществляется в некотором смысле произвольно. Покажем, как для любого заданного файла выбрать «хорошую» функцию хэширования. Для этого необхо- димо знать, во-первых, примерное число записей в файле и, во-вторых, сколько записей можно помещать в блок. Эта инфор- мация будет использоваться для подсчета числа блоков в файле, на основании которого можно определить диапазон изменения значений функции хэширования. Если для файла будет отведено слишком мало блоков, то эффективность доступа будет умень- шаться по мере переполнения этих блоков. Если, наоборот, 173
для размещения файла будет предусмотрено много блоков, то переполнение блоков будет происходить редко и, следовательно, доступ будет осуществляться быстро. Значительная часть памяти в этом случае останется не занятой данными. Будем стремиться к золотой середине, т. е. считать, что примерно две трети блока должны быть заняты данными. Именно этим объясняется, почему, зная, что в файле TOYINVEN 72 записи и что каждый блок может вместить восемь записей, для файла выделено 13 блоков. В 13 та- ких блоках можно разместить 13x8 = 104 записи, а 72 приблизи- тельно составляют две трети от 104. Функция хэширования должна быть выбрана таким образом, чтобы максимально удовлетворять следующим условиям: каждое ее значение должно быть целым из интервала O..NUMBEROFBLOCKSAVAIL ABLE * — 1; она должна быть удобной для вычисления, т. е. определение значения функции хэширования должно происходить быстро; для множества реально появляющихся значений первичного ключа все значения функции хэширования должны быть равно- вероятны. Если h «значение ключа» полностью удовлетворяет послед- нему условию, то говорят, что h перемешивает значения ключа. Перемешивание позволяет добиться того, чтобы все блоки содер- жали примерно одинаковое число записей. Если условие пере- мешивания не выполняется, то часть блоков может переполниться, в то время как другие блоки будут содержать лишь небольшое число записей, и в результате эффективность доступа может ока- заться ниже, чем при использовании перемешивания. При вычислении функции хэширования буквы, входящие в состав ключа, удобнее интерпретировать как целые числа в ин- тервале 0 ... 25, т. е. А = 0, В = 1, С = 2 и т. д. Например, числовое значение СВ53 тогда будет равно (2x26x26 + 1х26)х100 + 53 Используя аналогичные методы, любому ключу можно сопо- ставить численное значение. Остановимся на некоторых из наи- более часто используемых методов выбора функций хэширования. Деление на простое число. Число блоков выбирается таким образом, чтобы оно было простым. Используется следующая функция хэширования: h «значение ключа» «и (значение ключа) mod (число блоков). Выбор простого числа в качестве делителя позволяет лучше организовать перемешивание. Именно по этой причине для разме- щения файла TOYINVEN было выбрано 13 блоков. Использование нескольких цифр ключа. Можно в качестве значения функции хэширования использовать несколько цифр из * Число блоков в файле. — Прим. пер. 174
значения ключа. Например, если для этой цели выбрать вторую и пятую цифры шестизначного ключа, то h (932148) == 34 и h (716559) « 15. Сложение. Можно брать из значения ключа два (или более) подмножества цифр, формировать из цифр каждого подмножества число и использовать сумму этих чисел в качестве значения функ- ции хэширования. В этом качестве можно также использовать последние несколько цифр суммы. Если ключ состоит из шести цифр, то имеет право на существование такая функция хэширо- вания: h ((значение ключа» = последние две цифры числа ((пятая цифра) + (вторая цифра» х 7 -J- «третья цифра) + (четвертая цифра». Функция хэширования такого типа сложнее для вычисления, чем функция, для построения которой используется несколько цифр ключа, но в то же время она обеспечивает лучшее перемеши- вание. Кроме того, значения этой последней функции могут быть вычислены быстрее, чем значения функции, для построения кото- рой используется деление на простое число. Реализация с использованием несмежных блоков. Во время обсуждения методов хэширования внимание в основном концен- трировалось на определении номера блока. Не были рассмотрены вопросы, связанные с поиском места нахождения блока во внешней памяти. Эта задача решается точно так же, как и в случае исполь- зования прямой адресации. Если файл размещен в смежных бло- ках и известен адрес первого из этих блоков, то, зная номер блока, легко определить его адрес. Если же для размещения файла нет достаточного количества смежных блоков, можно для поиска адреса использовать индекс, как это было сделано в разд. 6.5.1. Поскольку индекс хранится по крайней мере в одном блоке, для организации доступа к файлу с помощью индекса необходимо по меньшей мере на одно обращение к блокам больше, чем в случае, когда индекс не используется. Напомним, что когда в файл с хэш-адресацией либо включа- ется, не вызывая переполнения, новая запись, либо удаляется или модифицируется запись, не находящаяся в области перепол- нения, то сначала определяется адрес блока, содержащего эту запись, затем блок из внешней памяти передается в оперативную, где выполняется вставка, удаление или модификация, и, нако- нец, блок помещается точно на то же место во внешней памяти, откуда он был взят. В случае переполнения для включения, удаления или моди- фикации записи необходимо сначала (для того чтобы определить, что произошло переполнение) передать в оперативную память блок, причем не тот, содержимое которого будет, впоследствии изменено, поскольку невозможно сразу определить адрес иско- мого блока. 175
6.5.3. НЕПРИГОДНОСТЬ ФАЙЛОВ С ХЭШ-АДРЕСАЦИЕЙ К ГРУППОВОЙ ОБРАБОТКЕ При использовании файла с хэш-адресацией для того, чтобы найти запись, нужно организовать доступ к меньшему числу блоков, чем при линейном поиске в последовательном файле. Так как хэширование удобно для организации произвольного доступа, файл с хэш-адресацией иногда называют файлом произ- вольного доступа или файлом прямого доступа. Эти же термины применяются и к файлам с прямой адресацией. По мнению автора, прямую адресацию можно считать частным случаем хэш-адре- сации. При групповой обработке, для которой коэффициент актив- ности близок к единице, использовать последовательный файл часто бывает выгоднее, чем файл с хэш-адресацией. Если требуется организовать доступ к записям, находящимся в каждом из сле- дующих друг за другом блоках последовательного файла, то удобнее работать с этими блоками по очереди, не тратя времени на вычисление их адресов (возможно, с помощью блоков индексов и блоков переполнения). Для того чтобы правильно выбрать организацию файла, не- обходимо хотя бы приблизительно знать коэффициент активности этого файла. Если коэффициент активности близок к нулю, сле- дует выбрать файл с хэш-адресацией (или с прямой адресацией), если же коэффициент активности близок к единице, предпочти- тельнее последовательный файл. Есть и другой фактор, который может повлиять на выбор способа организации файла. Важно знать, нужно или нет выводить содержимое файла в таком виде, когда его записи упорядочены по какому-нибудь ключу. Это может понадобиться для того, чтобы получить твердую копию справочного документа, такого, например, как телефонная книга. Если получать содержимое файла в упорядоченном виде надо часто, то предпочтительнее использовать последовательную организацию, поскольку записи в файле с хэш-адресацией обычно не упорядочены. Если же за- писи в упорядоченном виде требуются лишь эпизодически, то на выбор организации файла это не повлияет, поскольку в этом случае для сортировки записей можно использовать специальную программу. 6.5.4. УПРАЖНЕНИЯ 1. 'В файле, в котором размещены данные об автомоби- лях и их владельцах, в качестве ключа используется REGISTRA- TIONNUMBER (РЕГИСТРАЦИОННЫЙ-НОМЕР). Каждый ре- гистрационный номер состоит из буквы, трех цифр и еще трех букв. Выберите функцию хэширования для этого файла, считая, что число записей в файле никогда не превысит 3000 и что в каж- дый блок можно поместить не более 10 записей. Аргументируйте свой выбор. 176
2. Записи файла, содержащего данные о горных вершинах и их высотах, относятся к следующему типу: гecord MOUTAINNAME: packed array 1..12 of CHAR; COUNTRY: packed array 1..10 of CHAR; HEIGHT: REAL end. Два поля — MOUNTAINNAME и COUNTRY — составляют первичный ключ. Выберите функцию хэширования для этого файла, считая, что число записей в файле никогда не превысит 10 000 и что каждый блок может содержать 32 записи. Аргумен- тируйте свой выбор. 3. В гл. 5 описаны переменные, сохраняющие свои значения после завершения работы программы. Для того чтобы это было возможно, значения переменных помещаются в блоки и пере- даются во внешнюю память. Причем процесс остается скрытым от программиста, работающего на Паскале. В этом упражнении требуется использовать средства описания баз данных на Паскале для моделирования файла с хэш-адресацией TOYINVEN из разд. 6.5.2. Блокам будут соответствовать массивы, состоящие из восьми записей. С каждым блоком связывается указатель, ссы- лающийся на связанный список записей, не поместившийся в этот блок. Блоки переполнения создавать не надо. Если значениям указателя является ,,nil”, то переполнения не произошло. Исполь- зуйте следующее DBD: DBDNAME TY; const RECORDSPERBLOCK = 8; LASTBLOCKNO = 12; type STRING4 = packed array [1..4] of CHAR; OFLOWPTR = fOFLOWRECTYPE; OFLOWRECTYPE = record ITEMREFNO: STRING4; QUANTITYINSTOCK: 0..99999; NEXTITEM; OFLOWPTR end; TOYRECTYPE = record ITEMREFNO: STRING4; QUANTITYINSTOCK: 0 .99999; NORECORD: BOOLEAN end; BLOCKTYPE = array [1.RECORDSPERBLOCK] of ' TOYRECTYPE HASHRANGE = 0..LASTBLOCKNO; var HASHFILE: array: [HASHRANGE] of record 177
BLOCK: BLOCKTYPE; OVERFLOWFIELD: OFLOWPTR end; finish. Программа инициализации имеет следующий вид: program TOYINIT (OUTPUT); invoke TY; var BLOCKNUMBER: O..LASTBLOCKNO; WHICHRECORD: 1..RECORDSPERBLOCK; begin STARTTRANSACTION (UNRESTRICTED); for BLOCKNUMBER. = 0 to LASTBLOCKNO do with HASHFILE [BLOCKNUMBER] do begin for WHICHRECORD. = 1 to RECORDSPERBLOCK do BLOCK [WHICHRECORD].NORECORD: =TRUE; OVERFLOWFIELD: = nil end; WRITELN (’initialization completed’); ENDTRANSACTION end. Как и в разд. 6.5.2, ключ ITEMREFNO содержит две буквы, за которыми следуют две цифры. Используйте следующую функ- цию хэширования: function HASH (ITEMREFNO: STRING4): HASHRANGE; var FIRSTDIGIT, SECONDDIGIT: 0..9; begin FIRSTDIGIT: = ORD (ITEMREFNO [3])-ORD (’O’); SECONDDIGIT: = ORD (ITEMREFNO [4])-ORD ( O’); HASH: = (FIRSTDIGIT* 10+ SECONDDIGIT) mod 13 end. а. Напишите программу считывания записи файла TOYIN- VEN с терминального устройства и помещения этой записи в файл с хэш-адресацией. Сохраните эту программу для использования в 3, г. б. Напишите программу считывания значения ITEMREFNO с терминального устройства и выдачи из файла с хэш-адресацией значения QUANTITYINSTOCK, соответствующего введенному значению ITEMREFNO. В том случае, если в файле нет записи с искомым значением ITEMREFNO, предусмотрите выдачу соот- ветствующего сообщения. 178
в. Напишите программу считывания с терминального устрой- ства значения ITEMREFNO и удаления из файла с хэш-адреса- цией записи с введенным значением ITEMREFNO. Если в файле нет такой записи, предусмотрите выдачу соответствующего со- общения. г. Модифицируйте программу из упражнения 3, а так, чтобы она проверяла, есть ли в файле запись с введенным значением ITEMREFNO. Если такая запись в файле есть, программа должна выдать соответствующее сообщение и ждать от пользователя ответа. Если пользователь вводит символ Y, то новая запись должна быть помещена на место старой; если символ N, то сле- дует оставить файл без изменений. Тестирование программы. Программы из 3, а, б, в, г дол- жны быть проверены. Для этого используйте часть записей из разд. 6.5.2 вместе с любыми другими записями. Сначала выберите записи, предназначенные для ввода, так, чтобы переполнения не было. Затем, включая новые записи, добейтесь переполнения и снова проверьте работу всех программ. 6.6. ИНДЕКСНО- ПОСЛЕДОВАТЕЛЬНЫЕ ФАЙЛЫ 6.6.1. ВВЕДЕНИЕ В ИНДЕКСНО- ПОСЛЕДОВАТЕЛЬНУЮ ОРГАНИЗАЦИЮ Ранее было показано, что применение простой последо- вательной организации оправданно, если коэффициент активности достаточно высок; если же коэффициент активности невелик, то предпочтительнее использовать файлы с хэш-адресацией. В том случае, когда коэффициент активности в зависимости от приложе- ния становится то высоким, то низким, ни хэш-адресация, ни по- следовательная организация не обеспечивают эффективный до- ступ. Очевидно, что для таких случаев необходима другая орга- низация хранения файлов. Она получила название индексно- последовательной. При использовании индексно-последователь- ной организации записи хранятся в отсортированном виде и к ним возможен произвольный доступ, который обычно осуще- ствляется быстрее, чем для простых последовательных файлов, и медленнее, чем для файлов с хэш-адресацией. Попытаемся основную идею, лежащую в основе индексно- последовательной организации, пояснить с помощью примера. Для этого снова обратимся к файлу TOYINVEN. Будем считать, что файл содержит 72 записи. В блок может быть помещено восемь записей. Сначала поместим в каждый блок только по шесть запи- сей (для чего это сделано, будет объяснено далее). После этого содержимое блоков примет следующий вид: АА39 00061 2: АС22 00020 3: АН84 00621 АА62 00008 АС56 00664 AJ09 00000 АА91 00350 AD31 00059 AL62 00085 179
АВ82 00196 АЕ31 07154 AL72 00007 АВ83 00000 AF77 00011 АМ19 00034 АС05 01006 АН01 00322 АР76 00015 4: АР83 00914 5: AZ27 00065 6: DD68 01642 AQ01 00247 ВА32 00002 DT62 00095 AS63 00184 ВС59 00031 FA37 00000 AS80 00001 СЕ64 00087 FD80 00731 АТ50 00034 DD62 00315 FH08 00629 AV18 01075 DD63 00579 FN02 00317 7: FS44 00321 8: HS41 00059 9: ММ72 00006 FS52 00114 НН50 00002 MQ09 00014 FS80 00051 LE31 00050 МТ50 00078 GB75 00338 LX94 00608 РАН 00314 НА16 00579 МС55 00082 РС25 00080 HL12 00400 ММ71 00008 РК39 00041 10: PY05 00073 11: SY73 00094 12: WC33 00016 RB99 00775 SY75 00008 WN77 00019 RT05 00108 TL81 00034 ХВОЗ 00315 RT06 00061 VF62 00315 ХХ44 04000 SC84 00112 YK89 00458 YD30 00000 SX19 00060 VR34 00788 YH61 00070 Напомним, что записи в файле отсортированы. К файлу добав- ляется индекс, содержащий минимальные значения ключей для каждого блока и соответствующие этим блокам физические адреса. Индекс сам может быть простым последовательным файлом, раз- мещенным в нескольких блоках. Например Первый блок индекса АС05 адрес 1-го блока АН01 адрес 2-го блока АР76 адрес 3-го блока AV18 адрес 4-го блока DD63 адрес 5-го блока FN02 адрес 6-го блока HL12 адрес 7-го блока ММ71 адрес 8-го блока Второй блок индекса РК39 адрес 9-го блока SX19 адрес 10-го блока VR34 адрес 11-го блока YH61 адрес 12-го блока Тогда максимальное значение ключа для первого блока файла — АС05, для второго блока файла — АН01. Адрес n-го блока — это реальный физический адрес, включающий номер цилиндра, номер поверхности и номер сектора. Предположим, необходимо выяснить, сколько имеется в на- личии товара с шифром FH08. Для этого надо выполнить следую- щую последовательность действий: 1. Организовать в индексе линейный поиск первого ключа, значение которого больше или равно искомому значению ключа FH08. Выбрать соответствующий найденному значению ключа физический адрес блока, в котором находится запись с искомым значением ключа. Искомая запись не может находиться в блоках 180
1—5, так как FH08 больше соответственно АС05, АН01, АР76, AV18 и DD63. Она должна быть в блоке 6, поскольку FH08 < =* FN02. 2. Передать содержимое выбранного блока из внешней памяти в оперативную. Организовать в нем линейный поиск записи с ис- комым значением ключа. В этом примере искомая запись будет найдена в результате обращения только к двум блокам: первому блоку индекса и ше- стому блоку файла данных. Это, очевидно, значительно быстрее линейного поиска в последовательном файле, поскольку в послед- нем случае пришлось бы просматривать шесть блоков. Индекс помогает ускорить доступ, когда коэффициент активности очень низок. В том случае, когда коэффициент активности высок, можно не пользоваться индексом, а проводить групповую обработку записей точно так же, как с обычным последовательным файлом. По сравнению с хэш-адресацией и с простой последовательной организацией индексно-последовательная организация имеет пре- имущество, заключающееся в том, что она позволяет эффективно проводить как произвольный доступ, так и групповую обработку. 6.6.2. ВКЛЮЧЕНИЯ И УДАЛЕНИЯ (ПЕРЕПОЛНЕНИЕ ОТСУТСТВУЕТ) Для включения новой записи в файл с хэш-адресацией необходимо записать во внешнюю память измененную версию только одного блока файла, содержащего эту новую запись. Если нет переполнения, организовывать доступ к другим блокам файла не нужно; правда, возможно, понадобится еще обращение к ин- дексу. Так как индексно-последовательный файл предназначен и для произвольного доступа, включение в него новой записи свя- зано с помещением во внешнюю память измененной версии только одного блока. Исключения связаны с обработкой переполнений и с редко происходящими изменениями индекса. Нет смысла оста- навливаться на деталях какого-нибудь одного метода индексно- последовательной организации. Лучше поговорить о чертах, при- сущих всем методам этого семейства. /Во время создания индексно-последовательного файла часть выделенной для него памяти была оставлена свободной. Позже,, когда в файл помещается новая запись, ее следует расположить так, чтобы не нарушить упорядоченность. Для того чтобы это стало возможным, необходимо передвинуть все записи блока, которые должны следовать за новой записью. Такой сдвиг воз- можен только в том случае, если в блоке есть свободная зона. Например, после включения в файл TOYINVEN новой записи AS6900200 блок 4 примет следующий вид: АР83 00914 AQ01 00247 AS65 0 0184 181
AS69 00200 AS80 00001 АТ50 00034 AV18 01075 Записи с ключами AS80, АТ50 и AV18 передвинутся так, что со- держимое блока останется упорядоченным. Вносить изменения в индекс не потребуется, поскольку AV18 останется наибольшим значением ключа для записей, содержа- щихся в блоке 4. Когда из файл удаляется запись, следующие за ней записи передвигаются так, чтобы заполнить образовавшееся свободное пространство. Например, после удаления записи TL81 00034 блок 11 примет следующий вид: SY73 0 0 094 SY75 00008 VF62 00315 VK89 00458 VR34 0 0788 Индекс останется неизменным, поскольку не изменится макси- мальное для блока значение ключа UR34. Если же из файла уда- лить запись UR34 00788, то изменяется запись индекса, отно- сящаяся к блоку 11: UK89 адрес 11-го блока. Для всех блоков, кроме последнего, справедливо следующее: только удаление может привести к изменению индекса. Например, новая запись SY72 00350 после включения ее в файл окажется первой в блоке 11, а не последней в блоке 12. Если бы она стала последней, это бы привело к изменению индекса. Запись, зна- чение ключа которой больше, чем значение ключа последней записи в последнем блоке, будет помещена в последний блок, и соответствующим образом будет изменена запись индекса, от- носящаяся к последнему блоку. 6.6.3. ПЕРЕПОЛНЕНИЕ Можно предложить очевидный способ включения за- писи в блок, в котором нет свободного места. Он заключается в пе- реписывании последней записи этого блока в следующий за ним блок, последней записи этого второго блока в третий И| т. д. до тех пор, пока в каком-нибудь блоке не обнаружится свободное место. Но, естественно, такой способ требует значительных за- трат времени, и от него следует отказаться. Нужны какие-то другие методы борьбы с переполнением. Таких методов довольно много, но здесь будет рассмотрен только один. Этот довольно из- вестный метод напоминает второй метод борьбы с переполнением, описанным в разд. 6.5.2. К каждому блоку прикрепляется один (возможно пустой) связанный список записей, не поместившихся в этот блок. Указа- 182
гель, ссылающийся на начало этого списка, хранится в индексе вместе с физическим адресом блока. Индекс представляет собой файл, состоящий из записей, имеющих следующие поля: МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ ФИЗИЧЕСКИЙ АДРЕС БЛОКА ДАННЫХ МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА, ПРИНАДЛЕ- ЖАЩЕЕ ОДНОЙ ИЗ ЗАПИСЕЙ, НЕ ПОМЕСТИВШИХСЯ В БЛОК УКАЗАТЕЛЬ К ПЕРВОЙ ЗАПИСИ В СВЯЗАННОМ СПИСКЕ ЗАПИСЕЙ, НЕ ПОМЕСТИВШИХСЯ В БЛОК Покажем на примере работу по этому методу. Для этого вос- пользуемся файлом TOYINVEN из разд. 6.6.2. Будем считать, что все блоки, кроме 9 и 11, содержат по шесть записей (как и было в разд. 6.6.2) и два выделенных блока по восемь. Записи, входящие в состав блоков 9 и 11, приведены ниже: 9: ММ72 00006 11: SY71 00060 MQ09 00014 SY73 00094 MS33 00556 SY75 00008 МТ50 00078 TL81 00034 MW45 00004 VF62 00315 РАН 00314 VK89 00458 РС25 00080 VP16 00082 РК39 00041 VR34 00788 Таким образом, пока переполнение не произошло. Второй блок индекса содержит следующие данные: МАКСИМАЛЬНОЕ АДРЕС МАКСИМАЛЬНОЕ УКАЗАТЕЛЬ ЗНАЧЕНИЕ КЛЮЧА БЛОКА ЗНАЧЕНИЕ СПИСКА В БЛОКЕ ДАННЫХ ДАННЫХ КЛЮЧА ДЛЯ ЗАПИСЕЙ, ПЕРЕПОЛНИВШИХ БЛОК РК39 9 РК39 nil SX19 10 SX19 nil VR34 11 VR34 nil YH61 12 YH61 nil Заметим, что в каждой из этих записей значения двух полей МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА одинаковы. Но это только до тех пор, пока нет переполнения. Перейдем к включению записи MS34 00065 в блок 9 и записи VR33 00006 в блок 11. Включение должно быть произведено так, чтобы сохранилась упорядоченность файла. В связи с этим последние записи вытал- киваются из блоков 9 и 11 и попадают в область переполнения. Блоки 9 и 11 теперь состоят из следующих записей: 9: ММ72 00006 MQ09 00014 MS33 00356 11: SY71 00060 SY73 00094 SY75 00008 183
MS34 00065 TL81 00034 МТ50 00078 VF62 00315 MW45 00004 VK89 00458 РАН 00314 VP16 00082 РС25 00080 VR33 00006 В области переполнения также имеются две записи: КР39 0 0041 nil VR34 0 0788 nil Каждая из этих двух записей содержит указатель, ссылающийся на остальные записи, не поместившиеся в блок. В данный момент значением указателей является „nil”, поскольку других записей, относящихся к блокам 9 и 11, в области переполнения нет. Изме- нится и второй блок индекса. Он примет следующий вид: МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ АДРЕС БЛОКА ДАННЫХ РС25 9 SX19 10 VR33 11 YH61 12 МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА ДЛЯ ЗАПИСЕЙ, ПЕРЕПОЛНИВШИХ БЛОК РК39 SX19 VR34 YH61 УКАЗАТЕЛЬ СПИСКА к РК39 nil к VR34 nil Проведем теперь включение в индексно-последовательный файл одну за другой следующих шести записей: SY08 00416 VQ62 00057 PD95 00006 VR31 00016 MS36 00060 VA70 00011 В результате изменится содержимое только двух блоков файла 9 и 11: 9: ММ72 00006 11: SY08 00416 MQ09 00014 SY71 00060 MS33 00556 SY73 00094 MS34 00065 SY75 00008 MS36 00060 TL81 00034 МТ50 00037 VA70 00011 MW45 00004 VF62 00315 РАН 00314 VK89 00458 Новые записи были размещены в этих блоках так, чтобы сохранилась упорядоченность записей внутри блоков. Это привело к тому, что несколько записей было удалено из блоков и попало в область переполнения. Если в блок включается запись, значение ключа которой меньше максимального значения ключа для списка 184
записей переполнения этого блока, но больше максимального значения ключа для записей этого же блока, то эта запись должна быть помещена в список записей переполнения все того же блока. Ниже изображена область переполнения (записи, входящие в один и тот же список, связываются стрелками). * Когда запись попадает в область переполнения, она поме- щается на первое свободное место и с помощью указателей так связывается с другими записями списка, чтобы не нарушить их упорядоченность. Во втором блоке индекса теперь содержится следующая информация: а) МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ; б) АДРЕС БЛОКА ДАННЫХ; в) МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА ДЛЯ ЗАПИСЕЙ, ПЕРЕПОЛНИВШИХ БЛОК; г) УКАЗАТЕЛЬ СПИСКА. (а) (6) (в) (г) РАН 9 РК39 к РС25 SX19 10 SX19 nil VK89 11 VR34 к VP16 YH61 12 YH61 nil В блоке 9 записи находятся в отсортированном виде. Анализ индекса показывает, что список записей переполнения для блока 9 начинается с записи, значением ключа которой является РС25. Отметим, что записи переполнения в этом списке упорядочены и должны были бы находиться в блоке 9, если бы в нем было место. Для блока 11 список записей переполнения начинается с записи * Все списки записей переполнения находятся в одной области перепол- нения. — Прим. пер. 185
G ключом VP16. Легко проверить, что и в этом списке записи упо- рядочены. Последней в списке переполнения стоит запись, которая ранее была последней в блоке II. Для включения записи в файл необходимо, используя метод линейного поиска, найти в индексе первое МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА ДЛЯ ЗАПИСЕЙ, ПЕРЕПОЛНИВШИХ БЛОК, которое больше ключа новой записи. Для этого же блока необходимо проверить, не больше ли МАКСИМАЛЬНОЕ ЗНАЧЕ- НИЕ КЛЮЧА БЛОКА ДАННЫХ ключа новой записи. Если больше, то запись помещается непосредственно в блок данных; это может привести к переполнению блока и, следовательно, к изменению индекса. Если же ключ новой записи превышает МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ, то запись помещается в список записей переполнения этого блока. При этом индекс модифицируется только в том случае, если новая запись займет первое место в списке. Поиск значения QUANTITYINSTOCK по заданному ITEMREFNO начинается с просмотра индекса. Просмотр продол- жается до тех пор, пока не будет обнаружен блок, МАКСИМАЛЬ- НОЕ ЗНАЧЕНИЕ КЛЮЧА ДЛЯ ЗАПИСЕЙ ПЕРЕПОЛНЕНИЯ которого больше или равно искомому значению ключа. Для этого блока проверяется, больше или равно МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ искомому значению КЛЮЧА. Если да, то организуется линейный поиск в блоке дан- ных; если нет — просматривается список переполнения. Отметим, что никогда поиск не проводится и в блоке данных и в списке переполнения одновременно, поскольку, проанализировав макси- мальные значения ключей блока, можно точно определить, где — в блоке или в списке переполнения — находится запись. Процесс удаления записи напоминает процесс поиска записи. Если запись обнаружена в списке переполнения, то ее удаление сводится к модификации указателя, принадлежащего предшест- вующей в списке записи, и к изменению индекса, если удаленная запись была первой или последней записью списка. Если запись обнаружена в блоке данных, то она удаляется и производится перемещение следующих за ней записей к началу блока (см. разд. 6.6.2). Если для этого блока существует список переполне- ния, то первая запись перемещается из него в блок данных и соответственно модифицируется индекс. Покажем на примере файла TOYINVEN, как производится удаление записей. Рассмотрим удаление записи VR33 00006 из списка переполнения блока 11 и записи MQ0900014 из блока 9. В результате удаления записи из блока 9 первая запись списка переполнения этого блока РС25 00080 перемещается обратно в блок 9. Содержимое блока 9 после этого примет следующий вид: 9: ММ72 00006 MS33 00556 MS34 00065 186
MS36 00060 МТ50 00037 MW45 00004 РАН 00314 РС25 00080 Ниже приведена запись индекса, соответствующая блоку 9: МАКСИМАЛЬНОЕ АДРЕС МАКСИМАЛЬНОЕ УКАЗАТЕЛЬ ЗНАЧЕНИЕ КЛЮЧА В БЛОКЕ ДАННЫХ БЛОКА ДАННЫХ ЗНАЧЕНИЕ КЛЮЧА ДЛЯ ЗАПИСЕЙ, ПЕРЕПОЛНИВШИХ БЛОК СПИСКА РС25 9 РК39 KPD95 Область переполнения теперь выглядит так: Когда запись удаляется из блока данных файла с хэш-адреса- цией, ни одна из записей области переполнения не передается обратно в блок данных. Когда запись удаляется из блока данных индексно-последовательного файла, запись из области переполне- ния передается в этот блок. Это различие возникает из-за того, что когда из файла с хэш-адресацией удаляется запись, поле NORECORD этой записи принимает значение TRUE. Следова- тельно, когда в блок помещается новая запись, она попадает на место записи с NORECORD == TRUE или, если такой записи нет, — в область переполнения. Главное здесь заключается в том, что, новая запись помещается на место первой записи с NORE- CORD = TRUE, поскольку нет необходимости сохранять упоря- доченность внутри блоков файла с хэш-адресацией. Это приводит к тому, что блоки данных файлов с хэш-адресацией обычно хорошо заполнены. Из-за того, что индексно-исследовательский файл предназна- чен для групповой обработки, упорядоченность записей в нем постоянно поддерживается. Не разрешается, не сортируя, поме- щать новые записи в свободные места блоков. Если бы записи не 187
передавались обратно в блоки данных из области переполнения, то удаление записей приводило бы к образованию свободных мест в блоках и новые записи попадали бы в списки переполнения, а не в блоки данных. А, как известно, поиск в списке переполнения требует в среднем больше времени, чем поиск непосредственно в блоке данных. В общем запись, находящаяся в блоке данных, может быть обнаружена без доступа к другим блокам, а поиск за- писи из области переполнения может повлечь за собой доступ к нескольким блокам, поскольку элементы списка переполнения обычно размещаются в разных блоках. Поэтому блоки индексно- последовательного файла необходимо заполнять записями как можно плотнее и стараться уменьшить длину списков перепол- нения. Выше был описан метод организации индексно-последователь- ных файлов, предусматривающий хранение записей переполнения для разных блоков данных в одном и том же блоке переполнения. Это позволяет полнее использовать пространство блоков перепол- нения. Если возможно, следует размещать весь файл, включая индекс, данные и блоки переполнения, в пределах одного цилиндра. В этом случае обращение после просмотра индекса к блоку данных или блокам переполнения не приводит к перемещению головок записи-считывания. 6.6.4. ИЕРАРХИЧЕСКИЕ ИНДЕКСЫ ДЛЯ ИНДЕКСНО-ПОСЛЕДОВАТЕЛЬНЫХ ФАЙЛОВ До сих пор индексы представляли собой простые после- довательные файлы. Повысить скорость работы с индексом можно, преобразовав его в индексно-последовательный файл. Для больших файлов, занимающих несколько цилиндров, это имеет принци- пиальное значение. Доступ к такому файлу можно начать с линей- ного поиска в индексе, что позволит определить максимальные значения ключей для всех цилиндров. Затем организуется доступ к выбранному цилиндру и анализируется индекс блоков, находя- щихся в этом цилиндре. Индекс блоков представляет собой тот же индекс, который был использован в разд. 6.6.2 и 6.6.3, и содержит максимальные значения ключей для блоков данных. Линейный поиск в индексе позволит найти блок данных, который содержит искомую запись. Для простоты не будем учитывать переполнение и допустим, что каждый цилиндр содержит по четыре блока. На рис. 6.3 изображена структура файла с двухуровневым индек- сом. Использование индекса цилиндров позволяет в случае произ- вольного доступа к записи в файле большого объема значительно уменьшить общее число блоков, к которым необходимо организо- вать доступ. Основная идея, лежащая в основе иерархического индекса, заключается в том, что можно добиться повышения эффективности, 188
Индекс цилиндров Индексы блоков Блоки данных Рис. 6.3. Индексно-последовательная организация с двумя уровнями индексации используя индекс, позволяющий перейти к более детальному ин- дексу, который, в свою очередь, позволяет перейти к еще более детальному индексу и т. д. до тех пор, пока не будут достигнуты реальные данные. Такого рода иерархические структуры будут описаны в разд. 6.8. 6.6.5. УПРАЖНЕНИЯ 1. Физические блоки файлов скрыты от программиста. Данное упражнение связано с очень простой индексно-последова- тельной структурой, в которой связанный список разбивается на секции. а. Создайте текстовый файл, содержащий 12 записей, каждая из которых состоит из номера и названия месяца, например: 1 J anuary 2 February 3 March И Т. д. б. Напишите DBD для использования в следующих пунктах упражнения. в. Напишите программу для считывания данных из текстового файла в структуру, схема которой изображена на рис. 6.4. Индекс представляет собой массив записей. Каждая запись состоит из 189
Связанный список Индекс записей данных Рис. 6.4. Структура, предназначенная для хранения данных о номерах и назва- ниях месяцев порядкового номера месяца и указателя к первой записи той секции списка, последняя запись которой имеет поле с тем же самым номером месяца. Можно использовать не только указатель, ссылающийся на первую запись списка, но и указатель к последней записи списка. Это облегчит включение записей в конец списка. г. Напишите программу для считывания с терминального уст- ройства порядкового номера месяца и выдачи соответствующего номеру названия месяца, найденного с помощью просмотра всего списка без использования индекса. Ваша программа должна также определять номера значений ключей, с которыми введенный поряд- ковый номер месяца сравнивается в процессе поиска. д. Напишите программу для считывания с терминального уст- ройства порядкового номера месяца и поиска в индексе первого номера месяца, большего или равного введенному. Далее, восполь- зовавшись указателем, входящим в запись каждого индекса, необ- 190
ходимо найти в списке и вывести на терминальное устройство название месяца, соответствующее введенному номеру. Ваша про- грамма должна также выдавать общее число значений ключа, с которыми в процессе поиска сравнивается введенный номер (в это число должны входить и значения ключей в индексе). 2.а. Во втором методе борьбы с переполнением из разд. 6.5.2 указатель, ссылающийся на начало списка переполнения, хра- нится в блоке данных. В аналогичном методе из разд. 6.6.3 такой же указатель хранится в индексе, а не в блоке данных. Объясните, в чем смысл этого отличия. б. В методе из разд. 6.6.3 для обнаружения значения ключа никогда не ведется поиск и в блоке данных и в списке переполнения одновременно. Наоборот, второй метод из разд. 6.5.2 в принципе предусматривает поиск как в блоке данных, так и в списке перепол- нения блока. Объясните, из-за чего возникает такая разница. в. Объясните, почему в методе из разд. 6.6.3 записи переполне- ния объединяются в цепь, которая размещается вне блоков данных. 3. В этом упражнении используется индекс, который является связанным списком записей типа ♦ record HIGHESTKEYINBLOCK: packed array [1..4] of CHAR; POINTERTOBLOCK: BLOCKPTR; POINTERTONEXTINDEXRECORD: INDEXPTR end. DBD будет также включать следующие объявления: type STRING4 = packed array [1 ..4] of CHAR; BLOCKPTR =TBLOCK; TOYINVREC = record ITEMREFNO: STRING4; QUANTITYINSTOCK: INTEGER BLOCK = array [1..6] of TOYINVREC Блоки предназначаются для хранения записей типа TOYINV- REC, упорядоченных по возрастанию ITEMREFNO. Записи индек- са будут расположены в порядке возрастания значения поля HIGHESTKEYINBLOCK- Таким образом, вся структура будет моделью файла с индексно-последовательной организацией. а. Напишите DBD для использования в следующих пунктах упражнения. б. Напишите программу инициализации, с помощью которой указателю, ссылающемуся к началу связанного списка, присваи- вается значение „nil**. в. Напишите программу считывания записи файла TOYINVEN с терминального устройства и включения ее в индекс-последова- тельную структуру в соответствии со следующими правилами: 191
если ранее записи не вводились, то, используя инструкцию CREATE, создайте блок, поместите в него введенную запись и, снова используя инструкцию CREATE, создайте запись индекса, ссылающуюся на этот блок; если возможно, то поместите новую запись в существующий блок так, чтобы сохранилась упорядоченность. Если это приведет к переполнению, то с помощью инструкции CREATE создайте новый блок, поместите в него три записи из того блока, в котором произошло переполнение, создайте новую запись индекса и вклю- чите ее в индекс так, чтобы сохранилась упорядоченность; если значение ключа новой записи больше максимального значения ключа в индексе, создайте новый блок, поместите новую запись в этот блок, создайте новую запись индекса и включите ее в индекс, сохранив при этом упорядоченность. г. Напишите программу считывания значения ITEMREFNO с терминального устройства и поиска соответствующего ей значе- ния QUANTITYINSTOCK в индексно-последовательной струк- туре. Если поиск закончится успешно, то выведите на терминаль- ное устройство значение QUANTITYINSTOCK, если — нет, то предусмотрите соответствующее сообщение. д. Напишите программу считывания ITEMREFNO с терми- нального устройства и удаления из индексно-последовательной структуры записи с этим значением ITEMREFNO. В блоке, из которого удалили запись, переместите оставшиеся записи так, чтобы между ними не было промежутка. Модифицируйте соответ- ствующим образом индекс. Если блок окажется пустым, то его необходимо удалить с помощью инструкции DELETE. Кроме того, должна быть удалена соответствующая запись из индекса. 6.7. СОПРОВОЖДЕНИЕ ФАЙЛОВ Произвольный доступ к файлу с хэш-адресацией произ- водится тем быстрее, чем меньше записей находится в области переполнения. Если записей в области переполнения много, то можно следующим образом изменить структуру файла. Сначала все записи файла перепишем в простой последовательный файл. Затем уничтожим файл с хэш-адресацией и создадим новый файл, содержащий блоков больше, чем его предшественник. Естественно, что в этом случае надо изменить и функцию хэширования. После этого будем по очереди считывать записи из последовательного файла и помещать их в новый файл с хэш-адресацией, используя для определения номера блока, в который будет помещаться запись, функцию хэширования. В результате всей этой процедуры будет получен файл с хэш-адресацией, произвольный доступ к кото- рому будет осуществляться быстрее, чем к старой версии этого же файла. Такого рода операции входят в комплекс работ по сопро- вождению файла. Если обнаружено, что блоки файла с хэш-адресацией содержат всего по несколько записей и большая часть блоков остается 192
незанятой, то можно создать новый файл, содержащий меньшее число блоков, и соответствующим образом изменить функцию хэширования. Эта операция —другой пример организации работ по сопровождению файла. Реконструировать индексно-последовательный файл (напри- мер, такой, как в разд. 6.6.3) имеет смысл, когда списки перепол- нения становятся длинными и принадлежащие им записи разме- щаются в нескольких блоках, в особенности, если эти блоки не находятся в одном цилиндре. Для этого надо сначала переписать записи из индексно-последовательного файла в простой последо- вательный файл, сохранив при этом их упорядоченность. Затем, уничтожив индексно-последовательный файл, создать новый, со- держащий возможно другое число блоков файл и предусмотреть в каждом блоке файла значительное свободное пространство. И, на- конец, переписать все записи из последовательного файла в ин- дексно-последовательный. Это еще один пример операции по сопро- вождению файла, показывающий, как можно увеличить эффектив- ность доступа. В следующем разделе будут введены В-деревья. В-деревья являются, по сути дела, индексно-последовательными файлами, имеющими иерархические индексы. Отметим, что В-деревья не нуждаются в специальных средствах сопровождения. 6.8. В-ДЕРЕВЬЯ 6.8.1, ОСНОВНАЯ ТЕРМИНОЛОГИЯ Перед описанием В-деревьев введем несколько опреде- лений, относящихся к деревьям любых типов. На рис. 6.5, а показано дерево. Дерево состоит из двух мно- жеств: множества вершин и множества ориентированных дур. На рис. 6.5, а вершинам соответствуют прямоугольники, а дугам — стрелки. Корневой называется вершина, не имеющая входящих в нее дуг. Дерево всегда имеет только один корень. На рис. 6.5, а вер- шины названы Nl, N2, N3 и т. д.; N1—корневая вершина. Листом называется вершина, которая не имеет выходящих дуг. На рис. 6.5, a N5, N6, ..., N13 — листья. Рассмотрим произволь- ную вершину. Вершины, в которые входят дуги, начинающиеся в этой первой вершине, называются дочерними, а сама первая вершина—родительской. Например, N8, N9 и N10 — дочерние по отношению к N3 вершины. У листьев нет дочерних вершин. Важным отличительным свойством дерева является то, что каждая его вершина кроме корня имеет только одну родительскую вер- шину. Предположим, что для-двух вершин NJ и NK справедливо следующее: NK является дочерней по отношению к вершине, кото- рая, в свою очередь, является дочерней по отношению к вершине, 7 Ульман Дж. 193
Рис. 6.5. Примеры сбалансированного и несбалансированного деревьев которая и т. д. является дочерней по отношению к NJ. В этом случае NJ — предок NK, a NK — потомок NJ. Например, на рис. 6.5, a N6 является потомком Nl, a N1 является предком N8. N3 также является предком N8, так как отношение родитель- ский — дочерний является частным случаем отношения предок— потомок. Дерево называют сбалансированным тогда и только тогда, когда каждый его лист имеет одинаковое число предков. На рис. 6.5, а изображено сбалансированное дерево, а на рис. 6.5, б — несбалансированное. 6.8.2. ПОИСКИ В В-ДЕРЕВЬЯХ Введем понятие В-дерева с помощью примера, в котором используются записи, содержащие только по одному полю. Это единственное поле является ключом и состоит из двух букв, но все последующие рассуждения, относящиеся к этим записям, будут справедливы и для записей, состоящих из нескольких полей. На рис. 6.6 показано В-дерево, в котором DI, D2, ..., D7 являются блоками данных, содержащими записи, отсортированные по первичному ключу. Если запись отсутствует, т. е. NORE- CORD = TRUE, то ее обозначают "—если используется указа- тель, равный ,,nil“, то его обозначают "•*. Ill, 112 и 113 — это индексные блоки, которые ссылаются к блокам данных. К этим индексным блокам имеются ссылки из индексного блока более высокого уровня II. Индексный блок II играет роль корне- вого блока В-дерева. 194
11 J Fiji JH м ВЗ JM JIS D6 JJ7 Рис. 6.6. Пример В-дерева В разд. 6.6.3 в индексе хранились максимальные для каждого блока значения ключей. Для В-деревьев более удобен индекс, содержащий минимальные для каждого блока значения ключей. Эту разницу важно запомнить. Для первого блока, на который есть ссылка из индекса, бессмысленно хранить минимальное значение ключа, поскольку каждая запись со значением ключа, меньшим первого хранимого значения ключа для второго блока, должна быть помещена в первый блок. Именно поэтому каждый индексный блок В-дерева, изображенного на рис. 6.6, начинается с указа- теля, с которым не связано никакое значение ключа. Например, первым в индексном блоке 112 стоит указатель к блоку данных D3, а соответствующего ему значения ключа в индексном блоке 112 нет. Вторая запись в 112 ссылается к D4 и, кроме того, сообщает о том, что минимально возможное значение ключа в блоке D4 равно ЕЕ. Аналогично, третья запись в 112 ссылается к D5 и содержит минимально возможное для блока D5 значение ключа — GG. Первая запись корневого блока II состоит из указателя, ссылающегося к Ill, и не имеет связанного значения ключа. Вторая запись блока II ссылается к 112 и содержит минимально возможное для блока 112 значение ключа — ВН. Аналогично, третья запись блока 12 ссылается к 113 и содержит минимально возможное для блока 113 значение ключа JB. Чтобы найти в В-дереве запись с заданным значением ключа, необходимо начать с линейного поиска в корневом блоке первого значения ключа, которое больше искомого. Затем надо воспользо- ваться указателем, который принадлежит записи индекса, пред- шествующей той, которая была обнаружена ранее. Если в корневом блоке нет значения ключа, которое больше искомого, то надо вос- пользоваться указателем последней записи блока. Если указатель ссылается, в свою очередь, к еще одному индексному блоку, то поиск необходимо продолжать точно так же, как и раньше, и в этом индексном блоке. Когда, наконец, будет достигнут блок дан- ных, нужно будет опять воспользоваться методом линейного поиска для обнаружения записи с искомым значением ключа. Предположим, например, что искомым значением ключа явля- ется FT. Сначала сравним это значение ключа с значением ключа 7* 195
Рис. 6.7. В-дерево с тремя уровнями индексации из второй записи корневого блока II. Так как FT > ВН, то необ- ходимо сравнить искомое значение ключа с JB. Поскольку FT < < JB, поиск в блоке II закончен. Теперь перейдем к блоку 112, следуя за указателем, идущим от II. Поиск в блоке 112 продол- жается до тех пор, пока значение ключа какой-нибудь записи этого блока не станет больше, чем FT. Таким образом значением ключа является GG. Далее следуем за вторым указателем, веду- щим от 112 к D4. Применяя метод линейного поиска, находим в блоке D4 искомую запись со значением ключа FT. Рассмотрим еще один пример. Предположим, что надо найти запись со значением ключа, равным BG. Поскольку BG < ВН, переходим от блока II к блоку Ill. Поиск в блоке Ill не дает результата. Поэтому продолжаем поиск в блоке, на который ссылается последний указатель из Ill. Этим блоком является D2. В нем и удается обнаружить BG. Для В-дерева на рис. 6.6 произвольный доступ организован так, что всегда необходимо просматривать два индексных блока. Но естественно описанный метод поиска является более общим и пригоден для В-деревьев со многими уровнями индексации. На рис. 6.7 изображено В-дерево с тремя уровнями индексации. В-деревья относятся к семейству индексно-последовательных структур. Записи, расположенные внутри блоков данных В-де- ревьев, всегда упорядочены. В связи с этим В-деревья удобны для групповой обработки. Иерархический индекс нужен для про- извольного доступа и не обязателен для простого последователь- ного доступа. Таким образом, В-деревья удобны как для произ- вольного, так и для последовательного доступа. 6.8.3. ОСНОВНЫЕ СВОЙСТВА В-ДЕРЕВЬЕВ Хотя не было дано формального определения В-дерева, основные свойства В-деревьев должны быть перечислены. 196
1. Каждый блок рассчитан на хранение максимально возмож- ного нечетного числа записей (в блоке всегда больше двух записей). В индексных блоках учитываются и первые записи, не содержащие значений ключа. Все индексные блоки предназначены для хранения одного и того же числа записей. Все блоки данных также имеют одинаковую емкость. Но блоки данных и индексные блоки могут содержать разное число записей. 2. Нет такого блока (за исключением корневого), который может быть более чем наполовину пустым. 3. В-деревья всегда сбалансированы. 6,84. ВЫСОТА В-ДЕРЕВА Предположим, что в В-дереве хранится D блоков дан- ных. Для простоты допустим, что каждый индексный блок, за исключением корневого, содержит ровно i записей. Предполо- жим также, что L — число уровней индекса; например, для В-де- рева, изображенного на рис. 6.6, L = 2, L — это высота В-де- рева; блоки данных при определении L во внимание не прини- маются. Высота — важная характеристика В-дерева, поскольку именно она определяет количество индексных блоков, которые надо просмотреть, чтобы найти искомые данные, т. е. высота — важный фактор, влияющий на эффективность произвольного доступа. Поскольку каждый индексный блок содержит ровно i запи- сей, число индексных блоков, расположенных на уровне, пред- шествующем блокам с данными, равно DH. Число индексных блоков на третьем снизу уровне равно D (ixi), на четвертом — D/(iXiXi) и т. д. Число индексных блоков на уровне корневой вершины, естественно, равно единице. Поэтому D/(iL) = 1 и, следовательно, L = logf D. Если индексные блоки содержат больше i записей, то высота дерева будет меньше, чем logs D. Предположим, например, что D = 100 000, i = 10. Тогда для организации произвольного доступа необходимо будет обра- щаться не больше, чем к log10 100 000 = 5 индексным блокам. На первом уровне будет не более 100 000// = 10 000 индексных блоков. Если воспользоваться методом линейного поиска для обнаружения одного индексного блока из этих 10 000, то в сред- нем надо будет организовать доступ к 10 000/2 = 5000 индексным блокам. Используя В-дерево с i = 10 вместо одноуровневого индексно-последовательного файла с i записями в каждом индекс- ном блоке, можно значительно уменьшить число обращений к ин- 197
дексным блокам. В результате общее время, затрачиваемое на доступ, будет сокращено примерно в 1000 раз. Этот пример показывает, насколько эффективно применение В-деревьев. Использование хэширования позволяет найти искомую запись, организовав доступ к меньшему числу блоков, чем при хранении записей в В-дереве. Но преимуществом В-деревьев является то, что они сохраняют упорядоченность записей. Во время создания файла с хэш-адресацией надо хотя бы приблизительно знать общее число записей. Как будет видно в дальнейшем при органи- зации В-дерева, не обязательно знать общее число записей в файле. Это также является одним из преимуществ В-деревьев. 6.8.5. ВКЛЮЧЕНИЕ ЗАПИСИ В В-ДЕРЕВО Для включения записи в В-дерево необходимо сначала найти соответствующий блок данных, т. е. провести ту же опера- цию, что и в случае поиска записи. Если в блоке есть свободное место, включение производится так же, как описано в разд. 6.6.2: упорядоченность записей сохраняется и, если нужно, следующие за включаемой записи передвигаются. Если же в блоке нет сво- бодного места, то необходимо создать новый блок. После этого некоторые из записей старого блока передаются в новый блок. Причем число передаваемых записей должно быть таким, чтобы старый и новый блоки содержали равное число записей. После включения новой записи в блок данных соответствующим образом должны быть изменены записи индекса. Например, предположим, что необходимо включить в В-дерево, изображенное на рис. 6.6, запись ВА. В результате блок данных 2 имеет вид AW ВА BG Теперь поместим в это же В-дерево запись AY. Для этого потре- буется новый блок, поскольку в блоке D2 больше места нет. На новый блок D8 должен ссылаться указатель из индексного блока Ill. Та часть В-дерева, которая изменилась после выключения, изображена на рис. 6.8. Отметим, что старый блок D2 и новый блок D8 содержат одина- ковое количество записей. Опишем теперь процесс включения в В-дерево записей СЕ и DT. Поскольку в D3 места нет, создадим новый блок D9: [и В индексном блоке 112 нет места для указателя, ссылающегося на D9. Поэтому необходим новый индексный блок 114. Поскольку 198 НЕ D9 ВН СЕ —• де
Рис. 6.8. Модифицированная часть В-де- рева после операций включения в II также нет свободного места для указателя к 114, требуется новый индексный блок 12. В дереве должна быть только одна корневая вершина. В связи D1 JJ2 Л8 с этим генерируется новая корневая вершина 10, от которой идут указатели к II и 12. После описанного выше ввода новых блоков В-дерево существенно изменит свой вид (см. рис. 6.7). 6.8.6. УДАЛЕНИЕ ЗАПИСИ ИЗ В-ДЕРЕВА Для обнаружения записи, подлежащей удалению, можно использовать ту же процедуру, что и для поиска. После удаления записи необходимо заполнить образовавшееся свободное пространство. Для этого записи, находящиеся за удаленной, должны быть передвинуты так же, как описано в разд. 6.6.2. Если теперь блок оказался более чем наполовину пустым, необходимо найти соседний с ним блок, имеющий ту же самую родительскую вершину. Причем этот блок должен содержать более минимально необходимого числа записей. Если такой блок существует, то записи из него передаются в блок, из которого произведено удале- ние, в таком количестве, чтобы сделать число записей в этих двух блоках примерно одинаковым. Естественно, что все проводимые операции требуют соответствующей модификации индекса. Если блок, из koi ;рого удалена запись, оказывается более чем наполовину пустым и если не существует соседнего с ним блока, имеющего ту же самую родительскую вершину и содержа- щего более минимально необходимого числа записей, то первый блок сливается с соседним с ним блоком, имеющим ту же самую родительскую вершину. Затем производится уничтожение той записи индекса, указатель которой ссылается на удаленный блок. Если в результате этого индексный блок оказывается заполненным менее чем наполовину, повторяем описанную выше процедуру и делаем так до тех пор, пока дальнейших изменений индекса не потребуется. Рассмотрим несколько примеров удаления записей из В-дерева, изображенного на рис. 6.7. После удаления записи AW блок D2 становится более чем наполовину пустым. Поэтому производится слияние блоков D2 и D8 и соответствующим образом изменяется индексный блок Ill. На рис. 6.9 показана та часть В-дерева, которая претерпела изменения. Отметим, что вместо слияния блоков D2 и D8 можно было бы объединить блоки D1 и D2. Будем считать, что В-дерево, изображенное на рис. 6.7, не изменилось. Проведем удаление из него записи ВН. Следствием 199
27/ Лв Рис. 6.9. Измененная часть В-дерева после опе- рации удаления удаления является объединение блоков D3 и D9 в блок D3 и уничтожение блока D9. Тогда 112 окажется более или на- половину пустым. Соседний с 112 блок Ill содержит более минимально необ- ходимого числа записей; поэтому передадим запись из блока Ill в блок 112. Заметим, что изменения коснутся и блока И. Следовательно, возможно, что удаление записи повлечет за собой изменение предка родительской вершины этой записи. На рис. 6.10 показана измененная часть В-дерева. Удаление записи JB приводит к объединению блоков D6 и D7, что в свою очередь вызывает слияние 113 и 114. Затем происходит объединение блоков II и 12 и уничтожение блока 10, который после слияния II и 12 становится ненужным. В результате В-дерево существенно изменит свою структуру (рис. 6.11). Как было отмечено выше, важным преимуществом В-деревьев является то, что они не нуждаются в сопровождении. И действи- тельно, в процедуре удаления записи предусмотрены средства для уничтожения ненужных блоков, а процедура включения записи снабжена средствами генерации новых блоков. DI UZ В8 JJ3 Рис. 6.10. Модифицированная часть В-дерева ВЗ 274 В5 D7 Д/ В2 В8 Рис. 6.11. В-дерево, сформировавшееся в результате всех удалений 200
6.8.7. ОБЪЯВЛЕНИЕ В-ДЕРЕВЬЕВ На языке Паскаль достаточно просто написать процеду- ры для включения, удаления и поиска записей в связанных списках и файлах с хэш-адресацией. Аналогичные процедуры для В-де- ревьев написать сложнее. Чтобы освободить программиста от вы- полнения этой работы, будем считать, что эти процедуры являются стандартными. Также будем считать, что для описания В-деревьев зарезервирован стандартный тип языка Паскаль. Термин стандарт- ный в данном случае означает, что программист может использо- вать конструкцию без предварительного объявления как, напри- мер, стандартную процедуру READ или стандартный тип REAL. Использование стандартного типа и стандартных процедур для работы с В-деревьями позволит значительно упростить програм- мирование баз данных. Следует, к тому же, учесть, что к структу- рам типа В-деревьев возможен произвольный и последовательный доступ и что они не требуют сопровождения. Ниже приведен пример объявления: type TREENAME =s BTREE of RECORDTYPENAME on FIELD- NAME 1 В данном объявлении TREENAME — имя типа, объектами которого являются В-деревья. Листьями В-деревьев являются блоки, состоящие из записей типа RECORDTYPENAME, которые упорядоченны по возрастанию FIELDNAME1. FIELDNAME1 обязательно должно быть одним из полей комбинированного типа RECORDTYPENAME. Кроме того, FIELDNAME1 — ключ, в соответствии со значениями которого сортируются записи В-дере- ва. Иногда бывает необходимо, чтобы ключ сортировки состоял из двух полей. Например, чтобы сделать FIELDNAME1 —глав- ным, a FIELDNAME2—дополнительным ключом сортировки, достаточно объявить type TREENAME = BTREE of RECORDTYPENAME on FIELDNAME2 within FIELDNAME1 Конструкция" within" может быть повторена несколько раз. Например, если ключ сортировки состоит из четырех полей: FIELDNAME1, FIELDNAME2, FIELDNAME3, FIELDNAME4, то объявление будет выглядеть следующим образом: type TREENAME = BTREE of RECORDTYPENAME on FIELDNAME4 within... within FIELDNAME2 within FIELDNAME1 Ключевое слово "descending'', помещенное после "on" или "within", означает, что записи в В-дереве расположены в порядке убывания значения этого поля. Например, если записи должны быть расположены в порядке убывания значения поля FIELD- NAME2 внутри поля FIELDNAME1, необходимо объявить 201
type TREENAME = BTREE of RECORDTYPENAME on descending FIELDNAME2 within FIELDNAME1 Ключ сортировки В-дерева в дальнейшем будем просто называть ключом. До сих пор рассматривались В-деревья, состоящие только из записей. Но в общем случае элементы В-деревьев могут отно- ситься к любому дискретному типу, в том числе, целому, ссылоч- ному, символьному, или интервальному. Тогда в объявлениях В-деревьев не должно присутствовать ключевое слово "on", а записи, входящие в листья В-деревьев, автоматически разме- щаются в порядке возрастания их значений. Например, можно объявить type SUBEX = 100...400; TREENAME = BTREE of SUBEX; В этом примере в блоках, соответствующих листьям В-дерева, находятся не записи, а целые числа, лежащие в диапазоне от 100 до 400. Поскольку с помощью конструкции BTREE описываются но- вые типы данных, имеющие структуру В-дерева, можно объявлять переменные, принадлежащие этим типам. Например var ATREE: TREENAME; LOOKATTHESECONDFIELDOFTHISONE: record FIRSTFIELD: INTEGER; SECONDFIELD: TREENAME end Стандартные процедуры, которые будут рассмотрены в разд. 6.8.8, предназначаются для работы с переменными, имею- щими структуру В-деревьев, т. е. с переменными, относящимися к типу В-дерева. Пример DBD. В разд. 6.8.8. все стандартные процедуры будут работать со следующим очень простым DBD: DBDNAME МТ; type REFER ANGE = 1000..9999; INVPTR = t INVTYPE; INVTYPE = record ITEMREFNO: REFRANGE; QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: 10.99; end; 202
INVTREE= BTREE of INVTYPE on ITEMREFNO; var INVENTORY: INVTREE; finish В этом объявлении INVENTORY — относящаяся к типу В-дерева переменная, сохраняющая свое значение. Листья этого В-дерева являются записями типа INTYPE, отсортированными по возрастанию ITEMREFNO. 6.8.8. ПРОЦЕДУРЫ, ПРЕДНАЗНАЧЕННЫЕ ДЛЯ РАБОТЫ С В-ДЕРЕВЬЯМИ Переменная DBSTATUS. Ниже будет рассмотрен ряд процедур, которые можно использовать без предварительного объявления. Все эти процедуры могут присваивать значения гло- бальной переменной DBSTATUS, которую программист может исйользовать так же, как и любую другую глобальную перемен- ную. Отметим, что эту переменную объявлять не надо. Переменная DBSTATUS относится к перечисляемому типу, включающему следующие значения OKAY, NOTFOUND, DUPLICATE, EMPTY, EXHAUSTED. Если программа вызывает одну из описываемых ниже процедур, а значением переменной DBSTATUS в это время является не OKAY, то будет принудительно выполняться процедура ABORT- TRANSACTION и работа программы завершится. Процедура INSERT. Для включения новой записи в В-дерево используется процедура INSERT. Обращаться к ней следует так: INSERT «имя В-дерева), (имя записи)). Здесь первый параметр определяет В-дерево, в которое будет помещаться новая запись, а второй параметр — включаемую запись. Отметим, что тип записи должен обязательно совпадать с типом элементов В-дерева. Когда вызывается процедура INSERT, во включаемой записи должно быть определено значение ключа В-дерева. Приводимая ниже программа используется для считывания значения INPUTRECORD с терминального устройства и включе- ния его в В-дерево INVENTORY. program INVINSERT (INPUT. OUTPUT); invoke MT: var INPUTRECORD: INVTYPE; begin STARTTRANSACTION (UNRESTRICTED); WRITE ( type ITEMREFNO, QUANTITYINSTOCK, ’); WRITELN ( PRICEPERUNIT, SUPPLIERNO’); with INPUTRECORD do READLN (ITEMREFNO, QUANTITYINSTOCK, 203
PRICEPERUNIT, SUPPLIERNO); INSERT (INVENTORY, INPUTRECORD); if DBSTATUS = OKAY then ENDTRANSACTION else begin ABORTTRANSACTION; WRITELN (’insertion failed’) end end. Желательно, чтобы в программах такого рода был предусмот- рен контроль вводимых значений. Но как и ранее, для того чтобы не усложнять программу, здесь такой контроль не проводится. Если обнаруживается, что значение ключа вводимой записи уже присутствует в дереве, программа INSERT помещает новую запись следом за имеющей то же самое значение ключа записью, которая была включена в В-дерево последней. После этого пере- менной DBSTATUS присваивается значение DUPLICATE. Таким образом, записи с одинаковыми ключами располагаются в В-дереве в последовательности, которая соответствует очередности их вклю- чения в В-дерево. Если процедура INSERT не встречает записей с одинаковыми ключами, то переменной DBSTATUS присваивается значение OKAY. Процедура FIND KEY. Для того чтобы найти в В-дереве запись с заданным значением ключа, необходимо использовать процедуру FIND KEY: FIND KEY «имя В-дерева}, (значение ключа}, (указатель)). Здесь второй параметр является заданным значением ключа. Если ключ состоит более чем из одного поля, то значения полей, входящих в ключ, перечисляются через запятую. Третий пара- метр — это переменная ссылочного типа, объектами которого являются указатели, ссылающиеся на листья В-дерева. В резуль- тате выполнения процедуры INSERT указатель примет значение, ссылающееся на первую запись В-дерева с искомым значением ключа. Если запись с искомым значением ключа не найдена, то переменной DBSTATUS присваивается значение NOTFOUND; в противном случае — OKAY. Приводимая ниже программа считывает значение ITEMREFNO и выводит на терминальное устройство соответствующее ему зна- чение QUANTITYINSTOCK. program GETQUANT (INPUT, OUTPUT); invoke MT; var SOUGHTITEM: REFRANGE; ITEMPTR: INVPTR; begin STARTTRANSACTION (READONLY); 204
WRITELN (’please type item ref no’); READLN (SOUGHTITEM); FINDKEY (INVENTORY, SOUGHTITEM, ITEMPTR); if DBSTATUS = OKAY then WRITELN (’QUANTITY IN STOCK = ’, ITEMPTRf.QUANTITYINSTOCK); ENDTRANSACTION end. Процедуры FINDFIRST и FINDLAST. Формат вызова FIND- FIRST имеет следующий вид: FINDFIRST «имя В-дерева), (указатель)) В результате выполнения процедуры FINDFIRST указатель при- мет значение, ссылающееся на первую запись В-дерева. Аналогично вызывается и процедура FINDLAST. После ее выполнения указатель будет ссылаться к последней записи В-де- рева. Если во время выполнения процедур FINDFIRST и FINDLAST обнаруживается, что В-дерево пусто, то переменной DBSTATUS присваивается значение EMPTY. В противном случае DBSTATUS принимает значение OKAY. Процедуры FINDSUCC и FINDPRED. Вызов этих процедур производится следующим образом: FINDSUCC «имя В-дерева), (указатель)) FINDPRED «имя В-дерева), (указатель)) Перед вызовом указателю должно быть присвоено значение, ссы- лающееся к записи В-дерева. В результате выполнения процедуры FINDSUCC указатель будет ссылаться к следующей по порядку записи В-дерева; в результате выполнения процедуры FIND- PRED — к непосредственно предшествующей записи В-дерева. Если указатель не ссылается к записи В-дерева, то после вызова процедуры переменной DBSTATUS присваивается значение NOT- FOUND. Если в случае вызова FINDSUCC запись, на которую ссылается указатель, является последней записью В-дерева, или в случае вызова FINDPRED запись, на которую ссылается указа- тель, не имеет в В-дереве предшественников, то переменной DBSTATUS присваивается значение EXHAUSTED. В противном случае DBSTATUS принимает значение OKAY. Программа, приведенная ниже, позволяет выдать на терми- нальное устройство все входящие в В-дерево INVENTORY записи и разместить их в порядке возрастания значения поля ITEMREF- NO: program LISTINVEN (OUTPUT); invoke MT; var ITEMPTR: INVPTR; 205
begin STARTTRANSACTION (READONLY); FINDFIRST (INVENTORY, ITEMPTR); while DBSTATUS = OKAY do begin with ITEMPTR t do WRITELN (ITEMREFNO, QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); FINDSUCC (INVENTORY, ITEMPTR) end; ENDTRANSACTION end. Если в этой программе заменить FINDFIRST на FINDLAST a FINDSUCC на FINDPRED, то порядок размещения записей изменится на противоположный. Процедура REMOVE. В том случае, если необходимо удалить запись из В-дерева, можно использовать следующую процедуру: REMOVE ((имя В-дерева), (указатель)) Здесь указатель ссылается на запись,которая должна быть удалена. Если такая запись не найдена, то переменной DBSTATUS прис- ваивается значение NOTFOUND, в противном случае DBSTATUS принимает значение OKAY. С помощью следующей программы производится считывание с терминального устройства шифра товара и удаление из В-дерева INVENTORY записи, значение поля ITEMREFNO которой совпа- дает с введенным шифром товара. program CHOPITEM (INPUT. OUTPUT); invoke MT; var ITEMPTR: INVPTR; CHOPREFNO: REFRANGE; begin STARTTRANSACTION (UNRESTRICTED); WRITELN ('please type ITEM REF NO of item to be deleted'); READLN (CHOPREFNO); FINDKEY (INVENTORY, CHOPREFNO, ITEMPTR); if DBSTATUS = OKAY then begin REMOVE (INVENTORY. ITEMPTR); WRITELN ('removal completed'); ENDTRANSACTION end else begin WRITELN ('ITEMREFNO not found'); ABORTTRANSACTION end end. 206
Отметим принципиальную разницу, существующую между про- цедурами REMOVE и DELETE. С помощью REMOVE не только удаляется запись, принадлежащая какому-то листу В-дерева, но и, если необходимо, производится перемещение оставшихся в листе записей и модификация одного или нескольких индексных блоков. Процедура DELETE предназначена только для удаления записи. Модификация записи В-дерева. Для того чтобы чрезмерно не увеличивать число стандартных процедур, предназначенных для работы с В-деревьями, а также принимая во внимание то, что В-деревья планируется использовать главным образом для хра- нения индексов, а не данных, решено не заводить специальной процедуры для модификации записи В-дерева. Изменение записи можно осуществить так: сначала, используя процедуру FIND KEY, обнаружить запись, затем сделать копию этой записи и удалить саму запись из В-дерева с помощью REMOVE, и, наконец, моди- фицировать копию и поместить ее с помощью процедуры INSERT в В-дерево. Паскаль как ЯМД. Обычные языки программирования, снаб- женные средствами для работы с переменными, сохраняющими свои значения (например, такими как процедуры INSERT и FIND KEY), называются языками манипулирования данными. В дальнейшем в этой книге название ЯМД Паскаль будет исполь- зовано для обозначения таких версий языка Паскаль, которые позволяют работать с переменными, сохраняющими свои значения, включая в их число В-деревья. Процедуры ЯМД, такие как INSERT и FIND KEY, относятся к средствам более низкого уровня, чем выражения реляционной алгебры. Существуют сис- темы (они, правда, не рассматриваются в этой книге), которые дают возможность программисту в тексте обычной программы использовать выражения на языке запросов таком же, как реля- ционная алгебра. В результате определения значений этих выра- жений формируются данные, которые используются так же, как любые другие входные данные программы. В этом случае говорят, что язык запросов встроен в базовый язык. Язык запросов, кото- рый используется автономно (как реляционная алгебра в гл. 2), называется специализированным языком запросов. 6.8.9. УПРАЖНЕНИЯ 1. Целью этого упражнения является проверка, хо- рошо ли читатели разобрались в том, как производятся операции включения и удаления записей с В-деревьями. а. Вставить записи JC и JB в В-дерево на рис. 6.6 и нарисовать результирующее дерево. б. Вставить записи JE, JF и JG в В-дерево, полученное в 1, а, и нарисовать результирующее дерево. в. Удалить ВН из В-дерева, полученного в 1, б, и нарисовать результирующее дерево. г. Удалить DA и ЕЕ из В-дерева, полученного в 1, в, и нарисо- вать результирующее дерево. 207
2. Предположим, что в В-дереве хранятся данные об автомоби- лях, находящихся на стоянке. В состав этих данных входят: регистрационный номер, дата и время въезда на стоянку. Дата включает порядковые номера месяца и дня. Например, 902 озна- чает 2 сентября. DBD имеет следующий вид: DBDNAME РК; type STRING7 = packed array [1..7] of CHAR; CARENTRY = record REGNUMBER: STRING7; TIMEIN: 0..2359; DATE: 101..1231 end; PARKTREE = BTREE of CARENTRY on REGNUMBER; var CARSIN: PARKTREE; finish а. Напишите программу ввода с терминального устройства регистрационного номера автомобиля, времени и даты его въезда на стоянку и включения этой информации в В-дерево CARSIN. б. Напишите программу ввода с терминального устройства регистрационного номера автомобиля и удаления относящихся к этому автомобилю данных из В-дерева CARSIN. в. Напишите программу ввода с терминального устройства регистрационного номера автомобиля и поиска записи, относя- щейся к этому автомобилю, в CARSIN. г. Напишите программу вывода на терминальное устройство всех записей из CARSIN в порядке возрастания REGNUMBER. д. Напишите программу передачи всех записей из CARSIN в двоичный файл. Этот файл должен быть отсортирован по REGNUMBER. 3. Напишите программу считывания записей из двоичного файла, созданного в 2, д, в В-дерево. В качестве основного ключа сортировки должно быть использовано поле DATfe, а дополни- тельного—поле TIMEIN. DBD для этой задачи имеет следующий вид: DBDNAME PQ; type STRING7 = packed array [1..7] of CHAR; CARENTRY = record REGNUMBER: STRING7; TIMEIN: 0..2359; DATE: 101..1231 end; DATETIMETREE = BTREE of CARENTRY on TIMEIN within DATE; var WHENIN: DATETIMETREE; finish 208
а. Напишите программу считывания записей из файла, создан- ного в 2, д, и помещения их в В-дерево WHENIN. б. Напишите программу выдачи на терминальное устройство данных из WHENIN. 4. В этом упражнении используется файл товарных запасов из разд. 6.8.8, дополненный данными об изменениях цен. Каждому товару ставится в соответствие совокупность записей, включающих цену и дату введения этой цены. Записи, относящиеся к одному товару, хранятся в В-дереве, которое является полем записи INVTYPE. DBD для этого упражнения имеет следующий вид: DBDNAME MX; type REFRANGE = 1000..9999; PRICEREC=record PRICE:REAL; DATE: 880101..940101; end; PRICETREE = BTREE of PRICEREC on DATE; INVPTR = f INVTYPE; INVTYPE = record ITEMREFNO: REFRANGE; QUANTITYINSTOCK: INTEGER; PRICES: PRICETREE; SUPPLIERNO: 10..99; end; INVTREE = BTREE of INVTYPE on ITEMREFNO; var INVENTORY: INVTREE; ЯпЫ» а. Напишите программу считывания записи с терминального устройства и помещения ее в В-дерево INVENTORY. Полями вводимой записи должны быть ITEMREFNO, QUANTITYIN- STOCK, PRICE, DATE и SUPPLIERNO. б. Напишите программу ввода с терминального устройства значений ITEMREFNO, PRICE и DATE. Новые цена и дата должны быть включены в состав В-дерева PRICES, связанного с записью, значение поля ITEMREFNO которой совпадает с вве- денным. Если записи с введенным значением ITEMREFNO нет, то должно быть предусмотрено соответствующее сообщение. в. Напишите программу считывания с терминального устрой- ства значения ITEMREFNO и выдачи для товара с этим ITEM- REFNO его последней цены. г. Напишите программу считывания с терминального устрой- ства значений ITEMREFNO и даты и выдачи цены, соответствую- щей введенной дате. 209
ГЛАВА 7 РЕАЛИЗАЦИЯ ОПЕРАЦИЙ ВЫБОРА 7.1. ВВЕДЕНИЕ 7.1.1. ВЫБОР ПО ПЕРВИЧНОМУ КЛЮЧУ Если V — значение первичного ключа поля F для файла FNAME, то выполнение операции выбора sei F = V (FNAME) требует произвольного доступа к файлу FNAME. Конечно, это возможно только в том случае, когда FNAME — файл с произволь- ным доступом, а не простой последовательный файл. Таким образом, можно сказать, что организация данных в файле, обеспечивающая произвольный доступ, повышает скорость выполнения операций выбора по первичному ключу. Например, в разд. 6.2.2 показано, что хэш-адресация гарантирует быстрый ответ на запрос типа proj QUANTITYINSTOCK (sei ITEMREFNO = 'MQ09' ((TOYINVEN)) В этой главе будут рассмотрены способы повышения эффектив- ности реализации операций выбора для более широкой группы запросов, включающих не только первичный ключ, но и другие поля. Для этой цели и для обеспечения большей гибкости будут использованы индексы. 7.1.2. ВЛАДЕЛЬЦЫ И ЧЛЕНЫ ИНДЕКСОВ Индекс — это файл, с помощью которого можно опре- делить, где найти те или иные значения в другом файле. Например, в качестве индекса можно рассматривать указатель справочника по садоводству, элементами которого являются ВИНОГРАД 155, 165 МИНДАЛЬ 79, 84 СПАРЖА 15, 18, 224 ЯБЛОНЯ 78, 95, 107, 125 В этом примере числа являются номерами страниц справочника. Так, ссылки на спаржу есть на страницах 15, 18 и 224. Для ин- декса СПАРЖА служит примером значения, принимаемого пер- вичным ключом. 210
В дальнейшем, говоря об индексе, будут использоваться два термина — владелец и член. Каждая запись индекса состоит из единственного владельца, которым является значение первичного ключа, и нескольких членов, которыми являются элементы, свя- занные с владельцем. В рассмотренном выше примере для первой записи владельцем является ВИНОГРАД, а членами — 155 и 165; владелец второй записи индекса — МИНДАЛЬ, члены — 79 и 84. В общем любые два владельца могут иметь разное число членов. В пределах каждой записи индекса члены могут быть отсор- тированы. В приведенном примере они расположены в порядке возрастания номеров страниц, так же как и в обычном указателе книги. Когда члены отсортированы, их порядок определяется зна- чениями связанного с ними ключа, используемого для сортировки. В дальнейшем термин файл данных будет использоваться для обозначения того файла, на который индекс ссылается. Другими словами, файл данных — это файл, для доступа к записям кото- рого создаются индексы. 7.2. КАТЕГОРИИ ИНДЕКСОВ 7.2.1. ПЕРВИЧНЫЕ И ВТОРИЧНЫЕ ИНДЕКСЫ Первичный индекс позволяет определить, где найти значение первичного ключа. Индекс из разд. 6.6.1 является харак- терным примербм первичного индекса. Так как значение первич- ного ключа должно быть уникальным, то не более чем один член может быть связан с каждым владельцем в первичном индексе. Поле или набор полей, каждое из которых не является первич- ным ключом, можно использовать как вторичный ключ. Вторич- ный индекс позволяет определить положение всех значений вто- ричного ключа. Значение вторичного ключа может появляться более чем в одной записи файла данных; в связи с этим во вторич- ном индексе несколько членов могут быть связаны с одним вла- дельцем. В качестве простого примера рассмотрим файл ГОРОДА-СТРА- НЫ, содержащий данные, позволяющие установить, в какой стране находится тот или иной город. В этом примере ГОРОД — первич- ный ключ, СТРАНА — вторичный. ГОРОД СТРАНА БЕРН ШВЕЙЦАРИЯ БОРДО ФРАНЦИЯ ЖЕНЕВА ШВЕЙЦАРИЯ ЛИМОЖ ФРАНЦИЯ ЛИССАБОН ПОРТУГАЛИЯ ЛЮБЛЯНА ЮГОСЛАВИЯ ПОРТО ПОРТУГАЛИЯ РУАН ФРАНЦИЯ САРАЕВО ЮГОСЛАВИЯ СТРАСБУРГ ФРАНЦИЯ ТИТОГРАД ЮГОСЛАВИЯ ЦЮРИХ ШВЕЙЦАРИЯ 211
Индекс Файл данных Город Страна Страна ФРАНЦИЯ ПОРТУГАЛИЯ ШВЕЙЦАРИЯ ЮГОСЛАВИЯ Указатели БЕРН БОРДО ЖЕНЕВА ЛИМОЖ ЛИССАБОН ЛЮБЛЯНА ПОРТО РУАН САРАЕВО СТРАСБУРГ ТИТОГРАД ЦЮРИХ ШВЕЙЦАРИЯ ФРАНЦИЯ ШВЕЙЦАРИЯ ФРАНЦИЯ ПОРТУГАЛИЯ ЮГОСЛАВИЯ ПОРТУГАЛИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ШВЕЙЦАРИЯ Рис. 7.1. Файл данных с прямым вторичным индексом В принципе можно найти значение вторичного ключа с помощью линейного поиска в файле данных. Например, для реализации операции sei СТРАНА - 'ФРАНЦИЯ' (ГОРОДА.СТРАНЫ) можно просмотреть весь файл ГОРОДА-СТРАНЫ, ища СТРА- НА = 'ФРАНЦИЯ'- Для того чтобы не прибегать к просмотру всего файла, достаточно создать вторичный индекс по полю СТРА- НА (рис. 7.1). Для вторичного индекса, который на рис. 7.1 расположен слева, СТРАНА — владелец, а указатели — члены. Поле СТРАНА—вторичный ключ файла данных, но для вторич- ного индекса он первичный. Для того чтобы определить все города во Франции, необходимо найти значение ФРАНЦИЯ во вторичном индексе и по соответствующим ему указателям — города в файле данных. В этом случае нет необходимости органи- зовывать доступ к записям, в которые входят города, не находя- щиеся во Франции. Таким образом, использование индекса повышает скорость выполнения операции выбора за счет устранения доступа к не относящимся к запросу записям. Файл данных может иметь несколько вторичных индексов. Например, файл CUSTOMER, первичным ключом которого яв- ляется CUSTOMERNO, может иметь один вторичный индекс, связанный с полем NAME, и другой вторичный индекс, связанный с полем SALESAREA. Кроме того, имеет право на существование и третий вторичный индекс по SALESPERSON EMPLOYEENUM- BER; этот индекс можно использовать для определения всех клиентов, с которыми работал данный служащий. Если известна относительная частота выполнения различных операций выбора, можно решить, какие поля должны иметь вто- 212
ричные индексы. Нет никаких оснований создавать вторичный индекс для поля, непосредственно не связанного и конкретной операцией выбора. 7.2.2. ПЛОТНЫЕ И РАЗРЕЖЕННЫЕ ИНДЕКСЫ Плотный индекс позволяет точно определить, где найти каждый член. Индекс, связанный с полем СТРАНА (см. разд. 7.2.1), является плотным. Разреженный индекс дает возможность установить блок, кото- рый содержит данный член, но не позволяет точно определить, в каком месте блока находится этот член. Индекс индексно-после- довательного файла из разд. 6.6.1 — характерный пример раз- реженного индекса. 7.2.3. ПРЯМЫЕ И КОСВЕННЫЕ ИНДЕКСЫ Индекс называется прямым, если его члены являются указателями или адресами записей, содержащих значения вла- дельцев. Так, индекс по полю СТРАНА из разд. 7.2.1 является прямым, поскольку его члены представляют собой указатели к записям, содержащим названия тех городов, которые находятся в данной стране. Индекс называется косвенным, если его члены являются зна- чениями первичного ключа для записей файла данных, содержащих значение владельца. Ниже приведен пример косвенного индекса по полю СТРАНА для файла данных. ИНДЕКС СТРАНА ФРАНЦИЯ ПОРТУГАЛИЯ ШВЕЙЦАРИЯ ЮГОСЛАВИЯ ФАЙЛ ДАННЫХ ГОРОД БЕРН БОРДО ЖЕНЕВА ЛИМОЖ ЛИССАБОН ЛЮБЛЯНА ПОРТО РУАН САРАЕВО СТРАСБУРГ ТИТОГРАД ЦЮРИХ ГОРОДА БОРДО ЛИМОЖ РУАН СТРАСБУРГ ЛИССАБОН ПОРТО БЕРН ЖЕНЕВА ЦЮРИХ ЛЮБЛЯНА САРАЕВО ТИТОГРАД СТРАНА ШВЕЙЦАРИЯ ФРАНЦИЯ ШВЕЙЦАРИЯ ФРАНЦИЯ ПОРТУГАЛИЯ ЮГОСЛАВИЯ ПОРТУГАЛИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ШВЕЙЦАРИЯ 213
Преимущество косвенного индекса заключается в том, что его члены не подвергаются изменениям, когда записи файла данных меняют адреса. Например, если запись со значением первичного ключа ЖЕНЕВА удаляется из файла ГОРОДА-СТРАНЫ и файл сжимается, заполняя пустое пространство, члены косвенного индекса не меняются. Если же индекс прямой, то указатели к записям файла данных со значениями первичного ключа ЛИ- МОЖ, ..., ЦЮРИХ должны измениться для того, чтобы ссылаться на новые физические адреса. Недостатки использования косвенных индексов очевидны: для определения физического адреса записи в файле данных необхо- димо дополнительно организовать доступ к этому файлу, исполь- зуя значения первичного ключа, полученные из индекса, т. е. необходимо, основываясь на значении первичного ключа, идти к физическому адресу записи, содержащей значение этого ключа, а на это требуется какое-то время. В случае, когда файл данных является файлом прямого доступа (например, файлом с хэш- адресацией), косвенная индексация оправдана. С другой стороны, косвенная индексация часто приводит к большим затратам времени, поскольку поиск первичного ключа в файле данных не всегда осуществляется эффективно. 7.3. РЕАЛИЗАЦИЯ ДОСТУПА К ЧЛЕНАМ 7.3.1. ХРАНЕНИЕ ЧЛЕНОВ ИНДЕКСА ВНЕ ФАЙЛОВ ДАННЫХ В индексе, изображенном на рис. 7.1, каждая запись имеет различное число членов, которыми являются указатели. Связанный с каждым владельцем список членов можно хранить в одномерном массиве, состоящем из четырех элементов, рассчиты- вая на то, что все записи индекса будут содержать не более четырех членов. Вообще предусматривать во всех массивах достаточное для размещения максимального числа членов количество элементов невыгодно, поскольку массивы будут часто оставаться незапол- ненными. Отметим, что этот недостаток присущ любой системе хранения, использующей массивы. Чтобы не тратить впустую память для размещения членов каждой записи индекса, можно использовать связанные списки (рис. 7.2). В состав каждого элемента списка обязательно входит указа- тель, ссылающийся к следующему элементу. Этот указатель рань- ше, естественно, был не нужен; его введение — своеобразная плата за то, что теперь не отводится лишняя память под элементы массива (см. рис. 7.1). Подробнее о том, как используются списки, рассказывается в разд. 4.8.2. Все записи с определенным значением вторичного ключа можно найти в файле данных, отыскав сначала его значение во вторичном 214
Индекс Файл данных Город Страна ФРАНЦИЯ ПОРТУГАЛИЯ ШВЕЙЦАРИЯ ЮГОСЛАВИЯ Список ВЕРН БОРДО ЖЕНЕВА ЛИМОЖ ЛИССАБОН ЛЮБЛЯНА ПОРТО РУАН САРАЕВО СТРАСБУРГ ТИТОГРАД ЦЮРИХ Страна ШВЕЙЦАРИЯ ФРАНЦИЯ ШВЕЙЦАРИЯ ФРАНЦИЯ ПОРТУГАЛИЯ ЮГОСЛАВИЯ ПОРТУГАЛИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ФРАНЦИЯ ЮГОСЛАВИЯ ШВЕЙЦАРИЯ Рис. 7.2. Файл данных с вторичным индексом, реализованным сков с помощью спи- индексе и просмотрев затем список членов, относящихся к этому значению. Далее можно использовать каждый из найденных членов для доступа к записи файла данных, содержащей данное значение вторичного ключа. (Эта последовательность действий описана в процедуре FINDINVOICESOF из разд. 4.8.2). Если для владельцев используются все или почти все их члены, то в этом случае применение связанных списков оправдано, т. е. последо- вательный просмотр связанного списка эффективен тогда, когда требуются почти все элементы этого списка. Если список невелик, то вопрос эффективности не стоит столь остро. Если в результате модификации данных изменяется значение вторичного ключа, нужно удалить член из относящейся к этому значению ключа записи вторичного индекса и связать этот член с другой записью (т. е. с другим владельцем). Кроме того, удаление записи из файла данных повлечет за собой уничтожение членов вторичных индексов, ссылающихся на эту запись. Если члены хранятся в связанных списках, необходимо просмотреть связан- ный список, стремясь найти в нем точно один член, который следует удалить. В этом случае коэффициент активности списка низок и жела- телен прямой доступ к единственному члену, который связан с данным владельцем. Для того чтобы облегчить поиск, можно хра- нить члены в структурах типа В-деревьев, а не в связанных спис- ках. Отметим, что для хранения данных в В-деревьях требуется больше памяти, чем для хранения данных в связанных списках. Но, с другой стороны, с точки зрения программирования на Паскале удобнее использовать В-деревья, поскольку в распоря- жении программиста будут уже написанные процедуры FIND KEY и REMOVE. Помещение новой записи в файл данных приводит к включению таких новых членов во вторичные индексы, которые соответствуют 215
значениям вторичных ключей новой записи. По причинам, кото- рые будут изложены в разд. 7.4, обычно желательно располагать члены списков в том порядке, который соответствует порядку расположения записей в файле данных. В примере с индексирован- ной таблицей счетов из разд. 4.8.2 и в процедуре ENTERJX из разд. 5.4. новые члены помещаются в начало списка только из-за того, что это упрощает программирование. Обычно жела- тельно помещать новый член списка в позицию, которая не будет нарушать уже установленный порядок расположения элементов списка. В том случае, когда члены, относящиеся к одному владельцу, хранятся в В-деревьях, легко поместить новый член в нужное место с помощью процедуры INSERT. Причем действия этой про- цедуры не связаны с просмотром связанного списка членов. При удалении записи использование В-деревьев не повысит эффектив- ности доступа в том случае, если каждый владелец имеет только небольшое число членов. Но, все же, и тогда наличие готовых для использования процедур INSERT и REMOVE облегчит програм- мирование. Для того чтобы проиллюстрировать использование В-деревьев для хранения членов вторичных индексов, рассмотрим пример, связанный с таблицей товарных запасов (см. разд. 5.4). Файлом данных в этом случае будет файл товарных запасов. Он хранится в таблице, к строкам которой возможен прямой доступ по значе- ниям ITEMREFNO. Создадим вторичный индекс по полю SUP- PLIERNO так, чтобы для доступа к владельцам могла быть исполь- зована прямая адресация. Вторичный индекс будет размещаться в таблице SUPPINDEX, а члены вторичного индекса будут использоваться для прямого доступа к записям таблицы товарных запасов. Тогда DBD будет иметь вид DBDNAME MX; type REFRANGE =100.. 140; SUPPRANGE = 16.99; INVTYPE = record QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: SUPPRANGE; NORECORD: BOOLEAN end; M EM BERTREE = BTREE of REFRANGE; var INVTABLE: array [REFRANGE] of INVTYPE; SUPPINDEX: array [SUPPRANGE] of MEMBERTREE; finish 216
Начальные значения В-деревьям присваиваются автомати- чески. Однако, как и в разд. 5.4, необходимо инициализировать таблицу INVTABLE. (Напомним, что в разд. 5.4 для этой цели используется программа INITJX). Программа инициализации имеет следующий вид: program INITMX (OUTPUT); invoke MX; var I: REFRANGE; begin STARTTRANSACTION (UNRESTRICTED); for I:= 100 to 140 do INVTABLEfl].NORECORD: = TRUE; ENDTRANSACTION; WRITELN ('initialization completed’) end. Проводимая ниже программа вводит одну запись в INVTABLE и соответствующим образом модифицирует индекс, используя для этого процедуру INSERT. Отметим, что, помещая новый член в индекс, процедура INSERT оставляет все члены упорядоченными по значению ITEMREFNO. program ENTERMX (INPUT, OUTPUT); invoke MX; var ITEMREFNO: REFRANGE; SUPPNO: SUPPRANGE; begin STARTTRANSACTION (UNRESTRICTED); WRITE (’please type itemrefno, quantityinstock, ’); WRITELN (’PRICEPERUNIT, SUPPLIERNO’); READ (ITEMREFNO); with INVTABLE [ITEMREFNO] do begin READLN (QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); SUPPNO: = SUPPLIERNO; NORECORD: = FALSE end; INSERT (SUPPINDEX [SUPPNO], ITEMREFNO); if DBSTATUS< >OKAY then begin WRITELN (’index insertion failed’); ABORTTRANSACTION end else ENDTRANSACTION end. С помощью следующей программы считывается номер постав- щика и на терминальное устройство выдаются все записи из таблицы товарных запасов, содержащие товары, которые постав- щик поставляет с этим номером. Если обозначить номер поставщика 217
через S, то результат работы программы можно выразить о по- мощью следующей формулы: sei SUPPLIERNO - S (INVTABLE). program SUPPQUERY (INPUT, OUTPUT); invoke MX; var SUPPNO: SUPPRANGE; IRNO: REFRANGE; MEMPTR: JREFRANGE; begin WRITELN (’please type a supplier number’); READLN (SUPPNO); STARTTRANSACTION (READONLY); FINDFIRST (SUPPINDEX [SUPPNO], MEMPTR); if DBSTATUS = EMPTY then WRITELN (’no items are supplied by this supplier ’); while DBSTATUS = OKAY do begin IRNO: = MEMPTRf; {IRNO is the next ITEM REF NO to be processed} WRITELN ( ITEMREFNO = ’, IRNO); with INVTABLE [IRNO] do WRITELN (’QUANTITY IN STOCK = QUANTITYINSTOCK, ’ PRICE PER UNIT= ’, PRICEPERUNIT, ’ SUPPLIER NUMBER = ’, SUPPLIERNO); FINDSUCC (SUPPINDEX [SUPPNO], MEMPTR) end; ENDTRANSACTION end. Ниже приведена программа, осуществляющая считывание номера товара ITEMREFNO с терминального устройства и уничто- жение строки таблицы товарных запасов, соответствующей этому номеру. Кроме того, из вторичного индекса удаляется член, соот- ветствующий введенному значению ITEMREFNO. program DELMX (INPUT, OUTPUT); invoke MX; var SUPPNO: SUPPRANGE; ITEMREFNO: REFRANGE; MEMPTR: JREFRANGE; begin STARTTRANSACTION (UNRESTRICTED); WRITELN (’please type ITEMREFNO of record to be deleted); 1 ($ IRNO содержит значение очередного шифра товара *) 218
READLN (ITEMREFNO), SUPPNO «INVTABLE [1TEMREFNO].SUPPLIERNO; INVTABLE [JTEMREFNOJNORECORD. = TRUE, {deletes record from table} FINDKEY (SUPPINDEX (SUPPNO], ITEMREFNO, MEMPTR); if DBSTATUS < > OKAY then begin WRITELN (’deletion failed ); ABORTTRANSACTION end else {remove appropriate member from index} begin {member to be removed is the one pointed to by MEMPTR} REMOVE (SUPPINDEX (SUPPNO], MEMPTR); ENDTRANSACTION end end. Эта программа в определенном смысле проще, чем выполняю- щая те же функции программа DELJX (см. разд. 5.4). Напомним, что программа DELJX для того, чтобы обнаружить подлежащий удалению элемент, должна просмотреть связанный список членов. 7.3.2. ИСПОЛЬЗОВАНИЕ УКАЗАТЕЛЕЙ В ЗАПИСЯХ ФАЙЛА ДАННЫХ Указатель к владельцу. В разд. 7.3.1. была рассмотрена такая организация индексов, которая позволяет сделать доступ к их элементам эффективным и легко осуществимым. Теперь остановимся на методах, позволяющих экономить память, отводимую для файла данных и его индексов. Для начала заметим, что в простом файле ГОРОДА-СТРАНЫ из разд. 7.2.1 названия стран многократно повторяются. Например, ШВЕЙЦА- РИЯ три раза появляется в файле данных и один раз в индексе. Уменьшить память, необходимую для хранения файла данных, можно, заменив значение ШВЕЙЦАРИЯ поля СТРАНА указате- лем, ссылающимся на владельца ШВЕЙЦАРИЯ вторичного индекса. Ту же операцию можно проделать и с значениями ФРАН- ЦИЯ, ПОРТУГАЛИЯ и ЮГОСЛАВИЯ. В результате будет получена структура данных, показанная на рис. 7.3. Для того чтобы сделать рисунок более наглядным, члены записей индекса изображены слева, а владельцы — справа. Теперь в каждой записи файла данных вместо названия страны, представляемой в виде символьной строки, хранится указатель, ссылающийся к одному из владельцев индекса. Если число байтов, требующихся для хранения этого указателя, меньше числа байтов, необходимых 1 (* удаление члена из индекса *) 2 (* удаление члена, на который есть ссылка с помощью указателя MEMPTR *) 219
Файл данных Индекс Город Указатель Владелец Берн Бордо Женева Лимож Лиссабон Любляна Порто Руан Сараево Страсбург Титоград Цюрих Члены Франция Португалия | Швейцария Югославия Рис. 7.3. Файл данных, в котором поле СТРАНА заменено указателем к вла- дельцу для размещения наибольшего по длине названия страны, то замена названий указателями позволит сэкономить память. Для очень больших файлов экономия будет значительной; можно добиться еще большей экономии памяти, заменив указателями значения нескольких полей файла данных. Предположим, что необходимо определить, в какой стране находится город ЛЮБЛЯНА. Для этого найдем в файле данных название ЛЮБЛЯНА, воспользовавшись, например, алгоритмом линейного поиска. Затем, с помощью указателя, идущего от ЛЮБЛЯНА к записи индексного файла, владельцем которой является название страны ЮГОСЛАВИЯ, находим требуемую информацию. Процесс поиска в этом случае занимает больше вре- мени, чем в случае хранения строки ЮГОСЛАВИЯ непосредст- венно в самой записи ЛЮБЛЯНА. Таким образом, экономя память, проигрываем во времени доступа. Наконец, наступил подходящий момент для ввода новых поня- тий. Файл данных, который содержит только указатели к записям индексов, называется полностью инвертированным. Файл, который индексирован по вторичному ключу, называется инвертированным по вторичному ключу. Файл, который инвертирован по крайней мере по одному вторичному ключу, называется частично инверти- рованным. Простые кольцевые структуры. В примере о городах и странах с каждым владельцем-страной было связано несколько членов- указателей. Так, например, с Францией связаны четыре члена- указателя. Далее будет описан способ, который позволяет умень- 220
Файл данных Индекс Рис. 7.4. Кольцевая реализация индекса по полю СТРАНА шить память, требующуюся для размещения индекса; правда, время поиска страны, в которой находится данный город, увели- чится. Предположим, что в индексе нельзя использовать более одного члена-указателя для каждого владельца. Например, для записи индекса с владельцем ФРАНЦИЯ допустим только один член-указатель. Этот указатель ссылается к одному французскому городу. В записи файла данных, относящейся к этому французскому городу, имеется указатель, ссылающийся на следующий француз- ский город. В записи файла данных для этого следующего фран- цузского города, в свою очередь, предусмотрен указатель на еще один французский город и т. д. В записи файла данных для последнего французского города есть указатель, ссылающийся на запись во вторичном индексе, такой же, как и в начале этого раздела. Итак, если начать с любой записи индекса, то, следуя строго по указателям, обязательно возвращаемся к той же самой записи. 221
Цепочка указателей, связывающая владельца произвольно взятой записи индекса о самим собой, называется кольцом. У кольца всегда существует только один владелец и несколько членов. На рио. 7.4 изображена кольцевая структура. Для того чтобы найти страну, в которой находится город ЛЮБЛЯНА, необходимо использовать ЛЮБЛЯНА как первичный ключ при обращении к файлу данных. От записи ЛЮБЛЯНА, следуя по указателям через САРАЕВО и ЗАГРЕБ, попадаем на запись индекса с вла- дельцем ЮГОСЛАВИЯ. Если требуется найти все французские города, необходимо выполнить следующую последовательность действий. Сначала надо найти запись ФРАНЦИЯ в индексе. Затем, используя цепочку указателей, двигаться от одного французского города к другому, выдавая на терминальное устройство их названия. Последним элементом этой цепочки будет указатель, ссылающийся на запись индекса с владельцем ФРАНЦИЯ. С одной стороны, использование кольцевой структуры позво- ляет сэкономить память, требующуюся для хранения индекса, за счет уменьшения числа указателей, размещаемых в записях индекса. С другой стороны, поиск страны, в которой находится данный город, требует больше времени, чем такой же поиск, но для структуры, изображенной на рис. 7.3, т. е. опять удалось добиться экономии памяти за счет увеличения времени доступа. Кольца, состоящие из ячеек. Если кольцо состоит из несколь- ких тысяч членов, то время поиска владельца может сущест- венно возрасти. Если, к тому же, члены размещены на разных цилиндрах пакета дисков, время поиска может стать очень боль- шим. Для уменьшения времени доступа к владельцам записей можно разделить каждое кольцо на ряд колец меньшей длины, относящихся к одному и тому же владельцу. Если разместить такое небольшое кольцо на одном цилиндре, то его можно про- смотреть, не прибегая к перемещению головок записи-считывания. Ниже приведено перечисление полей записи вторичного индекса при условии, что с каждым владельцем связаны три независимых кольца'. ЗНАЧЕНИЕ ВТОРИЧНОГО КЛЮЧА УКАЗАТЕЛЬ, ССЫЛАЮЩИЙСЯ НА ПЕРВЫЙ ЧЛЕН ПЕРВОГО КОЛЬЦА УКАЗАТЕЛЬ, ССЫЛАЮЩИЙСЯ НА ПЕРВЫЙ ЧЛЕН ВТО- РОГО КОЛЬЦА УКАЗАТЕЛЬ, ССЫЛАЮЩИЙСЯ НА ПЕРВЫЙ ЧЛЕН ТРЕТЬЕГО КОЛЬЦА Например, если к записи файла данных, относящейся ко вто- рому кольцу, организуется доступ по первичному ключу, то, сле- дуя по цепочке указателей к владельцу этого кольца, не нужно будет просматривать записи третьего кольца. Если необходимо использовать вторичный индекс для опреде- ления всех членов, владельцем которых является заданное значе- 222
Рис. 7.5. Обычное и двунаправленное кольца ние вторичного ключа, то нужно сначала найти значение этого ключа во вторичном индексе, а затем просмотреть все три кольца по очереди. Время, затрачиваемое на это, будет не намного больше времени просмотра одного большого кольца. Единственный недостаток, связанный с делением кольца на части, заключается в том, что в каждую запись индекса необходимо дополнительно включать несколько указателей. Кольцо, разделенное на кольца меньшего размера, называется кольцом, состоящим из ячеек. Двунаправленные кольца. До сих пор рассматривались кольца, каждый элемент которых указывал только на своего последова- теля (рис. 7.5, а). В двунаправленных кольцах каждый элемент указывает не только на последователя, но и на предшественника (см. рис. 7.5, б). Использование двунаправ- ленных колец дает возможность при уничтожении записи файла данных воспользоваться указа- телем, ссылающимся на ее пред- шественника. Удаление сводится к замене указателя записи, предшеству- ющей удаляемой; вновь создан- ный указатель должен ссылать- ся на запись, следующую за удаляемой. Если кольцо не является двунаправленным, необходимо пройти по всему кольцу для того, чтобы найти предшественника удаляемой Рис. 7.6. Кольцо со структурой ко- ралла 223
записи. Другими словами, преимущество двунаправленных колец заключается в том, что просматривать их можно в любом из двух возможных направлений: как в сторону повышения, так и в сто- рону уменьшения значений ключа. Платой за это является увели- чение числа дополнительных полей для указателей до двух. Напом- ним, что для обычного кольца достаточно одного указателя на каждую запись. Кольца со структурой коралла. На рис. 7.6 изображено кольцо, структура которого напоминает структуру коралла. Так же, как и в двунаправленном кольце, в кольце со структурой коралла с каждым членом связаны два указателя. Но отличие состоит в том, что достичь владельца в этом кольце можно не более чем за два шага, использовав, если необходимо, указатель соседнего члена. Таким образом, преимущество такой структуры заключа- ется в быстроте перехода от члена к владельцу, а ее недостаток связан с усложнением программной реализации. Мультисписки. В этом разделе будем использовать файл PEOPLE, в котором хранится информация о росте (поле HEIGHT) и цвете волос (поле HAIRCOLOUR) различных людей. FIRSTNAME SURNAME HEIGHT HAIRCOLOUR (имя) (фамилия) (рост) (цвет волос) АЛАН АРНОЛЬД 164 ЖЕЛТЫЙ ДЖОН АРНОЛЬД 162 ЧЕРНЫЙ МЭРИ БЕК 159 ЧЕРНЫЙ ДЖЕЙН КОЛЛИНЗ 166 КОРИЧНЕВЫЙ ТОМАС КОЛЛИНЗ 180 КОРИЧНЕВЫЙ ДЖЕЙН ДЭВИС 164 РЫЖИЙ ВИЛЬЯМ ДЭВИС 173 ЧЕРНЫЙ АЛАН ФИШЕР 159 ЖЕЛТЫЙ МЭРИ ФИШЕР 158 КОРИЧНЕВЫЙ ЭН ГРИН 16о ЧЕРНЫЙ ДЖОН ГРИН 172 КОРИЧНЕВЫЙ ДЖОН НАЙТ 176 БЕЛЫЙ ТОМАС НАЙТ 181 ЧЕРНЫЙ ДЖЕЙН ЛИНЛИ 158 РЫЖИЙ АНРИ МАРШАЛ 182 КОРИЧНЕВЫЙ МЭРИ МАРШАЛ 159 КОРИЧНЕВЫЙ ДЖЕЙН РЕЙНОЛЬДС 163 ЖЕЛТЫЙ ВИЛЬЯМ РЕЙНОЛЬДС 169 ЧЕРНЫЙ ЭН ТОМАС 158 КОРИЧНЕВЫЙ ТОМАС ТОМАС 181 КОРИЧНЕВЫЙ ДЖОН ВИЛЬЯМС 177 КОРИЧНЕВЫЙ ВИЛЬЯМ ВИЛЬЯМС 173 РЫЖИЙ эн янг 169 КОРИЧНЕВЫЙ АЛАН янг 171 ЧЕРНЫЙ Первичный ключ образует два поля FIRSTNANE (имя) и SURNAME (фамилия). Файл с ключом FIRSTNAME является вторичным индексом, поскольку FIRSTNAME только часть пер- вичного ключа. На рис. 7.7 показана одна из возможных реали- заций индекса. Членом каждой записи при такой реализации 224
Индекс АЛАН ЭН ДЖЕЙН — • ДЖОН МЭРИ ТОМАС ШМ/^1 АЛАН ДЖОН МЭРИ ДЖЕЙН ТОМАС ДЖЕЙН ВИЛЬЯМ АЛАН (у МЭРИ Z/ ЭН /< ДЖОН гЬ ДЖОН Кх ТОМАС ДЖЕЙН АЛАН ПГ МЭРИ ДЖЕЙН \s ВИЛЬЯМ ТОМАС ДЖОН . V ВИЛЬЯМ ЭН V АЛАН Файл '\:нных АРНОЛЬД 164 СОЛОМЕННЫЙ АРНОЛЬД 162 ЧЕРНЫЙ БЕК 159 ЧЕРНЫЙ КОЛЛИНЗ 166 КАШТАНОВЫЙ КОЛЛИНЗ 180 КАШТАНОВЫЙ ДЭВИС 164 РЫЖИЙ ДЭВИС 173 ЧЕРНЫЙ ФИШЕР 159 СОЛОМЕННЫЙ ФИШЕР 158 КАШТАНОВЫЙ ГРИН 165 ЧЕРНЫЙ ГРИН 172 КАШТАНОВЫЙ НАЙТ 176 СЕДОЙ НАЙТ 181 ЧЕРНЫЙ ЛИНЛИ 158 РЫЖИЙ МАРШАЛ 182 КАШТАНОВЫЙ МАРШАЛ 159 КАШТАНОВЫЙ РЕЙНОЛЬДС 163 СОЛОМЕННЫЙ РЕЙНОЛЬДС 169 ЧЕРНЫЙ ТОМАС 158 КАШТАНОВЫЙ ТОМАС 181 КАШТАНОВЫЙ ВИЛЬЯМС 177 КАШТАНОВЫЙ ВИЛЬЯМС 173 РЫЖИЙ ЯНГ 169 КАШТАНОВЫЙ ЯНГ 171 ЧЕРНЫЙ Рис. 7.7. Реализация с помощью списков вторичного индекса по полю FIRST- NAME является указатель. Так как FIRSTNAME — часть первичного ключа, в состав записи файла данных включено значение поля FIRSTNAME, представляющее собой строку символов, и указа- тель к следующей записи с тем же значением FIRSTNAME. Каждый список членов заканчивается указателем "nil”. Указа- тели к владельцам в данном случае не нужны, поскольку значе- ние FIRSTNAME непосредственно включено в каждую запись файла данных. Сформируем еще один индекс по HAIRCOLOUR. Тогда струк- тура данных примет вид, представленный на рис. 7.8. Так как поле HAIRCOLOUR не является составляющей первичного ключа, включать его значения в записи файла данных нет необходимости. Вместо поля HAIRCOLOUR в записях файла данных появилось поле-указатель, что, по-видимому, позволит сэкономить память, требующуюся для размещения файла данных. Теперь для каж- дого значения HAIRCOLOUR имеется кольцо, последний член которого ссылается на запись-владельца в индексе по HAIRCO- LOUR. Эта ссылка необходима, поскольку только в этом индексе хранятся значения поля HAIRCOLOUR. Для того чтобы по за- данному первичному ключу (т. е. по имени и фамилии) определить 8 Ульман Дж. 225
Индекс по Индекс по FIRSTNAME ЭН АЛАН ДЖЕЙН ДЖОН МЭРИ ТОМАС ВИЛЬЯМ Файл данных АЛАН АРНОЛЬД 164 ДЖОН АРНОЛЬД 162 МЭРИ БЕК 159 ДЖЕЙН КОЛЛИНЗ 166 ТОМАС КОЛЛИНЗ 180 ДЖЕЙН ДЭВИС 164 ВИЛЬЯМ ДЭВИС 175 АЛАН Фишер 159 МЭРИ ФИШЕР 158 ЭН ГРИН 165 ДЖОН ГРИН 172 ДЖОН НАЙТ 176 ТОМАС НАЙТ 181 ДЖЕЙН ЛИНЛИ 158 АЛАН МАРШАЛ 182 МЭРИ МАРШАЛ 159 ДЖЕЙН РЕЙНОЛЬДС 165 ВИЛЬЯМ РЕЙНОЛЬДС 169 ЭН ТОМАС 158 ТОМАС ТОМАС 181 ДЖОН ВИЛЬЯМС 177 ВИЛЬЯМ ВИЛЬЯМС 175 эн ЯНГ 169 АЛАН ЯНГ 171 HAIRCOLOUR ЧЕРНЫЙ КАШТАНОВЫЙ РЫЖИЙ СЕДОЙ СОЛОМЕННЫЙ Рис. 7.8. Реализация индексов по полям FIRSTNAME и HAIRCOLOUR с по- мощью мультисписковых структур значение HAIRCOLOUR (т. е. цвет волос), необходимо, восполь- зовавшись кольцевой структурой, получить символьную строку, являющуюся значением поля HAIRCOLOUR вторичного индекса. Мультисписком называется такая структура данных, в состав которой входят множество записей, множество списков этих записей, и каждая запись обязательно является элементом двух или более списков. Обычно в разные списки входят различные множества записей. В таком контексте кольцо трактуется как список, поэтому структура данных PEOPLE (см. рис. 7.8) счи- тается примером мультисписка. В рассматриваемом примере каждая запись файла данных имеет два поля-указателя и относится к двум спискам. 226
Нетрудно придумать такие структуры данных, в которых каждая запись входит в состав большего числа списков. 7.4. РЕАЛИЗАЦИЯ ДОСТУПА К ВЛАДЕЛЬЦАМ ЗАПИСЕЙ В разд. 7.3 рассматривались вопросы, связанные только с размещением членов записей. В разд. 7.3.2 списки членов-ука- зателей были включены в состав записей файла данных, а в разд. 7.3.1 —нет. Перейдем теперь к рассмотрению принципов реализации доступа к владельцам записей. Метод доступа к вла- дельцам обычно не зависит от способа организации членов, по- этому их можно рассматривать независимо. В идеале требуется обеспечить произвольный доступ к вла- дельцам записей в индексе. Если в индексе много записей, можно, применяя в качестве значений первичных ключей владельцев этих записей, организовать прямую адресацию или хэш-адресацию. В разд. 4.8.2 и 7.3.1 индексы являются файлами с прямой адресацией. Если число реально используемых значений вторич- ных ключей значительно меньше диапазона их возможных значе- ний, предпочтительнее использовать хэш-адресацию, а не прямую адресацию. Когда же общее число владельцев невелико, не обяза- тельно использовать хэш-адресацию — можно удовлетвориться хранением владельцев в таблице и доступом к ним с помощью метода линейного поиска. Если число владельцев велико, можно рекомендовать исполь- зовать для их хранения структуру типа В-дерева. Хотя доступ в этом случае будет осуществляться медленнее, чем при хэш- адресации, но зато при использовании В-деревьев не надо в мо- мент создания DBD знать число возможных значений владельцев. С точки зрения программиста, работающего на ЯМД Паскаль, использование В-деревьев выгодно, поскольку позволяет приме- нять процедуры INSERT, FINDKEY и т. д. без объявления их. В файле PEOPLE так мало значений вторичных ключей FIRSTNAME и HAIRCOLOUR, что для их хранения невыгодно использовать В-деревья. Значения вторичных ключей удобнее поместить в таблицу, а выбирать нужный с помощью метода ли- нейного поиска. Несмотря на это, для демонстрации приемов программирования, предназначенных для работы с В-деревьями, при реализации двух индексов будут все-таки использованы В-деревья. В разд. 7.3.1 В-деревья использовались только для хранения членов записей. Теперь же эта структура будет приме- нима и для владельцев, т. е. индекс будет иметь структуру В-де- рева, элементами которого будут, в свою очередь, В-деревья. Члены, как в разд. 7.3.1, будут размещаться вне файла данных, и не будут объединены в мультисписки. Для простоты данные из файла PEOPLE будут храниться в таблице, представляющей собой массив записей. Кроме того, 8* 227
будут сформированы вторичные индексы по FIRSTNAME и HAIRCOLOUR, членами которых будут значения индексов таб- лицы. По заданному первичному ключу, т. е. по значениям FIRSTNAME и SURNAME, легко найти соответствующую строку таблицы. Если же начинать со значения вторичного ключа, опре- деляющего цвет волос (HAIRCOLOUR), то для поиска всех лю- дей, имеющих данный цвет волос, нужно воспользоваться индек- сом по HAIRCOLOUR. Аналогично для определения всех людей, имеющих заданное имя, можно использовать индекс по FIRST- NAME. Ниже приведен текст DBD. DBDNAME НА; type SUBSCRIPT =1..24; STRINGS = packed array [1 .8] of CHAR; PERSON = record FIRSTNAME, SURNAME: STRINGS; HEIGHT: INTEGER; HAIRCOLOUR: STRINGS end; INDEXLEAF = record OWNER: STRING8; MEMBERS: BTREE of SUBSCRIPT end; SECONDARYIND = BTREE of INDEXLEAF on OWNER; var TABLE: array [SUBSCRIPT] of PERSON; FIRSTNAMEIND, HAIRINDEX: SECONDARYIND; finish Далее речь пойдет о программе, предназначенной для считы- вания двоичного файла PEOPLE и заполнения таблицы, объяв- ленной в DBD. Кроме того, с помощью этой программы во вто- ричные индексы, по FIRSTNAME и HAIRCOLOUR вводятся члены. Например, предположим, что впервые в качестве значения поля HAIRCOLOUR встретилось РЫЖИЙ. Программа в этом случае должна сформировать новый элемент типа INDEXLEAF, значение поля OWNER которого равно 'РЫЖИЙ^^^_/. В при- водимой ниже программе для модификации обоих индексов ис- пользована одна и та же процедура UPDATEIND. program НАР1 (PEOPLE, OUTPUT); invoke НА; var I: SUBSCRIPT; PEOPLE: file of PERSON; 22?
procedure UPDATEIND (var INDEX: SECONDARYIND; var SECKEY: STRINGS); var LEAFPTR: jlNDEXLEAF; begin FINDKEY (INDEX, SECKEY, LEAFPTR); if DBSTATUS = OKAY then . begin {this secondary key value has turned up previously so}* * INSERT (LEAFPTR}.MEMBERS, I); {include subscript I among members}* if DBSTATUS < >OKAY then WRITELN (’insert failed’) end _ else {this is a new secondary key value, i.e. a new owner}J begin {to create a new owner entry in the secondary index}* DBSTATUS: = OKAY; CREATE (LEAFPTR); {create a new leaf recordf LEAFPTR f .OWNER: = SECKEY; INSERT (LEAFPTRt-MEMBERS, I); {include subscript I among members}8 if DBSTATUS < >OKAY then WRITELN (’first I insert failed’) else begin {to insert the new leaf into the index}7 INSERT (INDEX, LEAFPTR}); if DBSTATUS < >OKAY then WRITELN (’insert LEAF failed’) end end end {of procedure UPDATEIND};8 begin {program body} $ RESET (PEOPLE); STARTTRANSACTION (UNRESTRICTED); for I: = 1 to 24 do begin READ (PEOPLE, TABLE [I]); UPDATEIND (FIRSTNAMEIND, TABLE [I].FIRSTNAME); UPDATEIND (HAIRINDEX, TABLE [I].HAIRCOLOUR) end; ENDTRANSACTION end. 1 (* 9T0 значение вторичного ключа уже встречалось *) а (* включение индекса I в число членов ♦) 8 (* это новое значение вторичного ключа, т. е. новый владелец ♦ ) 4 (* создание новой записи во вторичном индексе *) 8 (* создание нового листа дерева *) • (* включение индекса I в число членов ♦) ’ (* помещение нового листа во вторичный индекс ♦ ) 8 (♦ конец процедуры UPDATEIND *) 8 (♦ тело программы ♦ ) 229
С помощью следующей программы с терминального устройства считывается цвет волос (HAIRCOLOUR) и определяются имена и фамилии всех людей, имеющих этот цвет волос. program НАР2 (INPUT, OUTPUT); invoke НА; var COLOUR: STRING8; LEAFPTR: JINDEXLEAF; SUBSPTR: ^SUBSCRIPT; S: SUBSCRIPT; begin STARTTRANSACTION (READONLY); WRITELN (’please type haircolour consisting of exactly eight', ’characters including trailing padding spaces if necessary’); READLN (COLOUR); FINDKEY (HAIRINDEX, COLOUR, LEAFPTR); if DBSTATUS < > OKAY then WRITELN (*no-one has hair that colour’) else begin {find first member owned by input HAIRCOLOUR value) FINDFIRST (LEAFPTRf.MEMBERS, SUBSPTR); while DBSTATUS = OKAY do begin S: = SUBSPTRf; with TABLE [SJ do WRITELN (FIRSTNAME, SURNAME); FINDSUCC (LEAFPTRt.MEMBERS, SUBSPTR) {find next member}1 2 end end; ENDTRANSACTION end. Приведенная выше программа позволяет быстро вычислить значение следующего выражения: proj FIRSTNAME, SURNAME (sei HAIRCOLOUR = COLOUR (PEOPLE)) Поскольку вторичного индекса no SURNAME не создано, для определения значения выражения proj FIRSTNAME (sei SURNAME = 'МАРШАЛ' (PEOPLE)) может быть использован только метод линейного поиска: ищутся те строки таблицы, для которых SURNAME = 'МАРШАЛ'. 1 (* поиск первого члена записи, владельцем которой является введенное значение HAIRCOLOUR «) 2 (* поиск следующего члена *) 230
7.5. ОПЕРАЦИИ ВЫБОРА ПО НЕСКОЛЬКИМ КЛЮЧАМ 7.5.1. УСЛОВИЕ И В данном разделе рассматривается реализация опера- ций выбора, включающих условие И. Определить значение выражения proj SURNAME (sei (FIRSTNAME = 'ДЖОН') and (HAIRCOLOUR = 'КОРИЧНЕВЫЙ') (PEOPLE)) можно, например, последовательно просматривая файл данных и извлекая из него записи, для которых FIRSTNAME = 'ДЖОН' и HAIRCOLOUR = 'КОРИЧНЕВЫЙ'. Но в этом случае много времени будет тратиться на анализ записей, не содержащих ни одного из двух значений вторичных ключей. Можно сделать и по-другому: найти во вторичном индексе по FIRSTNAME нуж- ные записи (т. е. записи со значением FIRSTNAME, равным 'ДЖОН') и затем с помощью указателей перейти к файлу данных, извлекая из него записи, для которых HAIRCOLOUR = 'КО- РИЧНЕВЫЙ'. Во втором случае просматриваться будут только записи, для которых FIRSTNAME = 'ДЖОН'. Если члены-ука- затели включены в состав записей файла данных, как в разд. 7.3.2, или если не создано индекса по HAIRCOLOUR, последний ва- риант является наилучшим. Если же, как в разд. 7.3.1, члены-указатели не включены в записи файла данных, а хранятся отдельно, можно, организовав доступ к вторичному индексу по FIRSTNAME, получить список членов, для которых FIRSTNAME = 'ДЖОН'. Аналогично, ис- пользуя вторичный индекс по HAIRCOLOUR, можно получить список членов, для которых HAIRCOLOUR = 'КОРИЧНЕВЫЙ'. Пересечение множеств, образованных из этих двух списков, даст в результате множество указателей, для которых справедливо и HAIRCOLOUR = 'КОРИЧНЕВЫЙ', и FIRSTNAME = 'ДЖОН'. Применяя этот метод, можно без потерь времени на дополни- тельные просмотры определить искомые записи. В том случае, если файл данных содержит много записей, можно, используя последний метод, значительно сократить общее число блоков, к которым необходимо организовывать доступ. Ниже показано, как на Паскале реализовать операцию пере- сечения двух множеств членов. Используемая для этого программа определяет фамилии людей, которые имеют заданные имя и цвет волос. program НАРЗ (INPUT, OUTPUT); invoke НА; var HPTR, FPTR: ^SUBSCRIPT; HLEAFPTR.FLEAFPTR: J INDEXLEAF; FIRSTNAME, COLOUR: STRINGS; 231
H: SUBSCRIPT; SOMEONE: BOOLEAN; begin STARTTRANSACTION (READONLY); WRITE (’type FIRSTNAME and HAIRCOLOUR ’); WRITELN (’separated by a single space’); READ (FIRSTNAME); GET (INPUT); READ (COLOUR); FINDKEY (HAIRINDEX, COLOUR, HLEAFPTR); SOMEONE: = (DBSTATUS - OKAY); DBSTATUS: = OKAY; FINDKEY (FIRSTNAMEIND, FIRSTNAME, FLEAFPTR); if (DBSTATUS = OKAY) and SOMEONE then begin FINDFIRST (HLEAFPTRj MEMBERS, HPTR); FINDFIRST (FLEAFPTRf .MEMBERS, FPTR); while DBSTATUS = OKAY do begin H: = HPTRf; if H = FPTRf then WRITELN (TABLE (H].SURNAME) else if H > FPTR f then FINDSUCC (FLEAFPTRf MEMBERS, FPTR) else FINDSUCC (HLEAFPTRf .MEMBERS, HPTR) end end; ENDTRANSACTION end. Первый вызов процедуры FINDKEY используется для поиска COLOUR во вторичном индексе HAIRINDEX. Если подходящая запись найдена, то формируется указатель HLEAFPTR, ссыла- ющийся на эту запись, если нет, то переменная SOMEONE принимает значение FALSE. В результате первого вызова процедуры FINDFIRST перемен- ная HPTR принимает значение, указывающее на первый член, связанный с владельцем COLOUR *. С помощью второго вызова этой же процедуры указатель FPTR устанавливается на первый член, относящийся к владельцу FIRSTNAME. В цикле 'while' проверяется, равны ли элементы, на которые указывают HPTR и FPTR. Если равны, то та запись таблицы, индексом которой является найденный элемент, является искомой, и значение ее поля SURNAME выдается на терминальное устройство. Если не равен, то с помощью процедуры FINDSUCC осуществляется переход к следующему листу этого дерева, значение текущего листа которого меньше. Это необходимо, поскольку все листья SUBSCRIPT В-деревьев MEMBERS упорядочены, а процедура * Напомним, что с каждым владельцем связана совокупность членов, объ- единенных в В-дерево. Каждый член или лист дерева является значением типа SUBSCRIPT. В данном случае выбирается первый лист дерева. — Прим. пер. 232
FINDSUCG позволяет перейти к следующему по порядку листу дерева. В рассмотренной программе предполагается, что листья В-де- ревьев MEMBERS отсортированы одинаково. Если это не так, то алгоритм, используемый для формирования пересечения двух множеств, элементами которых являются листья деревьев, будет работать не столь эффективно. При выполнении таких операций, как объединение, также необходимо, чтобы множества членов, относящихся ко всем вла- дельцам, были одинаково упорядочены. Предположим, что вместо В-дерева записей типа record OWNER: STRINGS8; MEMBERS: BTREE of SUBSCRIPT end в качестве вторичного индекса используется В-дерево записей другого типа record OWNER: STRINGS8; end MEMBER: SUBSCRIPT Тогда члены, связанные с одним и тем же владельцем, будут размещаться в В-дереве в порядке, который определяется очеред- ностью их поступления, поскольку именно так с помощью про- цедуры INSERT размещаются записи с одинаковыми ключами. Таким образом, не будет никакой уверенности в том, что данные с одинаковыми значениями OWNER расположены в порядке увеличения значений MEMBER. 7.5.2. УСЛОВИЕ ИЛИ Один из путей определения значений таких выраже- ний, как proj SURNAME (sei (FIRSTNAME = 'ДЖОН') or (HAIRCOLOUR = 'КОРИЧНЕВЫЙ') (PEOPLE)) заключается в последовательном просмотре файла PEOPLE с целью обнаружения записей, для которых FIRSTNAME = = 'ДЖОН', или HAIRCOLOUR = 'КОРИЧНЕВЫЙ'. Если, как в разд. 7.3.1., члены-указатели хранятся во вторичных индексных файлах по FIRSTNAME и HAIRCOLOUR, можно сделать доступ более эффективным, формируя объединение членов, связанных с владельцами ДЖОН и КОРИЧНЕВЫЙ в индексах. Объеди- нение представляет собой множество указателей, которые ссы- лаются к искомым записям файла данных. Таким образом, в этом случае время на анализ записей, не являющихся искомыми, тратится не будет. Если члены вторичных индексов отсортиро- ваны одинаково, довольно просто добиться, чтобы в объединении ни один из них не появлялся дважды. 233
7.6. КОСВЕННАЯ АДРЕСАЦИЯ 7.6.1. ПЛОТНЫЕ ПЕРВИЧНЫЕ ИНДЕКСЫ В предыдущих разделах было показано, что для обес- печения эффективного произвольного доступа по любому задан- ному значению первичного ключа необходимо выбрать специаль- ную организацию файла данных. Кроме того, было показано, как, используя плотные вторичные индексы, ускорить реализа- цию операций выбора, включающих значения вторичных ключей. В этом разделе будут рассмотрены условия, благоприятные для работы с плотным индексом по первичному ключу. Начнем с при- мера. В этом примере используются записи данных типа INVTYPE. Напомним, что в разд. 5.3 использовались похожие записи. Только теперь поле ITEMREFNO принадлежит типу 10000 ... 99999, поэтому прямая адресация таблицы товарных запасов нецелесообразна. Записи данных вводятся с терминального устрой- ства в случайном порядке, а доступ к ним возможен в порядке увеличения значения ITEMREFNO, т. е. как и в разд. 5.3, поле ITEMREFNO является первичным ключом. После ввода запись помещается на первое свободное место блока данных, для кото- рого NORECORD = TRUE. В то же время значение первичного ключа этой записи и указатель (или индекс) массива, ссылающийся на эту запись, сохраняются в В-дереве, которое в данном примере используется как плотный первичный индекс. Элементы В-дерева отсортированы по ITEMREFNO, а записи данных хранятся в произвольной последовательности (рис. 7.9). Когда запись уничтожается, во-первых, ее поле NORECORD принимает значе- ние TRUE, во-вторых, значение ее ключа ITEMREFNO удаляется из плотного первичного индекса. Анализируя рис. 7.9, можно сказать, что запись с ITEMREFNO = 46335 была удалена и на ее место будет помещена очередная запись вне зависимости от значения поля ITEMREFNO этой записи. Поскольку записи хранятся в произвольном порядке, их групповая обработка будет невозможна без использования индекса. С помощью индекса групповая обработка осуществляется сравнительно легко: сна- чала организуется доступ к значениям ITEMREFNO в том по- рядке, в котором они расположены, а затем находится соответ- ствующее каждому ITEMREFNO значение указателя, ссылающе- гося на запись данных, так что доступ к записям проводится в порядке увеличения значений ITEMREFNO. Приведем доводы в пользу применения плотных первичных индексов. Во-первых, в блоках данных, предназначенных для хранения файлов с хэш-адресацией или В-деревьев, примерно одна треть памяти расходуется впустую. Сэкономить память можно, размещая в блоках вместо данных записи первичных индексов. Данные же, расположенные в совершенно произволь- ном порядке, можно хранить где-то еще. Такой вариант может 234
БЛОКИ ЛИСТЬЕВ , В-ДЕРЕВА КЛЮЧ УКАЗА- ТЕЛЬ 10007 10009 33981 51932 66650 66651 77005 76923 96743 98044 ITEMREFNO (шифр товара) ЗАПИСИ ДАННЫХ QUANTITYINSTOCK SUPPLIERNO PRICEPERUNIT NORECORD (количество (номер (ценоза (отсутствие на складе) поставщика) единицу) записи) | 7 | » | 30.4 | FALSE | 33981 | 1 * 1 I 10 1 10.6 | FALSE | | 51932 8 10 17. 0 FALSE 46335 0 13 15.5 TRUE 66650 5 15 18.9 FALSE I 10007 9 10 20.8 FALSE 10009 6 12 50.0 FALSE 98044 6 J0 I | 10.6 FALSE 77005 1 13 49.9 FALSE 66651 7 15 70.0 FALSE 44913 0 12 15.8 TRUE 96743 3 10 | 66.6 FALSE Ряс. 7.9. Упрощенный вариант файла товарных запасов с плотным первичным индексом принести весомый эффект только тогда, когда запись первичного индекса занимает значительно меньше памяти, чем запись файла данных. Каждая запись плотного первичного индекса состоит из значения первичного ключа и указателя, определяющего, где в блоке данных находится запись данных. Заметим, что в блоке данных теперь нет свободных зон. Во-вторых, записи в блоках, содержащих листья В-дерева (или просто в блоках данных, как в разд. 6.6.2), необходимо пере- мещать, чтобы сохранить упорядоченность внутри блоков. Если существуют вторичные индексы, связанные с размещенными в этих блоках записями данных, то члены вторичных индексов надо модифицировать при каждом включении или удалении записи данных. Такая модификация индексов требует значительного времени. Избежать ее можно, используя плотные первичные ин- дексы и размещая записи данных отдельно в произвольном по- рядке. Записи, которые адресуются с помощью индексов, называются прикрепленными. Очень важное преимущество плотных первич- 235
ных индексов заключается в том, что с их помощью осуществляется доступ к прикрепленным записям, которые не меняют своего положения в памяти. Прикрепленные записи всегда остаются на своих местах в блоках данных. Для того чтобы в этом случае сохранить порядок сортировки, необходимо перемещать записи индекса внутри блоков, содержащих В-деревья. Такое переме- щение внутри первичного индекса не влечет за собой модифика- цию вторичных индексов. В примере, изображенном на рис. 7.9, можно легко ввести вторичный индекс по номеру поставщика. Члены этого вторич- ного индекса являются указателями того же типа, что и члены первичного индекса. Включение и удаление записей данных не будут приводить к перемещениям данных в памяти и, следовательно, не повлекут за собой изменений вторичных индексов (за исключением записей индексов, связанных с включаемыми или удаляемыми записями данных). Если же не использовать плотные первичные индексы, а хранить записи данных в блоках листьев В-дерева, то вторич- ный индекс по номеру поставщика необходимо будет модифици- ровать для того, чтобы отразить перемещения записей в файле данных. 7.6.2. МНОГОСТУПЕНЧАТАЯ КОСВЕННАЯ АДРЕСАЦИЯ Использование прямой адресации позволяет сразу же найти искомый элемент, в то время как косвенная адресация обес- печивает только переход к адресу искомого элемента. Косвенная адресация может быть многоступенчатой. Например, один адрес может обеспечивать переход ко второму адресу, тот в свою оче- редь — к третьему и т. д., до тех пор пока не будет достигнут искомый элемент. Ранее уже рассматривалось несколько примеров косвенной адресации. Так, в разд. 6.5.1 говорилось, что если файл прямого доступа не занимает непрерывной области в памяти, то для того, чтобы организовать доступ к блоку данных, необходимо получить адрес этого блока из индекса. Переход от ключа к физическому адресу блока, который находится в индексе, представляет собой пример косвенной адресации. Другим примером косвенной адре- сации является использование плотного первичного индекса. В этом случае непосредственного перехода от конкретного зна- чения первичного ключа к данным сначала в индексе ищется адрес данных. Во многих системах управления базами данных адреса, хра- нящиеся в первичных и вторичных индексах, являются косвен- ными (в них находятся адреса реальных данных). Такая много- ступенчатая косвенная адресация дает возможность перемещать данные в памяти, не затрагивая при этом значений первичных и 236
вторичных индексов. Уже были введена плотные первичные ин- дексы, позволяющие избежать перемещений записей в пределах блоков, хранящих листья В-деревьев, или блоков данных ин- дексно-последовательного файла. При многоступенчатой адреса- ции иногда происходят перемещения записей реальных данных. Такие перемещения в некоторых случаях бывают полезными. Во-первых, для сокращения времени доступа желательно, чтобы элементы данных, которые часто запрашиваются вместе, были физически расположены близко друг к другу, предпочти- тельнее в одном и том же блоке или по крайней мере в одном цилиндре. Чтобы достичь этого, элементы данных иногда прихо- дится перемещать. Во-вторых, до сих пор рассматривались преимущественно за- писи фиксированной длины, т. е. такие записи, для хранения которых требуется фиксированное число байтов. Во многих реаль- ных системах используются записи переменной длины. Напри- мер, поле записи, принадлежащей текстовому файлу, может быть символьной строкой произвольной длины (этот пример не отно- сится к языку Паскаль). В том случае, если такая запись уничто- жается, освобождаемая память может оказаться значительно боль- шей, чем требуется для размещения новой входной записи. Если небольшая входная запись размещается в достаточно большой области памяти, оставшаяся свободной часть памяти может ока- заться так мала, что ее нельзя будет использовать. Для решения задач такого рода желательно иметь возмож- ность перемещать записи. Эго позволит либо устранить свободные промежутки между ними, либо сделать эти промежутки доста- точно большими для размещения в них новых записей. Один из способов достижения этой гибкости заключается в том, чтобы сделать указатели, содержащиеся в первичных и вторичных ин- дексах, целыми числами. Иногда эти числа называют числами внутреннего ряда (ISN). Все ISN используются для обращения к специальному индексу, в котором находятся физические адреса, соответствующие значениям ISN. Когда данные перемещаются в памяти, их адреса, находящиеся в этом индексе, меняются, но их ISN остаются неизменными. Поэтому нет никакой необхо- димости модифицировать первичные и вторичные индексы. Существует другой метод, позволяющий обращаться к данным без предварительного доступа к специальному индексу. Указа- тели, содержащиеся в базах данных, можно разделить на две части. Первая часть — это физический адрес блока, содержащего элемент данных; вторая часть — целое, используемое для поиска этого элемента внутри блока. Эго целое используется для пря- мого доступа к данным индекса, находящимся в физическом блоке. Каждому такому целому в индексе поставлен в соответствие номер байта, начиная о которого в блоке размещаются искомые данные. 237
Таким образом, внутри блока можно перемещать данные, ме- няя при этом индекс этого блока и оставляя без изменения указа- тели в базе данных. Начиная с этого момента (и точно так же, как это было в на- чальных разделах), указатель будет считаться просто одним из типов адреса, с помощью которого можно получить искомый элемент данных. Но, все же, надо понимать, что на практике переход от указателя к реальному адресу может осуществляться за несколько шагов. 7.7. УПРАЖНЕНИЯ G Н К 1 A Q 9 В Q 5 В Q 8 В Р 7 С Q 3 С Р 4 А Р а. Изобразите структуру данных, считая, что записи файла хранятся в той последовательности, в которой они изображены, но связаны указателями в порядке возрастания первичного ключа G. В структуру данных также должны входить вторичные индексы по Н и К, реализованные в виде колец. Таким образом, каждая запись данных должна состоять из поля первичного ключа и трех полей-указателей. б. Изобразите структуру данных, считая, что записи файла хранятся в той последовательности, в которой они изображены, а с помощью плотного первичного индекса обеспечивается доступ к записям в порядке возрастания первичного ключа G. В струк- туру данных также должны входить вторичные индексы по Н и К, реализованные в виде указателей, которые не входят в состав файла данных FEX. В структуру должны входить три индекса, представляющие собой совокупность владельцев вместе со свя- занными с ними членами. в. Объясните, как структуры данных из упражнений 1, а и 1, б можно использовать для определения значения выражения proj G (sei (Н = X) and (К = Y) (FEX)), где X и Y вводятся с терминального устройства. г. Предположите, что в файле .FEX находится 8000 записей и что соответственно расширены структуры данных из 1, а и 1,6. Сравните структуры данных и перечислите их преимущества и недостатки. 238
2. Целью этого упражнения является создание плотного ин- декса, описанного в разд. 7.6.1, и вторичного индекса по SUPPLIERNO. При этом должно использоваться следующее DBD: DBDNAME XS; type REFRANGE= 10000 .99999; SUPPRANGE = 10 .15; INVTYPE = record ITEMREFNO: REFRANGE; QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: SUPPRANGE end; INDEXLEAF = record ITEMREFNO: REFRANGE; POINTER: flNVTYPE end; INDEXTREE = BTREE of INDEXLEAF on ITEMREFNO; var PRIMARYINDEX: INDEXTREE; SUPPLIERINDEX: array [SUPPRANGE] of INDEXTREE; finish Для простоты считается, что возможен прямой доступ к SUPPLIERINDEX. Включение в состав INDEXTREE значе- ний ITEMREFNO дает возможность для любого заданного значе- ния SUPPLIERNO организовывать доступ к записям файла то- варных запасов в порядке возрастания значений поля ITEM- REFNO. а. Используя приводимую ниже программу, введите несколько записей с терминального устройства и включите их в плотно- индексированный файл товарных запасов. Для ввода можно использовать и записи из разд. 7.6.1 и любые другие записи: program ENTERINV (INPUT, OUTPUT); invoke XS; var INVRECPTR: flNVTYPE; NEWLEAF: INDEXLEAF; SNO: SUPPRANGE: begin STARTTRANSACTION (UNRESTRICTED); CREATE (INVRECPTR); WRITE (’type ITEMREFNO, QUANTITYINSTOCK, ’); WRITELN (’PRICEPERUNIT, SUPPLIERNO’); with INVRECPTRf do 239
READLN (ITEMREFNO, QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); NEWLEAF.ITEMREFNO: == INVRECPTRJ. ITEMREFNO; NEWLEAF.POINTER: = INVRECPTR; INSERT (PRIMARYINDEX, NEWLEAF); if DBSTATUS <> OKAY then begin WRITELN (’ITEMREFNO has been inserted previously); ABORTTRANSACTION end else begin SNO: = INVRECPTRf .SUPPLIERNO; INSERT (SUPPLIERINDEX [SNO], NEWLEAF); if DBSTATUS < >OKAY then begin WRITELN (’supplier index insert failed’); ABORTTRANSACTION end else ENDTRANSACTION end end. б. Напишите программу вывода на терминальное устройство всех записей типа INVTYPE, к которым можно организовать доступ с помощью первичного индекса. Расположите записи в по- рядке возрастания ITEMREFNO, по одной записи на строке. в. Допустите, что переменная I относится к типу REFRANGE. Напишите программу считывания значения I о терминального устройства и вывода на него значения выражения proj QUANTITYINSTOCK (sei ITEMREFNO =. I (INVEN- TORY)) где INVENTORY — совокупность записей, к которым можно организовать доступ с помощью первичного индекса. Если записи с значением ITEMREFNO, равным значению I, не найдено, в про- грамме должно быть предусмотрено соответствующее сообщение. г. Пусть S относится к типу SUPPRANGE. Напишите про- грамму, которая считывает значение S с терминального устрой- ства, используя SUPPLIERINDEX, и выводит на терминальное устройство значение следующего выражения: proj ITEMREFNO, QUANTITYINSTOCK (sei SUPPLIER- NO = S (INVENTORY)) д. Напишите программу, которая считывает значение S с тер- минального устройства и выводит на него значение следующего выражения: proj ITEMREFNO (sei (SUPPLIERNO = S) and (QUANTITYINSTOCK<8) (INVENTORY)) e. Сделайте то же самое, что и в 2, д, для выражения 240
proj ITEMREFNO (sei (SUPPLIERNO <> S) and (QUANTITYINSTOCK<8) (INVENTORY)) ж. Напишите программу, о помощью которой осуществляется считывание с терминального устройства значения ITEMREFNO и удаление из набора INVENTORY любой записи, значение поля ITEMREFNO которой совпадает с введенным. При этом должна быть предусмотрена модификация индекса SUPPLIERINDEX. В том случае, если записи с заданным значением поля ITEM- REFNO не найдено, должно быть выдано соответствующее сооб- щение. з. Напишите программу, с помощью которой с терминального устройства считываются значения ITEMREFNO и QUANTITYIN- STOCK. Если записи с заданными значениями ITEMREFNO в INVENTORY не найдено, должно выдаваться соответствующее сообщение. Если такая запись обнаружена, то в ней необходимо изменить значение поля QUANTITYINSTOCK на соответству- ющее значение, считанное с терминального устройства. 3. В этом упражнении используется файл данных PEOPLE из разд. 7.4 и DBD НА. а. Пусть N и Н — переменные, предназначенные для хране- ния значений FIRSTNAME и HAIRCOLOUR соответственно. Напишите программу, с помощью которой производятся считыва- ние с терминального устройства значений N и Н и выдача зна- чения следующего выражения: proj SURNAME (sei (FIRSTNAME =» N) or (HAIRCOLOUR = = H) (PEOPLE)) б. Напишите программу считывания с терминального устрой- ства значений N и Н и выдачи значения следующего выражения: proj SURNAME (sei (FIRSTNAME = N) and not (HAIRCOLOUR = H) (PEOPLE)) в. Пусть L и U переменные, предназначенные для хранения значений HEIGHT. Напишите программу считывания с терми- нального устройства значений Н, L и U и выдачи значения сле- дующего выражения: proj FIRSTNAME, SURNAME (sei (HAIRCOLOUR = H) and (HEIGHT>L) and (HEIGHT<U) (PEOPLE))
ГЛАВА 8 РЕАЛИЗАЦИЯ СОЕДИНЕНИЙ 8.1. ИЕРАРХИЧЕСКИЕ МЕЖФАЙЛОВЫЕ ИНДЕКСЫ Для выполнения операции соединения, проводимой с двумя простыми файлами FNAME1 и FNAME2: FNAME1 FNAME2 G Н К Н а 7 Р 9 b 2 q 6 с 9 г 7 d 2 s 2 е 6 t 9 f 9 можно использовать следующую программу: RESET (FNAME 1); repeat READ (FNAME 1, FNAME 1 REC); RESET (FNAME2); repeat READ (FNAME2, FNAME2REC); if FNAME 1 REC H = FNAME2REC.H then WRITELN (FNAME1REC.G, FNAME1REC.H, FNAME2REC.K) until EOF (FNAME2) until EOF (FNAME 1) Если предположить, что файлы FNAME 1 и FNAME2 содержат тысячи записей, то выполнение этой программы, включающей вложенные циклы, займет очень много времени. Когда соедине- ние производится по полю, являющемуся ключом сортировки для обоих файлов, выполнение операции можно ускорить, исполь- зуя алгоритм групповой обработки из разд. 6.3.2. Этот алгоритм можно применить, например, к двум таким файлам: G Н КН d 2 s 2 b 2 q 6 242
в 6 р 7 а 7 t 9 с 9 р 9 f 9 Однако часто бывает невыгодно проводить сортировку файлов в удобном для выполнения операции соединения порядке. Еще один метод заключается в снабжении файла FNAME2 вторичным индексом по полю Н. Тогда для выполнения операции соединения можно использовать следующий алгоритм: RESET (FNAME1); repeat READ (FNAME1, FNAME1REC); поиск значения FNAME 1REC.H во вторичном индексе FNAME2 по Н; for каждой записи FNAME2, содержащей значение Н do WRITELN (FNAME1REC.G, FNAME1REC.H, FNAME2REC.K) until EOF (FNAME 1) Использование индекса позволяет не тратить время на орга- низацию доступа к тем записям FNAME2, значение поля Н кото- рых отличается от значения этого поля текущей записи файла FNAME 1. Более подробно преимущества этого метода будут ра- зобраны в разд. 8.7. Недостатком метода является то, что необхо- димо дополнительно тратить время на поиск в индексе. Можно устранить этот недостаток с помощью включения членов-указа- телей непосредственно в файл FNAME 1 (рис. 8.1). Тогда выпол- нение операции соединения описывалось бы с помощью следу- ющего алгоритма? for каждой записи FNAME 1 do for каждого члена-указателя этой записи do begin перейти от указателя к записи файла FNAME2; вывести значения поля К этой записи файла FNAME2 вместе со значениями полей G и Н текущей записи файла FNAME1 end На рис. 8.1 члены-указатели размещаются отдельно от файла, на записи которого они ссылаются. Точно так же сделано и в разд. 7.2.1. Другая возможность (см. разд. 7.3.2) заключается в размещении членов-указателей непосредственно в том файле, на записи которого они ссылаются. Важно отметить, что в этом случае с любой записью файла данных связываются указатели, ссылающиеся на другие записи того же самого файла. Такого рода организация указателей называется межфайловым индексом. 243
FNAME 1 FNAME 2 Рис. 8.1* Простой межфайловый индекс Пример, учитывающий ди- намику цен. Межфайловый ин- декс уже был использован в разд. 5.4, где каждая запись в файле товарных запасов была связана со списком записей, каждая из которых состояла из цены и даты ее введения. Для того чтобы данные из разд. 5.4 были представлены в пер- вой нормальной форме, их следует реорганизовать-. Для этого используем два файла: INVENTORY и PRICES. ТОВАРНЫЕ ЗАПАСЫ (INVENTORY) ШИФР ТОВАРА (ITEMREFNO) 124 131 133 КОЛИЧЕСТВО НОМЕР ПОСТАВЩИКА (QUANTITYINSTOCK) (SUPPLIERNO) 18 23 9 55 75 23 ЦЕНЫ (PRICES) ШИФР ТОВАРА дата ЦЕНА (ITEMREFNO) (DATE) (PRICES) 124 930612 12.30 124 920109 10.90 131 930526 9.10 131 930201 8.05 131 921001 8.50 131 920725 8.10 131 910701 50.00 Запрос «найти даты всех изменений цен для товаров, постав- ляемых поставщиком с номером 23», может быть выражен так: proj DATE (PRICES join sei SUPPLIERNO = 23 (INVENTOSY)) Метод реализации операции соединения, основанный на использо- вании нормализованных файлов INVENTORY и PRICES, преду- сматривает линейный поиск в файле PRICES всех тех значений ITEMREFNO, которые определены в результате выполнения операции sei SUPPLIERNO = 23 (INVENTORY) Для того чтобы не тратить время на просмотр записей PRICES, не содержащих искомого значения ITEMREFNO, следует приме- нить межфайловые индексы (рис. 8.2). Теперь в состав каждой записи файла INVENTORY входит массив указателей, ссыла- ющихся на записи файла PRICES. Это позволяет для любого заданного товара сразу же определить динамику изменения его 244
ВАТЕ (дата) ITEMREFNO (шифр товара) QUANTITYINSTOCK SUPPLIERNO POINTERS (количество (номер (указатели) на складе) поставщика) --------------- 124 " 1 1 « 1 L 'll 131 | 9 1 55 _J ГЫ 4 к-!!!—] | 75 | 1 2i • 1 и 1111 930612 920109 930526 930201 921001 920725 910701 PRICE Who) 12.30 10.90 9. 40 8. 95 8.50 8.10 50.00 Рис. 8.2. Межфайловый индекс, связывающий записи файла PR ICES с записями файла INVENTORY цены. Причем в этом случае не надо просматривать не относя- щиеся к запросу записи. В данном примере введение межфайло- вого индекса не приведет к значительному сокращению времени поиска, но для файлов, содержащих тысячи записей, использова- ние межфайлового индекса позволит существенно повысить эф- фективность реализации операций соединения. Пример иерархической структуры данных о товарных запасах. Изменим структуру файла TOYINVEN, описанного в разд. 6.5.2 и 6.6.1. Теперь каждая запись файла будет состоять из компо- нентов, которые размещаются в другом файле, носящем имя COMPONENTS. Ниже приведены примеры записей файла COM- PONENTS. ШИФР ТОВАРА ЧАСТЬ КОЛИЧЕСТВО НА СКЛАДЕ (ITEMREFNO) (PARTOF) (QUANTITYINSTOCK) FT61 АА39 00006 FV04 АА39 00315 КА55 AQ01 00043 KN21 АА39 00031 КХ39 AQ01 00050 LA 18 BC59 00427 Эта информация свидетельствует о том, что значением поля QUANTITYINSTOCK для записи с ITEMREFNO, равным FT61, является 6; для записи с ITEMREFNO, равным FV04, служит 315 и т. д. Кроме того, FT61, FV04 и KN21 —компоненты записи файла TOYINVEN с ключом АА39, КА55 и КХ39 — компоненты записи файла TOYINVEN с AQ01 и т. д. Следуя дальше, введем еще один файл SUBCOMPONENTS. В этом файле хранятся све- дения о значениях ITEMREFNO и QUANTITYINSTOCK для подкомпонент, из которых состоят компоненты. Ниже показано несколько записей файла SUBCOMPONENTS. ШИФР ТОВАРА ЧАСТЬ КОЛИЧЕСТВО НА СКЛАДЕ (ITEMREFNO) (PARTOF) (QUANTITYINSTOCK) РВ12 FT61 00050 РВ20 FT61 00043 РС81 FV04 00005 PD50 FV04 00100 РЕ31 FT61 00047 245
ITEMREFNO QUANTITY ITEMREFNO QUANTITY ITEMREFNO QUANTITY Рис, 8.3. Пример иерархического межфайлового индекса Для простоты будем считать, что, во-первых, одну и ту же компоненту нельзя использовать для построения нескольких запи- сей файла TOYINVEN, и, во-вторых, каждая подкомпонента мо- жет входить только в состав одной компоненты. Если АА39 — ключ записи файла TOYINVEN, то для ответа на запрос типа «определить значения ITEMREFNO и QUANTITYINSTOCK всех подкомпонент записи с ключом АА39» необходимо выполнить по крайней мере две операции соединения. Для того чтобы умень- шить время поиска ответа на такой запрос, надо исключить про- смотр не относящихся к нему записей. Для этого предназначен межфайловый индекс, структура которого изображена на рис. 8.3. Легко проверить, что указатели, показанные на рис. 8.3, согла- суются с нормализованными вариантами трех файлов. Тогда для поиска значений ITEMREFNO и QUANTITYINSTOCK всех под- компонент записи с ключом АА39 можно использовать межфай- ловые индексы. Алгоритм поиска будет выглядеть так: Найти запись файла TOYINVEN с ITEMREFNO = 'АА39' for каждой записи файла COMPONENTS, на которую осуществляется ссылка из найденной записи do for каждой записи файла SUBCOMPONENTS, на которую есть ссылка из построений записи файла COMPONENTS do WRITELN (SUBCOMPONENT. ITEMREFNO, SUBCOMPONENT. QUANTIT- YINSTOCK). Такая иерархическая структура межфайловых индексов яв- ляется простейшим примером организации иерархической базы данных. Отметим, что в процессе развития баз данных такой тип организации одним из первых завоеваЛ право на существование и начал использоваться в коммерческих системах. На рис. 8.3 указатели связывают записи, компоненты и под- компоненты в одном направлении. Поэтому их использование 246
не позволяет отвечать на запросы типа «для какой записи РС81 является подкомпонентом». В примере, связанном с динамикой изменения цен, использовались указатели, идущие от записей файла INVENTORY к записям файла PRICES. Эти указатели позволили ускорить процесс выполнения операции соединения при поиске дат изменения цен, относящихся к заданному постав- щику. Но эти указатели оказались ненужными для выполнения операции соединения при поиске всех поставщиков, осуществля- ющих изменение цен в заданный срок, т. е. такие указатели помо- гают только при запросах, реализация которых требует наличия связей, идущих от INVENTORY к PRICES. В следующем раз- деле будут рассмотрены указатели, обеспечивающие связь в двух направлениях. 8.2. ДВУНАПРАВЛЕННЫЕ межфайловые ИНДЕКСЫ 8.2.1. НАБОРЫ Рассмотрим файл ЛОШАДИ (HORSES) (аналогичный файл приведен в разд. 4.10.3) и простой файл ВЛАДЕЛЬЦЫ (OWNERS). ФАМИЛИЯ ВЛАДЕЛЬЦА АДРЕС ЭЙСОН КЛУМБЕРВИК БИСОН УИГХЭМСТЕНД ДЖИСОН ПЛУМХЭЙВЕМ Будем считать, что в файле ВЛАДЕЛЬЦЫ нет владельцев с одинаковыми фамилиями и что адреса владельцев состоят из одного слова. Рассмотрим запрос типа «определить масти лоша- дей, владельцы которых живут в Уигхэмстеде», который на языке реляционной алгебры может быть выражен так: МАСТЬ (ЛОШАДИ Join sei АДРЕС = 'УИГХЭМСТЕНД' (ВЛАДЕЛЬЦЫ)) Для того чтобы увеличить эффективность поиска, снабдим каждую запись файла ВЛАДЕЛЬЦЫ, содержащую данные о вла- дельце, членами-указателями, ссылающимися на все записи файла ЛОШАДИ, содержащими информацию о лошадях, которые яв- ляются собственностью данного владельца. (Подобного рода опе- рация уже проводилась в разд. 8.1). Кроме того, обеспечим каж- дую запись файла о лошадях указателем на соответствующую запись файла о владельцах (см. разд. 7.3.2). Это позволит быстро получить ответ на запрос типа «определить фамилию и адреса всех владельцев вороных лошадей», который на языке реляцион- ной алгебры имеет вид proj ФАМИЛИЯ ВЛАДЕЛЬЦА, АДРЕС (ВЛАДЕЛЬЦЫ join sei МАСТЬ « 'ВОРОНОЙ' (ЛОШАДИ)) 247
Рис. 8.4. Структура межфайлового индекса с указателями, идущими в двух направлениях Структура данных, приведенная на рис. 8.4, отличается от соответствующих структур из разд. 8.1 тем, что в ней существуют не только указатели к членам записей, но и указатели к владель- цам. Алгоритм, позволяющий получить ответ на сформулирован- ный выше запрос, имеет следующий вид: for каждой лошади, для которой справедливо sei МАСТЬ= 'ВОРОНОЙ' (ЛОШАДИ) do begin следовать по указателю от лошади к ее владельцу; вывести на терминальное устройство фамилию и адрес этого владельца end Выполнение такого алгоритма позволит избежать просмотра не относящихся к запросу записей файла ВЛАДЕЛЬЦЫ. Покажем теперь, как можно установить масти всех тех лоша- дей, владельцы которых живут в Уигхэмстеде: for каждого владельца, адрес которого Уигхэмстед do for каждой лошади, на которую есть ссылка от этого владельца do вывести на терминальное устройство масть этой лошади Очевидно, что такой алгоритм позволит избежать просмотра не относящихся к запросу записей файла ЛОШАДИ. Вместо того, чтобы хранить все члены-указатели в записях о владельцах (как в разд. 7.3.1), можно размещать их в записях- членах (как в разд. 7.3.2). Кольцевая структура, позволяющая сделать это, приведена на рис. 8.5. Рассмотрим запрос типа «опре- делить фамилии и адреса всех владельцев вороных лошадей». Использование кольцевой структуры для получения ответа на этот запрос приведет к большим затратам времени, чем в случае, если применяется структура, изображенная на рис. 8.4. Это объясняется тем, что теперь для того, чтобы добраться до вла- дельца, необходимо пройти по цепочке указателей до записи, от которой идет указатель на владельца. Преимущество кольце- вой структуры заключается в том, что теперь у каждой записи 248
Рис. 8.5. Реализация набора с помощью колец только один член-указатель, т. е. общее число полей, отведенных под указатели, сокращается. Отношение между записями файлов ВЛАДЕЛЬЦЫ и ЛО- ШАДИ относятся к типу "один ко многим", т. е. один владелец может иметь несколько лошадей, но у каждой лошади должен быть только один владелец. Для отношений такого типа органи- зация межфайловых индексов очень важна. Введем новое понятие-набор. Оно обозначает межфайловую индексную структуру типа "один ко многим", состоящую из вла- дельцев, ссылающихся на члены, и из членов, указывающих на своих владельцев. Набор можно реализовать, используя либо кольца, либо члены-указатели, которые не включены в состав файла, к записям которого они ссылаются. Так, например, на рис. 8.4 и 8.5 показаны оба способа реализации одного и того же набора. В процессе работы с файлами, связанными с помощью разных наборов, желательно отличать наборы друг от друга, а для этого надо снабдить их именами. Например, можно дать имя ЛОШАДИ -ВЛАДЕЛЬЦЫ набору, связывающему файлы ЛОШАДИ И ВЛАДЕЛЬЦЫ. Весьма полезны диаграммы, показывающие, как файлы свя- заны с помощью наборов и какой из файлов содержит записи-вла- дельцы, а какой записи-члены. На этих диаграммах файлы изобра- жаются в виде прямоугольных блоков, а наборы — в виде линий, соединяющих эти блоки. Имена файлов помещаются внутри пря- моугольников, а имена наборов — над линиями. На концах ли- ний, примыкающих к членам, размещается "лапа" (рис. 8.6). Это сделано для того, чтобы можно было отличить файл, содержащий записи-владельцы, от файла, содержащего записи-члены. В неко- торых книгах для этой цели используются другие символы, на- пример стрелка или окружность. Диаграммы, подобные изобра- Рис. 8.6. Диаграмма Бахмана, опи- сывающая набор на рис. 8.5 “--------- ЛОШАДИ- ВЛАДЕЛЬЦЫ J-------- ВЛАДЕЛЬЦЫ]----ЛОШАДИ 249
женной на рис. 8.6, носят название диаграмм Бахмана. Исполь- зование диаграмм Бахмана позволяет достаточно просто и на- глядно отображать структуры наборов данных. 8.2.2. РЕАЛИЗАЦИЯ ОПЕРАЦИЙ СОЕДИНЕНИЯ В СЛУЧАЕ ОТНОШЕНИЯ «МНОГИЕ КО МНОГИМ» Без использования автономных элементов связи. Три простых файла носят названия РЕЙСЫ (CRUISES), ПОРТЫ (PORTS) и ЗАХОДЫ (VISITS). Речь будет идти о заходах судов, выполняющих определенные рейсы, в те или иные порты (см. разд. 3.11): РЕЙСЫ НОМЕР. РЕЙСА НАЗВАНИЕ. ДАТА. ОТПЛЫТИЯ 1 СУДНА СИПРИСЕС 84 04 07 2 БРИСТОЛБЕЛЛ 84 04 12 3 МЭРИРОУЗ 84 05 29 4 БРИСТОЛБЕЛЛ 84 05 29 5 КАСЛКУИН 84 06 03 6 КАСЛКУИН 84 06 30 7 СИПРИСЕС 84 06 30 8 МЭРИРОУЗ 84 07 19 ЗАХОДЫ НОМЕР. РЕЙСА НАЗВАНИЕ- ПО РТА 2 ГИБРАЛТАР 3 ГИБРАЛТАР 5 ГИБРАЛТАР • ПОРТЫ 7 ГИБРАЛТАР 8 ГИБРАЛТАР НАЗВАНИЕ. ПОРТА СУТОЧНЫЙ. НАЛОГ 2 МАРСЕЛЬ ГИБРАЛТАР 100 3 МАРСЕЛЬ . МАРСЕЛЬ 250 6 МАРСЕЛЬ АЛЖИР 150 2 АЛЖИР АЛЕКСАНДРИЯ 150 3 АЛЖИР ПАЛЕРМО 200 7 АЛЖИР АФИНЫ 250 1 АЛЕКСАНДРИЯ СТАМБУЛ 150 2 АЛЕКСАНДРИЯ 3 АЛЕКСАНДРИЯ 5 АЛЕКСАНДРИЯ 6 АЛЕКСАНДРИЯ 4 ПАЛЕРМО 6 ПАЛЕРМО 7 ПАЛЕРМО 8 ПАЛЕРМО 1 АФИНЫ 3 АФИНЫ 5 АФИНЫ 8 АФИНЫ 1 СТАМБУЛ 1 СТАМБУЛ 8 СТАМБУЛ 250
Рис. 8.7. Пример межфайлового индекса «многие ко многим» Анализируя файл ЗАХОДЫ, можно установить, что, напри- мер, в рейсе 1 предусмотрены заходы в Александрию, Афины и Стамбул. Та же Александрия фигурирует и в рейсе 3, т. е. файл ЗАХОДЫ определяет отношение "многие ко многим", существу- ющее между рейсами и портами. Для ответа на запрос типа 'опре- делить названия портов, в которые хотя бы во время одного из своих рейсов заходит судно МЭРИРОУЗ", необходимо выпол- нить две операции соединения. Для того чтобы осуществить это быстро, можно предусмотреть в записях файла РЕЙСЫ члены- указатели к соответствующим записям файла ПОРТЫ (рис. 8.7). В такой структуре члены-указатели не находятся в тех файлах, на которые они ссылаются. В запись файла ПОРТЫ с ключом ГИБРАЛТАР включены указатели, ссылающиеся на все мар- шруты, в которых предусмотрено посещение Гибралтара. Поэтому можно, используя межфайловый индекс, обратиться к данным только о тех рейсах, которые связаны с посещением Гибралтара, 251
СЬязи Рис. 8.8. Использование элементов связи для реализации отношения (многие ко многим* между файлами РЕЙСЫ и ПОРТЫ и не просматривать информацию о других рейсах. Этот межфай- ловый индекс не является набором, поскольку с его помощью реализуется отношение 'многие ко многим', существующее между записями файлов РЕЙСЫ и ПОРТЫ. На рис. 8.7 указатели ин- декса несут ту же информацию, которая может быть извлечена из файла ЗАХОДЫ. Так что этот файл больше не нужен. 252
РЕЙСЫ РЕЙСЫ-ЗАХОДЫ J \^ПОРТЫ_ЗАХ0ДЫ ———« cotton порты Рис. 8.9. Диаграмма Бахмана для структуры данных, показанной на рис. 8.8 Более того, межфайловый индекс содержит избыточную ин- формацию. Например, запись о рейсе 6 включает член-указатель к ПАЛЕРМО, а запись Палермо, в свою очередь, имеет указа- тель к рейсу 6. Использование элементов связи. В предыдущем разделе рас- сматривался межфайловый индекс, члены-указатели которого не размещались в тех записях, на которые они ссылались. Если стремиться реализовать межфайловый индекс с помощью колец, то необходимо учесть тот факт, что каждая запись файла РЕЙСЫ может быть владельцем для более чем одной записи файла ПОРТЫ, и, в свою очередь, каждая запись файла ПОРТЫ может оказаться владельцем для более чем одной записи файла РЕЙСЫ. Сложности возникают из-за того, что в случае отношения ’многие ко многим' для каждого члена нужно несколько указа- телей к различным владельцам. Можно реализовать такие связи не непосредственно, а через цепочку других членов. В этом слу- чае обычная реализация кольца нецелесообразна, поскольку тре- буется более одного указателя на каждую запись кольца и число указателей для разных записей различно. В случае отношения 'один ко многим' (см. разд. 8.1) такая проблема не возникает. Это объясняется тем, что каждый член всегда имеет одного вла- дельца, а реализация кольца с одним указателем на каждую запись эффективна. Для того чтобы реализовать межфайловый индекс с помощью колец, можно декомпозировать отношение "многие ко многим' на два отношения 'один ко многим". Для этого необходимо создать вспомогательные записи, носящие название связей. На рис. 8.8 показаны связи и образующиеся с их помощью кольца для файлов РЕЙСЫ и ПОРТЫ. Каждой записью файла СВЯЗИ владеет только одна запись файла РЕЙСЫ и одна запись файла ПОРТЫ. Поэтому теперь достаточно одного указателя на каждую запись, входящую в кольцо. Стоит убедиться в том, что связи однозначно соответствуют записям нормализованного файла ЗАХОДЫ из разд. 8.2.2 и, кроме того, несут ту же самую информацию. Диа- грамма Бахмана, приведенная на рис. 8.9, позволяет представить структуру данных, изображенную на рис. 8.8 Можно дать файлу связей любое имя, например ЗАХОДЫ. Продемонстрируем, как используются связи для реализации операций соединения. Для этого приведем алгоритм, позволя- ющий получить ответ на запрос типа "определить названия пор- тов, посещаемых во время хотя бы одного из рейсов на судне МЭРИРОУЗ'. 253
for каждой записи файла РЕЙСЫ такой, что sei НАЗВАНИЕ СУДНА='МЭРИРОУ8' (РЕЙСЫ) do for каждой найденной при просмотре кольца связи, владелец которой является запись файла РЕЙСЫ do begin определить с помощью сканирования кольца запись файла ПОРТЫ, которая является владельцем этой записи связи; вывести на терминальное устройство значения поля НАЗВАНИЕ ПОРТА записи файла ПОРТЬ!, end. Связи, имеющие в составе поля данных. В предыдущем при- мере записи файла связи состояли из двух полей, являющихся указателями. Один указатель использовался для кольца, владель- цем элементов которого являлась запись РЕЙСЫ, а второй — для кольца с владельцем ПОРТЫ. Попробуем расширить эту структуру для включения в нее информации о дате каждого посе- щения порта. Для этого включим в каждую запись связи третье поле, предназначенное для хранения даты посещения порта. Эта дата должна соответствовать тем рейсу и порту, к которым дан- ная запись связи относится. Таким образом, третье поле — это пример поля данных, размещенного в записи связи. На практике обычно записи связи включают как поля-указатели, так и поля данных. Рассмотрим пример использования связей для трех про- стых файлов: SALESORDERDETAIL (содержание наряда), SA- LESOR DERHEADER (заголовок наряда) и INVENTORY (то- варные знаки). Нормализованные варианты этих файлов имеют следующие поля: ТОВАРНЫЕ ЗАПАСЫ (INVENTORY) содержание наряда (SALESORDERDETAIL) ЗАГОЛОВОК НАРЯДА (SALESORDERHEADER) ШИФР. ТОВАРА НОМЕР. НАРЯДА НОМЕР. НАРЯДА (ITEMREFNO) (SALESORDERNO) (SALESORDERNO) КОЛИЧЕСТВО. НА. СКЛАДЕ ШИФР. ТОВАРА НОМЕР. КЛИЕНТА (QUANTITYINSTOCK) ЦЕ НА. ЗА. ЕДИНИЦУ (PRICEPERUNIT) (ITEMREFNO) (CUSTOMERNO) НОМЕР. ПОСТАВЩИКА ЗАКАЗАННОЕ. ДАТА. ЗАКАЗА (SUPPLIERNO) КОЛИЧЕСТВО (QU ANTI TYORDER ED) (DATE) Для ответа на запрос типа '"определить CUST OMERNO для каждого клиента, который заказывает товар с шифром 114'", потребуется выполнить две операции соединения. На языке ре- ляционной алгебры этот запрос будет выглядеть так proj CUSTOMER (SALESORDERHEADER Join SALESORDERDETAIL Join sei ITEMREFNO = 114 (INVENTORY)) INVENTORY ^NCLUDEB SALESQRDERBETAIL SO2..-p4LESORBERHEAJJER | Рис. 8.10. Диаграмма Бахмана для структуры данных, изображенной на рис. 8.11 254
INVENTORY коварные запасы) SALESORDERDETAIL SALE5ORDERHEADER (содержание нарядов) (заголовки нарядов) ITEM REFNO (шифр товара) QUANTITY PRICE SUPPLIER QUANTITY SALESORDER CUSTOMER INSTOCK PERUNIT NO ORDERED NUMBER NUMBER (количество (цена за (номер (заказанное (номер (номер на складе) единицу) поставщика) количество) наряда) клиента) DATE (дата) Рис. 8.1 L Реализация наборов INCLUDED и LINESOF с помощью указателей В принципе можно выбрать записи с ITEMREFNO = 114 прямо из файла SALESORDERDETAIL, не используя при этом файл INVENTORY. Процесс выбора можно ускорить, преду- смотрев для файла вторичный индекс по ITEMREFNO. Более гибкий вариант состоит в замене владельцев этого индекса запи- сями файла INVENTORY. В действительности же удобнее свя- зать два файла INVENTORY и SALESORDERDETAIL с по- мощью набора. Кроме того, для ускорения выполнения операции соединения необходимо связать файлы SALESORDERDETAIL и SALESORDERHEADER с помощью второго набора (рис. 8.10). На рис. 8.11 дан конкретный пример описанной структуры, вклю- чающей несколько записей, в то время как на рис. 8.10 пред- ставлена обобщенная схема этой структуры. На рис. 8.11 записи файла SALESORDERDETAIL преобразованы в записи связи. В эти записи вместо полей ITEMREFNO и SALESORDERNO входят теперь мультисписковые указатели. Для получения ответа на запрос "определить CUSTOMERNO каждого клиента, который имеет заказ на товар с шифром' 114" можно использовать следующий алгоритм: Найти запись INVENTORY с ITEMREFNO = 114; for каждой связи SALESORDERDETAIL, владельцем которой является найденная запись INVENTORY do 255
with определить с помощью просмотра кольца, какая запись SALESORDERHEADER является владельцем текущей записи сдт FSORDFRDFTATT • WRITELN (SALESORDERHEADER. CUSTOMERNO) В результате работы этого алгоритма один и тот же CUSTO- MERNO может быть получен несколько раз. В этом и следу- ющих примерах устранение дубликатов проводиться не будет. Для сформулированного выше запроса результатом работы алго- ритма будет 308. Отметим, что к записи файла INVENTORY с ITEMREFNO =? = 114 организуется произвольный доступ. Метод доступа опре- деляется организацией файла INVENTORY (INVENTORY мо- жет, например, быть файлом с хэш-адресацией). Реализация других операций соединения осуществляется ана- логично. Рассмотрим пример алгоритма, позволяющего получить ответ на запрос «найти ITEMREFNO и DESCRIPTION всех товаров, заказываемых клиентом с номером 308»: for каждой записи, удовлетворяющей условию sei CUSTOMERNO = 308 (SALESORDERHEADER) do for каждой записи SALESORDERDETAIL, владельцем которой яв- ляется найденная запись SALESORDERHEADER do Определить с помощью просмотра кольца, какая запись файла INVEN- TORY является владельцем текущей записи SALESDRDERDETAIL; WRITELN (INVENTORY.ITEMREFNO, INVENTORY.DESCRIPTION). Если файл SALESORDERHEADER снабжен вторичным ин- дексом по CUSTOMERNO, то этот индекс можно использовать для того, чтобы ускорить первоначальный поиск в файле. В про- тивном случае для того, чтобы найти запись с CUSTOMERNO = = 308, можно использовать обычный метод линейного поиска. Полезно уметь представлять вторичные индексы и на диаграм- мах Бахмана. Для этого используется набор, владелец которого на диаграмме изображается в виде черной точки. Например, если считать, что существует вторичный индекс по CUSTOMERNO, то диаграмма Бахмана, изображенная на рис. 8.10, преобразуется в диаграмму, показанную на рис. 8.12. До сих пор считалось, что наборы следует реализовывать только с помощью колец. Важно отметить, что допускались только реализации, предусматривающие на каждую запись кольца по одному указателю. Но, естественно, возможны и другие реализа- ции наборов. Например, можно реализовать набор, размещая члены-указатели в В-дереве, которое принадлежит записи-вла- дельцу, и снабжая каждую запись-член указателем, ссылающимся на своего владельца в наборе. Этот вариант требует больше па- мяти, чем вариант, использующий кольца, но гарантирует более быстрый доступ, поскольку становится ненужным движение по цепочке членов. Другим важным достоинством использования 256
CUSTOMERNO | INVENTORY |-1УС^.°Е1|-< SALESOKJERPtTAIL UHKOF sALESDRDEIfHEACER | Рис. 8.12. Диаграмма Бахмана, показывающая, что файл SALESORDER- HEADER имеет вторичный индекс по полю CUSTOMERNO В-деревьев является наличие готовых процедур для работ с ними INSERT, FINDKEY и т. д. Итак, рассматривается реализация наборов, основанная на использовании В-деревьев. Для описанного ранее в этом разделе примера будут составлены программы, предназначенные для ра- боты с наборами. Удобно использовать организацию, основанную на В-деревьях, для доступа к файлам INVENTORY и SALE- SORDERHEADER. Если, например, хранить записи файла INVENTORY в блоках листьев В-дерева, то при выполнении операций включения и удаления их необходимо будет перемещать и, следовательно, нужно будет модифицировать указатели файла SALESORDERDETAIL, ссылающиеся на своих владельцев. А это весьма трудоемкая процедура. Для того чтобы избежать этого, будем использовать для файлов INVENTORY и SALESORDER- HEADER плотные первичные индексы. Ниже приведено DBD: DBDNAME HD; type REFRANGE = 100.. 140; SONRANGE = 50..99; INVPTR = tINVTYPE; SOHPTR = fSOHTYPE; DETAILPOINTER = fSODETAIL; INVLEAF = record {for primary index of INVENTORY}1 ITEMREFNO: REFRANGE; POINTER: INVPTR SOHLEAF = record {for primary index of SALESORDERHEADER} SALESORDERNUMBER: SONRANGE; POINTER: SOHPTR end; SODETAIL = record {linker}1 2 3 4 INCLUDED: INVPTR; {pointer to owner in INVENTORY}* QUANTITYORDERED: INTEGER; LINESOF: SOHPTR {pointer to owner in SALESORDERHEADER}5 end; 1 (* для первичного индекса файла INVENTORY *) 2 (* для первичного индекса файла SALESORDERHEADER *) 3 (* описание комбинированного типа для записей связи *) 4 ( * указатель, ссылающийся на запись-владельца в файле INVENTORY *) 6 (♦ указатель, ссылающийся на запись-владельца в файле SALESOR- DERHEADER *) 9 Ульман Дж. 257
SOMEMBER = BTREE of DETAILPOINTER; INVTYPE-record {INVENTORY record type}® ITEMREFNO: REFRANGE: QUANTITYINSTOCK: INTEGER; PRICEPERUNIT: REAL; SUPPLIERNO: 10..99; INCLUDED: SOMEMBER {points to linkers that this record owns}7 end; SOHTYPE = record {SALESORDERHEADER record type}® SALESORDERNUMBER: SONRANGE; CUSTOMERNUMBER: 100.999; DATE: 880101..920101; LINESOF: SOMEMBER {points to linkers that this record owns}9 end; var INVPRIMARY = BTREE of INVLEAF on ITEMREFNO; SOHPRIMARY= BTREE of SOHLEAF on SALESORDERNUMBER; finish В этом DBD наборы INCLUDED и LINESOF (см. рис. 8Л2) реализованы с помощью полей INCLUDED и LINESOF. Напри- мер, поле INCLUDED каждой записи файла INVENTORY (пер- вичный ключ ITEMREFNO) является В-деревом, состоящим из членов-указателей к связям (тип SODETAIL), которые относятся к данному ITEMREFNO. В свою очередь, поле INCLUDED записи связи является указателем, отсылающим к той записи файла INVENTORY, которую однозначно определяет содержание наряда. Перед тем как использовать только что описанную струк- туру базы данных для получения ответов на запросы, необходимо поместить в нее конкретные данные. Это можно сделать, вводя данные непосредственно с терминала. Но удобнее считывать дан- ные из обычного последовательного файла. Ниже приведена про- грамма, которая считывает записи INVENTORY из текстового файла (предпочтение отдано текстовому файлу, поскольку в дан- ном случае программу для него написать проще, чем для двоич- ного файла). • (* * описание комбинированного типа для записей файла INVENTORY *) * (* указывает на те записи связи, для которых данная запись является владельцем *) 8 (* описание комбинированного типа для записей файла SALESORDER- HEADER ♦ ) • (♦ указывает на те записи связи, для которых данная запись является владельцем *) 258
program ENTERINV (INVENTORY, OUTPUT); invoke HD; var NEWINVPTR: INVPTR; NEWLEAF: INVLEAF: INVENTORY: TEXT; begin STARTTRANSACTION (UNRESTRICTED); RESET (INVENTORY); while not EOF (INVENTORY) and (DBSTATUS = OKAY) do begin CREATE (NEWINVPTR); wttb NEWINVPTRf do READLN (INVENTORY, ITEMREFNO, QUANTITYINSTOCK, PRICEPERUNIT, SUPPLIERNO); NEWLEAF.ITEMREFNO: = NEWINVPTRf ITEMREFNO; NEWLEAF.POINTER: - NEWINVPTR; INSERT (INVPRIMARY, NEWLEAF) end; if DBSTATUS=OKAY then ENDTRANSACTION else begin WRITELN ( insert ITEMREFNO ’.NEWINVPTR!.ITEMREFNO, ‘failed’); ABORTTRANSACTION end end. Поскольку программа, предназначенная для считывания запи- вей файла SALESORDERHEADER, аналогична приведенной вы- ше, останавливаться на ней не имеет смысла. Будем считать, что записи файла SALESORDERHEADER введены. Приводимая ниже программа считывает записи SALESORDERDETAIL из текстового файла и формирует связи, каждая из которых соответ- ствует одной записи SALESORDERDETAIL: program ENTERDETAIL (SALESORDERDETAIL, OUTPUT); invoke HD; var ITEMREFNO: REFRANGE; SALESORDERNUMBER: SONRANGE; NEWLINKERPTR: DETAILPTR; INVLEAFPTR: jlNVLEAF; SOHLEAFPTR: fSOHLEAF; ITEMPTR: INVPTR; HEADERPTR: SOHPTR; NOPROBLEM: BOOLEAN; SALESORDERDETAIL: TEXT; 9* .59
begin STARTTRANSACTION (UNRESTRICTED); RESET (SALESORDERDETAIL); NOPROBLEM: = TRUE; while not EOF (SALESORDERDETAIL) and NOPROBLEM do begin CREATE (NEWLINKERPTR); {create a new linker record}7 READ (SALESORDERDETAIL, ITEMREFNO, SALESORDERNUMBER, NEWLINKERPTRtQUANTITYORDERED); FINDKEY (INVPRIMARY, ITEMREFNO, INVLEAFPTR); if DBSTATUS < > OKAY then begin WRITELN (’ITEMREFNO ’, ITEMREFNO, ’ not found’); NOPROBLEM: = FALSE; DBSTATUS: = OKAY and else begin ITEMPTR: = INVLEAFPTRf .POINTER; {pointer to INVENTORY record}* INSERT (ITEMPTRf .INCLUDED, NEWLINKERPTR); {insert pointer to member}^ NEWLINKERPTR! .INCLUDED: = ITEMPTR; end; FINDKEY (SOHPRIMARY, SALESORDERNUMBER, SOHLEAFPTR); if DBSTATUS < > OKAY then begin WRITELN (’SALES ORDER NUMBER ’, SALESORDERNUMBER, ’ not found’); NOPROBLEM: = FALSE; DBSTATUS: = OKAY end else begin SOHPTR: = SOHLEAFPTRf.POINTER; {pointer to SALESORDERHEADER record}* INSERT (SOHPTRt LINES OF, NEWLINKERPTR); {insert pointer to member} * NEWLINKERPTRf.LINES OF: = SOHPTR {insert pointer to owner} $ end end; if NOPROBLEM then ENDTRANSACTION else ABORTTRANSACTION end. 1 (* формирование новой записи связи *) 2 (* указатель к записи файла INVENTORY *) 3 (* вставка указателя к члену «) 4 (* указатель к записи файла SALESORDERHEADER *) 6 (* вставка указателя к владельцу *) 260
Приведем пример программы, использующей наборы для реа- лизации операций соединения. Ниже помещена программа, пред- назначенная для считывания с терминального устройства значе- ния ITEMREFNO и определения значений CUSTOMERNUMBER для всех клиентов, которые заказывают товар с заданным ITEM- REFNO: program WHOORDEREDITEM (INPUT, OUTPUT); invoke HD; var INVLEAFPTR: fINVLEAF; MEMBERPTR: DETAILPTR; WHICHITEM: REFRANGE; ITEMPTR: INVPTR; begin STARTTRANSACTION (READONLY); WRITELN (’Please type item reference number’); READLN (WHICHITEM); FINDKEY (INVPRIMARY, WHICHITEM, INVLEAFPTR); if DBSTATUS = NOTFOUND then WRITELN (’item not found’) else begin ITEMPTR: == INVLEAFPTR^.POINTER; FINDFIRST (ITEMPTRf .INCLUDED, MEMBERPTR); if DBSTATUS = NOTFOUND then WRITELN (’there are no orders for this item’) else repeat WRITELN (MEMBERPTRf.LINESOFf.CUSTOMERNUMBER); FINDSUCC (ITEMPTRt INCLUDED, MEMBERPTR) until DBSTATUS < >OKAY end; ENDTRANSACTION end. В этой программе, так же как и в следующей, не предусмотрен отсев одинаковых результатов, т. е. одни и те же номера клиентов могут фигурировать в качестве ответов более одного раза. В сле- дующей программе производится ввод номера клиента с терми- нального устройства и определение (с последующим выводом на терминальное устройство) шифров и описаний товаров, заказан- ных данным клиентом. Эта программа несколько сложнее преды- дущей, поскольку поле CUSTOMERNO — вторичный ключ, не обеспеченный вторичным индексом: ' program ITEMSORDEREDBYCUS (INPUT, OUTPUT); invoke HD; var SOHLEAFPTR: fSOHLEAF; HEADERPTR: SOHPTR; MEMBERPTR: DETAILPTR; 261
WHICHCUST: 100 .999 {CUSTOMERNUMBER range} begin STARTTRANSACTION (READONLY); WRITELN (’type customer number’); READLN (WHICHCUST); FINDFIRST (SOHPRIMARY, SOHLEAFPTR); if DBSTATUS =*NOTFOUND then WRITELN (’there are no orders’) else repeat if SOHLEAFPTR!.POINTER!.CUSTOMERNUMBER = WHICHCUST then begin HEADERPTR: = SOHLEAFPTRf POINTER; FINDFIRST (HEADERPTRf.LINESOF, MEMBERPTR); if DBSTATUS = OKAY then repeat with MEMBERPTRf lNCLUDEDf do WRITELN (ITEMREFNO, DESCRIPTION); FINDSUCC (HEADERPTRf .LINESOF, MEMBERPTR) until DBSTATUS < >OKAY; DBSTATUS: = OKAY; end; FINDSUCC (SOHPRIMARY, SOHLEAFPTR); until DBSTATUS < >OKAY; ENDTRANSACTION end. Реализация наборов музыкальной базы данных. Рассмотрим реализацию музыкальной базы данных, которая описана в разд. 2.2. Отметим, что реализация предусматривает использо- вание нескольких наборов. Кроме этого, файл MUSICIANS снабжается индексом по полю MNAME, файл COMPOSITIONS — индексом по полю TITLE, файл ENSEMBLES — индексом по MUSNAME PLAYS А PERFORMERS MUSICIANS } COMPOSER TITLE COMPOSITIONS | INSTRUMENTALIST А ENSEMBLEMEMBERS1 --------------- MEMBERS -| ENSEMBLES MANAGER DIRECTOR PERFORMANCES PERFORMED __________A V К'НОВУ Рис. 8.13. Диаграмма Бахмана, показывающая, как музыкальная база данных может быть реализована с помощью наборов 262
полю ENAME. Но это не означает, что доступ и этим файлам возможен только 6 помощью их индексов. На рис. 8.13 приведена диаграмма Бахмана для рассматриваемой базы данных. Считается (так же как и в разд. 2.2), что каждая запись файла PERFOR- MANCES содержит описание исполнения одного музыкального произведения. Поэтому каждая запись файла PERFORMANCES имеет точно одного владельца, которым является определенная запись файла COMPOSITIONS в наборе PERFORMED. Заметим, что файлы MEMBERS и PERFORMANCES состоят из записей связи. Следующий алгоритм позволяет получить ответ на запрос "определить фамилии всех исполнителей ансамбля СИЗ". Счи- тается, что существует только один ансамбль с названием СИЗ: Найти запись файла ENSEMBLES с ENAME = СИЗ; for для каждой записи связи ENSEMBLEMEMBERS, владельцем которой является найденная запись ENSEMBLES do begin Найти с помощью набора INSTRUMENTALIST владельца из файла PERFORMERS для ранее найденной записи связи; Найти с помощью набора PLAYS запись файла MUSICIANS, которая является владельцем записи PERFORMERS; WRITELN (MUSICIANS. MNAME) end. В только что приведенном алгоритме для ускорения выполне- ния трех операций соединения используются три набора. Далее рассматривается алгоритм, позволяющий ответить на запрос "определить названия всех ансамблей, исполнивших хотя бы одно произведение композитора ЭЙЭФСКОГО". Считается, что существует только один композитор с такой фамилией: Найти запись MUSICIANS с МИАМЕ='ЭЙЭФСКИЙ'; for каждой записи COMPOSITIONS, владельцем которой является най- денная запись MUSICIANS do for каждой записи PERFORMANCES, владельцем которой является найденная запись COMPOSITIONS do begin Определить с помощью набора WHOBY владельца ENSEMBLES для найденной записи PERFORMANCES; WRITELN (ENSEMBLES. ENAME) end. Ниже приведено DBD для рассматриваемой базы данных. Как и раньше, имя набора используется в качестве имени поля, которое является указателем владельца набора, и имени другого поля, которое является В-деревом, состоящим из указателей членов набора: DBNAME MU; type STRING12=packed array £1 ..12] of CHAR; DATETYPE = 15000101.. 19991231; TOMUSIC1ANS = f MUSICIANS; 263
TOPERFORMERS = ^PERFORMERS; TOENSEMBLEMEMBERS = f ENSEMBLEMEMBERS; TOENSEMBLES = f ENSEMBLES; TOCOMPOSITIONS= f COMPOSITIONS; TOPERFORM ANCES = f PERFORMANCES; MUSICIANSLEAF=record MNAME: STRING12; POINTER: TOMUSICIANS end; COMPOSITIONSLEAF = record TITLE: STRING 12; POINTER: TOCOMPOSITIONS end; ENSEMBLESLEAF = record ENAME: STRING 12; POINTER: TOENSEMBLES end; MUSICIANS = record MNAME: STRING12; BDATE: DATETYPE; BCOUNTRY: STRING 12; PLAYS: BTREE of TOPERFORMERS; MANAGER: BTREE of TOENSEM BLES; DIRECTOR: BTREE of-TOPERFORMANCES* end; PERFORMERS = record INSTRUMENT, GRADE: STRING 12; PLAYS: TOMUSICIANS; INSTRUMENTALIST: BTREE of TOENSEMBLEMEMBERS end; ENSEMBLEMEMBERS = record INSTRUMENTALIST: TOPERFORMERS; MEMBERS: TOENSEMBLES end; ENSEMBLES = record ENAME, ECOUNTRY: STRING 12; MANAGER: TOMUSICIANS; MEMBERS: BTREE of TOENSEMBLEMEMBERS; WHOBY: BTREE of TOPERFORM ANCES end; COMPOSITIONS = record TITLE: STRING 12; CDATE: DATETYPE; COMPOSER: TOMUSICIANS; 1 Далее следует строка COMPOSER: BTREE of TOCOMPOSITIONS; 264
PERFORMED: BTREE of TOPERFORMANCES end; PERFORMANCES = record PDATE: DATETYPE; TOWN, COUNTRY: STRING 12; DIRECTOR: TOMUSICIANS; PERFORMED: TOCOMPOSITIONS; WHOBY: TOENSEMBLES end; var MUSNAMEINDEX = BTREE of MUSLEAF on MNAME; TITLEINDEX = BTREE of COMPOSITIONLEAF on TITLE; ENSEMBLEINDEX = BTREE of ENSEMBLESLEAF on ENAME; finish. Три используемых здесь индекса являются в действительности вторичными индексами, поскольку имена полей MNAME, TITLE и ENAME не уникальны. Можно найти в базе данных всех ком- позиторов, носящих одну и ту же фамилию. Для этого нужно сначала использовать вызов FINDKEY (MUSNAMEINDEX, MNAME, MUSLEAFPTR), а затем несколько раз вызов FINDSUCC (MUSNAMEINDEX, MUSLEAFPTR). Программы загрузки базы данных информацией похожи на аналогичные программы из предыдущего раздела. Поэтому при- водить их здесь нет необходимости. Будем считать, что база данных уже сформирована, т. е. данные в нее уже введены. Следующая программа позволяет определить фамилии всех членов ансамбля СИЗ (считается, что такое название имеет только один ансамбль): program PLAYSINCEES (OUTPUT); invoke MU; var ENSEMBLESLEAFPTR: f ENSEMBLESLEAF; ENSEMBLESPTR: TOENSEMBLES; MEMBERSPTR: TOENSEMBLEMEMBERS: begin STARTTRANSACTION (READONLY); FINDKEY (ENSEMBLEINDEX, ’THE CEES ’, ENSEMBLESLEAFPTR); if DBSTATUS < >OKAY then WRIETLN (’not found ) else begin ENSEMBLESPTR: = ENSEMBLESLEAFPTR f .POINTER; FINDFIRST (ENSEMBLESPTRf MEMBERS, MEMBERSPTR); 265
repeat WRITELN (MEMBERSPTRf.INSTRUMENTALISTf.PLAYSf.MNAME); FINDSUCC (ENSEMBLESPTRf .MEMBERS, MEMBERSPTR) until DBSTATUS < >OKAY end; ENDTRANSACTION end. Приводимая ниже программа позволяет определить названия всех ансамблей, которые исполняли хотя бы одно произведение композитора ЭЙЭФСКОГО (снова считается, что такую фамилию носит только один композитор): program HASPLAYEDAYEFSKY (OUTPUT); invoke MU; var MUSICIANSLEAFPTR: JMUSICIANSLEAF: MUSICIANSPTR: TOMUSICIANS; COMPOSITIONSPTR: TOCOMPOSITIONS: PERFORMANCESPTR: TOPERFORMANCES; begin STARTTRANSACTION (READONLY): FINDKEY (MUSNAMEINDEX, AYEFSKY \ MUSICIANSLEAFPTR); if DBSTATUS <> OKAY then WRITELN ( AYEFSKY not found ) else begin MUSICIANSPTR: = MUSICIANSLEAFPTR:.POINTER: FINDFIRST (MUSICIANSPTRf COMPOSER, COMPOSITIONSPTR); while DBSTATUS = OKAY do begin FINDFIRST (COMPOSITIONSPTRf PERFORMED, PERFORMANCESPTR); while DBSTATUS = OKAY do begin WRITELN (PERFORMANCESPTR:.WHOBY RENAME); FINDSUCC (COMPOSITIONSPTR:.PERFORMED, PERFORMANCESPTR) end; DBSTATUS: = OKAY; FINDSUCC (MUSICIANSPTRrCOMPOSER, COMPOSITIONSPTR) end; end; ENDTRANSACTION end. 266
Заметим, что программа может выводить на терминальное устройство название одного и того же ансамбля более одного раза. Существует довольно простой с точки зрения программирова- ния путь, позволяющий избавиться от этого недостатка. Он за- ключается в помещении названий в В-дерево, причем только тех, которых там еще нет. Использование процедуры FIND KEY позволит установить, есть ли в В-дереве данное название или нет. В конце работы программы названия ансамблей из В-дерева должны быть переданы на терминальное устройство. 8.3. УПРАЖНЕНИЯ 1. а. Для примера из разд. 3.10.2 нарисовать диаграмму Бахмана, включающую набор, который можно использовать для реализации операций соединения файлов МАТЧИ и СТАДИОНЫ. б. Составить алгоритм, использующий описанный в преды- дущем упражнении набор для получения ответа на запрос «опре- делить расстояние от Вигчестера до стадиона, на котором будет проводиться матч, назначенный на 1989 03 17». в. Составить алгоритм, использующий описанный в 1.а на- бор для получения ответа на запрос «определить, на какие числа назначены матчи на стадионах, которые вмещают более 50000 зрителей». 2. а. Для примера о лошадях, их владельцах и жокеях из разд. 3.10.3 нарисовать диаграмму Бахмана, включающую на- боры, связывающие владельцев с лошадьми (см. разд. 8.2.1) и лошадей с жокеями (для этого следует использовать элементы связи). б. Составить алгоритм, использующий описанную в преды- дущем упражнении базу данных для получения ответа на запрос «определить фамилии владельцев всех тех лошадей, на которых участвует в скачках жокей ХОБСОН». В этом и последующих упражнениях устранять возможность появления одних и тех же ответов не надо. в. Составить алгоритм, использующий описанную в п. 2. а базу данных для получения ответа на запрос «определить всех жокеев, участвующих в скачках на лошадях, принадлежащих владельцу, имеющему фамилию ДЖИСОН». 3. а. Для примера о библиотеке из разд. 3.10.2 нарисовать диаграмму Бахмана, включающую наборы. б. Составить алгоритм для чтения номера читательского би- лета с терминального устройства, определения фамилии читателя и выдачи на терминальное устройство для каждой книги, взятой этим читателем, названия книги, автора и названия отделения библиотеки, из которого книга была получена. в. Составить алгоритм для считывания с терминального уст- ройства фамилии автора и названия книги и определения фами- лий и номеров билетов тех читателей, которым эта книга выдана. 267
4. а. Для базы данных из упражнения 3 разд. 3.8 нарисо- вать диаграмму Бахмана. б. Написать DBD для этой базы данных. в. Написать программу на языке ЯМД Паскаль для загрузки данными, вводимыми с терминального устройства, этой базы. г. Написать программу на ЯМД Паскаль, с помощью которой осуществляется считывание с терминального устройства фами- лии посетителя и определение общей стоимости заказанных им напитков. В этом и следующем упражнениях считается, что в базе данных не может быть двух людей с одинаковыми фамилиями. д. Написать программу на ЯМД Паскаль для считывания с терминального устройства фамилии посетителя и определения названий заказанных им напитков и стоимости каждого из них. 5. Используя DBD MU базы данных из разд. 8.2.4, написать программы на ЯМД Паскаль, позволяющие: а. Определить названия и даты написания всех произведений композитора ЭЙЭФСКОГО. б. Определить названия всех ансамблей, имеющих в своем составе не менее трех исполнителей. в. Определить фамилии руководителей всех тех ансамблей, в состав которых входит не менее трех исполнителей. г. Определить названия всех ансамблей, в состав которых вхо- дит хотя бы один человек, написавший музыкальное произведение. д. Определить названия всех ансамблей, использующих сак- софон и кларнет. е. Определить фамилии всех музыкантов, не написавших ни одного произведения. ж. Определить фамилии всех музыкантов, которые либо на- писали музыкальные произведения, либо являлись дирижерами. з. Определить фамилии всех тех музыкантов, которые дири- жировали во время исполнения собственных произведений. и. Определить названия и фамилии композиторов всех тех произведений, которые исполнялись только по одному разу. к. Определить названия ансамблей, исполнивших все произ- ведения ЭЙЭФСКОГО. л. Определить названия всех произведений, исполняемых ансамблями, в составах которых есть саксофонист. 8.4. РАБОЧАЯ ГРУППА ПО БАЗАМ ДАННЫХ КОДАСИЛ 8.4.1. ВВЕДЕНИЕ Использование индексов, наборов и методов прямого доступа позволяет увеличить скорость выполнения операций выбора и соединения. В гл. 2 речь шла о составлении правильных с точки зрения реляционной алгебры выражений для ответов на 268
запросы и не затрагивались вопросы, связанные с организацией эффективных путей доступа к данным. Термин реляционная база данных будет обозначать совокупность файлов, организация до- ступа. к которым неизвестна программисту. Примерами реляцион- ных баз данных являются база данных из разд. 2.2 (ее диаграмма приводится на рис. 8.13) и набор файлов фирмы «Типико» из гл. 1. Все существующие реляционные базы данных снабжены языка- ми запросов. Этими языками можно пользоваться, не зная орга- низации доступа к данным. Наиболее простым примером языка запросов является язык реляционной алгебры. Опираясь на понятия, введенные в гл. 5, можно сказать, что язык реляционной алгебры является языком более высокого уровня, чем ЯМД Паскаль. Это объясняется тем, что на Паскале явно описываются способы организации доступа к данным, а в случае использования языка реляционной алгебры эти способы остаются скрытыми от программиста. В то же время ЯМД Па- скаль—язык значительно более высокого уровня, чем любой другой язык, не имеющий в своем составе таких встроенных про- цедур, как INSERT и FINDKEY. Важно, что действия, выполня- емые с помощью этих процедур, остаются скрытыми от програм- миста. Те же языки, для которых такого рода действия не выпол- няются с помощью специальных встроенных средств, а програм- мируются, являются языками более низкого уровня, чем ЯМД Паскаль. ЯМД Паскаль может быть использован для получения ответов на запросы, написанных на языке реляционной алгебры. И дейст- вительно многие упражнения из разд. 7.7 были связаны с постро- ением программ на Паскале, предназначенных для вычисления значений выражений реляционной алгебры. В свою очередь программы, написанные на Паскале, можно преобразовать в по- следовательность еще более простых инструкций (см. разд. 5.1). Одна из причин, определяющая, почему часто желательнее ис- пользовать ЯМД Паскаль, а не язык реляционной алгебры, за- ключается в том, что Паскаль позволяет лучше управлять про- , цессом поиска ответа на запрос, и в связи с этим его использование даст возможность найти ответ быстрее, чем в случае применения системы, предназначенной для непосредственной интерпретации запросов, написанных на языке реляционной алгебры. Другая причина состоит в том, что использование средств Паскаля поз- воляет выйти за пределы ограничений, упомянутых в разд. 2.17. Например, упражнения 5.с, 6.а, 6.6, 6.в можно выполнить с по- мощью Паскаля, но нельзя с помощью языка реляционной алгебры. Правда, язык реляционной алгебры можно расши- рить, но тогда возникнут определенные трудности при его ис- пользовании. ЯМД Паскаль довольно прост. Более сложный и в практиче- ском смысле более полезный ЯМД разработан КОДАСИЛ. 269
Одним из первых достижений КОДАСИЛ* явилась унификация КОБОЛА — языка, предназначенного для коммерческих прило- жений. После этого в рамках КОДАСИЛ была создана Рабочая группа по базам данных (DBTG). В 1971 г. эта группа выпустила важный отчет, описывающий средства баз данных. Последующие отчеты, в которых давались различные рекомендации и вносились исправления и добавления в существующие проекты, в результате вылились в новую кон- цепцию организации целого семейства баз данных, известного как СУБД КОДАСИЛ. В СУБД КОДАСИЛ используются наборы. Разница между ЯМД КОДАСИЛ и ЯМД Паскалем заключается в том, что про- граммисту, работающему с ЯМД КОДАСИЛ, не надо опе- рировать с указателями. Устранение указателей делает програм- мирование проще, а сам язык становится ближе к естественному английскому. Вместо указателей в системе КОДАСИЛ исполь- зуются индикаторы текущего состояния. Эти индикаторы играют роль указателей, но средств для управления ими у программиста нет. Индикатор текущего состояния показывает, какая запись в данный момент обрабатывается. В том случае, когда для про- граммирования наборов использовался Паскаль, указатели свя- зывались либо с записями определенного типа, либо с самими на- борами. Например, в программе ENTERDETAIL (см. разд. 8.2.2) указатель ITEMPTR был связан с файлом INVENTORY и исполь- зовался для ссылки на ту запись, которая в данный момент обра- батывалась, т. е. ITEMPTR выполнял функции индикатора те- кущего состояния. Аналогично в программе WHOOREREDITEM (см. разд. 8.2.2) указатель MEMBERPTR определял, какой член из набора INCLUDED в данный момент обрабатывается. В СУБД КОДАСИЛ автоматически генерируются индикаторы текущего состояния для всех типов записей и для наборов. 8.4.2. СХЕМЫ КОДАСИЛ Язык описания данных. Для того чтобы объявить базу данных КОДАСИЛ, необходимо задать схему. Схема — это объект, аналогичный DBD Паскаля. Схема напоминает не- сколько усовершенствованную декларативную часть программы, написанной на КОБОЛЕ; DBD, в свою очередь, напоминает дек- ларативную часть программы, написанной на Паскале. Язык, на . котором задается схема, известен как язык описания данных (ЯОД). Введение в языки ЯОД и ЯМД КОДАСИЛ занимает не менее ста страниц. Поэтому тех, кто хочет подробно познако- * CODASYL (КОДАСИЛ) — акроним Conference on Data Systems Lan- guages (Ассоциация по языкам систем обработки данных). — Прим. пер. 270
миться с этим материалом, отсылаем к соответствующим книгам *. Здесь будут разработаны только некоторые ключевые проблемы, а вопрос, связанный с подсхемами, будет рассматриваться в разд. 9.5. Объявление типов записей. Схема служит для объявления типов записей и наборов. Поля-указатели в наборах скрыты от программиста. Объявление типов записей хотя и напоминает объявление комбинированных типов в Паскале, но имеет отличи- тельные детали. К ним, в первую очередь, относится способ раз- мещения, который может быть CALC, VIA или DIRECT. Можно считать, что записи одного типа образуют файл; тогда способ раз- мещения определяет организацию этого файла. Способ размещения CALC означает, что рассматривается файл прямого доступа, обычно файл с хэш-адресацией. Тогда програм- мист должен определить поля, составляющие ключ, и указать, допустимы ли записи с одинаковыми ключами. CALC образован от «address CALCulation». Способ размещения VIA означает, что к записям файла можно организовать доступ только с помощью наборов. Имя одного такого набора должно быть определено. Например, в разд. 8.2.2. файл PERFORMERS доступен только с помощью набора PLAYS. Способ размещения DIRECT означает, что доступ возможен с помощью указателя, размещенного в определенном месте. Этот указатель считается ключом базы данных. Объявление наборов. В ЯМД Паскаль набор определяется так же, как любой комбинированный тип. В схеме КОДАСИЛ набор определяется, как тип, который имеет имя, а также порядок (ORDER), владельца (OWNER) и по крайней мере одного члена (MEMBER). ORDER означает порядок, в котором располагаются записи — члены набора. Программист может упорядочить члены по какому-то ключу или задать порядок расположения членов каким-то другим образом. Члены, имеющие одно и то же значение ключа сортировки, называются дубликатами. Если программист вводит ключ сортировки, он должен указать, допустимо ли на- личие дубликатов. Если допустимо, то необходимо указать, в на- чало или конец списка дубликатов вставляется очередной дубли- кат. Если ключ сортировки не задан, то программист должен определить, куда (в начало или конец набора, после или до те- кущего члена) помещать новые члены. Здесь термин «текущий» означает тот член, на который указывает индикатор текущего состояния набора. Если в ЯМД Паскаль для нонена вреди членов набора записи с заданным значением ключа используется процедура FIND KEY, то значение ключа может считаться ключом поиска. В схеме КО- * Можно в качестве книги, достаточной для первоначального знакомства с предметом рекомендовать Дж. Мартин «Организация баз данный в вычисли- тельных системах». М. Мир, 1985 г. — Прим. пер. ZT1
ДАСИЛ набор может иметь и ключ поиска и ключ сортировки, и это не одно и то же. Владельцем (OWNER), набора может быть либо система (SYSTEM), либо какой-нибудь тип записи. Если владельцам является система, то это означает, что набор исполь- зуется как индекс (аналогично индексу TITLE в разд. 8.2.2). При объявлении набора можно определить, каким образом будет осуществляться поиск записи владельца в том случае, если ис- пользуются процедуры ЯМД. В схеме кроме определения вла- дельца каждого набора указывается и тип записи по крайней мере одного его члена. Член может быть объявлен либо MANDATORY, либо OPTI- NAL. MANDATORY означает, что запись-член, помещенная в набор, не может быть удалена из него с помощью программы, написанной на ЯМД. OPTIONAL означает, что запись-член может быть исключена из набора. Член может также быть объявлен либо AUTOMATIC, либо MANUAL. AUTOMATIC означает, что, когда новая запись-член создается, она автоматически включается в набор. MANUAL означает, что включение не производится автоматически, а в слу- чае необходимости может быть осуществлено программой на ЯМД. В СУБД КОДАСИЛ реализация наборов производится либо с помощью колец, либо с помощью В-деревьев. Человек, составляю- щий схему, не может по своему желанию выбрать ту или иную реализацию, поскольку в ЯОД и ЯМД КОДАСИЛ реализация скрыта от пользователя. Проектирование базы данных КОДАСИЛ. Так же как DBD Па- скаля, схема КОДАСИЛ описывает проект базы данных. Про- цесс проектирования, в результате которого создается схема, похож на процедуру формирования DBD Паскаля, осуществляе- мую с использованием наборов (см. разд. 8.2.2). Процесс проекти- рования как схемы, так и DBD Паскаля состоит из следующих основных шагов. 1. Сформировать нормализованные файлы (см. разд. 3.11). Проверить, содержат ли эти файлы всю необходимую информа- цию. Составить диаграмму Бахмана, включающую только файлы. 2. На диаграмме Бахмана рядом в каждым файлом, к запи- сям которого потребуется произвольный доступ, поставить CALC. 3. Ввести наборы, необходимые для реализации операций соединения. Включить их в диаграмму Бахмана. 4. Ввести вторичные индексы для всех вторичных ключей, по которым часто предполагается осуществлять доступ. Отразить эти индексы на диаграмме Бахмана. 5. Проанализировав диаграмму Бахмана и структуру файлов, сформированных на шаге 1, составить подробную схему или DBD Паскаля. Рассмотрим пример проектирования базы данных, связанной с рекламными объявлениями. Торговые компании заключают 272
контракты с рекламными агентствами на подготовку рекламных объявлений и публикаций их в журналах и газетах. Реклам- ные агентства, в свою очерень, заключают договоры с художни- ками на создание графических проектов рекламных объявлений. В базе данных содержатся фамилии (или названия) и адреса ху- дожников, агентств, издателей журналов и компаний, торгующих рекламируемой продукцией. Кроме того, в базе находятся данные, характеризующие контракты, заключенные между компаниями и агентствами, а также между агентствами и художниками. Эти данные включают дату заключения контракта, дату завершения работы и некоторые другие сведения. В случае контрактов между компаниями и агентствами н этим сведениями относятся: шифр товара, описание товара, тип рынка (потребительский, коммер- ческая электроника, коммерческий транспорт) и платежное со- глашение, а в случае контрактов между агентствами и худож- никами — номер контракта, заключенного между агентством и компанией, размеры рекламного объявления, возможные цвета, специальный код, характеризующий требуемый стиль графиче- ского проекта и гонорар художника. Каждое рекламное объявление может быть опубликовано один или несколько раз в одном или более журналах. Для каждой публикации важны следующие данные: шифр журнала, дата и цена публикации. Кроме этих данных запоминаются название журнала, периодичность выхода номеров в свет и фамилия изда- теля. Одному издателю могут принадлежать сразу несколько журналов. Проанализировав объекты, отношения и атрибуты, можно сделать вывод, что приводимая ниже совокупность файлов доста- точна для представления реляционной базы данных: КОМПАНИЯ (COMPANY) ШИФР. КОМПАНИЯ НАЗВАНИЕ. КОМПАНИИ АДРЕС. КОМПАНИИ АГЕНТСТВО (AGENCY) ШИФР. АГЕНТСТВА НАЗВАНИЕ. АГЕНТСТВА АДРЕС. АГЕНТСТВА ХУДОЖНИК (ARTIST) ШИФР. ХУДОЖНИКА ФАМИЛИЯ. ХУДОЖ- НИКА АДРЕС. ХУДОЖНИКА ИЗДАТЕЛЬ (PUBLISHER) ШИФР. ИЗДАТЕЛЯ ФАМИЛИЯ-ИЗДА- ТЕЛЯ АДРЕС. ИЗДАТЕЛЯ ТОВАР (PRODUCT) ШИФР. ТОВАРА {считается уникальным) ШИФР. КОМПАНИИ {имеется в виду торговая компания) НАЗВАНИЕ КАТЕГОРИЯ. РЫНКА ШИФР. ЖУРНАЛА НАЗВАНИЕ. ЖУРНАЛА ШИФР. ИЗДАТЕЛЯ ПЕРИОДИЧНОСТЬ ЖУРНАЛ (JOURNAL) !для идентификации издателя) издания) 273
КОНТРА КТ, С_ АГЕНТСТВОМ (AGENCYCONTRACT) ШИФР. КОНТРАКТА. С. АГЕНТОМ ШИФР.ТОВАРА ДАТА. ЗАКЛЮЧЕНИЯ. КОН- ТРАКТА ДАТА. ЗАВЕРШЕНИЯ- РАБОТ ШИФР. АГЕНТСТВА ЦЕНА. КОНТРАКТА РЕКЛАМНОЕ.ОБЕСПЕЧЕНИЕ (ADVERTISEMENT) ШИФР. ОБЪЯВЛЕНИЯ ШИФР. КОНТРАКТА КОНТРАКТ-С. ХУДОЖНИКОМ (ARTISTCONTRACT) ШИФР. КОНТРАКТА ШИФР. КОНТРАКТА.. С. АГЕНТОМ ДАТА. ЗАКЛЮЧЕНИЯ. КОН- ТРАКТА ДАТА. ЗАВЕРШЕНИЯ- РАБОТ РАЗМЕРЫ ‘ ЦВЕТА СТИЛЬ ШИФР. ХУДОЖНИКА ГОНОРАР ПУБЛИКАЦИИ (PLACEMENT) ШИФР. ОБЪЯВЛЕНИЯ ШИФР. ЖУРНАЛА ДАТА ЦЕНА Теперь довольно просто построить для базы данных диаг- рамму Бахмана. Каждому файлу будет соответствовать прямо- угольный блок, а линии (наборы) будут связывать файлы, имею- щие общие поля. Напомним, что наборы реализуют отношения «один ко многим». Например, издатель обычно владеет несколь- кими журналами, а артист имеет несколько контрактов. Один контракт может включать более одного рекламного объявления, каждое из которых может быть несколько раз опубликовано. В диаграмму Бахмана, изображенную на рис. 8.14, включен набор, связывающий файлы ТОВАР и РЕКЛАМНОЕ.ОБЪЯВ- ЛЕНИЕ. Этот набор позволяет определить все рекламные объяв- ления, относящиеся к любому заданному товару. В реляционной базе такой доступ будет реализован с помощью цепочки соеди- нений ТОВАР'а с КОНТРАКТ.С.АГЕНТСТВОМ, результата с КОНТРАКТ.С.АРТИСТОМ, результата с РЕКЛАМНОЕ.ОБЪ- ЯВЛЕНИЕ. Можно попытаться упростить доступ, включив поле ШИФР.ТОВАРА в файл РЕКЛАМНОЕ.ОБЪЯВЛЕНИЕ. Но тогда этот файл не будет третьей нормальной формой, поскольку ШИФР.ТОВАРА будет функционально зависеть от значения неключевого поля ШИФР. КОНТРАКТА, хотя реально это может быть и не так. Поле НАЗВАНИЕ не может быть ключом файла ТОВАР, поскольку несколько разных товаров могут иметь одно и то же название, например МИКРОВОЛНОВАЯ. ПЕЧЬ. Для того чтобы облегчить доступ к записям файла РЕК- ЛАМНОЕ. ОБЪЯВЛЕНИЕ для всех товаров, имеющих одина- ковое название, можно снабдить файл ТОВАР вторичным ин- дексом по полю НАЗВАНИЕ. С другой стороны, вместо создания этого индекса можно просто сделать ТОВАР файлом с хэш-адре- сацией, причем в качестве ключа использовать НАЗВАНИЕ. Естественно, что в этом случае надо бвггь готовым к тому, что у разных записей могут бвггь одинаковые значения поля НАЗВА- НИЕ. 274
Рис. 8.14. Диаграмма Бахмана базы данных о рекламных объявлениях Предпочтение было отдано второму пути, поэтому ТОВАР на рис. 8.14 помечен CALC. Файл ЖУРНАЛ также сделан фай- лом с произвольным доступом, поскольку часто требуется обра- щаться к записям этого файла, минуя файл ИЗДАТЕЛЬ. ПУБЛИКАЦИЯ является промежуточным файлом в связи с тем, что доступ к нему будет осуществляться только через файлы РЕКЛАМНОЕ.ОБЪЯВЛЕНИЕ и ЖУРНАЛ. Иногда может не понадобиться организовывать прямой доступ к файлу КОНТ- РАКТ_С_ ХУДОЖНИКОМ, что потребует знания значений пер- вичного ключа. Но в базе данных КОДАСИЛ для файла КОНТ- РАКТ-С- ХУДОЖНИКОМ не обязательно предусматривать поле ШИФР. КОНТРАКТА. Если это поле опускается, то прямой дос- туп будет невозможен. Поэтому файл КОНТРАКТ.С. ХУДОЖ- НИКОМ на рис. 8.14 не имеет атрибута CALC. Заметим, что вопрос, связанный со снабжением файлов вто- ричными индексами, здесь не рассматривался. 8.4.3. ЯМД КОДАСИЛ В состав ЯМД КОДАСИЛ входят процедуры, анало- гичные CREATE, DELETE, REMOVE, INSERT и FIND ЯМД Паскаль. Кроме того, ЯМД КОДАСИЛ имеет и другие средства. Останавливаться на синтаксисе ЯМД КОДАСИЛ, пожалуй, не имеет смысла. Действие процедуры FIND ЯМД Паскаль заключается в при- сваивании значения ссылочной переменной. В ЯМД КОДАСИЛ выполнение процедуры FIND приводит к присваиванию значений одному или нескольким индикаторам текущего состояния, свя- занным с одним типом записи и наборами. Рассмотрим базу данных КОДАСИЛ примера о товарных запасах из разд. 8.2.2. Приме- нение процедуры FIND для поиска записи файла INVENTORY приведет к тому, что индикатор текущего состояния файла INVENTORY будет указывать на найденную запись. Вследствие этого индикатор текущего состояния набора INCLUDES будет указывать на ту же самую запись. И наконец, результатом ис- пользования процедуры FIND будет присваивание индикатору 275
текущего состояния процесса значения, указывающего на най- денную запись. Этот индикатор ссылается на запись, которая была найдена последней, вне зависимости от того, какому она принадлежит типу. Использование процедуры FIND для поиска записи файла SALESORDERDETAIL приведет к следующим последствиям: индикатор текущего состояния для типа записи SALESOR- DERDETAIL будет указывать на найденную запись; индикатор текущего состояния для набора INCLUDES будет указывать на найденную запись; индикатор текущего состояния процесса будет указывать на найденную запись. Индикатор текущего состояния для типа записи INVENTORY останется неизменным, т. е. будет указывать на ту запись INVEN- TORY, которая была найдена последней. В результате можно сделать следующие выводы. Индикатор текущего состояния каждого типа записи указывает на запись этого типа, которая была найдена последней. Индикатор теку- щего состояния каждого набора указывает на найденную послед- ней запись, которая является либо владельцем, либо членом этого набора. Индикатор текущего состояния процесса меняется в ре- зультате выполнения любой процедуры FIND, в то время как индикаторы типов записи и наборов могут остаться неизменными. На ЯМД Паскаль операции присваивания, включающие пере- менные, сохраняющие свои значения, имеют такой же вид, как и на обычном Паскале. На ЯМД КОДАСИЛ доступ к переменным, сохраняющим свои значения, возможен только с помощью специ- альных переменных, называемых переменными рабочей области пользователя. Эти переменные, которые программисту не надо объявлять, относятся к переменным, не сохраняющим свои значения. Для каждого типа записи в схеме существует переменная рабочей об- ласти пользователя. Эта переменная относится к тому же типу, и ее имя совпадает с именем типа. Если в программе на ЯМД для поиска записи INVENTORY в базе данных используется процедура FIND, то затем можно применять процедуру GET для присваивания переменной рабочей области пользователя INVENTORY значения той записи базы данных, к которой ин- дикатор текущего состояния INVENTORY ссылается. После этого программист может выдать значение переменной рабочей области пользователя INVENTORY на терминальное устройство. Единственным путем передачи записей из базы данных на терми- нальное устройство является использование переменных рабочей области пользователя. Справедливое и обратное, т. е. единствен- ный способ передачи информации с терминального устройства в базу данных основан на использовании переменных рабочей об- ласти пользователя. Ниже приведем алгоритм, характеризующий работу программы, написанной на ЯМД и предназначенной для 276
формирования ответа на запрос «определить названия и адреса всех тех компаний, которые продают товар, называемый 'СИГАРЫ'. Дадим имя КОМПАНИЯ-ТОВАРЫ (COMPANYPRODUCTS) набору, связывающему файл КОМПАНИЯ с файлом ТОВАР. PRODUCT.DESCRIPTION: = 'СИГАРЫ'; {для типа за- писи PRODUCT (ТОВАР) существует переменная рабочей области пользователя с именем PRODUCT. Значением поля DESCRIPTION (название) этой переменной становится строка 'СИГАРЫ'.} FIND RECORD PRODUCT; {используя метод хэширова- ния, в базе данных с помощью процедуры FIND ищется первый экземпляр записи PRODUCT, поле DESCRIPTION (НАЗВА- НИЕ) которого имеет значение 'СИГАРЫ'. В результате выполнения процедуры FIND на найденный экземпляр записи будут ссылаться индикатор текущего состояния PRODUCT, индикатор текущего состояния набора COMPANYPRODUCTS и индикатор текущего состояния процесса.} while DBSTATUS = OKAY do begin FIND OWNER IN COMPANYPRODUCTS OF CURRENT OF RECORD PRODUCT; {это предложение написано на ЯМД КОДАСИЛ и близко к обычному английскому. В результате выполнения этого предложения индикатор текущего состоя- ния процесса будет ссылаться на экземпляр записи COMPANY (КОМПАНИЯ) (его значение будет использовано в следующем предложении). Кроме того, индикатор текущего состояния набора COMPANYPRODUCTS (КОМПАНИЯ-ТОВАРЫ) бу- дет указывать на тот же самый экземпляр записи COMPANY.} GET COMPNAME, COMPADDRESS; {передача из базы данных полей COMPNAME (НАЗВАНИЕ, КОМПАНИИ) и COMP ADDRESS (АДРЕС-КОМПАНИИ) того экземпляра за- писи COMPANY, на который указывает индикатор текущего состояния процесса, полям переменной рабочей области поль- зователя COMPANY.) WRITELN (COMPANY.COMPNAME, COMPANY.COM- PADDRESS); FIND NEXT DUPLICATE WITHIN RECORD PRODUCT {это предложение написано на ЯМД КОДАСИЛ. В результате его выполнения индикатор текущего состояния PRODUCT будет указывать на следующий экземпляр записи PRODUCT, для которого DESCRIPTION ='СИГАРЫ'. Если такой записи нет, то DBSTATUS присваивается значение, отличное от OKAY.) end *. * Отметим, что этому алгоритму придана структура программы, написан- ной на Паскале. Вместе с тем в него входят предложения, написанные на ЯМД. — Прим. пер. 277
Рис. 8.15. Диаграмма Бахмана базы данных для системы КО- ДАСИЛ Отметим, что ЯМД КОДАСИЛ — язык более высокого уровня, чем ЯМД Паскаль; кроме того, ЯМД КОДАСИЛ близок к есте- ственному английскому языку. Правда, требования, которые дол- жен выполнять программист, работая на ЯМД КОДАСИЛ, жестче и перечень возможных ошибок, обнаруживаемых системой, шире. Для изучения ЯМД КОДАСИЛ проще, чем ЯМД Паскаль, поскольку он лаконичнее и легче для понимания. 8.5. СУБД TOTAL Основным недостатком СУБД КОДАСИЛ является ее сложность. Поэтому для широкого применения были разработаны другие более простые системы. Примером одной из таких систем является TOTAL — СУБД, которая, как и КОДАСИЛ, исполь- зует наборы. В СУБД КОДАСИЛ можно объявить файлы с хэш-адресацией. Тогда программист может использовать процедуру ЯМД FIND для поиска записи с заданными значениями ключа. В ЯМД Па- скаль, таких средств для файлов с хэш-адресацией нет. Если программист хочет воспользоваться файлами с хэш-адресацией, то он должен сам написать все необходимые процедуры для работы с ними. В результате в распоряжении пользователя будут два семейства процедур: одно для работы с файлами с хэш-адресацией и другое — для работы с наборами. * В СУБД TOTAL реализо- вано оба этих семейства. В TOTAL считается, что каждый файл может быть либо владельцем, либо членом. Файлы-владельцы являются хэш-файлами. В них не может быть двух одинаковых значений ключей. Доступ к записям файлов-членов может быть организован только через наборы, реализованные с помощью колец. Поскольку механизмы доступа, ориентированные на файлы с хэш-адресацией и на наборы, различны, одно семейство проце- дур предназначено для работы с файлами-владельцами, а другое— с файлами-членами. На диаграмме Бахмана (рис. 8.15) файл SALESORDERHEA- DER член набора, владельцем которого является файл CUSTO- MERS, т. е. считается, что один клиент может сделать несколько заказов. В то же время файл SALESORDERHEADER является владельцем набора, членами которого являются записи файла * Имеется в виду семейство процедур, введенное в разд. 6.8.8 и предназна- ченное для работы с В-деревьями. Как эти процедуры могут быть использованы для работы с наборами показано в разд. 8.2.2. — Прим. пер. 278
Рис. 8.16. Диаграмма Бахмана базы данных для системы TOTAL SALESORDERDETALL. Таким образом, файл SALESORDER- HEADER является владельцем одного набора и членом другого. В СУБД TOTAL такие структуры данных представлены быть не могут. Поэтому изменим структуру и приведем ее к виду, изоб- раженному на рис. 8.16. Такого рода преобразования всегда мо- гут быть выполнены. Преобразуем структуру базы данных из разд. 8.2.2 (см. рис. 8.13) к виду, который можно реализовать в системе TOTAL (рис. 8.17). Отметим, что в этой диаграмме допускается исполнение во время одного выступления нескольких произведений. MNO, PNO, ENO и CNO имеют тот же смысл, что и в разд. 2.2. MUSI- CIANNAME может рассматриваться как индекс с хэш-адреса- цией, состоящий из неповторяющихся фамилий. Элемент связи MUSICIANNAMETONUMBER позволяет поставить в соответ- ствие одной фамилии более одного MNO. Аналогично, поскольку в СУБД TOTAL не разрешено использовать одинаковые значения ключей для записей файлов-владельцев, файл COMPOSITION- TITLE содержит каждое название только по одному разу. Эле- мент связи TITLETOCOMPOSITIONS соединяет каждое название со всеми записями СЬЮ и DATEOFCOMPOSITIONS, которые относятся к произведению с этим названием. Файл PERFOR- Рис. 8.17. Диаграмма Бахмана музыкальной базы данных для системы TOTAL 279 L
MANCENUMBER включен в базу данных и сделан владельцем, поскольку файл PERFORMANCEDATE и PLACE не может являться владельцем. Недостатком СУБД TOTAL является то, что для ответа на запрос типа «определить фамилии всех членов ансамбля СИЗ» необходимо просмотреть гораздо больше наборов, чем при ис- пользовании для этой же цели СУБД КОДАСИЛ. 8.6. СУБД ADABAS В разд. 7.5 было показано, что если указатели-члены вторичных индексов не включены в файлы, к записям которых они ссылаются, а хранятся отдельно, то логические связки AND и OR в операциях выбора можно реализовать с помощью операций пересечения и объединения, проводимых над множествами членов. Полученные в результате выполнения этих операций члены-указа- тели непосредственно ссылаются к искомым записям, т. е. до- полнительно не расходуется время на доступ к тем записям, дан- ные из которых не нужны. Если записи данных состоят из большого числа полей, уда- ется поместить в блок только несколько записей. В этом случае доступ к записям, не содержащим искомых данных, часто приво- дит к необходимости организации доступа к блокам, в которых вообще нет искомых записей, а это существенно снижает эффек- тивность работы СУБД. В отличие от записей данных записи ин- дексов состоят из ключей и указателей. Поэтому таких записей можно поместить в блок достаточно много. Если реализация AND и OR с помощью операций пересечения и объединения, выполняе- мых над множествами членов-указателей, происходит до начала доступа к блокам данных, то общее число обращений к блокам уменьшается и эффективность доступа растет. ADABAS — это СУБД, в которой индексы (включая и меж- файловые индексы) с целью повышения эффективности доступа хранятся отдельно от файлов данных. В этом отношении ADABAS кардинально отличается от КОДАСИЛ и TOTAL, в которых ука- затели, связанные с наборами, хранятся (или по крайней мере могут храниться) непосредственно в файлах данных. Тогда при организации доступа к наборам требуется обращение к записям данных. Поскольку доступ в ADABAS организован так, чтобы не затрагивать запивей данных, использование колец и наборов в этой системе невозможно. В еамом деле в ЯМД Паскаль при реализации связей между данными не обязательно пользоваться кольцами и наборами. Если наборы и использовались раньше, то только для того, чтобы обеспечить естественный переход к СУБД КОДАСИЛ и TOTAL. Если не обязательно реализовывать межфайловые индексы с помощью колец, то для отношений «многие по многим» не пот- ребуется вводить элементы связи (см. разд. 8.2.2). Продемонст- 280
рируем использование межфайловых индексов, хранящихся от- дельно от файлов данных, на следующем примере. Рассмотрим два файла данных STOWNS и SPEOPLE. Предположим, что они состоят из следующих записей: STOWNS (файл данных) НАЗВАНИЕ ГОРОДА (TOWNNAME) R1: ЕЙТОН R2: СИТОН R3: ДИТОН R4: ЭФТОН R5: ДЖИТОН СТРАНА (COUNTRY) ЭКСЛАНДИЯ ЭКСЛАНДИЯ ЗЕДЛАНДИЯ ВАЙЛАНДИЯ ЗЕДЛАНДИЯ SPEOPLE (файл данных) ФАМИЛИЯ (PERSONNAME) S1: УИЛ БИ S2: НАЙТ S3: СМИТ S4: ДЖОНС S5: ЯНГ ПРОФЕССИЯ (PROFESSION) ПРОГРАММИСТ ИНЖЕНЕР ПРОГРАММИСТ БУХГАЛТЕР ИНЖЕНЕР Содержимое файла STOWNS позволяет определить, в какой стране находится каждый город. Для простоты будем считать, что не может быть двух городов с одинаковыми названиями. Содержимое файла SPEOPLE позволяет установить профессию каждого человека; также будем считать, что не может быть двух людей с одинаковыми фамилиями. Отметим, что в файлах STOWNS и SPEOPLE не присутствуют идентификаторы R1, ..., R5 и S1, ..., S5. Они играют роль индексов таблиц, в которых находятся указатели к записям данных. Так, индекс R1 указывает на за- пись ЕЙТОН, S2 — на запись НАЙТ и т. д. Эти идентификаторы являются членами следующих вторичных индексов: СТРАНА (вторичный индекс) ПРОФЕССИЯ (вторичный индекс) (COUNTRY) ВЛАДЕЛЕЦ ЧЛЕНЫ (PROFESSION) ВЛАДЕЛЕЦ ЧЛЕНЫ ЭКСЛАНДИЯ Rl, R2 БУХГАЛТЕР S4 УАЙЛАНДИЯ R4 ИНЖЕНЕР S2, S5 ЗЕДЛАНДИЯ R3, R5 ПРОГРАММИСТ S1, S3 Также потребуются плотные первичные индексы: ГОРОДА (первичный индекс) (TOWNS) ВЛАДЕЛЕЦ ЧЛЕН ЭЙТОН R1 ДИТОН R3 ЭФТОН R4 ДЖИТОН R5 СИТОН R2 ЛЮДИ (первичный индекс) (PEOPLE) ВЛАДЕЛЕЦ ЧЛЕН ДЖОНС S4 НАЙТ S2 СМИТ S3 УИЛБИ S1 янг S5 В разд. 7.6.1. файлы данных содержали записи, расположенные в случайном порядке, в то время как первичные индексы были отсортированы по первичному ключу. Это позволяло включать и удалять записи данных, не производя перемещений других записей. Приводимые ниже межфайловые индексы показывают: пер- вый — тех, кто посетил тот или иной город; второй — какие го- 281
рода посещались тем или иным человеком. Отметим, что между городами и людьми существует отношение «многие по многим». Например, город СИТОН посетили несколько человек, а СМИТ побывал в нескольких городах. ГОРОДА. ЛЮДИ (межфайловый индекс) ЛЮДИ, ГОРОДА (межфайловый индекс) (TOWNBYPEOPLE) владелец члены (PEOPLETOTOWN) ВЛАДЕЛЕЦ ЧЛЕНЫ R1 S3 S1 R2 R2 S, S3 S2 R3, R5 R3 S2 S3 Rl, R4, R5 R4 S3, S4 S4 R4 R5 S2, S3 S5 R2 Содержимое первого из этих индексов полностью определяется содержимым второго и наоборот. Такая избыточность характерна для межфайловых индексов. Пример из разд. 8.2.2 подтверждает это. Чтобы определить всех тех, кто посетил город СИТОН, не- обходимо найти СИТОН в первичном индексе TOWNS * и уста- новить, что ему соответствует член R2. После этого надо найти в межфайловом индексе TOWNSBYPEOPLE и установить, что СИТОН посетили УИЛБИ и ЯНГ. В межфайловый индекс PEOPLETOTOWN в качестве вла- дельцев входят указатели S1, ..., S5. Эти указатели используются вместо фамилий, что позволяет проводить операции объединения и пересечения над индексами, не обращаясь к записям файла данных. Рассмотрим пример. Предположим, что необходимо определить имена всех инженеров, посетивших хотя бы один город в стране ЗЕДЛАНДИЯ. Для этого сначала находим ЗЕДЛАНДИЯ во вторичном индексе COUNTRY. Членами для владельца ЗЕДЛАН- ДИЯ являются R3 и R5. Далее находим в межфайловом индексе TOWNSBYPEOPLE, во-первых, владельца R3 и множество его членов {S2} и, во-вторых, владельца R5 и множество его членов {S2.S3}. После этого осуществляем операцию объединения двух множеств и получаем {S2.S3} множество, состоящее из указателей всех людей, посетивших хотя бы один город в ЗЕДЛАНДИИ. Затем ищем во вторичном индексе PROFESSION владельца ИН- ЖЕНЕР и определяем множество его членов |S2,S5}. Для того чтобы найти фамилии всех инженеров, посетивших хотя бы один горрд в ЗЕДЛАНДИИ, определяем результат пересечения {S2.S5} с {S2.S3}. Результат {S2} говорит о том, что необходимо организовать доступ к записи данных S2. Делаем это и получаем ответ НАЙТ. Очень важно, что для получения ответа не пришлось обращаться к тем записям данных, которые не являются иско- * Здесь и далее имена всех файлов, записей, полей и индексов будут даваться на английском языке, поскольку большинство из них именно в таком виде при- сутствует в программах на Паскале. — Прим. ред. 282
мыми. Если бы ключом межфайлового индекса TOWNSBYPEOPLE было поле TOWNNAME, необходимо было бы организовывать доступ к записям данных, относящимся к городам, которые на- ходятся в ЗЕДЛАНДИИ, поскольку для работы с межфайловым индексом были бы нужны названия городов. Тогда пришлось бы обращаться к нескольким записям файла STOWNS, которые не нужны для ответа на запрос. Рассмотрим этот пример подробнее. Записи файлов STOWNS и SPEOPLE будет хранить в таблицах. Тогда DBD будет иметь следующий вид: DBDNAME VT; type PESUBSCRIPT» 1.100; {subscript range for SPEOPLE} TNSUBSCRIPT = 1 ..50; {subscript range for STOWNS} STRING12 = packed array [1..12] of CHAR; PERSON = record PERSONNAME, PROFESSION: STRING 12 end; TOWN = record TOWNNAME, COUNTRY: STRING 12 end; PERSONLEAF = record PERSONNAME: STR1NG12; WHO: PESUBSCRIPT end; TOWNLEAF = record TOWNNAME: STRING12; WHICH: TNSUBSCRIPT end; PERSONMEMBERS = BTREE of PESUBSCRIPT; TOWNMEMBERS = BTREE of TNSUBSCRIPT; COUNTRYLEAF « record COUNTRY: STRING 12; MEMBERS: TOWNMEMBERS end; PROFESSIONLEAF = record PROFESSION: STRING12; MEMBERS: PERSONMEMBERS end; TPLEAF = record WHICH: TNSUBSCRIPT; MEMBERS: PERSONMEMBERS end; PTLEAF = record WHO: PESUBSCRIPT; MEMBERS: TOWNMEMBERS end; var STOWNS: array [TNSUBSCRIPT] of TOWN; SPEOPLE: array [PESUBSCRIPT] of PERSON; TOWNPRIMARY: BTREE of TOWNLEAF on TOWNNAME; 283
PERSONPRIMARY: BTREE of PERSONLEAF on PERSONNAME; COUNTRYSECONDARY: BTREE of COUNTRYLEAF on COUNTRY; PROFESSIONSECONDARY: BTREE of PROFESSIONLEAF on PROFESSION; TOWNSBYPEOPLE: BTREE of TPLEAF on TNSUBSCR1PT; PEOPLETOTOWNS: BTREE of PTLEAF on PESUBSCRIPT; finish Рассмотрим теперь процедуры, с помощью которых осущест- вляется выполнение операций пересечения и объединения. Пер- вая процедура позволяет включить в В-дерево записи С, которые находятся в В-деревьях А и В. Алгоритм основан на той же идее, что и программа НАРЗ из разд. 7.5.1: procedure INTERSECT (var А,В,С: PERSONMEMBERS); var APTR, BPTR: JPESUBSCRIPT; begin FINDFIRST (A,APTR); if DBSTATUS = OKAY then begin FINDFIRST (B.PBTR); while DBSTATUS = OKAY do begin if APTRf = BPTRf then begin INSERT (C, APTRt); FINDSUCC (A,APTR) end else if APTRf < BPTRT then FINDSUCC (A,APTR) else FINDSUCC (B,BPTR) end end; DBSTATUS = OKAY end; Эта программа осуществляет последовательный просмотр ли- стьев деревьев А и В. Если листья содержат одинаковую инфор- мацию, одно из них помещается в С. Если нет, то переходим к следующему листу того дерева, значение текущего листа которого меньше. Следующая процедура позволяет осуществить объединение листьев, принадлежащих деревьям А и В. Используемый для этого алгоритм похож на алгоритм, применяемый в программе слия- ния файлов из разд. 4.6. Чтобы в дереве С не было двух одинако- вых листьев, предусмотрены две проверки: BPTR | > APTR f и BTPR f < APTR f . 284
В принципе дубликаты можно было бы и разрешить: procedure UNION (var А,В,С: PERSONMEMBERS); var APTR, BPTR: JPESUBSCRIPT; YESA, YESB: BOOLEAN; procedure SOMELEFT (var YES: BOOLEAN); begin YES: = (DBSTATUS = OKAY); DBSTATUS: = OKAY end; begin {body of procedure UNION} 1 FINDFIRST (A,APTR); SOMELEFT (YESA); FINDFIRST (B,BPTR); SOMELEFT (YESB); while YESA and YESB do begin if BPTRt < APTRt then begin INSERT (C, BPTRt); FINDSUCC (B, BPTRf); SOMELEFT (YESB) end else begin if BPTRt > APTRt then INSERT (C, APTRt); FINDSUCC (A, APTRt); SOMELEFT (YESA) end end if YESA then repeat INSERT (C, APTRt); FINDSUCC (A,APTR) until DBSTATUS < >OKAY; if YESB then repeat INSERT (C, BPTRt); FINDSUCC (B,BPTR) until DBSTATUS < >OKAY end; Процедура SOMELEFT показывает, все листья В-дерева про- смотрены или нет. Включение очередного листа в В-дерево С происходит только тогда, когда BPTR f и APTR f не равны. Стандартная процедура FINDSUCC применяется к тому В- дереву, текущее значение листа которого в данный момент ме- ньше. В связи с этим листья в В-дереве получаются отсортиро- ванными. Используя описанные выше процедуры, следующая програм- ма, написанная на ЯМД Паскаль, считывает с терминального устройства название страны и профессию и определяет фамилии всех тех людей заданной профессии, которые хотя бы один раз посетили какой-нибудь город названной страны. Считается, что данные в базу уже загружены. 1 (* тело процедуры UNION ♦) 285
program PROFESSIONALVISITORS (INPUT, OUTPUT); invoke VT; var WHICHPROFESSION, WHATCOUNTRY: STRING 12; COUNTRYLEAFPTR. fCOUNTRYLEAF; PROFESSIONLEAFPTR: fPROFESSIONLEAF; POINTERTOTNSUBS. ^TNSUBSCRIPT; POINTERTOPESUBS: !PESUBSCRIPT; WHOWENT, PROFWHOWENT: PERSONMEMBERS; procedure UNION (var A,B,C: PERSONMEMBERS); begin {declaration omitted: see preceding textfend; procedure INTERSECTION (var A,B,C: PERSQNMEMBERS); begin {declaration omitted: see preceding textfend: begin {program body}^ WRITELN (’type 12-character name of profession and country’); READ (WHICHPROFESSION); GET (INPUT); READ (WHATCOUNTRY); STARTTRANSACTION (READONLY); FINDKEY (COUNTERSECONDARY, WHATCOUNTRY, COUNTRYLEAFPTR); if DBSTATUS < >OKAY then WRITELN (’no data for this country’) else begin FINDFIRST (COUNTRYLEAFPTR!.MEMBERS, POINTERTOTNSUBS); while DBSTATUS = OKAY do begin FINDKEY (TOWNBYPEOPLE, POINTERTOTNSUBS!, TPLEAFPTR); if DBSTATUS = OKAY then UNION (TPLEAFPTR!.MEMBERS, WHOWENT, WHOWENT) else DBSTATUS: = OKAY; FINDSUCC (COUNTRYLEAFPTR!.MEMBERS, POINTERTOTNSUBS) end; DBSTATUS: = OKAY; FINDKEY (PROFESSIONSECONDARY, WHICHPROFESSION, PROFESSIONLEAFPTR); if DBSTATUS <> OKAY then WRITELN (’none for this profession’) else begin INTERSECT (PROFESSIONLEAFPTR!.MEMBERS, WHOWENT, PROFWHOWENT); FINDFIRST (PROFWHOWENT. POINTERTOPESUBS); while DBSTATUS = OKAY do 1 (* объявление опущено *) 2 (* тело программы *) 286
begin WRITELN (SPEOPLE [POINTERTOPESUBSjJ.PERSONNAME); FINDSUCC (PROFWHOWENT, POINTERTOPESUBS); end end end; ENDTRANSACTION end. Эта программа работает так, как быДо описано в примере об инженерах, посетивших город в ЗЕДЛАНДИИ. В-деревья WHOWENT и PROFWHOWENT первоначально пусты. В первом цикле <while» используется процедура UNION, которая помещает в WHOWENT несколько указателей, ссылающихся ко всем запи- сям SPEOPLE, которые относятся к людям, посетившим задан- ную страну. Третья по порядку процедура FINDKEY позволяют получить члены-указатели индекса PROFESSIONSECONDARY, ссылающиеся ко всем записям SPEOPLE, которые относятся к людям заданной профессии. С помощью процедуры INTERSECT в В-дерево PROFWHOWENT помещаются указатели, ссылаю- щиеся ко всем людям заданной профессии, побывавшим хотя бы в одном городе заданной страны. Последний цикл <while» можно было бы объединить с процедурой INTERSECT, но чтобы не усло- жнять программу, этого не сделано. Язык запросов СУБД ADABAS включает ряд средств, позво- ляющих получать ответы на запросы, не прибегая к детальному описанию алгоритма, подобному только что приведенному. Кроме того, при использовании СУБД ADABAS не надо досконально знать структуру индексов. 8.7. РЕАЛИЗАЦИЯ СОЕДИНЕНИЙ С ПОМОЩЬЮ ВТОРИЧНЫХ ИНДЕКСОВ Ранее были рассмотрены методы реализации операций соединения с помощью различных межфайловых индексов. Не- достаток этих методов заключается в том, что необходимо заранее знать, над записями каких именно файлов будут проводиться опе- рации соединения. В гл. 2 для ответов на запросы использовалась реляционная алгебра. Это давало возможность проводить опера- ции соединения над файлами, имеющими по крайней мере одно общее поле. В то время взгляд на данные был чисто реляционным, т. е. файлы считались просто совокупностями записей и не рас- сматривались пути доступа к ним. Во время составления DBD не надо было знать, соединения каких именно файлов потребуются. Для того чтобы сохранить эту свободу и обеспечить высокую скорость реализации соединений, вместо межфайловых индексов можно использовать вторичные индексы. Покажем, как это де- лается на примере достаточно простого файла ЗАГОЛОВОК- 287
НАРЯДА, в каждую запись которого входят следующие поля: Я ШИФР.НАРЯДА. ШИФР-КЛИЕНТА и ДАТА. Будем также ] использовать упрощенный файл КЛИЕНТЫ, каждая запись i которого включает поля: ШИФР-КЛИЕНТА, ФАМИЛИЯ, ЛИ- ! МИТ. СОДЕРЖАНИЕ.НАРЯДА и КЛИЕНТЫ — файлы с хэш- 1 адресацией; кроме того, они обеспечены вторичными индексами ] по всем неключевым полям. Определим значение выражения 1 proj НОМЕР. НАРЯДА (ЗАГОЛОВОК-НАРЯДА-join sei 1 ФАМИЛИЯ-'ХОБСОН' (КЛИЕНТЫ)) Сначала для получения sei ФАМИЛИЯ = 'ХОБСОН' (КЛИ- ] ЕНТЫ) можно использовать вторичный индекс по полю ФАМИ- 1 ЛИЯ и для каждой из выбранных записей найти запись вторич- я ного индекса по полю ШИФР-КЛИЕНТА для файла ЗАГОЛО- | ВОК-НАРЯДА. Записи, которые затем извлекаются из файла 1 ЗАГОЛОВОК-НАРЯДА, и будут искомыми, т. е. содержащими I нужное значение поля ШИФР-КЛИЕНТА. I Только что описанный метод обычно работает дольше тех 1 методов, в которых используются межфайловые индексы, по- I скольку часть времени расходуется на доступ к владельцам во ] вторичных индексах. Например, время, затрачиваемое на поиск I значений поля ШИФР. КЛИЕНТА во вторичном индексе файла I ЗАГОЛОВОК-НАРЯДА, может превосходить время, необходи- 1 мое для поиска членов ЗАГОЛОВОК-НАРЯДА в наборах, вла- 1 дельцами которых являются выбранные клиенты. I Если воспользоваться термином, введенным в разд. 7.2.3, 1 то наборы можно считать прямыми индексами. В то же время ме- 1 тод, описанный в настоящем разделе, является методом косвен- I ной индексации, поскольку он предусматривает поиск значений 1 ключа вместо прямого следования по указателям. | 8.8. УПРАЖНЕНИЯ 1. а. Нарисуйте диаграмму Бахмана базы данных КО- ДАСИЛ для примера о посещениях различных городов разными людьми (см. разд. 8.6). Эта диаграмма должна быть такой же, как и диаграмма той же самой базы данных, описанной на ЯМД Паскаль с использованием наборов. б. Составьте алгоритм, позволяющий, используя эту базу данных, ответить на запрос «определить фамилии всех тех лю- дей, которые посетили хотя бы один город в ЗЕДЛАНДИИ». Не заботьтесь об устранении дубликатов. 2. Для упражнения 1 из разд. 3.11 выполните следующее: а. Нарисуйте диаграмму Бахмана структуры базы данных для системы КОДАСИЛ. Рядом с каждым файлом с хэш-адресацией на 288
диаграмме Бахмана поставьте CALC, а рядом в каждым файлом, доступным только с помощью по крайней мере одного набора, — VIA. б. Нарисуйте диаграмму Бахмана структуры базы данных для системы TOTAL. Составьте алгоритм, описывающий работу программы на ЯМД, которая позволяет ответить на запрос «опре- делить фамилии владельцев всех лошадей, на которых выступает жокей Хобсон». 3. Для каждого блюда в базе данных хранятся его название (например, РОСТБИФ) и список ингредиентов. Для каждого ингредиента, в свою очередь, необходимо знать, какое его ко- личество потребуется для приготовления различных блюд и еди- ницу измерения этого количества (например, килограмм, литр). Ингредиент обычно используется для приготовления нескольких блюд. Кроме того, известны запасы каждого ингредиента, единицы измерения и возможная цена. Для каждой даты — отдельно для завтрака и обеда — хранится общее число клиентов, число кли- ентов, заказавших то или иное блюдо из меню, и цена каждого блюда. Обычно меню каждый раз разное. а. Нарисуйте диаграмму Бахмана этой базы данных для си- стемы КОДАСИЛ. Файлы с хэш-адресацией обозначьте CALC. б. Для всех полей каждого типа записи подберите мнемони- ческие имена. в. Составьте алгоритм (описывающий поведение программы, написанной на ЯМД), позволяющий ответить на запрос «по задан- ной дате определить, когда на обед и (или) на завтрак меню вклю- чало блюдо, имеющее в качестве одного из ингредиентов красный перец». 4. Описание задачи дано в упражнении 6 разд. 3.11. а. Нарисуйте диаграмму Бахмана базы данных для системы КОДАСИЛ. Рядом с каждым файлом поставьте CALC или VIA: б. Нарисуйте диаграмму Бахмана базы данных для системы TOTAL. 5. Описание задачи дано в упражнении 2 разд. 3.11. а. Перечислите типы всех членов и владельцев межфайловых индексов для базы данных ADABAS. б. Объясните, как использовать индексы системы ADABAS для получения ответа на запрос «определить названия всех гор- ных вершин, которые были покорены ХОБСОНОМ». Считается, что двух вершин с одним и тем же названием быть не может. 6. Объясните, как использовать вторичные индексы (см. разд. 8.7) для получения ответа на запрос «определить названия всех горных вершин, которые были покорены ХОБСОНОМ». Файлы возьмите из упражнения 2 разд. 3.11. 10 Ульман Дж. 289
7. Нарисуйте диаграмму Бахмана базы данных, описывающей фирму «Типико», для системы КОДАСИЛ. Включите в базу дан- ных всю информацию из гл. 1. 8. Используя DBD из разд. 8.6, напишите программы на ЯМД Паскаль, позволяющие: а) определить названия всех городов, в которых побывали по крайней мере один инженер и по крайней мере один програм- мист; б) определить фамилии всех программистов, которые побы- вали хотя бы в одном городе ЭКСЛАНДИИ или УАЙЛАНДИИ.
ГЛАВА 9 ОРГАНИЗАЦИОННЫЕ ПРОБЛЕМЫ 9.1. ЦЕЛОСТНОСТЬ ДАННЫХ 9.1.1. КОНТРОЛЬ ТИПОВ Вполне очевидно, что информация, хранящаяся в базе данных, должна быть в меру возможности правильной, точной и согласованной. Необходимо предусматривать средства, которые бы защищали базу от потенциально неверных действий, связанных с корректировкой данных и записью новой информации. Одним из распространенных способов защиты является конт- роль типов. Простейший вариант — проверка принадлежности переменной выделенному для нее диапазону величин. Если, на- пример, установлено, чтобы содержимое поля НОМЕР-КЛИЕНТА находилось в пределах от 100000 до 999999, то система должна предотвратить попытк пользователя присвоить этому полю зна- чение 203. Или другой пример — торговые зоны, где располага- ются фирмы-клиенты, сокращенно обозначаются Ю, ЮЗ, 3, СЗ, С, СВ, В, ЮВ. Недопустимо, чтобы система позволила записать в поле ТОРГОВАЯ-ЗОНА значение «ПАРИЖ». 9.1.2. КОНТРОЛЬ ИЗМЕНЕНИЙ Если на фирме «Типико» операции ведутся таким об- разом, что количество не распределенного со склада товара, ука- занное в файле СОДЕРЖИМОЕ-НАРЯДОВ-НА.ПРОДАЖУ, не может увеличиваться, всякий прирост этого показателя дол- жен фиксироваться, расцениваясь как возможная ошибка. В ЯМД Паскаль для организации подобных проверок приходится спе-. циально писать программы. Более сложные СУБД оснащаются средствами, позволяющими объявлять ограничения, накладывае- мые на характер изменения переменных. Приведем еще один пример ограничений такого типа. В «Ти- пико» считается недопустимым, чтобы содержимое поля КОЛИ- ЧЕСТВО. НА.СКЛАДЕ в файле ТОВАРНЫЕ.ЗАПАСЫ возра- стало после единичной корректировки на величину, превышающую ЗАКАЗЫВАЕМОЕ-КОЛИЧЕСТВО. Если это правило наруша- ется, СУБД должна известить пользователя об ошибке. ю* 291
9.1.3. ДУБЛИРОВАНИЕ КЛЮЧЕЙ В СУБД КОДАСИЛ программист имеет возможность объявить, должны ли дублироваться значения ключей в файле прямого доступа с размещением типа CACL (т. е. с хэш-адреса- цией). Программист может также указать, допускается ли в на- боре дублирование ключей поиска или сортировки. Если дубли- рование запрещено, СУБД не позволит, чтобы программа на ЯМД повторно записала уже имеющееся в базе значение ключа и тем самым предотвратит потенциально ошибочную корректи- ровку данных. Таким способом можно, например, обеспечить отсутствие в файле ТОВАРНЫЕ.ЗАПАСЫ записей с одинако- выми значениями поля ШИФР.ТОВАРА. В тех случаях, когда предпринимается попытка записать дуб- ликат, в ЯМД Паскаль процедура INSERT присваивает перемен- ной DBSTATUS значение DUPLICATE. В результате эта по- пытка пресекается, даже если пользователь, программирующий на ЯМД, присвоит DBSTATUS значение OKAY. Информируя программиста о совпадении величин, СУБД защищает базу данных от случайного, несанкционированного, дублирования. 9.1.4. СВЯЗИ, ОБЪЕДИНЯЮЩИЕ РАЗЛИЧНЫЕ ПОЛЯ В файле СЛУЖАЩИЕ, ведущемся на фирме «Типико», значение поля ПОДОХОДНЫЙ.НАЛОГ.ЗА.ТЕКУЩИЙ.МЕ- МЯЦ не может превышать значения ПОДОХОДНЫЙ. НАЛОГ- ЗА-ТЕКУЩИЙ.ГОД. Это простейший пример ограничения, устанавливающего связь между содержимым нескольких полей. Примером более сложного условия может служить требова- ние, предъявляемое к счетам в методе двойной итальянской бухгалтерии: сумма их сальдо всегда должна быть нулевой. Та- кой интегральный показатель очень удобен при ручных расчетах. Можно организовать проверку этого ограничения и в СУБД, причем целесообразнее непосредственно запрограммировать ее, а не включать в объявления DBD. К ограничениям рассматриваемого типа относятся и условия, в которые входят внешние ключи. Например, «Типико» ведет файл КЛИЕНТЫ, первичным ключом которого служит НОМЕР. КЛИЕНТА. Однако это поле считается внешним ключом по от- ношению к файлу ЗАГОЛОВКИ.НАРЯДОВ, поскольку оно яв- ляется первичным ключом другого файла. По той же причине ШИФР.ТОВАРА оказывается внешним ключом для файла СО- ДЕРЖАНИЕ.СЧЕТОВ. В некоторых системах баз данных это свойство ключей используется следующим образом: если запись, помещенная в базу, содержит значение некоторого внешнего ключа, требуется, чтобы оно фигурировало в качестве первичного ключа любого другого файла той же базы. Такое требование поз- воляет, в частности, исключить ситуации, когда в нарядах на продажу значатся отсутствующие на складе товары. 292
9.1.5. ПРЕДВАРИТЕЛЬНЫЙ КОНТРОЛЬ ДАННЫХ Информация может быть искажена еще до того, как она вводится в базу данных. Рассмотрим два классических метода контроля подобных ошибок, осуществляемого вне базы. Контрольное суммирование. Предположим, например, что клиент направил «Типико» написанный от руки заказ, включаю- щий 100 наименований, который на фирме перепечатывается на машинке. Для выявления опечаток можно воспользоваться таким способом: просуммировать значения поля КОЛИЧЕСТВО во всех 100 позициях машинописной копии заказа и сравнить результат с аналогичной суммой, просчитанной по рукописному орйгиналу. Если они не совпадут, значит по меньшей мере одно значение поля КОЛИЧЕСТВО напечатано неправильно. Сумму значений не- которого поля принято называть итогом. В разд. 1.5.2 уже вы- сказывалась идея использования промежуточных итогов в ка- честве средства проверки целостности информации. В таких полях, как КОЛИЧЕСТВО или ЦЕНА-ЗА. ЕДИНИ- ЦУ, помещаются числа, отражающие реальные характеристики объектов. Можно, однако, суммировать и значения таких полей, как НОМЕР-КЛИЕНТА. Получаемые суммы, не имеющие кон- кретного содержания и применяемые лишь в качестве контроль- ных показателей, именуются проверочными итогами. Во многих случаях весьма полезно сравнивать количество записей до и после выполнения некоторой операции, например перепечатки. Этот показатель обычно называют контрольной суммой. Контроль числовых данных. Предположим, что число, записы- ваемое в поле НОМЕР-КЛИЕНТА, всегда содержит ровно пять цифр, которые обозначим как d5, d4, d8, d2, dx. Для выявления ошибок, возникающих при копировании этих цифр, можно вос- пользоваться суммой 5 5 dt. <=! Развивая идею, дополним пятизначное число шестой цифрой d0, вычисляемой согласно формуле / / 5 \ \ do = 11 - S dt mod 11 . \\<=о / / Разряд, содержащий дополнительную цифру do, называется контрольным. Если понадобится оценить правильность копиро- вания числа dsd4d8dad1do, достаточно проверить, выполняется ли условие /Б \ 1 S dt) mod 11=0. \«=r / 293
Если оно не соблюдается, то это означает, что имеет место ошибка, например, число 249639 неправильно скопировано как 247639. Отметим, что в таком виде метод не позволяет выявлять слу- чайные перестановки цифр, когда, скажем, после копирова- ния число 249639 превращается в 294369. Этот недостаток можно устранить, применив несколько более сложную формулу: / / s \ \ dQ = 11 — I I 2 wtd 11 mod 11), \\z=i / / где коэффициенты wt, именуемые весовыми, выбираются обычно такими: = 2, w2 = 3, ws = 4, w4 — 5, u>6 = 6. Поскольку весовые коэффициенты отличаются друг от друга, при переста- новке цифр сумма 5 2 wtdt, 1=1 очевидно, меняется. Если обнаруживается неравенство /5 \ 2 Wtdt mod 11 (> О, \<=о / где w0 = 1, можно утверждать, что произошла либо замена цифр (249639-> 247639), либо их перестановка (249639294639). Пример с пятью цифрами был взят просто как иллюстрация описанного метода контроля. Его нетрудно распространить и на случай произвольного количества цифр. В частности, контрольные разряды предусмотрены в международном стандартном книжном номере ISBN (нужно иметь в виду, что число 10 обозначено там как X). 9.2. ВОССТАНОВЛЕНИЕ ДАННЫХ 9.2.1. ТИПЫ ОТКАЗОВ Информация, содержащаяся в базе данных, зачастую представляет огромную ценность для ее владельца. Поэтому не- допустимо, чтобы отказы, например механические повреждения дисков, приводили к безвозвратной утере данных. Современная мощная СУБД обязательно должна иметь в своем составе средства восстановления данных после разнообразных сбоев и отказов. Аварии оборудования, такие, как порча диска из-за сопри- косновения головки с магнитным покрытием, называются систем- ными отказами. Этот же термин применяют по отношению к ошиб- кам, нарушающим нормальную работу (например, приводящим к тупиковым ситуациям) управляющих программ (прикладные программы, написанные, в частности, на ЯМД, к ним не отно- сятся). 294
Иногда выполнение программы на ЯМД прерывается вслед- ствие срабатывания средств контроля данных или из-за того, что в самой программе возникают сбои (например, целой переменной присваивается логическое значение). Ошибки такого рода име- нуются отказами транзакций. 9.2.2. ОТКАЗЫ ТРАНЗАКЦИЙ Транзакцией базы данных называют фрагмент выпол- няемой программы, заключенный между процедурами START- TRANSACTION и ENDTRANSACTION или ABORTTRAN- SACTION. Программист рассматривает такой фрагмент как обо- собленную секцию, на доступ которой к базе данных не могут по- влиять другие транзакции. Отказы транзакций базы данных зачастую имеют более серь- езные последствия, чем отказ управляющей программы (т. е. написанной не на ЯМД Паскаль). Если программа этого класса аварийно прерывается (например, вследствие попытки деления на нуль), информация, ранее записанная ею в стандартный файл Пдскаля, может оказаться неполной или бесполезной, и поэтому подобная выдача считается обычно ложной. После устранения причины отказа программа запускается повторно и вновь запол- няет свои выходные файлы. Если программа завершается успеш- но, то возникший ранее отказ практически никак не проявляется, поскольку все потенциально неверные данные, уже помещенные в выходные файлы, перезаписываются ею заново. Одна из особенностей, отличающих базу данных от стандарт- ного файла Паскаля, состоит в том, что она дает возможность корректировать данные выборочно, не перезаписывая всю базу в целом. Если транзакция прерывается, уже проделав все или чать корректировок, в базе могут остаться неверные данные. Ими воспользуются затем другие транзакции, получат на их основе ложные результаты и т. д. Предположим, например, что аварийно прервалась тразакция, предназначенная для формирования файла СОДЕРЖАНИЕ.СЧЕТОВ и составления товарной ведомости, отправляемой на склад «Типико». Если это произошло сразу после того, как транзакция записала очередной вид товара в СОДЕРЖА- НИЕ.СЧЕТОВ, не успев еще соответственно увеличить значение поля РАСПРЕДЕЛЕННОЕ. КОЛИЧЕСТВО в файле ТОВАР- НЫЕ.ЗАПАСЫ, то в базе оказываются данные, не согласованные между собой. В принципе, такую несогласованность можно устра- нить путем отмены результатов всех корректировок, произведенных сбившейся транзакцией. Появление в программе на ЯМД процедуры ENDTRANSAC- TION, помимо прочего, указывает СУБД, что все корректировки, осуществленные в ходе транзакции, следует расценивать как пра- вильные и согласованные с другими данными. Если после такой 295
процедуры происходит аварийное прерывание программы, ранее выполненная транзакция считается успешно завершенной и все ее результаты остаются в силе. 9.2.3. ДУБЛИКАТЫ ЗАПИСЕЙ Один из способов восстановления данных после потен- циально неправильной корректировки основан на использовании дубликатов. В упрощенном виде идея состоит в следующем. Записи, хранящиеся в базе данных, физически объединены в блоки (для пользователя, программирующего на ЯМД Паскаля, это обстоятельство не играет никакой роли, поскольку он имеет дело только с записями). Если требуется изменить содержание некоторой записи, блок, в котором она помещается, копируется из внешней памяти в буфер, относящийся к оперативной памяти. Перед корректировкой блок можно скопировать отсюда в специ- альный файл, именуемый файлом дубликатов. Дубликат блока — это копия его содержимого до выполнения корректировки. После того как дубликат скопирован в файл, СУБД корректирует за- пись, которая берется из блока, по-прежнему находящегося в бу- фере оперативной памяти. Блок с измененным содержимым пе- реписывается затем обратно во внешнюю память по тому же ад- ресу, что он занимал до операции. В результате старое содержи- мое блока стирается и на его месте оказывается скорректирован- ное. Что же касается дубликата уничтоженного блока, то он, будучи занесен в файл по другому физическому адресу внешней памяти, сохраняется в прежнем виде. Пока транзакция не завершится, СУБД оставляет в файле дубликатов все блоки, подвергшиеся корректировке в ходе ее выполнения. Благодаря этому можно восстановить все данные в той форме, которую они имели на момент появления команды STARTTRANSACTION, достаточно лишь переписать дубликаты по их прежним адресам. Описанный метод восстановления иногда называют повторным прогоном. Если все записи имеют фикси- рованную длину, можно экономить память, дублируя не блоки, а отдельные записи. В тех случаях, когда транзакция не повторяется сразу после ее аварийного завершения или когда работа базы данных нарушена вследствие отказа другого типа, файл дубликатов можно приме- нять для повторного прогона целой группы незаконченных тран- закций, вносивших изменения в содержимое базы. Если происхо- дит физическое повреждение дисков, этот метод неэффективен, поскольку очень часто портятся блоки, дубликаты которых от- сутствуют. 9.2.4. ОБРАЗЫ СКОРРЕКТИРОВАННЫХ ЗАПИСЕЙ Идея еще одного способа защиты базы данных от отка- зов транзакций состоит в том, что фактическая корректировка данных откладывается до выполнения процедуры ENDTRAN- 296
SACTION. До этого момента скорректированные записи не копи- руются в базу данных, заменяя прежнее содержимое. Вместо этого они заносятся в отдельный файл образов. Транзакция, обращаясь к некоторой записи, пытается оты- скать ее прежде всего среди записей, помещенных в файл образов. Если попытка удается, найденная запись рассматривается как последняя ее версия. В противном случае СУБД продолжает поиск в файлах самой базы данных. В тех системах, где реализован этот метод защиты, доступ к физической базе данных получает только процедура ENDTRANS- ACTION, которая корректирует базу, занося в нее образы, сфор- мированные транзакцией. Процедура ABORTTRANSACTION от- меняет эту корректировку, и, следовательно, содержимое базы данных остается таким же, каким оно было непосредственно перед процедурой STARTTRANSACTION. Второй метод защиты в среднем требует больших затрат вре- мени, чем метод, основанный на использовании дубликатов. Объясняется это тем, что при выполнении инструкции ЯМД FIND часто требуется доступ не только к файлам базы данных, но и к файлу образов. Что же касается файла дубликатов, то инфор- мация в него заносится всякий раз, когда производится коррек- тировка, однако при выполнении операций доступа, связанных только с поиском, система к этому файлу не обращается. 9.2.5. ДАМПЫ Стандартной мерой предосторожности, позволяющей снизить урон от таких аварий, как пожар или механическое пов- реждение дисков, служит периодическое копирование всего со- держимого базы данных на магнитную ленту или на вспомогатель- ные диски. Желательно, чтобы дублирующие носители хранились в отдельном помещении. Информация, переписанная на такой носитель, называется дампом. Общепринятой практикой является получение дампов не только баз данных, но и отдельных файлов, например стандартных файлов Паскаля. Периодичность, с кото- рой выводятся дампы, может варьироваться, но обычно это дела- ется не реже раза в неделю и не чаще одного раза в день. Если в результате аварии испорчены стандартные файлы Паскаля, программист-пользователь может взять их дамп и по- вторно прогнать программы корректировки, которые сформируют утерянную версию этих файлов. Восстановление базы данных представляет более сложную задачу, особенно если после вывода последнего дампа ее содержимое неоднократно корректировалось многими пользователями. В любой из транзакций могли участво- вать данные, полученные с помощью других транзакций, и весьма вероятно, что ни один из пользователей не сможет самостоятельно воспроизвести последовательность проделанных операций. Иными словами, чаще всего пользователи своими силами не способны 297
осуществить восстановление базы данных. Тем не менее это воз- можно, и в следующем разделе кратко описывается один из под- ходов к решению задачи. 9.2.6. РАЗВЕРТЫВАНИЕ БАЗЫ ДАННЫХ В качестве источника информации для восстановления базы данных СУБД использует файл, содержащий копии образов, сформированных транзакциями. Этот специально защищенный файл физически хранится отдельно от базы данных. Как и в са- мих образах, в их копиях указаны транзакции, выполнявшие операции корректирования данных. Подобный файл иногда на- зывают журналом базы данных. Выполняя по порядку корректи- ровки, отмеченные в журнале, можно восстановить утраченную версию базы, отталкиваясь от ее последнего дампа. Такой после- довательный процесс восстановления известен как развертывание базы данных из дампа. Развертывание базы с использованием образов скорректиро- ванных записей не обязательно должно сочетаться с методом за- щиты от отказов транзакций из разд. 9.2.4. Защита может быть организована и по способу, описанному в разд. 9.2.3. 9.3. УПРАВЛЕНИЕ ПАРАЛЛЕЛЬНЫМ ДОСТУПОМ 9.3.1. ПОНЯТИЕ ПАРАЛЛЕЛИЗМА Обсудим теперь, каким образом регулируется парал- лельное обращение к базе данных нескольких программ, напи- санных на ЯМД. Большинство серийно тиражируемых СУБД обеспечивает возможность параллельного выполнения таких опе- раций. Операционная система коллективного пользования позволяет многим абонентам, работающим, как правило, на терминалах, иметь доступ к ресурсам одной ЭВМ. Подобная система должна создавать у каждого пользователя иллюзию единоличного обла- дания ресурсами. Наиболее просто эта проблема решается сле- дующим образом: всем транзакциям (или заданиям) выделяются по порядку кванты времени небольшой продолжительности (на- пример, 20 мс), на протяжении которых центральный процессор (ЦП) выполняет только одну транзакцию, а остальные ждут своей очереди. Быстрое переключение ЦП с одной транзакции (или задания) на другую желательно и по той причине, что это позволяет ми- нимизировать простои ЦП, связанные с ожиданием внешних событий. Так, например, если транзакции понадобилось обра- титься к блоку, который еще не считан с диска в оперативную память, процессор вынужден ждать, пока этот блок не будет 298
скопирован, и лишь после этого сможет возобновить выполнение транзакции. Мультипрограммная операционная система не до- пускает подобных простоев. Когда одна транзакция приоста- навливается, система передает ЦП в распоряжение другой, чтобы процессор вместо пассивного ожидания мог проделать для нее некоторую часть работы. Если и вторая транзакция приостанав- ливается, управление ЦП получает третья, и т. д. В результате образуется целая совокупность активных транзакций, из которых одна выполняется, а другие либо задержаны, либо готовы к вы- полнению, но ждут своего кванта времени ЦП. В системах коллективного пользования или мультипрограм- мных системах транзакции могут прерываться и возобновляться в произвольные моменты времени. С точки зрения пользователя это выглядит так, как будто чужие транзакции (или задания) идут одновременно с его транзакцией. Иными словами, возникает впечатление, будто множество транзакций выполняется парал- лельно. Такое может происходить и на самом деле, если вычисли- тельная система объединяет несколько ЦП. Однако в любом слу- чае, имеет ли место реальный параллелизм, или это лишь иллю- зия, порожденная работой мультипрограммной системы, недо- пустимо, чтобы одна транзакция нарушала работу другой. В частности, необходимо обеспечить такую дисциплину доступа при которой транзакция не могла бы считывать данные в то же самое время, когда их корректирует другая транзакция. Это можно пояснить на следующем примере. Предположим, что транзакция, последовательно просматривая строки наряда на прродажу, проверяет условие, согласно которому КОЛИЧЕСТВО НА.СКЛАДЕ минус РАСПРЕДЕЛЕННОЕ, КОЛИЧЕСТВО должно быть равно или больше, чем ЗАКАЗАННОЕ, КОЛИЧЕ- СТВО. Если условие выполнено во всех строках, выводится сооб- щение, разрешающее произвести отгрузку товаров. Однако это сообщение окажется ошибочным, если выполняемая в параллель другая транзакция увеличила содержимое поля РАСПРЕДЕЛЕН- НОЕ, КОЛИЧЕСТВО для некоторых товаров, указанных в на- ряде. Двум транзакциям должно быть также запрещено одновре- менно корректировать одни и те же данные. Чтобы продемон- стрировать, что может произойти в противном случае, предпо- ложим, что каждая из транзакций располагает отдельным бу- фером для размещения блоков, считываемых из файла. Пусть эти транзакции, которые обозначим А и В, предприняли попытку изменить в некоторой записи файла ТОВАРНЫЕ,ЗАПАСЫ со- держимое поля РАСПРЕДЕЛЕННОЕ, КОЛИЧЕСТВО. Даль- нейшая последовательность событий может быть, например, такой. • 1. Транзакция А копирует блок из файла базы данных, на- ходящегося во внешней памяти, в буфер оперативной памяти. Далее в этом буфере она заносит в поле РАСПРЕДЕЛЕННОЕ, КО- 299
ЛИЧЕСТВО требуемое значение. Для определенности положим, что новое содержимое этого поля равно 15. 2. По какой-то причине выполнение транзакции А преры- вается до того, как она успела переписать скорректированный блок во внешнюю память. Управление ЦП переходит к транзак- ции В. Она считывает нескорректированный блок из внешней памяти в свой буфер, меняет значение поля РАСПРЕДЕЛЕН- НОЕ. КОЛИЧЕСТВО (заносит в него, например, 20) и копирует блок обратно во внешнюю память. 3. Выполнение транзакции А возобновляется, и она пере- писывает блок, в котором значение поля РАСПРЕДЕЛЕН- НОЕ. КОЛИЧЕСТВО равно 15, из буфера оперативной памяти во внешнюю, на место блока, ранее помещенного туда транзак- цией В. Порча этого блока является примером исчезновения инфор- мации в результате корректировки. 9.3.2. БЛОКИРОВКИ Средством, которое не позволяет транзакции считы- вать или корректировать данные в то время, когда они коррек- тируются другой транзакцией, служит система блокировок. Блокировкой называется маркер, который указывает, разрешен ли доступ к некоторой группе данных. Желательно, чтобы тран- закция устанавливала блокировку группы данных лишь на тот период, пока она их модифицирует. Эта блокировка должна за- прещать доступ к группе данных со стороны других транзакций. Любая транзакция, попытавшаяся обратиться к заблокирован- ным данным, прерывается и вновь получает управление ЦП лишь после снятия блокировки. В современных СУБД блокировки устанавливаются и сни- маются автоматически. Если транзакция обращается к группе данных, которая может блокироваться, выполняется следующая стандартная последовательность действий. 1. Устанавливается блокировка. Если группа уже заблоки- рована, транзакция переводится в состояние ожидания до мо- мента снятия блокировки. 2. Группа данных подвергается некоторой обработке. 3. Блокировка снимается, и группа данных становится до- ступной для другой транзакции, которая может в свою очередь установить блокировку. Чем обширнее заблокированная группа данных, тем много- численнее могут оказаться транзакции, ожидающие доступа к этой группе. Если блокируется целый файл, то все транзакции, попытавшиеся к нему обратиться, будут задержаны. Если же блокировка действует только по отношению к нескольким за- писям в том же файле, то число задержанных транзакций, пре- тендующих на доступ к этим записям, в среднем будет намного меньше. Размер группы данных, блокируемых как единое целое, определяет разрешающую способность механизма блокировок. 300
При уменьшении размера групп разрешение повышается, но увеличиваются и затраты на реализацию механизма, поскольку каждую группу приходится оснащать маркером блокировки. Между такими крайностями, как блокирование целых файлов, при котором минимален параллелизм, и блокирование отдель- ных записей, при котором максимальны затраты на реализацию, лежит блокирование блоков записей, что представляется вполне разумным компромиссом. Полезным усовершенствованием является применение бло- кировок нескольких типов. Так, монопольная блокировка дает транзакции исключительное право на доступ к группе данных. Последняя становится в этом случае совершенно недоступной для других транзакций. Транзакция должна устанавливать мо- нопольную блокировку, если предстоит модифицировать группу данных, и необходимо гарантировать, что ни одна другая тран- закция не считает или не модифицирует эти данные. Транзакции, которая намеревается выполнить операцию счи- тывания из группы данных, нет нужды запрещать другим тран- закциям параллельно с нею считывать (но не корректировать) информацию из той же группы. Блокировка с чтением не запре- щает параллельный доступ транзакций к данным с целью их счи- тывания, но в то же время она не дает возможности установить монопольную блокировку, пока сама не будет снята. Кроме того, блокировка с чтением предотвращает параллельное корректи- рование данных. 9.3.3. ЗАВИСАНИЯ И ТУПИКОВЫЕ СИТУАЦИИ Как правило, транзакция переводится в состояние ожидания после попытки установить монопольную блокировку уже заблокированной группы данных. Снятия одной и той же блокировки могут ожидать несколько транзакций одновременно. Они образуют очередь на доступ, причем последовательность транзакций в очереди можно регулировать. Если эта последова- тельность формируется так, что какая-то из транзакций всякий раз сдвигается в конец очереди, то она лишается возможности установить свою блокировку. Такая транзакция, именуемая зависшей, постоянно находится в состоянии ожидания." Зависа- ние легко устранить, изменив построение очереди таким образом, чтобы любая транзакция постепенно продвигалась к ее началу. Необходимо также учитывать возможность возникновения тупиковых ситуаций. Вот классический пример подобной си- туации: 1. Транзакция А устанавливает блокировку группы X. 2. Транзакция В устанавливает блокировку группы Y. 3. Транзакция А делает попытку заблокировать группу Y и прерывается. 301
9.1. Пример последовательности действий, приводящих к тупиковой ситуации Шаг Транзак- ция Группа данных Ожида ние 1 А W Нет 2 В X Нет 3 В W Да 4 С Y Нет 5 С X Да 6 D Z Нет 7 D Y Да 8 А Z Да Примечание. В колонке сожи- дание» указано, переводится ли в со- стояние ожидания транзакция, совер- шившая попытку доступа. 4. Транзакция В делает попытку заблокировать группу X и прерывается. Транзакция А, задержанная на шаге 3, ожидает, когда В освободит группу Y. Однако транзакция В не может этого сделать, поскольку сама за- держана на шаге 4 и ждет, когда транзакция А снимает блоки- ровку с группы X. Транзакции А и В находятся в тупико- вой ситуации, не давая друг Другу выйти из состояния ожидания. В тупиковую ситуацию могут вовлекаться все новые участники: если в нее попала транзакция А, то затем будет прервана транзакция В, ко- торой понадобились данные, заблокированные А, то же самое произойдет с транзакцией С, если она обратится к данным, заблокированным В, и т. д. Пример подобной цепочки событий иллюстрирует табл. 9.1. Анализ этой таблицы показывает, что причиной тупиковой ситуации послужило опре- деленное чередование ее строк. Чтобы избежать тупика, можно изменить порядок строк, рассортировав их по транзакциям сле- дующим образом: первая транзакция последовательно блокирует все группы данных, с которыми ей предстоит работать; затем то же самое проделывает со своими данными вторая транзакция и т. д. После такого переупорядочения таблицы транзакции А уже не придется устанавливать блокировку группу Z, ранее заблоки- рованной транзакцией D, которая прервана в результате цепочки событий, начавшейся с того, что транзакция А блокировала группу W. К сожалению, заранее определить порядок доступа к информации в базе данных практически невозможно. Крайне сложно, например, до начала операции соединения файлов указать, какие группы данных будут в ней участвовать. Избавиться от тупиковых ситуаций можно и другим спо- собом. Достаточно установить некоторую очередность доступа к группам данных, основанную, в частности, на перечислении их имен в алфавитном порядке (применительно к табл. 9.1 это W, X, Y, Z). Очевидно, при таком условии транзакция В будет вы- нуждена установить блокировку группы W прежде, чем группы X. Аналогично, транзакция С блокирует сначала X, а затем Y, и т. д. Доказано, что упорядочение доступа по алфавиту существенно снижает вероятность возникновения тупиковых ситуаций. Ее можно еще более уменьшить, введя правило: если какая-либо 302
транзакция нарушает порядок доступа, она выполняется по- вторно. Следует, однако, иметь в виду, что на практике значи- тельная часть повторных прогонов приходится на те транзакции, которые не стали бы причиной тупиковых ситуаций. До сих пор говорилось о способах устранения тупиковых си- туаций. Но возможен и другой подход: во многих случаях вы- годнее с точки зрения общих затрат допускать тупиковые ситуа- ции, и если они реально возникают, повторно выполнять попав- шие в них транзакции. Для того чтобы можно было обнаружить подобную ситуацию, СУБД должна регистрировать все группы данных, свободные для доступа, группы, монопольно занятые транзакциям, и группы, к которым произошло обращение. Любая транзакция, работающая только со свободными группами, может быть исключена из списка потенциальных участников тупико- вой ситуации. После окончания транзакции все занятые ею группы освобождаются и могут использоваться другими транзакциями. Те, в свою очередь, также удаляются из списка, и т. д. Если по завершении цикла анализа в списке не оказывается ни одной транзакции, делается вывод, что тупиковая ситуация отсутствует, 9.3.4. УПОРЯДОЧЕННОСТЬ В разд. 9.2.2 указывалось, что транзакция представ- ляет собой такой фрагмент программы, на выполнение которого не влияют идущие с ним в параллель другие транзакции. Иными словами, по отношению к транзакции параллелизм не должен проявляться. Следовательно, результаты, выдаваемые некоторой совокупностью транзакций, не зависят от того, выполняются ли они одновременно или последовательно, в определенном порядке. Любую последовательность операций вычислительной системы, обладающую таким свойством, называют упорядоченной, даже если в действительности эти операции выполняются в параллель. Можно обеспечить упорядоченность транзакций, если потре- бовать, чтобы ни одна из них, разблокировав одну группу дан- ных, не пыталась установить блокировку другой. Доказательство этого утверждения выходит за рамки книги и поэтому здесь не приводится. Добиться упорядоченности можно и с помощью другого метода, основанного на применении флажков. 9.3.5. ФЛАЖКИ Альтернативой механизму блокировок, предотвращаю- щему доступ одной транзакции к данным, обрабатываемым дру- гой, может служить механизм флажков. Рассмотрим вариант работы последнего в системе с повторными прогонами, описан- ной в разд. 9.2.3. Предположим, что СУБД нумерует последовательно стар- тующие транзакции, так что каждая из них получает уникальный номер: 1, 2, 3, ... В СУБД ведется список номеров всех транзак- ций, выполнение которых началось, но еще не завершилось. 303
Пусть группой данных является блок. Каждый из них снаб- жается заголовком, куда СУБД заносит служебную информацию. Там, в частности, помещается номер последней транзакции, при- ступившей к модификации содержащихся в блоке данных. Этот номер и называют флажком. Транзакция прогоняется заново, если она делает попытку изменить содержание блока, на котором стоит флажок другой, еще не законченной транзакции. При такой схеме проблема утери информации решается автоматически. Кроме самого блока, СУБД хранит список его дубликатов, отмеченных флажками транзакций. Любой транзакции разре- шается прочитать блок, если он не менялся с момента.ее старта. Если же с тех пор, как транзакция начала выполняться, содер- жимое блока корректировалось, СУБД предоставляет возмож- ность считывать его дубликаты, сделанные до старта. Такая орга- низация доступа гарантирует, что на работу транзакции не по- влияют другие, выполняемые с ней в параллель. Одновременно с корректировкой блока может считываться его дубликат. Подоб- ный параллелизм сложнее реализовать в рамках системы блоки- ровок и, как правило, при одинаковом разбиении данных на группы флажковый механизм обеспечивает более высокий уро- вень параллелизма. 9.4. УПРАВЛЕНИЕ ДОСТУПОМ 9.4.1. СРЕДСТВА ЗАЩИТЫ БАЗ ДАННЫХ В базе данных может храниться информация, носящая в той или иной степени секретный характер. Любая фирма стре- мится, например, засекретить от конкурентов информацию о своей финансовой деятельности. Управление доступом — это совокуп- ность средств, определяющих, кто обладает правом доступа, к ка- ким частям базы данных и с какими целями. Управление доступом часто отождествляется с защитой базы данных. Под защитой здесь понимается обеспечение секретности информации и предотвращение несанкционированных коррек- тировок. В литературе по базам данных этот термин употреб- ляется в том же значении и не имеет отношения к мерам противо- пожарной безопасности, сохранности аппаратных и программных средств и т. п. Кратко охарактеризуем теперь различные методы защиты, основанные на регламентации доступа. 9.4.2. ТАБЛИЦЫ ДОСТУПА Для получения доступа к системе пользователь обычно должен зарегистрироваться в ней, введя свою идентификацию и пароль. Во многих системах, сверх того, имеются средства, регулирующие доступ к отдельным группам данных. На каждую 304
9.2. Пример организации таблицы доступа Имя пользователя Имя файла ИМЯФАЙЛА1 ИМЯФАЙЛА2 ИМЯФАЙЛАЗ ИМЯФАЙЛА4 АХ4352 во ДЗ БО ТЧ АУ73М1 ДЗ БО БО ТЧ ВЕ64М8 ДЗ ДЗ БО ТЧ CF12S1 ДЗ ДЗ БО БО EK61HZ ТЧ ТЧ ТЧ ТЧ такую группу в СУБД может быть заведена таблица доступа, в которой перечислены идентификации пользователей, а также указано, к каким данным им разрешено обращаться и какие опе- рации допускаются. Простейшим примером может служить табл. 9.2, в которой каждая группа данных представляет собой файл. Здесь использованы следующие обозначения: ДЗ — «до- ступ запрещен», ТЧ — «только чтение», БО — «без ограничений». Из табл. 9.2 можно выяснить, в частности, что пользователю, зарегистрировавшемуся под именем АУ73М1, разрешается кор- ректировать содержимое файлов ИМЯФАЙЛА2 и ИМЯФАЙЛАЗ, а также считывать, но не корректировать, ИМЯФАЙЛА4. Руко- водствуясь подобной таблицей, система «выясняет», кому и что можно делать с отдельными группами данных. 9.4.3. ПАРОЛИ Рассмотрим еще один подход к обеспечению защиты данных. Он состоит в том, что пользователю при каждом обра- щении к группе данных с целью их считывания или корректи- ровки предписывается указывать специальные пароли. Пароль, задаваемый пользователем, будем именовать ключом секретности, а пароль, установленный в системе, — секретной блокировкой. Право на доступ пользователь получает лишь в том случае, если указанный им ключ совпал с блокировкой. При таком подходе таблицы типа табл. 9.2, регламентирующие доступ пользовате- лей к группам данных, не нужны. Вместо этого каждая группа снабжается секретными блокировками для всех видов доступа. В некоторых системах описанный метод доведен до такой сте- пени, что в каждой записи каждое поле имеет отдельные секрет- ные блокировки для каждого типа доступа. Его можно еще бо- лее усовершенствовать, предусмотрев, кроме того, отдельные блокировки и для всех процедур ЯМД, таких как FIND, INSERT, REMOVE, CREATE, DELETE. В этом случае пользователь, программирующий на ЯМД, уже не сможет ограничиться таким, например, обращением: 11 Ульман Дж. 305
FINDKEY (THISINDEX, KEYVAR, PTR). Вместо этого ему придется написать примерно следующее: PRIVACYKEY FOR FINDKEY OF THISINDEX IS 'ABACADABRA'; FINDKEY (THISINDEX, KEYVAR, PTR) Здесь 'ABACADABRA' является ключом секретности, соот- ветствующим используемому типу доступа. Инструкция PRI- VACYKEY FOR ... должна располагаться в программе ранее любой инструкции ЯМД, содержащей запрос на доступ к базе данных. 9.4.4. ПРОПУСКНАЯ СИСТЕМА Лица, не имеющие права пользоваться данными, не должны пропускаться в помещение, где установлены терминалы. К ним допускаются лишь обладатели удостоверений, выданных администрацией. 9.4.5. ШИФРОВАНИЕ Путем шифрования данные преобразуются таким обра- зом, что они теряют всякий смысл для того, кто не имеет соот- ветствующих дешифраторов. Будем называть текст, не подвергну- тый шифрованию, открытым. Кратко рассмотрим два элементар- ных классических метода шифрования, предполагая для про- стоты, что открытый текст содержит только буквы и пробелы. Оба эти метода предназначены для обработки блоков текста, каждый из которых представляет собой последовательность сим- волов. Число символов в блоке равно BLOCKLENGTH. Перестановка символов. В первом методе ключом к шифру служит перестановка, составленная из набора целых чисел 1, 2, 3...BLOCKLENGTH. Если, например, BLOCKLENGTH = = 6, в качестве ключа можно использовать 351462. Другим возможным ключом является 453261. Для хранения ключа отве- дем массив. CYPHERKEY! array [1..BLOCKLENGTH 1 of 1..BLOCK- LENGTH Приведенный ниже шифровальный алгоритм работает с мас- сивом BLOCK! array [1..BLOCKENGTH1 of CHAR; Еще более упрощая задачу, будем считать, что открытый текст состоит из целого числа блоков. Данный фрагмент про- граммы позволяет ввести открытый текст и, преобразовав его, вывести зашифрованный: RESET (PLAINTEXTFILE); while not EOF (PLAINTEXTFILE) do begin for i = 1 to BLOCKLENGTH do READ (BLOCK (i 1); for i = 1 to BLOCKLENGTH do WRITE (BLOCK ICYPHERKEY [ill) end . 306
Зашифрованный текст формируется путем перестановки сим- волов осуществляемой поблочно. Если, например, BLOCK- LENGTH = 6, ключ к шифру — 351462, а открытый текст — «ANGLESINGUMBOOTS», то будет получен следующий за- шифрованный текст: «GLAESNNGUIOTMOST». Дешифроваль- ный алгоритм реализуется с помощью программы RESET (CYPHERTEXTFILE); while not EOF (CYPHERTEXTFILE) do begin for i = 1 to BLOCKLENGTH do READ (BLOCK [CYPHERKEY [ill); for i = 1 to BLOCKLENGTH do WRITE (BLOCK li ]) end Количество возможных перестановок, а вместе с тем и сте- пень защищенности информации, возрастает с увеличением раз- мера блоков. Подстановка символов. Прежде чем обсуждать второй метод шифрования, запишем на Паскале две простые функции. Первая из них переводит буквы «А», «В», .... «Z» в целые числа 1, 2, .. 26, а пробел — в нуль: function NUMBERIC (CR: CHAR): INTEGER; begin if CH = ’’then NUMERIC: = 0 else NUMBERIC: = ORD (CH) — ORD (’A’) + 1 end Вторая функция осуществляет обратный перевод: function LETTER (J: INTEGER): CHAR; begin if J = 0 then LETTER: = ’’else LETTER: = CHR (J + ORD (’A’) — 1) end Ключ к шифру будем хранить в массиве CYPHERKEY: array 1..BLOCKLENGTH of 0 ... 26; В принципе алгоритм подстановки не запрещает, чтобы не- сколько элементов массива CYPHERKEY принимали одинако- вые значения. В перестановочном алгоритме это не допускается. Программа, реализующая алгоритм подстановки, заменяет буквы открытого текста другими буквами в соответствии с ключом: RESET (PLAINTEXTFILE); I: = 0; while not EOF (PLAINTEXTFILE) do begin READ (CH); I: = I + 1; J: = CYPHERKEY [I] + NUMERIC [CH]; if J > 26 then J: = J — 26; WRITE (LETTER (J)); if I = BLOCKLENGTH then I: = 0 end 11* 307
/Если, например, BLOCKLENGTH = 3, ключ к шифру — 142, а открытый текст — «ANGELSINGUMBOOTS», то будет выдан такой зашифрованный текст: «BRIFPUAMPAKWNFQPXU» Тот же ключ используется и в дешифрующей программе: RESET (CYPHERTEXTFILE); I: = 0; while not EOF (CYPHERTEXTFILE) do begin READ (CH); I: = I + 1; J: = NUMERIC (CH) — CYPHERKEY [II; if J < 0 then J: = J + 26; WRITE (LETTER (J)); if I = BLOCKLENGTH then I: = 0 end С целью повышения уровня защищенности данных можно сочетать методы шифрования, основанные на подстановке и пе- рестановке символов текста. 9.5. МОДЕЛИ И ПОДСХЕМЫ До сих пор считалось, что пользователь, программи- рующий на ЯМД, обладает исчерпывающими сведениями обо всех объявлениях в той базе данных, с которой он работает. В дей- ствительности же программисту эта информация может быть известна лишь частично: например, он ознакомлен только с не- которыми полями нескольких файлов. В этом случае ему доста- точно знать соответствующую часть объявлений. Кроме того, пользователь может программировать так, будто база данных включает нужный ему файл, в то время как фактически она со- держит лишь полную декомпозицию файла, и при необходимости он восстанавливается по этой декомпозиции без участия пользо- вателя. В некоторых базах данных СУБД допускает, чтобы програм- мисты обозначали одни и те же переменные, в частности поля записей, различными именами. Более того, иногда разрешается, чтобы программисты, работая с одной переменной, присваивали ей разные типы. Один программист может, например, рассматри- вать поле как строку из четырех символов, являющихся цифрами. В то же время другой программист использует это поле просто как числовое, а не как строку символов. Все это можно обобщить следующим образом: СУБД предостав- ляет разным пользователям возможность работы с иерархией мо- делей базы данных. Руководствуясь определениями КОДАСИЛ, модель в дальнейшем будем именовать подсхемой. По существу, подсхема — это подмножество объявлений базы данных (т. е. схемы), в рамках которого при необходимости и в пределах, до- пускаемых СУБД, могут меняться имена и типы переменных. По определению КОДАСИЛ, подсхема не позволяет пользова- 308
Рис. 9.1. Диаграмма Бахмана для подсхемы, используемой в отделе продаж фирмы «Типико» ТОВАРНЫЕ—ЗАПАСЫ | подсхем, обладает целым рядом до- телю, программирующему на ЯМД, применять фай- лы, представленные в виде своих полных декомпози- ций. Однако эта возмож- ность практически реа- лизована во многих ре- ляционных системах, хотя и не в полной мере, пос- кольку такой файл можно читать, но не корректи- ровать. Иерархия моделей, или стоинств, в том числе следующими. а. Пользователь может иметь упрощенное представление о базе данных, облегчая тем самым свою работу. б. Управление доступом приобретает большую надежность: программируя на ЯМД, пользователь не может обращаться к той части базы данных, что выходит за рамки его модели. В идеале модель должна быть минимальной — в том смысле, что пользо- ватель не должен располагать никакими сведениями о базе дан- ных, кроме непосредственно необходимых для его работы. в. Подсхема пользователя может оставаться неизменной, в то время как схема в целом подвергается редактированию и моди- фикации, например, с целью включения в базу новых файлов и путей доступа или дополнения уже имеющихся записей новыми полями. Подсхема не претерпевает изменений и в тех случаях, когда из исходной схемы удаляются поля, файлы или пути доступа, не используемые в подсхеме. Очевидно, если бы пользователь был вынужден работать с полной схемой, подобные преобразо- вания не могли не затронуть его. Определенная изоляция поль- зователя от изменений, происходящих в базе, может рассматри- ваться как одно из проявлений независимости данных (к этому вопросу предстоит еще вернуться в разд. 9.6). Приведенные соображения позволяют понять, почему в раз- личных подразделениях фирмы «Типико» используются неодина- ковые подсхемы. Отделу продаж, например, требуется подроб- ная информация о клиентах, а о поставщиках не нужна. Диа- грамма Бахмана для подсхемы отдела продаж изображена на рис. 9.1. Сопоставляя эту подсхему с полной схемой базы данных «Типико», нетрудно видеть, что из нее исключены все файлы, относящиеся к приобретению товаров, а также другие файлы, не связанные с работой отдела, например СЛУЖАЩИЕ. По- 309
Рис. 9.2. Диаграмма Бахмана для подсхемы, используемой в отделе закупок фирмы «Типико» скольку платежи фирмы поставщикам не проходят через отдел продаж, они не отражены на подсхеме, благодаря чему устра- няется риск перепутать их с выплатами, поступающими от клиен- тов. Наконец, наглядность подсхемы облегчает пользование ба- зой данных служащим отдела, которым было бы сложнее сориен- тироваться в полной схеме. Если бы руководство «Типико» сочло необходимым подклю- чить к базе данных новый файл, в котором перечислены все зда- ния, занимаемые фирмой, указаны расположение, размеры и принадлежность помещений в этих зданиях, то подсхема отдела продаж не претерпела бы изменений и, следовательно, это со- вершенно не отразилось бы на его работе. Такое расширение базы не сказалось бы и на работе отдела закупок, чья подсхема при- ведена на рис. 9.2. Вполне понятно, что в эту подсхему не вошли файлы, содержащие информацию о клиентах и их заказах. По своей структуре объявления модели или подсхемы анало- гичны DBD, однако последние определяют еще и связи между моделью и базой в целом. Обычно объявления DBD находятся в компетенции администратора базы данных, о котором пойдет речь в разд. 9.7. 9.6. НЕЗАВИСИМОСТЬ ДАННЫХ Независимость данных проявляется в том, что при разного рода реорганизациях базы, например, при смене хэш- функций, введении или удалении индексов, изменении размера блоков в В-деревьях, программы на ЯМД или языке запросов не нуждаются в модификации. Определенной независимости данных можно добиться даже без применения моделей или подсхем. В част- ности, программу на ЯМД Паскаль не нужно корректировать, если изменится размер блока в В-дереве. Независимость данных проявляется и в том, что программисту совершенно не обязательно знать, как устроены такие процедуры ЯМД, как FINDKEY, INSERT и т. д. В определенном смысле язык запросов типа реляционной алгебры является более высокоуровневым, чем ЯМД Паскаль, 310
поскольку он менее последнего связан в машиной реализацией. На языке реляционной алгебры можно описывать алгоритмы, не задумываясь о том, что выполнение операций выбора или соеди- нения можно ускорить за счет введения индексов. Действительно, для пользователя-программиста не имеет значения, как меняется организация доступа к данным. Чем выше уровень языка запро- сов, тем в меньшей степени программы зависят от данных. Независимость данных возможна лишь при условии, что су- ществует некий механизм, посредством которого модель пользо- вателя, представляющая базу или ее часть, отображается на фи- зическую базу данных. Именно этот механизм, а не модель, дол- жен изменяться в случае модификации физической базы данных. Каждой модели или подсхеме должен отвечать отдельный меха- низм отображения. При изменении физической базы данных со- ответствующим образом преобразуются все эти механизмы. Исследовательской группой ANS1/SPARC предложен метод, позволяющий ограничиться преобразованием лишь одного ме- ханизма отображения. Он имеет определенное сходство с косвен- ной адресацией, описанной в разд. 7.6.2. Идея метода заклю- чается во введении промежуточного абстрактного представления, связанного, с одной стороны, с физической базой данных, а с дру- гой — со всеми пользовательскими моделями. Если модифици- руется физическая база данных, меняется только отображение, посредством которого она связана с промежуточным абстрактным представлением. В то же время вся совокупность механизмов, отображающих различные модели на одно абстрактное пред- ставление, остается неизменной. 9.7. АДМИНИСТРАТОР БАЗЫ ДАННЫХ В гл. 6 и 7 объявления базы данных рассматривались вне связи с организационными проблемами, обсуждаемыми в на- стоящей главе. Из нее видно, что сложные современные СУБД оснащаются многочисленными средствами, обеспечивающими па- раллельное выполнение транзакций, управление доступом, за- щиту данных и их восстановление после отказов, возможность использования разнообразных моделей или подсхем. База данных — это ресурс коллективного пользования, ко- торый должен с высокой эффективностью удовлетворять запросы многих абонентов. Обычно в штате вычислительного центра имеется специалист (иногда — группа), в компетенцию которого входит разработка путей доступа к данным, выбор средств вос- становления базы после отказов, внедрение методов управления доступом. Специалиста, отвечающего за все эти вопросы, назы- вают администратором базы данных. Основной задачей админи- стратора является поддержание надежного функционирования базы данных, создание условий для эффективной работы поль- зователей. 311
Именно администратор, а не программист-пользователь, за- нимается составлением DBD или описанием схемы базы данных. Фактически он может быть единственным лицом, знающим эту схему во всех подробностях. Пользователи же чаще всего зна- комы только со своими подсхемами. Администратор контроли- рует производительность базы (оценивая ее по среднему времени реакции на запрос), осуществляет настройку СУБД в соответ- ствии с меняющимися режимами ее эксплуатации. На фирме «Типико» администратор базы данных входит в ру- ководство отдела обработки информации. Во многих других компаниях машинные методы обработки и средства баз данных являются составными частями объединенной системы делопроиз- водства, включающей также выполняемые вручную процедуры обработки деловых бумаг. Общее руководство такой объединен- ной системой осуществляет главный администратор, которому подчиняется администратор базы данных. 9.8. СПРАВОЧНИКИ ДАННЫХ В больших и достаточно давно эксплуатируемых базах и системах машинной обработки данных количество имен полей, записей, файлов, отчетов и т. п. столь велико, что ни один спе- циалист не в состоянии их запомнить. В таких системах прихо- дится заводить специальные справочники, в которых перечислены все имена с необходимыми пояснениями. Хранятся подобные спра- вочники в тех же базах данных под управлением СУБД. Справочник данных содержит метаданные, т. е. данные о дан- ных. Последние включают как информацию, подлежащую обра- ботке, так и вспомогательную информацию, например, сведения о программах, программистах, а иногда и о других служащих, имеющих отношение к обработке данных. Справочные системы, разрабатываемые различными фирмами, существенно отличаются по своей структуре, назначению и терминологии. Небольшой справочник данных может быть организован как файл с полями переменной длины, снабженный набором допол- нительных указателей. Ниже приведен типичный перечень полей подобного файла. Имена полей файла СПРАВОЧНИК.ДАННЫХ ИМЯ {Уникальное имя, используемое в качестве первичного ключа справочника) ТИП.ЭЛЕМЕНТА {Например, ПОЛЕ, ЗАПИСЬ, НАБОР, ФАЙЛ, БАЗА ДАННЫХ, УКАЗАТЕЛЬ, СХЕМА, ПОДСХЕМА) ОПИСАНИЕ {Пояснительный текст, в который могут вставляться клю- чевые слова типа КЛИЕНТ; последние используются для организации ссылок с помощью указателей) ПРИНАДЛЕЖНОСТЬ {Если, например, ТИП.ЭЛЕМЕНТА—ПОЛЕ, то ПРИНАДЛЕЖНОСТЬ содержит список всех записей, в которые входит это поле) 312
ПСЕВДОНИМЫ {Иногда бывает удобно, чтобы один и тот же объект можно было называть различными именами. Все псев- донимы заносятся в это поле} ДЛИНА {Количество байтов, занимаемы® элементом} ПУТИ.ДОСТУПА. К_ ДРУГИМ. ЭЛЕМЕНТАМ {В поле перечислены все пути доступа (например, указатели), ведущие от данного элемента к лю- бому другому элементу} ЧАСТОТА. ДОСТУПА {Частота доступа к данному элементу} ДАТА. И. ВРЕМЯ- ПОСЛЕДНЕГО. СЧИТЫВАНИЯ ДАТА. И. ВРЕМЯ- ПОСЛЕДНЕЙ. КОРРЕКТИРОВКИ ПОЛЬЗОВАТЕЛЬ. ОСУЩЕСТВИВШИЙ. ПОСЛЕДНИЙ. ДОСТУП {Имя поль - зователя} СРЕДНЕЕ. ВРЕМЯ-ДОСТУПА {Показатель, иарактеризующий произво- дительность СУБД} ВЛАДЕЛЕЦ {Владелец данного элемента. Если элемент является файлом, в поле указывается имя его владельца. Если это база данных, в поле заносится имя ее администратора} ПРАВА. ДОСТУПА {В поле перечислены все лица, имеющие право до- ступа к элементу, и разрешенные им операции} СЕКРЕТНАЯ-БЛОКИРОВКА {Содержимое поля может быть различным, в том числе противоположным предше- ствующим} Имя второго поля в этом перечне нельзя признать удачным. Дело в том, что нет общепринятого термина, который объеди- нил бы понятия файла, поля, базы данных, подсхемы и т. д. Остается принять, что ЭЛЕМЕНТ — это любой снабженный именем объект, входящий в состав системы обработки данных. Справочник данных может использоваться и как справочник файлов операционной системы. В этом случае каждая его запись описывает отдельный файл. Кроме имени, она содержит обозна- чение способа организации файла и физические адреса, необхо- димые для выполнения операций доступа. Справочник данных помогает администратору и пользовате- лям базы данных ориентироваться в множестве имен. Он может способствовать и их стандартизации. Фрагменты содержимого справочника могут использоваться при составлении документации по системе обработки данных. Достоинство справочника и в том, что с его помощью пользователям, программирующим на ЯМД, удается сэкономить время, перенося из него в свои программы объявления особо сложных записей. В некоторых системах та- кое копирование производится в обязательном порядке. Этим достигаются сразу две цели: усиливается контроль за использо- ванием имен и обеспечивается постоянное обновление самого справочника.
ОТВЕТЫ К УПРАЖНЕНИЯМ Упражнения 4.7 1.а PROGRAM CRINV(TINVOICE,INVOICE); TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNOzlO..99; DATE:900101..991231; AMOUNT:REAL END; VAR TINVOICE:TEXT; INVOICE:FILE OF INVREC; BUF:INVREC; BEGIN RESET (UNVOICE); REWRITE(INVOICE); WHILE NOT EOF(TINVOICE) DO BEGIN WITH BUF DO READLN(TINVOICE,INVOICENO, CUSTOMERNO,DATE,AMOUNT); WRITE(INVOICE,BUF) END END. l.B PROGRAM OUTINV(INVOICE,OUTPUT); TYPE INVREC=RECORD INVOICENO: 10ОО..9999; 314
CUSTOMERNO:10..99; DATE:900101..991231 ; AMOUNT:REAL END; VAR INVOICE:FILE OF INVREC; BUF:INVREC; BEGIN RESET (INVOICE); WHILE NOT EOF(INVOICE) DO BEGIN WITH BUF DO READ(INVOICE,BUF); WRITELN(INVOICENO,CUSTOMERNO, DATEAMOUNT) END END. PROGRAM OUTCX(INVOICE,INPUT,OUTPUT); TYPE INVREC=RECORD INVOICENO:1000..9999; CUSTOMERNO:10..99; DATE:900101 ..991231 ; AMOUNT:REAL END; VAR CX:10..99; BUF:INVREC; INVOICE:FILE QF INVREC; COUNTER:INTEGER; BEGIN COUNTER:=0; 315
RESET(INVOICE); READ(CX); WHILE NOT EOF(INVOICE) DO BEGIN READ(INVOICE.BUF); WITH BUF DO IF CX=CUSTOMERNO THEN BEGIN WRITELN(INVOICENO,CUSTOMERNO, DATE,AMOUNT); COUNTER:=1 END END; IF COUNTERS THEN WRITELN('ЗАПИСЕЙ С ВВЕДЕННЫМ ЗНАЧЕНИЕМ CX НЕТ’) END. 2.6 PROGRAM OUTMOUNT(INVOICE,INPUT.OUTPUT); TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNO:10..99; DATE:900101..991231; AMOUNT:REAL END; VAR CX:10..99; BUF:INVREC; INVOICE:FILE OF INVREC; SUM:REAL; BEGIN SUM:=O; RESET(INVOICE); 316
READ(CX); WHILE NOT EOF(INVOICE) DO BEGIN READ(INVOICE,BUF); WITH BUF DO IF CX=CUSTOMERNO THEN SUM:=SUM+AMOUNT END; WRITELN(SUM) END. 3.6 PROGRAM DELINV(INVOICE,NEWINV,INPUT,OUTPUT); TYPE INVREC=RECORD INVOICENO: 1OOO..9999; CUSTOMERNO:10..99; DATE:900101..991231; AMOUNT:REAL END; VAR COUNTER:INTEGER; CY:1000..9999; BUF:INVREC; INVOICE,NEWINV:FILE OF INVREC; BEGIN RESET(INVOICE); REWRITE(NEWINV); READ(CY); COUNTER:=0; WHILE NOT EOF(INVOICE) DO BEGIN READ(INVOICE,BUF); IF CYOBUF. INVOICENO THEN 317
WRITE(NEWINV,BUF) ELSE COUNTER:=1 END IF COUNTERS THEN WRITELN('ЗАПИСЕЙ CO ЗНАЧЕНИЕМ ПОЛЯ INVOICENO,РАВНЫМ ВВЕДЕННОМУ, НЕТ'); END. 4.а PROGRAM UPDATEINV(INVOICE,NEWINV,INPUT,OUTPUT); TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNO:10..99; DATE:900101..991231; AMOUNT:REAL END; VAR STOPLOOP:BOOLEAN; BUF:INVREC; INVOICE,NEWINV:FILE OF INVREC; BEGIN RESET(INVOICE); REWRITE(NEWINV); STOPLOOP:=EOF(INVOICE); WITH BUF DO BEGIN READ(INVOICENO,CUSTOMERNO,DATE,AMOUNT); WHILE NOT STOPLOOP DO BEGIN IF INVOICENO=INVOICE'r. INVOICENO THEN BEGIN STOPLOOP:=TRUE; WRITELN('ЗНАЧЕНИЕ ПОЛЯ INVOICENO ОДНОЙ 318
ИЗ ЗАПИСЕЙ совпадает С ВВЕДЕННЫМ') END ELSE IF INVOICENO<INVOICE*.INVOICENO THEN BEGIN STOPLOOP:=TRUE; WRITE(NEWINV,BUF) END; NEWINV*:=INVOICE*; GET(INVOICE); PUT(NEWINV) END; IF EOF(INVOICE) THEN WRITE(NEWINV,BUF) ELSE WHILE NOT. EOF(INVOICE) DO BEGIN NEWINV*:=INVOICE*; PUT(NEWINV); GET(INVOICE) END END END. 5. PROGRAM MODINV(IVOICE,INPUT,OUTPUT); TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNO:10..99; DATE:900101..991231; AMOUNT:REAL END; VAR CH:CHAR; STOPLOOP : BOOLEAN; 319
BUF:INVREC; NEWINV,INVOICE:FILE OF INVREC; BEGIN RESET(INVOICE); REWRITE(NEWINV); STOPLOOP:=EOF(INVOICE); WITH BUF DO BEGIN READ(INVOICENO,CH); WHILE NOT STOPLOOP DO BEGIN NEWINV*:=INVOICE*; IF INVOICENO=INVOCE*.INVOICENO THEN BEGIN STOPLOOP:=TRUE; IF CH='D' THEN BEGIN READ(DATE); NEWINV*.DATE=DATE END EISE IF CH='A' THEN BEGIN READ(AMOUNT); NEWINV*.AMOUNT=AMOUNT END ELSE WRITELN('ВВЕДЕННЫЙ СИМВОЛ HE D И HE A') END; PUT(NEWINV); GET(INVOICE) END END; IF EOF(INVOICE) THEN WRITELN('B ФАИЛЕ НЕТ ЗАПИСИ, ЗНАЧЕНИЕ ПОЛЯ INVOICENO КОТОРОЙ СОВПАДАЕТ С ВВЕДЕННЫМ'); 320
ELSE BEGIN WHILE NOT EOF(INVOICE) DO BEGIN NEWINV*:=INVOICE*; PUT(NEWINV); GET(INVOICE) END END END. 6.В PROGRAM DELINV(INVOICE,DTRANSAC,NEWINV); TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNO: 10..99; DATE:900101 ..991231; AMOUNT:REAL END; VAR INVOICE,NEWINV:PILE OP INVREC; STOPLOOP;BOOLEAN; DTRANSAC:TEXT; 1:1000..9999; BEGIN RESET(DTRANSAC); RESET(INVOICE); REWRITE(NEWINV); WHILE NOT EOF (DTRANSAC) DO BEGIN READ (DTRANSAC,1); STOPLOOP:=FALSE; WHILE NOT STOPLOOP AND NOT EOF(INVOICE) DO BEGIN IF I=INVOICE*.INVOICENO THEN BEGIN 321
STOPLOOP:=TRUE; GET(INVOICE) END ELSE IF I>INVOICE1*.INVOICENO THEN STOPLOOP:=TRUE ELSE BEGIN NEWINV1* ^INVOICE1*; PUT (NEWINV); GET(INVOICE) END END END; WHILE NOT EOF(INVOICE) DO BEGIN NEWINV1* ^INVOICE1*; PUT (NEWINV); GET(INVOICE) END END. 6.Д PROGRAM DELINV1(INVOICE,NEWINV.DTRANSAC.OUTPUT) TYPE INVREC=RECORD INVOICENO:10ОО..9999; CUSTOMERNO:10..99; DATE:900101..991231 AMOUNT:REAL END; VAR INVOICE,NEWINV:FILE OF INVREC; DTRANSAC:TEXT; 1:1000..9999; STOPLOOP:BOOLEAN; 322
BEGIN RESET(DTRANSAC); RESET(INVOICE); RESET(NEWINV); WHILE NOT EOF(DTRANSAC) DO BEGIN READ(DTRANSAC.I); STOPLOOP:=FALSE; WHILE NOT STOPLOOP AND NOT EOF(INVOICE) DO BEGIN IF I=INVOICE*.INVOICENO THEN BEGIN STOPLOOP:=TRUE; GET(INVOICE) END ELSE IF I > INVOKE*. INVOICENO THEN BEGIN STOPLOOP:=TRUE; WRITELN(’ЗАПИСЬ В ФАИЛЕ INVOICE HE НАЙДЕНА’) END ELSE BEGIN NEWINV*:=INVOCE*; PUT(NEWINV); GET(INVOICE) END END; IF EOF(INVOICE) THEN WRITELN(’ЗАПИСЬ В ФАИЛЕ INVOICE HE НАЙДЕНА’) END; WHILE NOT EOF(INVOICE) DO BEGIN NEWINV*:=INVOICE*; PUT(NEWINV); GET(INVOICE) 323
END END. 7. PROGRAM MODDAINV(INVOICE.NEWINV,VTRANS,OUTPUT) TYPE INVREC=RECORD INVOICENO:1OOO..9999; CUST0MERN0:10..99; DATE:900101..991231; AMOUNT:REAL END j TRANREC=RECORD INVOICENO: 10ОО.. 9999; CASE TAG:CHAR OF ’A’:(AMOUNT:REAL); •D’:(DATE:900101..991231) END; VAR I:1000..9999; CH:CHAR; AM:REAL; DA:900101..991231; NEWINV,INVOICE:FILE OF INVREC; VTRANS:TEXT; STOPLOOP:BOOLEAN; BEGIN RESET(INVOICE); REWRITE(NEWINV); RESET(VTRANS); WHILE NOT EOF(VTRANS) DO BEGIN READ(VTRANS,I,CH); STOPLOOP:=FALSE; 324
WHILE NOT STOPLOOP AND NOT EOF(INVOICE) DO BEGIN IF 1=INVOCE*.INVOICENO THEN BEGIN NEWINV*:=INVOICE*; STOPLOOP:=TRUE; IF CH='D’ THEN BEGIN READLN(VTRANS,DA) NEWINV*.DATE:=DA END ELSE IP CH='A* THEN BEGIN READLN(VTRANS,AM); NEWINV*.AMOUNT!=AM END - ELSE WRITELN(’В ПОЛЕ TAG ЗАПИСИ ФАЙЛА VTRANS HE СОДЕРЖИТСЯ НИ СИМВОЛ А НИ СИМВОЛ D'); PUT(NEWINV); GET(INVOICE) END ELSE IF I>INVOICE*.INVOICE THEN BEGIN STOPLOOP:=TRUE; WRITELN('ЗАПИСИ С ИСКОМЫМ ЗНАЧЕНИЕМ ПОЛЯ INVOICENO В ФАИЛЕ INVOICE НЕТ') END EISE BEGIN NEWINV*=INVOICE*; PUT(NEWINV); GET (INVOICE) END END; 325
В ФАИЛЕ INVOICE НЕТ*) END; WHILE NOT EOF(INVOICE) DO BEGIN NEWINV*:=INVOICE*; PUT(NEWINV); GET(INVOICE) END END. Упражнения 4.9 1.6 PROCEDURE «INSERT; BEGIN CURRMONTH*.NEXTMONTH:=LISTHEAD; LISTHEAD:=CURRMONTH END; PROCEDURE ONTPUTNAMEFORMONTHNO(N:INTEGER); VAR CURRENT:MONTHPTR; FOUND: BOOLEAN ;(* ЛОГИЧЕСКАЯ ПЕРЕМЕННАЯ, СИГНАЛИЗИРУЮЩАЯ ОБ ОБНАРУЖЕНИИ ИСКОМОЙ ЗАПИСИ В СПИСКЕ*) BEGIN WIHT CURRENT* DO BEGIN FOUND:=FALSE; CURRENT:=LISTHEAD;(*nOHCK НАЧИНАЕТСЯ С ГОЛОВЫ СПИСКА») WHILE (CURRENT <>NIL) AND NOT FOUND DO BEGIN FOUND:=(MONTHNUMBER=N); CURRENT :=NEXTMONTH END; IF FOUND THEN WRITELN(MONTHNAME) ELSE WRITELN(*МЕСЯЦ С ЗАДАННЫМ НОМЕРОМ В СПИСКЕ НЕ ОБНАРУЖЕН*) 326
END END; 2.a PROCEDURE INSERT(CURRENTWORD:WORDPTR); VAR FOLLOWWORD:WORDPTR; I:INTEGER; BEGIN NEW(FOLLOWWORD); (♦УКАЗАТЕЛЬ ССЫЛАЕТСЯ НА ВВОДИМОЕ СЛОВО♦) CURRENTWORD*.NEXTWORD:=FOLLOWWORD; FOLLOWWORD1*. NEXTWORD: =CURRENTWORD*. NEXTWORD; (♦СЧИТАЕТСЯ, ЧТО ВВОДИТСЯ РОВНО 10 СИМВОЛОВ») FOR I:=1 ТО 10 DO READ(FOLLOWWORD*.WORD(I]) • END; 2.6 PROCEDURE DELETE(PREVIOUSWORD:WORDPTR;VAR CURRENTWORD:WORDPTR) BEGIN IF CURRENTWORD*.WORD='ZZZZZZZZZZ’ THEN WRITELN(’ПОПЫТКА УДАЛИТЬ ЗАПИСЬ ZZZZZZZZZZ '); ELSE BEGIN PREVIOUSWORD*.NEXTWORD:=CURRENTWORD*.NEXTWORD; DISPOSE(CURRENTWORD); CURRENTWORD: =PREVIOUSWORD*. NEXTWORD END END; 2.2 PROGRAM TOYEDITOR (INPUT, OUTPUT); TYPE 327
WORDTYPE=PACKED ARRAY(1..10] OF CHAR; WORDPTR=*WORDRECTYPE; WOfiDRECTYPE=RECORD WORD:WORDTYPE; NEXTWORD:WORDPTR END; VAR FOUND:BOOLEAN; PREVIOUSWORD,CURRENTWORD:WORDPTR; SELECTOR:CHAR; (•ТЕКСТЫ ПРОЦЕДУР INSERT И DELETE ОПУЩЕНЫ») BEGIN NEW(CURRENTWORD); CURRENTWORD*.WORD:=’ZZZZZZZZZZ'; CURRENTWORD*.NEXTWORD:=CURRENTWORD; PREVIOUSWORD:=CURRENTWORD; REPEAT WRITELN('ДЛЯ ВЫВОДА СЛЕДУЮЩЕГО СЛОВА ВВЕДИТЕ СИМВОЛ И.ДЛЯ ВСТАВКИ СЛОВА -1.ДДЯ УНИЧТОЖЕНИЯ-D'); WRITELN('ДЛЯ ВЫВОДА ВСЕЙ ПОСЛЕДОВАТЕЛЬНОСТИ СЛОВ-Т, ДЛЯ ЗАВЕРШЕНИЯ РАБОТЫ-Х'); READLN(SELECTOR); IF SELECTOR IN I'N',*1’,'D','T','X') THEN CASE SELECTOR OF 'N':BEGIN PREVOIUSWORD:=CURRENTWORD; CURRENTWORD:=CURRENTWORD*.NEXTWORD; WRITELN(CURRENTWORD*.WORD) END; 'I':BEGIN WRITELN('ВВЕДИТЕ НОВОЕ СЛОВО'); INSERT(CURRENTWORD) END; 'D':DELETE(PREVIOUSWORD,CURRENTWORD); 328
•X’:WRITELN(’РАБОТА ЗАВЕРШЕНА’); ’T’:BEGIN IF GURRENTWORD=CURRENTWORD*. NEXTWORD THEN WRITELN(’В СПИСКЕ ТОЛЬКО ОДИН ЭЛЕМЕНТ*) ELSE BEGIN FOUND:=FALSE; WHILE NOT FOUND DO BEGIN FOUND:=(CURRENTWORD*.WORD=’ZZZZZZZZZZ*) CURRENTWORD:=CURRENTWORD*NEXTWORD END WHILE FOUND DO BEGIN WRITE(CURRENTWORD*.WORD,’ ’); CURRENTWORD:=CURRENTWORD*.NEXTWORD; FOUND:=(CURRENTWORD*.WORD=’ZZZZZZZZZZ’) END END END END; UNTIL SELECTORS X’ END. Упражнения 5.6 l.a PROGRAM READSEQ(INPUT,OUTPUT); INVOKE E1; VAR I:RANGE; BEGIN STARTTRANSACTION(UNRESTRICTED); WRITELN(’ВВЕДИТЕ ПОСЛЕДОВАТЕЛЬНОСТЬ СИМВОЛОВ ДЛИНОЙ- НЕ БОЛЕЕ LENGTH’); 1=0; WHILE (KLENGTH) AND NOT EOLN(INPUT) DO 329
BEGIN I:=I+1; READ(TEXTLINEII)) END; ENDTRANSACTION END. 1.6 PROGRAM WRITESEQ(INPUT.OUTPUT); INVOKE El; VAR I,L,U:RANGE; BEGIN STARTTRANSACTION(READONLY); WRITELN(’ВВЕДИТЕ L И U, ПРИЧЕМ L HE ДОЛЖНО БЫТЬ БОЛЬШЕ U’); READ(L.U); IP L<U THEN FOR I:=L TO U DO WRITE(TEXTLINEII)); ELSE WRITELNCL БОЛЬШЕ U'); ENDTRANSACTION END. 2.a DBDNAME AP TYPE IND='A’..'F'; MATR=ARRAYIIND,IND] OP INTEGER; VAR DIST:MATR; FINISH 2.6 PROGRAM ENTERAF (INPUT); INVOKE AP; 330
VAR J,I:IND; BEGIN STARTTRANSACTION(UNRESTRICTED); FOR I:='A' TO 'F' DO FOR J:='A’ TO *F' DO IF (I>=J) THEN READ(DIST(I,J)); ENDTRANSACTION END. 2.в PROGRAM DISTAF(INPUT,OUTPUT); INVOKE AF; VAR F,M,L:IND; SUM:INTEGER; BEGIN STARTTRANSACTION(READONLY); WRITELN('ВВЕДИТЕ ТРИ БУКВЫ, ЛЕЖАЩИЕ В ИНТЕРВАЛЕ A-F READLN(F,M,L); IF F<M THEN SUM:=DIST(M,F) ELSE SUM:=DIST(F,M]; IF L<M THEN SUM:=SUM+DISTIM,D ELSE SUMt=SUM+DIST(L,M) WRITELN('РАССТОЯНИЕ РАВНО',SUM:5); ENDTRANSACTION END. 3.a DBDNAME DIG TYPE DS='0'..'9'? VAR DIGITWORDS:ARRAY(1..40)>0F CHAR; 331
WHEREITSTARTS:ARRAYI' Q'..' 9' I OF 1..40; FINISH. 3.6 PROGRAM READDIG(INPUT); INVOKE DIG; VAR I: INTEGER; BEGIN STARTTRANSACTION(READONLY); FOR I: = 1 TO 40 DO READ(DIGITWORDSII)); ENDTRANSACTION END. 3.B PROGRAM READST(INPUT); INVOKE DIG; VAR J:DS; BEGIN STARTTRANSACTION(READONLY); FOR J:=’O’ TO '9' DO READ(WHEREITSTARTS(J)); ENDTRANSACTION END. З.Г PROGRAM NAMEDIGIT(INPUT.OUTPUT); INVOKE DIG; VAR SDIG:DS; К:INTEGER; BEGIN 332
STARTTRANSACTION(READONLY); WRITELN(’ВВЕДИТЕ ЦИФРУ ОТ О До 9*); READ(SDIG); FOR К:^WHEREITSTARTSISDIG) TO WHEREITSTARTSISUCC(SD1G)J-1 DO READ(DIGITWORDSIK)); ENDTRANSACTION END. 4.a PROGRAM ENTER JY; INVOKE JY; VAR P,S:INVPTRTYPE; INVREC: INVTYPE; FOUND‘.BOOLEAN; BEGIN STARTTRANSACTION(UNRESTRICTED); WRITELN (’ВВЕДИТЕ ЗНАЧЕНИЯ ITEMREFNO,QUANTITYINSTOCK, PHICEPERUNIT,SUPPLIERNO’); WITH INVREC DO BEGIN READLN (ITEMREFNO, QUANTITYINSTOCK, PRI CEPERUN IT, SUPPLIERNO); S:=NIL; FOUND:=FALSE; P:=STARTPOINTER; WHILE (PONIL) AND NOT FOUND DO BEGIN IF P* . ITEMREFNO^ ITENREFNO THEN BEGIN S:=P; P:-P*. NEXT ITEM END ELSE 333
BEGIN POUND:=TRUE; IF P*.ITEMREFNO>ITEMREFNO THEN BEGIN CREATE(P); P*.NEXTITEM:=St.NEXTITEM; S*.NEXTITEM:=P END END END; IF P=NIL THEN (•ЗАПИСЬ СТАНОВИТСЯ ПОСЛЕДНЕЙ В СПИСКЕ*) BEGIN CREATE(Р); (•ЕСЛИ S=NIL, ТО СПИСОК ПЕРЕД ВКЛЮЧЕНИЕМ ПУСТ») IF SoNIL THEN S*.NEXTITEM: =P; ELSE STARTPOINTER:=P; P*.NEXTITEM:=NIL END PT.ITEMREFNO:=ITEMREFNO; P*.QUANTITYINSTOCK:=QUANTITYINSTOCK; P*.PRICEPERUNIT:=PRICEPERUNIT; P*.SUPPLIERNO:=SUPPLIERNO END; ENDTRANSACTION END. 4.6 PROGRAM GETJY; INVOKE JY; VAR P:INVPTRTYPE; ITEMREFNO:REFRANCE; FOUND:BOOLEAN; BEGIN 334
STARTTRANSACTION(READONLY); . WRITELN(’ВВЕДИТЕ ЗНАЧЕНИЕ ITEMREFNO’); READLN(ITEMREFNO); P=STARTPOINTER; FOUND:“FALSE; WHILE (PoNIL) AND NOT FOUND D®- BEGIN IF P*.ITEMREFNO<ITEMREFNO THEN P=P*.NEXTITEM ELSE IF P*. ITEMREFNO ITEMREFNO THEN BEGIN FOUND=TRUE; WRITELN(P*.QUANTITYINSTOCK, P*.PRICEPERUNIT,P*.SUPPLIERNO); END. ELSE BEGIN FOUND:=TRUE; WRITELN(’ЗАПИСЬ HE НАЙДЕНА’) ENp END IF P=NIL THEN WRITEIN (’ЗАПИСЬ HE НАЙДЕНА’); ENDTRANSACTION END. Упражнения 6.5.4. 3.8 PROGRAM TOYINSERT(INPUT); INVOKE TY; VAR BLOCKNUMBER:О..LASTBLOCKNO; WHICHRECORD:!..RECORDSPERBLOCK; M:STRING4; N:O..99999; 335
P:OVERFLOWPTR; FOUND,T:BOOLEAN; (♦ОБЪЯВЛЕНИЕ ФУНКЦИИ HASH ОПУЩЕНО*) BEGIN STARTTRANSACTION(UNRESTRICTED); READ(M.N); BLOCKNUMBER:=HASH(M); WITH HASHFILE[BLOCKNUMBER] DO BEGIN IF OVERFLOWFIELD=NIL THEN BEGIN FOUND:=FALSE; T:=FALSE; WHICHRECORD:=1 ; WHILE (NOT FOUND) AND (NOT T) DO BEGIN IF BLOCK[WHICHRECORD].NORECORD=TRUE THEN BEGIN BLOCK!WHICHRECORDJ.ITEMREFNO:=M; BLOCKIWHICHRECORD].QUANTITYINSTOCK:=N; BLOCK[WHICHRECORD].NORECORD:=FALSE; FOUND:=TRUE END ELSE IF WHICHRECORD=RECORDSPERBLOCK THEN T:=TRUE ELSE WHICHRECORD:=WHICHRECORD+1 END END; IF NOT FOUND THEN BEGIN CREATE(P); P*.NEXTITEM:=OVERFLOWFIELD; OVERFLOW?IELD:=P; P*.ITEMREFNO:=M; P*.QUANTITYINSTOCK:=N 336
END END; ENDTRANSACTION END. 3.6 PROGRAM TOYOUTPUT(INPUT,OUTPUT); INVOKE TY; VAR BLOCKNUMBER:О..LASTBLOCKNO; WHICHRECORD:F..RECORDSPERBLOCK; M:STRING4; PiOVERFLOWPTR; POUND,T:BOOLEAN; (•ОБЪЯВЛЕНИЕ ФУНКЦИИ HASH ОПУЩЕНО») BEGIN STARTTRANSACTION(READONLY); READ(M); BLOCKNUMBER:=HASHIM); WITH HASHFILEI BLOCKNUMBER] DO BEGIN T:=FALSE; FOUND:=PALSE; WHICHRECORD:=1; WHILE (NOT POUND) AND (NOT T) DU BEGIN IF BLOCK IWHICHRECORD1.NORECORD=FALSE AND BLOCK(WHICHRECORD).ITEMREFNO=M THEN BEGIN WRITELN(BLOCKIWHICHRECORD].QUANTITYINSTOCK); POUND:=TRUE END ELSE IF WHICHRECORD=RECORDSPERBLOCK THEN T=TRUE 12 Ульыан Дж. 337
ELSE WHICHREC0RD:=WHICHREC0RD+1 END; IF NOT FOUND THEN BEGIN IF OVERFLOWFIELD=NIL THEN WRITELN(’ЗАПИСЬ HE НАЙДЕНА* ) ELSE BEGIN P:=OVERFLOWFIELD; WHILE (PONIL) AND (NOT FOUND) DO BEGIN IF P*.ITEMREFNO=M THEN BEGIN WRITELN(P*.QUANTITYINSTOCK) FOUND:=TRUE END ELSE P:=PT.NEXTITEM END IF NOT FOUND THEN WHITEEN('ЗАПИСЬ HE НАЙДЕНА’) END END end; ENDTRANSACTION END. 3.B PROGRAM TOYDELETE(INPUT,OUTPUT); INVOKE TY; VAR BLOCKNUMBER:Q..LASTBLOCKNO; WHICHRECORD:1..RECORDSPERBLOCK; M:STRING4; P,S:OVERFLOWPTR;
FOUND, P0UND1 -.BOOLEAN; (•ОБЯВЛЕНИЕ ФУНКЦИИ HASH ОПУЩЕНО») BEGIN STARTTRANSACTION(UNRESTRICTED); READ(M); BLOCKNUMBER:=HASH[MJ; WITH HASHFILE(BLOCKNUMBER] DO BEGIN FOUND1:=FALSE; POUND:=FALSE; WHICHRECORD:=1; WHILE (NOT FOUND) AND (NOT FOUND1) DO BEGIN IP BLOCKIWHICHRECORD1.NORECORD=FALSE AND BLOCK(WHICHRECORD].ITEMREFNO=M THEN BEGIN BLOCKIWHICHRECORD1.NORECORD:=TRUE; POUND:=TRUE END ELSE IF WH1CHRECORD=RECORDSPERBLOCK THEN FOUND1=TRUE ELSE WHICHRECORD:=WHICHRECORD+1 END IF NOT FOUND THEN BEGIN IF OVERFLOWFIELD=NIL THEN WRITELN(’ЗАПИСЬ HE НАЙДЕНА’) ELSE BEGIN P:=OVERFLOWFIELD; WHILE PoNIL AND NOT FOUND DO BEGIN IF P*.ITEMREFNO=M THEN BEGIN 12* 339
FOUND:=TRUE; IF P=OVERFLOWFIELD THEN OVERFLOWFIELD:=P*.NEXTITEM ELSE S*.NEXTITEM:P*.NEXTITEM END RISE BEGIN S=P; P=P*.NEXTITEM END END END; IF NOT FOUND THEN WRITELN('ЗАПИСЬ HE НАЙДЕНА') END END; ENDTRANSACTION END. Упражнения 6.6.5 4.6 DBNAME MON; TYPE MONTHPTR=*MONTHRECTYPE; MONTHRANGE=1..12; STRING9=PACKED ARRAY[1..9J OF CHAR; MONTHRECTYPE=RECORD MONTHNUMBER:MONTHRANGE; MONTHNAME:STRING9; NEXTMONTH:MONTHPTR END; INDEXTYPE=RECORD MONTHNUMBER:MONTHRANGE; 340
INDEXPTR:MONTHPTR END; VAR LISTHEADZMONTHPTR; INDEXFILE:ARRAY(1..4] OF INDEXTYPE; FINISH. 1 .B PROGRAM MONTHREAD(MONTHS); INVOKE MON; VAR MONTHS:TEXT; S,P:MONTHPTR; I:INTEGER; BEGIN STARTTRANSACTION(UNRESTRICTED); RESET(MONTHS); FOR I:=1 TO 12 DO BEGIN CREATE(P); READLN(MONTHS.P*.MONTHNUMBER,P*.MONTHNAME); IF 1=1 THEN LISTHEAD:=P ELSE S-f.NEXTMONTH^P; S:=P; IF (1+2) MOD 3=0 THEN BEGIN INDEXFILEI(1+2) DIV 3).INDEXPTR:=P; INDEXFILEt(1+2) DIV 3).MONTHNUMBER:=I+2 END END; ENDTRANSACTION END. 341
1 .г PROGRAM MONTHSEARCH(INPUT,OUTPUT); INVOKE MON; VAR M:MONTHRANGE; P:MONTHPTR; FOUND:BOOLEAN; I:INTEGER; BEGIN STARTTRANSACTION(READONLY); READ(M); I:=1; P:«LISTHEAD; FOUND:=FALSE; WHILE NOT POUND DO BEGIN IF P*.MONTHNUMBER=M THEN BEGIN FOUND:=TRUE; WRITELN(P*.MONTHNAME) END ELSE BEGIN I:=I+1 ; P:=P*.MONTHPTR END END; WRITELN( ’ ЧИСЛО ПРОСМОТРЕННЫХ КЛЮЧЕЙ РАВНО’, I) ENDTRANSACTION END. 1. Д PROGRAM MONTHINDSEARCH(INPUT,OUTPUT); INVOKE MON; VAR M:MONTHRANGE; 342
P:MONTHPTR; FOUND:BOOLEAN; I:INTEGER; BEGIN STARTTRANSACTION(READONLY); READ(M); I:=1; FOUND:=FALSE; WHILE NOT FOUND DO BEGIN IF INDEXFILE[I].MONTHNUMBER?=M THEN BEGIN FOUND:=TRUE; P:=INDEXFILEII).INDEXPTR END ELSE I:=1+1 END; FOUND:=FALSE; WHILE NOT FOUND DO BEGIN IF P+.MONTHNUMBER=M THEN BEGIN FOUND:=TRUE; WRITELN(P*.MONTHNAME) END RISE BEGIN I:=I+1; P:=P+.MONTHPTR END END; WRITELN('ЧИСЛО ПРОСМОТРОВ РАВНО ’,I); ENDTRANSACTION END. 343
З.а DBNAME INVIND; TYPE STRING4-PACKED ARRAYI1..4] OF CHAR; BLOCKPTR-TBLOCK; TOYINVREC-RECORD ITEMREFNO:STRING4; QUANTITYINSTOCK:INTEGER END; BLOCK-ARRAY(1..6) OF TOYINVREC; 1NDEXPTR-*INDEXTYPE; INDEXTYPE-RECORD HIGHESTKEYINBLOCK:STRING4; POINTERTOBLOCK:BLOCKPTR; POINTERTONEXTINDEXRECORD:1NDEXPTR END; VAR LISTHEAD:INDEXPTR; FINISH 3.6 PROGRAM INITIND; INVOKE INVIND; BEGIN STARTTRANSACTION(UNRESTRICTED); LISTHEAD:-NIL; ENDTRANSACTION END. 3.B PROGRAM INV INSERT (INPUT); (•СЧИТАЕТСЯ, ЧТО ВВОДИМОЕ ЗНАЧЕНИЕ ITEMREFNO НЕ СОВПАДАЕТ’ •' СУЩЕСТВУЮЩИМИ») 344
INVOKE 1NVIND; VAR P,P1 ,P2:INDEXPTR; DOP,A:TOYINVREC; K, J,I-.INTEGER; BB,В:BLOCKPTR; N1S:O..6; FOUND:BOOLEAN; BEGIN STARTTRANSACTION(UNRESTRICTED); READ(A.ITEMREFNO ,A.QUANTITYINSTOCK); FOUND:=FALSE; P:=LISTHEAD; WHILE NOT FOUND AND P^NIL DO BEGIN IF P*.HIGHESTKEYINBLOCK<A.ITEMREFNO THEN BEGIN P2-P; P=PT .POINTERTUNEXTINDEXRECORD END ELSE FOUND:=TRUE END; IF NOT FOUND THEN BEGIN CREATE(B); ВП1 J. ITEMREFNO: = A. ITEMREFNO; B*(1].QUANTITYINSTOCK:=A.QUANTITYINSTOCK; CREATE(P1); P1*.HIGHESTKEYINBLOCK:=A.ITEMREFNO; P1 *. PO1NTERTOBLOCK: =B; P К . PO INTERTON EXT INDEXRECORD: =N IL ; IF LISTHEADONIL THEN P2T.POINTERTONEXT1NDEXBLOCK:=P1 345
END ETSE BEGIN BB:=P^.POINTERTOBLOCK; NIS:=O; I:=1; WHILE BBTI1 ].ITEMREFNO<>PT.HIGH£STKEYTNBLOCK DO BEGIN IF NIS=O THEN IF A.ITEMREFNO*BBT11].ITEMREFNO THEN NIS=I; I:-1+1; END IF 1=6 THEN BEGIN D0P-BB46]; К=1-Г END ELSE K:=I; FOR J:=K DOWNTO NIS DO BBTIJ+1J:=BBTIJJ; BB*INIS)=A; IF 1=6 THEN BEGIN CREATE (B); BT(1]:=BBT(51; BT12 J:=BBT16); B*13J:=DOP; CREATE(P1 ); P1*.HIGHESTKEYINBLOGK:=DOP.ITEMREFNO; PP. POINTERTOBLOCK: =B; P1T.POINTERTONEXTINDEXRECORD:= B*.POINTERTONEXTINDEXRECORD; P*.POINTERTONEXTINDEXRECORD:=P1 END 34'
END; ENDTRANSACTION END. З.Г PROGRAM INSEARCH(INPUT ,OUTPUT); INVOKE INVIND; VAR P:INDEXPTR; I:INTEGER; В:BLOCKPTR; FOUND1.FOUND:BOOLEAN; A: ITEMREFNO; BEGIN STARTTRANSACTION (READONLY); READ(A); FOUND:=FAI£E; P:=LISTHEAD; WHILE NOT FOUND AND PoNIL DO BEGIN IF P*.HIGHESTKEYINBLOCK>A THEN FOUND:=TRUE ELSE P:=P*.POINTERTONEXTINDEXRECORD END; IF P=NIL THEN WRITELN (' ИСКОМОЙ ЗАПИСИ В ИНДЕКСНО- ПОСЛЕДОВАТЕЛЬНОЙ СТРУКТУРЕ НЕТ’); IF FOUND THEN BEGIN В:=P*.PO1NTERTOBLOCK; I:=1; FOUND1:=FALSE; WHILE В*(I).INTERFERNO<A AND NOT FOUND1 DO IF BT[I) INTERFERNO=A THEN BEGIN 347
WRITELN (В* 11). QUANTITYINSTOCK); FOUND:=TRUE END ELSE I:=1+1; IF В*(IJ.ITEMREFNO>A THEN WRITELN (’ ИСКОМОЙ ЗАПИСИ В ИНДЕКСНО-ПОСЛЕДОВАТЕЛЬНОЙ СТРУКТУРЕ НЕТ’) END; ENDTRANSACTION END.. З.Д PROGRAM INVDELETEfINPUT,OUTPUT); INVOKE INVIND; VAR P1, P: INDEXPTR; I: INTEGER; B:BLOCKPTR; FOUND1.FOUND:BOOLEAN; A .-ITEMREFNO; BEGIN STARTTRANSACTION(INRESTRICTED); READ(A); FOUND :=FAISE; P:=L1STHEAD; WHILE NOT FOUND AND P^ >NIL DO BEGIN IF P^.HIGHESTKEYINBLOCK=A THEN FOUND:=TRUE ELSE BEGIN P1 :=P P:=P*.POINTERTONEXTINDEXRECORD END 348
END; IF P=NIL THEN WRITELN (’ИСКОМОМ ЗАПИСИ В ИНДЕКСНО-ПОСЛЕДОВАТЕЛЬНОЙ СТРУКТУРЕ НЕТ’); IF FOUND THEN BEGIN В:=PT.POINTERBLOCK; I :=1; FOUND1:^FALSE; WHILE BT(I].ITEMREFNO<A AND NOT FOUND1 DO IF BT (I). ITEMREFNO << A THEN I:=1+1 ELSE FOUND1:=TRUE; IF NOT FOUND1 THEN WRITELN (’ ИСКОМОЙ ЗАПИСИ НЕТ <B ИНДЕКСНО-ПОСЛЕДОВА- ТЕЛЬНОЙ СТРУКТУРЕ’); ELSE BEGIN >IF Рт .HIGHESTKEYlNBLOCKoA THEN (♦УДАЛЯЕМАЯ ЗАПИСЬ HE ЯВЛЯЕТСЯ ПОСЛЕДНЕЙ В БЛОКЕ*) WHILE Вт 11 ]. ITEMREFNOoP* .HIGHESTKEYINBLOCK DO BEGIN ВЧ1):=В*(1+1); I:=I+1 END ELSE IF lol THEN P*.HIGHESTKEYINBLOCK=Bt(1-1 J.ITEMREFNO ELSE («УДАЛЯЕМАЯ ЗАПИСЬ ЯВЛЯЕТСЯ ЕДИНСТВЕННОЙ В БЛОКЕ*) BEGIN IF P=LISTHEAD THEN LISTHEAD:= P*.POINTERTONEXTINDEXRECORD ELSE 349
* Р1.POINTERTONEXTINDEXRECORD:= Р*.POINTERTONEXTINDEXRECORD DELETE(P*.POINTERTOBLOCK); DELETE(P) END END; ENDTRANSACTION END END. Упражнения 6.8.9 2. a PROGRAM CARINSERT(INPUT,OUTPUT); INVOKE PK; VAR ’ INPUTRECORD: CARENTRY; * BEGIN STARTTRANSACTION (UNRESTRICTED) ; WITH INPUTRECORD DO READLN (REGNUMBER, TIMEIN, DATE); INSERT (CARSIN, INPUTRECORD); IF DBSTATUS=OKAY THEN ENDTRANSACTION ELSE BEGIN ABORTTRANSACTION; WRITELN(’НЕУДАЧА*) END END. 2.6 PROGRAM CARREMOVE(INPUT,OUTPUT); INVOKE PK; VAR CARREGNUMBER: STRINGY; 350
REGPTR:*CARRENTRY; BEGIN STARTTRANSACTION(UNRESTRICTED}; READLN(CARREGNUMBER); FINDKEY(CARINT,CARREGNUMBER,REGPTR); IF DBSTATUS=OKAY THEN BEGIN REMOVE(CARSIN,REGPTR); ENDTRANSACTION END ELSE BEGIN WRITELN(’ИСКОМАЯ ЗАПИСЬ HE НАЙДЕНА’); ABORTTRANSACTION END END. 2.В PROGRAM CARSEARCH(INPUT,OUTPUT); INVOKE PK; VAR CARREGNUMBER:STRINGY; REGPTR:*CARENTRY; BEGIN STARTTRANSACTION(READONLY); READLN(CARREGNUMBER); FINDKEY(CARSIN,CARREGNUMBER,REGPTR); IF DBSTATUS=OKAY THEN WRITELN(REGPTR*.TIMEIN,REGPTR*.DATE); ENDTRANSACTION END. 2.г PROGRAM CARLIST(OUTPUT); 351
INVOKE PK; VAR REGPTR:*CARENTRY; * BEGIN STARTTRANSACTION(READONLY); FINDFIRST(CARSIN,CARPTR); WHILE DBSTATUS=OKAY DO BEGIN WITH REGPTR* DO WRIГ ELN(REGNUMBER,TIMEIN ,DATE); FINDSUC3(CARSIN,CARPTR) END; ENDTRANSACTION END. 4. a PROGRAM INVJ NSERT(INPUT,OUTPUT); INVOKE MX; VAR ITEMFTR:INVPTR; INVRECORD: INVTYPE; INPUT?RICES:PRICEREC; BEGIN STARTTRANSACTION(UNRESTRICTED); WITH INVRECORD DO READLN(ITEMREFNO,QUANTITYINSTOCK, INPUTFRICES.FRICE,INPUTFRICES.DATE,SUPPLIERNO); FINDKEYlINVENTORY,INVRECORD.ITEMREFNO,ITEMPTR); IF DBSTATUS=NOTFOUND THEN BEGIN INSERT (INVRECORD.PRICES, INPUTPRICES.); IF DBSTATUSoOKAY THEN BEGIN WRITELN('НЕУДАЧА* ); 352
ABORTTRANSACTION END RISK BEGIN INSERT(INVENTORY,INVRECORD); IF DBSTATUSoOKAY THEN BEGIN WRITELN('НЕУДАЧА*); ABORTTRANSACTION END ELSE ENDTRANSACTION END END ELSE BEGIN WRITELN('ЗАПИСЬ С ВВЕДЕННЫМ ITEMREFNO УЖЕ ЕСТЬ’) ENDTRANSACTION END END. 4.6 PROGRAM INVENTER(INPUT,OUTPUT); INVOKE MX; VAR ITEMPTR:INVPTR; INPUTPRICES:PRICEREC; A:REFRANGE; BEGIN STARTTRANSACTION(UNRESTRICTED); WITH INPUTPRICES DO READLN(A,PRICE,DATE); FINDKEY(INVENTORY,A,ITEMPTR); IF DBSTATUS=NOTFOUND THEN BEGIN 353
WRITELN('ЗАПИСИ С ВВЕДЕННЫМ ЗНАЧЕНИЕМ ITEMREFNO НЕТ') ENDTRANSACTION END ELSE BEGIN INSERT(ITEMPTR1 .PRICES,INPUTPRICES); IF DBSTATUS=OKAY THEN ENDTRANSACTION ELSE BEGIN WRITELN(’НЕУДАЧА'); ABORTTRANSACTION END END END. 4.в PROGRAM INVSEARCH(INPUT,OUTPUT); INVOKE MX; VAR ITEMPTR:INVPTR; A: REFRANGE; SPTR:TPRICEREC; BEGIN STARTTRANSACTION(READONLY); ‘ READLN(A); FINDKEY (INVENTORY, A, ITEMPTR); IF DBSTATUS=NOTFOUNL THEN WRITELN('ЗАПИСИ С ВВЕДЕННЫМ ЗНАЧЕНИЕМ ITEMREFNO HET’); ELSE BEGIN FINDLAST(ITEMPRT*.PRICES.SPTR); IF DBSTATUS=OKAY THEN WRITELN(SPTRT.PRICE) ELSE WRITELN('НЕУДАЧА')
END ENDTRANSACTION END. 4.г PROGRAM INVDATESEARCH(INPUT,OUTPUT); INVOKE MX; VAR SPTR:*PRICEREC; A:REFRANGE; В:880101..940101; FOUND:BOOLEAN; BEGIN STARTTRANSACTION(READONLY); READLN(A.B); FINDKEY(INVENTORY,A,ITEMPTR); IF DBSTATUS=NOT FOUND THEN WRITELN('ЗАПИСИ С ВВЕДЕННЫМ ЗНАЧЕНИЕМ ITEMREFNO HET’); ELSE BEGIN FINDLAST(ITEMPTR*.PRICES,SPTR); IF DBSTATUSOOKAY THEN WRITELN ('НЕУДАЧА') ELSE BEGIN FOUND:=FALSE; WHILE DBSTATUS=OKAY AND NOT FOUND DO IF SPTR*.DATE<=B THEN BEGIN WRITELN(SPTR*.PRICE); FOUND:=TRUE END ELSE FINDPRED(ITEMPTR*.PRICES,SPTR); IF NOT FOUND THEN. WRITELN('НЕУДАЧА’) END 355
END; ENDTRANSACTION END. Упражнения 7.7 2.6 PROGRAM OUTINV(OUTPUT); INVOKE XS; VAR INDEXPTR:*INDEXLEAF; BEGIN STARTTRANSACTION(READONLY); FINDFIRST(PRIMARYINDEX,INDEXPRT); WHILE DBSTATUS=OKAY DO BEGIN WITH INDEXPTR*.POINTER* DO WRITELN(ITEMREFNO,QUANTITYINSTOCK PRICEPERUNIT.SUPPLIERNO); FINDSUCC(PRIMARYINDEX,INDEXPTR) END ENDTRANSACTION END. 2.в PROGRAM OUTREFINV(INPUT,OUTPUT); INVOKE XS; VAR INDEXPTR:*INDEXLEAF; I:REFRANGE; BEGIN STARTTRANSACTION(READONLY); READLN(I); FINDKEY (PRIMARYINDEX,I,INDEXPTR); 356
IF DBSTATUS=OKAY THEN WRITELN(INDEX*.POINTER*.QUANTITYINSTOCK); ENDTRANSACTION END. 2.г PROGRAM OUTSUP INV(INPUT,OUTPUT); INVOKE XS; VAR S:SUPPRANGE; 1NDEXPTR:* INDEXLEAF; BEGIN STARTTRANSACTION (READONLY) ; READLN(S); FINDFIRST (SUPPLIERINDEXtS), INDEXPTR); WHILE DBSTATUS=OKAY DO BEGIN WITH INDEXPTR*.POINTER* DO WRITELN(ITEMREFNO,QUANTITYINSTOCK); FINDSUCC(SUPPLIERINDEX IS),INDEXPTR) end; ENDTRANSACTION END. 2.Д PROGRAM OUT1 REF INV (INPUT, OUTPUT); INVOKE XS; VAR S:SUPPRANGE; INDEXPTR: * INDEXLEAF; BEGIN STARTTRANSACTION(READONLY); READLN(S); FINDFIRST(SUPPLIERINDEXIS),INDEXPTR); 357
WHILE DBSTATUS=OKAY DO BEGIN WITH INDEXPTR*.POINTER* DO IF QUANTITYINST0CK<8 THEN WRITELN(ITEMREFNO); FINDSUCC(SUPPLIERINDEXIS],INDEXPTR) ENDJ ENDTRANSACTION END. 2.0 PROGRAM OUT2REFINV(INPUT,OUTPUT); INVOKE XS; VAR J,S:SUPPRANGE; INDEXPTR:*INDEXLEAF; BEGIN STARTTRANSACTION(READONLY); READLN(S); FOR J:=1O TO 15 DO IF J<>S THEN BEGIN FINDFIRST(SUPPLIERINDEXIJ),INDEXPTR); WHILE DBSTATUS=OKAY DO BEGIN WITH INDEXPTR*.POINTER* DO IF QUANTITYINST0CK<8 THEN WRITELN(ITEMREFNO); FINDSUCC(SUPPLIERINDEX(J),INDEXPTR); END END; ENDTRANSACTION END. 358
2.x PROGRAM DELETEINV(INPUT,OUTPUT); INVOKE XS; VAR I:REFRANGE; S:SUPPLIERNO; INDEXPTR: * INDEXLEAF; BEGIN. STARTTRANSACTION(UNRESTRICTED); READLN(I); • FINDKEY(PRYMARYINDEX,I,INDEXPTR); IF DBSTATUSoOKAY THEN BEGIN WRITELN('ЗАПИСЬ HE НАЙДЕНА'); ABORTTRANSACTION END ELSE BEGIN S:=INDEXPTR*.POINTER*.SUPPLIERNO; (•УДАЛЕНИЕ ЗАПИСИ ИЗ ФАЙЛА INVENTORY*) DELETE(INDEXPTR*.POINTER); (•УДАЛЕНИЕ ЗАПИСИ ИЗ ПЕРВИЧНОГО ИНДЕКСА») REMOVE(PRIMARYINDEX,INDEXPTR); FINDKEY(SUPPLIERINDEX(S),I,INDEXPTR); IF DBSTATUS=OKAY THEN BEGIN (•УДАЛЕНИЕ ЗАПИСИ ИЗ ВТОРИЧНОГО ИНДЕКСА») REMOVE(SUPPLIERINDEXtS),INDEXPTR); ENDTRANSACTION END ELSE BEGIN WRITELN('ИСКОМОЙ ЗАПИСИ ВО ВТОРИЧНОМ ИНДЕКСЕ НЕ НАЙДЕНО'); ABORTTRANSACTION 359
END END END. 2.3 PROGRAM MODINV(INPUT,OUTPUT); INVOKE XS; VAR I:REFRANGE; Q:INTEGER; INDEXPTR:*INDEXLEAF; BEGIN STARTTRANSACTION(UNRESTRICTED); READLN(I.Q); FINDKEY(PRIMARYINDEX,I,INDEXPTR); IF DBSTATUSOOKAY THEN BEGIN WRITELNI.'ЗАПИСЬ HE НАЙДЕНА' <; ABORTTRANSACTION END ELSE BEGIN INDEXPTR*.POINTER*.OUANTITYINSTOCK=Q; ENDTRANSACTION END END. 360
З.а PROGRAM НАР4(INPUT,OUTPUT); INVOKE HA; VAR N,H:STRINGS; HPTR, FPTR: ’’'SUBSCRIPT; SOMEONE:BOOLEAN; HLEAFPTR,FLEAFPTR:*INDEXLEAF; BEGIN STARTTRANSACTION(READONLY); READLN(N.H); FINDKEY(HAIRINDEX,H,HLEAFPTR); SOMEONE:=(DBSTATUS=OKAY); DBSTATUS:=OKAY; FINDKEY(FIRSTNAMEIND.N,FLEAFPTR); IF DBSTATUS=OKAY THEN FINDFIRST(FLEAFPTR*.MEMBERS,FPTR); (♦ЕСТЬ ЭЛЕМЕНТЫ В ОБОИХ ИНДЕКСАХ*) WHILE (DBSTATUS=OKAY) AND SOMEONE DO BEGIN IF HPTR*<=FPTR* THEN BEGIN WRITELN(TABLE(HPTR*),SURNAME); FINDSUCC(HLEAFPTR*.MEMBERS,HPTR); SOMEONE:=(DBSTATUS=OKAY); IF HPTR*=FPTR* THEN FINDSUCC(FLEAFPTR*.MEMBERS,FPTR) END ELSE BEGIN WRITELN(TABLEIFPTR*].SURNAME); FINDSUCC(FLEAFPTR*.MEMBERS,FPTR); END END (♦ЕСТЬ ЭЛЕМЕНТЫ В ИНДЕКСЕ ПО HAIRCOLOUR*) WHILE SOMEONE DO 361
BEGIN WRITELN(TABLE(HPTR*].SURNAME); FINDSUCC (HLEAFPTR'1'. MEMBERS, HPTR); SOMEONE:=(DBSTATUS=OKAY) END; ENDTRANSACTION END. 3.6 PROGRAM HAP5(INPUT,OUTPUT); INVOKE HA; VAR H.N:STRINGS; LEAFPTR:’INDEXLEAF; SPTR:’SUBSCRIPT; .BEGIN STARTTRANSACTION(READONLY); READLN(N.H); FINDKEY(FIRSTNAMEIND,N,LEAFPTR); IF DBSTATUS=OKAY THEN BEGIN FINDFIRST(LEAFPTR’.MEMBERS,SPTR); WHILE DBSTATUS=OKAY DO BEGIN IF TABLEISPTR’] .HAIRCOLOURoH THEN WRITELN(TABLEISPTR’J.SURNAME); FINDSUCC(LEAFPTR’.MEMBERS,SPTR) END END; ENDTRANSACTION END. З.в PROGRAM HAP6(INPUT,OUTPUT); 362
INVOKE НА; VAR H:STRINGS; L, U .’INTEGER; LEAFPTR:* INDEXLEAF; SPTR-Л SUBSCRIPT; BEGIN STARTTRANSACTION (READONLY); READLN (H,L,U) ; FINDKEY(HAIRINDEX,H,LEAFPTR); IF DBSTATUS=OKAY THEN BEGIN FINDFIRST (LEAFPTR*. MEMBERS , SPRT); WHILE DBSTATUS=OKAY DO BEGIN WITH TABLE(SPRT*) DO IF HEIGHT<U AND HEIGHT>L THEN WRITELN(FIRSTNAME,SURNAME); F1NDSUCC(LEAFPTR*.MEMBERS,SPTR) END END; ENDTRANSACTION END. Упражнения 8«3 4.0 DBNAME DR; (•ФАЙЛЫ ДАННЫХ PERSONFILE И DRINKFILE ОРГАНИЗОВАНЫ В ВИДЕ R-ДЕРЕВЬЕВ* ) (•ПЕРВИЧНЫЕ ИНДЕКСЫ ДЛЯ НИХ НЕ ЗАВЕДЕНЫ») TYPE PERSONPTR=* PERSONTYPE; DRINKPTR=*DRINKTYPE; 363
LINKPTR=*LINKDRPER; DRINKTYPE=RECORD DRNAME:STRING10; COSTPERGLASS:REAL; QULD:MEMBERS END; PERSONTYPE=RECORD NAME:STRING12; QULP:MEMBERS END; LINKDRPER=RECORD IN1:DRINKPTR; QUNTITY:1..20; IN2:PERS0NPTR END; VAR PERSONFILE=BTREE OF PERSONTYPE ON NAME; DRINKFILE=BTREE OF DRINKTYPE ON DRNAME; FINISH. 4.г PROGRAM ALLDRINKSFORPER(INPUT,OUTPUT); INVOKE DR; VAR N:STRING12; PPTR:PERSONPTR; DPTR:DRINKPTR; MEMPTR: LINKPTF.; BEGIN STARTTRANSACTION(READONLY); READLN(N); FINDKEY(PERSONFILE,N,PTR); IF DBSTATUSOOKAY THEN WRITELN('ВВЕДЕННОЙ ФАМИЛИИ HE НАЙДЕНО’) 364
ELSE BEGIN FINDFIRST(PPTR*.QULP,MEMPTR); WHILE (DBSTATUS=OKAY) DO BEGIN WRITELN(MEMPTR*.IN1*.DRNAME, MEMPTR*.IN1*.COSTPERGLASS); FINDSUCC(PPTR*.QULP,MEMPTR) END END; ENDTRANSACTION END. 4.Д PROGRAM ALLCOSTDRINKS(INPUT,OUTPUT); INVOKE DR; VAR N:STRING12; PPTR:PERSONPTR; DPTR:DRINKPTR; MEMPTRJLINKPTR; ALLCOST:REAL; BEGIN STARTTRANSACTION(READONLY); («ВВОД ФАМИЛИИ») ’ READLN(N); FINDKEY(PERSONFILE.N,PPTR); IF DBSTATUSoOKAY THEN WRITELN(’ВВЕДЕННОЙ ФАМИЛИИ HE НАЙДЕНО В БАЗЕ ДАННЫХ’) ELSE BEGIN ALLCOST=O; FINDFIRST(PPTR*.QULP,MEMPTR); WHILE (DBSTATUS=OKAY) DO 365
BEGIN ALLCOST:=ALLCOST+MEMPTR*.QUANTITY»M04PTR*.INI *. COSTPERGLASS; FINDSUCC(PPTR*.QULP, MEMPTR) END; WRITELN(’ОБЩАЯ СТОИМОСТЬ»’,ALLCOST) END; ENDTRANSACTION END. 5.a PROGRAM TITLENUMAYEFSKY(OUTPUT); INVOKE MU; VAR MUSICIANLEAFPTR:*MUSICIANLEAF; MUSICIANPTR:TOMUSICIANS; COMPOSITIONSPTR:TOCOMPOSITIONS; I:INTEGER; BEGIN STARTTRANSACTION(READONLY); FINDKEY(MUSNAMEINDEX,'AYEFSKY'.MUSICIANLEAFPTR); IF DBSTATUS<>OKAY THEN WRITELN('НЕТ КОМПОЗИТОРА AYEFSKY'); ELSE ’ BEGIN MUSICIANPTR:=MUSICIANLEAFPTR*.POINTER; FINDFIRST(MUSICIANPTR*.COMPOSER,COMPOSIONSPTR); WHILE BDSTATUS=OKAY DO BEGIN WRITELN(COMPOSITIONSPTR*.TITLE,COMPOSITIONSPTR*. CDATE); I:=O; FINDFIRST(COMPOSITIONSPTR*.PERFORMED, PERFORMANCESPTR); 366
WHILE DBSTATUS ==OKAY DO BEGIN I:=I+1; FINDSUCC (COMPOSITIONSPTF1 .PERFORMED, PERFORMANCE SPTR END; DBSTATUS=OKAY; WRITELN (* ЧИСЛО ИСПОЛНЕНИЕ- ' ,1) ; FINDSUCC(MUSICIANPTR1 .COMPOSER, COMPOS ITIONSPTR) END END; ENDTRANSACTION END. 5.в PROGRAM MANTREEPERF(OUTPUT); INVOKE MU; VAR ENSEMBLESLEAFPTR: * ENSEMBLESLEAF; ENSEMBLESPTR:TOENSEMBLES; MEMBERSPTR: TOENSEMBLEMEMBERS; I .-INTEGER; BEGIN STARTTRANSACTION (READONLY); FINDFIRST(ENSEMBLEINDEX,ENSEMBLESLEAFPTR ; WHILE DBSTATUS=OKAY DO BEGIN ENSEMBLESPTR: =ENSEMBLESLEAFPTR^ . POINTER; I:=O; FINDFIRST(ENSEMBLESPTR1, .MEMBERS, MEMBERSPTR '; WHILE DBSTATUS=OKAY CO BEGIN I:=I+1; FINDSUCC(ENSEMBLESPIR*.MEMBERS,MEMBERSPTR)
END; IF I>=3 THEN WRITELN(ENSEMBLESPTR1.MEMBERS); DBSTATUS:=OKAY; FINDSUCC(ENSEMBLEINDEX,ENSEMBLESLEAFPTR) END; ENDTRANSACTION END. 5.Д PROGRAM ENAMESAXCL(OUTPUT); INVOKE MU; VAR ENSEMBLESLEAFPTR:'rENSEMBLESLEAF; ENSEMBLESPTR:TOENSEMBLES; MEMBERSPTR:TOENSEMBLEMEMBERS; FOUND1.FOUND :BOOLEAN; BEGIN STARTTRANSACTION(READONLY); FINDFIRST(ENSEMBLEINDEX,ENSEMBLESLEAFPTR); WHILE DBSTATUSOOKAY DO BEGIN ENSEMBLESPTR: = ENSEMBLES LEAFPTR1-. POINTER; FINDFIRST(ENSEMBLESPTR1,MEMBERS,MEMBERSPTR); FOUND1:=FALSE; FOUND :=FALSE; WHILE DBSTATUS=OKAY AND (NOT FOUND1 OR NOT FOUND) DO BEGIN IF MEMBERSPTR1.INSTRUMENTALIST1.INSTRUMENT= ’SAXOPHONE’ THEN FOUND1:=TRUE ELSE IF MEMBERSPTR1.INSTRUMENTALIST1.INSTRUMENT= ’CLARNET’ THEN FOUND :=TRUE; FINDSUCC(ENSEMBLESPTR1.MEMBERS,MEMBERSPTR) END.