数据库物理结构(InterBase 和 Firebird)
Alexey Kovyazin, Sergey Vostrikov, последнее обновление 05-июня-2004
Физическая структура базы данных
Зачем нам изучать физическую структуру базы данных InterBase?
Обычно, когда мы говорим о физической структуре базы данных InterBase, мы имеем в виду представление данных с точки зрения низкоуровневой организации данных - вплоть до уровня байтов. Многие программисты, разрабатывающие приложения с использованием языков высокого уровня, пренебрегают изучением низкоуровневых деталей. Однако знание основных принципов организации данных внутри базы данных является ключом к эффективному проектированию приложений баз данных. Поэтому мы совершим экскурс во внутренности организации базы данных InterBase и узнаем, как она устроена.
Итак, для чего предназначена система управления базами данных (СУБД)? Очевидно, для хранения и управления данными. Это звучит банально, но стоит задуматься. Пользователь помещает данные в СУБД, которая некоторым образом преобразует эти данные в понятные ей внутренние форматы. Вы можете представить «0 и 1», если слова «внутренний формат данных» вызывают некоторые трудности с ассоциациями. СУБД хранит эти данные и по первому требованию должна извлечь их из своего формата, преобразовать в подходящий вид и передать пользователю.
Предмет этой главы - как СУБД хранит свои данные, в каком виде и как они организованы на самом низком уровне. Мы постараемся объяснить вам, как из битов и байтов, лежащих на жестком диске, мы получаем ценные данные.
Файлы базы данных InterBase
Обычно, когда мы говорим о базе данных, мы имеем в виду саму СУБД и пользовательскую информацию, а также клиентские программы, работающие с данными. В этой главе мы будем рассматривать базу данных как файлы базы данных.
База данных InterBase представляет собой один или несколько файлов, содержащих информацию обо всем, что связано с этой базой. Информация о пользователях является исключением, поскольку пользователи определяются на уровне всего сервера и хранятся отдельно, в базе данных безопасности admin.ib (в версиях до 7 это был ISC4.GDB).
Совет: обратитесь к главе «Безопасность сервера и базы данных», чтобы узнать больше о принципах безопасности InterBase.
Итак, вся информация о базе данных хранится в этих файлах: сами данные, индексы, триггеры, хранимые процедуры и т.д.
База данных InterBase для среднего проекта представляет собой один файл, поскольку современные версии InterBase могут использовать 64-битный ввод-вывод для работы с файлом данных, что дает возможность иметь файл данных размером до 64 ГБ. Более ранние версии InterBase имели ограничение в 4 гигабайта на каждый файл базы данных (до 64 Тбайт для всей базы данных). Как можно предположить, 64 гигабайт вполне достаточно для хранения информации практически любого приложения базы данных. Но при необходимости мы можем разделить базу данных на несколько файлов. Кстати, существуют базы данных InterBase размером в сотни гигабайт.
IBSurgeon - проводник по базе данных InterBase
Нам необходимо детально знать структуру файлов базы данных InterBase. Поэтому желательно иметь какой-либо удобный инструмент, позволяющий работать непосредственно с файлами базы данных, а не через ядро сервера InterBase. Самый простой способ - использовать обычный шестнадцатеричный просмотрщик и пытаться понять структуру файлов базы данных, рассматривая их HEX-представление. Это была бы довольно утомительная работа.
Но, к счастью, существует инструмент для прямой работы с базами данных InterBase. Это IBSurgeon Editor - инструмент для прямой низкоуровневой работы с базами данных InterBase, который можно использовать для изучения внутренней структуры баз данных InterBase и диагностики поврежденных баз данных с целью их восстановления. Более подробно см. приложение «Инструменты администратора и разработчика InterBase».
IBSurgeon использует собственный альтернативный механизм доступа к базе данных, который позволяет открывать и просматривать базы данных в любом состоянии, включая сильно поврежденные, которые не могут быть открыты ядром сервера InterBase/FireBird/Yaffil.
Мы будем использовать IBSurgeon для иллюстрации внутренней структуры базы данных.
Файлы *.IB/*.FDB изнутри
IB - это расширение, рекомендуемое для файлов базы данных InterBase, а FDB - для Firebird (ранее это был GDB). Первое, что мы должны сказать о структуре файла IB, это то, что он представляет собой набор страниц строго определенного размера. Размер файла базы данных кратен размеру страницы, который неизменен для всех файлов этой базы данных. Различные версии InterBase поддерживают разные размеры страниц, что показано в таблице 1. Размер страницы устанавливается при создании базы данных и не может быть изменен в течение ее жизненного цикла. Другими словами, мы можем изменить размер страницы только при восстановлении базы данных из резервной копии.
Таблица 1. Размеры страниц, поддерживаемые различными версиями InterBase
| Версия InterBase | Размер страницы, байт | ||||
| 1024 | 2048 | 4096 | 8192 | 16384 | |
| InterBase 4.0 | * | * | * | * | |
| InterBase 5.x | * | * | * | * | |
| InterBase 6.x-7.x | * | * | * | * |
Чтение и запись данных в базе данных выполняются постранично, многие важные характеристики сервера и базы данных, такие как размер кэша базы данных, зависят от размера страницы и измеряются в «страницах».
Давайте откроем любую базу данных InterBase с помощью IBSurgeon. Достаточно дважды щелкнуть по файлу базы данных. Рисунок 1 показывает список страниц, который появляется после того, как IBSurgeon открыл базу данных:

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

Рисунок 2. Взаимосвязи между различными типами страниц в базе данных InterBase
Вы, должно быть, заметили, что некоторые типы страниц не имеют ссылок на другие типы страниц. Однако здесь нет противоречия, дело в том, что эти типы страниц связаны и используются на другом структурном уровне. Они могут быть связаны с таблицей RDB$PAGES и другими системными таблицами (эту таблицу и другие системные объекты мы рассмотрим ниже - в главе «Логическая структура базы данных»). На рисунке 2 мы можем видеть только явные ссылки между страницами на физическом уровне.
Рассмотрим подробно, какие типы страниц существуют в базе данных InterBase. В файле ods.h из набора исходных кодов InterBase содержится информация обо всех возможных типах страниц. Мы будем часто обращаться к этому файлу, чтобы получать данные не только об ODS, но и о многих других фундаментальных вещах ядра InterBase из первоисточника. Всего объявлено 11 типов страниц, но только 9 из них заслуживают объяснения (это хорошо видно из таблицы 2). Типы страниц с идентификаторами 0 и 1 не определены или не используются.
Таблица 3. Типы страниц в FB
| Определение в ods.h | Идентификатор типа страницы | Описание страницы |
| pag_undefined | 0 | Неопределенная - если страница имеет этот тип, она, вероятно, свободна |
| pag_header | 1 | Страница заголовка базы данных |
| pag_pages | 2 | Страница инвентаризации страниц (или страница инвентаризации пространства - SIP) |
| pag_transactions | 3 | Страница инвентаризации транзакций (TIP) |
| pag_pointer | 4 | Страница указателей |
| pag_data | 5 | Страница данных |
| pag_root | 6 | Корневая страница индекса |
| pag_index | 7 | Страница индекса (B-дерева) |
| pag_blob | 8 | Страница данных BLOB |
| pag_ids | 9 | Gen-ids |
| pag_log | 10 | Информация журнала упреждающей записи |
Каждая страница имеет свой заголовок, содержащий информацию о типе страницы и номере следующей страницы того же типа. Полный список параметров, которые содержит заголовок каждой страницы, мы можем получить, рассмотрев структуру pag в файле определений ods.h.
/\* 基本页面头 \*/
typedef struct pag {
SCHAR pag\_type; /\*页面类型标识符\*/
SCHAR pag\_flags; /\*页面标志\*/
USHORT pag\_checksum; /\*页面校验和:在5.0版本之后等于12345 \*/
ULONG pag\_generation; /\*页面代次 \*/
ULONG pag\_seqno; /\* 上次更新的WAL序列号 - 已弃用\*/
ULONG pag\_offset; /\* 上次更新的WAL偏移量 - 已弃用\*/
} \*PAG;
#### 页面类型及其用途
让我们详细考虑每种页面类型,了解它们的功能和所包含的信息。我们将一步一步来--从第一页开始。
对数据库的任何操作都始于读取数据库头页面(或头页面)。数据库头页面在所有数据库文件中排在第一位。因此,它在图2中首先显示(如果我们想象该图表示数据库文件从左到右、从上到下的扩展)。
头页面包含关于整个数据库的信息。在图3中,数据页面以IBSurgeon向我们展示的方式呈现:

