Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

資料庫架構設計入門:用第二正規化避免重複資料

最後更新:2026年1月7日資料庫

上一篇文章我們學到:好的資料庫架構要避免重複,方法是用「關聯」讓每筆資料只存在一個地方。

但實際設計的時候,你會遇到一個問題:怎麼判斷哪些欄位該放在同一個表單?哪些該分開?

這篇文章會用一個訂房網站的例子,帶你一步步學會「拆表單、刪欄位」的技巧,這個技巧背後的原則叫做第二正規化形式(Second Normal Form)。

把所有欄位放在同一張表單

假設我們要設計一個飯店訂房網站,需要有一張「訂房」表單來記錄訂房資料。

一開始,你可能會想把所有需要的資訊都放進來:

會員會員信箱飯店名稱飯店電話房型單價入住天數總價
Daviddavid@mail.com台北大飯店02-1234-5678雙人房300026000
Daviddavid@mail.com高雄大飯店07-8765-4321單人房200036000
Emilyemily@mail.com台北大飯店02-1234-5678總統套房10000110000
會員信箱david@mail.com
飯店名稱台北大飯店
飯店電話02-1234-5678
房型雙人房
單價3000
入住天數2
總價6000
會員信箱david@mail.com
飯店名稱高雄大飯店
飯店電話07-8765-4321
房型單人房
單價2000
入住天數3
總價6000
會員信箱emily@mail.com
飯店名稱台北大飯店
飯店電話02-1234-5678
房型總統套房
單價10000
入住天數1
總價10000

這張表看起來很方便,櫃檯人員需要的資訊一目瞭然,會員要查自己的訂房紀錄也很清楚。

包山包海,什麼都有,感覺很不錯?

修改資料時的重複問題

這張表在「查詢」的時候很方便,但在「修改」的時候就會出問題。

會員改信箱要改很多次

假設 David 已經在網站上訂了 5 次房,現在他要改信箱。

我們來看一下訂房表單上的資料:

會員會員信箱飯店名稱…
Daviddavid@mail.com台北大飯店…
Daviddavid@mail.com高雄大飯店…
Daviddavid@mail.com台中大飯店…
Daviddavid@mail.com台南大飯店…
Daviddavid@mail.com花蓮大飯店…
會員信箱david@mail.com
飯店名稱台北大飯店
……
會員信箱david@mail.com
飯店名稱高雄大飯店
……
會員信箱david@mail.com
飯店名稱台中大飯店
……
會員信箱david@mail.com
飯店名稱台南大飯店
……
會員信箱david@mail.com
飯店名稱花蓮大飯店
……

你會發現:David 的信箱在這張表上出現了 5 次。

所以當 David 要把信箱從 david@mail.com 改成 david.new@mail.com 的時候,你要改 5 次。

這很奇怪,因為「改信箱」跟「訂房」根本沒有關係。會員改信箱是會員自己的事,為什麼訂房表單要跟著改?

而且如果你漏改了其中一筆,就會變成:

會員會員信箱飯店名稱…
Daviddavid.new@mail.com台北大飯店…
Daviddavid.new@mail.com高雄大飯店…
Daviddavid@mail.com台中大飯店…
Daviddavid.new@mail.com台南大飯店…
Daviddavid.new@mail.com花蓮大飯店…
會員信箱david.new@mail.com
飯店名稱台北大飯店
……
會員信箱david.new@mail.com
飯店名稱高雄大飯店
……
會員信箱david@mail.com
飯店名稱台中大飯店
……
會員信箱david.new@mail.com
飯店名稱台南大飯店
……
會員信箱david.new@mail.com
飯店名稱花蓮大飯店
……

同一個 David,有兩種不同的信箱,到底哪個才是對的?這就是「資料不一致」。

飯店改電話要改更多次

如果台北大飯店要改電話呢?問題更嚴重。

假設台北大飯店很熱門,已經被訂了 100 次。那這張訂房表單上就有 100 筆資料的「飯店電話」欄位寫著台北大飯店的電話。

現在台北大飯店要把電話從 02-1234-5678 改成 02-1111-2222,你就要改 100 次。

飯店改電話跟訂房有什麼關係?沒有關係。但因為我們把電話放在訂房表單裡,所以每一筆訂房資料都存了一份電話,改一次電話就要改 100 次。

如果這間飯店被訂了 10,000 次呢?你就要改 10,000 次。

這就是資料重複造成的問題:同樣的資料存在太多地方,修改的時候就要改很多次,而且很容易漏改,造成資料不一致。

如何拆表單避免重複

問題出在哪裡?

「會員信箱」重複出現,是因為我們把它放在訂房表單裡。但其實「會員信箱」跟「訂房」沒有關係,它是屬於「會員」的資料。

同樣的,「飯店電話」也不應該放在訂房表單裡,它是屬於「飯店」的資料。

