寫給開發者看的關係型數據庫設計

一 Codd的RDBMS12法則——RDBMS的起源html

二 關係型數據庫設計階段sql

三 設計原則數據庫

四 命名規則安全

  數據庫設計,一個軟件項目成功的基石。不少從業人員都認爲,數據庫設計其實不那麼重要。現實中的情景也至關雷同,開發人員的數量是數據庫設計人員的數倍。多數人使用數據庫中的一部分,因此也會把數據庫設計想的如此簡單。其實否則,數據庫設計也是門學問。網絡

  從筆者的經歷看來,筆者更同意在項目早期由開發者進行數據庫設計(後期調優須要DBA)。根據筆者的項目經驗,一個精通OOP和ORM的開發者,設計的數據庫每每更爲合理,更能適應需求的變化,若是追其緣由,筆者我的猜想是由於數據庫的規範化,與OO的部分思想雷同(如內聚)。而DBA,設計的數據庫的優點是能將DBMS的能力發揮到極致,可以使用SQL和DBMS實現不少程序實現的邏輯,與開發者相比,DBA優化過的數據庫更爲高效和穩定。如標題所示,本文旨在分享一名開發者的數據庫設計經驗,並不涉及複雜的SQL語句或DBMS使用,所以也不會侷限到某種DBMS產品上。真切地但願這篇文章對開發者能有所幫助,也但願讀者能幫助筆者查漏補缺。數據結構

一 Codd的RDBMS12法則——RDBMS的起源架構

  Edgar Frank Codd(埃德加·弗蘭克·科德)被譽爲「關係數據庫之父」,並由於在數據庫管理系統的理論和實踐方面的傑出貢獻於1981年獲圖靈獎。在1985年,Codd 博士發佈了12條規則,這些規則簡明的定義出一個關係型數據庫的理念,它們被做爲全部關係數據庫系統的設計指導性方針。框架

  1. 信息法則 關係數據庫中的全部信息都用惟一的一種方式表示——表中的值。
  2. 保證訪問法則 依靠表名、主鍵值和列名的組合,保證能訪問每一個數據項。
  3. 空值的系統化處理 支持空值(NULL),以系統化的方式處理空值,空值不依賴於數據類型。
  4. 基於關係模型的動態聯機目錄 數據庫的描述應該是自描述的,在邏輯級別上和普通數據採用一樣的表示方式,即數據庫必須含有描述該數據庫結構的系統表或者數據庫描述信息應該包含在用戶能夠訪問的表中。
  5. 統一的數據子語言法則 一個關係數據庫系統能夠支持幾種語言和多種終端使用方式,但必須至少有一種語言,它的語句可以一某種定義良好的語法表示爲字符串,並能全面地支持如下全部規則:數據定義、視圖定義、數據操做、約束、受權以及事務。(這種語言就是SQL)
  6. 視圖更新法則 全部理論上能夠更新的視圖也能夠由系統更新。
  7. 高級的插入、更新和刪除操做 把一個基礎關係或派生關係做爲單個操做對象處理的能力不只適應於數據的檢索,還適用於數據的插入、修改個刪除,即在插入、修改和刪除操做中數據行被視做集合。
  8. 數據的物理獨立性 無論數據庫的數據在存儲表示或訪問方式上怎麼變化,應用程序和終端活動都保持着邏輯上的不變性。
  9. 數據的邏輯獨立性 當對錶作了理論上不會損害信息的改變時,應用程序和終端活動都會保持邏輯上的不變性。
  10. 數據完整性的獨立性 專用於某個關係型數據庫的完整性約束必須能夠用關係數據庫子語言定義,並且能夠存儲在數據目錄中,而非程序中。
  11. 分佈獨立性 無論數據在物理是否分佈式存儲,或者任什麼時候候改變分佈策略,RDBMS的數據操縱子語言必須能使應用程序和終端活動保持邏輯上的不變性。
  12. 非破壞性法則 若是一個關係數據庫系統支持某種低級(一次處理單個記錄)語言,那麼這個低級語言不能違反或繞過更高級語言(一次處理多個記錄)規定的完整性法則或約束,即用戶不能以任何方式違反數據庫的約束。

二 關係型數據庫設計階段運維

(一)規劃階段數據庫設計

  規劃階段的主要工做是對數據庫的必要性和可行性進行分析。肯定是否須要使用數據庫,使用哪一種類型的數據庫,使用哪一個數據庫產品。

