正規化(1NF、2NF、3NF)

什麼是正規化(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)
001C001
002C002
003C001

訂單明細資料表(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 的基礎上,根據實際的查詢需求適度進行反正規化。

延伸閱讀