所以解法是:把「會員的資料」和「飯店的資料」從訂房表單裡搬出去,各自變成獨立的表單。

拆出會員表單和飯店表單

我們來想一下,哪些欄位是「屬於會員」的?哪些是「屬於飯店」的?

  • 「會員」和「會員信箱」是綁在一起的 → 知道是哪個會員,就知道信箱是什麼
  • 「飯店名稱」和「飯店電話」是綁在一起的 → 知道是哪間飯店,就知道電話是什麼

所以我們把這些綁在一起的欄位,各自拆成獨立的表單:

會員表單:

會員會員信箱
Daviddavid@mail.com
Emilyemily@mail.com
會員信箱david@mail.com
會員信箱emily@mail.com

飯店表單:

飯店名稱飯店電話
台北大飯店02-1234-5678
高雄大飯店07-8765-4321
飯店電話02-1234-5678
飯店電話07-8765-4321

訂房表單:

會員飯店名稱房型單價入住天數總價
David台北大飯店雙人房300026000
David高雄大飯店單人房200036000
Emily台北大飯店總統套房10000110000
飯店名稱台北大飯店
房型雙人房
單價3000
入住天數2
總價6000
飯店名稱高雄大飯店
房型單人房
單價2000
入住天數3
總價6000
飯店名稱台北大飯店
房型總統套房
單價10000
入住天數1
總價10000

現在會員改信箱,只需要在會員表單改一次就好了!飯店改電話,也只需要在飯店表單改一次!

訂房表單還有重複問題

但仔細看訂房表單,你會發現「台北大飯店」出現了兩次。

如果台北大飯店被訂了 100 次,「台北大飯店」就會出現 100 次。這時候如果飯店要改名字,不是又要改 100 次嗎?

沒錯,如果我們在訂房表單裡存的是「飯店名稱」這個文字,那改名字確實要改 100 次。

用 ID 來做關聯

解決方法是:訂房表單裡不要存「飯店名稱」,改成存「飯店 ID」。

我們先在飯店表單加上一個 ID 欄位:

飯店表單:

飯店 ID飯店名稱飯店電話
H001台北大飯店02-1234-5678
H002高雄大飯店07-8765-4321
飯店名稱台北大飯店
飯店電話02-1234-5678
飯店名稱高雄大飯店
飯店電話07-8765-4321

然後訂房表單改成存「飯店 ID」:

訂房表單:

會員飯店 ID房型單價入住天數總價
DavidH001雙人房300026000
DavidH002單人房200036000
EmilyH001總統套房10000110000
飯店 IDH001
房型雙人房
單價3000
入住天數2
總價6000
飯店 IDH002
房型單人房
單價2000
入住天數3
總價6000
飯店 IDH001
房型總統套房
單價10000
入住天數1
總價10000

現在如果台北大飯店要改名字,只需要改飯店表單裡的那一筆就好。訂房表單裡存的是「H001」,不需要改。

當你要顯示訂房資料的時候,只要透過「H001」去飯店表單查,就能找到對應的飯店名稱。這就是「關聯」的概念。

訂房表單透過「飯店 ID」關聯到飯店表單,查詢時再去找對應的飯店名稱

會員也用 ID 來關聯

同樣的道理,「會員」欄位也應該改成「會員 ID」:

會員表單:

會員 ID會員名稱會員信箱
U001Daviddavid@mail.com
U002Emilyemily@mail.com
會員名稱David
會員信箱david@mail.com
會員名稱Emily
會員信箱emily@mail.com

訂房表單:

會員 ID飯店 ID房型單價入住天數總價
U001H001雙人房300026000
U001H002單人房200036000
U002H001總統套房10000110000
飯店 IDH001
房型雙人房
單價3000
入住天數2
總價6000
飯店 IDH002
房型單人房
單價2000
入住天數3
總價6000
飯店 IDH001
房型總統套房
單價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
飯店名稱台北大飯店
飯店電話02-1234-5678
房型雙人房
單價3000
飯店名稱台北大飯店
飯店電話02-1234-5678
房型總統套房
單價10000
飯店名稱高雄大飯店
飯店電話07-8765-4321
房型單人房
單價2000

問題來了:「台北大飯店」出現了兩次,因為它有兩種房型。

如果台北大飯店要改電話,又要改兩次了。如果這間飯店有 10 種房型,就要改 10 次。

這樣不行,我們又回到重複的問題了。

為什麼會這樣?

我們來想一下:

  • 「飯店電話」是由什麼決定的?是由「飯店」決定的。知道是哪間飯店,就知道電話是什麼。
  • 「單價」是由什麼決定的?是由「房型」決定的。知道是哪個房型,就知道單價是多少。

這是兩組不同的東西:

  • 一間飯店有一個電話
  • 一種房型有一個單價

