在前几章里,我们探讨了如何捕捉一个组织真实的业务运作,并用可视化语义对其进行建模。由此得到的模型与配套图表,让领域团队与数据团队更容易就业务的核心实体及其交互达成共识。然而,当建模过程推进到物理阶段时,需要认识到:在数据库层面组织数据有多种方式。将数据拆分为更小、模块化片段的过程称为范式化(Normalization) 。本章将讲清它的工作原理以及随之而来的利弊。
范式化不是二元的“是/否”判断,而是一个连续谱系:设计需满足越来越严格的规则才能达到某个“范式等级”。虽然“Normalization”包含“normal(规范)”一词并带来正向联想,但更高的范式级别并不必然意味着更好的设计。正如本章将看到的:范式级别越高,对数据库结构施加的约束越强,异常数据更少,但代价是维护与分析变得更复杂。
本章将按不同等级来剖析范式化,理解各级的规则、优点与取舍。在总体概览之后,我们会聚焦最常见的范式类型,理解其之所以流行的原因。目标是提供关于范式化的完整全景(收益与成本),以便读者为手头任务找到合适的平衡点。
本章将涵盖以下主题:
- 把范式化理解为一个连续谱
- 范式化的优缺点
- **度到几分为宜?**如何找到“甜蜜点”
- 回顾范式化要防范的数据异常
- 从第一范式到第六范式的深入解析
数据库范式化概述
数据库范式化是以降低表内冗余与依赖为目标来组织数据库的过程。其方法是把一张“大表”拆成多张“小表”,并用外键(FK)把它们关联起来。这样做能减少数据不一致并提升数据完整性;同时得到的模块化设计也更易扩展与修改。
范式化通过一系列逐级递进的形式化规则(范式)来实现,从第一范式(1NF)到第六范式(6NF) ——其中 1NF–3NF 最常用,且对多数场景已足够。
每一范式都建立在前一范式之上,为“每张表必须满足的条件”再加一层。只有当数据库中的每张表都满足该范式(以及它的所有前置范式)时,才算达到该范式。
忽视范式规则,可能导致自相矛盾且难以管理的数据。下图展示了一张几乎违反所有一致性原则的表:
图 9.1 —— 未遵循任何范式规则的数据示例
仅两行数据,你能看出多少问题?
- 无唯一标识符,难以界定“一行代表什么”。
- Genre 列存多个值,阻碍分析。
- 两行看似重复,但没有主键无法判断。
- USA 与 America 用不同值描述同一实体。
- Label 列重复录入导致拼写不一致。
- Price 两条记录不同,无法判定哪个正确。
- RIAA Certification 是 Certified Sales 的函数,3x Platinum 却只有 2,000,000 销量,这是不可能的。
- RIAA Certification 与 RIAA Cert. ID 相互矛盾。
- 增加一个新的 Format 值,需要复制所有 Album 值(冗余)。
- 若停止销售 CD 格式,删除两行会连同专辑信息一并删除(级联误删)。
范式化通过模式设计与数据库规则来预防上述异常。随着范式程度提高,异常的可能性降低,但表的数量通常会上升;由范式带来的质量保障,需要以跨多表查询(而非单表)带来的复杂度为代价。
在进入各级范式之前,先理解它要防护的对象——数据异常。
数据异常(Data anomalies)
坏数据形态多种多样:从拼写错误到编码不当,有些质量问题难以完全避免。但非规范化(去范式)设计会让我们轻易踩入一些众所周知且可预防的坑。
要理解范式化如何防止异常,需先拆解它所缓解的两类风险:冗余与依赖:
- 冗余(Redundancy) :同一数据在一表内或多表间重复。重复让 DML 同步变得困难。
- 依赖(Dependency) :某个属性的值依赖另一个属性。依赖可分函数依赖(如“年龄”依赖“出生日期”)与多值依赖(如把姓名、年龄、爱好放在同一表,删除一种爱好可能会误删所有拥有该爱好的人)。
带着这两类风险,来看几种常见异常,它们很容易“混”进非规范化设计。
更新异常(Update anomaly)
当冗余信息只被部分更新时便发生更新异常。比如,同一信息分散在多条记录/多张表里,更新时可能只改了其中一处,导致冲突/矛盾。
如下例中,出生年份(本该是 Pirate 维度的属性)被放进了 Pirate Prizes 事实表,为事实表消费者提供了额外上下文,却引入了重复数据,埋下更新异常隐患(见图 9.2):
图 9.2 —— 更新异常
由于出生年份被重复存储,而非仅在“海盗实体”的属性里保存一次,就可能出现自相矛盾的记录。当然,把多维度细节堆在一张表里并不只会带来更新异常。
插入异常(Insertion anomaly)
当难以或无法一致且准确地插入新数据时,就发生插入异常。若把不同粒度的大量信息硬塞一张表里,插入不满足“所有字段齐备”的记录就变得困难。
如下例中,“海盗”与“船”被存于同一表,且两列都被 NOT NULL 强制要求。对 Anne Bonny 而言,这是个难题——她追随 Calico Jack,并未独自担任船长:
图 9.3 —— 插入异常
此例表约束使得不指定船只就无法插入海盗信息。这种依赖不仅妨碍插入,也会影响删除。
删除异常(Deletion anomaly)
当删除数据导致无意丢失相关信息时,便是删除异常。与插入异常类似,它源于把过多维度塞进一张表(如前例中的 Pirate Id 与 Ship)。
Calico Jack 很难长久拥有一艘船。Kingston 被收回(货物完好)不到两个月。然而,如图 9.4 所示,删除表中的 Kingston 也会把 Calico Jack 的所有记录一并抹掉:
图 9.4 —— 删除异常
这些异常有个共性:显而易见。但有些异常只有依靠业务知识才能识别。
领域异常(Domain anomaly)
当数据库设计未纳入业务规则,从而允许生成违反业务约束的值时,就会出现领域异常。它更难捕捉,因为需要对数据具备业务理解。
下例展示了一组 Pirate 的属性(图 9.5)。不了解 pirate 与 privateer 的区别(后者拿着“袭扰许可状”为某主权者实施海盗行为)的人,可能会忽略对 Blackbeard 的不当标注(他是正宗海盗)。
图 9.5 —— 领域异常
小结:范式化通过拆分表来减少依赖与冗余,并将业务规则编码进设计,从而防止上述数据异常。接下来,我们将跟随各级范式的应用,看看这是如何实现的。
通过示例理解数据库范式化(Database normalization through examples)
数据库范式化最早由关系模型之父 Edgar F. Codd 提出。Codd 首先提出了 第一范式(1NF) ,随后扩展到 第二范式(2NF) 与 第三范式(3NF) 。之后,Codd 与 Raymond F. Boyce 合作提出了 Boyce–Codd 范式(BCNF) ,也常被称为 3.5NF。
随着数据库理论的发展,人们又提出了更高的范式,直至 第六范式(6NF) ,其限制性依次增强。尽管理解全部范式很重要,但在典型业务场景中,通常无需超过 3NF。
为理解范式如何组织数据库并防止数据异常,下面我们做一个演练:从一份完全未规范化的数据出发,依次应用各级范式要求。示例使用音乐行业的数据。与现实不同的是,这里假定专辑名唯一,且不会出现两位艺人拥有完全同名专辑的情况。
基于此设定,让我们先把图 9.1 的“脏数据”转换为 1NF。
1NF
满足 1NF 需要:
- 每条记录唯一
- 每个单元格只包含一个原子值
确保记录唯一是关系数据库设计的基本要求。基于“Album 唯一”的前提,先删除重复的第二行,并在 Album 列上设置主键(PK)。
接着确保每个单元格都为原子值。把多值塞进同一单元格会妨碍分析(如统计不同 Genre 的数量、或判断是否存在某个 Genre,都需要先解析字符串)。在我们的例子里,Genre 列包含多值:
图 9.6 —— 多值数据
将这些值旋转拆分为多行,并为 Genre 建立单独的维度表,即可满足 1NF。这样既能高效分析 Genre,又不违反 Album 表的主键约束、也不复制其属性:
图 9.7 —— 满足 1NF 的设计
现在我们已拥有唯一标识且无多值单元格,继续前往 2NF。
2NF
满足 2NF 需要:
- 已满足 1NF
- 每个非候选键属性必须完全依赖于整个候选键(不得有部分依赖)
解释一下:候选键是不可再简化且仍保持唯一性的标识。在下图中出现了部分依赖:
图 9.8 —— 部分依赖示例
虽然 Artist + Year + Genre 能唯一定位记录,但真正的候选键仅为 Artist + Year。获格莱美提名/获奖数完全依赖于候选键(Artist 与 Year),若缺其一便无意义;而 Genre 只依赖 Artist(不依赖 Year),构成部分依赖,从而违反 2NF。
回到原始示例:在下表中,Album 与 Format 共同构成候选键,但除 Price 之外,几乎所有属性(如 Artist、Length)都只依赖于 Album,即对候选键呈现部分依赖,不满足 2NF:
图 9.9 —— 存在部分依赖的表
该设计迫使我们为每一种 Format都复制整套 Album 属性。为满足 2NF,可将此表分解为两张表,消除部分依赖——此时每个属性要么依赖 Album,要么依赖 Album + Format 这个完整候选键:
图 9.10 —— 满足 2NF 的设计
现在表中每个属性都依赖某个候选键了,继续看 3NF 的要求。
3NF
满足 3NF 需要:
- 已满足 2NF
- 不存在传递函数依赖(TFD)
什么是“传递”?当一个非主键列依赖于另一个非主键列时,就形成传递函数依赖(如 Label Country 依赖 Label)。TFD 会引发更新异常,如下所示:
图 9.11 —— 因传递依赖导致的更新异常
要消除这一问题,需为这些传递依赖拆出独立维度表,使其所有属性都完全依赖于该表的主键。得到的模式即可满足 3NF:
图 9.12 —— 满足 3NF 的设计
记忆口诀:前三范式
- 1NF:数据应“依赖键(the key) ”
- 2NF:数据应“完全依赖整个键(the whole key) ”
- 3NF:数据应“只依赖于键(and nothing but the key) ”
我们已移除了 TFD。继续进入略严一些的 BCNF。
BCNF(Boyce–Codd 范式,亦称 3.5NF)
满足 BCNF 需要:
- 已满足 3NF
- 每个非平凡函数依赖的决定属性本身必须是键
1974 年,Boyce 与 Codd 提出更严格的 3NF,进一步减少冗余,并让结构更好地反映业务规则。现实中“满足 3NF 但不满足 BCNF”并不常见。为了构造一个案例,我们暂别音乐场景,看一家餐馆的订位表:
图 9.13 —— 看似 3NF 合规但隐藏业务约束的表
该表当前不出现数据异常,但未反映/强制几个重要业务规则:
- 2 号桌只对电话预约开放;
- 1 号桌只接待现场排队;
- 4 号桌只留给老板儿时好友 Tony Soprano。
为满足 Boyce、Codd(以及 Soprano) ,需要引入一张 Reservation Types 表,维护允许的组合(甚至包括 Tony 事先打电话预约 4 号桌这种从未发生但被允许的情况)。调整后如下:
图 9.14 —— 满足 BCNF(也满足 Soprano)的设计
上例中,各桌的订位规则彼此不同。但如果每张桌都接受所有已知预约方式,还能进一步减少冗余吗?Ronald Fagin 认为可以——这正是他提出 第四范式(4NF) 的原因。
第四范式(4NF)
当满足以下条件时,即达到 4NF:
- 已满足 BCNF 的规则
- 每个非平凡的函数依赖与多值依赖都以**超键(superkey)**为决定因素
回顾:超键是能唯一标识一行记录的列组合(与候选键不同,超键可以包含多余列)。多值依赖指:在数据库中,某个属性的取值依赖于多个其他属性的取值。看下面这张表(展示三家门店按介质格式的专辑库存)来理解实际含义:
图 9.15 —— 存在多值依赖的表
High Fidelity 与 e-music.com 分别只卖一种格式(黑胶与数字),而 Empire Records 为所有专辑提供 CD 与黑胶。把所有信息都放在一张表中,意味着 Album 同时依赖 Format 和 Store,而它们并非超键。将信息拆为两张表即可去除多值依赖,从而满足 4NF:
图 9.16 —— 满足 4NF 的设计
但这只在 Store Formats 表中的规则成立时为真;若“任何门店都可售卖任何格式”,则图 9.14 中满足 BCNF(也满足 Soprano) 的设计同样满足 4NF。由于判定 4NF 需要业务知识,没有系统化的启发式能够保证它;接下来会看到,第五范式 也有同样的特点。
第五范式(5NF)
当满足以下条件时,即达到 5NF:
- 已满足 4NF
- 在不丢失信息的前提下,表无法进一步分解
在去除了函数依赖与多值依赖之后,若还要进一步分解,只能依据现有业务规则分析数据。看下列表(列出专辑与其可用的发行格式):
图 9.17 —— 专辑与可用格式表
若再继续分解,会得到如下所示的“唯一专辑”与“唯一格式”两张表;但它们无法通过连接还原图 9.17 的原始数据:
图 9.18 —— “唯一专辑”与“唯一格式”的连接
尝试连接这些小表会得到笛卡儿积,产生所有可能组合,从而丢失原始细节:
图 9.19 —— 继续分解导致连接产生笛卡儿积并丢失信息
在实践中,5NF 不常用,因为难以达成且多数数据库并不需要。很少出现“满足 5NF 但不满足 4NF(进而不满足 BCNF)”的情况。
不过,即便达到 5NF,仍无法防止领域异常(domain anomaly) ,而这在现实里并不罕见,值得规避。
领域–键范式(DKNF)
当满足以下条件时,即达到 DKNF:
- 已满足 5NF
- 领域约束不在任何一张表中出现
本章示例多次出现 RIAA 认证 与 认证销量。在音乐行业,RIAA 认证按销量分级:50 万为 Gold,此后每增加 100 万为 Platinum、2x Platinum……直至 Diamond(示例如此设定)。如下表:
图 9.20 —— 满足 5NF 但不满足 DKNF 的设计
认证销量 与 RIAA 认证都依赖专辑主键,但二者间存在隐式领域约束(销量决定认证级别)。
因此,尽管该设计满足 5NF,仍可能出现领域异常(看似合规却不合业务逻辑):
图 9.21 —— 即使满足 5NF 仍出现领域异常
为避免领域异常,应将 RIAA 认证拆至独立表,为每个认证级别明确边界条件;查询时可依据“认证销量”推导出正确的认证:
图 9.22 —— 满足 DKNF 的设计
第六范式(6NF)
当满足以下条件时,即达到 6NF:
- 已满足 5NF
- 每张表都只包含主键以及至多一个属性
6NF 将“消除依赖”推向极致:通过物理结构使依赖无从存在。在 6NF 中,每张表最多一个属性。如下为一张满足 5NF 的表,包含一个主键与四个属性;注意最后一条记录的 Price 为空:
图 9.23 —— 满足 5NF 但不满足 6NF 的设计
要满足 6NF,需将该表拆成五张更小的表。注意拆分后的各表都以原主键 Album 为对齐列,但 Album Price 表的记录更少(因为 Hello Nasty 没有该属性):
图 9.24 —— 满足 6NF 的设计
由于 6NF 会急剧增加表的数量,并提高查询与维护成本,因此对大多数业务并不实用。6NF 在一些集合式建模(如 Anchor Modeling)中较为知名,但不在本书范围内。不过,它依赖的一些数据库特性值得一提,Snowflake 亦有支持(后文详述):
- 在 6NF 下,连接会很多且复杂,但通过连接消除(join elimination)等优化,查询性能可能显著好于巨大的反规范化表。当查询未显式引用某些表的属性时,优化器可智能跳过相关连接。(详见第 12 章“将转换型建模付诸实践”。)
- 多表插入(multi-table insert)是 Snowflake 的另一特性,在多种建模场景(尤其 Data Vault(第 17 章)与 Anchor Modeling)中非常有用。在图 9.24 中,我们为同一主键创建了五张表;借助多表插入,可在一次装载中同时写入多表,并加入条件逻辑控制哪些表在何时更新。
我们从 1NF 到 6NF 逐步收紧规则,看到范式化如何通过拆分结构、约束依赖来减少冗余与异常;同时也要权衡:范式越高,表更多、查询更复杂。在多数业务系统中,3NF/BCNF 往往已足够;更高范式适用于特定、受控的场景。
处于范式化谱系上的数据模型(Data models on a spectrum of normalization)
前文展示了:沿着范式等级向上推进时,冗余与数据异常的可能性会下降,但表的数量与复杂度会上升。基于此,我们可以把常见的数据建模模式放到范式化谱系上来观察,帮助判断其在不同设计情景下的适配性。第 7 章《将概念建模付诸实践》讨论了维度建模流程,而与之相关的星型 / 雪花模式会在第 17 章《以现代技术扩展数据模型》中与 Data Vault 一同详细说明(它们常用于信息集市层)。不过,先在范式化语境下进行可视化,有助于后续理解其设计。需注意:以下论断反映的是一般倾向,而非绝对规则。
下图将本书提到的多种建模模式与方法置于范式化谱系上:
图 9.25 —— 常见数据模型在范式化谱系上的位置
可以认为,1NF(“依赖键”)是处理关系型数据的最低要求。由于易于理解,1NF 常见于 OBT(One Big Table,单大表) 做法:把事实与维度属性全部放在一张表中。OBT 维护困难,但查询极其简单(无需连接)。只要表不会过大(行数或列数),业务用户一般乐于使用 OBT。
星型与雪花模式通常出现在遵循 Kimball 维度方法时,适合用于数据平台的自助分析或报表层。两者都以中心事实表为核心,事实表通常规范到 3NF。
- 在星型中,维度表直接连到事实表,维度表通常反规范化到 2NF,是维护成本与易用性之间的良好权衡。
- 在雪花中,维度表一般规范到 3NF,形成层级结构:以分析复杂度换取更易维护与更少冗余。
在范式化谱系的另一端是 Data Vault。虽严格来说 Data Vault 并不满足 6NF,但其设计吸收了许多 6NF 的原则。
小结(Summary)
通过本章的多个示例,我们看到范式化如何组织数据库、降低表内的冗余与依赖。
表中的依赖与冗余会提升数据异常发生的概率,且形式多样:
- 更新异常源于冗余,可能只更新到部分副本;
- 插入 / 删除异常则源于物理依赖:当不同粒度的信息被捆在一张表中,想插入或删除不满足全部条件的记录就会困难;
- 领域异常最难识别,因为它需要对数据具备业务理解。
为避免这些异常,可按一系列逐级递进的**形式化规则(范式)**进行设计,从 1NF 到 6NF。最常用的是 1NF–3NF,对大多数数据库已足够。记忆口诀“依赖键(1NF)、完全依赖整个键(2NF)、只依赖于键(3NF) ”有助于回忆前三范式的关注点。
通过把一份完全未规范化的数据逐步改造直至各级范式,我们看到共同主题:范式越高,异常越少;表越多,维护与分析越重。
这些例子说明:“更多范式 ≠ 更好设计”。更高范式只是对数据库施加更严格的规则。理解各级范式针对的具体问题后,广泛或选择性地应用其原则,能帮助建模者在严谨性与可用性之间取得最佳平衡。
下一章将把“严谨与可用”置于舞台中央,讨论数据库命名规范与组织方式。结合本章洞见,我们将准备好迎接物理模型。