为什么数据库的关系模型很重要?


61

我正在处理一个项目,在那里我将不得不与老板一起实现数据库。我们是一家很小的初创公司,因此工作环境非常个人化。

他以前曾给我提供过公司数据库之一,它完全违背了我在学校为RDBMS所教(和读到的)的知识。例如,这里有整个数据库由一个表组成(每个独立数据库)。这些表之一是20+列长,对于上下文,这是一个表中的一些列名:

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

关键是,在他应该拥有保存实体数据(名称,大小,购买日期等)的单个表的情况下,他将所有数据都推入了每个数据库的一个大表中。

我想改进此设计,但是我不确定为什么正确归一化和分段的数据模型实际上可以改进此产品。虽然我熟悉大学的数据库设计并且知道如何进行设计,但是我不确定为什么它实际上可以改善数据库。

为什么好的关系模式可以改善数据库?


33
一句话:规范化。
罗伯特·哈维

9
亲密的选民-为自己辩护!:-)
罗比·迪

6
对于新员工,通常批评既定程序却不了解其背后的原因,即使这些原因在技术上并不合理。首先找出您的老板为何采用这种方式。他/她可能非常清楚这不是一个好的设计,但是没有足够的知识(或更可能是时间)来做得更好。如果您尊重当前设计的原因,那么您提出的任何更改都可能会得到更积极的回应。
Pedro

5
He [the boss] had given me one of his databases before and it completely went against what I was taught (and read about) in school for RDBMS<-欢迎来到现实世界!
Möoz

5
我想起了我最喜欢的关系数据库报价:“规范化直到痛苦,反规范化直到工作”
Jake

Answers:


70

性能参数通常是最直观的参数。您尤其要指出如何在不正确的规范化数据库中添加良好的索引是困难的(注意:在某些极端情况下,非规范化实际上可以提高性能,但是当您都不熟悉关系数据库时,您可能会很不容易查看这些案例)。

另一个是存储大小参数。具有大量冗余的非规范化表将需要更多的存储空间。这也涉及性能方面:拥有的数据越多,查询的速度就越慢。

还有一个论点更难理解,但实际上更重要,因为您不能通过向它扔更多硬件来解决它。那就是数据一致性问题。正确规范化的数据库将自行确保具有特定ID的产品始终具有相同的名称。但是在非规范化的数据库中可能会出现这种不一致的情况,因此在避免不一致时需要格外小心,这将花费编程时间来纠正错误,并且仍然会产生错误,使您的客户满意。


19
数据仓库化是非规范化的一个主要优势,特别是如果您保证有大量数据永不更改,并且您想以节省存储空间的方式更快速,更有效地查询数据。很好的答案,这对于任何不确定为什么不希望使用3NF之外的SQL新手来说都是一个仅供参考。


11
我不确定为什么一致性参数“很难理解”。在我看来,这要简单得多:如果值更改,则必须更新该值的所有副本。与更新数百个或数千个相同数据的副本相比,更新单个副本更不容易出错。这同样适用数据之间的关系。(如果我以两种方式存储关系,则必须更新关系的两个副本。)这在非规范化DB中是一个非常普遍的问题。这是非常困难的,以防止这种腐败现象在实践中(一个例外是物化视图类型使用)。
jpmc26 2016年

4
最后一段应以粗体突出显示。:-)如果没有规范化,就不可能保证数据的完整性。由于每个未规范化的数据库最终都会出现某种数据异常,因此仅在业务逻辑层控制输入是愚蠢的事情。
DanK 2016年

2
@IsmaelMiguel通常的做法是永远不会从数据库中硬删除这样的主数据。您只能通过在其上设置不再可用的标志来对其进行软删除。在这种特殊情况下,最好在产品和订单之间建立外键关系,这意味着当您尝试删除任何订单引用的产品时,数据库将引发错误。
菲利普

24

我将不得不与老板一起实现数据库...

使用专用的数据库管理软件可能会容易得多(对不起;无法抗拒)。

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

如果此数据库仅关心“记录”在哪里,何时何地销售了哪个产品,那么您可能可以将“ OK数据库”的定义扩展到足以覆盖它的范围。如果此数据还用于其他任何用途,那么它的确非常差。

