上一篇文章我們學到:好的資料庫架構要避免重複,方法是用「關聯」讓每筆資料只存在一個地方。
但實際設計的時候,你會遇到一個問題:怎麼判斷哪些欄位該放在同一個表單?哪些該分開?
這篇文章會用一個訂房網站的例子,帶你一步步學會「拆表單、刪欄位」的技巧,這個技巧背後的原則叫做第二正規化形式(Second Normal Form)。
把所有欄位放在同一張表單
假設我們要設計一個飯店訂房網站,需要有一張「訂房」表單來記錄訂房資料。
一開始,你可能會想把所有需要的資訊都放進來:
| 會員 | 會員信箱 | 飯店名稱 | 飯店電話 | 房型 | 單價 | 入住天數 | 總價 |
|---|---|---|---|---|---|---|---|
| David | david@mail.com | 台北大飯店 | 02-1234-5678 | 雙人房 | 3000 | 2 | 6000 |
| David | david@mail.com | 高雄大飯店 | 07-8765-4321 | 單人房 | 2000 | 3 | 6000 |
| Emily | emily@mail.com | 台北大飯店 | 02-1234-5678 | 總統套房 | 10000 | 1 | 10000 |
這張表看起來很方便,櫃檯人員需要的資訊一目瞭然,會員要查自己的訂房紀錄也很清楚。
包山包海,什麼都有,感覺很不錯?
修改資料時的重複問題
這張表在「查詢」的時候很方便,但在「修改」的時候就會出問題。
會員改信箱要改很多次
假設 David 已經在網站上訂了 5 次房,現在他要改信箱。
我們來看一下訂房表單上的資料:
| 會員 | 會員信箱 | 飯店名稱 | … |
|---|---|---|---|
| David | david@mail.com | 台北大飯店 | … |
| David | david@mail.com | 高雄大飯店 | … |
| David | david@mail.com | 台中大飯店 | … |
| David | david@mail.com | 台南大飯店 | … |
| David | david@mail.com | 花蓮大飯店 | … |
你會發現:David 的信箱在這張表上出現了 5 次。
所以當 David 要把信箱從 david@mail.com 改成 david.new@mail.com 的時候,你要改 5 次。
這很奇怪,因為「改信箱」跟「訂房」根本沒有關係。會員改信箱是會員自己的事,為什麼訂房表單要跟著改?
而且如果你漏改了其中一筆,就會變成:
| 會員 | 會員信箱 | 飯店名稱 | … |
|---|---|---|---|
| David | david.new@mail.com | 台北大飯店 | … |
| David | david.new@mail.com | 高雄大飯店 | … |
| David | david@mail.com | 台中大飯店 | … |
| David | david.new@mail.com | 台南大飯店 | … |
| David | david.new@mail.com | 花蓮大飯店 | … |
同一個 David,有兩種不同的信箱,到底哪個才是對的?這就是「資料不一致」。
飯店改電話要改更多次
如果台北大飯店要改電話呢?問題更嚴重。
假設台北大飯店很熱門,已經被訂了 100 次。那這張訂房表單上就有 100 筆資料的「飯店電話」欄位寫著台北大飯店的電話。
現在台北大飯店要把電話從 02-1234-5678 改成 02-1111-2222,你就要改 100 次。
飯店改電話跟訂房有什麼關係?沒有關係。但因為我們把電話放在訂房表單裡,所以每一筆訂房資料都存了一份電話,改一次電話就要改 100 次。
如果這間飯店被訂了 10,000 次呢?你就要改 10,000 次。
這就是資料重複造成的問題:同樣的資料存在太多地方,修改的時候就要改很多次,而且很容易漏改,造成資料不一致。
如何拆表單避免重複
問題出在哪裡?
「會員信箱」重複出現,是因為我們把它放在訂房表單裡。但其實「會員信箱」跟「訂房」沒有關係,它是屬於「會員」的資料。
同樣的,「飯店電話」也不應該放在訂房表單裡,它是屬於「飯店」的資料。
所以解法是:把「會員的資料」和「飯店的資料」從訂房表單裡搬出去,各自變成獨立的表單。
拆出會員表單和飯店表單
我們來想一下,哪些欄位是「屬於會員」的?哪些是「屬於飯店」的?
- 「會員」和「會員信箱」是綁在一起的 → 知道是哪個會員,就知道信箱是什麼
- 「飯店名稱」和「飯店電話」是綁在一起的 → 知道是哪間飯店,就知道電話是什麼
所以我們把這些綁在一起的欄位,各自拆成獨立的表單:
會員表單:
| 會員 | 會員信箱 |
|---|---|
| David | david@mail.com |
| Emily | emily@mail.com |
飯店表單:
| 飯店名稱 | 飯店電話 |
|---|---|
| 台北大飯店 | 02-1234-5678 |
| 高雄大飯店 | 07-8765-4321 |
訂房表單:
| 會員 | 飯店名稱 | 房型 | 單價 | 入住天數 | 總價 |
|---|---|---|---|---|---|
| David | 台北大飯店 | 雙人房 | 3000 | 2 | 6000 |
| David | 高雄大飯店 | 單人房 | 2000 | 3 | 6000 |
| Emily | 台北大飯店 | 總統套房 | 10000 | 1 | 10000 |
現在會員改信箱,只需要在會員表單改一次就好了!飯店改電話,也只需要在飯店表單改一次!
訂房表單還有重複問題
但仔細看訂房表單,你會發現「台北大飯店」出現了兩次。
如果台北大飯店被訂了 100 次,「台北大飯店」就會出現 100 次。這時候如果飯店要改名字,不是又要改 100 次嗎?
沒錯,如果我們在訂房表單裡存的是「飯店名稱」這個文字,那改名字確實要改 100 次。
用 ID 來做關聯
解決方法是:訂房表單裡不要存「飯店名稱」,改成存「飯店 ID」。
我們先在飯店表單加上一個 ID 欄位:
飯店表單:
| 飯店 ID | 飯店名稱 | 飯店電話 |
|---|---|---|
| H001 | 台北大飯店 | 02-1234-5678 |
| H002 | 高雄大飯店 | 07-8765-4321 |
然後訂房表單改成存「飯店 ID」:
訂房表單:
| 會員 | 飯店 ID | 房型 | 單價 | 入住天數 | 總價 |
|---|---|---|---|---|---|
| David | H001 | 雙人房 | 3000 | 2 | 6000 |
| David | H002 | 單人房 | 2000 | 3 | 6000 |
| Emily | H001 | 總統套房 | 10000 | 1 | 10000 |
現在如果台北大飯店要改名字,只需要改飯店表單裡的那一筆就好。訂房表單裡存的是「H001」,不需要改。
當你要顯示訂房資料的時候,只要透過「H001」去飯店表單查,就能找到對應的飯店名稱。這就是「關聯」的概念。
訂房表單透過「飯店 ID」關聯到飯店表單,查詢時再去找對應的飯店名稱
會員也用 ID 來關聯
同樣的道理,「會員」欄位也應該改成「會員 ID」:
會員表單:
| 會員 ID | 會員名稱 | 會員信箱 |
|---|---|---|
| U001 | David | david@mail.com |
| U002 | Emily | emily@mail.com |
訂房表單:
| 會員 ID | 飯店 ID | 房型 | 單價 | 入住天數 | 總價 |
|---|---|---|---|---|---|
| U001 | H001 | 雙人房 | 3000 | 2 | 6000 |
| U001 | H002 | 單人房 | 2000 | 3 | 6000 |
| U002 | H001 | 總統套房 | 10000 | 1 | 10000 |
現在不管是會員改名字、改信箱,還是飯店改名字、改電話,都只需要改一個地方就好了!
房型和單價還是會重複
但是,還有另一個問題:「房型」和「單價」。
假設台北大飯店的「雙人房」被訂了 50 次,那「雙人房」和「3000」這兩個值就會在訂房表單裡重複 50 次。
如果雙人房要漲價,從 3000 改成 3500,你就要改 50 次。
問題出在哪裡?
我們來想一下:「單價」是由什麼決定的?
- 單價是由「房型」決定的,不是由「訂房」決定的
- 雙人房的單價就是 3000,不管是誰訂的、什麼時候訂的,都是 3000
- 換句話說,只要知道是哪個房型,就知道單價是多少
所以「房型」和「單價」是綁在一起的,應該放在同一張表單。但現在它們被放在訂房表單裡,導致每訂一次房,就多存一次「房型」和「單價」,造成重複。
所以「房型」和「單價」也應該拆出去,用同樣的方式做關聯。
把房型放進飯店表單?
你可能會想:那就把「房型」和「單價」放進飯店表單就好了啊?
我們試試看:
飯店表單:
| 飯店 ID | 飯店名稱 | 飯店電話 | 房型 | 單價 |
|---|---|---|---|---|
| H001 | 台北大飯店 | 02-1234-5678 | 雙人房 | 3000 |
| H001 | 台北大飯店 | 02-1234-5678 | 總統套房 | 10000 |
| H002 | 高雄大飯店 | 07-8765-4321 | 單人房 | 2000 |
問題來了:「台北大飯店」出現了兩次,因為它有兩種房型。
如果台北大飯店要改電話,又要改兩次了。如果這間飯店有 10 種房型,就要改 10 次。
這樣不行,我們又回到重複的問題了。
為什麼會這樣?
我們來想一下:
- 「飯店電話」是由什麼決定的?是由「飯店」決定的。知道是哪間飯店,就知道電話是什麼。
- 「單價」是由什麼決定的?是由「房型」決定的。知道是哪個房型,就知道單價是多少。
這是兩組不同的東西:
- 一間飯店有一個電話
- 一種房型有一個單價
但現在我們把它們全部塞進同一張表單,結果就是:每多一種房型,飯店的名稱和電話就要多寫一次。
所以正確的做法是:把「房型」也拆成獨立的表單。
拆出房型表單
我們把房型拆成獨立的表單,並加上 ID:
房型表單:
| 房型 ID | 房型名稱 | 單價 | 飯店 ID |
|---|---|---|---|
| R001 | 雙人房 | 3000 | H001 |
| R002 | 總統套房 | 10000 | H001 |
| R003 | 單人房 | 2000 | H002 |
注意看,房型表單裡有「飯店 ID」這個欄位,用來記錄這個房型屬於哪間飯店。
訂房表單可以刪掉飯店 ID
現在訂房表單還長這樣:
| 會員 ID | 飯店 ID | 房型 ID | 入住天數 | 總價 |
|---|---|---|---|---|
| U001 | H001 | R001 | 2 | 6000 |
| U001 | H002 | R003 | 3 | 6000 |
| U002 | H001 | R002 | 1 | 10000 |
你會發現「飯店 ID」其實是多餘的。
為什麼?因為只要知道「房型 ID」,就能從房型表單查到這個房型屬於哪間飯店。
例如:R001 是雙人房,從房型表單可以查到它的飯店 ID 是 H001,也就是台北大飯店。
從訂房表單的「房型 ID」可以一路查到對應的飯店
所以我們可以把訂房表單的「飯店 ID」刪掉:
訂房表單:
| 會員 ID | 房型 ID | 入住天數 | 總價 |
|---|---|---|---|
| U001 | R001 | 2 | 6000 |
| U001 | R003 | 3 | 6000 |
| U002 | R002 | 1 | 10000 |
現在房型要漲價,也只需要改一個地方了!
如何透過關聯查詢飯店資訊
現在訂房表單裡沒有「飯店」欄位了,那如果要知道 David 訂的是哪間飯店,怎麼辦?
- 先從訂房表單找到 David 訂的房型 ID(R001)
- 再從房型表單找到 R001 屬於哪間飯店(飯店 ID 是 H001)
- 最後從飯店表單找到 H001 是台北大飯店,電話是 02-1234-5678
這就是「關聯」的威力:資料只存一個地方,需要的時候再透過 ID 去查。
用欄位相依關係決定如何拆表
回顧一下我們剛才做了什麼。
一開始的表單
一開始的訂房表單有 8 個欄位,全部塞在一起:
| 會員 | 會員信箱 | 飯店名稱 | 飯店電話 | 房型 | 單價 | 入住天數 | 總價 |
|---|
找出欄位之間的相依關係
我們問自己一個問題:這個欄位是由什麼決定的?
- 「會員信箱」是由什麼決定的?→ 由「會員」決定。知道是哪個會員,就知道信箱是什麼。
- 「飯店電話」是由什麼決定的?→ 由「飯店」決定。知道是哪間飯店,就知道電話是什麼。
- 「單價」是由什麼決定的?→ 由「房型」決定。知道是哪個房型,就知道單價是多少。
這就是「相依關係」:A 決定了 B,所以 A 和 B 是綁在一起的。
把相依的欄位拆成獨立的表單
根據相依關係,我們把欄位分組:
- 「會員」和「會員信箱」是一組 → 拆成會員表單
- 「飯店」和「飯店電話」是一組 → 拆成飯店表單
- 「房型」和「單價」是一組 → 拆成房型表單
- 剩下的「入住天數」和「總價」才是真正屬於訂房的 → 留在訂房表單
拆完之後的結果
最後我們得到四張表單:
會員表單:
| 會員 ID | 會員名稱 | 會員信箱 |
|---|
飯店表單:
| 飯店 ID | 飯店名稱 | 飯店電話 |
|---|
房型表單:
| 房型 ID | 房型名稱 | 單價 | 飯店 ID |
|---|
訂房表單:
| 會員 ID | 房型 ID | 入住天數 | 總價 |
|---|
每張表單裡的欄位都是「綁在一起」的,不會有多餘的重複。
這個原則叫做第二正規化形式(Second Normal Form, 2NF)。
什麼是第二正規化形式?
第二正規化形式(Second Normal Form,簡稱 2NF)是資料庫設計的一個重要原則。
核心概念
第二正規化形式的核心概念是:
把相依的欄位放在同一張表單,不相依的欄位拆到不同的表單。
什麼叫「相依」?就是「A 決定了 B」的關係:
- 知道「會員」,就知道「會員信箱」→ 會員信箱相依於會員
- 知道「飯店」,就知道「飯店電話」→ 飯店電話相依於飯店
- 知道「房型」,就知道「單價」→ 單價相依於房型
相依的欄位應該放在一起,不相依的欄位應該拆開。
為什麼這樣做可以避免重複?
因為相依的欄位本來就是「綁在一起」的。
如果你把它們拆開放到不同的表單,就會造成重複。例如把「會員信箱」放在訂房表單,那同一個會員訂了幾次房,信箱就要重複幾次。
反過來說,如果你把不相依的欄位硬塞在一起,也會造成重複。例如把「飯店電話」和「房型單價」放在同一張表單,那同一間飯店有幾種房型,電話就要重複幾次。
程式設計的類似概念:高內聚、低耦合
這個概念在程式設計裡叫做「高內聚、低耦合」:
- 高內聚(High Cohesion):相關的東西放在一起。一個模組裡面的功能應該是緊密相關的,不要把不相干的東西混在一起。
- 低耦合(Low Coupling):不相關的東西分開來。模組和模組之間應該盡量獨立,不要互相牽扯太深。
套用到資料庫設計:
- 高內聚:一張表單裡的欄位應該是相依的,都是描述同一件事情
- 低耦合:不同表單之間用「關聯」來連結,而不是把資料重複存一份
為什麼不能只靠直覺判斷?
你可能會問:一開始我們用「個體、性質、關係」來判斷,不是也可以把會員和飯店拆出來嗎?
沒錯,但有些情況光靠直覺會漏掉。
像這個例子,一開始我們直覺認為「訂房」是「會員」和「飯店」的關係。但仔細分析欄位之間的相依關係後,才發現「房型」也應該拆出來,而且「訂房」其實是「會員」和「房型」的關係。
這就是為什麼我們要從「欄位之間的相依關係」來思考,而不是只靠直覺判斷「什麼是一個東西」。
資料庫正規化重點整理
這篇文章介紹了第二正規化形式的核心概念:
- 問題:如果把所有欄位放在同一張表單,修改資料時會需要改很多次
- 解法:拆表單、刪欄位,把相依的欄位拆成獨立的表單
- 判斷方式:觀察哪些欄位之間有相依關係(A 變了,B 就要跟著變)
- 第二正規化形式:把相依的欄位放在同一張表單,不相依的拆開
- 關聯的威力:資料只存一個地方,需要的時候再透過關聯去查
這樣拆完之後,表單已經精簡很多了。但這樣夠了嗎?可能還沒有,後續的文章會繼續介紹其他的正規化原則。