為企業設計數據庫時,主要目標是正確的表示數據、數據之間的聯系以及與企業業務相關的數據約束。為了實現這個目標,我們可以使用一種或多種數據庫設計技術。前面講了實體 - 聯系(ER)建模。下面將講述另一種數據庫設計技術——規范化。
規范化 是一種數據庫設計技術,從分析屬性之間的聯系(即函數依賴)入手。屬性刻畫了企業重要數據的特性或者這些數據之間聯系的特性。規范化使用一系列測試(描述為范式)幫助我們確定這些屬性的最佳組合,最終生成可支持企業數據需求的一組適當關系。
本篇主要目標是介紹函數依賴的概念,并且討論如何將關系規范化到第三范式。下一篇還將給出函數依賴的形式化描述以及比 3NF 更高的范式。
1. 規范化的目的
規范化(normalization):生成一組既具有所期望的特性又能滿足企業數據需求的關系的技術。
進行規范化的目的是確定一組合適的關系以支持企業的數據需求。所謂合適的關系,應具有如下性質:
- 屬性的個數最少,且這些屬性是支持企業的數據需求所必需的。
- 具有緊密邏輯聯系(描述為 函數依賴)的諸屬性均在同一關系中。
- 最少 的冗余,即每個屬性僅出現一次,作為外部關鍵字的屬性除外。連接相關關系必須用到外部關鍵字。
數據庫擁有一組合適的關系的好處是:數據庫易于用戶訪問,數據易于維護,在計算機上占有較小的存儲空間。而使用未能被適當規范化的關系帶來的問題詳見第 3 小節。
2. 規范化對數據庫設計的支持
規范化是一種能夠應用于數據庫設計任何階段的形式化技術。這里著重強調規范化的兩種使用方法。方法 1 將規范化視為一種自下而上的獨立的數據庫設計技術。方法 2 則將規范化作為一種確認技術使用:用規范化技術檢驗關系的結構,而這些關系的建立可能采用自上而下的方法,比如 ER 建模。不管使用哪一種方法,目標都是一致的,即建立一組設計良好(well-designed)的關系以滿足企業的數據需求。
上圖給出了一些能夠用來進行數據庫設計的數據源示例。盡管用戶的需求規格說明書是首選的數據源,但是僅基于直接從其他數據源獲得的信息進行數據庫設計也是可能的,這些數據源包括表單和報表。上圖還說明對同一個數據源兩種方法均適用。然而盡管理論上如此,實際操作時究竟采用哪一種方法還要取決于數據源反映出的數據庫的大小、范圍以及復雜度,同時還取決于數據庫設計者的偏好及其專長。是否將規范化作為一種自上而下的獨立的數據庫設計技術(即方法 1)使用,常常受限于數據庫設計者對設計細節的掌握程度。然而,當我們將規范化作為一種確認技術(即方法 2)使用時,就沒有了這種限制。因為在這種使用方法中,數據庫設計者在任何時刻都能專注于數據庫的一部分,例如一個單一的關系,因此,不管數據庫的大小或者復雜度如何,規范化都能發揮效能。
3. 數據冗余與更新異常
如上所述,關系數據庫設計的一個主要目標就是將屬性組合成關系時力求最少的數據冗余。如果能夠達到這個目標,就可能為數據庫帶來以下好處:
能用最少的操作完成對數據庫中存儲數據的更新,由此可以降低數據庫中出現數據不一致的概率。
減少存儲基本關系所需的文件存儲空間,因而將成本降低至最低。
當然,關系數據庫(的運行)也依賴于一定的數據冗余的存在。這種冗余一般是以主關鍵字(或者候選關鍵字)的多個副本的形式出現,這些副本在與之相關聯的關系(即主關鍵字或候選關鍵字所述的關系)中,做為外部關鍵字出現,用以表示數據之間的聯系。
舉例說明:
關系 StaffBranch 是關系 Staff 和 關系 Branch 的另一種表達方式,這些關系的結構如下所示:
Staff (staffNo, sName, position, salary, branchNo)
Branch (branchNo, bAddress)
StaffBranch (staffNo, sName, position, salary, branchNo, bAddress)
注意,每個關系的主關鍵字都有下劃線。
在關系 StaffBranch 中存在冗余數據:同一個分公司的信息在每一個屬于該分公司的員工信息里反復出現。相反,在關系 Branch 中,每個分公司的信息只出現了一次,而且在關系 Staff 中只有分公司的編號(branchNo)這一屬性的值重復出現,這是為了能夠表示出每一個員工都歸屬于哪一個分公司。存在冗余數據的關系可能存在一些問題——更新異常,更新異常又可分為插入異常、刪除異常和修改異常。關系 Staff 關系 Branch 和關系 StaffBranch表如下所示。
Staff:
staffNo | sName | position | salary | branchNo |
---|---|---|---|---|
SL21 | John White | Manager | 30000 | B005 |
SG37 | Ann Beech | Assistant | 12000 | B003 |
SG14 | David Ford | Supervisor | 18000 | B003 |
SA9 | Mary Howe | Assistant | 9000 | B007 |
SG5 | Susan Brand | Manager | 24000 | B003 |
SL41 | Julie Lee | Assistant | 9000 | B005 |
Branch:
branchNo | bAddress |
---|---|
B003 | 163 Main St, Glasgow |
B007 | 16 Argyll St, Aberdeen |
B005 | 22 Deer Rd, London |
StaffBranch:
staffNo | sName | position | salary | branchNo | bAddress |
---|---|---|---|---|---|
SL21 | John White | Manager | 30000 | B005 | 22 Deer Rd, London |
SG37 | Ann Beech | Assistant | 12000 | B003 | 163 Main St, Glasgow |
SG14 | David Ford | Supervisor | 18000 | B003 | 163 Main St, Glasgow |
SA9 | Mary Howe | Assistant | 9000 | B007 | 16 Argyll St, Aberdeen |
SG5 | Susan Brand | Manager | 24000 | B003 | 163 Main St, Glasgow |
SL41 | Julie Lee | Assistant | 9000 | B005 | 22 Deer Rd, London |
3.1 插入異常
插入異常主要有兩類,我們用上表中的關系 StaffBranch 來解釋這兩類異常。
在關系 StaffBranch 中插入一位新員工的信息時,這些信息中必須包括該員工將被分配到分公司的信息。比如,在插入某一被分配到編號為 B007 的分公司工作的員工信息時,我們必須正確輸入分公司 B007 的所有信息,由此確保這些數據與關系 StaffBranch 已有的關于分公司 B007 的元組中的信息一致。而 Staff 和 Branch 關系則不存在這種潛在的不一致性,因為在關系 Staff 中,秩序為每個員工錄入相應的分公司編號就可以了,而在關系 Branch 中,編號為 B007 的分公司的信息是作為一個單獨的元組存儲在數據庫中的。
在向關系 StaffBranch 中插入一個新的分公司的信息時,由于該分公司目前可能還沒有給員工,因此有必要在錄入與員工相關的信息時將其設為 NULL,如將 staffNo 賦值為 NULL。但是 staffNo 是關系 StaffBranch 的主關鍵字,若試圖為 staffNo 錄入 NULL 值,則會違反實體完整性約束,這樣做是不允許的,我們也因此無法向關系 StaffBranch 中插入一個 staffNo 為 NULL的一個新的分公司的元組。Staff 和 Branch 的關系設計則避免了這類問題的出現,因為分公司的信息在關系 Branch 中單獨錄入,與員工信息分離,而員工被分配到哪個分公司工作的信息則會在以后的時間再錄入關系 Staff 中。
3.2 刪除異常
從關系 StaffBranch 中刪除一個元組時,若該元組表示某分公司最后一名員工,則刪除元組之后,該分公司的信息也從數據庫中丟失了。例如,如果從關系 StaffBranch 中刪除員工編號為 SA9 的元組,則編號為 B007 的分公司的信息也將從數據庫中消失。同樣的,Staff 和 Branch 的分開設計避免了這個問題。
3.3 修改異常
如果我們想要修改關系 StaffBranch 中某分公司的某個屬性值,比如修改分公司 B003 的地址,那么我們必需更新所有 B003 的員工的元組。若如此修改操作未能在關系 StaffBranch 中所有相關的元組上執行,數據庫則會產生不一致:同屬分公司 B003 的員工,其元組在分公司地址這一屬性上的取值可能會有不同。
上面的示例說明了 Staff 和 Branch 的分開設計比 StaffBranch 的設計具有更令人滿意的特性。也就是說,當關系 StaffBranch 發生更新異常時,我們可以通過將其分解為 Staff 和 Branch 兩個關系來避免這些異常。當把較大的關系分解成較小的關系時,有兩個很重要的特性:
無損連接(lossless-join):該特性確保了緣關系的任意實例信息能通過較小關系的對應實例確定出來。
依賴保持(dependency preservation):該特性確保了只需簡單的在較小的關系上支持某些約束,就可以繼續支持在原關系上存在的約束。也就是說,我們不必對較小的關系執行連接操作就可以檢驗他們是否違反了原關系上的約束。
4. 函數依賴
與規范化相關的一個重要概念就是 函數依賴,函數依賴描述了屬性之間的聯系(Maier,1983)。
4.1 函數依賴的特征
為了討論函數依賴,假設有某一關系模式,該關系模式具有屬性(A,B,C, ... ,Z),我們用一個 全域關系(universal relation)R = (A,B,C,...,Z)來描述數據庫。該假設意味著每個數據庫中的屬性都有一個唯一的名字。
函數依賴(functional dependency):描述一個關系中屬性之間的聯系。例如,假設 A 和 B 均為關系 R 的屬性,若 A 的每個值都和 B 中一個唯一的值相對應,則稱 B 函數依賴于 A,記為 A → B(A,B可能由一個或多個屬性組成)。
函數依賴是屬性在關系中的一種語義特性。該語義特性表明了屬性和屬性是如何關聯起來的,確定了屬性之間的函數依賴。當存在某一函數依賴時,這個以來就被視為屬性之間的一種 約束。
考慮某一關系,它擁有屬性 A、B,其中屬性 B 函數依賴于屬性 A。假設知道 A 的值,我們來驗證該關系是否支持這種依賴。結果我們發現無論任何時候,對于所有元組,若屬性 A 的值等于給定值,則該元組的屬性 B 的值都是唯一的。因此,當兩個元組的屬性 A 的值相同時,其屬性 B 的值也是相同的。反之則不然,對于一個給定的 B 的值,可能對應著幾個不同的 A 的值。屬性 A、B 之間這種以來可以用下圖表示。
另外一種描述屬性 A、B 之間的這種聯系的術語為 “A 函數決定 B”。也許一些讀者更喜歡使用后者,因為它與屬性之間的函數依賴的箭頭的方向相同,顯得更自然一些。
決定方:位于函數依賴箭頭左邊的屬性或屬性組。
當存在函數依賴時,位于箭頭左邊的屬性或屬性組稱為 決定方(determinant) 。例如,上圖中,A 是 B 的決定方。
在確定一個關系中屬性間的函數依賴時,必須明確它是僅當屬性取某一特定值時成立,還是該屬性取值集中任意值時均成立,區分清楚這一點很重要。換句話說,函數依賴是關系模式(內涵)的性質,而不是模式的某個實例(外延)的性質。
另一個規范化時有用的函數依賴的性質是:決定方應該具有最少的屬性,這些屬性是保證右邊的屬性函數依賴于它所必不可少的。我們稱其為 完全函數依賴。
完全函數依賴(full functional dependency):假設 A 和 B 是某一關系的屬性(組),若 B 函數依賴于 A,但不函數依賴于 A 的任一真子集,則稱 B 完全函數依賴于 A。
對于函數依賴 A → B,如果去掉 A 中的任一屬性都使得該依賴不再成立,那么 A → B 就是完全函數依賴。如果去掉 A 中的某些屬性,依賴仍然成立,那么函數依賴 A → B 就是 部分函數依賴。
概括而言,規范化時要用到的函數依賴具有下列性質:
- 函數依賴左邊的屬性(組)(即決定方)與右邊的屬性(組)是一對一的聯系(注意,若反過來看,也就是右邊與左邊的屬性(組)之間則既可能為一對一的聯系,也可能為一對多的聯系)。
- 恒成立。
- 決定方具有最少的、足以支持與右邊的屬性(組)之間依賴關系的屬性,即右邊的屬性(組)完全依賴于左邊的屬性(組)。
至此,我們已經討論了在規范化時所關心的一些函數依賴,但是還有必要了解一種函數依賴,即 傳遞依賴(transitive dependency)。因為關系中若存在傳遞依賴,就有可能引起更新異常。本節只簡單介紹傳遞依賴,目的是在需要時我們能夠識別出他們。
傳遞依賴:假設 A、B、C 是某一關系的屬性,若 A → B,B → C,則稱 C 通過 B 傳遞依賴于 A(假設 A 并不函數依賴于 B 或 C)。
4.2 識別函數依賴
如果我們能夠完全理解每一個屬性的意義以及這些屬性之間的聯系,那么確定屬性之間所有的函數依賴應該非常簡單。這類信息應由企業提供,可能是通過與用戶討論形成,同時(或者)也可能是以文檔的形式出現,比如用戶需求規格說明書。但是,如果與用戶無法溝通,并且(或者)文檔并不完備,那么數據庫設計人員就有必要基于該數據庫應用的領域,利用自己的常識或經驗來補充那些缺失的信息。
4.3 利用函數依賴確定主關鍵字
確定關系函數依賴集的主要目的是確定該關系必須滿足的完整性約束集。首先要考慮辨別的一種重要的完整性約束是候選關鍵字,候選關鍵字中的一個將被選作關系的主關鍵字。
5. 規范化過程
規范化是一種基于關系的主關鍵字(或者候選關鍵字)和函數依賴對關系進行分析的形式化技術(Codd,1972b)。規范化技術涉及一系列的規則,這些規則能夠用來對關系進行單獨測試以保證數據庫可以被規范化到任意程度。當某種規范化的要求未能得到滿足時,就將違反需求的關系分解為多個關系,直至分解后的每一個關系都能滿足規范化的要求為止。
最早提出的三個范式為第一范式(1NF)、第二范式(2NF)和第三范式(3NF)。后來,R.Boyce 和 E.F.Codd 又提出了一種增強的第三范式,稱為 Boyce-Codd 范式(BCNF)(Codd,1974)。除了第一范式,所有這些范式都是基于關系的屬性之間的函數依賴的(Maier,1983)。隨后還提出了比 BCNF 更高層的范式——第四范式(4NF)和第五范式(5NF)(Fagin,1977, 1979)。但是,需要用到第四范式、第五范式的情況相當少。本篇中將只講述前三種范式,對 BCNF、4NF 和 5NF 的討論則留到下一章進行。
規范化的過程包括一系列步驟,每一步都對應著某種具有已知性質的特定范式。隨著規范化的進行,關系的個數逐漸增多,關系的形式也逐漸受限(結構越來越好),也就越來越不容易出現更新異常。對于關系數據模型,應該認識到在建立關系時只有滿足第一范式(1NF)的要求是必須的,后面的其他范式都是可選的,這一點很重要。但是為了避免出現前面討論過的更新異常的情況,通常建議將規范化至少進行到第三范式(3NF)。從第一范式到第五范式是是逐步遞進的,第二范式是在滿足第一范式的基礎上增加條件形成的,以此類推。
下圖為規范化過程的縱覽圖,途中突出顯示了規范化過程中每一個步驟的主要操作。
這里我們將規范化視為一種自下而上的技術,主要講述如何利用這種技術從案例表單中抽取屬性信息,并先將其轉化為非范式(Unnormalized Form,UNF)表格的形式,然后將其逐漸分解以滿足每一種范式的要求,分解一直進行到原案例表中的屬性都被分解為若干滿足 3NF 要求的關系為止。盡管這里使用的例子都是從某一范式規范化到更高一級的范式,但是,對于其他的例子來說并不一定必須這么做。在解決某些特殊問題時,我們可以將 1NF 的關系轉換為 2NF 的關系,或者在某些情況下,直接將其轉化為 3NF 的關系。
為了簡化對規范化的講述,我們假設在所用案例中,每一個關系都有一個函數依賴集,每一個關系都已被指派了一個主關鍵字。也就是說,在規范化的過程開始之前,充分、完全地理解屬性的意義及其之間的聯系是必要的。這些信息是進行規范化的基礎,我們將利用這些信息來驗證一個關系是否已經滿足了指定范式的要求。
6. 第一范式(1NF)
非范式(UNF):包含一個或多個重復組的表。
第一范式(1NF):屬于第一范式的關系,其每一行和每一列相交的位置有且僅有一個值。
我們開始進行規范化之前,首先要將數據從數據源(例如標準的數據輸入表單)轉換為包含行和列的表格形式。這種格式的表是非范式的,因而被視為 非規范化的表(unnormalized table)。為了將非規范化的表轉化為第一范式,我們需要確定并刪除表中的重復組。一個重復組可以是一個屬性或一組屬性,它對應表的某個(些)關鍵屬性的一個實例可能出現多個值。注意,這里所說的 “關鍵屬性” 是指非規范化的表中那些可以唯一標識每一行的屬性(組)。從非規范化的表中消除重復組的常用方法有兩種:
在含有重復數據的那些行的空白列上輸入合適的數據,也就是在需要填充的位置復制非重復數據。這種方法通常被看做是對表的平板化(flattening)處理。
將重復數據單獨移到一個新的關系中,同時也將原來關系中的關鍵屬性(組)復制到這個新的關系中。有時候,非規范化的表可能包含多個重復組,或者在重復組里又有重復組。在這些情況下,重復使用這一方法直到不再存在重復組位置。若結果關系均不含重復組,則他們都是 1NF 的。
這兩種方法得到的結果關系都是 1NF 的關系,在行、列交叉處都只包含原子(或單一)值。盡管兩種方法都是正確的,然而方法 1 在對原 UNF 的表進行平板化的過程中也引入了較多的冗余;方法 2 則創建了兩個或更多的關系,這些關系的冗余度都低于原 UNF 的表的冗余度。換句話說,在對原 UNF 的表的規范化過程中,方法 2 比方法 1 做的工作更多。但是,不管從哪一種方法開始,原 UNF 的表終將被規范化為一組相同的 3NF 的關系。
7. 第二范式(2NF)
第二范式基于完全函數依賴的概念,第二范式適用于具有合成關鍵字的關系,即主關鍵字有兩個或兩個以上的屬性構成。主關鍵字僅包含一個屬性的關系已經至少是 2NF 的。不是 2NF 的關系可能會出現前面討論過的更新異常。
第二范式 (2NF):滿足第一范式的要求并且每個非主關鍵字屬性都完全函數依賴與主關鍵字的關系。
將 1NF 的關系規范化為 2NF 關系需要消除部分依賴。如果存在部分依賴,就要將部分依賴的屬性從原關系移出,移到一個新的關系中去,同時將這些屬性的決定方也復制到新的關系中。
8. 第三范式(3NF)
盡管 2NF 的關系比 1NF 關系的數據冗余度低,但是仍然存在更新異常問題。此時的更新異常是由依賴傳遞引起的,我們需要消除這種依賴,繼續將關系規范化到第三范式。
第三范式(3NF):滿足第一范式和第二范式的要求并且所有非主關鍵字屬性都不傳遞依賴于主關鍵字的關系。
將 2NF 的關系規范化為 3NF 需要消除傳遞依賴。如果存在傳遞依賴,就將傳遞依賴的屬性(組)移到一個新的關系中,并將這些屬性的決定方也復制到該關系中。
9. 2NF 和 3NF 的一般化定義
在前兩節的 2NF 和 3NF 的定義中,不允許存在對主關鍵字的部分依賴和傳遞依賴,以此避免出現前述的更新異常現象。然而,這些定義并沒有考慮關系中的其他候選關鍵字(如果存在不止一個候選關鍵字)。下面將在考慮關系的所有候選關鍵字的基礎上,給出 2NF 和 3NF 的更一般化定義。注意,考慮關系的候選關鍵字并不會影響 1NF 的定義,因為 1NF 與關鍵字和函數依賴無關。在更一般化的定義中,我們規定:屬于任何一個候選關鍵字的屬性都叫做主屬性(candidate-key attribute);提到部分依賴、完全依賴和傳遞依賴時不僅僅是基于主關鍵字,而是基于所有的候選關鍵字。
第二范式(2NF):滿足第一范式的要求并且每個非主屬性都完全函數依賴于任何一個候選關鍵字的關系。
第三范式(3NF):滿足第一范式和第二范式的要求并且沒有一個非主屬性傳遞依賴于任何一個候選關鍵字。
在使用 2NF 和 3NF 的一般化定義時,必須注意的是所有候選關鍵字上的部分依賴和傳遞依賴,而不只是主關鍵字上的。這使得規范化過程變得更加復雜,但一般化定義能為關系增加附加約束,從而有可能發現關系中隱藏的、被遺漏的冗余。
在進行規范化時,是僅簡單的分析主關鍵字上的依賴,還是使用一般化定義進行規范化,這需要權衡。前者使得規范化過程簡單,并且可以發現關系中存在的大多數問題和明顯的數據冗余;后者則有更多的機會發現關系中被遺漏的冗余。實際上, 常見的情形是,無論使用基于主關鍵字的定義還是使用 2NF、3NF 的一般化定義,對關系進行分解的結果相同。