(二)概念階段

  概念階段的主要工做是收集並分析需求。識別需求,主要是識別數據實體和業務規則。對於一個系統來講,數據庫的主要包括業務數據和非業務數據,而業務數據的定義,則依賴於在此階段對用戶需求的分析。須要儘可能識別業務實體和業務規則,對系統的總體有初步的認識,並理解數據的流動過程。理論上,該階段將參考或產出多種文檔,好比「用例圖」,「數據流圖」以及其餘一些項目文檔。若是可以在該階段產出這些成果,無疑將會對後期進行莫大的幫助。固然,不少文檔已超出數據庫設計者的考慮範圍。並且,若是你並不精通該領域以及用戶的業務,那麼請放棄本身獨立完成用戶需求分析的想法。用戶並非技術專家,而當你自身不能扮演「業務顧問」的角色時,請你選擇與項目組的相關人員合做,或者將其視爲風險呈報給PM。再次強調,大多數狀況,用戶只是行業從業者,而非職業技術人員,咱們僅僅從用戶那裏收集需求,而非依賴於用戶的知識。

  記錄用戶需求時,可使用一些技巧,固然這部份內容有些可能會超出數據庫設計人員的職責:

  • 努力維護一系列包含了系統設計和規格說明信息的文檔,如會議記錄、訪談記錄、關鍵用戶指望、功能規格、技術規格、測試規格等。
  • 頻繁與干係人溝通並收集反饋。
  • 標記出你本身添加的,不屬於客戶要求的,未決內容。
  • 與全部關鍵干係人儘快確認項目範圍,併力求凍結需求。

  此外,必須嚴謹處理業務規則,並詳細記錄。在以後的階段,將會根據這些業務規則進行設計。

  當該階段結束時,你應該可以回答如下問題:

  • 須要哪些數據?
  • 數據該被怎樣使用?
  • 哪些規則控制着數據的使用?
  • 誰會使用何種數據?
  • 客戶想在覈心功能界面或者報表上看到哪些內容?
  • 數據如今在哪裏?
  • 數據是否與其餘系統有交互、集成或同步?
  • 主題數據有哪些?
  • 核心數據價值幾何,對可靠性的要求程度?

  而且獲得以下信息:

  • 實體和關係
  • 屬性和域
  • 能夠在數據庫中強制執行的業務規則
  • 須要使用數據庫的業務過程

(三)邏輯階段

  邏輯階段的主要工做是繪製E-R圖,或者說是建模。建模工具不少,有不一樣的圖形表示方法和軟件。這些工具和軟件的使用並不是關鍵,筆者也不建議讀者花大量時間在建模方法的選擇上。對於大多數應用來講,E-R圖足以描述實體間的關係。建模關鍵是思想而不是工具,軟件只是起到輔助做用,識別實體關係纔是本階段的重點。

  除了實體關係,咱們還應該考慮屬性的域(值類型、範圍、約束)

(四)實現階段

  實現階段主要針對選擇的RDBMS定義E-R圖對應的表,考慮屬性類型和範圍以及約束。

(五)物理階段

  物理階段是一個驗證並調優的階段,是在實際物理設備上部署數據庫,並進行測試和調優。

三 設計原則

(一)下降對數據庫功能的依賴

  功能應該由程序實現,而非DB實現。緣由在於,若是功能由DB實現時,一旦更換的DBMS不如以前的系統強大,不能實現某些功能,這時咱們將不得不去修改代碼。因此,爲了杜絕此類狀況的發生,功能應該有程序實現,數據庫僅僅負責數據的存儲,以達到最低的耦合。

(二)定義實體關係的原則

  當定義一個實體與其餘實體之間的關係時,須要考量以下:

  • 牽涉到的實體 識別出關系所涉及的全部實體。
  • 全部權 考慮一個實體「擁有」另外一個實體的狀況。
  • 基數 考量一個實體的實例和另外一個實體實例關聯的數量。

  關係與表數量

  • 描述1:1關係最少須要1張表。
  • 描述1:n關係最少須要2張表。
  • 描述n:n關係最少須要3張表。

(三)列意味着惟一的值

  若是表示座標(0,0),應該使用兩列表示,而不是將「0,0」放在1個列中。

