前面的文章介紹了第二正規化和第三正規化,都是從「欄位之間的相依關係」來決定要不要拆表單、刪欄位。
這篇文章要介紹的第一正規化形式,角度不太一樣——我們要從「資料列」的層級來看,有沒有需要調整的地方。
先來看一個實際的例子。
一個會員有多支電話怎麼辦?
假設我們有一張會員表單:
| 會員 | 會員信箱 | 會員電話 |
|---|---|---|
| David | david@mail.com | 0912-345-678 |
| Emily | emily@mail.com | 0923-456-789 |
看起來很合理對吧?會員、會員信箱、會員電話,很正常。
但這張表單有一個隱藏的問題:如果會員有多支電話怎麼辦?
表單裡只有一個「會員電話」欄位,如果 David 有兩支、三支電話,要怎麼處理?
不要用多個欄位存多支電話
你可能會想:那我就多開幾個欄位啊!
| 會員 | 會員信箱 | 會員電話一 | 會員電話二 | 會員電話三 | 會員電話四 |
|---|---|---|---|---|---|
| David | david@mail.com | 0912-345-678 | 0922-111-222 | ||
| Emily | emily@mail.com | 0923-456-789 |
這樣就可以記錄四支電話了!
拜託不要這樣做。
增加欄位的問題
問題一:欄位數量永遠不夠
如果會員有五支電話怎麼辦?十支呢?你要一直開新欄位嗎?
還記得我們說過,關聯式資料庫很少允許「一直往右增加欄位」這件事。欄位的數量應該在設計時就固定下來,不能因為某個會員的電話變多了,就臨時加欄位。
問題二:查詢變得很複雜
如果你想知道「這個會員有幾支電話」,你要怎麼做?
你必須把這十個欄位全部讀出來,然後一個一個檢查哪些有值、哪些是空的。
如果你想從一支電話找到它屬於哪個會員呢?
你要把所有會員的十個電話欄位全部讀出來,一個一個比對,才能找到是哪一筆資料。
這樣的查詢非常沒有效率,而且很容易出錯。
不要把多支電話塞在同一格
你可能又會想:那我不要增加欄位,我把所有電話都塞在同一格裡面!
| 會員 | 會員信箱 | 會員電話們 |
|---|---|---|
| David | david@mail.com | 0912-345-678, 0922-111-222 |
| Emily | emily@mail.com | 0923-456-789 |
這樣就不用一直開新欄位了!
這樣也不行。
塞在同一格的問題
問題一:很難確保資料格式一致
這一格裡面可能有人用逗號分隔,有人用空格分隔,有人用換行分隔。你很難確保每一筆資料的格式都一樣。
問題二:處理資料很麻煩
如果你想新增一支電話,你要:
- 把整格資料讀出來
- 解析裡面的內容
- 加上新的電話
- 重新組合成字串
- 整筆塞回去
如果你想知道這個會員有幾支電話,你也要把整格讀出來、解析、計算。
相比之下,如果每支電話是獨立的一筆資料,新增和查詢都會簡單很多。
用拆表單解決一對多關係
第一正規化形式告訴我們:遇到一對多的關係,就拆表單。
什麼是「一對多」?就是「一個會員可以有多支電話」這種情況。
我們把電話拆成獨立的表單:
會員表單:
| 會員 ID | 會員名稱 | 會員信箱 |
|---|---|---|
| U001 | David | david@mail.com |
| U002 | Emily | emily@mail.com |
會員電話表單:
| 電話 | 會員 ID |
|---|---|
| 0912-345-678 | U001 |
| 0922-111-222 | U001 |
| 0933-333-444 | U001 |
| 0923-456-789 | U002 |
現在 David 有三支電話,我們只要在「會員電話表單」裡新增三筆資料就好。
如果他之後又新增了一百支電話,我們就再新增一百筆資料。完全不需要動到欄位的設計。
拆表單的好處
新增電話很簡單
要新增電話?就在「會員電話表單」新增一筆資料,填上電話和會員 ID 就好。
查詢電話數量很簡單
要知道 David 有幾支電話?從「會員電話表單」篩選出「會員 ID = U001」的資料,數一數有幾筆就好。
從電話找會員很簡單
要從一支電話找到會員?從「會員電話表單」找到那支電話,就能看到它的「會員 ID」是什麼。
欄位往右長 vs 資料往下長
這裡有一個很重要的觀念:
在關聯式資料庫裡,資料列往下增加是正常的,但欄位往右增加要非常謹慎。
錯誤的做法(往右長):
| 會員 | 電話一 | 電話二 | 電話三 | 電話四 | … |
|---|
正確的做法(往下長):
| 電話 | 會員 ID |
|---|---|
| 0912-345-678 | U001 |
| 0922-111-222 | U001 |
| 0933-333-444 | U001 |
| … | … |
拆表單之後,原本想要「往右長」的欄位,變成了「往下長」的資料列。這才是關聯式資料庫正確的設計方式。
第一正規化形式
第一正規化形式(First Normal Form,簡稱 1NF)的核心概念是:
每個欄位只存一個值,遇到一對多的關係就拆表單。
具體來說:
- 不要為了「同性質的多筆資料」開很多欄位(例如電話一、電話二、電話三)
- 不要在同一格裡塞很多筆資料(例如用逗號分隔多支電話)
- 遇到一對多關係,就拆成獨立的表單
什麼時候可以違反第一正規化?
當然,有時候你可能會刻意違反這個原則。
用 JSON 格式存多筆資料
現在很多資料庫已經支援 JSON 格式。
什麼是 JSON?簡單來說,它是一種資料格式,可以在一個欄位裡存放「結構化的資料」,包括清單。
舉個例子,你的會員表單可以這樣設計:
| 會員 ID | 會員名稱 | 會員電話 |
|---|---|---|
| U001 | David | [“0912-345-678”, “0922-111-222”, “0933-333-444”] |
| U002 | Emily | [“0923-456-789”] |
「會員電話」這個欄位存的是一個 JSON 陣列,裡面可以放很多支電話。
這樣做的好處是:不用另外開一張「會員電話表單」,設計上比較簡單。
JSON 和逗號分隔的字串有什麼差異?
你可能會問:這跟前面說的「把多支電話塞在同一格」有什麼不一樣?
前面的做法是用逗號分隔的字串:
0912-345-678, 0922-111-222, 0933-333-444這只是一串文字,資料庫不知道裡面有幾筆資料,也不知道怎麼拆開它。你要自己寫程式去解析。
JSON 格式則是:
["0912-345-678", "0922-111-222", "0933-333-444"]這是一個「結構化的清單」,資料庫知道裡面有三筆資料。支援 JSON 的資料庫可以直接:
- 查詢「這個清單裡有沒有某支電話」
- 取得「這個清單裡的第一筆資料」
- 計算「這個清單裡有幾筆資料」
簡單來說,逗號分隔的字串只是文字,JSON 則是資料庫看得懂的結構。
但即使是 JSON,它的查詢效率還是比不上拆表單。所以 JSON 適合「不常查詢」的附屬資料,如果需要頻繁查詢,還是拆表單比較好。
什麼情況適合用 JSON?
如果你的情境符合以下條件,可以考慮用 JSON:
- 電話只是附屬資訊:你不需要經常單獨查詢電話
- 不需要反向查詢:你不需要「從電話找到會員」這種操作
- 資料量不大:每個會員的電話數量有限,不會有幾百支
- 你確定這樣可以簡化設計:拆表單反而讓系統變得太複雜
什麼情況不適合用 JSON?
如果你需要:
- 經常查詢「某支電話屬於哪個會員」
- 統計「所有會員總共有幾支電話」
- 對電話做排序、篩選、分組等操作
那就不適合用 JSON,還是乖乖拆表單比較好。
重點:知道規則,才能打破規則
用 JSON 存多筆資料,是刻意違反第一正規化形式。
這沒有不對,但你要知道自己在做什麼。你知道這個原則,評估過利弊之後,選擇不遵守。
這跟「不知道有這個原則,隨便亂設計」是完全不一樣的。
第一正規化重點整理
這篇文章介紹了第一正規化形式的核心概念:
- 問題:一個會員有多支電話,要怎麼存?
- 錯誤做法一:開很多欄位(電話一、電話二…)→ 欄位數量永遠不夠,查詢很複雜
- 錯誤做法二:把多筆資料塞在同一格 → 格式難統一,處理很麻煩
- 正確做法:拆表單,讓資料往下長,而不是欄位往右長
- 第一正規化形式:每個欄位只存一個值,遇到一對多關係就拆表單
到目前為止,我們已經學了三個正規化形式:
- 第一正規化:一對多關係要拆表單
- 第二正規化:相依的欄位放同一張表單
- 第三正規化:可以算出來的欄位要刪掉
這三個原則的核心都是一樣的:避免重複資料,保持資料一致。