数据库规范化 - 谁是正确的?

15

我的教授(自称多年来对系统开发有深刻理解)和我正在就数据库设计进行争论。

举个例子: 我的教授坚持认为这个设计是正确的: (列的列表)

Subject_ID
Description
Units_Lec
Units_Lab
Total_Units

注意总单位列。他说必须包括此列。 我试图解释这是不必要的,因为如果需要,只需通过简单地添加两个查询来实现。

我向他展示了一本书中的例子,但是他坚持认为我们制作系统时不应太过依赖书籍。 同样的情况也适用于此类类似案例:

student_ID
prelim_grade
midterm_grade
prefinal_grade
average
他希望我包括平均值!无论我走到哪里,都会看到一些文章说这是规范化的违规操作。如果我需要平均值,我可以很容易地计算三个成绩。他列举了一些场景,包括(“嘿!如果查询被意外删除怎么办?你会怎么做?这就是为什么你需要将它包含在你的表中!”)
我需要重构我的数据库(大约有40多个表)来满足他的要求吗?我错了吗,只是忽略了这些问题?
另一件事是他想在付款表中包括总金额,我认为这是不必要的。(只需计算产品的单价和数量即可。)他指出我们需要该列来计算对于整个系统管理至关重要的借方和/或贷方,用于平衡交易。请告诉我您的想法。

4
你提供的解决方案更加常规是正确的。但请记住你的教授将评估你的方案。你可以以建设性的方式提出异议,但不应偏离他对此异议的解决方案。 - Andomar
@redskins80在这里是正确的。然而,需要注意的是,如果你想在教授的课堂上得到好成绩,与他争论太多从来都不是一个好主意,无论他有多么错误... - Tim Pote
Andomar 在这方面是正确的。尽一切所能来取得好成绩。但当你参加工作面试时(请看下面答案下面的评论),确保你以正确的方式回答这个问题。 - KP Taylor
谢谢,但那个项目已经超出了我们的范围。我想知道如何赢得这场争论。 - Arman
1
我们举一个简单的例子来计算3个等级。但是,如果它是一个需要每次查询运行2-3秒钟的更大的计算呢?而且你每天有成千上万的用户?我不是说它应该存储在那个表中,但ETL进程定期预先计算数据以供UI访问,这样它就不需要运行那个“快速”(或者你所说的“快速”)查询了。 - L_7337
7个回答

13

您说得没错!规范化的一项规则是要减少那些可以通过使用其他属性值轻松推导出来的属性。即,通过进行一些数学计算。在您的情况下,只需加起来就可以得到总单位列。

告诉您的教授,拥有该特定列将显示传递依赖关系的明显迹象,并根据第三范式建议减少这些依赖关系。


1
同意@redskins80的观点;有些情况下,数据去规范化是有益的,但仅在需要经常使用且每次从其来源计算非常昂贵的情况下进行优化。 - lanzz
3
在这种情况下唯一可能的例外情况——我有点牵强地 试图 给这位教授 一些 怀疑之余的好处——是如果根据系统中的业务规则,Total_Units 不必总是等于另外两列的总和... 比如说,如果 Total_Units 可以包括由不理解数据库规范化的教授自行授予的奖励单位。 :-) - KP Taylor
@redskins80 确实...就像我说的,我试图去解释教授的观点,但这有点难以置信。 - KP Taylor
2
@Arman,你也可以告诉你的教授,像我这样经营电子商务团队的人是绝不会雇佣任何按照他建议写数据库表的人的。那样做可能会让你在第一轮面试中被淘汰。 - KP Taylor
1
@KPTaylor:你从不对模式进行反规范化处理吗? - Quassnoi
显示剩余3条评论

13

当你说你的解决方案更符合规范时,你是正确的。

然而,有一件事叫做去规范化(google一下),它是有意违反规范化规则以提高查询性能的。

例如,您想按总单位数递减的顺序检索前五个主题(无论该物品是什么)。

您的解决方案需要在两个表(subjectunit)上进行全扫描,连接结果集并对输出进行排序。

而您的教授的解决方案只需要从一个基于 total_units 的索引中获取前五条记录。

当然,这是以增加维护成本为代价的(包括计算资源和开发方面)。

我无法告诉您谁是“正确”的:我们对项目本身、数据量、要进行的查询等一无所知。这是需要针对每个项目做出的决定(对于某些项目,这可能是核心决策)。

问题在于,教授确实有一个可以或不可以的理由。

他为什么不亲自向您解释以上所有内容,是另一个问题。