(四)列的順序

  列的順序對於表來講可有可無,可是從習慣上來講,採用「主鍵+外鍵+實體數據+非實體數據」這樣的順序對列進行排序顯然能獲得比較好的可讀性。

(五)定義主鍵和外鍵

  數據表必須定義主鍵和外鍵(若是有外鍵)。定義主鍵和外鍵不只是RDBMS的要求,同時也是開發的要求。幾乎全部的代碼生成器都須要這些信息來生成經常使用方法的代碼(包括SQL文和引用),因此,定義主鍵和外鍵在開發階段是必須的。之因此說在開發階段是必須的是由於,有很多團隊出於性能考慮會在進行大量測試後,在保證參照完整性不會出現大的缺陷後,會刪除掉DB的全部外鍵,以達到最優性能。筆者認爲,在性能沒有出現問題時應該保留外鍵,而即使性能真的出現問題,也應該對SQL文進行優化,而非放棄外鍵約束。

(六)選擇鍵

1 人工鍵與天然鍵

  人工健——實體的非天然屬性,根據須要由人強加的,如GUID,其對實體毫無心義;天然健——實體的天然屬性,如身份證編號。

  人工鍵的好處:

  • 鍵值永遠不變
  • 永遠是單列存儲

  人工鍵的缺點:

  • 由於人工鍵是沒有實際意義的惟一值,因此不能經過人工鍵來避免重複行。

  筆者建議所有使用人工鍵。緣由以下:

  • 在設計階段咱們沒法預測到代碼真正須要的值,因此乾脆放棄猜想鍵,而使用人工鍵。
  • 人工鍵複雜處理實體關係,而不負責任何屬性描述,這樣的設計使得實體關係與實體內容獲得高度解耦,這樣作的設計思路更加清晰。

  筆者的另外一個建議是——每張表都須要有一個對用戶而言有意義的天然鍵,在特殊狀況下也許找不到這樣一個項,此時可使用複合鍵。這個鍵我在程序中並不會使用其做爲惟一標識,可是卻能夠在對數據庫直接進行查詢時使用。

  使用人工鍵的另外一根弊端,主要源自對查詢性能的考量,所以選擇人工鍵的形式(列的類型)很重要:

  • 自增值類型 因爲類型輕巧查詢效率更好,但取值有限。
  • GUID 查詢效率不如值類型,可是取值無限,且對開發人員更加親切。

2 智能健與非智能鍵

  智能鍵——鍵值包含額外信息,其根據某種約定好的編碼規範進行編碼,從鍵值自己能夠獲取某些信息;非智能鍵,單純的無心義鍵值,如自增的數字或GUID。

  智能鍵是一把雙刃劍,開發人員偏心這種包含信息的鍵值,程序盼望着其中潛在的數據;數據庫管理員或者設計者則討厭這種智能鍵,緣由也是很顯然的,智能鍵對數據庫是潛在的風險。前面提到,數據庫設計的原則之一是不要把具備獨立意義的值的組合實現到一個單一的列中,應該使用多個獨立的列。數據庫設計者,更但願開發人員經過拼接多個列來獲得智能鍵,即以複合主鍵的形式給開發人員使用,而不是將一個列的值分解後使用。開發人員應該接受這種數據庫設計,可是不少開發者卻想不明白二者的優略。筆者認爲,使用單一列實現智能鍵存在這樣一個風險,就是咱們可能在設計階段沒法預期到編碼規則可能會在後期發生變化。好比,構成智能鍵的局部鍵的值用完而引發規則變化或者長度變化,這種編碼規則的變化對於程序的有效性驗證與智能鍵解析是破壞性的,這是系統運維人員最不但願看到的。因此筆者建議若是須要智能鍵,請在業務邏輯層封裝(使用只讀屬性),不要再持久化層實現,以免上述問題。

(七)是否容許NULL

  關於NULL咱們須要瞭解它的幾個特性:

  • 任何值和NULL拼接後都爲NULL。
  • 全部與NULL進行的數學操做都返回NULL。
  • 引入NULL後,邏輯不易處理。

  那麼咱們是否應該容許列爲空呢?筆者認爲這個問題的答案受到咱們的開發語言的影響。以C#爲例,由於引入了可空類型來處理數據庫值類型爲NULL的情形,因此是否容許爲空對開發者來講意義並不大。但有一點必須注意,就是驗證非空必需要在程序集進行處理,而不應依賴於DBMS的非空約束,必須確保完整數據(全部必須的屬性均被賦值)到達DB(所謂的「安全區」,咱們必須定義在多層系統中那些區域獲得的數據是安全而純淨的)。

