15

获取三层信息:

第 1 层:信息

该层包含具有UNIQUE自然索引的数据和易于传输的代理键。

Table Surnames:

+-----------------------------+--------------+
|    ID (Auto Increment, PK)  |    Surname   |
+-----------------------------+--------------+
|               1             |     Smith    |
|               2             |    Edwards   |
|               3             |     Brown    |
+-----------------------------+--------------+

Table FirstNames

+-----------------------------+--------------+
|   ID (Auto Increment, PK)   |   FirstName  |
+-----------------------------+--------------+
|               1             |     John     |
|               2             |     Bob      |
|               3             |     Mary     |
|               4             |     Kate     |
+-----------------------------+--------------+

自然键

或者,如 Mike Sherrill 所解释的,上面的两个表可以不ID使用 Surname 和 FirstName 作为自然主键。在这种情况下,假设下面的层引用varchar而不是int.

第 2 层:人

在这一层中,使用了复合索引。此值可以是UNIQUEPRIMARY,具体取决于代理键是否用作主键。

+-----------------+--------------+
|    FirstName    |    LastName  |
+-----------------+--------------+
|        1        |       2      |
|        1        |       3      |
|        2        |       3      |
|        3        |       1      |
|        4        |       2      |
|       ...       |      ...     |
+-----------------+--------------+

第三层:父母

在这一层中,人们之间的关系是通过一个ParentsOf表格来探索的。

ParentsOf

+-----------------+-----------------+
|      Person     |   PersonParent  |
+-----------------+-----------------+

 OR

+-----------------+-----------------+-----------------+-----------------+
| PersonFirstName |  PersonSurname  | ParentFirstName |  ParentSurname  |
+-----------------+-----------------+-----------------+-----------------+

问题

假设引用完整性在其核心对我来说非常重要,并且我将拥有FOREIGN KEYS这些索引,以便我让数据库负责在这方面监控其自身的完整性,并且如果我要使用 ORM,它会像Doctrine一样,它具有对复合主键的本机支持......

请帮助我理解:

  • 在第一层使用代理键与自然键时发生的权衡列表。

  • 在第二层使用复合键与代理键时发生的权衡列表,可以转移到第三层。

我对听哪个更好不感兴趣,因为我知道专业人士在这个话题上存在重大分歧,这将引发一场宗教战争。相反,我非常简单和尽可能客观地问,通过将代理键传递给每个层与维护主键(自然/复合或代理/复合),您将采取哪些权衡。任何人都可以在 SO 和其他网站上找到说从不总是使用代理键的人。相反,对权衡的合理分析是我在您的回答中最欣赏的。

编辑:有人指出,姓氏示例是使用 6NF 的不良示例。为了保持问题完整,我将保留它。如果您无法想象这个用例,一个更好的可能是“杂货项目”列表。又名:

+-----------------------------+--------------+
|   ID (Auto Increment, PK)   |   Grocery    |
+-----------------------------+--------------+
|               1             | Sponges      |
|               2             | Tomato Soup  |
|               3             | Ice Cream    |
|               4             | Lemons       |
|               5             | Strawberries |
|               6             | Whipped Cream|
+-----------------------------+--------------+

+-----------------------------+--------------+
|   ID (Auto Increment, PK)   |   Brand      |
+-----------------------------+--------------+
|               1             | Bright       |
|               2             | Ben & Jerry's|
|               3             | Store Brand  |
|               4             | Campbell's   |
|               5             | Cool Whip    |
+-----------------------------+--------------+    

自然复合键示例:

+-----------------------------+--------------+
|           Grocery           |   Brand      |
+-----------------------------+--------------+
|           Sponges           | Bright       |
|           Ice Cream         | Ben & Jerry's|
|           Ice Cream         | Store Brand  |
|           Tomato Soup       | Campbell's   |
|           Tomato Soup       | Store Brand  |
|           Lemons            | Store Brand  |
|           Whipped Cream     | Cool Whip    |
+-----------------------------+--------------+ 

推荐配对

+-----------------+-----------------+-----------------+-----------------+
|     Grocery1     |  Brand1        | Grocery2        |  Brand2         |
+-----------------+-----------------+-----------------+-----------------+

重申一下,这也只是一个例子。这不是我建议进行的方式,但它应该有助于说明我的问题。

