第二章 - 关系模型介绍
约 3451 个字 预计阅读时间 12 分钟
关系数据库的结构
关系数据库由表 (table) 的集合构成,每张表被赋予一个唯一的名称。一般来说,表中的一行代表了一组值之间的某种联系,\(n\) 个值之间的一种联系在数学上可以用一个 \(n\) 元组来表示。由此,在关系模型中,术语关系 (relation) 被用来指代表,术语元组 (tuple) 被用来指代行,术语属性 (attribute) 被用来指代列。
我们用关系实例 (relation instance) 这个属于来指代一个关系的特定实例,也就是说关系实例包含一组特定的行。
由于关系是元组的集合 (set),所以元组在关系中出现的顺序是无关紧要的。
对于关系的每个属性都存在一个允许取值的集合,称作该属性的域 (domain)。如果一个域中的元素被认为是不可再分的单元,则该域就是原子的 (atomic)。我们要求对所有关系 \(r\) 而言,\(r\) 的所有属性的域都是原子的。
空值 (null value) 是一个特殊的值,它表示值位置或不存在。
数据库模式
当我们谈论数据库时,必须在数据库模式 (database schema) 和数据库实例 (database instance) 之间进行区分。前者是数据库的逻辑设计,后者是在给定时刻数据库中数据的一个快照。
关系的概念对应于程序设计语言中变量的概念,而关系模式 (relation schema) 的概念对应于程序设计语言中类型定义的概念,关系实例 (relation instance) 的概念对应于程序设计语言中变量的值的概念。
一般来说,一个关系模式由一个属性列表及各属性所对应的域组成。
码
我们必须有一种方式来区分一个给定关系中的不同元组,这种区分是用听它们的属性来表示的。也就是说,一个元组的所有属性值必须能够唯一标识元组,一个关系中不可以有两个元组在所有属性上取值完全相同。
超码 (superkey) 是一个或多个属性的集合,将这些属性组合在一起可以允许我们在一个关系中唯一地标识出一个元组。
形式化地说,令 \(R\) 表示关系 \(r\) 模式中的属性集合。如果我们说 \(R\) 的一个子集 \(K\) 是 \(r\) 的一个超码,那么在我们考虑关系 \(r\) 的实例时,就限制了其中没有两个可区分元组会在 \(K\) 的所有属性上取值完全相等。也就是说,如果 \(t_{1}\) 和 \(t_{2}\) 在 \(r\) 中且 \(t_{1} \neq t_{2}\),则 \(t_{1}.K \neq t_{2}.K\)。
超码中可能包含无关紧要的属性。如果 \(K\) 是一个超码,那么 \(K\) 的任意超集也是超码。我们通常只考察这样的超码:它们的任意真子集都不是超码。这样的最小超码称作候选码 (candidate key)。
Note
几个不同的属性集都可以作为候选码的情况是存在的。
我们用主码 (primary key) 这个术语来代表被数据库设计者选中来作为在一个关系中区分不同元组的主要方式的候选码。因此,主码也被称作主码约束 (primary key constraint)。主码必须慎重选择,应该选择那些其值从不变化或极少变化的属性。
码是整个关系的一种性质,而不是单个元组的性质。关系中的任意两个不同的元组都不允许同时在码属性上具有相同的值,换言之,码的指定代表了被建模的内容在现实中的约束。
关系内容上存在另一种类型的约束,即外码约束 (foreign-key constraint)。从 \(r_{1}\) 关系的 \(A\) 属性(集)到 \(r_{2}\) 关系的主码 \(B\) 的外码约束表明:在任何数据库实例中,\(r_{1}\) 中每个元组对 \(A\) 的取值也必须是 \(r_{2}\) 中某个元组对 \(B\) 的取值。\(A\) 属性集被称作从 \(r_{1}\) 引用 \(r_{2}\) 的外码 (foreign key)。\(r_{1}\) 关系也被称作此外码约束的引用关系 (referencing relation),且 \(r_{2}\) 被称作被引用关系 (referenced relation)。
引用完整性约束 (referential integrity constraint) 要求引用关系中的任意元组在指定属性上出现的取值也必然出现在被引用关系中至少一个元组的指定属性上。
Note
请注意,在外码约束中,被引用属性(集)必须是被引用关系的主码。引用完整性约束下相对更泛化,它放松了被引用属性构成被引用关系主码的要求。
模式图
一个带有主码和外码约束的数据库模式可以用模式图 (schema diagram) 来表示。下图是以大学组织机构为例的一个模式图,其中每个关系显示为一个框,关系名用灰色显示在顶部,并且在框内列出了各属性:
关系查询语言
查询语言 (query language) 是用户用来从数据库中请求获取信息的语言。这类语言通常比标准的程序设计语言的层次更高。查询语言可以分为命令式的、函数式的以及声明式的。
在命令式查询语言 (imperative query language) 中,用户指导系统在数据库上执行特定的运算序列以计算出所需的结果;这类语言通常有一个状态变量的概念,状态变量在计算的过程中被更新。
在函数式查询语言 (functional query language) 中,计算被表示为对函数的求值,这些函数可以在数据库中的数据上运行或在其它函数给出的结果上运行。函数没有附带作用,并且它们并不更新程序的状态。
在声明式查询语言 (declarative query language) 中,用户只需描述所需信息,而不用给出获取该信息的具体步骤序列或函数调用,所需的信息通常使用某种形式的数学逻辑来描述。
关系代数
关系代数 (relational algebra) 由一组运算组成,这些运算接受一个或两个关系作为输入,并生成一个新的关系作为它们的结果。
其中一些运算只在一个关系上进行运算,称作一元 (unary) 运算。另一些运算在一堆关系上进行运算,称作二元 (binary) 运算。
在讨论正式的关系代数时,如集合的数学定义所要求的那样,我们要求去除重复项。在后续章节,我们将讨论如何将关系代数扩展到可以包含重复项的多重集合上。
选择运算
选择 (select) 运算选出满足给定谓词的元组。我们使用 \(\sigma\) 来表示选择运算,将谓词写在 \(\sigma\) 的下标中,作为参数的关系写在 \(\sigma\) 后的括号内。
例如,为了选出教职工关系中对应于物理系的教师的元组,我们可以这样写:
\[\sigma_{\text{dept.name} = \texttt{Physics}} (\text{instructor})\]
通常,我们允许在选择谓词中使用 \(=, \neq, <, \leq, >, \geq\) 来进行比较,也可以使用连接词 \(\wedge, \vee, \neg\) 将几个谓词组合成一个更长的谓词。
投影运算
假设我们希望列出某个关系中的部分属性,同时滤去另一部分,投影 (project) 运算就可以生成这样的关系。它是一种一元运算,常用 \(\Phi\) 来表示运算,将希望出现在结果中的属性作为 \(\Phi\) 的下标列出,参数关系写在 \(\Phi\) 后的括号内。
例如,为了得到教职工关系中的 ID、名字、工资这三个属性,我们可以这样写:
\[\Phi_{\text{ID}, \text{name}, \text{salary}} (\text{instructor})\]
投影运算 \(\Phi_{L}(E)\) 原则上只允许在列表 \(L\) 中出现属性名,但泛化版本可以允许在列表 \(L\) 中出现涉及属性的表达式。
例如,如果我们比起工资更关心月薪,我们可以这样写:
\[\Phi_{\text{ID}, \text{name}, \frac{\text{salary}}{12}} (\text{instructor})\]
关系运算的复合
关系运算的结果本身也是关系,这一事实是重要的。这就允许我们对关系运算进行复合。
例如,为了得到教职工关系中物理系教师的名字,我们可以这样写:
\[\Phi_{\text{name}}(\sigma_{\text{dept.name} = \texttt{Physics}} (\text{instructor}))\]
一般来说,由于关系代数运算的结果与其输入具有相同的类型,因此可以将关系代数运算复合在一起成为关系代数表达式 (relational-algebra expression)。
笛卡儿积运算
笛卡儿积 (Cartesian-product) 运算用 \(\times\) 表示,它允许我们结合来自任意两个关系的信息。我们将关系 \(r_{1}\) 与 \(r_{2}\) 的笛卡儿积记作 \(r_{1} \times r_{2}\)。
数据库关系的笛卡儿积在其定义上与集合的笛卡儿积的数学定义略有不同。关系代数中的 \(r_{1} \times r_{2}\) 不是将来自 \(r_{1}\) 和 \(r_{2}\) 的元组生成元组对 \((t_{1}, t_{2})\),而是将 \(t_{1}\) 和 \(t_{2}\) 拼接成单个元组。
由于相同的属性名可能同时出现在 \(r_{1}\) 和 \(r_{2}\) 的模式中,因此我们需要设计一种命名机制来在这些属性之间进行区分。我们通过将属性所属的关系名作为其前缀,再记录到新的关系中,从而不导致歧义。
这种命名规范要求,作为笛卡儿积运算参数的两个关系具有不同的名称。这一要求在某些情况下会导致问题,例如当需要一个关系与其自身做笛卡儿积时。这将用后文中的更名运算来避免这些问题。
一般来说,如果有关系 \(r_{1}(R_{1})\) 和 \(r_{2}(R_{2})\),那么 \(r_{1} \times r_{2}\) 就是这样的一个关系 \(r(R)\):它的模式 \(R\) 是 \(r_{1}\) 和 \(r_{2}\) 的拼接。\(r\) 中包含所有这样的元组 \(t\):对于 \(t\) 存在 \(r_{1}\) 中的一个元组 \(t_{1}\) 和 \(r_{2}\) 中的一个元组 \(t_{2}\),满足 \(t\) 和 \(t_{1}\) 在 \(R_{1}\) 中的属性上取值相同,\(t\) 和 \(t_{2}\) 在 \(R_{2}\) 中的属性上取值相同。如果在 \(r_{1}\) 中有 \(n_{1}\) 个元组,\(r_{2}\) 中有 \(n_{2}\) 个元组,则 \(r\) 中就有 \(n_{1} \times n_{2}\) 个元组。
连接运算
笛卡儿积会将两个关系中的所有元组全部组合在一起,但更多时我们希望在组合两个关系时带有一定的约束条件。不难想到,这可以通过复合笛卡儿积运算和选择运算来实现。
由于这个操作非常常用,我们将此定义为连接 (join) 运算。形式化地说,对于关系 \(r(R)\) 和 \(s(S)\),并令 \(\theta\) 为 \(R \cup S\) 模式的属性上的一个谓词,那么有连接运算:
集合运算
很多集合运算同样适用于关系。但是为了使概念的迁移更加严谨,我们需要先做一些定义。
一个关系的属性数量称作其元数 (arity)。当属性有关联的类型时,对于每个 \(i\),两个输入关系的第 \(i\) 个属性的类型都相同,这样的关系称作相容 (compatible) 关系。
对于两个相容关系,我们可以对其进行并 (union)、交 (intersection) 和集差 (set-difference) 运算。
赋值运算
有时通过将一个关系代数表达式中的一部分赋值给临时的关系变量,可以方便地编写该表达式。赋值 (assignment) 运算用 \(\gets\) 表示,其工作方式和程序设计语言中的赋值是类似的。
更名运算
不同于数据库中的关系,关系代数表达式的结果并没有可以用来指代它们的名称。在某些情况下,给它们命名是有用的。更名 (rename) 运算用 \(\rho\) 来表示。给定一个关系代数表达式 \(E\),下述表达式
返回以 \(x\) 命名的表达式 \(E\) 的结果。
对于一个关系 \(r\),\(r\) 本身就是一个平凡的关系代数表达式。我们对其应用更名运算,就可以得到新名称下的同一个关系。这就解决了诸如前文在介绍笛卡儿积时遇到的问题。
更名运算的第二种形式如下:假设一个关系代数表达式 \(E\) 的元数为 \(n\),那么表达式
返回以 \(x\) 命名的表达式 \(E\) 的结果,并将其属性重命名为 \(A_{1}, A_{2}, \cdots, A_{n}\)。这种形式的更名运算可用于在涉及属性上的表达式的关系代数运算的结果中为属性命名。
等价查询
用关系代数来编写查询的方式通常不止一种。我们考虑如下两个表达式:
不难发现,这两条查询是等价 (equivalent) 的。这一点在数据库系统的查询优化器中有很大的作用,这是它找到最高效的查询方式的理论基础。