Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

資料模型與正規化

當 BCNF 還不夠:4NF、5NF 與反正規化的工程取捨

從多值相依到連接相依,看見三正規化之上的隱藏異常,並學會何時該有意識地走回頭路

當「第三正規化」還不夠:一張排班表如何同時滿足、又同時違反兩個範式?

你已經會把訂單拆成 ordersorder_items,會用外鍵串起 studentsenrollments,也背得出「每個非鍵欄位都要完全相依於主鍵」。但請看這張看似無害的醫院排班表:

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 演算法與相依理論的章節。

AI 共讀助教正在陪你讀:當 BCNF 還不夠:4NF、5NF 與反正規化的工程取捨
嗨!我是這篇文章的共讀助教,只根據〈當 BCNF 還不夠:4NF、5NF 與反正規化的工程取捨〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。