当前位置: 首页 > 科技观察

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

时间:2023-03-18 00:36:09 科技观察

为什么数据库不应该使用外键社区对关系数据库的支持非常完善。我们在上一篇文章中分析过为什么MySQL的自增主键不单调不连续。在本文中,我们将分析关系数据库中的另一个重要概念——外键(ForeignKey)。在关系数据库中,外键也称为关系键。它是多个列,提供关系数据库中关系表之间的连接。这组数据列是当前关系表中的外键,也一定是另一个关系表中的候选键(CandidateKey),我们可以通过候选键找到当前表中的唯一元素。一般情况下,我们会将关系表中的主键作为其他表中的外键,以满足关系数据库的外键约束。图1-关系数据库和外键外键不仅仅是数据库表中的一个整数,它还提供额外的一致性保证。因为数据库往往是整个系统的真实来源(SourceofTruth),所以保证数据的一致性和正确性非常重要。虽然关系型数据库提供了外键和触发器等特性来保证一致性,但是在如今的生产环境中已经很少使用了。参照完整性(ReferentialIntegrity)是数据的属性。如果数据有这个属性,那么数据中的所有引用都是合法的。在关系数据库的上下文中,这意味着关系数据库引用另一个表的值必须存在[^3]。ALTERTABLEpostsADDCONSTRAINTFOREIGNKEY(author_id)REFERENCESauthors(id);上面的SQL语句可以给关系表加上外键约束。这条SQL语句执行的前提是posts表中有author_id字段。从SQL语句中的CONSTRAINT关键字我们也可以推断出外键不是一种数据类型,而是不同关系表之间的一种约束。图2-无状态服务和数据库不使用外键的原因其实很简单。MySQL、PostgreSQL等关系型数据库很难横向扩展,而无状态服务往往可以轻松扩展。由于外键等特性需要数据库进行额外的工作,而这些操作会占用数据库的计算资源,所以我们可以将大部分需求迁移到无状态服务上,以减少数据库的工作量。根据更新和删除时的不同行为,我们可以将外键分为RESTRICT、CASCADE、SETNULL[^4]。当我们对关系表的字段添加外键约束时,需要指定外键的类型。最常见的是RESTRICT和CASCADE。其中RESTRICT是默认的外键类型。不同类型的外键会带来不同的额外开销,而这些额外开销是我们不使用外键的原因:使用RESTRICT会在更新或删除一条记录时,检查外键对应的记录是否存在;使用CASCADE会在更新或删除记录时触发级联更新或删除操作;注意:MySQL中的NOACTION和RESTRICT具有相同的语义[^5]。下面我们将详细介绍关系型数据库是如何处理以上两种不同类型的外键的,以及我们在应用中应该如何模拟这些功能。一致性检查当我们使用默认的外键类型RESTRICT时,在创建、修改或删除记录时会检查引用的有效性。在MySQL等数据库中其实很容易触发外键的一致性检查。假设我们的数据库中有posts(id,author_id,content)和authors(id,name)两张表,执行如下操作向posts表插入数据时,检查authors表中是否存在author_id;修改posts表数据时,检查authors表中是否存在author_id;删除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]。Afterseveralbenchmarktests,wecangettheresultsshownbelow:BenchmarkBaseline-83770309503ns/opBenchmarkForeignKey-83331317162ns/opBenchmarkBaseline-83192315506ns/opBenchmarkForeignKey-83381315577ns/opBenchmarkBaseline-83298312761ns/opBenchmarkForeignKey-83829345342ns/opBenchmarkBaseline-83753291642ns/opBenchmarkForeignKey-83948325239ns/opTheauthorperformed4外键基准测试。在测试中,它们明显弱于不使用外键的,外键带来的额外开销分别为~2.47%、~0.02%、~10.41%和~11.52%。这里的benchmark测试只是比较简单的量化分析,但是从结果中我们也可以看出一个大的趋势——外键的完整性检查确实会带来额外的性能开销,这些开销在高并发业务中需要慎重考虑。在应用程序中模拟数据库外键的功能其实还是比较容易的。我们只需要遵循以下准则:当向表中插入数据或修改表中的数据时,应该额外执行一条SELECT语句,以确保其引用的数据存在于数据库中;在删除数据之前,需要执行一条额外的SELECT语句来检查是否有对当前记录的引用;需要注意的是,为了保证一致性,我们需要在事务中执行上述查询和修改语句,从而充分模拟外键的功能;当我们向posts表插入或修改数据时,需要的处理比较简单,只需要执行有限的SELECT语句,按照如下模式进行相应的操作即可:BEGINSELECT*FROMauthorsWHEREid=FORUPDATE;--INSERTINTOposts.../UPDATEposts...END但如果我们要删除authors表中的数据,则需要查询所有引用authors数据的表;如果有10张表指向authors表的外键,我们需要查询这10张表中是否有对应的记录。这个过程比较麻烦,但也是实现诚信的必要代价。但是,这种模拟外键的方法实际上比使用外键要高效得多。消耗资源,它不仅需要查询关联的数据,还需要通过网络发送更多的数据包。级联操作当我们在关系数据库中创建外键约束时,如果使用如下SQL语句指定更新或删除记录时的CASCADE行为,那么在客户端更新或删除数据时会触发级联操作:ALTERTABLEpostsADDCONSTRAINTFOREIGNKEY(author_id)REFERENCESauthors(id)ONUPDATECASCADEONDELETECASCADE;当客户端更新authors表中记录的主键时,数据库将更新所有引用posts表中记录的外键;当客户端删除authors表中的记录时,数据库会删除与authors表关联的所有记录;但是,无论是执行更新还是删除操作,数据库都能保证各个关系表之间引用的一致性和有效性,不会有引用不存在的记录,这与RESTRICT行为是一样的。所有的外键更新和删除都可以通过执行额外的检查和操作来确保数据的一致性。图4-复杂的级联操作虽然级联删除的出发点是保证数据的完整性,但是我们在设计不同的关系表之间的关系时,也需要注意级联删除导致的数据的大规模删除。如上图,当客户端要删除数据库中autos表的数据时,如果我们在authors和posts中都指定了级联删除的行为,那么数据库会删除所有关联的posts记录和posts的评论表关联的数据。这种涉及多级的级联删除行为在数据量小的数据库中不会出现问题,但是在数据量大的数据库中删除关键数据可能会造成雪崩,一条记录的删除可能会被放大到几十次甚至上百次,这些对磁盘的随机读写会带来巨大的开销,这是我们要尽可能避免的。如果能更好地设计表之间的关系,谨慎使用CASCADE行为,对于保证数据库中数据的合法性具有重要意义。使用此功能可以避免数据库中的过期和非法数据。但是,在使用时也需要合理估计最坏的可能情况。手动实现数据库的级联删除操作是可行的。如果我们在一个事务中按顺序删除所有的数据,确实可以保证数据的一致性,但这和外键的级联删除功能没有太大区别。会表现更差。如果我们可以接受一个时间窗口内的数据不一致,我们可以将一个大的删除任务拆分成多个子任务,分批执行,以减少峰值对数据库的影响。DELETEFROMpostsWHEREauthor_id=1LIMIT100;DELETEFROMpostsWHEREauthor_id=1LIMIT100;...DELETEFROMauthorsWHEREid=1;与数据库外键的CASCADE相比,这种方式会带来更多的开销,但是我们可以减少瞬时对数据库性能的影响。总结一下外键在更新和删除时提供的不同行为可以帮助我们保证数据库中数据的一致性和引用的有效性,但是使用外键也需要数据库承担额外的开销,这在大多数服务中都有在横向扩展的今天,高并发场景下外键的使用确实会影响服务吞吐量的上限。可以在数据库外手动实现外键的功能,但是会带来大量的维护成本或者需要我们在数据一致性上做一些妥协。我们可以从可用性和一致性上分析使用外键、模拟外键和不使用外键的区别:不使用外键牺牲了数据库中数据的一致性,但可以减轻数据库的负载;模拟外键将部分工作移出数据库,我们可能需要放弃部分一致性以获得更高的可用性,但是为了这部分可用性,我们会付出更多的研发和维护成本,同时也会增加网络通信与数据库的次数;外键的使用保证了数据库中数据的一致性,所有的计算任务都交给了数据库;在大多数对并发要求不高或者对一致性要求不高的系统中,我们可以直接使用数据库提供的外键帮助我们校验数据,但是在复杂的场景或者对一致性要求不高的大型团队中,则是确实不使用外键可以减轻数据库的负担,大的团队也有更多的时间和精力去设计其他的方案,比如:分布式关系型数据库。我们在考虑是否应该在数据库中使用外键时,需要关注的是我们的数据库承担了这部分计算任务后,是否会影响系统的可用性。我们不应该一刀切地决定使用或不使用外键。应根据具体情况做出决定。这里我们介绍两个在使用外键时可能遇到的问题:RESTRICT外键会在更新和删除关系表中的数据时检查外键约束的有效性,以确保外键不会引用不存在的记录;CASCADE外键在更新和删除关系表中的数据时会触发关联记录的更新和删除,这在数据量很大的数据库中可能会产生一个数量级的放大效应;我们在很多情况下是无法选择是否使用外键的。大部分公司的DBA对于数据库系统的使用都会有比较明确的规定,但是我们必须清楚做出选择使用外键或者不使用外键的原因。最后,让我们看看一些未解决的相关问题。有兴趣的读者可以仔细思考以下问题:数据库中是否还有其他一些我们不会在生产环境中使用的特性?为什么?分布式关系型数据库和MySQL等传统数据库有什么区别?