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

ops/2025/1/22 21:08:00/

一、引言

当我们需要持久化地存储数据时,关系型数据库通常是首选。它不仅种类丰富、稳定,而且得到了广泛社区的支持。本文将探讨关系型数据库中的一个重要概念——外键(Foreign Key)。


二、外键的作用

在关系型数据库中,外键也被称为关系键。它是一组数据列,用于在不同关系表之间建立连接。这组数据列在当前表中是外键,而在另一个表中必须是候选键(Candidate Key)。通过候选键,我们可以在当前表中找到唯一的元素。通常,我们会使用关系表中的主键作为其他表中的外键,以满足关系型数据库外键的约束。

图 1 - 关系型数据库外键

外键不仅是一个数据库表中的列,它还提供了额外的一致性保证。因为数据库通常是整个系统的“真理之源”(Source of Truth),所以保证数据的一致性和正确性非常重要。关系型数据库提供了外键、触发器等特性来保证一致性,但在实际生产环境中,这些特性却很少被使用。

引用完整性(Referential Integrity)是数据的属性,如果数据拥有该属性,那么数据中所有的引用都是合法的。在关系型数据库的上下文中,这意味着关系型数据库中引用另一个表中的值必须存在。

上述 SQL 语句可以向关系表中增加外键约束,该语句的执行前提是 posts 表中存在 author_id 字段。从 SQL 语句中的 CONSTRAINT 关键字可以推测出,外键不是一种数据类型,而是不同关系表之间的约束。

图 2 - 无状态服务与数据库

不使用外键的原因其实很简单。MySQL、PostgreSQL 等关系型数据库很难水平扩容,但无状态的服务往往可以很容易地扩容。由于外键等特性需要数据库执行额外的工作,这些操作会占用数据库的计算资源。因此,我们可以将大部分需求迁移到无状态的服务中完成,以降低数据库的工作负载。

根据更新和删除时的行为不同,外键可以分为 RESTRICTCASCADESET NULL 等几种。当我们为关系表中的字段增加外键约束时,需要指定外键的类型。最常见的类型是 RESTRICTCASCADE,其中 RESTRICT外键的默认类型。不同类型的外键会带来不同的额外开销,而这些额外开销正是我们不使用外键的理由:

  • 使用 RESTRICT 会在更新或删除记录时对外键对应的记录是否存在进行一致性检查。

  • 使用 CASCADE 会在更新或删除记录时触发级联更新或删除操作。

需要注意的是,MySQL 中的 NO ACTIONRESTRICT 具有相同的语义。

接下来,我们将详细介绍关系型数据库如何处理上述两种不同类型的外键,以及我们如何在应用中模拟这些功能。


三、一致性检查

当我们使用默认的外键类型 RESTRICT 时,在创建、修改或删除记录时都会检查引用的合法性。假设我们的数据库中包含 posts(id, author_id, content)authors(id, name) 两张表,在执行以下操作时都会触发数据库外键的检查:

  • posts 表中插入数据时,检查 author_id 是否在 authors 表中存在。

  • 修改 posts 表中的数据时,检查 author_id 是否在 authors 表中存在。

  • 删除 authors 表中的数据时,检查 posts 中是否存在引用当前记录的外键

作为专门用于管理数据的系统,数据库与应用服务相比能够更好地保证完整性。上述操作是引入外键带来的额外工作,但也是数据库保证数据完整性的必要代价。这些分析是理论上的定性分析,我们可以通过简单的定量分析来研究引入外键对性能的影响。

在这里,我们在数据库中同时创建 authorspostsforeign_key_posts 三种表。postsforeign_key_posts 两个表中的列完全相同,只是 foreign_key_posts 表为 author_id 字段增加了 RESTRICT 类型的外键约束。

图 3 - 外键性能测试关系图

我们先在 authors 表中插入一条记录,随后分别在 postsforeign_key_posts 中插入多条新数据列引用该条记录。posts 表不会检查外键的合法性,而 foreign_key_posts 表会进行额外的检查。通过多次基准测试,我们可以得到如下结果:

执行了 4 次外键的基准测试,虽然结果不是特别稳定,但使用外键的用例在每次测试中都明显弱于不使用外键的用例。外键带来的额外开销分别为约 2.47%、0.02%、10.41% 和 11.52%。基准测试只是一个简单的定量分析,但从结果中可以看到,外键的完整性检查确实会带来额外的性能开销。在高并发的服务中,这些开销需要慎重考虑。

想要在应用程序中模拟数据库外键的功能其实比较容易,我们只需要遵循以下准则:

  • 向表中插入数据或修改表中的数据时,执行额外的 SELECT 语句确保引用的数据在数据库中存在。

  • 删除数据之前,执行额外的 SELECT 语句检查是否存在当前记录的引用。

需要注意的是,为了保证一致性,我们需要在事务中执行上述查询和修改语句,这样才能完整模拟外键的功能。当我们向 posts 表中插入或修改数据时,处理相对简单。我们只需要执行有限的 SELECT 语句并按照以下模式执行对应的操作即可:

但是,如果我们删除 authors 表中的数据,就需要查询所有引用 authors 数据的表。如果有 10 个表都有指向 authors 表的外键,我们就需要在 10 个表中查询是否存在对应的记录。这个过程相对麻烦,但也是实现完整性的必要代价。然而,这种模拟外键的方法其实远比使用外键更消耗资源。它不仅需要查询关联数据,还要通过网络发送更多的数据包。


四、级联操作

当我们在关系型数据库中创建外键约束时,如果使用如下 SQL 语句指定更新或删除记录时使用 CASCADE 行为,那么在客户端更新或删除数据时就会触发级联操作:

  • 当客户端更新 authors 表中记录的主键时,数据库会同时更新 posts 表中所有引用该记录的外键

  • 当客户端删除 authors 表中的记录时,数据库会删除所有与 authors 表关联的记录。

数据库可以保证各个关系表之间引用的一致性和合法性,不会出现引用到不存在记录的情况。与 RESTRICT 行为一样,所有外键的更新和删除行为都可以通过执行额外的检查和操作保证数据的一致性。

图 4 - 复杂的级联操作

虽然级联删除的出发点是保证数据的完整性,但在设计关系表之间的关系时,我们需要注意级联删除引起的数据大规模删除问题。如上图所示,当客户端想要在数据库中删除 authors 表中的数据时,如果我们同时在 authorsposts 中指定了级联删除的行为,那么数据库会同时删除所有关联的 posts 记录以及与 posts 表关联的 comments 数据。

这种涉及多级的级联删除行为在数据量较小的数据库中不会导致问题,但在数据量较大的数据库中,删除关键数据可能会引起雪崩效应。一条记录的删除可能会被放大到几十倍甚至上百倍,这些对磁盘的随机读写会带来巨大的开销,是我们想要尽可能避免的情况。如果我们能够较好地设计各个表之间的关系并且慎用 CASCADE 行为,这对于保证数据库中数据的合法性有着重要意义。使用该特性可以避免数据库中出现过期的、不合法的数据。

五、外键数据库性能的影响

1. 插入和更新操作延迟

外键约束会增加数据库在插入和更新操作时的负担。每次向包含外键的表中插入或更新数据时,数据库都需要检查外键约束,确保引用的记录在父表中存在。这会增加额外的查询开销,尤其是在外键引用的表较大时,这种检查可能会显著延迟操作。

2. 删除操作复杂性

删除被外键引用的记录时,数据库需要检查是否存在依赖记录,可能导致级联删除或拒绝删除,增加了操作的复杂性。

3. 锁竞争

外键约束可能导致更多的锁竞争,尤其是在高并发环境下,多个事务同时修改相关表时。这可能会导致性能瓶颈。

4. 索引开销

为了提高外键约束的检查效率,通常需要对被引用的字段创建索引,这会增加存储开销和维护成本。

5. 查询性能

虽然外键本身不会直接影响查询性能,但它们可能会通过触发器和复杂的查询逻辑间接影响。在某些情况下,外键可以优化连接操作(JOIN),因为数据库可以根据外键约束来选择最佳的连接方法,从而提高连接操作的性能。

6. 性能影响案例分析

