主键、外键及关系模型
在关系数据库中,一张表中的每一行数据被称为一条记录。一条记录就是由多个字段组成的。例如,students
表的两条记录:
id | name | score |
---|---|---|
1 | 小明 | 100 |
2 | 小强 | 0 |
每一条记录都包含若干定义好的字段。同一个表的所有记录都有相同的字段定义。
主键
对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为主键。
对主键的要求,最关键的一点是:记录一旦插入到表中,主键最好不要再修改,因为主键是用来唯一定位记录的,修改了主键,会造成一系列的影响。
所以,选取主键的一个基本原则是:不使用任何业务相关的字段作为主键。
作为主键最好是完全业务无关的字段,我们一般把这个字段命名为id
。常见的可作为id
字段的类型有:
- 自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键;
- 全局唯一GUID类型:使用一种全局唯一的字符串作为主键。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了GUID算法,可以自己预算出主键。
如果使用INT自增类型,那么当一张表的记录数超过2147483647(约21亿)时,会达到上限而出错。使用BIGINT自增类型则可以最多约922亿亿条记录。
联合主键
关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。对于联合主键,允许一列有重复,只要不是所有主键列都重复即可。
没有必要的情况下,我们尽量不使用联合主键,因为它给关系表带来了复杂度的上升。
外键
当我们用主键唯一标识记录时,我们就可以在students
表中确定任意一个学生的记录:
id | name | other columns... |
---|---|---|
1 | 小明 | ... |
2 | 小红 | ... |
我们还可以在classes
表中确定任意一个班级记录:
id | name | other columns... |
---|---|---|
1 | 一班 | ... |
2 | 二班 | ... |
但是我们如何确定students
表的一条记录,例如,id=1
的小明,属于哪个班级呢?
由于一个班级可以有多个学生,在关系模型中,这两个表的关系可以称为"一对多",即一个classes
的记录可以对应多个students
表的记录。
为了表达这种一对多的关系,我们需要在students
表中加入一列class_id
,让它的值与classes
表的某条记录相对应:
id | class_id | name | other columns... |
---|---|---|---|
1 | 1 | 小明 | ... |
2 | 1 | 小红 | ... |
5 | 2 | 小白 | ... |
这样,我们就可以根据class_id
这个列直接定位出一个students
表的记录应该对应到classes
的哪条记录。
在students
表中,通过class_id
的字段,可以把数据与另一张表关联起来,这种列称为外键
。
外键并不是通过列名实现的,而是通过定义外键约束实现的:
ALTER TABLE students
ADD CONSTRAINT fk_class_id
FOREIGN KEY (class_id)
REFERENCES classes (id);
由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,class_id
仅仅是一个普通的列,只是它起到了外键的作用而已。
要删除一个外键约束,也是通过ALTER TABLE
实现的:
ALTER TABLE students
DROP FOREIGN KEY fk_class_id;
注意:删除外键约束并没有删除外键这一列。
多对多
通过一个表的外键关联到另一个表,我们可以定义出一对多关系。有些时候,还需要定义“多对多”关系。例如,一个老师可以对应多个班级,一个班级也可以对应多个老师,因此,班级表和老师表存在多对多关系。
多对多关系实际上是通过两个一对多关系实现的,即通过一个中间表,关联两个一对多关系,就形成了多对多关系。
一对一
一对一关系是指,一个表的记录对应到另一个表的唯一一个记录。
还有一些应用会把一个大表拆成两个一对一的表,目的是把经常读取和不经常读取的字段分开,以获得更高的性能。例如,把一个大的用户表分拆为用户基本信息表user_info
和用户详细信息表user_profiles
,大部分时候,只需要查询user_info
表,并不需要查询user_profiles
表,这样就提高了查询速度。