但現在我們把它們全部塞進同一張表單,結果就是:每多一種房型,飯店的名稱和電話就要多寫一次。

所以正確的做法是:把「房型」也拆成獨立的表單。

拆出房型表單

我們把房型拆成獨立的表單,並加上 ID:

房型表單:

房型 ID房型名稱單價飯店 ID
R001雙人房3000H001
R002總統套房10000H001
R003單人房2000H002
房型名稱雙人房
單價3000
飯店 IDH001
房型名稱總統套房
單價10000
飯店 IDH001
房型名稱單人房
單價2000
飯店 IDH002

注意看,房型表單裡有「飯店 ID」這個欄位,用來記錄這個房型屬於哪間飯店。

訂房表單可以刪掉飯店 ID

現在訂房表單還長這樣:

會員 ID飯店 ID房型 ID入住天數總價
U001H001R00126000
U001H002R00336000
U002H001R002110000
飯店 IDH001
房型 IDR001
入住天數2
總價6000
飯店 IDH002
房型 IDR003
入住天數3
總價6000
飯店 IDH001
房型 IDR002
入住天數1
總價10000

你會發現「飯店 ID」其實是多餘的。

為什麼?因為只要知道「房型 ID」,就能從房型表單查到這個房型屬於哪間飯店。

例如:R001 是雙人房,從房型表單可以查到它的飯店 ID 是 H001,也就是台北大飯店。

從訂房表單的「房型 ID」可以一路查到對應的飯店

所以我們可以把訂房表單的「飯店 ID」刪掉:

訂房表單:

會員 ID房型 ID入住天數總價
U001R00126000
U001R00336000
U002R002110000
房型 IDR001
入住天數2
總價6000
房型 IDR003
入住天數3
總價6000
房型 IDR002
入住天數1
總價10000

現在房型要漲價,也只需要改一個地方了!

如何透過關聯查詢飯店資訊

現在訂房表單裡沒有「飯店」欄位了,那如果要知道 David 訂的是哪間飯店,怎麼辦?

  1. 先從訂房表單找到 David 訂的房型 ID(R001)
  2. 再從房型表單找到 R001 屬於哪間飯店(飯店 ID 是 H001)
  3. 最後從飯店表單找到 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):不相關的東西分開來。模組和模組之間應該盡量獨立,不要互相牽扯太深。

套用到資料庫設計:

  • 高內聚:一張表單裡的欄位應該是相依的,都是描述同一件事情
  • 低耦合:不同表單之間用「關聯」來連結,而不是把資料重複存一份

為什麼不能只靠直覺判斷?

你可能會問:一開始我們用「個體、性質、關係」來判斷,不是也可以把會員和飯店拆出來嗎?

沒錯,但有些情況光靠直覺會漏掉。

像這個例子,一開始我們直覺認為「訂房」是「會員」和「飯店」的關係。但仔細分析欄位之間的相依關係後,才發現「房型」也應該拆出來,而且「訂房」其實是「會員」和「房型」的關係。

這就是為什麼我們要從「欄位之間的相依關係」來思考,而不是只靠直覺判斷「什麼是一個東西」。

資料庫正規化重點整理

這篇文章介紹了第二正規化形式的核心概念:

  1. 問題:如果把所有欄位放在同一張表單,修改資料時會需要改很多次
  2. 解法:拆表單、刪欄位,把相依的欄位拆成獨立的表單
  3. 判斷方式:觀察哪些欄位之間有相依關係(A 變了,B 就要跟著變)
  4. 第二正規化形式:把相依的欄位放在同一張表單,不相依的拆開
  5. 關聯的威力:資料只存一個地方,需要的時候再透過關聯去查

這樣拆完之後,表單已經精簡很多了。但這樣夠了嗎?可能還沒有,後續的文章會繼續介紹其他的正規化原則。

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

資料庫

目錄

  • 把所有欄位放在同一張表單
  • 修改資料時的重複問題
  • 會員改信箱要改很多次
  • 飯店改電話要改更多次
  • 如何拆表單避免重複
  • 拆出會員表單和飯店表單
  • 訂房表單還有重複問題
  • 用 ID 來做關聯
  • 會員也用 ID 來關聯
  • 房型和單價還是會重複
  • 把房型放進飯店表單?
  • 為什麼會這樣?
  • 拆出房型表單
  • 訂房表單可以刪掉飯店 ID
  • 如何透過關聯查詢飯店資訊
  • 用欄位相依關係決定如何拆表
  • 一開始的表單
  • 找出欄位之間的相依關係
  • 把相依的欄位拆成獨立的表單
  • 拆完之後的結果
  • 什麼是第二正規化形式?
  • 核心概念
  • 為什麼這樣做可以避免重複?
  • 程式設計的類似概念:高內聚、低耦合
  • 為什麼不能只靠直覺判斷?
  • 資料庫正規化重點整理