在开始项目之前,我知道关于反规范化的事情,但认为它不会对系统有益。我的教授解释是:1. 视图可能会被意外删除。2. 借方/贷方必须相应列出。(与仅查询过去交易相反) - Arman
@Arman:你希望我们给出什么答案?你的解决方案更规范 - 是的。你的解决方案更有效 - 我们不知道,也无法从提供的有限数据中得出结论。谁是对的 - “mu”。 - Quassnoi
2
很棒的回答。如果规范化是一切,那么所有数据库都将处于第五范式,你几乎无法在不编写具有多个连接的巨大SQL查询的情况下找出程序中发生的问题。我曾经处理过过度规范化的系统,这真的很麻烦。规范化和易用性之间有一个良好的平衡点。 - L_7337

6
除了redskins80的很好的回答,我想指出为什么这是一个坏主意:每当您需要更新其中一个源列时,您还需要更新计算列。这是更多的工作,可能会很容易地包含错误(也许在一年后,当不同的程序员正在更改系统时)。
也许您可以使用计算列代替?那将是一个可行的中间地带。
编辑:去规范化有其位置,但这是最后的措施。这就像化疗:医生只注射毒药来治疗对您健康构成更大威胁的疾病。这是最后可能采取的步骤。

...或者在包含总计列的表上创建一个视图。 - KP Taylor
1
我无法强调usr是多么正确!在进行数据库更新时,每个需要保持同步的信息位都可能成为潜在的错误等待发生。 - Gnosophilon
1
更糟糕的是:每次更改一行,就必须重新计算所有行的平均值。 - wildplasser
@usr,Redskins80的回答在哪里?它被删除了吗? - Shreyans jain
@Shreyansjain 不确定,我没有看到被删除的答案。它可能已经被 Stack Overflow 的工作人员永久删除了。或者,redskins80 把自己改名为 @arijeet 了?我给他的回答点了赞。 - usr

6

我认为这个问题的答案并不完整,因此很重要加入我的观点。原始问题得到了很好的回答,但这里存在一个故障。所以我只考虑下面引用的新增问题:

另一件事是他想在付款表中包括总金额,我认为这是不必要的(只需计算产品的单价和数量)。他指出我们需要该列来计算关键的借方和/或贷方,这对于整个系统管理至关重要,需要用于平衡交易。请告诉我你的想法。

这次编辑很有趣。基于这是一个处理货币的交易系统,它必须是可核算的。我使用了一些基本术语:交易、产品、价格、金额。

在这种情况下,非常普遍甚至是必要的去反范式化。为什么?因为你需要它是可核算的。所以当交易被注册时,就是这样,它可能永远不会被修改。如果你需要进行更正,那么你就进行另一笔交易。

现在你可以计算例如产品价格*数量*税等。这在规范化方面是有意义的。但是然后你将需要完全锁定所有相关记录。所以以产品表为例:如果你在交易发生之前更改价格,那么当交易发生时应该考虑到它。但是如果价格在交易之后更改,则不会影响交易。

因此,仅仅加入transaction.product_id=products.id是不可接受的,因为该产品可能会更改。例如:

2012-01-01 price = 10
2012-01-05 price = 20
Transaction happens here, we sell 10 items so 10 * 20 = 200
2012-01-06 price = 22

现在我们要查询2012年1月10日的交易记录,所以我们需要执行以下操作:

SELECT 
    transactions.amount * products.price AS totalAmount 
FROM transactions 
INNER JOIN products on products.id=transactions.product_id

这样会得出10 * 22 = 220,因此是不正确的。

所以你有两个选择:

  1. 不允许在产品表上进行更新。因此,你需要将该表版本化,对于每条记录,都要添加一个新的INSERT而不是更新。这样事务就可以指向产品的正确版本。

  2. 或者你可以将这些字段添加到事务表中。因此,在插入并保存事务时(在数据库事务中),添加totalAmount到事务表并计算它。

是的,这是非规范化的,但它有一个很好的理由,它使其可以追溯。你只需知道,并且通过交易、锁等进行验证,当发生交易时,它与价格=20等所描述的产品相关联。

除此之外,非规范化仍然有好处,例如很容易运行报告。月份、年份等的总交易金额都很容易计算。

规范化有好处,例如没有重复存储、单一编辑点等。但在这种情况下,你不希望使用该概念,因为这不允许并且不适用于交易日志数据库。

将交易视为现实世界中发生的事情的注册。它发生了,你把它写下来。现在你不能改变历史,它就像当时一样被写下来了。未来不会改变它,它已经发生了。


