您当前的位置:首页 > 电脑百科 > 数据库 > 百科

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

时间:2020-06-13 11:47:42  来源:  作者:

作者:Draveness

来源:真没什么逻辑

为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。

当我们想要持久化地存储数据时,使用关系型数据库往往都是最稳妥的选择,这不仅因为今天的关系型数据库种类非常丰富并且稳定,还因为不同社区对关系型数据库的支持都非常完备。我们在前面的文章中曾经分析过 为什么 MySQL 的自增主键不单调也不连续,这篇文章我们来分析关系型数据库中另一个重要的概念 — 外键(Foreign Key)。

在关系型数据库中,外键也被称为关系键,它是关系型数据库中提供关系表之间连接的多个列1,这一组数据列是当前关系表中的外键,也必须是另一个关系表中的候选键(Candidate Key),我们可以通过候选键在当前表中找到唯一的元素2。在通常情况下,我们都会使用关系表中的主键作为其他表中的外键,这样才可以满足关系型数据库对外键的约束。

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

图 1 - 关系型数据库与外键

外键不仅仅是数据库表中的一个整数,它还提供了额外的一致性保证。因为数据库往往是整个系统的真理之源(Source of Truth),所以保证数据的一致性和正确性非常重要,关系型数据库虽然提供了外键、触发器等特性保证一致性,但是在今天的生产环境中却很少被使用。

引用完整性(Referential Integrity)是数据的属性,如果数据拥有该属性,那么数据中所有的引用都是合法的,在关系型数据库的上下文中,这就意味着关系型数据库中引用另一个表中的值必须存在3。

ALTER TABLE posts
ADD 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 类型的外键约束:

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

图 3 - 外键性能测试关系图

我们先在 authors 表中插入一条记录,随后分别在 posts 和 foreign_key_posts中插入多条新数据列引用该条记录,前者不会检查外键的合法性,而后者会做额外的检查。你可以在 这里 找到作者用来测试外键额外开销的 Go 语言代码6,经过多次基准测试,我们可以得到如下所示的结果:

BenchmarkBaseline-8     	    3770	    309503 ns/op
BenchmarkForeignKey-8   	    3331	    317162 ns/op

BenchmarkBaseline-8     	    3192	    315506 ns/op
BenchmarkForeignKey-8   	    3381	    315577 ns/op

BenchmarkBaseline-8     	    3298	    312761 ns/op
BenchmarkForeignKey-8   	    3829	    345342 ns/op

BenchmarkBaseline-8     	    3753	    291642 ns/op
BenchmarkForeignKey-8   	    3948	    325239 ns/op

作者执行了 4 次外键的基准测试,虽然 4 次测试的结果不是特别稳定,但是使用外键的用例在每次测试中都明显弱于不使用外键的用例,外键带来的额外开销分别为 ~2.47%、~0.02%、~10.41% 和 ~11.52%。这里的基准测试只是一个比较简单的定量分析,但是我们也可以从结果中看到大概的趋势 — 外键的完整性检查确实会带来额外的性能开销,而这些开销在高并发的服务中需要慎重考虑。

想要在应用程序中模拟数据库外键的功能其实比较容易,我们只需要遵循以下的几个准则:

  • 向表中插入数据或者修改表中的数据时,都应该执行额外的 SELECT 语句确保它引用的数据在数据库中存在;
  • 在删除数据之前需要执行额外的 SELECT 语句检查是否存在当前记录的引用;

需要注意的是为了保证一致性,我们需要在事务中执行上述的查询和修改语句,这样才能完整模拟外键的功能;当我们向 posts 表中插入或者修改数据时,需要的处理相对比较简单,我们只需要执行有限的 SELECT 语句并按照如下所示的模式执行对应的操作就可以了:

BEGIN
SELECT * FROM authors WHERE id = <post.author_id> FOR UPDATE;
-- INSERT INTO posts ... / UPDATE posts ...
END

但是如果我们要删除 authors 表中的数据,就需要查询所有引用 authors 数据的表;如果有 10 个表都有指向 authors 表的外键,我们就需要在 10 个表中查询是否存在对应的记录,这个过程相对比较麻烦,不过也是为了实现完整性的必要代价,不过这种模拟外键方法其实远比使用外键更消耗资源,它不仅需要查询关联数据,还要通过网络发送更多的数据包。

级联操作

当我们在关系型数据库中创建外键约束时,如果使用如下所示的 SQL 语句指定更新或者删除记录时使用 CASCADE 行为,那么在客户端更新或者删除数据时就会触发级联操作:

ALTER TABLE posts
ADD CONSTRAINT FOREIGN KEY (author_id)
REFERENCES authors(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
  • 当客户端更新 authors 表中记录的主键时,数据库会同时更新 posts 表中所有引用该记录的外键;
  • 当客户端删除 authors 表中的记录时,数据库会删除所有与 authors 表关联的记录;

不过无论是执行更新还是删除操作,数据库都可以保证各个关系表之间引用的一致性和合法性不会出现引用到不存在记录的情况,与 RESTRICT 行为一样,所有外键的更新和删除行为都可以通过执行额外的检查和操作保证数据的一致。

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

图 4 - 复杂的级联操作

虽然级联删除的出发点也是保证数据的完整性,但是在设计关系表之间的不同关系时,我们也需要注意级联删除引起的数据大规模删除的问题。如上图所示,当客户端想要在数据库中删除 authos 表中的数据时,如果我们同时在 authors 和 posts中指定了级联删除的行为,那么数据库会同时删除所有关联的 posts 记录以及与 posts 表关联的 comments 数据。

这种涉及多级的级联删除行为在数据量较小的数据库中不会导致问题,但是在数据量较大的数据库中删除关键数据可能会引起雪崩,一条记录的删除可能会被放大到几十倍甚至上百倍,这些对磁盘的随机读写会带来巨大的开销,是我们想要尽可能避免的情况。如果我们能够较好地设计各个表之间的关系并且慎用 CASCADE 行为,这对于保证数据库中数据的合法性有着很重要的意义,使用该特性可以避免数据库中出现过期的、不合法的数据,但是在使用时也要合理预估可能造成的最坏情况。

手动实现数据库的级联删除操作是可行的,如果我们在一个事务中按照顺序删除所有的数据,确实可以保证数据的一致性,但是这与外键的级联删除功能没有太大的区别,反而会有更差的表现。如果我们能够接受在一个时间窗口内的数据不一致,就可以将一个大号的删除任务拆成多个子任务分批执行,降低对数据库影响的峰值。

DELETE FROM posts WHERE author_id = 1 LIMIT 100;
DELETE FROM posts WHERE author_id = 1 LIMIT 100;
...
DELETE FROM authors WHERE id = 1;

与数据库外键的 CASCADE 相比,这种方式会带来更大的额外开销,只是我们能降低对数据库性能的瞬时影响。

总结

外键提供的几种在更新和删除时的不同行为都可以帮助我们保证数据库中数据的一致性和引用合法性,但是外键的使用也需要数据库承担额外的开销,在大多数服务都可以水平扩容的今天,高并发场景中使用外键确实会影响服务的吞吐量上限。在数据库之外手动实现外键的功能是可能的,但是却会带来很多维护上的成本或者需要我们在数据一致性上做出一些妥协。我们可以从可用性、一致性几个方面分析使用外键、模拟外键以及不使用外键的差异:

  • 不使用外键牺牲了数据库中数据的一致性,但是却能够减少数据库的负载;
  • 模拟外键将一部分工作移到了数据库之外,我们可能需要放弃一部分一致性以获得更高的可用性,但是为了这部分可用性,我们会付出更多的研发与维护成本,也增加了与数据库之间的网络通信次数;
  • 使用外键保证了数据库中数据的一致性,也将全部的计算任务全部交给了数据库;

在大多数不需要高并发或者对一致性有较强要求的系统中,我们可以直接使用数据库提供的外键帮助我们对数据进行校验,但是在对一致性要求不高的、复杂的场景或者大规模的团队中,不使用外键也确实可以为数据库减负,而大团队也有更多的时间和精力去设计其他的方案,例如:分布式的关系型数据库。

当我们考虑应不应该在数据库中使用外键时,需要关注的核心我们的数据库承担这部分计算任务后会不会影响系统的可用性,在使用时也不应该一刀切的决定用或者不用外键,应该根据具体的场景做决策,我们在这里介绍了两个使用外键时可能遇到的问题:

  • RESTRICT 外键会在更新和删除关系表中的数据时对外键约束的合法性进行检查,保证外键不会引用到不存在的记录;
  • CASCADE 外键会在更新和删除关系表中的数据时触发对关联记录的更新和删除,在数据量较大的数据库中可能会有数量级的放大效果;

我们在很多时候其实并不能选择是否使用外键,大多数公司的 DBA 都会对数据库系统的使用有比较明确的规定,但是我们要清楚做出使用外键和不使用外键这一抉择的原因。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • 数据库中还有哪些特性是我们在生产环境中不会使用的?为什么?
  • 分布式的关系型数据库与 MySQL 等传统数据库有哪些区别?


Tags:外键   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
作者:Draveness来源:真没什么逻辑为什么这么设计(Why&rsquo;s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并...【详细内容】
2020-06-13  Tags: 外键  点击:(68)  评论:(0)  加入收藏
首先我们明确一点,外键约束是一种约束,这个约束的存在,会保证表间数据的关系“始终完整”。因此,外键约束的存在,并非全然没有优点。1. 引言其实这个话题是老生常谈,很多人在工作...【详细内容】
2020-04-03  Tags: 外键  点击:(82)  评论:(0)  加入收藏
Piotr Kononow是一位业务分析师、软件架构师和项目经理,他拥有15年以上编程经验和背景(SQL,java,C++…)。他的专长是数据仓库/ BI和商业应用,这是他的一篇文章:...【详细内容】
2019-08-15  Tags: 外键  点击:(224)  评论:(0)  加入收藏
概述闲来无事,看了下高性能mysql这本书,其中的一些MySQL高级特性写的还不错,在这里总结分享下。01分区表1、分区表限制 一张表最多1024个分区 分区表中无法使用外键约束2、分区...【详细内容】
2019-07-12  Tags: 外键  点击:(330)  评论:(0)  加入收藏
▌简易百科推荐
1增1.1【插入单行】insert [into] <表名> (列名) values (列值)例:insert into Strdents (姓名,性别,出生日期) values (&#39;开心朋朋&#39;,&#39;男&#39;,&#39;1980/6/15&#3...【详细内容】
2021-12-27  快乐火车9d3    Tags:SQL   点击:(2)  评论:(0)  加入收藏
最近发现还有不少做开发的小伙伴,在写存储过程的时候,在参考已有的不同的写法时,往往很迷茫, 不知道各种写法孰优孰劣,该选用哪种写法,以及各种写法的优缺点,本文以一个简单的查询...【详细内容】
2021-12-23  linux上的码农    Tags:sql   点击:(9)  评论:(0)  加入收藏
《开源精选》是我们分享Github、Gitee等开源社区中优质项目的栏目,包括技术、学习、实用与各种有趣的内容。本期推荐的HasorDB 是一个全功能数据库访问工具,提供对象映射、丰...【详细内容】
2021-12-22  GitHub精选    Tags:HasorDB   点击:(5)  评论:(0)  加入收藏
作者丨Rafal Grzegorczyk译者丨陈骏策划丨孙淑娟【51CTO.com原创稿件】您是否还在手动对数据库执行各种脚本?您是否还在浪费时间去验证数据库脚本的正确性?您是否还需要将...【详细内容】
2021-12-22    51CTO  Tags:Liquibase   点击:(4)  评论:(0)  加入收藏
场景描述:由于生产环境的表比较复杂,字段很多。这里我们做下简化,只为说明今天要聊的问题。有两张表 tab1,tab2: tab1 数据如下: tab2 数据如下: 然后给你看下,我用来统计 name=&#3...【详细内容】
2021-12-20  Bald    Tags:SQL   点击:(7)  评论:(0)  加入收藏
前言知识无底,学海无涯,知识点虽然简单,但是比较多,所以将MySQL的基础写出来,方便自己以后查找,还有就是分享给大家。一、SQL简述1.SQL的概述Structure Query Language(结构化查...【详细内容】
2021-12-16  谣言止于独立思考    Tags:SQL基础   点击:(13)  评论:(0)  加入收藏
前言作为一名测试工程师,工作中在对测试结果进行数据比对的时候,或多或少要和数据库打交道的,要和数据库打交道,那么一些常用的 SQL 查询语法必须要掌握。最近有部分做测试小伙...【详细内容】
2021-12-14  柠檬班软件测试    Tags:SQL   点击:(15)  评论:(0)  加入收藏
话说C是面向内存的编程语言。数据要能存得进去,取得出来,且要考虑效率。不管是顺序存储还是链式存储,其寻址方式总是很重要。顺序存储是连续存储。同质结构的数组通过其索引表...【详细内容】
2021-12-08  小智雅汇    Tags:数据存储   点击:(18)  评论:(0)  加入收藏
概述DBConvert Studio 是一款强大的跨数据库迁移和同步软件,可在不同数据库格式之间转换数据库结构和数据。它将成熟、稳定、久经考验的 DBConvert 和 DBSync 核心与改进的现...【详细内容】
2021-11-17  雪竹聊运维    Tags:数据库   点击:(26)  评论:(0)  加入收藏
一、前言 大家好,我是小诚,《从0到1-全面深刻理解MySQL系列》已经来到第四章,这一章节的主要从一条SQL执行的开始,由浅入深的解析SQL语句由客户端到服务器的完整执行流程,最...【详细内容】
2021-11-09  woaker    Tags:SQL   点击:(35)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条