关系数据库由由埃德加·科德(IBM)在1969年左右提出。自推出后就成为商业应用的主要数据库模型(与其他数据库模型,如分级、网络或对象模型相比)。如今已有许多商业关系数据库管理系统(RDBMS),如Oracle,IBM DB2和Microsoft SQL Server等;也有许多免费的开源关系数据库,如MySQL,mSQL(mini-SQL)和嵌入式JavaDB(Apache Derby)等。
关系数据库将数据存储在表(table)中,一个表由行和列组成。行称为记录(record)或元组(tuple),列称为字段(field)或属性(attribute)。数据库的表类似于电子表格。不过关系数据库可以在这些表格中产生关联,使得可以有效地存储大量的数据,以及高效地检索数据。
SQL(结构化查询语言)通常用来对关系数据库进行操作。
数据库的设计对经验的要求比理论要高,因为你必须做出许多选择。数据库通常是为了某种应用需求而高度定制的,因此,在数据库设计的指导里,通常都是指出不要做什么而不是要做什么,但最后的决定权还是在设计者的手中。
在进行需求分析时,需要尽可能地收集需求,以及定义你的数据库的最终目的。 比如要开发书店查询应用,就要先知道应用有什么需求,包括如何添加书籍,如何查询现有书籍,如何查询订单,如何定义生成报告格式等等。
在这个阶段的分析中,在纸上画出输入表单,以及查询和报告的草图,通常会有不少帮助。
在明确数据库设计需求后,接下来就要确定有哪些数据需要存储到数据库中。
通常我们都是将数据基于分类存储到不同的表中。例如,设计一个书店的数据库,就需要对书本、作者、出版社、顾客以及订单等分类进行分表;同时,对于每个表,则要定义好需要哪些列(记录),以书本为例,则需要有标题、作者、出版社、出版日期、ISBN、价格等信息。
另外,对于每一个表,我们需要选择一列(或者多列)作为主键(primary key)。
在关系模型中,表不可以含有重复的行,否则会导致检索出现歧义。为保证唯一性,每个表都有某一列(或者多列)作为主键,其目的是可以唯一区分每一行。如果主键只由某列构成,则被称为简单键(simple key),若由多列组成,则称为组合键(composite key)。
大多数商业数据库都基于主键来生成索引以提高查询的速度。另外,主键还被用来被其他表用作关系引用(详见下文)。
主键的选取由库的设计者来决定,通常要遵循以下原则:
另外,对于主键的选取还有一些Best Practice:
目前的数据库都可以不主动指定主键,而是由于数据库自己添加额外的一列类型为自增整数(AutoNumber)并指定为主键。
在关系数据库中包含独立且不相关的表格通常没有太大意义,如果真是这种情况你可以考虑使用NoSQL或者电子表格来存储这些内容。
关系型数据库的核心所在就是“关系”二字,甚至可以说设计关系型数据库的关键就是明确各个表之间的关系。
表间关系的类型有如下三种:
考虑一个族谱关系的例子,一个母亲可能会有0个或多个小孩,但是任意一个小孩都有且只有一个母亲。这样的关系便称为一对多。
一对多的关系不能只用一个表来保存,为什么?
以前面的例子来说,我们一开始可能会考虑建立一个名为Mothers的表,其中保存了母亲的信息如年龄,姓名,血型等,对于其下的小孩,可以创建不同的列,如老大,老二,老三等。但这样我们会面临一个问题,即列的数量是不确定的。
换个方向来说,我们可以建立名为Children的表,其中存储小孩的基本信息,以及其母亲的信息。这样看似能满足要求,但是由于不同的小孩可能会有相同的母亲,因此表中的重复数据是很多的。
因此,考虑支持一对多的数据库关系,我们应该建立两个表,分别为Mothers和Children,只保存各自的属性,并且设置分别的主键为MotherID和ChildrenID。然后我们可以通过在Children新建一列包含MotherID建立一对多的关系,如下图所示:
其中,Children表中的MotherID列又被称为约束或外键(Foreign Key),用SQL描述如下:
CREATE TABLE Mothers (
MotherID INTEGER NOT NULL AUTO_INCREMENT,
Name VARCHAR(100) NOT NULL,
Age SMALLINT NOT NULL,
BloodType VARCHAR(2) NOT NULL,
PRIMARY KEY (MotherID)
);
CREATE TABLE Children (
ChildrenID INTEGER NOT NULL AUTO_INCREMENT,
MotherID INTEGER NOT NULL,
Name VARCHAR(100) NOT NULL,
Age SMALLINT NOT NULL,
Sex VARCHAR(50) NOT NULL,
BloodType VARCHAR(2) NOT NULL,
PRIMARY KEY (ChildrenID),
FOREIGN KEY (MotherID) REFERENCES Mothers(MotherID)
);
考虑一个“产品销售”数据库的例子,某个客户的订单包含一个或者多个产品,而某个产品又可能出现在多个订单之中,
这样的关系便称为是多对多的。
为了构建这样的关系,我们先从两个表订单Orders和产品Products开始看。表Products含有关于产品的信息(如名称、介绍、库存)以及一个主键ProductID;表Orders则包含订单信息(如客户ID、订单日期、订单状态)以及主键OrderID。同样地,我们没法简单地将所有购买的产品保存在订单表里,因为订单所包含的产品记录是不固定的;同理,也没法将所有关联订单保存在产品表里。
因此,为了支持这种多对多的关系,我们需要第三个表。在本例子中,姑且将其命名为OrderDetails,其中每一行都包含了特定的订单信息,对于这个表,主键应为组合键,包含两列信息, 分别为OrderID和ProductID,而这两列也是对应Orders和Products表的Foreign Key,如下图所示:
用SQL描述如下:
CREATE TABLE Orders (
OrderID INTEGER NOT NULL AUTO_INCREMENT,
OrderDate DATETIME DEFAULT CURRENT_TIMESTAMP,
CustomerID INTEGER NOT NULL,
PRIMARY KEY (OrderID)
);
CREATE TABLE Products (
ProductID INTEGER NOT NULL AUTO_INCREMENT,
Name VARCHAR(100) NOT NULL,
Stock INTEGER DEFAULT 0,
PRIMARY KEY (ProductID)
);
CREATE TABLE OrderDetails (
OrderID INTEGER NOT NULL,
ProductID INTEGER NOT NULL,
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
PRIMARY KEY (OrderID, ProductID)
);
事实上,多对多的关系是以两组一对多的关系来实现的,额外引入的表被称为junction table即连接表。从上面的例子可以看到,每个产品(product)都会在OrderDetails表里出现多次,但OrderDetails里的每一行都只包含一个产品,若每个订单有多个产品则用多行来表示。相应地,对订单(Order)也是类似。
考虑一个“产品信息”数据库,其中除了产品名称,产品数量等基本信息外,还需要保存产品图片,产品详细等富文本详情信息,一个产品只有0个或者一个详情,一个详情有且只对应一个产品,因此这类关系就可以归类为一对一关系。有些数据库限制了列的数量,或者我们需要将部分敏感信息用另外的表保存,这些情况都可以引进一对一的关系。
回到前面的例子,我们需要分裂出一个称为ProductDetails的表,与Products构成一对一的关系。
用SQL描述如下:
CREATE TABLE Products (
ProductID INTEGER NOT NULL AUTO_INCREMENT,
Name VARCHAR(50) NOT NULL,
PRIMARY KEY (ProductID)
);
CREATE TABLE ProductDetails (
ProductID INTEGER NOT NULL AUTO_INCREMENT,
DetailInfo VARCHAR(65535),
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
PRIMARY KEY (ProductID)
);
可以看到在ProductDetails表中,主键和外键都为同一列, 这保证了一对一的正确性。值得一提的是,这里保证了Products
可以对应0个或1个ProductDetails,但ProductDetails必须对应一个Products,如果后者对前者不是强关联,如“丈夫-妻子”
的关系,那么后者可以不以主键作为外键,而是以另外一列声明为UNIQUE的属性作为外键即可。
当设计好一个数据库或者拿到已有的数据库时,我们可能会想要:
在进行这些操作时,下列的规则就可以作为参考。
范式(Normal Form),指的是符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度,可以在某种程度上认为是一张数据表的表结构所符合的某种设计标准的级别。常见的范式有第一范式、第二范式…第六范式,其严格程度依次上升,一般设计上满足第三范式即可满足日常使用。
第一范式又称为1NF(First Normal Form),是对关系模式的基本要求,不满足第一范式的数据库就不是关系型数据库。数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。
同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。 如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。
简而言之,第一范式就是没有重复的列。
第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式必须先满足第一范式(1NF)。第二范式要求数据库表中的每个实例或行必须可以被唯一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。
例如,员工信息表中加上了员工编号(EmployeeID)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个唯一属性列也就是我们之前提到过的主键。
第二范式也要求实体的属性完全依赖于主键。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,例如含有多列的主键,如前文提到的OrderDetails,主键为ProductID和OrderID,若含有一列为产品单价ProductPrice,则不符合2NF,因为ProductPrice只依赖于ProductID而不依赖于OrderID,因此此属性应该保存在Products表中。
简而言之,第二范式就是属性应完全依赖于其主键。
满足第三范式(3NF)必须先满足第二范式(2NF)。第三范式要求数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖。所谓传递函数依赖,指的是如果存在”A → B → C”的决定关系,则C传递函数依赖于A。
例如,存在一个部门信息表,其中每个部门有部门编号(DepartmentID)、部门名称(DepartmentName)、部门简介等信息。这样一个表就不是3NF的,因为存在传递依赖(EmplyeeID->DepartmentID->DepartmentName),因此在员工信息表中列出部门编号后就不应再将部门名称、部门简介等与部门有关的信息再加入员工信息表中,而是将这部分数据保存在部门信息表中,如果不存在部门信息表,则根据第三范式也应该构建它,否则就会有数据冗余,并且容易产生更新、插入的异常。
简而言之,第三范式就是任一非主键属性不应依赖于其它任何非主键属性。
除了设计范式,我们也可以通过完整性规则(Integrity rules)来检查自己的设计。常见的完整性规则如下:
实体完整性指表中行的完整性。主要用于保证操作的数据(记录)非空、唯一且不重复。即实体完整性要求每个关系(表)有且仅有一个主键,每一个主键值必须唯一,而且不允许为“空”(NULL)或重复。
参照完整性属于表间规则。对于永久关系的相关表,在更新、插入或删除记录时,如果只改其一,就会影响数据的完整性。如删除父表的某记录后,子表的相应记录未删除,致使这些记录称为孤立记录。对于更新、插入或删除表间数据的完整性,统称为参照完整性。通常,在客观现实中的实体之间存在一定联系,在关系模型中实体及实体间的联系都是以关系进行描述,因此,操作时就可能存在着关系与关系间的关联和引用。
域完整性是指数据库表中的列必须满足某种特定的数据类型或约束。其中约束又包括取值范围、精度等规定。表中的CHECK、FOREIGN KEY 约束和DEFAULT、 NOT NULL定义都属于域完整性的范畴。
又叫业务逻辑完整性(Business logic Integrity),是对数据表中字段属性的约束,用户定义完整性规则(User-defined integrity)也称域完整性规则。包括字段的值域、字段的类型和字段的有效规则(如小数位数)等约束,是由确定关系结构时所定义的字段的属性决定的。如百分制的考试成绩取值范围在0-100之间,订单数量应该小于等于库存量等。
通常我们可以通过对指定的列创建索引来加快数据库的读取和查询速度。在实现上,索引通常是一个结构化文件,可以提高SELECT的速度,却会对INSERT, UPDATE和DELETE的速度有一定负面影响。如果没有索引,进行一次条件查询(比如SELECT * FROM Customers WHERE name=”Sam”),就需要对整个数据库进行一次线性查找和比较。而在带索引的结构中(如B树),查询的时间就能减少到对数级别。当然在这种情况下,插入和删除的时间也从常数上升到对数级别,不过在实践中由于查找的频率远远大于插入和删除,因此索引带来的好处也是很明显的。
对于特定的表来说,索引可以是1列,多列组合(称为组合索引,Concatenated Index)或者是某列的部分内容(称为部分索引,Partial Index)。在一个表里我们也可以建立多个索引,例如需要经常通过电话号码或者名字来查询某个客户,就可以在这两列建立对应的索引。索引最终还是根据实际需要自行选择,值得一提的是大多数RDBMS都会自动基于主键建立索引。
总结一下,在关系数据库设计中,我们首先要明确设计的最终目标,再根据目标决定哪些数据要持久化存储;对于这些数据,要按照功能和逻辑来进行拆分,并且存放在不同的表中,并且明确之间的关系;对于设计好的表,要进行重构,根据设计范式对大表进行拆分和优化;对于每个表要增加对应的完整性检查,关键是实体完整性和参照完整性;最后在实际使用中,对于高频查询的记录构建索引提升效率,以及其他因地制宜的优化。