首先,我们全篇做的所有工作只为了达成一个目的 —— 将一个冗余的表进行解耦,这个工作发生在我们使用 SQL 语句创建表之前。可以沿着这一个线索来阅读全文:
分解
我们将一些设计不好的大表进行拆分,本质上是为了后续操作这个表时出现各种重复插入,或者是删除异常,插入异常等意外状况。分解就是将属性耦合的关系拆分成两个或多个非耦合的关系。但是,分解有可能会造成信息的丢失,这种我们称有损分解,反之,称无损分解。
"信息丢失" 是个模糊的概念,因此这里有更清晰的表述:如果分解之后的两个关系如果能通过自然连接恢复原关系的信息,那么这就是无损分解。即,假定一个完整的关系 r(R) 被垂直分解成了两个表 r(R1) 和 r(R2) ,其中 R = R1 ∪ R2。能证明有损分解发生的一个符号化表达就是:r ⊂ ΠR1 (r) ⋈ ΠR2 (r)。
假定有这样一个关系:
street,city,和 salary 属性似乎只和员工的 name 或 ID 有关系。下面是一个不好的分解:
原本在 employee 关系当中,有这样的元组:
ID | name | street | city | salary |
---|---|---|---|---|
10001 | Kim | Main | Perryridge | 75000 |
10002 | Kim | North | Hampton | 60000 |
它们被切分成 r(R1):
ID | name |
---|---|
10001 | Kim |
10002 | Kim |
以及 r(R2) ( 假定这里我们将 { name,street,city,salary } 视作是一个候选码 ):
name | street | city | salary |
---|---|---|---|
Kim | Main | Perryrige | 75000 |
Kim | North | Hampton | 60000 |
如果重新对这两个表进行自然连接:
ID | name | street | city | salary |
---|---|---|---|---|
10001 | Kim | Main | Perryridge | 75000 |
10001 | Kim | North | Hampton | 60000 |
10002 | Kim | Main | Perryridge | 75000 |
10002 | Kim | North | Hampton | 60000 |
可以看到,复原的这个关系并不是原来的 r(R)。我们获得了更多的元组,却得到了更少的信息。分解后的版本没法表示姓名与地址,或薪水之间的联系,这个联系就是在分解过程中丢失的信息。
函数依赖与闭包
注意。本小节谈论的闭包是 —— 关于函数依赖集的闭包。后文还有一个属性集的闭包,不要搞混了。
为了能达到无损分解的目的,我们需要知道 "哪些能拆,哪些不能拆"。想要做到这一点,我们需要一些符号来表达属性之间的相互联系。首先,要知道函数依赖的概念。另外本篇约定使用希腊字母 ( α,β,γ 等 ) 来表述一个属性的集合,而用 A,B 等表示一个单独的属性。
设 α → β,对于关系 r(R) 中的任意元组 t1 和 t2,若 t1[α] = t2[α],那么 t1[β] = t2[β] 必定成立。此时称 β 函数依赖于 α。每个依赖可以简记为 f。
有一些特殊情况: β ⊂ α,那么 α → β 总成立。这又分为两个情况:
- 特殊的,若 β = α,即 α → α 总是成立,我们一般只会在后续算法的 "初始化" 过程利用这个特性。
- 如果 β ⊆ α,那么 α → β 成立,我们称这种情况为平凡的函数依赖。见后文的阿姆斯特朗公理:自反性。
我们大部分情况下讨论的是非平凡的函数依赖。
一个数据库可能包括因需求而人为设置的各种形式的依赖 fi,它们显式地构成了函数依赖的集合 F。注意,除了我们显式声明的函数依赖集 F 之外,其内部的 fi 之间或许还可以构成隐式地依赖。这些隐含的依赖又称被 F "逻辑蕴含"。F 集及其蕴含的所有隐式依赖共同组成了 F 的闭包,记为 F+。
Armstrong 公理
想要高效地从已有的 F 中快速查找出蕴含的依赖,我们需要有一套有效而完备的公理(或经验),而不是凭借 "合理的直觉"。这就是我们引入 Armstrong 公理的原因。这里一共包含了原本的三条以及延伸出的另三条附加规则(可以通过前面的三条定理来证明)
- 自反律:若 β ⊆ α ( 非平凡的函数依赖 ),则 α → β。
- 增补律:若 α → β ,那么 γα → γβ。
- 传递律:若 α → β,且 β → γ,那么 α → γ。
- 合并律:若 α → β,且 α → γ,那么 α → βγ。
- 分解律:若 α → βγ,那么 α → β 且 α → γ。
- 伪传递律:若 α → β,且 γβ → θ,那么 γα → θ。
这很重要,我们依赖于这些公理去挖掘更多的函数依赖。除此之外,后文求正则覆盖也可能要用到它。
属性闭包与判断候选码
注意。本小节谈论的闭包是 —— 属性集的闭包。前文还有一个函数依赖集 F 的闭包 F+。
如果 α → B,那么称属性 B 被 α 函数决定。根据已有的函数依赖集 F,我们同样能挖掘出由 α 所能推出的所有其它属性,这些属性又组成了一个集合,我们称它又是 α 的闭包,即 α+。
比如,假定一个关系 r( A,B,C,G,H,I ) 以及它的函数依赖集 { A → B,A → C,CG → H,CG → I,B → H },我们要查找的是 ( AG ) +。假设结果集为 result,在第一步,我们可以令 result = { AG } ( AG → AG 总是成立的 )。
- A → B 使得 result := result ∪ { B } = { ABG }。
- A → C 使得 result = { ABCG }。
- CG → H 使得 result = { ABCGH }。
- CG → I 使得result = { ABCGHI }。
- result = R,因此可知 ( AG )+ = R。
显然,如果一个关系 R 的所有属性都能被 α 属性决定 —— 或者说 α+ 囊括了所有 R 的属性,那么显然 α 是一个超码。我们在后文的候选码查找中利用了这个特性。
正则覆盖
一个错综复杂的依赖关系其实并不利于无损分解。我们都希望呈现在眼前的是一个最小,且精简的依赖集。换句话说,我们需要知道哪些属性是 "冗杂" 的。对于一个依赖 α → β,多余的属性 A ( 或 B ) 有可能在左侧出现,于是我们可能简化成:( α - A ) → β;它也有可能右侧出现,于是我们简化成:α → ( β - B )。被证实可删除的属性称之为无关属性。
如果从左侧删除了属性,那么这个依赖会变得更强。而如果从右侧删除了属性,那么这个依赖会变得更弱。因此,如何检验删除属性之后的依赖关系没被破坏,根据删除的方向不同,判别的方式也不同。
设一个函数的函数依赖集合为 F,假设所有的无关属性都被删除,那么将得到一个正则覆盖,它的符号表示是:Fc,有些资料也称正则覆盖是最小依赖集。在 Fc 当中,任意 fi: αi → βi 左侧的 αi 都是唯一的,即不存在 α1 和 α2,且 α1 = α2。所以我们推导正则覆盖的第一步是:利用 Armstrong 公理的合并律整理那些 α 相同的项。
从右侧移除
若要判断的属性位于依赖的右侧,例如 { A → BC },我们推测 B 或 C 是无关属性。则删除该属性,在余下的函数依赖集 F' 中计算该依赖左部集合的闭包 α+。若 α+ 中包含要判断的属性,则该属性就是无关属性。
第一个例子是:F{ A → BC,B → AC,C → AB }。我们关注 B 属性,它在依赖右侧。删除该属性,余下 F' = { A→C,B → AC,C→AB }。计算左侧剩余属性的闭包 (A)+。因为 A → C,C → AB,所以 (A)+ 中包含删去的属性 B。故 B 是无关属性。
从左侧删除
若要判断的属性位于依赖的左侧,例如 { AB → C } 中,我们推测 A 或 B 是无关属性。那么,删除该属性,在原本的依赖集 F 中计算该依赖剩余左部集合的闭包 α+。若 α+ 闭包中包含该依赖右侧所有的属性,则该属性则是无关属性。 比如,在函数依赖集 F{ Z → X,X → P,XY → WP,XYP → ZW } 中,我们推测 XYP → ZW 中的 P 属性可能是多余的。删除它,求解剩余左侧集合 (XY) 的闭包,求取域是原来的 F{ Z → X,X → P,XY → WP,XYP → ZW }。
因为 XY → WP,所以 (XY)+ = { X,Y,W,P };又因为 XYP → ZW,所以 (XY)+ = { X,Y,Z,W,P }。包含原依赖右侧 ZW,故可知 P 是无关属性。
另一个例子:F{ A → BC,B → C,AB → C }。 关注 AB → C 的 A 属性。按照步骤,我们要求 (B)+ 是否包含 C。但显然,删除它后余下的部分 B → C 在 F 中已经存在,因此 A 一定是无关属性。
3NF 与 BCNF
Boyee-Codd Normal Form,即 BCNF,巴斯范式是当今比较令人满意的范式,因此本章节暂不会讨论更高级别的范式。
1NF 要求所有的属性是原子的,在当今已是讨论数据库设计的基本条件。而 2NF → BCNF 逐级删去了非主属性对码的部分函数依赖 ( 2NF ) ,传递函数依赖 ( 3NF ),以及主属性内部之间的部分传递依赖以及传递函数依赖 ( BCNF )。对于主码为单属性的表来说,它一定满足 2NF。
对于 3NF 和 BCNF 的判定,网上有很多类似的解释,但是笔者更倾向于引用《数据库系统概念》中明确的,程序化,符号化的表达。
BCNF 判别条件
F 是关系 R 的函数依赖的集合,而 F+ 是 F 依赖集的闭包。闭包 F+ 内的所有形如 α → β 的函数依赖 f,下面至少一项成立:
- α → β 是平凡的函数依赖,即 β ⊆ α。
- α 是 R 的一个超码 ( 超码可以包含候选码 )。
3NF 判别条件
F 是关系 R 的函数依赖的集合,而 F+ 是 F 依赖集的闭包。闭包 F+ 内的所有形如 α → β 的函数依赖 f,下面至少一项成立:
- α → β 是平凡的函数依赖,即 β ⊆ α。
- α 是 R 的一个超码 ( 超码可以包含候选码 )。
- β - α 的每个属性 Ai 都出现在了任意一个候选码当中。
注意,Ai 可能分别出现在不同的候选码当中,而不一定全部出现在一个候选码才行。相比于 BCNF,3NF 放宽了对部分非平凡的函数依赖的限制。从判别条件中可知:满足高范式的关系必然满足低范式的要求;同样,违反低范式的关系一定也不满足高范式的要求。
候选码查找
凭借 "直觉" 推理候选码的效率太低了,这里有一个便捷的方法:
首先,将关系 r(R) 中的属性 R 划分为以下集合:L,LR ,R,N。然后审查依赖集合 F 中的所有依赖 fi :αi→ βi。其中:
- L 包含了仅出现在依赖左侧的元素。
- N 包含了在依赖左右侧都没有出现的元素 ( 注意,这可不是在表明它是无关属性 )。
- LR 包含了在左右两侧全部出现的元素。
- R 包含了仅在右侧出现的元素。
显然,这四个集合之间两两相交为空集,我们称它们是 R 的一个划分。下面有以下重要的推论:
- 如果一个元素 x ∈ L,那么它必定出现在任意候选码中。
- 如果一个元素 x ∈ N,那么它也必定出现在任意候选码中 ( 因为没有别的途径获取它了 )。
- 如果一个元素 x ∈ R,那么它必定不会出现在任意一个候选码中,否则,该候选码就不是最小超码。
因此,在以下情况下,我们可以快速定位候选码:
- 在没有 N 类元素的情况下,如果 L 的闭包 L+ 包含了 R 的所有元素 ( 简记 L+ = R ),则 L 集合本身就是唯一候选码。
- 在有 N 类元素的情况下,如果 L ∪ N ( 简记 LN ) 的闭包包含了 R 的所有元素,则 LN 本身就是唯一候选码。
否则,事情会稍微有点麻烦。我们需要从 LR 当中不断提取一个或者多个元素的集合 Yi,并测试 ( LRYi )+ 是否为 R。下面分别是一个 "方便的" 和另一个 "不太方便的" 例子。
例1:设关系模式 R( A,B,C,D,E,P ),其函数依赖集: F = { A → D,E → D,D → B,BC → D,DC → A },求 R 的所有候选码。 可得 L = { C,E } ,R = ∅,N = { P },LR = { A,B,D } 显然 ( CEP )+ = { CEPDBA },所以CEP是R的唯一候选码。
例2: 设关系模式 R( S,D,I, B, O, Q ), 其函数依赖集: F = { S→D, I→B, B→O, O→Q, Q→I },求R的所有候选码。 可得 L = { S },R = { D },N = ∅,LR = { I, B, O, Q } ∵ S+ = { SD }, (SI)+ = { SIDBOQ },(SB)+ = { SBDOQI },(SO)+ = { SODQIB },(SQ)+ = { SQDIBO },
∴ 候选码集合 = { SI,SB,SO,SQ }
BCNF 无损分解
分解 BCNF 范式的大体思路如下。假定输入是一个关系 r(R) 及其函数依赖集 F,而我们的输出是 R 的一个满足 BCNF 的无损分解 ρ={ R1,R2,...,Rk }。随后:
- 令 ρ = (R)。
- 如果 ρ 满足所有模式都满足 BCNF 范式,则到第 4 步。
- 如果 ρ 中有一个关系模式 S 不满足 BCNF 范式,那么我们必定能找到函数依赖 f: X → A,X 不是 S 的任何一个候选码,并且 A 不属于 X。设 S1= XA,S2 = S - A,用 { S1 和 S2 } 代替 ρ 内的关系 S,并到第 2 步。
- 输出 ρ。
这种分解思路会使我们构造出一条分解树出来。如下图左侧的关系图。同样地,图中也列出了这个分解思路的特性。
下面是一个较复杂的例子。设关系 Student( S#,Sname,Sdpet,Mname,Cname,Grade ),其函数关系为:
其中 { S#,Cname } 是主键。为了方便我们的推导,可以先给这些属性起别名:
候选码为 { AB }。显然,从前两个函数依赖我们可以得知:当前的关系 R 不属于 2NF。从 BCNF 的判别条件来看,A → CDE 是破坏 BCNF 的那个函数依赖,因此,我们将关系 R 分解成 R1 和 R2。其中:
R1 = XA = { ACDE },而 R2 = R - A = { ABF }。
额外的,我们需要做 F(R1) 在 F(R) 的投影:对于 F(R) 中的每一个函数依赖 f,删除掉那些不在 R1 的属性,然后将投影后的函数依赖 f' 添加到 F(R1) 内部。如果删除属性后出现左侧或右侧为空的情况,比如 AB → F 被 F(R1) 投影之后变成了: AB → _ 。那么这样的函数依赖就被 F(R1)舍弃掉。
因此,F(R1) = { A → CDE,D → E },其主键为 A,而 F(R2) = { AB → F },其主键为 AB 。
R2 显然没有必要继续分解了。而 F(R1) 中的 D → E 不满足 BCNF 的条件:它是非平凡的依赖,且 D 也并不作为主键。额外的,属性 E 也没有出现在任一候选码中,因此它导致 R1 也不满足 3NF ( 更通俗的解释是:非主属性 E 和主属性 A 之间存在传递依赖 )。所以根据算法,我们要继续将关系 R1 分解为 R11 和 R12。其中:
R11 = XA = { DE },F(R11) = { D → E },主键为 D。
R12 = R1 - A = { ACD },F(R12) = { A → CD },主键为 A。
最终,我们得到 R = ρ{ R11,R12,R2 } = { r( Sdept,Mname ),r( S#,Sname,Sdept ),r( S#,Cname,Grade )}。
当然,在分解这些表之前,如果我们率先求正则覆盖,那么很容易得到:既然只凭 A 就能推出 CDE, 那么 AB → CDE 右侧的所有属性都是无关属性,进而这一条依赖就是多余的。不过,最终得到的结果并不会变化。
3NF 无损分解
有了之前的预备知识,那么 3NF 的分解相对来说则比较简单。首先,将给定的依赖集 F 进行正则覆盖得到 Fc。对于 Fc 的每一个函数依赖 f:α → β,我们都令子关系的属性集合 Ri = αβ。最后,如果没有一个 (αβ)k 是超码 ( 包含了任意候选码 ),只需将任意候选码的属性集单独建立一个关系即可。
举例子:设关系 r(R),R = { ABCDEF }。假定 F = { A → F,B → A,D → B,DE → B,DF → B },求 R 的一个 3NF 分解。
第一步:求正则覆盖 Fc。可知 DE → B 中的 E 和 DF → B 中的 F 均为无关属性,因为 D → B 已经给出。整理之后的 Fc = { A → F,B → A,D → B }。
第二步:划分 L,R,LR,N 集合,寻求候选码。可知 L = { D },R = { F },LR = { AB },N = { CE },因此候选码必定包含 { CDE } ( 参考前文候选码的查找方法 )。验证 (CDE)+ 包含所有属性,因此 { CDE } 是唯一候选码。
第三步:根据算法,我们立刻能通过 Fc 得到 ρ = { R1(AF),R2(AB),R3(BD) }。由于子表内都不包含 (CDE) 候选码,因此额外添加一个 R4(CDE)。最终,这个表的关系被分解为 ρ = { R1(AF),R2(AB),R3(BD),R4(CDE) }。
参考资料
数据库规范化:模式分解算法(3NF,BCNF分解,附带口诀,通俗易懂) - 不言不语技术 - 博客园 (cnblogs.com)
【数据库】-- 无损分解和保持依赖 - 走位,走位 - 博客园 (cnblogs.com)
四、转换成BCNF的保持无损连接的分解_七夜飘雪-CSDN博客_分解为bcnf
数据库系统概论—保持无损连接性的BCNF分解算法_哔哩哔哩_bilibili
数据库-候选码问题_wodegeCSDN的博客-CSDN博客_数据库l类属性lr类属性
候选码/最小依赖集与第三范式的分解(Candidate Key & Minimal Basis & 3NF Decomposition)_哔哩哔哩_bilibili