前段時間因為項目需要搭建一個web服務器,后端Web框架我調研了幾個,比如Python的Flask,Django,
NodeJs的Express,JavaEE的Spring,以及C++的CppCMS, 經過權衡拓展性開發效率,最后選擇了Django。
也許Python不是最好的選擇,但至少目前來看工作的還挺順利。
但是當時對於數據庫的選擇,卻走了一點彎路。因為平時對於數據庫接觸不多,所以一開始選擇了MongoDB
作為數據庫。這在初期是沒啥問題的,但后來隨着項目推進,產品經理不斷地增加需求(我要...我要...我還要...),
數據庫中各個collection的耦合讀也越來越高,甚至同一個collection也有大量的冗余數據。雖然也有辦法進行優化,
但是我經過查閱資料和進度考量,最后還是決定趁着項目尚未release,將NoSQL替換為關系型的數據庫。
關系數據庫簡介
關系數據庫由由埃德加·科德(IBM)在1969年左右提出。自推出后就成為商業應用的主要數據庫模型(與其他數據庫模型,
如分級,網絡或對象模型相比)。如今已有許多商業關系數據庫管理系統(RDBMS),如Oracle,IBM DB2和Microsoft SQL Server等。
也有許多免費的開源關系數據庫,如MySQL,mSQL(mini-SQL)和嵌入式JavaDB(Apache Derby)等。
關系數據庫將數據存儲在表(table)中。一個表由行和列組成。行稱為記錄(record)或元組(tuple),列稱為字段(field)或屬性(attribute)。
數據庫的表類似於電子表格。不過關系數據庫可以在這些表格中產生關聯,使得可以有效地存儲大量的數據,以及高效地檢索數據。
SQL(結構化查詢語言)通常用來對關系數據庫進行操作。
關系數據庫設計步驟
數據庫的設計對經驗的要求比理論要高,因為你必須作出許多選擇。數據庫通常是為了某種應用的需求而高度定制的,因此,
通常在數據庫設計的指導里,通常都是指出不要做什么而不是要做什么,但最后的決定權還是在設計者的手中。
一、需求分析
盡可能地收集需求,以及定義你的數據庫的最終目的。 比如要開發書店查詢應用,就要先知道應用有什么需求,
如如何添加書籍,如何查詢現有書籍,如何查詢訂單,生成的報告格式如何,等等。
在這個階段的分析中,在紙上畫出輸入表單,以及查詢和報告的草圖,通常會有不少幫助。
二、收集數據,組織表並設定主鍵
一旦需求明確,接下來就要確定有哪些數據需要存儲到數據庫中。通常我們都是將數據基於分類存儲到不同的表中。
比如設計一個書店的數據庫,就需要對書本,作者,出版社,顧客,訂單等分類進行分表;而對每個表,
則要定義好需要哪些列(記錄),以書本為例,需要有標題,作者,出版社,出版日期,ISBN,價格等信息。
對於每一個表,我們需要選擇一列(或者多列)作為主鍵(primary key)
。
關於主鍵
在關系模型中,表不可以含有重復的行,否則會導致檢索出現歧義。為保證唯一性,每個表都有某一列(或者多列)作為主鍵,
其目的是可以唯一區分每一行。如果主鍵只由某列構成,則被成為簡單鍵(simple key),若由多列組成則成為組合鍵(composite key)。
大多數商業數據庫都基於主鍵來生成索引以提高查詢的速度。另外,主鍵還被用來被其他表用作關系引用(詳見下文)。
主鍵的選擇由庫的設計者來決定,要遵循以下原則:
- 主鍵的值必須是唯一的(即不可重復)
- 主鍵不能為空
除此之外,對於主鍵的選取還有一些best practice:
- 主鍵的值不可修改。因為主鍵可能會在其他表中用來引用,如果改了主鍵的值,就需要把其他表的引用都更新。
- 主鍵可以是任何類型,但最好是整數(效率原因)
- 主鍵最好用簡單鍵,如果一定要用組合鍵,要盡量用最少的列
目前的數據庫大都可以不主動指定主鍵,而是由於數據庫自己添加額外的一列類型為自增整數(AutoNumber)並指定為主鍵。
三、建立關系
在關系數據庫中包含獨立且不相關的表格通常沒有太大意義,如果真是這種情況你可以考慮使用NoSQL或者電子表格來存儲這些內容。
關系數據庫的魅力所在就是“關系”二字,甚至可以說設計關系數據庫的成敗所在就是明確各個表之間的關系。表間關系的類型有如下三種:
- 一對多(one-to-many)
- 多對多(many-to-many)
- 一對一(one-to-one)
一對多
考慮一個族譜關系的例子,一個母親可能會有0個或多個小孩,但是任意一個小孩都有且只有一個母親。這樣的關系便稱為一對多。
一對多的關系不能只用一個表來保存。為什么?以前面的例子來說,我們一開始可能會考慮建立一個名為Mothers的表,
其中保存了母親的信息如年齡,姓名,血型等,對於其下的小孩,可以創建不同的列,如老大,老二,老三...
但這樣我們會面臨一個問題,即列的數量是不確定的。換個方向來說,我們可以建立名為Children的表,其中存儲小孩的基本信息,
以及其母親的信息。這樣看似能滿足要求,但是由於不同的小孩可能會有相同的母親,因此表中的重復數據是很多的。
因此,考慮支持一對多的數據庫關系,我們應該建立兩個表,分別為Mothers和Children,只保存各自的屬性,並且設置分別的主鍵為
MotherID和ChildrenID。然后我們可以通過在Children新建一列包含MotherID建立一對多的關系,如下圖所示:
其中Children表里的MotherID列又被稱為約束或外鍵(Foreign Key),用SQL來描述如下:
CREATE TABLE `Mothers` (
`MotherID` INTEGER NOT NULL AUTO_INCREMENT,
`Name` VARCHAR(100) NOT NULL,
`Age` SMALLINT NOT NULL,
`BloodType` VARCHAR(2) NOT NULL,
PRIMARY KEY (`MotherID`)
);
CREATE TABLE `Children` (
`ChildrenID` INTEGER NOT NULL AUTO_INCREMENT,
`MotherID` INTEGER NOT NULL,
`Name` VARCHAR(100) NOT NULL,
`Age` SMALLINT NOT NULL,
`Sex` VARCHAR(50) NOT NULL,
`BloodType` VARCHAR(2) NOT NULL,
PRIMARY KEY (`ChildrenID`),
FOREIGN KEY (`MotherID`) REFERENCES `Mothers` (`MotherID`)
);
多對多
考慮一個“產品銷售”數據庫的例子,某個客戶的訂單包含一個或者多個產品,而某個產品又可能出現在多個訂單之中,
這樣的關系便稱為是多對多的。為了構建這樣的關系,我們先從兩個表訂單Orders
和產品Products
開始看。
表Products
含有關於產品的信息(如名稱,介紹,庫存)以及一個主鍵ProductID
;表Orders
則包含訂單信息
(如客戶ID,訂單日期,訂單狀態)以及主鍵OrderID
。同樣地,我們沒法簡單地將所有購買的產品保存在訂單表里,
,因為訂單所包含的產品記錄是不固定的;同理,也沒法將所有關聯訂單保存在產品表里。
因此,為了支持這種多對多的關系,我們需要第三個表。在本例子中,姑且將其命名為OrderDetails
,
其中每一行都包含了特定的訂單信息,對於這個表,主鍵應為組合鍵,包含兩列信息, 分別為OrderID
和ProductID
,
而這兩列也是對應Orders
和Products
表的Foreign Key,如下圖所示:
SQL描述如下:
CREATE TABLE `Orders` (
`OrderID` INTEGER NOT NULL AUTO_INCREMENT,
`OrderDate` DATETIME DEFAULT CURRENT_TIMESTAMP,
`CustomerID` INTEGER NOT NULL,
PRIMARY KEY (`OrderID`)
);
CREATE TABLE `Products` (
`ProductID` INTEGER NOT NULL AUTO_INCREMENT,
`Name` VARCHAR(100) NOT NULL,
`Stock` INTEGER DEFAULT 0,
PRIMARY KEY (`ProductID`)
);
CREATE TABLE `OrderDetails` (
`OrderID` INTEGER NOT NULL,
`ProductID` INTEGER NOT NULL,
FOREIGN KEY (`OrderID`) REFERENCES `Orders` (`OrderID`),
FOREIGN KEY (`ProductID`) REFERENCES `Products` (`ProductID`),
PRIMARY KEY (`OrderID`,`ProductID`)
);
事實上,多對多的關系是以兩組一對多的關系來實現的,額外引入的表被稱為junction table
即連接表。
從上面的例子可以看到,每個產品(product)都會在OrderDetails
表里出現多次,但OrderDetails
里的每一行都只包含一個產品,若每個訂單有多個產品則用多行來表示。相應地對訂單(Order)也是類似。
一對一
考慮一個“產品信息”數據庫,其中除了產品名稱,產品數量等基本信息外,還需要保存產品圖片,產品詳細等富文本詳情信息,
一個產品只有0個或者一個詳情,一個詳情有且只對應一個產品,因此這類關系就可以歸類為一對一關系。
有些數據庫限制了列的數量,或者我們需要將部分敏感信息用另外的表保存,這些情況都可以引進一對一的關系。
回到前面的例子,我們需要分裂出一個稱為ProductDetails
的表,與Products
構成一對一的關系。圖我就不畫了,
用SQL描述如下:
CREATE TABLE `Products` (
`ProductID` INTEGER NOT NULL AUTO_INCREMENT,
`Name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`ProductID`)
);
CREATE TABLE `ProductDetails` (
`ProductID` INTEGER NOT NULL AUTO_INCREMENT,
`DetailInfo` VARCHAR(65535),
FOREIGN KEY (`ProductID`) REFERENCES `Products` (`ProductID`),
PRIMARY KEY (`ProductID`)
);
可以看到在ProductDetails
表中,主鍵和外鍵都為同一列, 這保證了一對一的正確性。值得一提的是,這里保證了Products
可以對應0個或1個ProductDetails
,但ProductDetails
必須對應一個Products
,如果后者對前者不是強關聯,如“丈夫-妻子”
的關系,那么后者可以不以主鍵作為外鍵,而是以另外一列聲明為UNIQUE
的屬性作為外鍵即可。
精煉及規格化
當設計好一個數據庫或者拿到已有的數據庫時,我們可能會想要:
- 增加更多的列
- 為某個表中的可選數據創建一個新表並建立一對一關系
- 將一個大表分裂為兩個小表
- ...
在進行這些操作時,下列的規則就可以作為參考。
規范規則(Normalization)
范式(Normal Form),指的是符合某一種級別的關系模式的集合,表示一個關系內部各屬性之間的聯系的合理化程度,
可以在某種程度上認為是一張數據表的表結構所符合的某種設計標准的級別。常見的范式有第一范式、第二范式、...第六范式,
其嚴格程度依次上升,一般設計上滿足第三范式即可滿足日常使用。
第一范式(1NF)
第一范式又稱為1NF(First Normal Form),是對關系模式的基本要求,不滿足第一范式的數據庫就不是關系數據庫。
數據庫表中的字段都是單一屬性的,不可再分。這個單一屬性由基本類型構成,包括整型、實數、字符型、邏輯型、日期型等。
同一列中不能有多個值,即實體中的某個屬性不能有多個值或者不能有重復的屬性。 如果出現重復的屬性,
就可能需要定義一個新的實體,新的實體由重復的屬性構成,新實體與原實體之間為一對多關系。
簡而言之,第一范式就是沒有重復的列。
第二范式(2NF)
第二范式(2NF)是在第一范式(1NF)的基礎上建立起來的,即滿足第二范式必須先滿足第一范式(1NF)。
第二范式要求數據庫表中的每個實例或行必須可以被唯一地區分。為實現區分通常需要為表加上一個列,以存儲各個實例的惟一標識。
例如員工信息表中加上了員工編號(EmployeeID)列,因為每個員工的員工編號是惟一的,因此每個員工可以被惟一區分。
這個唯一屬性列也就是我們之前提到過的主鍵
。
第二范式也要求實體的屬性完全依賴於主鍵。所謂完全依賴是指不能存在僅依賴主關鍵字一部分的屬性,
例如含有多列的主鍵,如前文提到的OrderDetails
,主鍵為ProductID
和OrderID
,若含有一列為產品單價ProductPrice
,
則不符合2NF,因為ProductPrice只依賴於ProductID而不依賴於OrderID
,因此此屬性應該保存在Products
表中。
簡而言之,第二范式就是屬性應完全依賴於其主鍵。
第三范式(3NF)
滿足第三范式(3NF)必須先滿足第二范式(2NF)。第三范式要求數據表中如果不存在非關鍵字段對任一候選關鍵字段的傳遞函數依賴。
所謂傳遞函數依賴,指的是如果存在"A → B → C"的決定關系,則C傳遞函數依賴於A。
例如,存在一個部門信息表,其中每個部門有部門編號(DepartmentID)、部門名稱(DepartmentName)、部門簡介等信息。
這樣一個表就不是3NF的,因為存在傳遞依賴(EmplyeeID->DepartmentID->DepartmentName
),因此在員工信息表中列出部門編號后就不應再將部門名稱、
部門簡介等與部門有關的信息再加入員工信息表中,而是將這部分數據保存在部門信息表中,如果不存在部門信息表,
則根據第三范式也應該構建它,否則就會有數據冗余,並且容易產生更新、插入的異常。
簡而言之,第三范式就是任一非主鍵屬性不應依賴於其它任何非主鍵屬性。
鮑依斯-科得范式(BCNF)
BCNF(Boyce Codd Normal Form)又稱為3.5NF,其出現的目的是為了彌補3NF的缺陷。在滿足3NF的前提下,
如果數據庫表中如果不存在任何字段對任一候選關鍵字段的傳遞函數依賴則稱為符合BCNF。
只有少部分情況下滿足3NF而不滿足BCNF,這里以今日會議室預訂表
為例。考慮有以下表格:
會議室編號 | 開始時間 | 結束時間 | 會議類型 |
---|---|---|---|
1 | 09:30 | 10:30 | A1 |
1 | 10:00 | 11:30 | A2 |
2 | 09:00 | 09:30 | B1 |
2 | 14:30 | 17:30 | B2 |
其中會議類型的定義如下:
- A1, 在會議室1里的一般會議
- A2,在會議室1里的管理層會議
- B1,在會議室2里的一般會議
- B2,在會議室2里的管理層會議
這個表里所有字段都是候選關鍵字段,因此顯然是滿足3NF的,但同時存在以下決定關系:
- (會議室編號)-> (會議類型)
- (會議類型) -> (會議室編號)
即關鍵字段影響關鍵字段的情況,因此不滿足BCNF。
簡而言之,BCNF就是任一屬性不應依賴於其它非主鍵屬性。
其他更嚴格的范式超出了本文的范圍,因此不再贅述。
完整規則(Integrity)
除了設計范式,我們也可以通過完整性規則(Integrity rules)來檢查自己的設計。常見的完整性規則如下:
實體完整性(Entity Integrity Rule)
實體完整性指表中行的完整性。主要用於保證操作的數據(記錄)非空、唯一且不重復。即實體完整性要求每個關系(表)
有且僅有一個主鍵,每一個主鍵值必須唯一,而且不允許為“空”(NULL)或重復。
參照完整性(Referential Integrity Rule)
參照完整性屬於表間規則。對於永久關系的相關表,在更新、插入或刪除記錄時,如果只改其一,就會影響數據的完整性。
如刪除父表的某記錄后,子表的相應記錄未刪除,致使這些記錄稱為孤立記錄。對於更新、插入或刪除表間數據的完整性,
統稱為參照完整性。通常,在客觀現實中的實體之間存在一定聯系,在關系模型中實體及實體間的聯系都是以關系進行描述,
因此,操作時就可能存在着關系與關系間的關聯和引用。
域完整性(Domain Integrity)
域完整性是指數據庫表中的列必須滿足某種特定的數據類型或約束。其中約束又包括取值范圍、精度等規定。表中的CHECK、
FOREIGN KEY 約束和DEFAULT、 NOT NULL定義都屬於域完整性的范疇。
用戶定義完整性(User-defined Integrity)
又叫業務邏輯完整性(Business logic Integrity),是對數據表中字段屬性的約束,用戶定義完整性規則(User-defined integrity)
也稱域完整性規則。包括字段的值域、字段的類型和字段的有效規則(如小數位數)等約束,是由確定關系結構時所定義的字段的屬性決定的。
如百分制的考試成績取值范圍在0-100之間,訂單數量應該小於等於庫存量等。
其他
通常我們可以通過對指定的列創建索引來加快數據庫的讀取和查詢速度。在實現上,索引通常是一個結構化文件,可以提高SELECT
的速度,
卻會對INSERT
, UPDATE
和DELETE
的速度有一定負面影響。如果沒有索引,進行一次條件查詢(比如SELECT * FROM Customers WHERE name="Sam"
)
就需要對整個數據庫進行一次線性查找和比較。而在帶索引的結構中(如B樹),查詢的時間就能減少到對數級別。當然在這種情況下,
插入和刪除的時間也從常數上升到對數級別,不過在實踐中由於查找的頻率遠遠大於插入和刪除,因此索引帶來的好處也是很明顯的。
對於特定的表來說,索引可以是1列,多列組合(稱為組合索引,Concatenated Index)或者是某列的部分內容(稱為部分索引,Partial Index)。
在一個表里我們也可以建立多個索引,例如需要經常通過電話號碼或者名字來查詢某個客戶,就可以在這兩列建立對應的索引。
索引最終還是根據實際需要自行選擇,值得一提的是大多數RDBMS都會自動基於主鍵建立索引。
后記
總結一下,在關系數據庫設計中,我們首先要明確設計的最終目標,再根據目標決定哪些數據要持久化存儲; 對於這些數據,
要按照功能和邏輯來進行拆分,並且存放在不同的表中,並且明確之間的關系; 對於設計好的表,要進行重構,
根據設計范式對大表進行拆分和優化; 對於每個表要增加對應的完整性檢查,關鍵是實體完整性和參照完整性;
最后在實際使用中,對於高頻查詢的記錄構建索引提升效率,以及其他因地制宜的優化。
博客地址:
歡迎交流,文章轉載請注明出處.