这种方法存在不足之处。我要重申,这个问题是要求了解下面每种方法的优缺点,而不是强调其中一种方法比另一种更好。我相信大多数人都能够通过这个特定示例的可疑性质来回答核心问题。此编辑适用于那些不能。

下面有一些非常好的答案,如果您对前进的方向感到好奇,请阅读它们。

结束编辑

谢谢!

4

8 回答 8

26

这里有一些权衡:

单一代理(人工创建):

  • 所有子表的外键只需要一个列来引用主键。

  • 很容易更新表中的自然键,而不需要用外键更新每个子表

  • 较小的主/外键索引(即不宽) 这可以使数据库运行得更快,例如,当父表中的一条记录被删除时,需要搜索子表以确保这不会创建孤儿。窄索引扫描速度更快(只是视觉上)。

  • 您将拥有更多索引,因为您很可能还希望索引数据中存在的任何自然键。

自然复合键表:

  • 数据库中的索引更少

  • 数据库中的列更少

  • 更容易/更快地插入大量记录,因为您不需要获取序列生成器

  • 更新复合中的一个键需要同时更新每个子表。

然后还有另一类:人工复合主键

我只发现了一个有意义的例子。当您需要标记每个表中的每条记录以实现行级安全性时。

例如,假设您有一个为 50,000 个客户端存储数据的数据库,并且每个客户端不应该看到其他客户端的数据——这在 Web 应用程序开发中很常见。

如果每条记录都标记有一个client_id字段,则您正在创建一个行级安全环境。大多数数据库都有在正确设置时强制执行行级安全性的工具。

首先要做的是设置主键和外键。通常一个表有一个id字段作为主键。通过添加client_id键现在是复合键。并且有必要携带client_id到所有子表。

复合键基于 2 个代理键,是一种确保客户端之间和整个数据库内数据完整性的防弹方法。

在此之后,您将创建视图(或者如果使用 Oracle EE 设置虚拟专用数据库)和其他各种结构,以允许数据库强制执行行级安全性(这是它拥有的主题)。

假设这个数据结构不再规范化到第 n 次。client_id每个 pk/fk 中的字段对其他正常模型进行非规范化。该模型的好处是易于在数据库级别强制执行行级别的安全性(这是数据库应该做的)。每次选择、插入、更新、删除都仅限于client_id您当前设置的会话。数据库具有会话意识

概括

代理键始终是安全的选择。他们需要更多的工作来设置并需要更多的存储空间。

我认为最大的好处是:

  • 能够更新一个表中的 PK 并且所有其他子表都会立即更改而无需被触及。

  • 当数据被弄乱时——它会在某个时候由于编程错误而变得混乱,代理键使清理变得更加容易,并且在某些情况下只能因为有代理键才能做到这一点。

  • 查询性能得到提高,因为 db 能够搜索属性以定位 s.key,然后通过单个数字键连接所有子表。

Natural Keys 尤其是复合 NKeys 让编写代码变得很痛苦。当您需要连接 4 个表时,“where 子句”将比使用单个 SKey 时长得多(并且更容易搞砸)。

代理键是“安全”的途径。自然键在一些地方是有益的,我会说数据库中大约 1% 的表。

于 2014-05-25T00:43:14.713 回答
10

首先,您的第二层至少可以表达四种不同的方式,它们都与您的问题相关。下面我使用的是伪 SQL,主要是 PostgreSQL 语法。无论结构如何,某些类型的查询都需要递归和多个附加索引,所以我不会再多说。使用支持聚集索引的 dbms 可能会影响这里的一些决策,但不要假设聚集索引上的六个连接会比简单地从单个覆盖索引中读取值更快;测试,测试,测试。

其次,第一层确实没有太多的权衡。外键可以引用声明的列not null unique,其方式与引用声明的列的方式完全相同primary key。代理键将表的宽度增加了 4 个字节;对于大多数(但不是全部)数据库应用程序来说,这是微不足道的。

第三,正确的外键和唯一约束将在所有这四种设计中保持参照完整性。(但请参见下面的“关于级联”。)

A. 代理键的外键

create table people (
  FirstName integer not null
    references FirstNames (ID),
  LastName integer not null
    references Surnames (ID),
  primary key (FirstName, LastName)
);

B. 自然键的外键

