此页面为机器翻译。请阅读英文原文。 English

IBSurgeon 文库

表。主键和生成器

_NOTICE: 本文档是阿列克谢·科维亚津和谢尔盖·沃斯特里科夫所著《InterBase 世界》一书中的一章。

InterBase 是一种关系型 DBMS。此外,这意味着 InterBase 中的所有数据都以表的形式存储。从 SQL 的角度来看,表与普通表格非常相似,这种普通表格可以手绘在一张纸上,也可以在 Microsoft Excel 等程序中创建。InterBase 中的表具有列和行,数据就放置在其中。表必须有一个名称,该名称在一个数据库内是唯一的。表是数据库中信息的主要存储方式,因此在创建表时应该非常谨慎。

有一些规则描述了如何在关系数据库中创建表,这些表反映现实世界的数据,同时允许在数据库中组织有效的信息存储。应用这些规则来设计“正确”数据库的过程称为规范化。我们有意给“正确”一词加了引号,因为“规范化数据库”和“优化数据库”并不是同义词。你不必无条件地遵守规范化规则–始终要根据给定问题的规格进行调整。

数据库中的表规范化在文献[14]中有详细讨论,因此我们不会试图涵盖无法涵盖的内容,而是回到我们的讨论主题–InterBase 表。让我们考虑一下允许创建表的 DDL(DDL–数据定义语言,详见术语表)语句的语法:

CREATE TABLE table [EXTERNAL [FILE] “”] ( [, | …]);

这里 table 是所创建表的名称, 是所创建表的列(有时我们会说–字段)的描述。选项 table [EXTERNAL [FILE] “”] 表示将创建所谓的外部表,它不存储在共享的数据库文件中,而是存储在名为 的单独文件中。如你所见,一切都很简单–我们定义表名及其包含的列。现在我们将详细考虑如何定义列。创建列的语法由以下 DDL 语句描述:

= col { datatype | COMPUTED [BY] (< expr>) | domain}

[DEFAULT { literal | NULL | USER}]

[NOT NULL] [ ]

[COLLATE collation]

这是一个相当大的定义,但在列的定义中,只有给定语句的一小部分是必需的。表中的每一列必须有一个名称,该名称在表内是唯一的,并且必须有一个由 datatype 语句定义的数据类型,或一个用于计算列值的表达式(对于计算列),或由 domain 定义的域(见下文)。数据类型已在“数据类型”一章中讨论过;因此,你可以轻松理解如何形成创建表的 SQL 表达式。

让我们连接到之前“创建数据库”一章中创建的 FIRSTBASE.gdb 数据库,并尝试在实践中使用表。在创建、删除和更新表时,任何 InterBase 管理工具–从“InterBase 管理员和开发人员工具”应用程序中列出的工具,以及任何 InterBase 克隆版本供应套件中的标准实用程序 isql.exe–都是合适的。

以下是一个名为 TABLE_EXAMPLE 的简单表示例,包含 3 个不同类型的字段:

CREATE TABLE Table_example ( ID INTEGER, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION);

此表说明了在数据库开发过程中最常遇到的情况。然而,还有其他定义字段的方法。例如,我们可以使用域来设置字段类型。域是用户为方便应用某些类型参数组合而定义的类型。例如,可以定义域 D_ID 来指定标识符字段。定义域后,我们可以使用它来设置字段类型:

CREATE DOMAIN D_ID AS INTEGER; CREATE TABLE Тable_example ( ID D_ID, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION);

字段 ID 将具有由域 D_ID 定义的类型。因此,在域中定义了字段类型、所需的检查和约束后,我们可以多次应用此域来创建相同功能的字段。例如,货币字段,无需繁琐且容易出错的变量类型定义复制。定义表中列的第三种方式是将其定义为计算列(COMPUTED BY)并指定计算其值的条件。例如,我们可能希望在我们的表中有一个计算 PRICE_1 字段值 10% 的列。在这种情况下,应编写以下命令:

CREATE TABLE Тable_example ( ID INTEGER, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION, PRICE_10 COMPUTED BY (PRICE_1.0.1));

但不要认为,一旦我们将数据插入 PRICE_1 字段,PRICE_10 字段中就会有该字段值的十分之一。不,这里的过程更复杂。实际上,只有在我们引用 PRICE_10 字段时,例如在执行对此表的 SELECT 查询时,才会得到所需的十分之一。也就是说,计算字段中不存储任何数据,而是执行与字段关联的表达式计算,并将结果作为查询答案产生。

