为什么在数据库设计中要使用一对一关系?

36

我很难确定何时在数据库设计中使用一对一关系,或者是否有必要使用它。

如果您可以仅选择查询所需的列,那么是否有必要将表拆分为一对一的关系?我想更新大型表对性能的影响比较大,而且我确定这取决于表在某些操作(读/写)方面的使用情况。

因此,在设计数据库模式时,您如何考虑一对一关系?您使用什么标准来确定是否需要一个,并且与不使用一个相比的好处是什么?


4
这个网页总结得很好,基本上就是问:在什么情况下使用数据库的11种关系才有意义。 - Willem van Rumpt
我知道这是一个老问题,但我有很多合法的一对一关系的例子,其中大部分都是唯一正确的解决方案:https://dev59.com/LnRB5IYBdhLWcg3wxZ_Y#28313151 - Tripartio
6个回答

61

从逻辑角度来看,1:1的关系应该总是合并到一个单独的表中。

另一方面,对于这样的"垂直分区"或“行拆分”,可能存在物理考虑因素,特别是如果您知道某些列将比其他列更频繁地访问或以不同的模式访问,例如:

  • 你可能希望对一对一关系的两个“端点”表进行聚类分区处理,以不同的方式处理它们。
  • 如果您的DBMS允许,您可能希望将它们放在不同的物理磁盘上(例如,在SSD上更注重性能,在廉价HDD上使用其他方法)。
  • 您已经测量了缓存的效果,并且希望确保“热”列保持在缓存中,而“冷”列则不会“污染”缓存。
  • 您需要一种比整行更“窄”的并发行为(例如锁定)。这是高度特定于DBMS的。
  • 您需要在不同的列上具有不同的安全性,但是您的DBMS不支持列级权限。
  • 触发器通常是特定于表的。虽然您可以在理论上只有一个表,并使触发器忽略行的“错误半部分”,但某些数据库可能会对触发器可以执行的操作施加额外的限制。例如,Oracle不允许您从行级触发器修改所谓的“变异”表 - 通过具有单独的表,仅可以使其中一个表变异,因此您仍然可以从触发器修改另一个表(但是有其他方法可以解决这个问题)。
数据库非常擅长操作数据,所以我不会仅为了更新性能而拆分表格,除非你已经对代表性的数据量进行了实际基准测试,并得出性能差异确实存在且足够显著(例如抵消了JOIN的增加需求)。
另一方面,如果您正在讨论“1:0或1”(而不是真正的1:1),那么这是完全不同的问题,需要不同的答案...
另请参阅:何时应使用一对一关系?

从我在答案中收集到的信息来看,似乎应该主要使用1:1来进行性能优化或安全方面的考虑。我喜欢使用超类型/子类型的想法,但据我理解,那被认为是1:0或1。 - chobo
我认为第一句话需要一些解释/推理。为什么在我们的模型中将其简化为“一个用户一个地址”时,逻辑上地址不会成为一个实际的独立实体呢? - user3738870
@user3738870 我的意思是1:1实体行表现为单个表行。你不能只有一个实体行而没有另一个,就像你不能只有表行的一半而没有另一半一样。 - Branko Dimitrijevic
我明白了,我认为那句话中“should”的语气太强了,用“could”会更好。 - user3738870

10

职责分离和数据库表抽象化。

如果我有一个用户,并为每个用户设计了一个地址系统,但随后我更改了该系统,则只需向Address表中添加新记录即可,而不是添加全新的表并迁移数据。

编辑

当前,如果您想要一个人员记录,并且每个人都有一个确切的地址记录,则可以在Person表和Address表之间建立一对一的关系,或者您只需在Person表中添加地址列。

将来,如果您决定允许一个人拥有多个地址,则在一对一关系方案中无需更改数据库结构,只需更改处理返回的数据方式。然而,在单表结构中,您需要创建一个新表并将地址数据迁移到新表中,以创建最佳实践的一对多关系数据库结构。


如果您将记录添加到“address”表中,而没有向“user”表添加记录,则这不再是一对一的关系。 - Branko Dimitrijevic
@BrankoDimitrijevic,显然我的原始解释没有清楚地表达我的观点。我只是想说,如果您将来的数据策略是让每个人拥有多个地址,那么您只需要向地址表中添加一个新记录,现在您就拥有了Person和Address之间的一对多关系。 - bdparrish
@bdparrish 是的,关于演化的观点是正确的。如果当前期望的1:1关系在未来可能变为1:N(甚至只是“1:0或1”),那么“预分裂”表可能是合理的。我同意突出这一点。 - Branko Dimitrijevic

5
在理论上,规范化的形式似乎是最好的选择。但在现实世界中,通常需要做出取舍。我认识的大多数大型系统都会进行权衡,而不是试图完全规范化。
我来举个例子。如果您正在一个银行应用中,有1000万个存折账户,通常的交易只是查询某个账户的最新余额。您有一个表A,仅存储这些信息(账号、账户余额和账户持有人姓名)。
您的账户还有另外40个属性,例如客户地址、税号、映射到其他系统的ID,它们在表B中。
A和B之间存在一对一的映射关系。
为了能够快速检索账户余额,您可能希望针对具有账户余额和账户持有人姓名的小表采用不同的索引策略(例如哈希索引)。
包含其他40个属性的表可能存储在不同的表空间或存储器中,采用不同类型的索引,例如您想按名称、账号、分行ID等排序。您的系统可以容忍这些40个属性的较慢检索,而您需要通过账号快速检索账户余额。
将所有43个属性放在一个表中似乎是很自然的,但可能是“天生缓慢”的,而且单纯检索单个账户余额也是不可接受的。

