數據庫設計的三大范式:為了建立冗余較小、結構合理的數據庫,設計數據庫時必須遵循一定的規則。在關系型數據庫中這種規則就稱為范式。范式是符合某一種設計要求的總結。要想設計一個結構合理的關系型數據庫,必須滿足一定的范式。
在實際開發中最為常見的設計范式有三個:第一范式是最基本的范式。如果數據庫表中的所有字段值都是不可分解的原子值,就說明該數據庫表滿足了第一范式;第二范式在第一范式的基礎之上更進一層。第二范式需要確保數據庫表中的每一列都和主鍵相關,而不能只與主鍵的某一部分相關(主要針對聯合主鍵而言)。也就是說在一個數據庫表中,一個表中只能保存一種數據,不可以把多種數據保存在同一張數據庫表中;第三范式需要確保數據表中的每一列數據都和主鍵直接相關,而不能間接相關。總結一下,就是:第一范式(確保每列保持原子性);第二范式(確保表中的每列都和主鍵相關);第三范式(確保每列都和主鍵列直接相關,而不是間接相關)。
在目前的企業信息系統中,數據庫還是最佳的數據存儲方式,雖然已經有很多的書籍在指導我們進行數據庫設計,但應該那種方式是設計數據庫的表結構的最好方法、設計時應遵從什麼樣的原則、四個范式如何能夠用一種方式達到順暢的應用等是我一直在思考和總結的問題,下文是我針對這幾個問題根據自己的設計經歷准備總結的一篇文章的提綱,歡迎大家一塊進行探討,集思廣益。其中提到了領域建模的概念,但未作詳細解釋,希望以後能夠有時間我們針對這個命題進行深入探討。
1.不應該針對整個系統進行數據庫設計,而應該根據系統架構中的組件劃分,針對每個組件所處理的業務進行組件單元的數據庫設計;不同組件間所對應的數據庫表之間的關聯應盡可能減少,如果不同組件間的表需要外鍵關聯也盡量不要創建外鍵關聯,而只是記錄關聯表的一個主鍵,確保組件對應的表之間的獨立性,為系統或表結構的重構提供可能性。
//注意他這裡說的是"不要創建外鍵關聯",創建外鍵關聯的語句是: //foreign key(member_id) references member (id); //我們幾乎沒有用到這條語句,因為我們就是這樣做的,用到外鍵時,只是記錄關聯表的主鍵,而非在數據庫級別上創建外鍵。 //也不知道是歪打正著,還是前輩DBA過於強大,已經考慮好了。
2.采用領域模型驅動的方式和自頂向下的思路進行數據庫設計,首先分析系統業務,根據職責定義對象。對象要符合封裝的特性,確保與職責相關的數據項被定義在一個對象之內,這些數據項能夠完整描述該職責,不會出現職責描述缺失。並且一個對象有且只有一項職責,如果一個對象要負責兩個或兩個以上的職責,應進行分拆。
// 領域模型驅動的方式,目前用的還不是很熟,考慮的不夠多。因為經常的數據庫中的表只是拿來做存儲用而已, //特別是小需求,要加什麼字段,找到相關表加上去就行了,不太考慮領域模型。這個在中文站老業務表裡很常見
3.根據建立的領域模型進行數據庫表的映射,此時應參考數據庫設計第二范式:一個表中的所有非關鍵字屬性都依賴於整個關鍵字。關鍵字可以是一個屬性,也可以是多個屬性的集合,不論那種方式,都應確保關鍵字能夠保證唯一性。在確定關鍵字時,應保證關鍵字不會參與業務且不會出現更新異常,這時,最優解決方案為采用一個自增數值型屬性或一個隨機字符串作為表的關鍵字。
4.由於第一點所述的領域模型驅動的方式設計數據庫表結構,領域模型中的每一個對象只有一項職責,所以對象中的數據項不存在傳遞依賴,所以,這種思路的數據庫表結構設計從一開始即滿足第三范式:一個表應滿足第二范式,且屬性間不存在傳遞依賴。
//數據庫三范式記不得的同學去查資料溫習一下。 //個人認為第三范式的目的是盡量減少數據冗余,保證相同的數據只存在一份。 //第三范式其實我們遵守的並不是很嚴格,特別是老的數據庫表中會有冗余字段。這個要看情況決定吧。
5.同樣,由於對象職責的單一性以及對象之間的關系反映的是業務邏輯之間的關系,所以在領域模型中的對象存在主對象和從對象之分,從對象是從1-N或N-N的角度進一步完善主對象的業務邏輯,所以從對象及對象關系映射為的表及表關聯關系不存在刪除和插入異常。
//最後一句看不懂,可能是"所以表及表關聯關系不應該出現刪除和插入異常。"?
6.在映射後得出的數據庫表結構中,應再根據第四范式進行進一步修改,確保不存在多值依賴。這時,應根據反向工程的思路反饋給領域模型。如果表結構中存在多值依賴,則證明領域模型中的對象具有至少兩個以上的職責,應根據第一條進行設計修正。第四范式:一個表如果滿足BCNF,不應存在多值依賴。
//第四范式我們遵守的並不多吧。 //例如: //VAS_WP_CONFIG.config_name字段的值包括:adv(廣告主題)/glare(炫彩滾動主題)/theme_simple(普通主題)/theme_cartoon(動畫主題)/ theme_none(不顯示背景主題) //cate_background(類目背景)/video(公司視頻)/board_cartoon(動畫招牌)/board_simple(普通招牌)等。 //如果遵守第四范式,則需要新增一張VAS_WP_CONFIG_NAME表,存儲配置名稱枚舉值,而VAS_WP_CONFIG.config_name字段改為VAS_WP_CONFIG.config_name_id。 //這樣做更利於擴展,不會因為每個人的理解不一致而向VAS_WP_CONFIG.config_name字段裡設置亂七八糟的值,但是這樣需要維護更多的小表,造成數據值表的數量膨脹,DBA可能會覺得管理上有更多的困難。 //我們采用潛規則約定、java枚舉類等其它方式來進行保證。但有時候效果並不是很好,經常發現舊數據庫表中枚舉字段的值五花八門,不全是約定的。
7.在經過分析後確認所有的表都滿足二、三、四范式的情況下,表和表之間的關聯盡量采用弱關聯以便於對表字段和表結構的調整和重構。並且,我認為數據庫中的表是用來持久化一個對象實例在特定時間及特定條件下的狀態的,只是一個存儲介質,所以,表和表之間也不應用強關聯來表述業務(數據間的一致性),這一職責應由系統的邏輯層來保證,這種方式也確保了系統對於不正確數據(髒數據)的兼容性。當然,從整個系統的角度來說我們還是要盡最大努力確保系統不會產生髒數據,單從另一個角度來說,髒數據的產生在一定程度上也是不可避免的,我們也要保證系統對這種情況的容錯性。這是一個折中的方案。
8.應針對所有表的主鍵和外鍵建立索引,有針對性的(針對一些大數據量和常用檢索方式)建立組合屬性的索引,提高檢索效率。雖然建立索引會消耗部分系統資源,但比較起在檢索時搜索整張表中的數據尤其時表中的數據量較大時所帶來的性能影響,以及無索引時的排序操作所帶來的性能影響,這種方式仍然是值得提倡的。
//索引目前都是DBA根據具體的SQL來創建的,不過開發寫SQL時,也應該適當考慮一下字段的索引。
9.盡量少采用存儲過程,目前已經有很多技術可以替代存儲過程的功能如"對象/關系映射"等,將數據一致性的保證放在數據庫中,無論對於版本控制、開發和部署、以及數據庫的遷移都會帶來很大的影響。但不可否認,存儲過程具有性能上的優勢,所以,當系統可使用的硬件不會得到提升而性能又是非常重要的質量屬性時,可經過平衡考慮選用存儲過程。
//目前都是杜絕使用存儲過程的,我覺得用起來比較方便,對於我們來說,主要原因是會給DBA帶來管理方面的麻煩, //因為時間一長,存儲過程的邏輯和使用場景,往往沒人能了解,容易產生更多問題
10.當處理表間的關聯約束所付出的代價(常常是使用性上的代價)超過了保證不會出現修改、刪除、更改異常所付出的代價,並且數據冗余也不是主要的問題時,表設計可以不符合四個范式。四個范式確保了不會出現異常,但也可能由此導致過於純潔的設計,使得表結構難於使用,所以在設計時需要進行綜合判斷,但首先確保符合四個范式,然後再進行精化修正是剛剛進入數據庫設計領域時可以采用的最好辦法。
11.設計出的表要具有較好的使用性,主要體現在查詢時是否需要關聯多張表且還需使用復雜的SQL技巧。我感覺遵守的范式越多,就越使SQL復雜,具體情況具體分析。設計出的表要盡可能減少數據冗余,確保數據的准確性,有效的控制冗余有助於提高數據庫的性能
因此,考慮了以上條件之後,表設計約定規則如下:
//規則1:表必須要有主鍵。 //規則2:一個字段只表示一個含義。 //規則3:總是包含兩個日期字段:gmt_create(創建日期),gmt_modified(修改日期),且這兩個字段不應該包含有額外的業務邏輯。 //規則4:MySQL中,gmt_create、gmt_modified使用DATETIME類型。 //規則5:禁止使用復雜數據類型(數組,自定義類型等)。 //規則6: MySQL中,附屬表拆分後,附屬表id與主表id保持一致。不允許在附屬表新增主鍵字段。 //規則7: MySQL中,存在過期概念的表,在其設計之初就必須有過期機制,且有明確的過期時間。過期數據必須遷移至歷史表中。 //規則8: MySQL中,不再使用的表,必須通知DBA予以更名歸檔。 //規則9: MySQL中,線上表中若有不再使用的字段,為保證數據完整,禁止刪除。 //規則10: MySQL中,禁止使用OCI驅動,全部使用THI驅動。
關於MySQL的部分學習筆記總結:
一、事務跟存儲引擎
1.四種事務隔離級別:read uncommited, read commited(大多數db默認的),repeatable read(mysql默認), seriazable。
2.mysql是默認的auto commited, 也就是說每次查詢默認都是自動提交的(show variables like 'autocommited')。mysql可以通過set transaction isolatioin level命令來設置隔離級別,例如:set session transaction isolation level read commited。
3.mysql中像innodb采用mvcc(多版本並發控制)來處理並發。mvcc只工作在read commited,repeatable read這兩種事務隔離級別上。read uncommited隔離級別不兼容mvcc是因為在該級別得下的查詢,不讀取符合當前事務版本的數據行,而是最新版本的數據行。seriazable隔離級別不兼容MVCC,因為該級別下的讀操作會對每個返回行進行加鎖。
4.選擇存儲引擎,並發選用myisam,事務選擇innodb,myisam比innodb更容易出錯,出錯了恢復的時間也比較長。只有myisam支持全文檢索。
5.把表從一種存儲引擎轉到另一種引擎:
// 1. alter table mytable engine=falcon; 操作費時,可能會占用服務器的所有i/o處理能力。 // 2. create table innodb_table like myisam_table; // alter table innodb_table engine=innodb; // insert into innodb_table select * from myisam_table;
二、數據類型
1.盡可能的要把field定義為Not NULL, mysql比較難優化使用了可空列的查詢,它會使索引,索引統計更加復雜。可空列需要更多的存儲空間,還需要mysql內部進行特殊處理,當可空列被索引時,每條記錄都需要一個格外的字節。 即使要在表中存儲"沒有值"的字段,考慮使用0,特殊字段或者空字符串來代替。
2.datetime與timestamp能保存同樣的數據:精確度為秒,但是timestamp使用的空間只有datetime的一半,還能保存時區,擁有特殊的自動更新能力。但是timestamp保存的時間范圍要比datetime要小得多。mysql能存儲的最細的時間粒度為秒
3.mysql支持很多種別名,如bool,integer,nummeric.
4.float與double類型支持使用標准的浮點運算進行近似計算。 Decimal類型保存精確的小數,在>=mysql5.0,mysql服務器自身進行了decimal的運算,因為CPU不支持直接對它進行運算,所以慢一點。
5.mysql會把text與blob類型的列當成有實體的對象來進行保存。他們有各自的數據類型家族(tinytext,smalltext,text,mediumtext,longtext; blob類似); mysql對blob與text列排序方式和其他類型有所不同,它不會按照字符串的完整長度來排序。而只是按照max_sort_length規定的若干個字節來進行排序。
6.采用enum來代替字符串類型。mysql在內部把每個枚舉值都保存為整數。enum在內部是按照數字進行排序的,而不是按照字符串。enum最不好的就是字符串列表是固定的,添加和刪除必須使用alter table。
7.ip地址,一般會采用varchar(15)列來保存。事實上,IP地址是個無符號的32位整數,而不是字符串。mysql提供了inet_aton()和inet_nota()函數在證書與ip地址之間進行轉換。
三、索引
1.聚集索引不僅僅是一種單獨的索引類型,而且是一種存儲數據的方式。Innodb引擎的聚集索引實際上在同樣的結構中保存了B-Tree索引和數據行。當表有聚集索引時,它的數據行實際上保存在索引的葉子上。注意是存儲引擎來實現索引。
2.myisam與innodb數據布局:myisam索引樹(無論是主鍵索引還是非主鍵索引)葉子節點都是指向的數據行,而innodb中聚集索引,主鍵索引樹葉子節點就帶得有數據的內容,而非主鍵索引樹中葉子節點指向主鍵值,而不是數據的位置。
3.mysql有兩種產生排序結果的方式:使用文件排序,或者掃描有序的索引。目前只有myisam支持全文索引。
4.myisam表有表級鎖;myisam表不支持事務,實際上,myisam並不保證單條命令完成;myisam只緩存了mysql進程內部的索引,並保存在鍵緩存區內。OS緩存了表的數據;行被緊密的保存在一起,磁盤上的數據有很小的磁盤占用和快速的全表掃描。
5.innodb支持事務和四種事務隔離級別;在mysql5.0中,只有innodb支持外鍵;支持行級鎖與mvcc;所有的innodb表都是按照主鍵聚集的;所有索引(出開主鍵)都是按主鍵引用行;索引沒有使用前綴壓縮,因此索引可能比myisam大很多;數據轉載緩慢;阻塞auto_increment,也就是用表級鎖來產生每個auto_increment。
四、MYSQL性能分析
1.mysql提供了一個benchmark(int 循環次數,char* 表達式); 可以分析表達式執行所花時間。 例如:
// select BENCHMARK(10000,SHA1('aaaaaaaaaaaaaaaa'))
2.mysql有兩種查詢日志:普通日志和慢速日志。
五、MYSQL高級特性
1.在mysql中,只有myisam存儲引擎支持全文索引。myisam全文索引是一種特殊的具有兩層結構的B樹。
2.存儲引擎事務在存儲引擎內部被賦予acid屬性,分布式(XA)是一種高層次事務,它可以歷喲內部個兩段提交的方式將acid屬性擴展到存儲引擎外部,甚至數據庫外部。階段1:通知所有提交者准備提交 階段2:通知所有參與者進行真正提交。
3.mysql 的字符集和校對規則有 4 個級別的默認設置:服務器級、數據庫級、表級和字段級。Mysql4.1 開始支持 SQL 的子查詢。
/******************************************/ /* 數據庫全名 = [email protected]:3318【mysql】 */ /* 表名稱 = task_new */ /******************************************/ CREATE TABLE `task_new` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `task_name` varchar(128) NOT NULL COMMENT '任務名稱', `image` varchar(128) DEFAULT NULL COMMENT '任務圖標', `description` varchar(1024) NOT NULL COMMENT '任務描述', `content` varchar(1024) NOT NULL COMMENT '任務內容', `finished_message` varchar(128) DEFAULT NULL COMMENT '任務完成提示信息', `task_scope` int(11) NOT NULL COMMENT '任務范圍, 0-平台任務, 1-游戲任務', `series_task` int(11) NOT NULL DEFAULT '0' COMMENT '任務類型: 系列任務,單獨任務', `task_type` int(11) NOT NULL DEFAULT '0' COMMENT '任務類型: 固定任務, 推廣任務, 日常任務', `pre_task` varchar(128) DEFAULT NULL COMMENT '前置任務', `post_task` varchar(128) DEFAULT NULL COMMENT '後置任務', `task_status` int(11) NOT NULL COMMENT '任務狀態, 待審核、未開始、生效中、已暫停、已完成、審核未通過', `auto_task` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否手動任務, 0-否, 1-是', `is_required` tinyint(4) NOT NULL COMMENT '是否必須任務', `event_type` varchar(64) DEFAULT NULL COMMENT '關心的事件類型', `task_target` bigint(20) DEFAULT '0' COMMENT '任務目標', `reset_num` int(11) NOT NULL COMMENT '重置次數', `reset_cycle` int(11) NOT NULL COMMENT '重置周期', `task_interval` int(11) NOT NULL COMMENT '任務間隔', `xiaoer` bigint(20) unsigned NOT NULL COMMENT '創建人', `review_id` bigint(20) unsigned NOT NULL COMMENT '審核人ID', `last_start_time` datetime DEFAULT NULL COMMENT '上次生效時間', `gmt_create` datetime NOT NULL COMMENT '創建時間', `gmt_modified` datetime NOT NULL COMMENT '修改時間', `start_time` datetime NOT NULL COMMENT '開始時間', `end_time` datetime NOT NULL COMMENT '結束時間', `start_condition` varchar(1024) NOT NULL COMMENT '任務觸發條件', `end_condition` varchar(1024) NOT NULL COMMENT '任務完成條件', `enable` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否可用', `rule` varchar(4096) NOT NULL COMMENT '任務規則', `priority` int(11) NOT NULL DEFAULT '1' COMMENT '任務優先級', `progress_rule` varchar(2048) NOT NULL DEFAULT '' COMMENT '進度計算規則', `order_no` int(11) DEFAULT '1' COMMENT '排序號', `classification` int(11) DEFAULT '0' COMMENT '0:默認分類\n1:玩游戲\n2:抽獎', `level` int(11) DEFAULT '0' COMMENT '針對同一個分類,不同的等級', `ext1` longtext COMMENT '擴展字段1(UU中使用該字段指示按鈕跳轉)', `ext2` longtext COMMENT '擴展字段2,暫時預留', `channel` int(11) DEFAULT '0' COMMENT '任務渠道:0-uu或者1-game_box', `consecutive_day` int(11) DEFAULT '1' COMMENT '連續完成任務的天數', `activity` varchar(256) DEFAULT 'default' COMMENT '任務所屬的活動名字', `device` text COMMENT '機型', `packages` text COMMENT '應用', PRIMARY KEY (`id`), KEY `name_channel` (`task_name`,`channel`), KEY `activity` (`activity`(255)) ) ENGINE=InnoDB AUTO_INCREMENT=1194 DEFAULT CHARSET=utf8 COMMENT='任務表';