以电商平台订单系统为例,订单表中的用户ID(user_id)作为外键指向用户表的主键。在高并发场景下,大量订单的插入和更新操作会导致频繁的外键检查,从而影响性能。

7. 优化策略

为了最大限度地减少外键对性能的影响,可以采取以下策略:

  • 合理设计表结构:适度反范式化可以减少外键的使用;表分区技术可以减少单表数据量,降低外键检查的开销。

  • 索引优化:为外键列和引用列创建适当的索引,减少回表操作。

  • 查询优化:使用索引字段和外键字段进行查询,避免全表扫描。

  • 事务和锁机制优化:尽量使用行锁而非表锁,减少锁竞争。

  • 数据库配置调整:适当增加缓冲池大小,提高数据缓存效率。

  • 应用层逻辑:在某些场景下,可以在应用层进行数据一致性检查,而非依赖数据库外键约束。

六、总结

外键数据库设计中是一个双刃剑。它可以在数据库层面确保数据的完整性和一致性,但也会增加数据库的负担,影响性能和开发灵活性。在实际应用中,需要根据具体的业务需求和性能要求来决定是否使用外键。对于数据完整性要求高的核心业务表,使用外键可以确保数据的正确性;而对于性能要求高的非核心业务表,可以在应用层通过逻辑控制来确保数据一致性,从而提高系统的整体性能。


http://www.ppmy.cn/ops/152278.html

相关文章

Models如何使用Gorm与数据库进行交互?

Gorm是Models与MySQL数据库连接的中间体(Models是通过Gorm与数据库连接起来的) Golang的代码解析成SQL语句,把查到的数据解析成GOlang的数据结构 GORM 是什么? GORM 是一个 Go 语言的 ORM(对象关系映射)库…

一文速通stack和queue的理解与使用

CSTL之stack和queue 1.stack1.1.stack的基本概念1.2.stack的接口 2.queue2.1.queue的基本概念2.2.queue的接口 3.priority_queue3.1.priority_queue的基本概念3.2.priority_queue的接口3.3.仿函数 4.容器适配器5.deque5.1.deque的简单了解5.2.deque的优缺点 🌟&…

Pytest+Allure+Excel接口自动化测试框架实战

🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 1. Allure 简介 简介 Allure 框架是一个灵活的、轻量级的、支持多语言的测试报告工具,它不仅以 Web 的方式展示了简介的测试结果,而且允许…

图数据库 | 19、高可用分布式设计(下)

相信大家对分布式系统设计与实现的复杂性已经有了一定的了解,本篇文章对分布式图数据库系统中最复杂的一类系统架构设计进行探索,即水平分布式图数据库系统(这个挑战也可以泛化为水平分布式图数据仓库、图湖泊、图中台或任何其他依赖图存储、…

Python新春烟花

目录 系列文章 写在前面 技术需求 完整代码 下载代码 代码分析 1. 程序初始化与显示设置 2. 烟花类 (Firework) 3. 粒子类 (Particle) 4. 痕迹类 (Trail) 5. 烟花更新与显示 6. 主函数 (fire) 7. 游戏循环 8. 总结 注意事项 写在后面 系列文章 序号直达链接爱…

2025年前端面试题汇总

JavaScript核心 异步编程 Promise、async/await 的工作原理及应用场景。如何处理并发请求,使用Promise.all()或Promise.race()等方法。解释事件循环机制,理解微任务(microtask)与宏任务(macrotask)的区别。…

工业相机 SDK 二次开发-Halcon 插件

本文介绍了 Halcon 连接相机时插件的使用。通过本套插件可连接海康 的工业相机。 一. 环境配置 1. 拷贝动态库 在 用 户 安 装 MVS 目 录 下 按 照 如 下 路 径 Development\ThirdPartyPlatformAdapter 找到目录为 HalconHDevelop 的文 件夹,根据 Halcon 版本找到对…

14-美妆数据分析

前言 美妆数据分析可以帮助企业更好地理解市场趋势、客户偏好和产品表现 import pandas as pd import numpy as np 一、数据清洗 data pd.read_csv(rC:\Users\B\Desktop\美妆数据.csv,encodinggbk) data.head()data.info()data data.drop_duplicates(inplaceFalse) data.r…