文章目录
一、引言
前面根据网上资料拼凑,已经写过一篇关系数据库的文章了。
但是总感觉有些概念模糊不清,甚至部分概念都没有介绍。虽然对整个关系数据库已经有了大概认识,但从它作为当下最主流的数据库之一这点来讲,这个理解程度应该是远远不够的。
所以这次主要对《数据库系统概念》(第六版)的第一、二章——关系数据库及关系模型进行学习。这类书可能不那么有趣,但是概念上的完整性,逻辑的严谨度是有保障的。
二、关系数据库
数据模型是描述数据、数据联系、数据语义以及一致性约束的概念工具的集合。
这简短的一句话,就把数据模型描述的非常确切了。
我原来对数据模型的理解就是现实世界中事物的抽象。但这边可以看出它除了抽象数据本身之外,对数据间的联系、数据语义和一致性约束都是有描述的。它是一套概念工具的集合。
1. 关系模型介绍
关系模型现已称为主要的数据模型。之所以占据主要位置,是因为与早期的数据模型如网络模型或层次模型相比,关系模型以其简易性简化了编程者的工作。
这点我上篇文也说过,网络模型和层次模型中,要进行查询,程序员必须在脑中对整个模型结构、查询路径有个浮现,但是数据量一旦大了,光靠人脑来想象很明显难以做到。
1.1. 关系数据库的结构
关系数据库由表(table)的集合构成,每个表有唯一的名字。
一般来说,表中一行代表了一组值之间的一种联系。由于一个表就是这种联系的一个集合,表这个概念和数学上的关系这个概念是密切相关的,这也正是关系数据模型名称的由来。在数学术语中,元组(tuple)是一组值的序列(或列表)。在n个值之间的一种联系可以在数学上用关于这些值的一个n元组(n-tuple)来表示,换言之,n元组就是一个有n个值的元组,它对应于表中的一行。下面是一张名为instructor的表,
ID | name | dept_name | salary |
---|---|---|---|
10101 | Sri | Comp.Sci | 65000 |
12121 | Wu | Finance | 90000 |
15151 | Mozart | Music | 40000 |
这样,在关系模型的术语中,关系(relation)用来指代表,而元组(tuple)用来指代行。类似地,属性(attribute)指代的是表中的列。
我们用关系实例(relation instance)这个术语来表示一个关系的特定实例,也就是所包含的一组特定的行。上述的instructor的实例有3个元组,对应于3个教师。
关系实例是一个很具体的东西,可以说是一张具体的表。
本文中,我们主要使用多个不同的关系来说明作为关系数据模型基础的各种概念。这些关系代表一个大学的一部分。
由于关系是元组集合,所以元组在关系中出现的顺序是无关紧要的。因此,无论关系中的元组是排序好的,还是无序的,都没有关系;颠倒顺序后的关系还是与原来一样的,因为它们具有同样的元组集合。为便于说明,在显示关系时,大多数情况下都按其第一个属性排序。
对于关系的每个属性,都存在一个允许取值的集合,称为该属性的域(domain)。这样instructor关系的salary属性的域就是所有可能的工资值的集合,而name属性的域是所有可能的教师名字的集合。
我们要求对所有关系r而言,r的所有属性的域都是原子的。如果域中元素被看作是不可再分的单元,则域是原子的(atomic)。例如,假设instructor表上有一个属性phone_number,它存放教师的一组联系电话号码。那么phone_number的域就不是原子的,因为其中的元素是一组电话号码,是可以被再分为单个电话号码这样的子成分的。
重要的问题不在于域本身是什么,而在于我们怎样在数据库中使用域中元素。现在假设phone_number属性存放单个电话号码,即便如此,如果我们把电话号码的属性值拆分成国家编号、地区编号以及本地号码,那么我们还是把它作为非原子值来对待。所以我们把每个电话号码视作不可再分的单元,那么phone_number属性才会有原子的域。
本章中,我们假设所有属性的域都是原子的。
空(null)值是一个特殊的值,表示值未知或不存在。如前所述,如果我们在关系instructor中包括属性phone_number,则可能某教师根本就没有电话号码,或者电话号码未提供。这时我们就只能使用空值来强调该值未知或不存在。以后我们会看到,空值会给数据库访问和更新带来很多困难,因此尽量避免使用空值。
1.2. 数据库模式
当我们谈论数据库时,我们必须区分数据库模式(database schema)和数据库实例(database instance),前者是数据库的逻辑设计,后者是给定时刻数据库中数据的一个快照。
关系的概念对应于程序设计语言中变量的概念,而关系模式(relation schema)的概念对应于程序设计语言中类型定义的概念。
一般说来,关系模式由属性序列及各属性对应域组成。
关系实例的概念对应于程序设计语言中变量的值的概念。给定变量的值可能随时间发生变化;类似地,当关系被更新时,关系实例的内容也随时间发生了变化。相反,关系的模式是不常变化的。
尽管知道关系模式和关系实例的区别非常重要,我们常常使用同一个名字,比如instructor,既指代模式,也指代实例。在需要的时候,我们会显示地指明模式或实例。例如,“instructor模式”或“instructor关系的一个实例”。然而,在模式或实例的含义清楚的情况下,我们就简单地使用关系的名字。
dept_name | building | budget |
---|---|---|
Biology | Watson | 90000 |
Comp.Sci | Taylor | 100000 |
Elec.Eng. | Taylor | 85000 |
Physics | Watson | 70000 |
考察上面的department关系,关系的模式是: | ||
department(dept_name, building, budget) |
请注意属性dept_name既出现在instructor模式中,又出现在department模式中。这样的重复并不是一种巧合。实际上,在关系模式中使用相同属性正是将不同关系的元组联系起来的一种方法。例如,假设我们希望找出在Watson大楼工作的所有教师的相关信息。我们首先在department关系中找出所有位于Watson的系的dept_name。接着,对每一个这样的系,我们在instructor关系中找出与dept_name对应的教师信息。
我们继续砍大学数据库的例子。
大学里的每门课程可能要讲授多次,可以在不同学期授课,我们需要一个关系来描述每次课的授课情况或分段情况。该关系模式为:
section(course_id, sec_id, semester, year, building, room_number, time_slot_id),
课程段关系(课程号, 段号,学期, 年份, 建筑楼, 房间号, 事件段 )。
下面关系给出了section关系的一个示例:
course_id | sec_id | semester | year | building | room_number | time_slot_id |
---|---|---|---|---|---|---|
BIO-101 | 1 | Summer | 2009 | Painter | 514 | B |
BIO-301 | 1 | Summer | 2010 | Painter | 514 | A |
CS-101 | 1 | Fall | 2009 | Packard | 101 | H |
我们需要一个关系来描述老师和他们所授课程段之间的关系。描述此联系的关系模式是:
teaches(ID, course_id, sec_id, semester, year),
教授关系(教授号, 课程号, 段号, 学期, 年份)。
下面给出了teaches关系的一个示例:
ID | course_id | sec_id | semester | year |
---|---|---|---|---|
10101 | CS-101 | 1 | Fall | 2009 |
10101 | CS-315 | 1 | Spring | 2010 |
10101 | CS-347 | 1 | Fall | 2009 |
正如你料想的,在一个真正的大学数据库中还维护了更多的关系。
1.3. 码
我们必须有一种能区分给定关系中的不同元组的方法。这用它们的属性来表示。也就是说,一个元组的属性值必须是能够唯一区分元组的。换句话说,一个关系中没有两个元组在所有属性上的取值都相同。
超码(superkey)是一个或多个属性的集合,这些属性的组合可以使我们在一个关系中唯一地标识一个元组。例如,instructor关系的ID属性足以将不同的教师元组区分开来,因此,ID是一个超码。另一方面,instructor的name属性却不是一个超码,因为几个教师可能同名。
形式化地描述,设R表示关系r模式中的属性集合。如果我们说R的一个子集K是r的一个超码,则限制了关系r中任意两个不同元组不会在K的所有属性上取值完全相等,即如果t1和t2在r中且t1≠t2,则t1.K≠t2.K。
超码中可能包含无关紧要的属性。例如,ID和name的组合是关系instructor的一个超码。如果K是一个超码,那么K的任意超集也是超码。
我们通常只对这样的一些超码感兴趣,它们的任意真子集都不能称为超码。这样的最小超码称为候选码(candidate key)。
几个不同的属性集都可以做候选码的情况是存在的。假设name和dept_name的组合足以区分instructor关系的各个成员,那么{ID}和{name,dept_name}都是候选码。虽然属性ID和name一起能区分instructor元组,但它们的组合{ID,name}并不能成为候选码,因为单独的属性ID已是候选码。
我们用主码(primary key)这个术语来代表被数据库设计者选中的、主要用来在一个关系中区分不同元组的候选码。码(不论是主码、候选码或超码)是整个关系的一种性质,而不是单个元素的性质。关系中的任意两个不同的元组都不允许同时在码属性上具有相同的值。码的指定代表了被建模的事物在现实世界中的约束。
主码的选择必须慎重。正如我们注意到的那样,人名显然是不足以作为主码的,因为可能有多个人重名。在美国,人的社会保障号可以作候选码。而非美国居民通常不具有社会保障号,所以跨国企业必须设置他们自己的唯一标识符。另外也可以用一些属性的唯一组合作为码。
主码应该选择那些值从不或极少变化的属性。例如,一个人的地址就不应该作为主码的一部分,因为它很可能发生变化。另一方面,社会保障号却可以保证绝不变化。企业产生的唯一标识符通常不变,除非两个企业合并了,这种情况下可能在两个公司中会使用相同的标识符,因此需要重新分配标识符以确保其唯一性。
习惯上把一个关系模式的主码属性列在其他属性前面;例如,department中的dept_name属性最先列出,因为它是主码。主码属性还加上了下划线。
一个关系模式(如r1)可能在它的属性中包括另一个关系模式(如r2)的主码。这个属性在r1上称作参照r2的外码(foreign key)。关系r1也称为外码依赖的参照关系(referencing relation),r2叫做外码的被参照关系(referenced relation)。例如,instructor中的dept_name属性在instructor上是外码,它参照department,因为dept_name是department的主码。在任意的数据库实例中,从instructor关系中任取一个元组,比如ta,在department关系中必定存在某个元组,比如tb,使得ta在dept_name属性上的取值与tb在主码dept_name上的取值相同。
现在考察section和teaches关系。如下需求是合理的:如果一门课程是分段授课的,那么它必须至少由一位教师来讲授;当然它可能由不止一位教师来讲授。为了施加这种约束,我们需要保证如果一个特定的(course_id, sec_id, semester, year)组合出现在section中,那么该组合也必须出现在teaches中。可是,这组值并不构成teaches的主码,因为不止一位教师可能讲授同一个这样的课程段。其结果是,我们不能声明从section到teaches的外码约束(尽管我们可以在相反的方向上声明从teaches到section的外码约束)。
从section到teaches的约束是参照完整性约束(referential integrity constraint)的一个例子。参照完整性约束要求在参照关系中任意元素在特定属性上的取值必然等于被参照关系中某个元组在特定属性上的取值。
也就是参照关系上特定属性的取值必然在被参照关系中可以找到,即被参照物必须存在且唯一。
结合这点来理解上面的例子,其实就是在教授关系和课程分段关系之间添加一种约束。
课程分段是被教授的,那么唯一标识的课程分段在在教授关系中可以找到。但是教授关系中课程分段并不是唯一的,因为课程分段可能被多个人教授。所以约束失败。但反过来,教授关系中教授的课也是必须存在于课程分段关系中的,且确实是唯一的。所以可以约束。很绕,我觉得理解参照完整性约束的含义即可。
△1.4. 模式图
一个含有主码和外码依赖的数据库模式可以用模式图(schema diagram)来表示。下图展示了大学组织的模式图。每一个关系用一个矩形来表示,关系的名字显示在矩形上方,矩形内列出各属性。主码属性用下划线标注。外码依赖用从参照关系的外码属性到被参照关系的主码属性之间的箭头来表示。
除外码约束之外,模式图中没有显示表示出参照完整性约束。
后面将学习一种不同的、称作实体-联系图的图形化表示。实体-联系图有助于我们表示几种约束,包括通用的参照完整性约束。
1.5. 关系查询语言
查询语言(query language)是用户用来从数据库中请求获取信息的语言。这些语言通常比标准的程序设计语言层次更高。查询语言可以分为过程化的和非过程化的。在过程化语言(procedural language)中,用户指导系统对数据库执行一系列操作以计算出所需后果。在非过程化语言(nonprocedural language)中,用户只需描述所需信息,而不用给出获取该信息的具体过程(有点声明式和命令式的意思?)。
实际使用的查询语言既包含过程化方式的成分,又包含非过程化方式的成分。后面将学习被广泛使用的查询语言SQL。
有一些“纯”查询语言:关系代数是过程化的,而元组关系演算和域关系演算是非过程化的。这些语言简洁且形式化,默认商用语言的“句法修饰”,但它们说明了从数据库中提取数据的基本技术。后面,我们会详细研究关系代数和关系演算的两种形式,即元组关系演算和域关系演算。关系代数包括一个运算的集合,这些运算以一个或两个关系为输入,产生一个新的关系作为结果。关系演算使用谓词逻辑来定义所需的结果,但不需给出获取结果的特定代数过程。
1.6. 关系运算
所有的过程化关系查询语言都提供了一组运算,这些运算要么施加于单个关系上,要么施加于一对关系上。这些运算具有一个很好的,并且也是所需的性质:运算结果总是单个的关系。这个性质使得人们可以以模块化的方式来组合几种这样的运算。特别是,由于关系查询的结果本身也是关系,所以关系运算可施加到查询结果上,正如施加到给定关系集上一样。
在不同的语言中,特定关系运算的表示是不同的,但都符合我们本章所描述的通用结构。
最常用的关系运算是从单个关系(如instructor)中选出满足一些特定谓词(如salary>85000美元)的特殊元组。其结果是一个新关系,它是原始关系(instructor)的一个子集。例如,如果我们从下面关系中选择满足谓词“工资大于85000美元”的元组,
ID | name | dept_name | salary |
---|---|---|---|
10101 | Sri | Comp.Sci | 65000 |
12121 | Wu | Finance | 90000 |
15151 | Mozart | Music | 40000 |
我们得到的结果如下所示:
ID | name | dept_name | salary |
---|---|---|---|
12121 | Wu | Finance | 90000 |
另一个常用的运算是从一个关系中选出特定的属性(列)。其结果是一个只包含那些被选择属性的新关系。例如,假设我们从instructor关系中只希望列出教师的ID和工资,但不列出name和dept_name的值,那么其结果由ID和salary两个属性,
ID | salary |
---|---|
10101 | 65000 |
12121 | 90000 |
15151 | 40000 |
结果中的每个元组都是从instructor关系中的某个元组导出的,不过只具有被选中的属性。
连接运算可以通过下述方式来结合两个关系:把分别来自两个关系的元组对合并成单个元组。有几种不同的方式来对关系进行连接。下面显示了一个连接来自instructor和department表中元组的例子,新元组给出了有关每个教师及其工作所在系的信息。此结果是通过把instructor关系中的每个元组和department关系中对应于教师所在系的元组合并形成的。
ID | name | salary | dept_name | building | budget |
---|---|---|---|---|---|
10101 | Sri | 65000 | Comp.Sci | Taylor | 100000 |
12121 | Wu | 90000 | Finance | Painter | 120000 |
15151 | Mozart | 40000 | Music | Packard | 80000 |
上面这种连接被称作自然连接,在这种连接形式中,对于来自instructor关系中的一个元组与department关系中的一个元组来说,如果它们在dept_name属性上的取值相同,那它们就是匹配的。所有这样匹配的元组对都会在连接结果中出现。通常说来,两个关系上的自然连接运算所匹配的元组在两个关系共有的所有属性上取值相同。
笛卡尔积运算从两个关系中合并元组,但不同于连接运算的是,其结果包含来自两个关系元组的所有对,无论它们的属性值是否匹配。
因为关系是集合,所以我们可以在关系上施加标准的集合运算。并运算在两个“相似结构”的表(比如一个所有毕业生的表和一个所有大学生的表)上执行集合并。例如,我们可以得到一个系中所有学生的集合。另外的集合运算如交和集合差也都可以被执行。
正如我们此前讲到的,我们可以在查询结构上施加运算。例如,如果我们想找出工资超过85000美元的那些教师的ID和salary,我们可以执行上述例子中的前两种运算。首先我们从instructor关系中选出salary值大于85000美元的元组,然后从结果中选出ID和salary两个属性,结果关系如下所示,由ID和salary构成。
ID | salary |
---|---|
12121 | 90000 |
在此例中,我们可以以任一次序来执行运算,但正如我们将看到的,并非在所有情况下均可如此。
有时候,查询结果中包含重复的元组。例如,如果我们从下面instructor关系中选出dept_name属性,就会有重复的情况,
ID | name | dept_name | salary |
---|---|---|---|
10101 | Sri | Comp.Sci. | 65000 |
12121 | Wu | Finance | 90000 |
15151 | Mozart | Music | 40000 |
13131 | Yao | Comp.Sci. | 70000 |
其中包括“Comp.Sci.”,它出现了两次。一些关系语言严格遵守集合的数学定义,去除了重复。另一些考虑到从大的结果关系中去除重复需要大量相关的处理,就保留了重复。在后面这类情况中,关系并非是纯粹数学意义上的真正关系。
当然,数据库中的数据是随时间而改变的。关系可以随新元组的插入、已有元组的删除或更改元组在特定属性上的值而更新。整个关系可被删除,新的关系可被创建。
1.7. 关系代数
关系代数定义了在关系上的一组运算,对应于作用在数字上的普通代数运算,如加法、减法或乘法。正如作用在数字上的代数运算以一个或多个数字作为输入,返回一个数字作为输出,关系代数运算通常以一个或两个关系作为输入,返回一个关系作为输出。
后面会详细介绍关系代数,下面给出几个运算的概述:
符号(名字) | 使用示例 |
---|---|
σ(选择) | σsalary>=85000 (instructor)返回输入关系中满足谓词的行 |
π(投影) | πID,salary(instructor)对输入关系的所有行输出指定的属性。从输出中去除重复元素 |
⋈(自然连接) | instructor⋈department 从两个输入关系中输出这样的元组对:它们在具有相同名字的所有属性上取值相同 |
×(笛卡尔积) | instructor×department 从两个输入输入关系中输出所有的元组对(无论它们在共属性上的取值是否相同) |
∪(并) | 输出两个输入关系中元组的并 |
1.8. 总结
- 关系数据模型(relational data model)建立在表的集合的基础上。数据库系统的用户可以对这些表进行增删改查元组。表达这些操作的语言有许多种。
- 关系的模式(schema)是指它的逻辑设计,而关系的实例(instance)是指它在特定时刻的内容。数据库的模式和实例的定义是类似的。关系的模式包括它的属性,还可能包括属性类型和关系上的约束,比如主码和外码约束。
- 关系的超码(superkey)是一个或多个属性的集合,这些属性上的取值保证可以唯一识别出关系中的元组。候选码是一个最小的超码,也就是说,它是一组构成超码的属性集,但这组属性的任意子集都不是超码。关系的一个候选码被选作主码(primary key)。
- 在参照关系中的外码(foreign key)是这样的一个属性集合:对于参照关系中的每个元组来说,它在外码属性上的取值肯定等于被参照关系中某个元组在主码上的取值。
- 模式图(schema diagram)是数据库中模式的图形化表示,它显示了数据库中的关系,关系的属性、主码和外码。
- 关系查询语言(relational query language)定义了一组运算集,这些运算可作用于表上,并输出表作为结果。这些运算可以组合成表达式,表达所需的查询。
- 关系代数(relational algebra)提供了一组运算,它们以一个或多个关系为输入,返回一个关系作为输出。注入SQL这样的实际查询语言是基于关系代数的,但增加了一些有用的句法特征。
2. 术语回顾
- 表
- 关系
- 元组
- 空值
- 数据库模式
- 数据库实例
- 关系模式
- 关系实例
- 码
- 超码
- 候选码
- 主码
- 外码
- 参照关系
- 被参照关系
- 属性
- 域
- 原子域
- 参照完整性约束
- 模式图
- 查询语言
- 过程化语言
- 非过程化语言
- 关系运算
- 选择元组
- 选择属性
- 自然连接
- 笛卡尔积
- 集合运算
- 关系代数