图3. 数据库头页面。
您可以通过获取数据库统计信息来了解头页面的内容。为此,您可以使用命令行工具 **gstat** 或“管理员和InterBase开发者工具”应用程序列表中另一个更方便的InterBase管理工具。有关接收统计信息和头页面数据描述的更多详细信息,请参见“统计信息”章节。
应当注意,头页面包含诸如页面大小、ODS版本号(相关信息将在下面找到)、数据库创建日期、事务信息以及各种其他信息等重要信息。例如,实现ID存储了关于此数据库是在哪个操作系统下创建的信息。
连接数据库时,InterBase服务器从文件开头读取前1024字节的信息,并根据读取的值判断连接行中指定的文件是否为InterBase数据库。然后服务器从头页面读取ODS版本号和此数据库中的页面大小,如果ODS版本与服务器实现兼容,则使用从前1024字节获得的适当页面大小重新读取整个头页面。之后,从头页面读取其余重要的数据库参数,如读写模式、数据库方言等。
在头页面上有一个指向第一个指针页面的引用,指针页面存储指向包含元数据的数据页面的引用:RDB$Pages表(参见下文“InterBase数据库逻辑结构”章节)。在图2中,此引用用带有“数据库中第一个指针页面的编号”字样的箭头表示。服务器从头页面读取第一个指针页面的编号并继续到该页面。指针页面由有序的数据页面编号数组组成,这些数据页面构成某个表(表被视为由数据库逻辑结构描述的SQL对象)。现在您可以看到IBSurgeon如何解释指针页面(见图4):