1
这就是我感到困惑的地方。在你的例子中,你有一个包含43个属性的表格,并且你将其中3个属性提取到一个新表格中以便更快地查询。难道你不能编写一个仅从大表格中选择三列的查询吗?如果你从具有43个属性的表格和只选择其中三个字段的表格中查询,是否存在性能差异? - chobo
哦,刚才再读了一遍,主要是为了将它们放在不同的存储设备上。 - chobo
同样的,当你使用BLOBs和CLOBs时,你经常会将它们隔离在自己相关的表中,以避免对不涉及它们的列产生额外开销。 - wmorrison365
1
@wmorrison365 - 假设我有一个包含二进制大对象图像的表,当查询该表时我不选择它们,那么这个二进制列仍然会有很多开销吗?还是你指的是在具有该数据类型的表上进行更新和插入操作? - chobo

2
通常人们谈论1:0..1的关系时会称其为1:1,实际上,任何情况下典型的RDBMS都无法支持文字意义上的1:1关系。因此,即使它技术上需要1:0..1关系而不是1:1的文字概念,我认为在这里讨论子类也是公平的。
当您拥有多个实体/表之间的完全相同的字段时,1:0..1就非常有用了。例如,联系信息字段如地址、电话号码、电子邮件等可能对于员工和客户都是共同的,可以将它们拆分成一个纯粹用于联系信息的实体。
联系表将保存通用信息,如地址和电话号码等。
因此,员工表保存特定于员工的信息,例如员工号、雇佣日期等。它还将具有对员工联系信息的外键引用。
客户表将保留客户信息,如电子邮件地址、雇主名称和一些人口统计数据,例如性别和/或婚姻状况。该客户还将具有对其联系信息的外键引用。
通过这样做,每个员工都将有一个联系人,但并非每个联系人都有一个员工。客户也适用相同的概念。

2
使用1对1关系来模拟现实世界中的实体是有道理的。这样,当您向您的“世界”添加更多实体时,它们只需要与它们相关的数据相关联(而不是更多)。
这确实是关键,您的数据(每个表)应该只包含足以描述它所代表的真实世界事物的数据,没有多余的字段,所有字段都与“事物”相关。这意味着在整个系统中重复的数据较少(会带来更新问题!),您可以独立检索各个数据(例如不必拆分/解析字符串)。
要想了解如何做到这一点,您应该学习“数据库规范化”(Normalization)、“正则化形式”(Normal Form)和“第一、二、三正则化形式”。这描述了如何分解您的数据。带有示例的版本总是很有帮助的。也许可以尝试这个教程

真正的1:1关系是“现实世界中的单个对象”。你可能在谈论“1到0或1”。 - Branko Dimitrijevic
我只是想说,如果你有实体A和实体B,你可以将它们分组到一个表中。但是,当实体C出现时,它仅与实体B中存在的数据项(列)相关,那么它别无选择,只能与整个分组表相关联(即使它对实体A的列没有兴趣)。 - wmorrison365
@wmorrison365,我不确定你所说的“pertains”的意思,但如果C和B之间的关系是真正的1:1 并且 B和A之间的关系也是真正的1:1,则C和A之间的关系也必须是1:1。因此,你不能只有其中之一而没有全部。这基本上就是“成为单个对象”的意思。 - Branko Dimitrijevic
@bdparrish 没错。无论 A 和 B 是否合并,它们都与 C 之间存在 1:N 的关系。然而,在物理层面上,你通常只需要通过一个外键(例如从 C 到 B)来强制执行这种关系。 - Branko Dimitrijevic
@chobo 两者都是。我们之前讨论的不是超类型/子类型,而且一般情况下超类型/子类型并不是1:1的。只有在恰好存在一个子类型时,区分类别层次结构才是1:1的。如果存在多个子类型,那么当你"实例化一个对象"时,你会在父表中插入一行和_一个_子表中的一行。由于你没有在_所有_子表中插入,因此这是"1对0或1",而不是完全"1对1"。非区分类别层次结构在理论上可能是1:1(如果你总是在父表和所有子表中都插入),但这基本上破坏了其目的... - Branko Dimitrijevic
显示剩余5条评论

1

过去项目的几个样本:

  • 一个TestRequests表只能有一个匹配的Report。但是根据请求的性质,报告中的字段可能完全不同。
  • 在一个银行项目中,一个Entities表保存各种类型的实体:基金、房地产属性、公司。大多数这些实体具有相似的属性,但基金需要额外的约120个字段,而它们只占记录的5%。

这两个示例都不是"1:1",而是"1:0或者1"。 - Branko Dimitrijevic
@Branko:完全正确,但我不认为真正的1:1有任何用处。此外,我不知道如何对真正的1:1执行引用完整性!一侧必须单独保存,然后才能检查另一侧。 - iDevlop
这种双向外键在这里进行了讨论。https://dev59.com/nUnSa4cB1Zd3GeqPSO2j - sam yi
@iDevlop 请查看我的回答以获取一些用途。 "真正的" 1:1 通常是通过推迟 FOREIGN KEY 约束来强制执行的,但并非所有 DBMSes 都支持(尤其是 MS SQL Server)。可以通过在两个表中使用不同的 PK(如有必要引入代理 PK),然后允许其中一个 FK 子端点为 NULL(以打破插入循环),然后通过触发器保护非 NULL 不会恢复为 NULL 来在某种程度上模拟它。通常最好在应用程序级别实现 1:0..1 并强制执行 1:1。 - Branko Dimitrijevic
@Branko:对你的回复点赞。哪个已知的关系型数据库支持1:1? - iDevlop
@iDevlop 从我个人的经验来看,Oracle和PostgreSQL支持延迟约束。我认为IBM DB2、MySQL和Interbase/Firebird不支持,但我需要再次确认一下。 - Branko Dimitrijevic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接