但是...

使用此数据的应用程序/查询是否响应不良/缓慢?如果没有,那么就没有真正要解决的问题。当然,它看起来和感觉都很难看,但是如果它起作用了,那么您就不会因为暗示它“可以”变得更好而获得任何“要点”。

如果您发现确定的症状(即问题)看起来像是由不良的数据建模引起的,那么请设计更好的解决方案。复制这些“数据库”之一,对数据进行规范化,看看您的解决方案是否运行得更好。如果它是相当好(和我完全相信,任何对这个数据更新操作将大量改进),然后回到你的老板,并告诉他们的改善。

用..很好的视图来重新创建他的数据“单表视图”是完全可能的。


11
对单表weltanschauung的抵制通常来自那些不了解联接的SQL缺乏经验的人-尤其是在缺少数据(即外部联接)方面。
罗比·迪

6
@RobbieDee更常见的是来自那些看到非规范化数据因变得不一致而被破坏的人。我就是这样一个人。在Phill建议的情况下,我只会考虑这种结构:这是一种日志记录/报告表,其中的数据将永远不会更新或仅通过擦除干净并完全从其他来源完全导出来进行更新。
jpmc26 2016年

2
即使应用程序对这样的数据库执行令人满意的操作,它仍然不像正常规范化的数据库那样灵活。如果商店名称或公司名称更改,则必须在各处进行更新,而不仅仅是在商店或公司表中。在某些情况下,这可能实际上是您想要的(例如,如果数据主要是出于存档目的而收集的),但是我们需要了解有关特定应用程序的更多信息。
扎克·利普顿

1
@Zach:同意,这就是为什么销售日志可能是可接受的情况。假设您希望每个销售都与进行销售时的商店名称相关联,而不是与 “商店的当前名称”相关联,然后尝试“规范化”,这会带来相当大的复杂性(因为记录商店名称的表格将需要随着时间的流逝而成为一系列序列,而不仅仅是每个商店ID都有一个值)
Steve Jessop

可能的经验法则是,如果提议的规范化引入的唯一复杂性是现在需要在其中加入一些查询来拾取他们需要报告的所有列,那么您就不应该进行改变:- )
史蒂夫·杰索普

14

为什么好的关系模式可以改善数据库?

答案是:它并不总是可以改善数据库。您应该意识到,您可能被教过的东西叫做“ 第三范式”

其他形式在某些情况下有效,这是回答问题的关键。您的示例看起来像First Normal Form,如果可以帮助您更好地了解其当前状态。

3NF规则在“改善”数据库的数据之间建立关系:

  1. 防止无效数据进入您的系统(如果关系是1对1的关系,尽管上面写有代码,但仍会导致错误)。如果您的数据在数据库中是一致的,则不太可能导致数据库外部的不一致。

  2. 它提供了一种验证代码的方式(例如,多对一关系是限制对象的属性/行为的信号)。在编写使用数据库的代码时,有时程序员会注意到数据结构,以指示其代码应如何工作。或者,如果数据库与他们的代码不匹配,他们可以提供有用的反馈。(不幸的是,这更像是一厢情愿的想法。)

  3. 提供可以显着帮助您减少构建数据库时出错的规则,这样就不会基于数据库生命周期中随时可能出现的任意要求来构建规则。相反,您正在系统地评估信息以实现特定目标。

  4. 适当的数据库结构可以通过以下方式连接数据,从而最大程度地减少数据存储量,减少存储检索数据的调用,最大化内存中资源和/或最小化针对特定数据集的数据排序/操作(与查询相比)针对它执行。但是“适当的”结构取决于数据量,数据的性质,查询的类型,系统资源等。通过规范化,可能会使性能变差(即,如果将所有数据加载为1个表,则联接可能会减慢速度)一个问题)。事务处理(OLTP)与业务智能(数据仓库)有很大的不同。

在一家拥有小型数据集的小型公司中,您可能会发现现在的方式没有任何问题。除非您长大,否则以后“修复”会很痛苦,因为随着表的增加,使用该表的系统可能会变慢。