图4. InterBase数据库指针页面
该页面包含一个数据页面向量;这些数据构成数据库中的某个表。该向量表示一个指针数组,对应于文件中数据页面的编号。服务器读取4字节的数据页面编号并继续到所需的数据页面。当它继续到RDB$Pages的第一个数据页面时,服务器开始构建内部数据库表示,该表示稍后由服务器用于所有数据库操作。RDB$Pages不仅存储指向包含数据库信息的数据页面的引用,还存储指向在提供数据库工作中发挥作用的其他页面的引用。
我们经常提到这个表,严格来说它属于数据库逻辑结构。然而,一切都是相互关联的,因此我们不能描述某件事而不提及另一件事。
重要页面类型之一是事务清单页面(TIP)。这些页面像所有页面一样由头部和主体部分组成,主体部分表示2字节序列的数组。这些序列描述数据库中事务的状态(有关事务的更多详细信息,请参见“事务”章节)。
表4. TIP中可能的事务状态
| | |
| --- | --- |
| PIP上的序列值 | 含义 |
| 0 | 事务尚未开始、处于活动状态或已丢失而未提交或回滚 |
| 1 | 事务执行了提交 |
| 2 | 事务执行了回滚 |
| 3 | 悬空事务(用于2PC) |
每个记录版本都有其事务标识符,这使得同时执行的事务能够“了解”彼此的状态并在多用户工作期间解决冲突(有关记录版本和其他内容的更多信息,请参见“InterBase多代架构”章节)。
数据库头页面、指针页面和TIP属于“维护”页面类型,仅由服务器使用。InterBase用户永远不会显式获取它们包含的信息。存储页面分配信息的页面(通常称为页面清单页面(PIP)或空间清单页面(SIP))也属于维护页面类型。这些页面从第二个页面开始定位,即第一个PIP紧跟在头页面之后,并以其他类型页面的固定页面间隔出现在数据库中。这些间隔的大小表示PIP出现在多少其他类型页面之后,取决于为此数据库设置的页面大小。页面清单页面不计入指针页面,也不在RDB$Pages中指向。这些页面的完整性对整个数据库的成功工作至关重要,因为PIP内容描述了数据库中所有其他页面的状态。每个数据库页面可以有3种状态:未分配、已分配且有空间、已分配且_已满_。当需要为新数据提供额外空间时,服务器检查PIP以查找_未分配_的页面。如果存在这样的页面,服务器将其状态修改为已分配且有空间。如果没有_未分配_的页面,数据库将扩展--添加新的数据页面。
图5给出了IBSurgeon中数据页面的示例及其包含的数据。

