![为什么数据库不应该使用外键](http://img.jiangsulong.com/220417/045JKX7-2.jpg)
文章插图
图 3 - 外键性能测试关系图
我们先在 authors 表中插入一条记录,随后分别在 posts 和 foreign_key_posts中插入多条新数据列引用该条记录,前者不会检查外键的合法性,而后者会做额外的检查 。你可以在 这里 找到作者用来测试外键额外开销的 Go 语言代码6,经过多次基准测试,我们可以得到如下所示的结果:
BenchmarkBaseline-83770309503 ns/opBenchmarkForeignKey-83331317162 ns/opBenchmarkBaseline-83192315506 ns/opBenchmarkForeignKey-83381315577 ns/opBenchmarkBaseline-83298312761 ns/opBenchmarkForeignKey-83829345342 ns/opBenchmarkBaseline-83753291642 ns/opBenchmarkForeignKey-83948325239 ns/op
作者执行了 4 次外键的基准测试,虽然 4 次测试的结果不是特别稳定,但是使用外键的用例在每次测试中都明显弱于不使用外键的用例,外键带来的额外开销分别为 ~2.47%、~0.02%、~10.41% 和 ~11.52% 。这里的基准测试只是一个比较简单的定量分析,但是我们也可以从结果中看到大概的趋势 — 外键的完整性检查确实会带来额外的性能开销,而这些开销在高并发的服务中需要慎重考虑 。想要在应用程序中模拟数据库外键的功能其实比较容易,我们只需要遵循以下的几个准则:
- 向表中插入数据或者修改表中的数据时,都应该执行额外的 SELECT 语句确保它引用的数据在数据库中存在;
- 在删除数据之前需要执行额外的 SELECT 语句检查是否存在当前记录的引用;
BEGINSELECT * FROM authors WHERE id = <post.author_id> FOR UPDATE;-- INSERT INTO posts ... / UPDATE posts ...END
但是如果我们要删除 authors 表中的数据,就需要查询所有引用 authors 数据的表;如果有 10 个表都有指向 authors 表的外键,我们就需要在 10 个表中查询是否存在对应的记录,这个过程相对比较麻烦,不过也是为了实现完整性的必要代价,不过这种模拟外键方法其实远比使用外键更消耗资源,它不仅需要查询关联数据,还要通过网络发送更多的数据包 。级联操作当我们在关系型数据库中创建外键约束时,如果使用如下所示的 SQL 语句指定更新或者删除记录时使用 CASCADE 行为,那么在客户端更新或者删除数据时就会触发级联操作:
ALTER TABLE postsADD CONSTRAINT FOREIGN KEY (author_id)REFERENCES authors(id)ON UPDATE CASCADEON DELETE CASCADE;
- 当客户端更新 authors 表中记录的主键时,数据库会同时更新 posts 表中所有引用该记录的外键;
- 当客户端删除 authors 表中的记录时,数据库会删除所有与 authors 表关联的记录;
![为什么数据库不应该使用外键](http://img.jiangsulong.com/220417/045JIA4-3.jpg)
文章插图
图 4 - 复杂的级联操作
虽然级联删除的出发点也是保证数据的完整性,但是在设计关系表之间的不同关系时,我们也需要注意级联删除引起的数据大规模删除的问题 。如上图所示,当客户端想要在数据库中删除 authos 表中的数据时,如果我们同时在 authors 和 posts中指定了级联删除的行为,那么数据库会同时删除所有关联的 posts 记录以及与 posts 表关联的 comments 数据 。
这种涉及多级的级联删除行为在数据量较小的数据库中不会导致问题,但是在数据量较大的数据库中删除关键数据可能会引起雪崩,一条记录的删除可能会被放大到几十倍甚至上百倍,这些对磁盘的随机读写会带来巨大的开销,是我们想要尽可能避免的情况 。如果我们能够较好地设计各个表之间的关系并且慎用 CASCADE 行为,这对于保证数据库中数据的合法性有着很重要的意义,使用该特性可以避免数据库中出现过期的、不合法的数据,但是在使用时也要合理预估可能造成的最坏情况 。
手动实现数据库的级联删除操作是可行的,如果我们在一个事务中按照顺序删除所有的数据,确实可以保证数据的一致性,但是这与外键的级联删除功能没有太大的区别,反而会有更差的表现 。如果我们能够接受在一个时间窗口内的数据不一致,就可以将一个大号的删除任务拆成多个子任务分批执行,降低对数据库影响的峰值 。
推荐阅读
- 真香,数据库和缓存一致性的几种实现方式
- MySQL数据库运维的基本命令
- 为什么端口明明开着,nmap却扫描不出来,看老司机怎么指点迷津
- ?甲亢为什么会胖
- 为什么店铺流量有但是没有销量 为什么我开的淘宝店没有流量
- 为什么揉眼睛时会看到奇怪的图案?看完绝对涨知识!
- 人造雪是“假雪”吗?北京冬奥会为什么一定要用人造雪?
- 为什么门窗关好后家里还漏风?学会这么做,不进冷风还隔音
- 淘宝店铺买流量有用吗 为什么我开的淘宝店没有流量
- 满月宝宝为什么总爱哭