通常,随着公司的成长,您会希望强调快速交易。但是,如果您现在花时间在这个项目上,而不是花在公司可能更需要的其他事情上,那么您可能永远不会遇到这个问题,因为您的公司永远不会真正成长。那就是“优化前的挑战”-现在在这里度过您的宝贵时间。

祝好运!


4
没有提到,但我认为对程序员来说重要的一点是,编辑一个“事物”只需要编辑一行,而不必循环整个数据库来查找和替换该事物。
slebetman '16

@slebetman绝对不要使用代码侧循环来更新单个表中的多行,无论它是否已标准化。使用WHERE子句。当然,这些仍然可能出错,但是在正常情况下这种可能性较小,因为您只需要通过主键匹配一行即可。
jpmc26 2016年

@ jpmc26:通过循环数据库,我的意思是构造一个查询来更新所有受影响的行。有时一个WHERE就足够了。但是我见过一些邪恶的结构,这些结构要求在同一表中进行子选择,以获取所有受影响的行,而不影响不应更改的行。我什至看到过单个查询无法完成工作的结构(需要更改的实体根据行位于不同的列中)
slebetman

这个问题的答案很多,这也不例外。
Mike Chamberlain

11

使用一个大的“神表”不好的原因有多种。我将尝试说明组成的示例数据库的问题。假设您正在尝试对体育赛事进行建模。我们会说您想对游戏以及这些游戏中的团队进行建模。具有多个表的设计可能看起来像这样(这在目的上非常简单,因此不要陷入可以应用更多规范化的地方):

Teams
Id | Name | HomeCity

Games
Id | StartsAt | HomeTeamId | AwayTeamId | Location

一个表数据库看起来像这样

TeamsAndGames
Id | TeamName | TeamHomeCity | GameStartsAt | GameHomeTeamId | GameAwayTeamId | Location

首先,让我们看一下在这些表上建立索引。如果我需要一个团队在本国的索引,则可以很容易地将其添加到Teams表或TeamsAndGames表中。请记住,每次创建索引时,都需要将其存储在磁盘上的某个位置,并在将行添加到表中时进行更新。就Teams表格而言,这非常简单。我成立了一个新团队,数据库更新了索引。但是TeamsAndGames呢?好吧,从Teams例。我添加了一个团队,索引得到了更新。但是当我添加游戏时也会发生!即使该字段对于一个游戏将为空,但仍必须为该游戏更新索引并将其存储在磁盘上。对于一个索引,这听起来还不错。但是,当您需要为填入该表的多个实体创建多个索引时,您会浪费大量空间来存储索引,并且需要大量的处理器时间来更新不适用的内容。

第二,数据一致性。在使用两个单独的表的情况下,我可以使用从Games表到Teams表的外键来定义游戏中有哪些球队在比赛。假设我使HomeTeamIdand AwayTeamId列不为空,那么数据库将确保我投入的每个游戏都有2个团队,并且这些团队都存在于我的数据库中。但是单表方案呢?好吧,由于此表中有多个实体,因此这些列应为可为空(您可以使它们不可为空,并在其中填充垃圾数据,但这只是一个可怕的想法)。如果这些列为空,则数据库不能再保证插入游戏时有两个团队。

但是,如果您决定还是坚持下去,该怎么办?您设置外键,使这些字段指向同一表中的另一个实体。但是现在数据库将只确保这些实体存在于表中,而不是它们是正确的类型。您可以轻松地将其设置GameHomeTeamId为其他游戏的ID,而数据库完全不会抱怨。如果您在多表方案中尝试过,数据库将抛出异常。

您可以尝试通过说“很好,我们将确保我们永远不会在代码中这样做”来减轻这些问题。如果您对第一次编写无错误代码的能力充满信心,并且可以考虑用户可能尝试的各种奇怪组合,那么请继续吧。我个人对执行上述任何一项操作的能力并不自信,因此我将让数据库为我提供额外的安全网。

(如果您的设计是在行之间复制所有相关数据而不是使用外键的情况,这会变得更糟。任何拼写/其他数据不一致都将难以解决。如何确定“ Jon”是否拼写为“ John” ”还是出于故意(因为他们是两个不同的人)?)

