Skip to content

[Daily Questions Challenge 20]
資料庫正規化(Database Normalization)

[Daily Questions Challenge 20] 資料庫正規化(Database Normalization)

為什麼需要正規化

資料庫正規化(Database Normalization)是設計關聯式資料庫(Relational Database)時,用來消除資料冗餘與異常的一套方法論。

假設學校要記錄學生的選課狀況,最直覺的做法可能是設計一張這樣的表:

StudentIDStudentNameCourses
S01小明資料庫設計, 演算法
S02小華資料庫設計

這個設計看起來簡單,但會帶來幾個問題:

  • 更新異常:如果小明的名稱需要修改,必須找出他出現的每一筆資料並逐一更新。
  • 插入異常:想新增一門課,但還沒有學生選課,就無法插入這筆資料。
  • 刪除異常:刪除小華的選課紀錄,可能同時失去關於小華本人的資訊。
  • 查詢困難:Courses 欄位存放逗號分隔的字串,無法直接用 SQL 篩選特定課程的學生。

正規化的目的,就是透過拆分資料表,系統性地解決這些問題。

第一正規化(1NF)

規則:每個欄位只能存放原子值(Atomic Value),不允許多值欄位或重複的欄位群組。

原始設計中,Courses 欄位同時存放了多門課,違反了 1NF 的要求。修正方式是將每門課拆成獨立的一行:

StudentIDStudentNameCourseIDCourseNameGrade
S01小明C01資料庫設計A
S01小明C02演算法B
S02小華C01資料庫設計C

這張表的主鍵(Primary Key)是 (StudentID, CourseID) 的組合,因為兩個欄位合在一起才能唯一識別一筆資料。

不過,這樣的設計仍有問題:StudentName 只跟 StudentID 有關,CourseName 只跟 CourseID 有關,它們都只依賴主鍵的一部分,而不是整個組合——這是第二正規化要解決的問題。

第二正規化(2NF)

規則:必須先滿足 1NF,且所有非主鍵欄位必須完全相依於整個主鍵,不允許部分相依(Partial Dependency)。

上面的表中有兩個部分相依:

  • StudentName 只依賴 StudentID,與 CourseID 無關
  • CourseName 只依賴 CourseID,與 StudentID 無關

修正方式是將這些部分相依的欄位拆出去,形成獨立的資料表:

Students 表

StudentIDStudentName
S01小明
S02小華

Courses 表

CourseIDCourseName
C01資料庫設計
C02演算法

Enrollments 表

StudentIDCourseIDGrade
S01C01A
S01C02B
S02C01C

現在每張表的非主鍵欄位都完整地依賴自己的主鍵,達到 2NF。

但如果 Courses 表進一步加上授課老師的資訊:

CourseIDCourseNameInstructorIDInstructorDept
C01資料庫設計T01資訊工程系
C02演算法T01資訊工程系

InstructorDept 並非直接依賴 CourseID,而是透過 InstructorID 間接相依——這是遞移相依,也是第三正規化要解決的問題。

第三正規化(3NF)

規則:必須先滿足 2NF,且所有非主鍵欄位必須直接相依於主鍵,不允許透過其他非主鍵欄位間接相依(Transitive Dependency)。

上例中的相依關係是:

CourseID → InstructorID → InstructorDept

InstructorDept 透過 InstructorID 間接依賴 CourseID,形成遞移相依。修正方式是將老師資訊拆成獨立的資料表:

Courses 表

CourseIDCourseNameInstructorID
C01資料庫設計T01
C02演算法T01

Instructors 表

InstructorIDInstructorDept
T01資訊工程系

每個非主鍵欄位現在都直接依賴自己所在表的主鍵,達到 3NF。

在實務上,3NF 是多數生產系統的設計目標。達到 3NF 之後,資料冗餘大幅減少,更新資料時只需修改一個地方,插入與刪除異常也隨之消失。

反正規化(Denormalization)的取捨

正規化改善了資料一致性,但也有代價:查詢時需要更多 JOIN。

當系統面臨讀取效能瓶頸時,有時會刻意做出反正規化(Denormalization)的選擇,透過引入冗餘來換取查詢速度。

舉例來說,一個分析報表系統需要頻繁地查詢學生姓名、課程名稱與成績。在嚴格正規化的設計下,每次查詢都需要 JOIN Students、Courses、Instructors 三張表。當資料量龐大、查詢頻率極高時,這些 JOIN 會成為效能瓶頸。

常見的反正規化手法包括:

  • 合併資料表:將 Enrollments 直接存入 StudentName、CourseName,減少查詢時的 JOIN 次數。
  • 新增冗餘欄位:在訂單表中直接存放商品名稱,即使商品資訊日後異動,歷史訂單仍保有當時的快照。
  • 預計算欄位:將複雜的聚合結果(如學生已修學分數)預先計算並存放,減少查詢時的運算量。

正規化與反正規化的取捨可以這樣對比:

正規化反正規化
資料一致性高(單一來源)較低(多處需同步)
寫入複雜度高(需同步更新多處)
讀取效能較慢(JOIN 多)較快(JOIN 少)
適合情境OLTP(高寫入頻率)OLAP、報表、快取層

反正規化不等於設計不良,而是根據系統的讀寫比例與一致性需求,做出的刻意取捨。

總結

資料庫正規化是一個循序漸進的過程:

  • 1NF:消除多值欄位,讓每個欄位只存單一原子值。
  • 2NF:消除部分相依,確保非主鍵欄位完整依賴整個主鍵。
  • 3NF:消除遞移相依,確保非主鍵欄位直接依賴主鍵,而非透過其他非主鍵欄位間接相依。

三個階段完成後,資料冗餘大幅降低,更新與刪除異常也隨之消失。面試時能說清楚各階段解決的具體問題,並補充反正規化的取捨,是這道題目的完整回答。

參考