create table people (
  FirstName varchar(n) not null
    references FirstNames (FirstName),
  LastName varchar(n) not null
    references Surnames (Surname),
  primary key (FirstName, Surname)
);

C.外键到代理键,附加代理键

create table people (
  ID serial primary key,
  FirstName integer not null
    references FirstNames (ID),
  LastName integer not null
    references Surnames (ID),
  unique (FirstName, LastName)
);

D. 自然键的外键,附加代理键

create table people (
  ID serial primary key,
  FirstName varchar(n) not null
    references FirstNames (FirstName),
  LastName varchar(n) not null
    references Surnames (Surname),
  unique (FirstName, Surname)
);

现在让我们看看ParentsOf 表。

A. 上面 A 中代理键的外键

create table ParentsOf (
  PersonFirstName integer not null,
  PersonSurname integer not null,
  foreign key (PersonFirstName, PersonSurname)
    references people (FirstName, LastName),

  ParentFirstName integer not null,
  ParentSurname integer not null,
  foreign key (ParentFirstName, ParentSurname)
    references people (FirstName, LastName),

  primary key (PersonFirstName, PersonSurname, ParentFirstName, ParentSurname)
);

要检索给定行的名称,您需要四个连接。您可以直接加入“FirstNames”和“Surnames”表;您无需通过“人员”表加入即可获取名称。

B. 上面 B 中自然键的外键

create table ParentsOf (
  PersonFirstName varchar(n) not null,
  PersonSurname varchar(n) not null,
  foreign key (PersonFirstName, PersonSurname)
    references people (FirstName, LastName),

  ParentFirstName varchar(n) not null,
  ParentSurname varchar(n) not null,
  foreign key (ParentFirstName, ParentSurname)
    references people (FirstName, LastName),

  primary key (PersonFirstName, PersonSurname, ParentFirstName, ParentSurname)
);

此设计需要零连接来检索给定行的名称。许多 SQL 平台根本不需要读取表,因为它们可以从主键上的索引中获取所有数据。

C.外键到代理键,C中的附加代理键,上图

create table ParentsOf (
  Person integer not null
    references People (ID),
  PersonParent integer not null
    references People (ID),
  primary key (Person, PersonParent)
);

要检索姓名,您必须通过“people”表加入。您总共需要六个连接。

D. 自然键的外键,D 中的附加代理键,如上

该设计与上面的 C 具有相同的结构。因为上面 D 中的“people”表具有引用“FirstNames”和“Surnames”表的自然键,所以您只需要对“people”表进行两次连接来获取名称。

关于 ORM

ORM 不会像 SQL 开发人员编写 SQL 那样构建 SQL。如果 SQL 开发人员编写的 SELECT 语句需要六个连接来获取名称,那么 ORM 可能会执行七个更简单的查询来获取相同的数据。这可能是个问题;它可能不会。

关于级联

代理 ID 号使每个外键引用都成为隐含的​​、未声明的“ON UPDATE CASCADE”。例如,如果您对 surnames 表运行此更新语句。. .

update surnames
set surname = 'Smythe'
where surname = 'Smith';

那么所有的史密斯都会变成史密斯。防止这种情况的唯一方法是撤销对“姓氏”的更新权限。隐含的、未声明的“ON UPDATE CASCADE”并不总是一件好事。仅仅为了防止不需要的隐式“级联”而撤销权限并不总是一件好事。

于 2014-05-25T05:12:40.953 回答
2

使用自然键可以实现更简单、更快的查询,因为不需要一直连接到外键链以找到“自然”值,例如在屏幕上显示。

于 2014-05-26T10:52:54.500 回答
2

鉴于现代数据库设计通常需要考虑可伸缩性、移动性(断开连接的操作)和冲突解决,在这里我将避免纯粹的学术讨论,并考虑一些务实的考虑,其中密钥的选择可能会产生很大的影响。