第三,几乎每一列都必须是可为空的,或者必须用复制的数据或垃圾数据填充。游戏不需要TeamNameTeamHomeCity。因此,每个游戏都需要在其中放置某种占位符,或者它必须为可空值。如果它可以为null,则数据库将愉快地使用no进行游戏TeamName。即使您的业务逻辑说永远都不会发生,这也将需要一个没有名字的团队。

还有很多其他原因使您想要单独的表(包括保持开发人员的理智)。甚至有一些原因使较大的表可能更好(去规范化有时可以提高性能)。那些情况很少而且相去甚远(通常,当您具有性能指标来表明这确实是问题,而不是缺少索引或其他问题时,通常会得到最佳处理)。

最后,开发一些易于维护的东西。仅仅因为它“有效”并不意味着就可以了。试图维护神桌(如神课)是一场噩梦。您只是稍后要忍受痛苦。


1
“团队:ID |名称| HomeCity”。只要确保您的数据模式不会使您的应用程序错误地声称“超级碗XXXIV”是由洛杉矶公羊队赢得的。SB XXXIV 应该出现在查询当前由LA Rams队赢得的所有冠军中。“神表”有好有坏,您肯定提出了一个不好的表。更好的选择是“游戏ID |主队名|主队城市|客队名称|客队城市|游戏开始于|等...”。这是对信息建模的首次尝试,例如“ New Orleans Saints @ Chicago Bears 1p Eastern”。
史蒂夫·杰索普

6

每日一句名言:“ 理论与实践应该相同……在理论上

归一化表

包含冗余数据的独特的万事俱备表具有一个优点:由于不需要进行任何联接,因此使行代码的报告非常易于代码编写和快速执行。但这代价很高:

  • 它包含关系的冗余副本(例如IngCompanyIDvrCompanyName)。与规范化模式相比,更新主数据可能需要更新更多行。
  • 它混合了所有内容。您无法确保在数据库级别上实现轻松的访问控制,例如,确保用户A只能更新公司信息,而用户B只能更新产品信息。
  • 您不能在数据库级别确保一致性规则(例如,用于强制要求公司ID仅存在一个公司名称的主键)。
  • 您不能完全受益于DB优化器,后者可以利用规范化表的大小和多个索引的统计信息来确定复杂查询的最佳访问策略。这可能会很快抵消避免连接的有限好处。

标准化表

上面的缺点是标准化模式的优点。当然,查询的编写可能会稍微复杂一些。

简而言之,规范化的模式可以更好地表达数据之间的结构和关系。我会挑衅性的说,这与使用一套有序办公室抽屉所需的纪律与使用垃圾箱的便利性之间的区别是一样的。


5

我认为您的问题至少有两个部分:

1.为什么不同类型的实体不应该存储在同一张表中?

这里最重要的答案是代码的可读性和速度。A的SELECT name FROM companies WHERE id = ?可读性比a大得多,SELECT companyName FROM masterTable WHERE companyId = ?并且您偶然查询废话的可能性也较小(例如SELECT companyName FROM masterTable WHERE employeeId = ?,当公司和员工存储在不同的表中时,这是不可能的)。至于速度,可以通过顺序读取完整表或从索引中读取来检索数据库表中的数据。如果表/索引包含较少的数据,则两者都更快,如果数据存储在不同的表中,则情况会更快(并且您只需要读取其中一个表/索引)。

2.为什么将单一类型的实体拆分为存储在不同表中的子实体?

在这里,原因主要是为了防止数据不一致。使用单表方法,对于订单管理系统,您可以将客户订购的产品的客户名称,客户地址和产品ID作为单个实体存储。如果客户订购了多种产品,则数据库中将具有该客户名称和地址的多个实例。最好的情况是,数据库中只是重复数据,这可能会使数据变慢。但是更糟糕的情况是,输入数据时有人(或某些代码)犯了一个错误,从而使公司最终在数据库中使用不同的地址。仅此一项就够糟糕的了。但是如果您要根据公司名称查询公司地址(例如SELECT companyAddress FROM orders WHERE companyName = ? LIMIT 1),您可以任意获得返回的两个地址之一,甚至不会意识到存在不一致之处。但是,每次运行查询时,实际上可能会获得一个不同的地址,具体取决于DBMS在内部如何解决查询。这可能会在其他地方破坏您的应用程序,而导致破坏的根本原因将很难找到。

