【问题标题】:How should I normalize this database design?我应该如何规范这个数据库设计?
【发布时间】:2017-10-04 01:33:32
【问题描述】:

我正在尝试设计一个数据库。在我看来,我的设计似乎已标准化为 Fourth Normal Form - 但我仍然认为它已损坏,我终生无法解决如何修复它。

背景:我们有四种类型的测试,每种类型都有几十种测试。我们分批运行测试,每批只包含一种类型的测试。所以一个测试结果属于一个批次,也属于一个测试。这给出了一个类似这样的数据库计划:

问题是这种设计允许一个用于 A 类测试的结果,但该结果在 B 类的批次中。

我不能做的一件事是将 Test 和 Batch 表合并到一个表中。每周都有一个新批次,而测试持续数月或数年。并且一个 Batch 可以包含许多 Tests(尽管始终属于同一 Type),并且一个 Test 通常在许多 Batch 中执行多次。

我可以在 Test 和 Batch 之间插入多对多连接,但我无法立即看出这有什么帮助。

有没有一种干净的方法来重新组织它,以便我们没有循环连接路径?这是必要的吗?还是可取的?

或者我应该只用我所拥有的,不再担心它吗? :-)

[编辑 1] 请注意,测试包含有关其运行方式、修复发现的问题等的详细信息,这些详细信息在多个批次中保持不变,因此测试必须独立于它可能(或可能不)运行的任何批次存在.

[编辑 2] 有人指出最好有一个 TestBatch 表,它为我们提供了这样的结构:

我同意这是个好主意,但实际上并不能解决问题。它只是将问题从 Result 转移到 TestBatch。我们现在可以有一个用于 A 类测试的 TestBatch,但 TestBatch 在 B 类的 Batch 中。

[编辑 3] 感谢@philip-kelley 的出色建议,我相信我们已经有了答案。首先,我们将 TestBatch 直接链接回 Type,因此:

这并不能立即解决问题。事实上,它使情况变得更糟——现在可能有一个用于测试的类型,一个用于批处理的不同类型,以及直接从 TestBatch 加入的第三种类型。

但第二步是将外键从 TestBatch 更改为 Test,使其包含 Type 以及 TestID。并将外键更改为 Batch 以包含 Type 以及 BatchID。

这样,我们可以确定 TestBatch 与 Test 和 Batch 具有相同的 Type。

【问题讨论】:

  • 等等,我错过了一些东西。如果each batch contains tests of only one typea test result belongs to a batch,那么Result that is for a Test of Type A 怎么可能是in a Batch for Type B
  • 在现实世界中 - 它不能。每个结果都是针对特定测试和特定批次的,并且该测试和该批次必须属于同一类型。在我的数据库设计中 - 很容易得到 A 类测试的结果,但结果是 B 类批次。这就是我认为我的设计被破坏的原因。

标签: database-design relational-database database-normalization


【解决方案1】:

@HLGEM 的回答描述了逻辑模型,以及一些物理模型细节。支持并强制执行您的业务规则的物理实现看起来像这样。 (这是伪代码,仅显示关键列——您需要为名称、分数等属性添加列。实际的实现细节取决于系统,可能会有点棘手,但任何 RDBMS 都应该能够来支持这一点。请注意,列出的所有列都不能为空。)

CREATE TABLE TestType
  TestType    int
  <primary key on TestType>


CREATE TABLE Test
  TestId       int
  TestType     int
  <primary key on TestId>
  <foreign key into TestType on column TestType>


CREATE TABLE Batch
  BatchId      int
  TestType     int
  <primary key on BatchId>
  <foreign key into TestType on column TestType>


CREATE TABLE TestInBatch
  TestInBatchId  int
  TestId         int
  BatchId        int
  TestType       int
  <primary key on TestInBatchId>
  <unique constraint on TestId, BatchId>
  <foreign key on (TestId, TestType) into Test, columns (TestId, TestType)>
  <foreign key on (BatchId, TestType) into Batch, columns (BatchId, TestType)>


CREATE TABLE Result
  ResultId       int
  TestInBatchId  int
  <primary key on ResultId>
  <foreign key into TestInBatch on column TestInBatchId>

【讨论】:

  • 当然!您在 TestInBatch 中添加了一个 TestType 字段,并将其用作 Test 和 Batch 外键中的第二个字段。这意味着对于给定的 TestInBatch,TestType 始终是固定的,并且它始终与 Test 和 Batch 匹配。完美的! :-)
  • 如果我们要在 TestInBatch 表中有一个 TestType 字段,用于 Test 和 Batch 的外键,那么我可能会将该字段用作返回的直接外键链接TestType 表也是如此。我用的是Laravel,所以能直接链接会很方便。
  • 使用 TestType 与 Test 和 Batch 之间的外键,以及从它们到 TestInBatch 的外键,TestInBatch 和 TestType 之间的外键不是绝对必要的......但是当它有助于实现或表现,继续。
  • @BrendanWhite 一般的话题是database/sql下的“subtyping”,一个TestType值是一个类型“tag”或者“discriminator”。