影响您选择的因素有:

  • 如何处理可能具有相同自然键的不同记录。例如相同的名字和姓氏。
  • 如果使用服务器分配的代理键(需要某种映射层),Web 或移动客户端如何保存复杂的模型图。替代方法是避免映射问题并使用客户端分配的 v4 UUID。
  • 继上述之后,您如何在临时断开连接的环境(例如移动应用程序)或客户端可以相互对等/共享而无需首先与服务器同步的环境中处理冲突解决。对象标识是支持和解决这些问题的重要概念。
  • 根据密钥的选择,通过对数据库进行分片的可扩展性可能很容易也可能很困难。自动递增代理键很难分片,需要先验选择固定数量的分片,这样键就不会发生冲突,而基于 v4 UUID 的代理键很容易并且可以由客户端分配。复合键和自然键很难,因为键虽然相对稳定,但仍可能发生变化,这需要能够将记录从一个分片迁移到另一个分片。
  • 您的客户如何管理对象身份?用户界面通常需要构建一个本地模型图,以便以后持久化到“云中的服务器”。在持久化之前的这段时间内,这些对象需要身份,而在持久化之后,服务器对象身份和客户端对象身份之间需要达成一致或映射。
  • 您是否强制数据库(包括应用程序服务器)之上的所有内容来处理身份映射问题或将其构建到数据库密钥设计中,并帮助解决数据库的可扩展性/分片问题?

我的建议是查看整个系统的特征,并超越理论数据库设计,看看对于位于数据库之上的非平凡的完整堆栈来说,什么可以很好地工作。您对关键设计所做的选择可以决定系统的可用性,也可以帮助或损害开发复杂性,从而增加或减少最终的上市时间以及质量和可靠性的整体权衡。

于 2014-06-03T06:59:27.853 回答
1

如果我们的复合键由列 STUDENT 和 COURSE 组成,数据库将确保我们永远不会输入重复值。

例子。

使用复合键,这是不允许的,数据库会阻止它。

STUDENT     COURSE
1           CS101
1           CS101

但是如果我们选择一个代理键作为键,我们就需要找到另一种方法来防止这种重复。

考虑哪些字段组合是可能的键可以帮助您更好地发现和理解问题。

于 2015-02-05T22:28:44.900 回答
1

我曾经看过这个主键标准列表。我发现这是进行此类讨论的一个很好的起点

  • 独特的
  • 稳定的(不一定是不可变的)
  • 不可约
  • 简单的
  • 熟悉的

有时两个或多个标准之间存在冲突,我们必须在它们之间做出妥协。不幸的是,许多人甚至从未考虑过如何设计密钥,他们使用某种自动生成的密钥,可能是身份列、guid 或其他什么。

代理键的一个缺点是执行声明性规则变得更加困难(大多数 DBMS 不支持检查约束中的子查询)。我在想这样的规则:

CHECK ( jobtitle <> 'BOSS' OR salary > 100 )

然而,我发现代理键最大的问题是你可以摆脱非常奇怪的结构,甚至没有注意到。

于 2014-05-29T04:00:51.120 回答
1

您可以在数据库中找到的一个常见用例是对历史记录进行版本控制

以用户表为例:

ID   Name        Value       DeletedFlag
1    Smith       78          0
2    Martin      98          0
3    John        78          1
4    Edouard     54          0
5    John        64          0

John 填写了一个信息,然后决定删除它并填写一个新信息。

如果您不使用唯一的 pk,您将无法管理这种情况。

它使开发和生产中非常容易将某些数据标记为已删除,并取消标记以进行一些测试或数据更正,而不是备份或恢复或引起很多混乱。

在整数上重建索引也更快,并且占用更少的磁盘空间。

于 2014-06-03T13:10:34.603 回答
0

我认为您在数据方面误解了一些基本的东西:

1)您采用单个标识符(人名 - 假设确实唯一标识一个人),然后将其拆分为亚原子部分,然后由于 6NF,将它们放入单独的关系变量中。通常出于实际原因进行这种拆分,名字/姓氏是一个常见的例子;与将属性重新组合在一起的情况相比,通常根据拆分的复杂性、频率等因素做出决定。这里的拆分是不切实际的。

2) 6NF 总是可以实现的,但并不总是可取的。在这种情况下,定义一个能够验证这些部分组合是否有效的约束变得更加困难(假设您已经将日期按时间颗粒划分为日、月和年,并将每个部分存储在单独的 relvar 中!)。

3) 对于人员标识符,名字和姓氏的组合很少是足够的。通常根据所需的信任级别选择标识符。雇主检查参考资料、资格等,然后发出工资单参考资料。警方的提议可能要求您在路边查看您的驾驶执照,但如果您被判有罪,则会采集指纹。DBMS 无法验证一个人,因此自动增量整数也很少足够。

于 2016-11-21T13:30:56.497 回答