當 BCNF 還不夠:4NF、5NF 與反正規化的工程取捨
從多值相依到連接相依,看見三正規化之上的隱藏異常,並學會何時該有意識地走回頭路
當「第三正規化」還不夠:一張排班表如何同時滿足、又同時違反兩個範式?
你已經會把訂單拆成 orders 與 order_items,會用外鍵串起 students 與 enrollments,也背得出「每個非鍵欄位都要完全相依於主鍵」。但請看這張看似無害的醫院排班表:
shifts(employee, day, location)
-- 規則一:同一天同一位員工只能在一個地點
-- 規則二:同一天同一個地點只指派一位員工
這三個欄位全部都是鍵的一部分(任兩欄都能決定第三欄),沒有任何非鍵屬性,所以它輕鬆通過 1NF、2NF、3NF,甚至連 BCNF(Boyce-Codd Normal Form)都挑不出毛病。然而它仍然藏著一種會在你新增資料時悄悄產生「幽靈組合」的異常。要看見它,你得跨過 BCNF,進入第四正規化(4NF)與第五正規化(5NF)的世界——那裡的主角不再是函數相依(functional dependency),而是多值相依(multivalued dependency)與連接相依(join dependency)。
這篇進階篇假設你已熟悉 1NF–3NF 與函數相依的定義。我們直接往上走:先把 BCNF 的本質講透,再進到 4NF/5NF,最後談談為什麼真實世界的工程師有時會「故意」反正規化(denormalization),以及這背後的形式化代價。

