我应该如何规范这个数据库设计?

How should I normalize this database design?

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

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

问题在于此设计允许结果用于类型 A 的测试,但该结果位于类型 B 的批次中。

我不能做的一件事是将测试和批处理 table 合并为一个 table。每周都有一个新的批次,而测试会持续数月或数年。并且一个 Batch 可以包含许多 Test(尽管总是相同的 Type),并且一个 Test 通常在许多 Batch 中进行多次。

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

有没有一种干净的方法 re-organise 这样我们就没有循环连接路径了?这是必要的吗?还是理想的?

还是我应该随心所欲,不再担心它? :-)

[编辑 1] 请注意,测试详细说明了它如何 运行s,谁修复了发现的问题等,这些细节在多个批次中保持不变,因此测试必须独立于它可能存在的任何批次而存在(或可能不会)运行 in.

[编辑 2] 已经指出最好有一个 TestBatch table,它给我们这样的结构:

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

[Edit 3] 感谢@philip-kelley 的极好的建议,我相信我们已经有了答案。首先,我们linkTestBatch直接返回Type,因此:

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

但第二步是将外键从 TestBatch 更改为 Test,以便它包括 Type 以及 TestID。并将外键更改为 Batch 以包括 Type 和 BatchID。

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

创建一个 TestBatch table,其中包含与特定批次关联的测试。使用 table 的 PK 作为结果 table.

中的 FK

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

TestBatch 加入测试和批处理,包含BatchID、TestID 和它自己的ID。然后 Results table 包含来自 TestBatch 的 ID 作为其外键。

因此,要查看针对此的结果,您可以将结果加入 TestBatch,然后从测试和批处理 table 中获取描述性详细信息,如下所示:

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 但不能同时加入两者。

向您展示它如何处理数据(暂时忘记结果 table 以及您可以在@PhillipKelleys 优秀答案中看到的官方 FK 和 PK)代码是为 SQL 服务器,我使用了 temp tables,因此您可以在提交结构之前尝试一下,但如果您想创建真正的 tables,请删除 # 符号。 Identity 是 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

@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>