什麼是正規化(What is Normalization)
這篇正規化教學將帶你認識資料庫正規化的核心概念與實作方式。正規化(Normalization)是資料庫設計中最重要的基礎觀念之一,目的是透過一系列規則,將資料表結構調整到最佳狀態,消除資料冗餘(Redundancy)與更新異常(Update Anomaly)。
透過正規化,我們可以確保資料庫中的每筆資料只儲存一次,避免因為重複儲存而導致的資料不一致問題。正規化通常會經過多個階段,最常見的是第一正規化(1NF)、第二正規化(2NF)和第三正規化(3NF),每個階段都有明確的規則與目標。
為什麼需要正規化(Why Normalization)
在設計資料庫時,如果沒有經過正規化,資料表可能會出現以下問題:
| 問題類型 | 說明 |
|---|---|
| 資料冗餘(Redundancy) | 相同的資料重複儲存在多個地方,浪費儲存空間 |
| 插入異常(Insert Anomaly) | 無法單獨新增某些資料,必須連帶新增其他不相關的資料 |
| 更新異常(Update Anomaly) | 修改一筆資料時,必須同時更新多個地方,容易遺漏造成不一致 |
| 刪除異常(Delete Anomaly) | 刪除某筆資料時,可能連帶刪除其他仍然需要的資料 |
未正規化的資料表範例(Unnormalized Table Example)
假設我們有一張「訂單資料表」,記錄了客戶的訂單資訊:
| 訂單編號 | 客戶名稱 | 客戶地址 | 商品名稱 | 商品單價 |
|---|---|---|---|---|
| 001 | 王小明 | 台北市信義區 | 鍵盤, 滑鼠 | 1500, 500 |
| 002 | 李小華 | 台中市西屯區 | 螢幕 | 8000 |
| 003 | 王小明 | 台北市信義區 | 耳機 | 1200 |
這張資料表存在明顯的問題:「王小明」的名稱和地址重複出現了兩次,而且「商品名稱」和「商品單價」欄位中包含了多個值(用逗號分隔)。這些問題正是正規化要解決的。
第一正規化 1NF(First Normal Form)
第一正規化(1NF)的規則很簡單:資料表中的每個欄位都必須是不可再分割的原子值(Atomic Value),也就是說每個欄位只能存放單一值,不能包含重複群組或多值資料。
1NF 規則
要符合第一正規化,資料表必須滿足以下條件:每個欄位只包含一個值(不能有逗號分隔的多值)、每一列都是唯一的(通常透過主鍵達成)、欄位的順序不影響資料的意義。
1NF 範例(Example)
將上面未正規化的資料表轉換為符合 1NF 的格式,我們需要把多值欄位拆開,讓每個欄位只包含一個值:
| 訂單編號 | 客戶名稱 | 客戶地址 | 商品名稱 | 商品單價 |
|---|---|---|---|---|
| 001 | 王小明 | 台北市信義區 | 鍵盤 | 1500 |
| 001 | 王小明 | 台北市信義區 | 滑鼠 | 500 |
| 002 | 李小華 | 台中市西屯區 | 螢幕 | 8000 |
| 003 | 王小明 | 台北市信義區 | 耳機 | 1200 |
現在每個欄位都只包含單一值,資料表已經符合 1NF。但是我們可以發現,「王小明」和「台北市信義區」仍然重複出現,這就是接下來 2NF 要處理的問題。
第二正規化 2NF(Second Normal Form)
第二正規化(2NF)建立在 1NF 的基礎上,額外要求:所有非鍵欄位(Non-key Column)都必須完全依賴於主鍵(Primary Key),不能只依賴主鍵的一部分。這個規則主要針對複合主鍵(Composite Primary Key)的情況。
2NF 規則
要符合第二正規化,資料表必須先符合 1NF,並且消除部分相依(Partial Dependency)。所謂部分相依,是指非鍵欄位只依賴複合主鍵中的某一部分,而非整個主鍵。
2NF 範例(Example)
在上面的 1NF 資料表中,假設主鍵是(訂單編號, 商品名稱),我們可以發現「客戶名稱」和「客戶地址」只依賴「訂單編號」,而不依賴「商品名稱」。這就是部分相依,需要透過拆分資料表來消除。
將資料表拆分為兩張表:
訂單資料表(orders):
| 訂單編號(PK) | 客戶名稱 | 客戶地址 |
|---|---|---|
| 001 | 王小明 | 台北市信義區 |
| 002 | 李小華 | 台中市西屯區 |
| 003 | 王小明 | 台北市信義區 |
訂單明細資料表(order_items):
| 訂單編號(FK) | 商品名稱 | 商品單價 |
|---|---|---|
| 001 | 鍵盤 | 1500 |
| 001 | 滑鼠 | 500 |
| 002 | 螢幕 | 8000 |
| 003 | 耳機 | 1200 |
透過拆分資料表,每個非鍵欄位都完全依賴於各自資料表的主鍵,消除了部分相依的問題。但是「王小明」的名稱和地址在訂單資料表中仍然重複出現,這是 3NF 要解決的。
第三正規化 3NF(Third Normal Form)
第三正規化(3NF)建立在 2NF 的基礎上,額外要求:所有非鍵欄位都必須直接依賴於主鍵,不能透過其他非鍵欄位間接依賴主鍵。也就是說,要消除遞移相依(Transitive Dependency)。
3NF 規則
要符合第三正規化,資料表必須先符合 2NF,並且消除遞移相依。所謂遞移相依,是指非鍵欄位 A 依賴於非鍵欄位 B,而 B 依賴於主鍵,形成「主鍵 → B → A」的間接相依關係。
3NF 範例(Example)
在 2NF 的訂單資料表中,「客戶地址」依賴於「客戶名稱」,而「客戶名稱」依賴於「訂單編號」。這就形成了遞移相依:訂單編號 → 客戶名稱 → 客戶地址。我們需要將客戶資訊獨立成一張資料表。
拆分後的資料表結構如下:
客戶資料表(customers):
| 客戶編號(PK) | 客戶名稱 | 客戶地址 |
|---|---|---|
| C001 | 王小明 | 台北市信義區 |
| C002 | 李小華 | 台中市西屯區 |
訂單資料表(orders):
| 訂單編號(PK) | 客戶編號(FK) |
|---|---|
| 001 | C001 |
| 002 | C002 |
| 003 | C001 |
訂單明細資料表(order_items):
| 訂單編號(FK) | 商品名稱 | 商品單價 |
|---|---|---|
| 001 | 鍵盤 | 1500 |
| 001 | 滑鼠 | 500 |
| 002 | 螢幕 | 8000 |
| 003 | 耳機 | 1200 |
現在每張資料表的非鍵欄位都直接依賴於主鍵,沒有遞移相依的問題。客戶資料只需要儲存一次,如果王小明的地址變更了,只需要修改客戶資料表中的一筆紀錄即可。
用 SQL 實作正規化(SQL Implementation)
了解正規化的概念後,讓我們用 SQL 來實際建立符合 3NF 的資料表結構:
-- 建立客戶資料表
CREATE TABLE customers (
customer_id VARCHAR(10) PRIMARY KEY,
customer_name VARCHAR(50) NOT NULL,
customer_address VARCHAR(100)
);
-- 建立訂單資料表
CREATE TABLE orders (
order_id VARCHAR(10) PRIMARY KEY,
customer_id VARCHAR(10),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
-- 建立訂單明細資料表
CREATE TABLE order_items (
order_id VARCHAR(10),
product_name VARCHAR(50),
unit_price INT,
PRIMARY KEY (order_id, product_name),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
新增測試資料:
-- 新增客戶資料
INSERT INTO customers VALUES ('C001', '王小明', '台北市信義區');
INSERT INTO customers VALUES ('C002', '李小華', '台中市西屯區');
-- 新增訂單資料
INSERT INTO orders VALUES ('001', 'C001');
INSERT INTO orders VALUES ('002', 'C002');
INSERT INTO orders VALUES ('003', 'C001');
-- 新增訂單明細
INSERT INTO order_items VALUES ('001', '鍵盤', 1500);
INSERT INTO order_items VALUES ('001', '滑鼠', 500);
INSERT INTO order_items VALUES ('002', '螢幕', 8000);
INSERT INTO order_items VALUES ('003', '耳機', 1200);
查詢完整的訂單資訊,可以透過 SQL JOIN 將三張資料表合併:
SELECT
o.order_id AS 訂單編號,
c.customer_name AS 客戶名稱,
c.customer_address AS 客戶地址,
oi.product_name AS 商品名稱,
oi.unit_price AS 商品單價
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
ORDER BY o.order_id;
查詢結果:
| 訂單編號 | 客戶名稱 | 客戶地址 | 商品名稱 | 商品單價 |
|---|---|---|---|---|
| 001 | 王小明 | 台北市信義區 | 鍵盤 | 1500 |
| 001 | 王小明 | 台北市信義區 | 滑鼠 | 500 |
| 002 | 李小華 | 台中市西屯區 | 螢幕 | 8000 |
| 003 | 王小明 | 台北市信義區 | 耳機 | 1200 |
雖然查詢結果看起來和未正規化時一樣,但底層的資料儲存結構已經大不相同。每個客戶的資料只儲存一次,修改客戶地址只需要更新一筆紀錄。
正規化整理表(Normalization Summary)
| 正規化階段 | 規則 | 消除的問題 |
|---|---|---|
| 1NF | 每個欄位只包含原子值,不可再分割 | 重複群組、多值欄位 |
| 2NF | 符合 1NF,且消除部分相依 | 非鍵欄位對複合主鍵的部分相依 |
| 3NF | 符合 2NF,且消除遞移相依 | 非鍵欄位之間的間接相依 |
什麼時候不該正規化(Denormalization)
正規化雖然能消除資料冗餘,但過度正規化可能導致查詢時需要大量的 JOIN 操作,影響效能。在某些情況下,我們會刻意保留一些冗餘資料來提升查詢速度,這就是所謂的反正規化(Denormalization)。
常見適合反正規化的場景包括:讀取頻率遠高於寫入頻率的報表系統、需要高效能查詢的資料倉儲(Data Warehouse)、以及對即時性要求極高的應用程式。在實務開發中,通常會在 3NF 的基礎上,根據實際的查詢需求適度進行反正規化。
延伸閱讀
- SQL 教學 — 完整 SQL 語法教學
- CREATE TABLE — 建立資料表
- SQL Constraints — 資料約束教學
- SQL JOIN — 多表連結查詢
- SQL SELECT — 資料查詢語法
- SQL INSERT INTO — 新增資料教學
- SQL UPDATE — 更新資料教學
- SQL DELETE FROM — 刪除資料教學
- SQL INDEX — 索引教學
- SQL VIEW — 檢視表教學