因此,我们已经考虑了在表中指定字段的 3 种主要方式。现在让我们详细考虑创建列时可以设置的选项。选项 [DEFAULT {literal | NULL | USER}]–允许设置列的默认值。这对于自动数据填充非常方便。有 3 种设置默认值的方式。第一种指定为 literal,允许将默认值设置为文本常量、数字或日期。例如,我们可以生成以下表达式来创建具有文本默认值的列:NAME VARCHAR(80) DEFAULT ‘Василий Станиславович’

因此,插入表中的所有字段都将采用默认值,即如果未为字段 NAME 定义其他值,则将出现字符串“Vasily Stanislavovich”。设置默认值的第二种方式是在列定义中指定 DEFAULT NULL。在新建的记录中,此列的值将为 NULL,当然,除非其他值已被明确设置。示例:

PRICE_1 DOUBLE PRECISION DEFAULT NULL

设置默认值的第三种方式是在列定义中指定 DEFAULT USER。这样,在新建的记录中,此字段将包含当前用户的名称,即与 InterBase 建立连接并执行此插入的用户(有关用户的更多详细信息,请参阅“InterBase 中的安全性:用户、其功能和权限”一章(第 4 部分))。对于某些字段,字段具有非空值至关重要。例如,根据问题规格不能为空的字段。为了在数据库级别设置字段必须具有定义值的约束,有必要在列描述中添加以下内容:

NAME VARCHAR(80) NOT NULL

因此,将有一个不能存储空值的字段。通常,约束 NOT NULL 与选项 DEFAULT 结合使用,该选项明确为此字段分配一个正确的值。但通常约束 NOT NULL 是不够的。例如,在数据库中存储价格的情况下,很明显价格不能取负值(尽管如果我们购买商品时能获得额外付款那就太好了)。为了使服务器根据正数条件检查插入数据库的价格值,有必要按以下方式定义列:

PRICE_1 DOUBLE PRECISION CHECK (PRICE_1>0)

插入 PRICE_1 列的值将根据正数条件进行检查。应注意,不同的兼容选项可以组合,例如,我们可以设置非空值并检查正数:

PRICE_1 DOUBLE PRECISION NOT NULL CHECK (PRICE_1>0)

在创建列时,某些选项不能组合使用,例如,不可能同时设置默认值为NULL和非空值约束。应当指出,检查(checks)可以对数据库中的数据管理执行一系列有用的功能。我们将在“数据库约束”一章中详细讨论它们的用法。

至此,我们已经讨论了创建带有不同选项的表和字段的方法。然而,有些情况下我们需要修改已经存在的表。当然,我们可以完全重新创建表。首先,我们应该执行删除表的命令,然后再次创建它。例如:

DROP TABLE Table_example; CREATE TABLE Table_example(ID NUMERIC(15,2);

但这种方式修改表有明显的缺点。使用DROP命令删除表时,表中包含的所有数据都会被删除,为了不丢失这些数据,有必要将它们复制到临时表中。这相当麻烦。因此,有ALTER TABLE命令用于轻松修改表的结构,它允许添加新字段、删除现有字段,以及添加/删除引用完整性约束。

例如,我们想向用于存储人员父名(patronymic)数据的表中添加一列:

ALTER TABLE Table_example ADD Patronimic VARCHAR(80);

执行此命令后,我们的表Table_example将有一个名为Patronimic的新列,类型为VARCHAR(80)。如果我们想从表中删除名为NAME的列,应该执行以下操作:

ALTER TABLE Table_example DROP Name;

您可以在[1]中看到ALTER TABLE语句的完整语法。这是一个非常有用的命令,我们将经常使用它。

那么,您可能会问,如果需要修改一列该怎么办?例如,我们决定存储姓名时使用字段HUMAN_NAME而不是NAME。在这种情况下,我们可以使用ALTER TABLE:

ALTER TABLE Table_example ALTER COLUMN NAME TO HUMAN_NAME;

如果我们决定修改字段的类型,例如增加字段中存储的字符数,我们将不得不使用ALTER DOMAIN语句更改该字段的域(参见上文“数据类型”一章)。

至此,我们已经讨论了在InterBase中创建和修改表。现在是时候稍微深入数据库理论了。如前所述,InterBase是一个关系型数据库。此外,这意味着表中的每条记录都应该有一个特征,根据该特征可以将一条记录与另一条记录区分开来。唯一键的特殊机制就是为此目的服务的。

表中的主键

当然,我们可以创建一个不包含任何键的表。我们并不被禁止这样做。但是,如前所述,不遵守规范化规则就不可能创建高效的数据库。键的存在是规范化最重要的元素。因此,虽然我们的目标不是深入讨论数据库理论和规范化,但我们应该引入键的定义并回顾它们在InterBase中的功能。我们将一步一步来,从最常见的键类型–主键开始。

那么,什么是主键?它是表中的一个或多个字段,唯一地标识该表中的记录。听起来很难,但实际上一切都很简单。想象一个普通的表,例如会计表。第一列是什么?没错,是序号–1、2、3……这个数字表示表中的唯一一行,只要知道这个数字就足以在该表中找到一行。在这个例子中,它就是主键。关系型数据库中绝大多数表必然有一个主键(PK–Primary key的缩写)。创建表时的通用准则是创建主键。主键可以在创建表时创建,也可以在之后创建。假设在创建表时我们决定字段ID将是我们的主键。那么我们可以通过以下方式添加主键:

CREATE TABLE Table_example ( ID INTEGER NOT NULL, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION, CONSTRAINT pkTable PRIMARY KEY (ID));

为表table_example创建主键需要做什么?让我们看看表定义中有什么变化?首先,列ID增加了额外的定义NOT NULL。这很重要,因为主键应该是唯一的,并且禁止未定义的值。而NULL正如您所知是一个未定义的值。因此,主键中包含的所有字段都应该有NOT NULL约束。要完成主键的创建,应该在表的末尾写入:CONSTRAINT ()

您可以在“数据库约束”一章中找到约束的完整语法[1],对于我们的主键示例,它将如下所示:

CONSTRAINT pkTable PRIMARY KEY (ID)

这里pkTable是主键的名称,ID是它包含的列。这种为表定义主键的方式在批量创建表时很方便(例如,在基于不同CASE工具生成的脚本构建数据库原型时)。但是,如果我们需要在表已经存在并填充了数据时添加/删除主键,该怎么办?为此,应该使用ALTER TABLE命令的另一个扩展。向我们的表添加主键的示例:

ALTER TABLE TABLE_EXAMPLE ADD CONSTRAINT FF PRIMARY KEY (ID);

这样,表Table_example将拥有与之前示例中创建表时完全相同的主键。要删除主键,应输入以下命令:

ALTER TABLE Table_example DROP CONSTRAINT pkTable;

这样,名为pkTable的键将从数据库中移除。

生成器–主键的最佳朋友

我们必须说几句关于主键实现的话。由于主键旨在支持唯一性,一个表中的任意两条记录不能有相同的键值。也就是说,为了满足条件,在向表中插入新记录时,InterBase必须检查表中的所有记录,以确定表中是否已存在这样的值。为了快速搜索,InterBase有一个索引机制–特殊的InterBase对象,允许非常快速地找到表中的记录。因此,在创建和删除主键时,会为主键中包含的字段(或字段组)创建或删除索引。

如前所述,主键可以包含多个字段。这样,我们可以注意到这些字段值组合的唯一性。例如,如果我们为字段ID和NAME定义键,服务器将控制表中不存在这些字段的相同组合。也就是说,字段ID和NAME的组合,如1和“Ivanov”、2和“Ivanov”将是正确的,因为它们在字段ID的值上有所不同。

因此,主键可以包含任意类型的多个字段。然而,在实践中最常见的键类型是计数器–一个包含递增值的整数字段。为什么会这样?这反映了自然键和替代键之间的老争论。自然键的概念认为,作为键,我们应该尝试使用数据库中反映的数据域中实际存在的值。例如,如果我们为护照办公室开发一个人口登记系统,根据这一概念,护照号码和系列的组合应作为主键。确实,每个人都必须有唯一的护照号码和系列组合。但是,如何处理一个人一生中可能更换护照的事实(由于达到一定年龄、结婚等)?在这种情况下,我们将不得不更改与具体人对应的护照号码和系列,即实际上更改我们的主键。从数据库应用程序开发的角度来看,这是不可取的:考虑到表之间复杂的通信系统(下一章将专门讨论这一点),开发人员将不得不费很大力气来控制这种情况。

因此,在大多数情况下会使用替代键。替代意味着人工的,即不存在于我们数据库所描述的数据域中,而是为了方便开发数据库应用程序而人工创建的。如前所述,通常计数器作为主键。一些数据库管理系统,如Paradox和MS SQL,有特殊的类型–计数器(自动递增)。当向表中添加新记录时,该类型的字段值会自动按增量增加–通常是按单位。在InterBase中没有计数器类型的字段,但可以实现这种行为。为了创建在向表中添加记录时自动填充的字段,需要使用资源集合:其中第一个是生成器。

什么是生成器?简单来说,生成器是一个命名计数器。在数据库内,我们可以创建一个计数器,给它一个在此数据库内唯一的名称,并控制该计数器的值。这就是生成器。以下是一个DDL语句示例,将为您解释:

CREATE GENERATOR g1; SET GENERATOR g1 TO 2445;

在这个示例的第一行中,创建了名为g1的生成器,第二行中为该生成器分配了值2445。现在问题是如何使用所获得的生成器。InterBase中有一个内置函数GEN_ID,用于获取和更改生成器的值。该函数接受生成器名称和要应用于该生成器的增量值作为参数,并返回一个整数值,该值对应于将增量添加到生成器后得到的生成器值。以下是在触发器或存储过程中调用GEN_ID函数的示例:

Current_value = GEN_ID (g1, 1)

如果我们想获取生成器的值,可以使用以下查询:

SELECT GEN_ID(g1, 1)FROM RDB$ DATABASE

由于表RDB $ Database始终只包含一条记录,因此作为给定查询的结果,我们将获得生成器g1的值。

这里current_value是一个变量(在后续章节中您将找到如何在InterBase中使用变量的信息),g1是一个生成器,1是增量。在此示例中,生成器g1的值在添加增量1后将进入变量current_value,即生成器的下一个值。请注意,增量可以不等于1!而且,它甚至可以是负数:Current_value = GEN_ID (g1, -23)

执行此函数的结果是,生成器g1的当前值将减去23。如您所见,生成器的可能应用范围相当广泛–它不仅可用于获取主键值,还可用于监视数据库中的全局更改。

熟悉数据库的人可能会问:“如果同时多个客户端尝试向同一表插入数据并同时‘拉取’生成器,会发生什么?他们会获得相同还是不同的生成器值?”他们将明确获得不同的生成器值。无论尝试获取生成器值的“同时性”如何,每个申请者都将获得唯一的值。这是由生成器的“构造”保证的:它们在服务器的最低级别工作,没有记录和插入过程影响它们–通常说生成器“在事务上下文之外”工作。如果您想了解事务,请阅读“事务。事务参数”一章(第1部分);关于生成器如何安排–“InterBase数据库结构”(第4部分)。好了,凭借生成器,我们有了一个可靠的机制来创建唯一的主键。然而,我们能使用这个机制吗?如何将从生成器获得的值放入主键字段?

为此,有两种方法–代表客户端插入主键和代表服务器插入主键。要掌握第一种方法,我们应该参考“FIBPlus主要组件的使用”一章,要理解第二种方法,请参考“触发器”一章(第1部分)。这里我们将简要回顾两种方法的要点。

在代表客户端创建主键的情况下,会发生以下情况。当生成将要插入数据库的记录时,执行GEN_ID函数调用(, 1),并将获得的值替换为该记录。然后进行插入操作,我们保证获得唯一的主键。

第二种方法–代表服务器创建主键–通常完全消除了客户端对主键值是什么的担忧。在这种情况下,插入记录时触发器会工作–一个特殊的数据库对象,可以在插入/删除/更新表记录时执行任何操作。在这个触发器中执行以下操作:调用GEN_ID函数,获取所需的生成器值并将其插入表中。第二种方法的优点是,在开发客户端应用程序时,完全不需要担心主键的创建,您只需编写一次必要的触发器。但缺点是,我们无法在插入后立即在应用程序中接收生成键的值!如果我们使用第一种方法,我们可以接收主键的值,尽管每次插入时都需要关心其创建。很难确切地说哪种方法更好,这取决于具体问题。在本书的后续部分,我们将考虑解决主键工作问题的可能变体。

结论

因此,在本章中,我们回顾了如何在InterBase中创建和更新表,以及如何处理主键。这样,我们考虑了InterBase中可以被条件性地称为静态的主要对象,因为它们只存储信息而不执行其转换。接下来,我们将讨论数据库内信息控制和信息转换的方法。