BCNF 的真正定義:不是「更嚴格的 3NF」,而是換了一把尺
很多教材把 BCNF 描述成「比 3NF 嚴格一點點」,這容易讓人以為它只是把門檻調高。其實 BCNF 換了一套判準。
3NF 允許一種例外:若 $X \to A$ 違反了「$X$ 必須是超鍵」,只要 $A$ 是某個候選鍵(candidate key)的一部分(稱為 prime attribute),3NF 仍然放行。
BCNF 不留這個例外。它的定義乾淨俐落:
對於關聯 $R$ 上每一個非平凡函數相依 $X \to A$,$X$ 都必須是 $R$ 的超鍵(superkey)。
差別就在那個「prime attribute 豁免」。我們用一個經典的反例感受它。
看一個例子:通過 3NF 卻違反 BCNF
考慮選課與授課關係:
teaching(student, course, instructor)
相依:
(1) {student, course} -> instructor -- 每位學生在某課只跟一位老師
(2) instructor -> course -- 每位老師只教一門課
候選鍵有兩個:{student, course} 與 {student, instructor}。
先驗 3NF。相依 (2) instructor -> course 裡,左邊 instructor 不是超鍵(光憑老師決定不了學生),照理違反。但 3NF 看右邊:course 是候選鍵 {student, course} 的一部分,是 prime attribute——豁免!所以 teaching 通過 3NF。
再驗 BCNF。相依 (2) 的左邊 instructor 不是超鍵,沒有任何豁免——直接違反 BCNF。
這個違反帶來什麼實際傷害?看資料:
student | course | instructor
--------|----------|------------
Alice | DB | Prof.Chen
Bob | DB | Prof.Chen
Alice | OS | Prof.Lin
「Prof.Chen 教 DB」這個事實被儲存了兩次(Alice 列、Bob 列)。如果 Prof.Chen 改教別科,你得掃過所有他的學生列去更新——這正是 3NF 本想消滅、卻在這個豁免縫隙裡漏掉的更新異常(update anomaly)。
分解(decomposition)成兩張表即可進 BCNF:
CREATE TABLE instructor_course (
instructor VARCHAR(64) PRIMARY KEY,
course VARCHAR(64) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE student_instructor (
student VARCHAR(64) NOT NULL,
instructor VARCHAR(64) NOT NULL,
PRIMARY KEY (student, instructor),
FOREIGN KEY (instructor) REFERENCES instructor_course(instructor)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
「Prof.Chen 教 DB」現在只存一次。
BCNF 的代價:可能丟掉相依保持性
但分解不是免費的。BCNF 分解有一個你必須知道的取捨:它不保證 dependency preservation(相依保持)。
在上面的分解裡,原本的相依 (1) {student, course} -> instructor 跨越了兩張表——student 在右表、course 在左表、instructor 兩邊都有。要檢查這條相依有沒有被違反,你必須先 JOIN 回去才驗得了。換句話說,你無法只靠各表的本地約束(local constraint)來強制它。
這帶來一個形式化結論,值得記住:
任何關聯都能無損地(lossless-join)分解到 BCNF,但不一定能同時保持所有相依。3NF 則永遠能同時做到無損 + 相依保持。
這就是為什麼工業界的標準目標常常是 3NF 而非 BCNF——3NF 給你「兩全」,BCNF 在某些 schema 上逼你二選一。當相依保持比那一點點冗餘更重要時,停在 3NF 是經過深思的工程決策,不是偷懶。
跨過函數相依:多值相依與 4NF
現在回到開頭那張排班表的近親。函數相依處理的是「一個值決定另一個值」。但有些約束是「一個值決定一整組值」,而且這組值彼此獨立。這就是多值相依。
動手算一下:兩個獨立的多值欄位
假設我們要記錄每位老師「會教的科目」與「會的程式語言」,這兩件事互相獨立:
teacher_skills(teacher, subject, language)
Prof.Wang 會教 {資料庫, 作業系統},也會 {Python, Go}。如果硬塞進一張表,1NF(不可有多值欄位)會逼你做笛卡兒積:
teacher | subject | language
---------|---------|---------
Wang | 資料庫 | Python
Wang | 資料庫 | Go
Wang | 作業系統| Python
Wang | 作業系統| Go
科目 2 個、語言 2 個,就得寫 $2 \times 2 = 4$ 列。若 Wang 多會一種語言,你得新增 2 列(每個 subject 各一),否則表就不一致了——這就是多值相依造成的更新異常。
形式上,我們寫成兩條多值相依:
$$\text{teacher} \twoheadrightarrow \text{subject} \quad\text{且}\quad \text{teacher} \twoheadrightarrow \text{language}$$
符號 $\twoheadrightarrow$ 讀作「multidetermines(多值決定)」。它的精確含義是:給定一個 teacher,其 subject 的集合與 language 的集合相互獨立——表中必須出現所有組合的笛卡兒積。
4NF 的定義:對每個非平凡多值相依 $X \twoheadrightarrow Y$,$X$ 必須是超鍵。
上表中 teacher 不是超鍵(光憑老師不能決定某一列),所以違反 4NF。修法是把兩個獨立的多值事實拆開:
CREATE TABLE teacher_subjects (
teacher VARCHAR(64) NOT NULL,
subject VARCHAR(64) NOT NULL,
PRIMARY KEY (teacher, subject)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE teacher_languages (
teacher VARCHAR(64) NOT NULL,
language VARCHAR(64) NOT NULL,
PRIMARY KEY (teacher, language)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
現在新增一種語言只要 1 列。原表能用 JOIN 完美還原(lossless join):
SELECT s.teacher, s.subject, l.language
FROM teacher_subjects s
JOIN teacher_languages l ON s.teacher = l.teacher;
關鍵直覺:當你發現「一張表把兩件互相獨立的『一對多』事實塞在一起,導致筆數變成乘法」,那就是 4NF 在呼叫你。注意函數相依是多值相依的特例(若 $X \to Y$ 則 $X \twoheadrightarrow Y$),所以 4NF 嚴格強過 BCNF。
5NF 與連接相依:當分解成兩張表還不夠
4NF 把表拆成兩塊。但存在一種更刁鑽的情況:一張表的某個約束,只有把它拆成三張或更多張表、再全部 JOIN 回來才能正確表達。這需要 5NF(也叫 Project-Join Normal Form, PJNF)與連接相依(join dependency)。
回到開頭的排班場景,換個版本來體會:
supply(agent, company, product)
語意:agent 代理 company;company 生產 product;agent 銷售 product
而且有一條業務規則:
若 agent 代理 company、company 生產 product、且 agent 有賣 product,
則 agent 必然替 company 賣這個 product。
這條「三向循環」規則無法用任何單一的二元分解捕捉——拆成 (agent,company) + (company,product) 兩張表會丟資訊,拆成其他兩兩組合也一樣。它需要三張投影表,JOIN 三次才無損還原:
CREATE TABLE agent_company (
agent VARCHAR(64) NOT NULL,
company VARCHAR(64) NOT NULL,
PRIMARY KEY (agent, company)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE company_product (
company VARCHAR(64) NOT NULL,
product VARCHAR(64) NOT NULL,
PRIMARY KEY (company, product)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE agent_product (
agent VARCHAR(64) NOT NULL,
product VARCHAR(64) NOT NULL,
PRIMARY KEY (agent, product)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
還原時三表 JOIN:
SELECT ac.agent, ac.company, cp.product
FROM agent_company ac
JOIN company_product cp ON ac.company = cp.company
JOIN agent_product ap ON ap.agent = ac.agent AND ap.product = cp.product;
形式上,這是一條連接相依 $\bowtie\{(A,C),(C,P),(A,P)\}$ 成立。5NF 的定義:每個非平凡連接相依都必須由候選鍵蘊含(implied by the candidate keys)。
為什麼合在一張表會出錯
若把三個事實塞回一張 supply 表,會發生假元組(spurious tuple)問題。假設我們已知:
agent | company | product
------|---------|--------
Smith | Honda | car
Smith | Toyota | car
Jones | Honda | car
現在 Jones 開始代理 Toyota(Toyota 也生產 car,Jones 也賣 car)。依照那條業務規則,系統必須自動推出「Jones 替 Toyota 賣 car」這一列。在合併表裡你得手動補;漏補就違反規則,補了又是冗餘。三表分解則讓這個推論「免費」由 JOIN 自然產生——這正是 5NF 的價值:把隱含的組合邏輯交給關聯代數,而非交給容易出錯的人工維護。
5NF 在實務中相當罕見(多數 schema 在 4NF 之後就已是 5NF),但理解它能讓你辨認出那種「怎麼拆兩張都不對」的真實多對多對多場景。
反正規化:什麼時候該「故意走回頭路」
學了五個範式,你可能以為終點就是把所有表推到最高範式。錯。正規化消除冗餘、保障一致性,但代價是查詢時要 JOIN,而 JOIN 是有成本的。
考慮 Uedu 課程列表頁,要顯示每門課的「已選人數」。完全正規化的做法是即時 COUNT:
SELECT c.id, c.class_name, COUNT(e.user_id) AS enrolled
FROM classrooms c
LEFT JOIN enrollments e ON e.classroom_id = c.id
GROUP BY c.id, c.class_name;
若每次載入首頁都對百萬列的 enrollments 做聚合,成本是 $O(n)$ 級的掃描。反正規化的做法是在 classrooms 加一個 enrolled_count 快取欄位,於選課/退選時用觸發器或應用層維護:
ALTER TABLE classrooms ADD COLUMN enrolled_count INT NOT NULL DEFAULT 0;
-- 選課時(在 transaction 內,與 INSERT enrollment 同步)
UPDATE classrooms SET enrolled_count = enrolled_count + 1 WHERE id = %s;
查詢變成 $O(1)$ 讀取。代價是:你引入了一個衍生資料(derived data),它可能與真實計數不同步——這正是正規化原本要消滅的冗餘。
反正規化的判準是工程權衡,不是對錯:
傾向反正規化,當:
- 讀遠多於寫(首頁、儀表板、報表)
- JOIN/聚合成本經實測成為瓶頸
- 衍生欄位能用 transaction 或 trigger 原子維護
傾向保持高範式,當:
- 寫多讀少、資料一致性是硬需求(金流、成績、同意紀錄)
- 冗餘欄位的同步邏輯複雜、容易出 bug
重點是順序:先正規化到 3NF/BCNF 想清楚正確的資料模型,再為了實測出來的效能瓶頸有意識地、有文件記錄地反正規化。沒有先正規化就直接堆冗餘欄位,那不是反正規化,那是還沒設計。
重點回顧
- BCNF 不是「更嚴格的 3NF」,而是換了判準:它取消了 3NF 對 prime attribute 的豁免,要求每個函數相依的左邊都是超鍵。
- BCNF 與相依保持可能不可兼得:任何 schema 都能無損分解到 BCNF,但不保證保持所有相依;3NF 則永遠能同時做到無損 + 相依保持,這是工業界常以 3NF 為目標的原因。
- 4NF 處理多值相依:當一張表把兩件互相獨立的「一對多」事實塞在一起、使筆數變成笛卡兒積時,就該拆表。函數相依是多值相依的特例,故 4NF 強過 BCNF。
- 5NF 處理連接相依:少數場景的約束只能靠拆成三張以上的表、再 JOIN 還原來表達,否則會產生假元組;實務罕見但概念重要。
- 反正規化是有意識的權衡:先把模型正規化想對,再針對實測的讀取瓶頸引入受控的衍生資料,而非跳過設計直接堆冗餘。
深入探討(研究所視角)
正規化的形式骨架:相依的閉包與蘊含。 整個正規化理論建立在「給定一組相依,還能推出哪些相依」之上。對函數相依,這由 Armstrong 公理(自反 reflexivity、增廣 augmentation、遞移 transitivity)完整刻畫,並能在多項式時間內計算屬性閉包 $X^+$,進而判定候選鍵與範式。多值相依則有自己的一組推論規則(含「互補律 complementation」:$X \twoheadrightarrow Y \Rightarrow X \twoheadrightarrow (R - X - Y)$),與函數相依規則合在一起形成更豐富的蘊含系統。連接相依的蘊含問題則更難——一般情況下其推理沒有有限的完備公理化,這也是 5NF 在理論上「終點站」地位的來源之一。
分解的兩個正確性條件,能用關聯代數精確檢驗。 無損連接(lossless-join)可由 chase 演算法判定:把分解後各投影 JOIN 回去,當且僅當還原出的關聯恰等於原關聯時才無損。對二元分解,有一個漂亮的充分條件:分解 $R$ 為 $R_1, R_2$ 無損,若 $(R_1 \cap R_2) \to R_1$ 或 $(R_1 \cap R_2) \to R_2$ 成立——也就是共同欄位是其中一邊的鍵。相依保持則檢查各分解片段上相依的投影之聯集,其閉包是否等於原相依集的閉包。把這兩個條件形式化後,「為何 BCNF 可能犧牲相依保持、3NF 不會」就從「背結論」變成「可證明的定理」。
更高的範式與時序/NoSQL 的張力。 還有 DK/NF(Domain-Key Normal Form),它斷言:一個關聯若其所有約束都是定義域約束與鍵約束的邏輯後果,則它不可能有任何插入或刪除異常——這是正規化的「理想終點」,但通常無法達成或無法自動化。另一方面,當代許多系統(document store、寬列儲存、事件溯源 event sourcing)刻意採用反正規化或無 schema 設計,把一致性責任從資料庫引擎搬到應用層或最終一致性(eventual consistency)模型。這不是否定正規化理論,而是把它的洞見——「哪裡有冗餘、哪裡會有異常」——當成風險地圖,再依 CAP 取捨與讀寫模式做出明確的工程選擇。讀懂 4NF/5NF 的人,正是最有資格決定「何時、何處、如何」安全地偏離它的人。
延伸閱讀方向:Codd 的原始正規化系列論文、Fagin 對 4NF(多值相依)與 PJNF(5NF)的奠基工作、以及資料庫教科書(如 Silberschatz 或 Garcia-Molina)中關於 chase 演算法與相依理論的章節。