資料庫初探 正規化
最近開始接觸一些資料庫的東西,主要是為了學習 MongoDB。 不過由於本人的資料庫知識基本上還停留在大學課程的程度... 加上只有關聯式資料庫的底子。 所以為了學習 MongoDB,只好先來看一些基礎的東西。 首先就從正規化開始。
定義來說:資料庫的正規化、標準化,是資料庫設計中的一系列原理和技術,以減少資料庫中資料冗餘,增進資料的一致性。 其實這東西就像是軟體工程一樣,完全不做其實不會怎樣,只是後續管理會很複雜。 但是做太多了又會搞死自己... 所以如何拿捏正規化的程度,以及在 maintain 跟 development resources 的平衡上就需要做一些取捨。
雖然目前正規化共有第一正規化、第二正規化、第三正規化、BC正規化、第四正規化、第五正規化、DK正規化、第六正規化等,但是實務上大多只會做到第三正規化而已。
首先先來看看三種正規化的定義以及目的:
名稱 | 目的 | 具體作法 |
---|---|---|
第一正規化 | 排除 重複群 的出現 | 要求資料庫的每個列的值域都是由原子值组成,每個欄位的值都只能是單一值 |
第二正規化 | 資料表裡的所有資料都要和該資料表的 PK 有完全依賴關係 | 如果有哪些資料只和 PK 的一部份有關的話,就得把它們獨立出來變成另一個資料表 |
第三正規化 | 非 PK 屬性之間應該是獨立無關 | 把相關的屬性簡化,只保留必要的屬性。 |
這邊就來舉幾個例子
第一正規化
資料庫系統常見於電子商務或是財務系統,這邊就參考 Wikipedia 的交易紀錄範例 首先先來看看交易資料表的結構
顧客 | 日期 | 數量 |
---|---|---|
Pete | Monday | 19.00 |
Pete | Wednesday | -84.00 |
Sarah | Friday | 100.00 |
看起來沒什麼問題,裡頭看得到 哪個顧客
在 哪一天
下單了 多少單位
但是問題來了,如果一個顧客在一天之中下了多筆訂單呢?
像這樣:
顧客 | 日期 | 數量 |
---|---|---|
Pete | Monday | 19.00 -28.20 |
Pete | Wednesday | -84.00 |
Sarah | Friday | 100.00 150.00 -40.00 |
這樣就出現了 重複群
,也就是同個 Table 中同一個欄位的不同筆資料內的資料數量不一致,這樣會導致後續管理上很大的問題。
如果是在大學的作業或是報告,可能會有像是以下的解決方式:
顧客 | 日期 | 數量1 | 數量2 | 數量3 |
---|---|---|---|---|
Pete | Monday | 19.00 | -28.20 | |
Pete | Wednesday | -84.00 | ||
Sarah | Friday | 100.00 | 150.0 | -40.00 |
有經驗的人看到這個解決方式就知道其實是個很沒有彈性的方式,而且也不符合第一正規化的原則。 所以正確的方式應該是把每一筆交易獨立出來變成單獨的紀錄
顧客 | 日期 | 數量 |
---|---|---|
Pete | Monday | 19.00 |
Pete | Monday | -28.20 |
Pete | Wednesday | -84.00 |
Sarah | Friday | 100.00 |
Sarah | Friday | 150.00 |
Sarah | Friday | -40.00 |
但是問題又來了,如果有人同一天下了兩筆數量一樣的訂單時,就會出現兩筆資料一致的狀況
顧客 | 日期 | 數量 |
---|---|---|
Some guy | Some day | 20.00 |
Some guy | Some day | 20.00 |
所以通常在資料庫的設計,或是很多資料庫系統預設都會在每一筆紀錄上加上唯一的識別 id
id | 顧客 | 日期 | 數量 |
---|---|---|---|
1 | Pete | Monday | 19.00 |
2 | Pete | Monday | -28.20 |
3 | Pete | Wednesday | -84.00 |
4 | Sarah | Friday | 100.00 |
5 | Sarah | Friday | 150.00 |
6 | Sarah | Friday | -40.00 |
7 | Some guy | Some day | 20.00 |
8 | Some guy | Some day | 20.00 |
識別 id 不一定是流水號,有些系統會用 UUID 的方式產生,所以不一定會有順序性。
這樣就完成了第一正規化!
第二正規化
第二正規化的重點在於讓 Table 內所有的欄位,都與主鍵(Primary Key)以及候選鍵(Candidate Key)有直接關係,而不是間接相關或是部分相關。 如果有的話,就需要把那些欄位獨立出來變成另外一個 Table。
來看看 Wikipeida 上的範例
元件列表:
元件 ID (主鍵) | 價格 | 供應商ID (主鍵) | 供應商名稱 | 供應商住址 |
---|---|---|---|---|
65 | 59.99 | 1 | Stylized Parts | VA |
73 | 20.00 | 1 | Stylized Parts | VA |
65 | 69.99 | 2 | ACME Industries | CA |
由於某個元件可能是由不同供應商提供,所以這個 Table 的主鍵為 元件ID+供應商ID 的組合。 在這個表中,由於價格直接與元件以及供應商相關,所以符合第二正規化。 但是供應商名稱以及供應商地址就只與供應商ID相關,而跟元件無關,屬於部分相關,這部分不符合第二正規化。
所以依照第二正規化的規定,需要把跟主鍵無直接關係的欄位獨立出來變成另外一個 Table。 修改過後的資料表如下
元件列表:
元件 ID (主鍵) | 價格 | 供應商ID (主鍵) |
---|---|---|
65 | 59.99 | 1 |
73 | 20.00 | 1 |
65 | 69.99 | 2 |
供應商列表:
供應商ID (主鍵) | 供應商名稱 | 供應商住址 |
---|---|---|
1 | Stylized Parts | VA |
2 | ACME Industries | CA |
這樣不管是供應商被併購、改名或是地址變更,都不會動到原本的元件列表!
第三正規化
第二正規化中提到了資料表中的欄位都必須和主鍵有直接相關。在第三正規化中更進一步的規定說資料表中的所有欄位之間必須不能有依賴關係。 同樣參考 Wikipedia 中的範例
訂單列表:
訂單編號(Order Number)(主鍵) | 客戶名稱 (Customer Name) | 單價 (Unit Price) | 數量 (Quantity) | 小計 (Total) |
---|---|---|---|---|
1000 | David | $35.00 | 3 | $105.00 |
1001 | Jim | $25.00 | 2 | $50.00 |
1002 | Bob | $25.00 | 3 | $75.00 |
這個表中的欄位,都符合第一正規化以及第二正規化。
但是在 小計 這個欄位不符合第三正規化。由於小計這個欄位的值可以透過 單價 以及 數量 算出,所以根據第三正規化的規定必須移除。
在做查詢的時候,本來用 SELECT Order.Total FROM Order
就直接改用 SELECT UnitPrice * Quantity FROM Order
這些就是基本的資料庫正規化,對於 Backend 工程師好像是常識一般,但是對我這 Server 外行來說也是學習了不少!