7. 資料庫正規化 Normalization
一個資料庫會包含很多張表,那麽這些表要如何因應需求來做設計呢?該設計哪些表?該設計哪些欄位?這兩節會告訴大家。
資料庫正規化(Normalization)是資料庫設計的一個非常重要的基本概念,目的是要去除重復的數據,增加數據的一致性。實際的作法是會將重復的欄位,抽出來變成另一個新的表。
正規化還分成一階正規化、二階正規化、三階正規化、DK/NF正規化等等不同級別,一般來說我們的應用軟體會做到二階或三階正規化。
讓我們用實際的例子來看:假設我們要設計一個場景是紀錄「使用者 User」參與多個「活動 Event」
未正規化
user name | user city | zipcode | event 1 | event 1 date | event 2 | event 2 date | event 3 | event 3 date |
---|---|---|---|---|---|---|---|---|
ihower | hsinchu | 300 | RubyConf | 2015/9/11 | JSConf | 2015/5/1 | CSSConf | 2016/1/1 |
john | taipei | 100 | RubyConf | 2015/9/11 |
這種 Table 設計缺點很多:
- ihower 如果需要參加第四個活動,就必須變更 Table 的 Schema 增加更多欄位。但是變更 Scmema 是一件成本很高的事情 :(
- john 沒有參加這麽多活動,多餘的欄位都是 NULL,對資料庫來說是浪費空間
- ihower 跟 john 都有參加 RubyConf,但是 2015/9/11 這個活動日期重復存了。而且如果活動改日期,表示這些值都要改
一階: 移除重復語意的 columns
剛剛的 event 1, event 1 date, event 2, event 2 date, event 3, event 3 date 倆倆都是重復的,讓我們消滅它們:
user name | user city | zipcode | event | event date |
---|---|---|---|---|
ihower | hsinchu | 300 | RubyConf | 2015/9/11 |
ihower | hsinchu | 300 | JSConf | 2015/5/1 |
ihower | hsinchu | 300 | CSSConf | 2016/1/1 |
john | taipei | 300 | RubyConf | 2015/9/11 |
缺點:
- 相比於沒有正規化的,現在新報名不需要修改 Schame 了
- 但是重復的資料更多,包括用戶數據和活動數據
二階: 移除重復語意的 row
接下來需要拆表了,一口氣我們拆成三張表:users 表、events 表、registration 表,並且 users 和 events 要加上可識別的 id 欄位。
相信對 Rails 已經很熟悉的同學,這就是 User model 和 Event model 透過 Registration model 來達成多對多關系。
users table
user id | user name | user city | zipcode |
---|---|---|---|
1 | ihower | hsinchu | 300 |
2 | john | taipei | 100 |
events table
event id | event | event date |
---|---|---|
1 | RubyConf | 2015/9/11 |
2 | JSConf | 2015/5/1 |
3 | CSSConf | 2016/1/1 |
registrations table
user id | event id | register_at |
---|---|---|
1 | 1 | 2016-03-16 12:00:00 |
1 | 2 | 2016-03-16 12:30:00 |
1 | 3 | 2016-03-17 12:00:00 |
2 | 1 | 2016-03-18 12:00:00 |
這好多了,看起來沒有重復的資料了,如果要改 user 或 event 的數據,只需要改一個地方。
但是還有一個小地方可以改進,讓我們繼續看下去:
三階: 移除不依賴主 ID 的資料
在 users table 中,city 名字其實只跟 zipcode 相關,跟 user id 沒關系,因此這個 city 可以拆出來。在 users table 中只需要留著 zipcode 就好了。
users table
user id | user name | zipcode |
---|---|---|
1 | ihower | 300 |
2 | john | 300 |
zipcodes table
zipcode | user city |
---|---|
300 | hsinchu |
100 | taipei |
小結
正規化讓數據不會重復和高度一致性,節省空間、增加修改數據時的效率、避免數據不一致的錯誤。
8. 資料庫設計實務
資料庫正規化看起來好像很難,所幸我們實務設計的時候,並不是從一階二階三階這樣慢慢思考的,而是用 Model 的關系來思考。
關聯設計 Associations
在傳統資料庫課程中,會使用 ER diagram (entity-relationship model) 這種描述關聯的標準圖表。
在這種圖表中,定義了一對一關系、一對多關系、多對多關系等的圖表。也就是在 Rails 中我們已經熟悉只用 has_one
, has_many
, belongs_to
等。
Primary Key 主鍵
所謂的 Primary Key 主鍵就是可以唯一識別的欄位,在 Rails 中會預設產生一個欄位是 id
。
這個主鍵有一些特性:
- 不能 NULL 也不能重復
- 最常見是 Simple ID Column Key (單一 column) 的設計。但也可以是 Compound/Composite Key (多個 columns 組成一個 primary key),但 Rails 不支援。
如何選擇你的 Primary Key ?
- 最常見是自動遞增的整數(Auto incrementing Primary Key),這是 Rails 的預設方式,也是大家熟悉的 ID
- UUID 通用唯一識別碼: 1. 分佈式系統喜歡用 2. 或是當作 token URL 功能
- Natural key (例如身份證號碼, ISBN, 國碼 ISO ALPHA-2) 等等,不過你需要真的確認不會重復,例如 ISBN 其實會重復的
由於 Rails 預設使用自動遞增的整數當作 ID,一般不建議去改這件事情。在實戰應用章節中,有提到你可以自己增加別的字串來當作 Model URL。
加 primary key 的 SQL 語法:
CREATE TABLE events (id INTEGER NOT NULL PRIMARY KEY, name TEXT, capacity INTEGER);
加 auto increment primary key 的 SQL (各家語法不一樣,以下是 SQLite3)
CREATE TABLE events (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT, capacity INTEGER);
這些在 Rails Migration 中已經幫我們做了。
Foreign Key (Reference Key) 外鍵
所謂的 Foreign Key 是指用來關聯一對多的欄位欄位,例如上述 registrations 表中的 user_id 和 event_id。外鍵的命名沒有特別規定,通常是 _id
結尾。
你不需要特別告訴資料庫這個是 foreign key,就可以當他是 foreign key 來使用。在 Rails 中,寫 belongs_to
的那個 model,就是 foreign key 欄位的那個 model。
在 Rails Migration 中可以用 add_foreign_key 語法告訴資料庫這個是 foreign key,如此資料庫會提供 Referential integrity (Reference constraint) 驗證:
- 確保新增或修改時,要參考的數據存在,不然資料庫會報錯
- 刪除資料時,確保沒有其他資料參考我,不然資料庫會報錯
傳統資料庫設計非常重視數據的正確性,不過在 Rails 中則偏好在應用層解決,我們有學過利用 dependent
屬性來處理刪除的情況,例如:
class Event < ApplicationRecord
has_many :registrations, :dependent => :destroy
end
其中 :dependent
可以有幾種不同的處理方式,例如:
:destroy
把依賴的 registrations 也一並刪除,並且執行 Registration 的 destroy 回呼:delete
把依賴的 registrations 也一並刪除,但不執行 Registration 的 destroy 回呼:nullify
這是預設值,不會幫忙刪除 registrations,但會把 registrations 的外部鍵 event_id 都設成NULL:restrict_with_exception
如果有任何依賴的 registrations 資料,則連 event 都不允許刪除。執行刪除時會丟出錯誤例外 ActiveRecord::DeleteRestrictionError。:restrict_with_error
不允許刪除。執行刪除時會回傳 false,在 @event.errors 中會留有錯誤訊息。
逆正規化 denormalized
資料庫正規化並不是完全的真理,在不同場景下甚至會做逆正規化的設計。
在一般應用的場景下,也就是營運用途(OLTP)的 Schema 通常會達到比較高的正規化。但是在做分析用途(OLAP)的 Schema,則會偏好逆正規化的設計。因為不會修改數據,只會查詢,所以不會有修改數據造成數據不一致的風險。這領域叫做 Data Warehouse 數據倉庫。從 OLTP 資料庫搬資料轉到 OLAP 的過程就做 ETL (Extract-Transform-Load)
另外,在一些需要局部效能最佳化的場景,也會做一些逆正規化的設計,例如 Rails 的 計數快取 Counter Cache 功能,將數量額外用一個欄位先存下來,免去之後計算的查詢時間。這也是一種逆正規化的設計。
Rails 補充
rails-erd 這個 gem 可以分析 Rails 產生 ERD 圖表。
這個 gem 需要安裝 graphviz 工具,請執行 brew install graphviz
就可以安裝。