為什麼要正規化:三種更新異常
假設有一張「訂單」表,把所有東西放進去:
| order_id | user_name | user_email | product_name | product_price | quantity |
|---|---|---|---|---|---|
| 1 | Alice | [email protected] | Laptop | 1200 | 1 |
| 2 | Alice | [email protected] | Mouse | 30 | 2 |
| 3 | Bob | [email protected] | Laptop | 1200 | 1 |
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_city、customer_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 記錄為什麼這個欄位是反正規化的。