使用多表方法,您将意识到从公司名称到公司地址存在功能依赖性(如果公司只能有一个地址),则可以将(companyName,companyAddress)元组存储在一个表中(例如company),以及另一个表(例如order)中的(productId,companyName)元组。然后UNIQUE,对company表的约束可能会迫使每个公司在您的数据库中只有一个地址,这样就不会出现公司地址不一致的情况。

注意:实际上,出于性能原因,您可能会为每个公司生成一个唯一的companyId并将其用作外键,而不是直接使用companyName。但是一般方法保持不变。


3

TL; DR -他们基于如何设计数据库,他们是当他们在学校教书。

我本可以在10年前写这个问题。我花了一些时间来理解为什么我的前辈们以他们的方式设计数据库。您正在与以下某人合作:

  1. 使用Excel作为数据库或
  2. 他们从离开学校开始就使用最佳实践。

我不怀疑它是第一名,因为您的表中实际上有ID号,因此我假设第二名。

放学后,我在一家使用AS / 400(又名IBM i)的商店工作。我发现他们在设计数据库的方式中有些奇怪的事情,并开始主张我们进行更改以遵循我被教如何设计数据库的方式。(当时我很笨)

耐心的年长程序员向我解释了为什么这样做是为什么。他们没有更改架构,因为它会导致比我还旧的程序损坏。从字面上看,一个程序的源代码的创建日期是我出生前一年。在我们正在使用的系统上,他们的程序必须实现数据库的查询计划程序为您处理的所有逻辑和操作。(您可以通过对其中一个查询运行EXPLAIN来看到它)

他是我正在尝试实现的技术的最新专家,但是保持系统的运行比进行更改更为重要,因为“这与我所教的内容背道而驰”。我们每个人的每个新项目都开始充分利用我们能够使用的关系模型。不幸的是,那时的其他程序员/顾问仍然在设计他们的数据库,就像他们在使用该系统以前的约束一样。


我遇到的与关系模型不符的一些示例:

  • 日期存储为儒略日数,需要连接到日期表才能获取实际日期。
  • 具有相同类型的顺序列的非规范化表(例如code1,code2, ..., code20
  • NxM个长度CHAR列,表示长度为M的N个字符串的数组。

我做出这些设计决策的原因都是基于数据库最初设计时系统的约束。

日期 -有人告诉我,使用日期函数(哪个月,日或星期几)处理日期要比创建包含所有这些信息的每个可能日期的表花费更多的处理时间。

相同类型的顺序列 -所处的编程环境允许程序在行的一部分上创建数组变量。而且,这是减少读取操作数量的更简单方法。

NxM Length CHAR列 -将配置值推入一列以减少文件读取操作更加容易。

C语言中一个构思不佳的示例等效于反映他们拥有的编程环境:

#define COURSE_LENGTH 4
#define NUM_COURSES 4
#define PERIOD_LENGTH 2

struct mytable {
    int id;
    char periodNames[NUM_COURSES * PERIOD_LENGTH];  // NxM CHAR Column
    char course1[COURSE_LENGTH];
    char course2[COURSE_LENGTH];
    char course3[COURSE_LENGTH];
    char course4[COURSE_LENGTH];
};

...

// Example row
struct mytable row = {.id= 1, .periodNames="HRP1P2P8", .course1="MATH", .course2="ENGL", .course3 = "SCI ", .course4 = "READ"};

char *courses; // Pointer used to access the sequential columns
courses = (char *)&row.course1;


for(int i = 0; i < NUM_COURSES; i++) {

    printf("%d: %.*s -> %.*s\n",i+1, PERIOD_LENGTH, &row.periodNames[PERIOD_LENGTH * i], COURSE_LENGTH,&courses[COURSE_LENGTH*i]);
}

产出

1:HR->数学
2:P1-> ENGL
3:P2-> SCI
4:P8->读

据我了解,当时其中一些被认为是最佳实践。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.