為什麼要正規化:三種更新異常

假設有一張「訂單」表,把所有東西放進去:

order_iduser_nameuser_emailproduct_nameproduct_pricequantity
1Alice[email protected]Laptop12001
2Alice[email protected]Mouse302
3Bob[email protected]Laptop12001

Insertion Anomaly(插入異常):要新增一個用戶,但他還沒有訂單——因為 user_email 沒有獨立的表,沒有辦法插入只有 user 的 row。

Update Anomaly(更新異常):Alice 換 email,要更新所有她的訂單 row——如果改了第 1 筆但漏了第 2 筆,資料就不一致了。

Deletion Anomaly(刪除異常):刪除 order_id=3,同時刪掉了 Bob 這個用戶的所有資訊。

正規化就是消除這三種異常。


1NF(第一正規化)

規則:每個欄位只能有原子值(Atomic Value),不能有集合或重複群組。

違反 1NF 的設計

orders: id | product_names     | prices
        1  | "Laptop, Mouse"   | "1200, 30"

修法:把每個值拆出來成獨立的 row,或拆成獨立的關聯表。


2NF(第二正規化)

前提:已滿足 1NF,且有複合主鍵(Composite Primary Key)。

規則:每個非主鍵欄位必須完全依賴於整個主鍵,不能只依賴主鍵的一部分。

違反 2NF 的設計(主鍵是 (order_id, product_id)):

order_items: order_id | product_id | product_name | quantity

product_name 只依賴 product_id,不依賴 order_id——這是部分依賴。

修法:把 product_name 移到獨立的 products 表。


3NF(第三正規化)

規則:每個非主鍵欄位必須直接依賴於主鍵,不能透過另一個非主鍵欄位依賴(傳遞依賴)。

違反 3NF 的設計(主鍵是 order_id):

orders: order_id | customer_id | customer_city | customer_country

customer_city 依賴 customer_id,而不是直接依賴 order_id——這是傳遞依賴。

修法:把 customer_citycustomer_country 移到 customers 表。


BCNF(Boyce-Codd Normal Form)

3NF 的加強版,處理有多個候選鍵的邊界情況。實際上大多數設計達到 3NF 就夠了,BCNF 是更理論化的工具。


正規化到什麼程度?

大多數業務系統目標是 3NF——消除明顯的資料重複和更新異常。

不需要強求的情況:

  • 報表 / 分析查詢的表(可以反正規化提高讀取效能)
  • 歷史快照(訂單裡的 unit_price 故意不正規化,因為它是時間點的記錄)
  • 緩存表(user_order_count 是計算值,重複存儲是為了讀取效能)

何時反正規化(Denormalization)

正規化的代價是讀取需要 JOIN。高頻的複雜 JOIN 在大資料量下可能成為瓶頸。

反正規化的合理場景

  • 讀寫比超高(讀取 >>>>> 寫入),JOIN 是瓶頸
  • 明確的查詢模式(這個欄位組合 99% 的查詢都需要)
  • 可以接受讀到輕微過時的資料(eventual consistency)

反正規化的代價

  • 更新異常風險上升,需要應用層維護一致性
  • 更多的 storage
  • 寫入邏輯更複雜

反正規化是有意識的選擇,不是「設計懶得拆」。先正規化,測到性能問題後再考慮反正規化,並且用 comment 記錄為什麼這個欄位是反正規化的。