【解决方案2】:

创建一个包含与特定批次关联的测试的 TestBatch 表。使用该表的 PK 作为结果表中的 FK。

无论如何,您都需要 TestBatch,因为与特定批次关联的测试是您需要捕捉的历史时刻。每次构建新批次时,可能会添加新测试,但您不希望它们与较早的已完成批次相关联。

TestBatch 连接到测试和批处理,并包含 BatchID、TestID 和它自己的 ID。然后 Results 表包含来自 TestBatch 的 ID 作为其外键。

因此,要查看与此相反的结果,您可以将结果连接到 TestBatch,然后从 Test 和 Batch 表中获取描述性详细信息,如下所示:

Select r.ResultId, R.Col1, r.col2, b.BatchId, b.batchdate, t.testId, t.Test_description
From Results r
join TestBatch tb on r.TestBatchid = tb.TestBatchid
join Batch b on tb.batchid = b.batchid
join Test t on tb.testid = t.testid

Type 可能主要用于在创建批次时为 TestBatch 创建记录。 并加入上述按类型过滤在这种情况下,您通常只想加入 Type to Batch 或 Test 但不能同时加入两者。

为了向您展示这如何处理数据(暂时忘记结果表以及您可以在@PhillipKelleys 优秀答案中看到的官方 FK 和 PK)代码是为 SQL 服务器编写的,我使用临时表所以你在提交结构之前可以玩一下,但如果你想创建真正的表格,请删除# 符号。身份是 SQl 服务器用来创建自动生成的字段的东西,用代码代替您的数据库后端来做类似的事情。:

Create table #type (Typeid int identity, TypeDescription varchar(100))

Insert into #type (TypeDescription)
values ('Geography'),  ('History'),  ('Biology'),  ('Math')

Create table #Batch (BatchID int identity, TypeID int, BatchDate datetime)

insert into #Batch (TypeID, BatchDate)
values (1, getdate()-1), (1, getdate() +2) , (4, getdate())

Create table  #Test (testId int identity, TestDescription varchar(50), TypeId int)
Insert into #Test (TestDescription, TypeId )
values ('fall midterm', 1), ('fall final', 1),  ('fall midterm', 3), ('fall final', 3), ('fall final', 2), ('fall midterm', 4), ('fall final', 4)

Create  table #TESTBATCH (TestBatchID int identity, TestID int, BATCHID int )

Insert into #testBatch ( BATCHID, TestID)
values(1, 1), (1, 2), (2,1), (2,2), (3,6), (3, 7)

select * from #type
select * from #Batch
select * from #test
select * from #testBatch

这将显示所有当前批次的详细信息

select B.batchdate, t.TypeDescription, te.TestDescription, t2.TypeDescription
from #testBatch tb
join #batch b on b.batchid = tb.batchid
join #type t on t.typeid = b.typeid
join #test te on te.testid = tb.testid
join #type t2 on t2.typeid = te.typeid

这将显示所有当前测试,即使是没有当前批次的测试

select  te.TestDescription, t2.TypeDescription, B.batchdate, t.TypeDescription
from #test te 
join #type t2 on t2.typeid = te.typeid
left join #testBAtch tb on te.testID = tb.testId
left join #batch b on b.batchid = tb.batchid
left join #type t on t.typeid = b.typeid

【讨论】:

  • 好点 - 我可以看到 TestBatch 表是多么值得拥有。但是,它不会解决原来的问题。这只是意味着我会在 TestBatch 而不是 Result 上遇到问题。例如 - 你建议我通常只想加入 Type to Batch 或 Test 但不能同时加入。我的问题是 - 如果我从 TestBatch 加入到 Batch 以键入一个函数,但我从 TestBatch 加入到 Test 以键入另一个函数怎么办?如果批次与测试有不同的类型怎么办?对于同一个 TestBatch,我会有两种不同的类型,具体取决于我所在的函数。
  • 类型是一个查找表。当您想要查找指定类型的文本值时,您只想在连接中使用它。如果您在同一个查询中同时需要测试和批处理,您可以加入两次,但通过测试批处理保持真实关系。一般来说,由于您说批处理一次只能用于一种类型,因此您希望仅通过批处理加入 Type,除非批处理根本不是查询的一部分。
猜你喜欢
  • 2010-11-18
  • 1970-01-01
  • 1970-01-01
  • 2020-05-08
  • 2018-07-08
  • 2013-07-26
  • 2011-02-06
  • 2011-10-07
相关资源
最近更新 更多