1
你谈论的是历史和财务数据。通常会存储一些计算结果,因为那是当时收取的成本,永远不会改变。如果你从产品*价格进行计算,而价格在交易后6个月发生了变化,那么你就会得到错误的价值。你的教授很聪明,请听他的话。此外,如果你从数据库中进行大量报告,你不希望经常计算那些不能更改的值而没有其他数据输入记录。为什么要在应用程序的历史上多次执行计算,而你只需要执行一次呢?这样会浪费宝贵的服务器资源。

你的教授很聪明?也许他在这个特定问题上碰巧是对的。虽然当然,如果我没记错的话,你可以通过一个历史价格/产品表来解决问题,并基于此计算付款总额,但通常这样做会带来很多麻烦,所以只需存储实际付款金额即可。 - reiniero
关于您对报告的评论:您说得对,报告数据库通常是非规范化的,而事务性数据库则是规范化的。OP没有提到它们设计的哪一个。没有更多信息,我会假设是事务性的... - reiniero
@reiniero,即使是事务性数据库也会有基于它们的报告。我所说的是那些不经常改变的东西,将其强制执行在数据库中对性能来说是更好的选择,尤其是如果计算它意味着你会得到错误的答案,因为当前价格不是收取的价格。时间数据是数据库设计中的特殊情况。在时间数据上进行这种计算以记录交易时的收费情况并不是非正规化的行为。 - HLGEM
如果您想保留记录的历史记录,应该创建一个表来实现。规范化的数据库不应该有这些计算值。 - AndreDuarte
@Andre,请了解一下时间数据以及如何正确处理它。此外,没有必要使用规范化作为避免重复做某事的借口。只需要计算一次的计算应该只做一次,特别是如果经常引用它。每次打开屏幕或运行报告时都这样做是愚蠢的,特别是如果你有包含数千条记录的报告需要计算。我并不是说你应该总是这样做,而是有一些有效的情况需要这样做。 - HLGEM
@HLGEM 我明白了。每种情况都需要得到适当的评估。但我认为这个解决方案应该在最后一种情况下使用。 - AndreDuarte

1

如果你想要实现好的、老式的、经典的关系模型,我认为你所做的是正确的。

一般来说,这实际上是一种哲学问题。有些系统,比如Oracle,甚至允许你放弃传统的关系模型,转而使用对象(通过在表中保留复杂结构),这违反了第一范式,但给你提供了面向对象模型的强大功能(你可以使用继承、重写方法等),在某些情况下非常棒。所使用的语言仍然是SQL,只是进行了扩展。

我知道我的回答偏离了主题(因为我们考虑了一个全新的数据库类型),但我认为这是一个有趣的事情,在这个普遍的问题的场合分享一下。

实际应用的数据库设计很难只涉及到制作哪些表。目前,当涉及到数据的存储和处理时,有无数的可能性。有我们都熟知和喜爱的关系系统,也有对象数据库(如db4o)、对象关系数据库(不要与对象关系映射混淆,我指的是像Oracle 11g这样带有对象的工具)、XML数据库(如eXist)、流数据库(如Esper)以及当前蓬勃发展的noSQL数据库(有些人坚持认为它们不应该被称为数据库),如MongoDB、Cassandra、CouchDB或Oracle NoSQL。

在某些情况下,规范化失去了意义。每个模型都有完全不同的目的。我认为“数据库”这个术语的含义比以前广泛得多。
当涉及到关系数据库时,我同意你的观点而不是教授的观点(尽管我不确定强烈反对他是否明智)。
现在,重点来了。我认为你可以通过展示自己思想开放并且理解需要考虑很多选择(包括他的观点),但是情况需要你规范化数据来赢得他的支持。
我知道我的回答对于一个stackoverflow帖子来说有点罗嗦,但希望不要被看作是疯狂的胡言乱语。
祝你在关系博弈中好运。

0
规范化的目的是为了消除冗余,从而消除事务性系统中的更新异常。关系型数据库仍然是处理事务、数据仓库、主数据和许多商业智能解决方案的最佳选择。大多数NOSQL具有较低的完整性要求。因此,你失去了我的推文-令人恼火但不至于灾难性。但是失去我价值百万美元的股票交易就是一个大问题。选择不是NOSQL vs.关系型数据库。NOSQL在某些方面表现非常出色。但是关系型数据库并没有消失。它仍然是处理事务、更新导向解决方案的最佳选择。当数据是只读或者读取频率很高时,可以放松规范化的要求。这就是为什么在数据仓库中冗余不是一个如此巨大的问题;因为没有更新。

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