图5. 页面清单页面
一旦页面被分配,InterBase在SIP上写入其状态,然后写入页面本身。之后,我们必须将这个新形成的页面添加到某个大量页面中,例如表的数据库页面。为此,我们应该在此大量页面的最后一页上写入对此新页面的引用--例如,在表的最后一个数据页面上。如果服务器在写入SIP后中断工作,但未在引用刚分配页面的页面上写入引用,则此页面将成为孤立页面。孤立页面在物理上已创建,在SIP上已保留,但没有来自其他页面的引用,这意味着服务器将无法找到它并在磁盘上写入数据。孤立页面在图2中用红色方块标记。孤立页面主要是由于服务器意外断电而产生的,并由专门的数据库修复工具 **gfix**(或FirstAID)(或IBSurFirstAID)“修复”。
在考虑数据页面之前,我们应该提到重要的页面类型:_生成器_和_索引_ _页面_。生成器页面表示4字节数字的数组,显示生成器状态。实际上,生成器是一个普通的计数器。
在图6中您可以看到一个生成器页面。请注意,尽管IBSurgeon显示生成器名称,但这并不意味着这些名称存储在生成器页面上。这样做是为了方便研究数据库的用户。实际上,生成器名称存储在系统表RDB$Generators中。

图 6. 生成器页面(g en-ids)
如您在此示例中所见,数据库包含以 RDB$ 前缀开头的系统生成器和用户自定义生成器。如果您想了解在开发 InterBase 数据库应用程序时生成器的功能和用法,请参阅«表。主键和生成器»一章。生成器页面与其他页面一起计入 RDB$Pages 表中。
每个表至少有一个索引根页面,无论它是否有索引。此页面包含指向相应表的索引页面的指针。我们可以说,索引根页面对于索引页面的重要性等同于指针页面对于数据页面的重要性。因此,IBSurgeon 以类似的方式表示它。索引根页面的示例见图 7。

图 7. 索引根页面
索引根页面包含存储索引值的页面列表,以及索引信息–索引的选择性和各种标志。有关索引及其在 InterBase 数据库中的角色和用法的更多详细信息,请参阅«索引»一章。
索引页面直接存储索引值,或者如果索引级别 >0,则存储对底层索引页面的引用。以下是索引页面的示例(图 8)。

图 8. 索引(B-tree)页面
索引页面存储索引数据的压缩值。使用了相当复杂的索引机制,尤其是在创建复合索引(包含多个字段)时。
一般来说,数据页面和包含 BLOB 值的页面存储用户信息。数据页面包含数据库用户表中的记录、记录片段、旧版本、版本之间的差异、BLOB 字段等。至于 BLOB 字段,它们与数据页面上的记录相关联,包含无法放在数据页面上的大型数据。BLOB 值的引用式存储允许存储大型数据。
IBSurgeon 中数据页面的表示示例见图 9:

图 9. 数据页面
数据页面头部包含页面类型、所有者表标识符(relationID)。记录从页面末尾开始存储在数据页面上,随着填充,它们被分配到更靠近页面开头的位置。
如果我们查看行索引(包含两个值–页面上的偏移量及其长度),就可以确认这一点。如您所见,在行的开头有分配在页面末尾的记录–例如,第一条记录的偏移量为 8156 字节,长度为 34 字节–因此它结束于 8156+34=8192 字节–正好在页面的边缘(在我们的例子中,页面大小为 8192 字节)。当页面被填满时(顶部有数据,底部有记录索引),服务器开始将新记录和旧记录的版本写入新页面。从上述页面填充机制中,我们可以很容易地理解为什么 InterBase 专家强烈建议使用大尺寸的数据页面(至少 4096 字节,最好 8192 字节)。如果我们创建一个表,其中一条记录的大小相当大(例如 10 个 VARCHAR (255) 字段),它们将占用超过 2550 字节。这意味着这样的记录对于小尺寸页面(1024 或 2048)来说太大了。显然,为了读取一条记录而需要从磁盘加载多个页面不会加快数据库的工作速度。因此,建议在创建或恢复数据库时重新定义数据页面的大小,因为默认设置的大小为 1024 字节。我们刚刚简要介绍了 InterBase 数据文件页面的主要类型及其功能。现在我们可以进入更高的结构层次。
ODS
ODS 是 On-Disk Structure(磁盘结构)的缩写,即 InterBase 数据库在磁盘上的数据结构。ODS 定义了数据库文件中的数据组织方式。实现磁盘结构的主要常量和数据结构的定义位于 InterBase 源代码集中的 ods.h 文件中。ODS 在 InterBase 的开发过程中不断变化,在处理具体数据库时,服务器会检测 ODS 版本号以了解它所处理的内容。ods.h 文件向我们展示了以下磁盘结构版本:
-
ODS 5 由 InterBase 3.3 使用,以上版本不再支持
-
ODS 6 和 ODS 7 从未发布
-
ODS 8 由 InterBase 4.0 使用
-
ODS 9 由 InterBase 4.5 及以上版本使用
-
ODS 10 随 InterBase 6 发布
-
ODS 11 随 InterBase 7.0 发布
除了主要的 ODS 版本外,还有取决于创建它们的数据库服务器具体版本的次要版本。主版本号写在数字的整数部分,表示版本;次版本号写在小数部分。例如,服务器版本 4.0 创建的数据库具有 ODS 8.0,而 InterBase 4.2 创建的是 8.2。次版本之间的自下而上转换自动执行。例如,只需用 InterBase 5.6 打开由服务器 4.0 创建的 ODS 8.0 数据库,该数据库的 ODS 版本就会变为 8.2。主版本之间的转换只能通过使用旧版本进行数据库备份和使用新服务器版本进行恢复来执行。版本之间的转换过程在第 1.4 章«迁移»中有详细描述。
在 InterBase 4.x 和 5.x 版本中实现 ODS 支持的一个重要方面是 InterBase 服务器 4.x 和 5.x 与比具体服务器实现低一个版本的向后兼容性。InterBase 支持多种可能的 ODS,并根据连接到具体数据库时的 ODS 版本选择所需 ODS 实现的支持。决定在具体情况下选择哪种 ODS 支持实现的机制称为 Y-Valve(Steve Trenton 的版权)。
简单来说,与 InterBase 4.0 匹配的 ODS 8.x 数据库可以在 InterBase 5.x 中打开。
完整的 ODS 兼容性表如下所示:
| InterBase 版本 | 主 ODS | 次 ODS |
| 4.0/4.1 | 8.0 | … |
| 4.2 | 8.2 | 8.2 |
| 5.0/5.1 | 9.0 | 8.2 |
| 5.5 | 9.1 | 8.2 |
| 5.6 | 9.1 | 8.2 |
| 6.0 | 10.0 | 9.0/9.1 |
| 7.0 | 11.0 | 10.0 |
| 7.1 | 11.1 | 10.0 |
ODS 具有向下兼容性。换句话说,更高版本的服务器及其所有工具能够处理由早期版本服务器创建的数据库,但反之则不行。如果您尝试用 InterBase 5.x 打开在 InterBase 6 版本中创建的数据库,您将收到错误消息«不支持的磁盘结构:发现 ODS 10,支持 ODS 9»。
关于自下而上版本转换的描述,请参阅«迁移»一章。
ODS 对于备份和数据库提取以及损坏数据库的恢复问题非常重要。备份 gbak 和恢复 gfix 工具会检查 ODS 版本,如果它们必须服务的数据库的 ODS 版本大于它们实现的版本,它们将无法工作。这意味着 4.x 的 gbak 无法为 5.x 服务器创建的数据库创建备份,但反之则很容易。
数据库物理与逻辑结构之间的桥梁
我们大致了解了数据库文件的物理结构。现在我们必须进入数据库的逻辑结构。让我们在数据库中信息表示的物理和逻辑层面之间架起一座桥梁,以免概念上出现割裂,材料上出现空白。存储在不同数据库页面中的所有内容必须以某种方式组织在计算机内存中;数据库文件中的数据必须转换为一组服务器内部对象和变量。根据 Ann Harrison 的术语,这组对象称为内部数据库映像 [1.. 因此,我们将尝试考虑创建内部数据库映像的过程。
-
服务器从文件开头读取1024字节,如果确实是InterBase数据库文件,它会确定该数据库的页面大小,并重新读取整个头页面。
-
从头部信息中,页面服务器提取存储数据页引用的指针页编号,从而确定RDB$Pages表。
-
服务器继续访问该指针页,并开始从指向的数据页读取信息。它用数据填充第一个RDB$Pages表。该表类似于物理对象(数据库文件的页面)与逻辑对象(表)之间的桥梁。RDB$Pages的结构与其他系统表一样,在InterBase中是严格固定的。
-
在获得关于关系(relations–实际上,它与普通表相同,为简化起见,我们可以在概念上替换这些术语)的页面分配数据后,InterBase开始构建数据结构:首先是系统表、约束和索引,然后是用户对象。
-
在系统元数据和用户元数据(表、约束、索引及其他数据库对象)初始化完成后,InterBase将数据库句柄返回给请求打开数据库的用户。该句柄本质上是一个标识符,告诉InterBase要操作哪个数据库,因为多个用户可以同时工作,这意味着可以同时打开多个数据库。
-
完成这些操作后,数据库被视为已打开,服务器即可执行用户的查询。现在,既然已经建立了连接数据库物理结构和逻辑结构的桥梁,我们就可以开始研究逻辑结构的特性了。
InterBase数据库逻辑结构
逻辑结构是一个相当模糊的概念,因此我们将尝试逐步掌握关键思想,希望之后它们会变得直观易懂。我们首先要考虑与数据库逻辑结构相关的是系统表及其内容。系统表描述系统本身以及用户元数据。一般来说,术语“元数据”意为“描述一组数据的数据”。前缀“meta”意为“描述一个集合”。例如,元语言是描述一组语言的语言。元数据描述用户数据,即表、触发器、视图、存储过程等–所有实现信息存储和处理规则的内容,正是这些规则构成了创建该具体数据库的目的。
第一次知道所有元数据–用户表、触发器、视图以及所有系统对象–都存储在相同的表中,并且可以通过普通SQL查询从中读写数据时,确实很有趣。这些表“视觉上”的区别仅在于它们的名称以RDB$开头。这4个字符保留用于系统对象的名称。任何用户表、列或其他对象都无权使用以这些字符开头的名称。形式上,你可以创建名称以保留字符开头的表,但InterBase文档不建议这样做。
一个问题随之而来:如果关于数据库结构的数据存储在与用户数据相同的表中,那么描述这些表的表的信息存储在哪里?这是“鸡和蛋”问题的经典例子–如果它们相互依赖,怎么可能一个先于另一个出现?答案是,系统表在其原始状态下固定在InterBase初始代码中,并在创建数据库时按特定顺序自动打开。我们已经讨论过RDB$Pages表,它将数据库文件中的物理页面与数据库的特定对象进行比较。该表的结构如下所示:
表5. 系统表RDB$Pages
| 列名 | 数据类型 | 描述 |
| RDB$PAGE_NUMBER | INTEGER | 物理页编号 |
| RDB$RELATION_ID | SMALLINT | 为其分配页面的表的标识符 |
| RDB$PAGE_SEQUENCE | INTEGER | 此页的编号 |
| RDB$PAGE_TYPE | SMALLINT | 页面类型–参见表3 |
每个数据页都与某个表相关联。这种关联通过RDB$RELATION_ID字段维护,其中存储了对表的引用。如上所述,在构建内部数据库映像的过程中,服务器创建此表并根据固定算法用数据填充它。准确地说,在构建内部数据库映像时,RDB$Pages并不是一个表,它只是一个InterBase已知的特定格式的数据文件。根据固定算法,服务器从该文件中读取数据并创建一个对整个数据库至关重要的表–RDB$Relations。该表描述数据库中的所有表。如果我们执行SQL查询:
SELECT * from RDB$Relations
来查看RDB$Relations包含哪些表的引用,我们会发现它包含RDB$Pages以及它自身。显然,在这种情况下,服务器有点“狡猾”,它通过回溯的方式将RDB$Relations中的这些及其他系统表替换进去,从而使其合法化。服务器将它们记录为“普通”表,可以在其中添加或删除记录。换句话说,为元数据操作提供了标准的SQL接口。
一个相当合理的问题可能会出现–为什么InterBase开发者要让他们的系统数据适应用户界面?要知道,内部访问和读取操作的机制会更快。当然,提供一种统一的机制来处理描述元数据的表是有很大意义的。
问题在于,数据库逻辑结构不仅由表组成,还包括其他对象。InterBase中有以下对象:
-
表
-
视图
-
触发器
-
计算字段
-
验证
-
存储过程
-
表达式索引
-
异常
-
用户
-
字段
-
索引
-
用户自定义函数(UDF)
虽然我们还不确定某些对象的功能,但我们可以确定的是,所有对象都必须以某种对用户方便且可从InterBase内核访问的方式描述和存储。最好的方式是将这些对象保存在系统表中。它们的添加和修改通过SQL查询执行。这是一个聪明的解决方案,不是吗?服务器实现与具体数据库完全分离–所有相互关联都由SQL及其扩展–存储过程和触发器语言来描述。
因此,所有服务器对象都存储在表中。每种类型的对象都有一个表,描述数据库中定义的所有实例。例如,触发器有RDB$Triggers表,存储过程有RDB$Procedures表,视图在RDB$Relations表中描述。
让我们详细考虑最后一个表的结构,它描述数据库中的所有表和视图。RDB$RELATIONS表的结构取自InterBase 6语言参考,如下表6所示。
表6. 系统表RDB$Relations
| 列名 | 数据类型 | 长度 | 描述 |
| RDB$VIEW_BLR | BLOB | 80 | BLR:对于视图,包含InterBase每次访问视图时执行的查询的BLR(二进制语言表示)。 |
| RDB$VIEW_SOURCE | BLOB | 80 | 文本:对于视图,包含实现该视图的SQL查询代码。 |
| RDB$_DESCRIPTION | BLOB | 80 | 表或视图的用户描述 |
| RDB$RELATION_ID | SMALLINT | 包含表/视图的内部标识符 | |
| RDB$SYSTEM_FLAG | SMALLINT | 定义表类型:用户数据–0;系统信息 > 0。 | |
| RDB$DBKEY_LENGTH | SMALLINT | db$key的长度 | |
| RDB$FORMAT | SMALLINT | 保留供InterBase内部使用。包含给定表的元数据修改计数器。 | |
| RDB$FIELD_ID | SMALLINT | 表中的字段数。 | |
| RDB$RELATION_NAME | CHAR | 31 | 唯一的表名。 |
在这个系统表的描述中,我们看到了缩写BLR。要理解它是什么,我们需要对SQL进行一番探讨。众所周知,视图、触发器和存储过程是用SQL语言扩展(每个DBMS服务器都有自己的扩展)编写的代码。它接近人类语言,这使得编写查询变得容易。但InterBase显然会将其翻译成更“机器化”的东西–即BLR(二进制语言表示)。任何查询、视图、触发器、存储过程总是被翻译成BLR,然后传输到InterBase内核执行。
BLR
BLR是一种特殊的语言,用作程序员编写的SQL代码与服务器接受的机器代码之间的中间链接。没有人直接用BLR编写代码–这将相当困难,因为为了获得尽可能高的运行速度,这种语言使用了所谓的逆波兰记录。下面是一个小例子:
blr_begin,
blr\_assignment,
blr\_field, 0, 7, 'D','A','T','E','I','Z','M',
blr\_variable, 1,0,
blr\_assignment,
blr\_field, 0, 4, 'R','A','T','E',
blr\_variable, 0,0,
blr\_block,
您的查询、存储过程、触发器和其它触发器的BLR由服务器内核中的特殊预处理器生成。如表7所示,对于视图,会存储其文本(初始)视图以及编译后的视图,即BLR。当引用任何具有BLR的对象时,服务器执行该对象的二进制代码,而不会每次都解释这些对象的初始文本,从而可以加快复杂查询的执行速度。
InterBase中的对象层次结构
为了清晰了解数据库对象代表什么,我们将尝试根据“谁包含什么”的原则构建数据库对象的层次结构。数据库文件的物理页面首先必须包含在我们的层次结构中,作为数据组织的最低层。然后表作为基本对象,描述所有其它类型的对象。表描述存储过程、触发器、计算字段、验证、表达式索引、异常等。请注意–只是描述!表仅包含这些对象的声明和定义,而对象通过BLR实现。因此,我们可以将表表示为框架形式,支撑所有其它数据库对象。BLR将位于框架底部作为实现层,然后是触发器、存储过程、表达式索引和视图。
为了安抚那些可能反对许多对象(如视图)的BLR存储在系统表中的InterBase内部结构专家,我们要说明这种关系在图片上难以表达,为简化起见我们将其忽略。该方案的目的不是绝对准确地重建数据库对象的相互依赖关系;它只是说明它们之间的紧密联系。
这些对象类型直接与BLR相关联,BLR无需任何中间逻辑即可实现它们,这一事实将它们统一起来。异常应单独分配–它们代表用户定义的特殊错误类型。异常在InterBase内核级别处理,因此没有BLR。诸如检查之类的约束类型被分配在触发器之上,因为实际上触发器实现了约束和检查的逻辑。
数据库逻辑和物理结构的对象层次结构如图2所示。
图10. InterBase数据库逻辑结构的对象
当然,该方案仅大致描述了数据库中对象的逻辑结构和相互联系,并给出了总体概念。任何想研究InterBase数据库元数据结构的人都可以对数据库系统表进行逆向工程,考虑其对象之间的所有相互联系,也可以参考文档和InterBase原始代码。此表仅显示主要数据库对象。让我们简要描述这些对象在数据库中执行的主要功能。
表–包含用户和系统数据的主要对象。表具有唯一名称,并包含一组命名字段。用户可以在表中放置数据、提取和修改数据。可以说,表类似于手工绘制的普通纸质表格。
触发器–可执行的代码部分,用于在数据操作时实现附加操作。触发器在插入、修改或删除操作之前或之后执行,并允许实现对新建记录的值替换以及许多其它功能。
存储过程是在数据库级别实现业务逻辑的强大工具。它在服务器级别执行,运行速度非常快,并允许对数据集执行一系列操作。InterBase存储过程返回标准SQL数据集,可对其执行所有SQL操作,包括与其它表的联合。
视图是在服务器上执行的编译后的SQL查询。视图允许组织数据集,将部分业务逻辑转移到服务器上。
验证是对表中字段值设置的约束。例如,我们可以指出给定字段只接受正值。字段值的约束由触发器实现,并允许在数据库级别有效控制引用完整性。通常使用约束来防止错误值被放入表中。
用户–InterBase允许我们拥有多个用户来操作数据库,并在他们之间分配对不同数据库对象的访问权限。因此,我们可以控制对某些数据库操作的权限。
用户定义函数(UDF)–由用户定义的函数。这是InterBase最强大的功能之一,允许我们通过自己的函数扩展标准SQL接口。例如,处理字符串的函数,如UPPER(将所有符号设置为大写),在InterBase附带的UDF标准库中实现。由于可以创建自己的UDF,开发人员几乎可以通过任何函数扩展InterBase功能。我们可以使用任何允许创建动态库的编程环境(Visual C++、C++ Builder、Delphi等)来创建UDF。
结论
在本章中,我们首次探讨了InterBase数据库中数据存储和处理的实现问题。不幸的是,如果不涉及大量术语和不精确的类比,我们无法对主题进行简要概述。如果我们更详细地描述数据库的物理和逻辑结构,无论如何我们都必须参考InterBase原始代码,但那将是另一本书了。
尽管如此,我们认为每个程序员都有必要熟悉他每天使用的产品的内容。
参考书目
- 《InterBase的磁盘结构》作者:Ann.W.Harrison
2.《InterBase中的空间管理》作者:Ann W.Harrison
- 《数据页的结构》作者:Paul Beach(感谢Dave Schnepper和Deej Bredenberg)