Java进阶面霸|为什么数据库不应该使用外键( 二 )


向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类型的外键约束:
Java进阶面霸|为什么数据库不应该使用外键
文章图片
图3-外键性能测试关系图
我们先在authors表中插入一条记录 , 随后分别在posts和foreign_key_posts中插入多条新数据列引用该条记录 , 前者不会检查外键的合法性 , 而后者会做额外的检查 。 你可以在这里找到作者用来测试外键额外开销的Go语言代码6 , 经过多次基准测试 , 我们可以得到如下所示的结果:
BenchmarkBaseline-83770309503ns/opBenchmarkForeignKey-83331317162ns/opBenchmarkBaseline-83192315506ns/opBenchmarkForeignKey-83381315577ns/opBenchmarkBaseline-83298312761ns/opBenchmarkForeignKey-83829345342ns/opBenchmarkBaseline-83753291642ns/opBenchmarkForeignKey-83948325239ns/op作者执行了4次外键的基准测试 , 虽然4次测试的结果不是特别稳定 , 但是使用外键的用例在每次测试中都明显弱于不使用外键的用例 , 外键带来的额外开销分别为~2.47%、~0.02%、~10.41%和~11.52% 。 这里的基准测试只是一个比较简单的定量分析 , 但是我们也可以从结果中看到大概的趋势—外键的完整性检查确实会带来额外的性能开销 , 而这些开销在高并发的服务中需要慎重考虑 。
想要在应用程序中模拟数据库外键的功能其实比较容易 , 我们只需要遵循以下的几个准则:
向表中插入数据或者修改表中的数据时 , 都应该执行额外的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行为一样 , 所有外键的更新和删除行为都可以通过执行额外的检查和操作保证数据的一致 。


推荐阅读