主键、外键与索引

Posted on Wed, 25 Dec 2024 16:14:23 +0800 by LiangMingJian


主键

在关系数据库中,一张表中的每一行数据被称为一条记录。一条记录就是由多个字段组成的。对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为主键。

例如,假设我们把 name 字段作为主键,那么通过名字小明或小红就能唯一确定一条记录。但是,这么设定,就没法存储同名的同学了,因为插入相同主键的两条记录是不被允许的。

对主键的要求,最关键的一点是:记录一旦插入到表中,主键最好不要再修改,因为主键是用来唯一定位记录的,修改了主键,会造成一系列的影响。所以,选取主键的一个基本原则是:不使用任何业务相关的字段作为主键。

作为主键最好是完全业务无关的字段,我们一般把这个字段命名为 id。常见的可作为 id 字段的类型有:

  • 自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键,记录数约为2147483647(约 21 亿)
  • 全局唯一 GUID 类型:使用一种全局唯一的字符串作为主键,类似 8f55d96b-8acc-4636-8cb8-76bf8abc2f57。GUID 算法通过网卡 MAC 地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了 GUID 算法,可以自己预算出主键,记录数约 922 亿亿。

联合主键

关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。

对于联合主键,允许一列有重复,只要不是所有主键列都重复即可:

id_numid_typeother columns…
1A
2A
2B

如果我们把上述表的 id_num 和 id_type 这两列作为联合主键,那么上面的 3 条记录都是允许的,因为没有两列主键组合起来是相同的。没有必要的情况下,我们尽量不使用联合主键,因为它给关系表带来了复杂度的上升。

外键

当我们用主键唯一标识记录时,我们就可以在 students 表中确定任意一个学生的记录,同时还可以在 classes 表中确定任意一个班级记录,但是我们如何确定students 表的一条记录,例如,id=1 的小明,属于哪个班级呢?

为了表达这种一对多的关系,我们需要在 students 表中加入一列 class_id,让它的值与 classes 表的某条记录相对应:

idclass_idnameother columns…
11小明
21小红
52小白

这样,我们就可以根据 class_id 这个列直接定位出一个 students 表的记录应该对应到 classes 的哪条记录。

例如:

  • 小明的 class_id 是 1,因此,对应的 classes 表的记录是 id=1 的一班
  • 小红的 class_id 是 1,因此,对应的 classes 表的记录是 id=1 的一班
  • 小白的 class_id 是 2,因此,对应的 classes 表的记录是 id=2 的二班

在 students 表中,通过 class_id 的字段,可以把数据与另一张表关联起来,这种列称为外键。

外键并不是通过列名实现的,而是通过定义外键约束实现的。

ALTER TABLE students
ADD CONSTRAINT fk_class_id
FOREIGN KEY (class_id)
REFERENCES classes (id);

其中,外键约束的名称 fk_class_id 可以任意,FOREIGN KEY (class_id) 指定了 class_id 作为外键,REFERENCES classes (id) 指定了这个外键将关联到 classes 表的 id 列(即 classes 表的主键)。

通过定义外键约束,关系数据库可以保证无法插入无效的数据。即如果 classes 表不存在 id=99 的记录,students 表就无法插入 class_id=99 的记录。

由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,class_id 仅仅是一个普通的列,只是它起到了外键的作用而已。

要删除一个外键约束,也是通过 ALTER TABLE 实现的:

ALTER TABLE students
DROP FOREIGN KEY fk_class_id;

注意:删除外键约束并没有删除外键这一列。删除列是通过 DROP COLUMN ... 实现的。

索引

在关系数据库中,如果有上万甚至上亿条记录,在查找记录的时候,想要获得非常快的速度,就需要使用索引。

索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。

例如,对于 students 表:

idclass_idnamegenderscore
11小明M90
21小红F95
31小军M88

如果要经常根据 score 列进行查询,就可以对 score 列创建索引:

ALTER TABLE students
ADD INDEX idx_score (score);

使用 ADD INDEX idx_score (score) 就创建了一个名称为 idx_score,使用列 score 的索引。索引名称是任意的,索引如果有多列,可以在括号里依次写上:

ALTER TABLE students
ADD INDEX idx_name_score (name, score);

索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如 gender 列,大约一半的记录值是 M,另一半是 F,因此,对该列创建索引就没有意义。

可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。

对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。

唯一索引

在设计关系数据表的时候,看上去唯一的列,例如身份证号、邮箱地址等,因为他们具有业务含义,因此不宜作为主键。

但是,这些列根据业务要求,又具有唯一性约束:即不能出现两条记录存储了同一个身份证号。这个时候,就可以给该列添加一个唯一索引。例如,我们假设 students 表的 name 不能重复:

ALTER TABLE students
ADD UNIQUE INDEX uni_name (name);

通过 UNIQUE 关键字我们就添加了一个唯一索引。

也可以只对某一列添加一个唯一约束而不创建唯一索引:

ALTER TABLE students
ADD CONSTRAINT uni_name UNIQUE (name);

这种情况下,name 列没有索引,但仍然具有唯一性保证。

无论是否创建索引,对于用户和应用程序来说,使用关系数据库不会有任何区别。这里的意思是说,当我们在数据库中查询时,如果有相应的索引可用,数据库系统就会自动使用索引来提高查询效率,如果没有索引,查询也能正常执行,只是速度会变慢。因此,索引可以在使用数据库的过程中逐步优化。

参考文件 1: SQL教程 @廖雪峰