資料庫正規化入門(上):從資料異常到第一、第二正規化
從一張塞滿重複資料的訂單表出發,搞懂第一、第二正規化到底在解決什麼問題,並學會用複合主鍵拆表消除重複依賴。
為什麼要正規化?
假設你在做一個線上訂購系統,一開始隨手就把所有欄位塞進同一張表:
| order_id | customer_name | customer_email | product_name | product_price | quantity |
|---|---|---|---|---|---|
| 1 | 小明 | ming@example.com | 鍵盤 | 1200 | 1 |
| 2 | 小明 | ming@example.com | 滑鼠 | 500 | 2 |
| 3 | 小美 | mei@example.com | 鍵盤 | 1200 | 1 |
看起來能動,但只要系統跑起來,很快就會出問題:
- 更新異常 (Update Anomaly):小明改了 email,你得記得把「所有」他買過的訂單都改一遍,漏掉一筆,資料就不一致了。
- 插入異常 (Insertion Anomaly):想先建立一個還沒下單的新客戶,卻沒有 order_id 可以掛,資料塞不進去。
- 刪除異常 (Deletion Anomaly):小美只買過一次東西,如果那筆訂單被刪除,她的 email 也跟著整個消失,你其實沒有「刪訂單」的意圖,卻連客戶資料都一起弄丟了。
這些問題的根源都一樣:同一份資訊(客戶 email、商品價格)在表格裡重複出現太多次。正規化 (Normalization) 就是一套逐步拆表的方法,目標是讓「每一份事實只存在一個地方」,從根本上避免這些異常。
正規化分成好幾個階段,每個階段叫做一個「正規形式 (Normal Form)」,常見的是 1NF、2NF、3NF,而且它們是遞進的關係 —— 要滿足 3NF,必須先滿足 2NF;要滿足 2NF,必須先滿足 1NF。這篇(上)先從最基礎的 1NF、2NF 開始,下篇再處理稍微燒腦一點的 3NF,以及「正規化是不是做越多越好」的取捨問題。
第一正規化 (1NF):每個欄位都是不可分割的值
規則:每一個欄位都必須是單一值,不能是一組值或一個清單。
來看一個違反 1NF 的例子:
| order_id | customer_name | products |
|---|---|---|
| 1 | 小明 | 鍵盤, 滑鼠 |
| 2 | 小美 | 鍵盤 |
products 欄位塞了一串用逗號分隔的商品,這就是典型的「重複群組 (Repeating Group)」。問題是:你沒辦法直接用 SQL 查出「誰買過滑鼠」,也很難統計每個商品賣了幾件 —— 資料庫沒辦法理解一個欄位裡藏了兩筆資訊。
拆成 1NF 後,一筆訂單、一個商品各佔一列:
| order_id | customer_name | product |
|---|---|---|
| 1 | 小明 | 鍵盤 |
| 1 | 小明 | 滑鼠 |
| 2 | 小美 | 鍵盤 |
現在每個欄位都是原子值,可以正常查詢、篩選、加總了。但你應該也發現了:order_id 已經不能單獨當主鍵,因為它會重複出現 —— 這裡的主鍵要換成 (order_id, product) 這種複合主鍵 (Composite Key)。這一點,正是 2NF 要處理的問題的起點。
第二正規化 (2NF):消除「部分依賴」
規則:必須先符合 1NF,而且每個非主鍵欄位都要完全依賴整個主鍵,不能只依賴主鍵的一部分。
2NF 只在主鍵是「複合主鍵」時才有意義 —— 如果主鍵只有一個欄位,2NF 自動成立。
延續上面的例子,把商品價格也加進去,主鍵是 (order_id, product):
| order_id | product | customer_name | product_price | quantity |
|---|---|---|---|---|
| 1 | 鍵盤 | 小明 | 1200 | 1 |
| 1 | 滑鼠 | 小明 | 500 | 2 |
| 2 | 鍵盤 | 小美 | 1200 | 1 |
檢查每個非主鍵欄位依賴的是主鍵的哪一部分:
customer_name只跟order_id有關 —— 只依賴主鍵的一部分。product_price只跟product有關 —— 也只依賴主鍵的一部分。quantity需要同時知道order_id和product才有意義(這一單買了這個商品幾件)—— 這才是完全依賴整個主鍵。
customer_name、product_price 都只部分依賴主鍵,這就是「部分依賴 (Partial Dependency)」,違反了 2NF。後果跟一開始一樣:product_price 重複存了兩次,鍵盤漲價的話,兩列都要記得改。
拆法是把「只依賴主鍵一部分」的欄位,連同它依賴的那部分主鍵,獨立成自己的表:
-- 訂單表:customer_name 只依賴 order_id
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_name VARCHAR(50)
);
-- 商品表:product_price 只依賴 product
CREATE TABLE products (
product VARCHAR(50) PRIMARY KEY,
product_price INT
);
-- 訂單明細表:quantity 需要 order_id + product 才有意義,保留複合主鍵
CREATE TABLE order_items (
order_id INT,
product VARCHAR(50),
quantity INT,
PRIMARY KEY (order_id, product),
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product) REFERENCES products(product)
);
現在鍵盤漲價,只要改 products 裡的一列;客戶改名字,也只要改 orders 裡的一列。
小結
到這裡,我們解決了兩個問題:
- 1NF:一個欄位不能塞多筆資訊,拆成一列一個原子值。
- 2NF:複合主鍵底下,非主鍵欄位不能只依賴主鍵的一部分,要完全依賴整個主鍵。
下篇會接著處理一個更隱蔽的問題 —— 就算每個非主鍵欄位都好好依賴著主鍵,欄位跟欄位之間卻可能偷偷互相依賴,這就是 3NF 要解決的「遞移依賴」。
面試常見問題
Q1. 什麼是資料庫正規化?為什麼要做?
把資料表拆分成多張較小的表,讓每一份事實只存在一個地方,藉此消除資料重複,避免更新異常、插入異常、刪除異常。代價是查詢時需要更多 JOIN。
Q2. 1NF 的具體要求是什麼?怎麼判斷一張表違反 1NF?
每個欄位都必須是不可分割的原子值。如果一個欄位裡塞了用逗號分隔的清單、或是一組重複出現的欄位(例如 product1、product2、product3),就代表違反了 1NF,應該把清單拆成多列。
Q3. 什麼是複合主鍵 (Composite Key)?什麼情況下會用到?
由兩個以上欄位組成的主鍵,單一欄位無法唯一識別一列時就需要它。例如訂單明細表,單靠 order_id 無法識別「這一單裡的哪個商品」,要搭配 product 才能唯一定位一列,所以主鍵是 (order_id, product)。
Q4. 什麼是部分依賴 (Partial Dependency)?
在複合主鍵的情況下,某個非主鍵欄位只依賴主鍵的「一部分」,而不是整個主鍵。例如主鍵是 (order_id, product),但 product_price 其實只跟 product 有關,跟 order_id 無關 —— 這就是部分依賴,會導致 product_price 在每個包含該商品的訂單裡重複存一次。
Q5. 2NF 和 1NF 的差異是什麼?
1NF 只管欄位裡的值是不是原子值,不處理欄位之間的依賴關係。2NF 建立在 1NF 之上,進一步要求(在複合主鍵的情況下)每個非主鍵欄位都要完全依賴整個主鍵,消除部分依賴。如果主鍵只有單一欄位,2NF 會自動成立。
Q6. 如果主鍵只有一個欄位(不是複合主鍵),還需要考慮 2NF 嗎?
不需要另外處理。2NF 的問題(部分依賴)只會發生在複合主鍵底下 —— 主鍵只有一欄時,任何非主鍵欄位要嘛依賴這個唯一的欄位,要嘛就是不依賴,不存在「依賴主鍵一部分」這種情況,所以只要符合 1NF,自動就符合 2NF。