沉溺于虚无之海

Posted

技术标签:

【中文标题】沉溺于虚无之海【英文标题】:Drowning in a Sea of Nulls 【发布时间】:2011-03-10 14:57:56 【问题描述】:

我继承的一个应用程序跟踪对材料样本执行的实验室测试结果。数据存储在单个表 (tblSampleData) 中,主键为 SampleID,235 列代表潜在的测试结果。问题是每个样本只执行少数测试,因此每行包含超过 200 个空值。实际上,还有第二个类似的表 (tblSampleData2),其中包含另外 215 个主要为空的列和 SampleID 的主键。这两个表具有一对一的关系,并且大多数 SampleID 在两个表中都有一些数据。 然而,对于每个 SampleID,很容易就有 400 个空列!

这是糟糕的数据库设计吗?如果是这样,哪个范式规则被打破了?如何查询此表以确定哪些列组通常与数据一起填充?我的目标是拥有 45 个表,其中包含 10 列和更少的空值。我怎样才能做到这一点?如何避免破坏现有应用程序?

到目前为止,这些表有大约 200,000 条样本记录。用户要求我为更多测试添加更多列,但我宁愿建立一个新表。这是明智的吗?

【问题讨论】:

这个应用程序对数据库运行什么样的查询? 基本 CRUD。插入新的测试记录,在测试完成时更新,将结果读入图表和报告,很少删除。读取查询针对每个客户的小数据集。 在下面查看我的长答案,但了解 400 个左右的结果列的数据类型会很有趣——特别是它们是否都是 same 数据类型。 大约 50% 的浮点数、40% 的整数、一堆 varchar(50) 以及一些日期时间和位字段。 【参考方案1】:

我看到文章/论文表明在数据库中简单地使用 NULL 会破坏第一种正常形式。

根据我从您对数据库的描述中收集到的信息,更好的设计可能如下:

包含始终与样本相关联的字段的样本表。例如,

Sample
------ 
SampleID 
SampleDate 
SampleSource

然后是一个测试类型表,其中每个可以执行的测试类型都有一个条目。

TestType
--------
TestTypeID
TestName
MaximumAllowedValue

最后,有一个中间表,表示上述两个表之间的多对多关系,并保存测试的结果。

TestResult
----------
SampleID
TestTypeID
TestResult

这将消除空值,因为 TestResult 表将只包含对每个样本实际执行的测试的条目。我曾经设计了一个数据库,其目的与我认为您正在做的几乎相同,这就是我采用的方法。

【讨论】:

+1。我还会在 SampleID 和 TestTypeID 上的 TestResult 上设置一个唯一键(如果合适的话;从我认为的问题描述中)。 我喜欢这个答案,但我想确保我理解它。我当前的 450 列会变成 450 TestType 行,其中 TestNames 与原始表列名匹配吗?我喜欢这样,因为我不需要每次需要添加一些新测试时都创建一个新表。这是否有意义:我可以包含一个 TestGroup 表来识别类似测试的类或类别? TestType 表将包含一个 TestGroupID 外键。 TestGroupNames 将代表我以前认为应该是单独的表名。 正确,TestType 表中的 450 列将变为 450 行。然后,对于每个样本,您只需进行实际执行的测试,以便在 TestResult 表中输入条目。随着新测试的添加,这肯定会使数据库更易于维护。是的,您当然可以包含您所描述的 TestGroup 表。这将更容易将测试分组以进行显示,例如在报告上。正如 Carl 在他的评论中提到的,请确保正确设置键和约束以避免重复的测试结果条目。 这个结构如何处理不同的TestResult数据类型?目前测试结果为浮点数、整数和varchars,但上表仅包含一个TestResult 列,没有指定数据类型。 在不知道您正在使用的实际数据的情况下,很难为此提出建议。一种可能性是在TestResult 表中包含三列,一列用于float,一列用于int,一列用于varchar。在 TestType 表中,有一个名为 TestDataType 的字段,其值为 1、2 或 3。然后,在插入 TestResult 表时,根据 TestDataType 的值使用适当的列。这只是一个想法。【参考方案2】:

您可以使用众所周知的Entity Attribute Value model (EAV)。何时适合使用 EAV 的描述非常适合您的用例:

这种数据表示类似于存储稀疏矩阵的节省空间的方法,其中只存储非空值。

生产数据库中 EAV 建模的一个例子是可以应用于患者的临床发现(既往病史、当前主诉、体格检查、实验室测试、特殊调查、诊断)。在所有医学专业中,这些数量可能达到数十万(每个月都在开发新的测试)。然而,大多数去看医生的人的发现相对较少。

在您的具体情况下:

实体是一个材料样本。 属性是测试类型。 该值是特定样本的测试结果。

EAV 有一些严重的缺陷并造成许多困难,因此只能在适当的时候应用它。如果您需要在一行中返回特定样本的所有测试结果,则不应使用它。

在不破坏现有应用程序的情况下,很难修改数据库以使用这种结构。

【讨论】:

+1。如果您有数百列大部分为空,那么您做错了.. 迁移到 EAV 将使用户更容易定义新属性,而无需再修改数据库。【参考方案3】:

我不确定设计是否真的那么糟糕。 NULL 值实际上应该相对便宜的存储。在 SQL Server 中,每一行都有一个(或多个)内部位字段,用于指示哪些列值为 NULL。

如果应用程序的性能不需要改进,并且由于更改表架构而重构的成本效益不是积极的,为什么要更改它?

【讨论】:

他似乎表示他需要不时添加测试,这涉及反复更改表架构以及任何相关的查询或过程。这肯定也是有代价的。 所有答案都为规范化和数据库设计提供了宝贵的见解。最后,我保持表结构不变,并为我的新测试数据添加了列。如果您查看实验室网页中噩梦般的意大利面条代码(不是我的代码!),您就会理解我的决定。要重构应用程序以使用新的表结构,我必须从头开始重写应用程序。这是我的第一个 Stack Overflow 问题,我对快速而深思熟虑的回答感到惊讶。谢谢!【参考方案4】:

仅仅因为没有破坏正常形式的规则并不意味着它不是糟糕的数据库设计。通常,您最好使用更紧凑的行更小的设计,因为这样可以在页面中容纳更多行,因此数据库要做的工作更少。在当前的设计下,数据库服务器不得不投入大量空间来保存空值。

避免破坏现有应用程序是困难的部分,如果其他应用程序只需要读取访问权限,您可以编写一个看起来与旧表相同的视图。

【讨论】:

【参考方案5】:

如果您确实更改了表结构,我建议您使用一个名为 tblSampleData 的视图,该视图返回的数据与现在的表相同。这将保留一些兼容性。

【讨论】:

无论如何重构应用程序可能是明智的,但这将防止应用程序一开始就崩溃。【参考方案6】:

    您可能甚至不需要 RDBMS 来处理这些数据。将数据存储在结构化二进制文件或 DBM/ISAM 表中。

    未标准化。通常,缺乏标准化是所有问题的根源。但是在这种情况下,缺乏规范化并不是世界末日,因为这些数据是“只读的”,只有一个键,与其他任何东西都没有关系。所以更新异常不必担心。您只需要担心原始数据是否一致。

    如果您将 NULL 视为在整个应用程序中具有相同含义的“特殊值”,那么所有这些 NULL 并没有什么太大的问题。没有收集数据。数据不可用。对象拒绝回答问题。数据异常。数据待定。已知数据未知。对象说他们不知道……等等,你明白了。允许没有 defined 原因且没有 defined 含义的 NULL 是非常错误的。

    我说标准化。要么定义特殊值并创建一个巨大的表。或者,为 VB 和 php 程序员保留 NULL,并正确拆分数据。如果您需要支持旧代码,请创建一个 VIEW 以加入备份数据。根据您的描述,您正在谈论几个小时的工作才能使这件事正确无误。这不是一个糟糕的交易。

【讨论】:

【参考方案7】:

假设您有具有 40 个测量通道的测试机 X。如果您知道在每次测试中测试人员只会使用几个通道,您可以将设计更改为:

tblTest: testId, testDate tblResult: testId, machineId, channelId, Result

您总是可以使用交叉表检索以前的布局。

【讨论】:

【参考方案8】:

我会选择 1 个主表,每个样本有 1 行,它将包含每个样本应具有的所有列:

Sample
-------
SampleID  int auto increment PK
SampleComment
SampleDate
SampleOrigin
....

然后我会为每个不同的测试或类似测试的“类”添加一个表,并包括与这些相关的所有列(使用实际的测试名称而不是 XYZ):

TestMethod_XYZ
---------------
SampleID    int FK Sample.SampleID
MeltTemp
BurnTemp
TestPersonID
DateTested
...

TestMethod_ABC
---------------
SampleID    int FK Sample.SampleID
MinImpactForce
TestPersonID
DateTested
....

TestMethod_MNO
---------------
SampleID    int FK Sample.SampleID
ReactionYN
TimeToReact
ReactionType
TestPersonID
DateTested
...

当您搜索结果时,您将搜索适用的测试方法表并连接回实际样品表。

【讨论】:

你阐述了我最初的想法。但是,我希望提出一个聪明的查询来确定测试的类别。也就是说,根据现有数据,每列应该分成哪些可能的表。当然,我可以让实验室里的人帮我对他们的测试进行分类,但这有什么乐趣呢?【参考方案9】:

EAV 是一种选择,但查询会杀死你。

是否可以将数据迁移到像 MongoDB 这样的 NoSQL 数据库?我相信这将是解决您的问题的最有效和最简单的方法。既然您提到您基本上是在进行 CRUD 查询,NoSQL 应该非常有效。

【讨论】:

不太可能迁移。我从来没有听说过 MongoDB。我会用谷歌搜索它和 NoSQL。 这是一个无模式数据库,从您的问题描述来看,它似乎非常适合:mongodb.org。【参考方案10】:

目前的设计很差。通常,具有大量 NULL 值的数据库表明设计不佳,违反了第四范式。但设计的最大问题不是违反正常原则,而是添加新测试类型需要更改数据库结构而不是简单地添加一些数据 到几个“定义”测试的表。更糟糕的是,它需要对现有表进行结构更改,而不是添加新表。

您可以通过采用其他人描述的键值系统来实现完美的第四范式。但是您可以通过执行以下任一操作来显着改进数据库的设计并仍然保持您的理智(在使用没有 ORM 的键值系统时很难做到这一点):

    尝试发现代表任何单个测试所需的最大测量次数。如果测试返回不同的数据类型,您需要发现最大测试返回的每种数据类型的最大值数。创建一个仅包含这些列的表,标记为 Meas1、Meas2 等。您可能需要 10 个或 40 个而不是 400 个列。然后创建一组表,描述每个列对每个测试的“含义”。根据存储的测试类型,此信息可用于提供有意义的提示和报告列标题。这不会完全消除 NULL,但会大大减少它们,只要任何新测试可以“适合”到您指定的测量数量,新测试就可以作为数据添加而不是结构更改。

    发现每个测试的实际测量列表,并创建一个单独的表来保存每个测试的结果(测试 ID、运行它的人、时间等基本信息仍然放在一个表中)。这是一种多表继承模式(不知道有没有实名)。您仍然需要为每个新测试创建一个新的“数据”表,但现在您不会触及其他现有的生产表,您将能够获得完美的范式。

我希望这能提供一些开始的想法。

【讨论】:

我们不使用 ORM。您能否详细说明为什么维护键值系统很困难?如果它更容易维护,我会考虑你的想法,但我不太了解结构。我会尝试搜索多表继承模式,或者也许有人可以建议一个链接或显示一个示例表结构?

以上是关于沉溺于虚无之海的主要内容,如果未能解决你的问题,请参考以下文章

笔之海

Embeded Linux之海思UART

小说的世界观

无尽之海:从手机到万物

Steam领取《黑色沙漠》/Epic领取《无光之海》

未来,未来!--入职2周年小记