(八)屬性切割

  一種錯誤想法是,屬性與列是1:1的關係。對於開發者,咱們公開屬性而非字段。舉個例子來講,對於實體「員工」有「名字」這一屬性,「名字」能夠再被分解爲「姓」和「名」,對於開發人員來講,顯然第二種數據結構更受青睞(「姓」和「名」做爲兩個字段)。因此,在設計時咱們也應該根據須要考慮是否切割屬性。

(九)規範化——範式

  當筆者還在大學時,範式是學習關係型數據庫時最頭疼的問題。我想也許會有讀者仍然不理解範式的價值,簡單來講——範式將幫助咱們來保證數據的有效性和完整性。規範化的目的以下:

  • 消滅重複數據。
  • 避免編寫沒必要要的,用來使重複數據同步的代碼。
  • 保持表的瘦身,以及減從一張表中讀取數據時須要進行的讀操做數量。
  • 最大化彙集索引的使用,從而能夠進行更優化的數據訪問和聯結。
  • 減小每張表使用的索引數量,由於維護索引的成本很高。

  規範化旨在——挑出複雜的實體,從中抽取出簡單的實體。這個過程一直持續下去,直到數據庫中每一個表都只表明一件事物,而且表中每一個描述的都是這件事物爲止。

1 規範化實體和屬性(去除冗餘)

  1NF:每一個屬性都只應表示一個單一的值,而非多個值。

  須要考慮幾點:

  • 屬性是原子性的 須要考慮熟悉是否分解的足夠完全,使得每一個屬性都表示一個單一的值。(和「(三)列意味着惟一的值」描述的原則相同。)分解原則爲——當你須要分開處理每一個部分時才分解值,而且分解到足夠用就行。(即便當前不須要完全分解屬性,也應該考慮將來可能的需求變動。)
  • 屬性的全部實例必須包含相同數量的值 實體有固定數量的屬性(表有固定數量的列)。設計實體時,要讓每一個屬性只有固定數量的值與其相關聯。
  • 實體中出現的全部實體類型都必須不一樣

  當前設計不符合1NF的「臭味」:

  • 包含分隔符類字符的字符串數據。
  • 名字尾端有數字的屬性。
  • 沒有定義鍵或鍵定義很差的表。

