为什么数据库不应该使用外键社区对关系数据库的支持非常完善。我们在上一篇文章中分析过为什么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=
