为什么数据库不应该使用外键

作者:Draveness
来源:真没什么逻辑
为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响 。如果你有想要了解的问题,可以在文章下面留言 。
【为什么数据库不应该使用外键】当我们想要持久化地存储数据时,使用关系型数据库往往都是最稳妥的选择,这不仅因为今天的关系型数据库种类非常丰富并且稳定,还因为不同社区对关系型数据库的支持都非常完备 。我们在前面的文章中曾经分析过 为什么 MySQL 的自增主键不单调也不连续,这篇文章我们来分析关系型数据库中另一个重要的概念 — 外键(Foreign Key) 。
在关系型数据库中,外键也被称为关系键,它是关系型数据库中提供关系表之间连接的多个列1,这一组数据列是当前关系表中的外键,也必须是另一个关系表中的候选键(Candidate Key),我们可以通过候选键在当前表中找到唯一的元素2 。在通常情况下,我们都会使用关系表中的主键作为其他表中的外键,这样才可以满足关系型数据库对外键的约束 。
为什么数据库不应该使用外键

文章插图
图 1 - 关系型数据库与外键
外键不仅仅是数据库表中的一个整数,它还提供了额外的一致性保证 。因为数据库往往是整个系统的真理之源(Source of Truth),所以保证数据的一致性和正确性非常重要,关系型数据库虽然提供了外键、触发器等特性保证一致性,但是在今天的生产环境中却很少被使用 。
引用完整性(Referential Integrity)是数据的属性,如果数据拥有该属性,那么数据中所有的引用都是合法的,在关系型数据库的上下文中,这就意味着关系型数据库中引用另一个表中的值必须存在3 。
ALTER TABLE postsADD CONSTRAINT FOREIGN KEY (author_id)REFERENCES authors(id);上述 SQL 语句可以向关系表中增加外键约束,该 SQL 语句的执行前提是 posts 表中存在 author_id 字段 。从 SQL 语句中的 CONSTRAINT 关键字我们也能推测出外键不是一种数据类型,它是不同关系表之间的约束 。
为什么数据库不应该使用外键

文章插图
图 2 - 无状态服务与数据库
不使用外键的原因其实很简单,MySQL、PostgreSQL 等关系型数据库很难水平扩容,但是无状态的服务往往都可以很容易地扩容 。由于外键等特性需要数据库执行额外的工作,而这些操作会占用数据库的计算资源,所以我们可以将大部分的需求都迁移到无状态的服务中完成以降低数据库的工作负载 。
根据更新和删除时的行为不同,我们可以将外键分成 RESTRICT、CASCADE 和 SET NULL 等几种4,当我们为关系表中的字段增加外键约束时,需要指定外键的类型,最常见的也就是 RESTRICT 和 CASCADE 两种,其中 RESTRICT 为外键的默认类型,不同类型的外键会带来不同的额外开销,而这些额外开销就是我们不使用外键的理由:
  • 使用 RESTRICT 会在更新或者删除记录时对外键对应的记录是否存在进行一致性检查;
  • 使用 CASCADE 会在更新或者删除记录时触发级联更新或者删除操作;
注意:MySQL 中的 NO ACTION 和 RESTRICT 具有相同的语义5 。
接下来我们会详细介绍关系型数据库如何处理上述两种不同类型的外键,而我们应该如何在应用中模拟这些功能 。
一致性检查当我们使用默认的外键类型 RESTRICT 时,在创建、修改或者删除记录时都会检查引用的合法性 。想要在 MySQL 等数据库中触发外键的一致性检查其实非常容易,假设我们的数据库中包含 posts(id, author_id, content) 和 authors(id, name)两张表,在执行如下所示的操作时都会触发数据库对外键的检查:
  • 向 posts 表中插入数据时,检查 author_id 是否在 authors 表中存在;
  • 修改 posts 表中的数据时,检查 author_id 是否在 authors 表中存在;
  • 删除 authors 表中的数据时,检查 posts 中是否存在引用当前记录的外键;
作为专门用于管理数据的系统,数据库与应用服务相比能够更好地保证完整性,而上述的这些操作都是引入外键带来的额外工作,不过这也是数据库保证数据完整性的必要代价 。上述的这些分析都是理论上的定性分析,我们其实可以简单的定量分析一下引入外键对性能的影响 。
在这里我们在数据库中同时创建 authors、posts 和 foreign_key_posts 三种表,如下所示,其中 posts 和 foreign_key_posts 两个表中的列完全相同,只是 foreign_key_posts 表为 author_id 字段增加了 RESTRICT 类型的外键约束:


推荐阅读