为什么数据库不应该使用外键?
剧照 :女王的棋局
作者:Draveness
来源:真没什么逻辑
ALTER TABLE posts
ADD CONSTRAINT FOREIGN KEY (author_id)
REFERENCES authors(id);
posts
表中存在 author_id
字段。从 SQL 语句中的 CONSTRAINT
关键字我们也能推测出外键不是一种数据类型,它是不同关系表之间的约束。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
类型的外键约束: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
向表中插入数据或者修改表中的数据时,都应该执行额外的 SELECT
语句确保它引用的数据在数据库中存在;在删除数据之前需要执行额外的 SELECT
语句检查是否存在当前记录的引用;
posts
表中插入或者修改数据时,需要的处理相对比较简单,我们只需要执行有限的 SELECT
语句并按照如下所示的模式执行对应的操作就可以了:BEGIN
SELECT * FROM authors WHERE id =
FOR UPDATE; -- INSERT INTO posts ... / UPDATE posts ...
END
authors
表中的数据,就需要查询所有引用 authors
数据的表;如果有 10 个表都有指向 authors
表的外键,我们就需要在 10 个表中查询是否存在对应的记录,这个过程相对比较麻烦,不过也是为了实现完整性的必要代价,不过这种模拟外键方法其实远比使用外键更消耗资源,它不仅需要查询关联数据,还要通过网络发送更多的数据包。级联操作
CASCADE
行为,那么在客户端更新或者删除数据时就会触发级联操作:ALTER TABLE posts
ADD CONSTRAINT FOREIGN KEY (author_id)
REFERENCES authors(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
当客户端更新 authors
表中记录的主键时,数据库会同时更新posts
表中所有引用该记录的外键;当客户端删除 authors
表中的记录时,数据库会删除所有与authors
表关联的记录;
RESTRICT
行为一样,所有外键的更新和删除行为都可以通过执行额外的检查和操作保证数据的一致。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
外键会在更新和删除关系表中的数据时触发对关联记录的更新和删除,在数据量较大的数据库中可能会有数量级的放大效果;
数据库中还有哪些特性是我们在生产环境中不会使用的?为什么? 分布式的关系型数据库与 MySQL 等传统数据库有哪些区别?
近期热门文章推荐:
评论