2 屬性間的關係(去除冗餘)

  2NF-實體必須符合1NF,每一個屬性描述的東西都必須針對整個鍵(能夠理解爲oop中類型屬性的內聚性)。

  當前設計不符合2NF的「臭味」:

  • 重複的鍵屬性名字前綴(設計以外的數據冗餘) 代表這些值可能描述了某些額外的實體。
  • 有重複的數據組(設計以外的數據冗餘) 這標誌着屬性間有函數依賴型。
  • 沒有外鍵的複合主鍵 這標誌着鍵中的鍵值可能標識了多種事物,而不是一種事物。

  3NF-實體必須符合2NF,非鍵屬性不能描述其餘非鍵屬性。(與2NF不一樣,3NF處理的是非鍵屬性和非鍵屬性之間的關係,而不是和鍵屬性之間的關係。

  當前設計不符合3NF的「臭味」:

  • 多個屬性有一樣的前綴。
  • 重複的數據組。
  • 彙總的數據,所引用的數據在一個徹底不一樣的實體中。(有些人傾向於使用視圖,我更傾向於使用對象集合,即由程序來完成。)

  BCNF-實體知足第一範式,全部屬性徹底依賴於某個鍵,若是全部的斷定都是一個鍵,則實體知足BCNF。(BCNF簡單地擴展了之前的範式,它說的是:一個實體可能有若干個鍵,全部屬性都必須依賴於這些鍵中的一個,也能夠理解爲「每一個鍵必須惟一標識實體,每一個非鍵熟悉必須描述實體。」

3 去除實體組合鍵中的冗餘

  4NF-實體必須知足BCNF,在一個屬性與實體的鍵之間,多值依賴(一條記錄在整個表的惟一性由多個值組合起來決定的)不能超過一個。

  當前設計不符合4NF的「臭味」:

  • 三元關係(實體:實體:實體)。
  • 潛伏的多值屬性。(如多個手機號。)
  • 臨時數據或歷史值。(須要將歷史數據的主體提出,不然將存在大量冗餘。)

4 儘可能將全部關係分解爲二元關係 

  5NF-實體必須知足4NF,當分解的信息無損的時候,確保全部關係都被分解爲二元關係。

  5NF保證在第四範式中存在的任何能夠分解爲實體的三元關係都被分解。有的三元關係能夠在不丟失信息的前提下被分解爲二元關係,當分解爲兩個二元關係的過程要丟失信息時,關係被宣稱爲處於第四範式中。因此,第五範式建議是,最好把現有的三元關係都分解爲3個二元關係。

  須要注意的是,規範化的結果多是更多的表,更復雜的查詢。所以,處理到何種程度,取決於性能和數據架構的多方考量。建議規範化到第四範式,緣由是5NF的判斷太過隱晦。例如:表X(老師,學生,課程)是一個三元關係,能夠分解爲表A(老師,學生),表B(學生,課程),表C(老師,課程)。表X表示某個老師是上某個學生的某個課程的老師;表A表示老師教學生;表B表示學生上課;表C表示老師教課。單獨看是沒法發現問題的,可是從數據出發,"表X=表A+表B+表C"並不必定成立,即不能經過鏈接構建分解前的數據。由於可能有多種組合,喪失了表X反饋出的業務規則。這種現象,容易在設計階段被忽略,但好在在開放階段會被顯現,並且並不常常發生。

  推薦作法:

  • 儘量地遵照上述規範化原則。
  • 全部屬性描述的都應該是體現被建模實體的本質的內容。
  • 至少必須有一個鍵,它惟一地標識和描述了所建實體的本質。
  • 主鍵要謹慎選擇。
  • 在邏輯階段能作多少規範化就作多少(性能不是邏輯階段考慮的範疇)。

(十)選擇數據類型(MS SQL 2008)

  MS SQL的經常使用類型:

精確數字 不會發生精度損失 bit tinyint smallint int bigint decimal
近似數字 對於極值可能發生精度損失 float(N) real
日期和時間   date time smalldatetime datetime datetime2 datetimeoffset
二進制數據   bingary(N) varbinary(N) varbinary(max)
字符(串)數據   char(N) varchar(N) varchar(max) nchar(N) nvarchar(N) nvarchar(max)
存儲任意數據   sql_variant
時間戳   timestamp
GUID   uniqueidentifier
XML 不要試圖使用該類型規避1NF xml
空間數據   geometry geography
層次數據   heirarchyid

  MS SQL中不在支持的或糟糕的類型選擇

  • image:被varbinary(max)取代。
  • text和ntext:被varchar(max)和nvarchar(max)取代。
  • money和smallmoney:開發過程當中很差用,建議使用decimal。

  經常使用類型選擇:

  類型選擇的最基本規則是選擇知足須要的最輕的類型,由於這樣查詢更快。

bool 建議使用bit而非char(1),由於開發語言對其支持覺好,能夠直接映射爲bool或bool?。
大值數據 使用全部備選類型中最小的那種,類型越大,查詢越慢,當字節大於8000時,應使用max。
主鍵 自增主鍵根據預期範圍選擇int或bigint,GUID使用uniqueidentifier而非varchar(N)。

(十一)優化並行

  設計DB時就應該考慮到對並行進行優化,好比,MS SQL中的timestamp類型就是極好的選擇。

四 命名規則

  • 表——「模塊名_表名」。表名最好不要用複數,緣由是在使用ORM框架開發時,代碼生成器根據DB生成類定義,表生成了某個實例的類型定義,而不是實例集合。表名不要太長。緣由之一,某些軟件對錶名最大長度有限制;緣由之二,使用代碼生成器每每會根據表名生產類型名稱,以後懶人會直接使用這一名稱,若是將太長的名稱跨網絡邊界顯然不是明智之舉。
  • 字段——bool類型用「Is」、「Can」、「Has」等表示;日期類型命名必須包含「Date」;時間類型必須包含「Time」。
  • 存儲過程——使用「proc_」前綴。
  • 視圖——使用「view_」前綴。
  • 觸發器——使用「trig_」前綴。
相關文章
相關標籤/搜索