楔子
我們已經學習了許多 SQL 高級功能,包括空值的處理、連接查詢、子查詢、集合運算、通用表表達式、高級分組選項、窗口函數等等,這些特性可以幫助我們實現各種復雜的數據分析和報表功能。
從今天開始我們將會進入開發篇的學習,了解如何設計數據庫的模式、管理表和操作表中的數據、理解數據庫事務、索引、視圖以及編寫服務器端程序。首先,讓我們來看看如何設計數據庫的結構。
數據庫設計流程
數據庫設計是對數據進行組織和結構化的過程,關鍵問題是數據模型的設計。一個良好的設計對於數據庫系統至關重要,它可以減少系統中的數據冗余、確保數據的一致性和完整性,同時易於維護和擴展。
常見的數據庫設計流程主要包括以下幾個步驟:
- 需求分析,收集和分析用戶對系統的數據存儲和處理需求,記錄需要存儲的數據對象。
- 概念設計,根據需求創建數據庫的概念模型。也就是找出其中的實體、實體的屬性以及實體之間的關系,結果通常是實體關系圖(Entity-Relationship Diagram)。
- 邏輯設計,設計數據庫的關系模式,包括定義表的主鍵和外鍵。另外,還需要通過規范化的流程對關系模式進行優化,減少數據的冗余,維護數據的一致性和完整性。
- 物理設計,結合具體使用的數據庫管理系統,確定物理數據的存儲結構,包括索引的優化等。
- 實施運行,根據設計結果建立實際的數據庫環境,進行測試和上線運行。同時,還包含運行環境的維護、調整和優化。
以上流程之間並不是簡單的順序關系,有可能需要反復迭代;但對於簡單的應用系統,也有可能跳過一些步驟進行快速設計。
接下來我們介紹數據庫設計過程中使用的兩種常用技術:實體關系圖 和 規范化。
實體關系圖
實體關系圖(Entity-Relationship Diagram)是一種用於數據庫設計的結構圖,它描述了數據庫中的實體以及這些實體之間的關系。ERD 包括實體、屬性以及關系三個部分。
實體代表了一種對象或者概念。例如,員工、部門和職位都可以稱為實體。實體在 ERD 中通常使用長方體來表示。
屬性表示實體的某種特性,例如員工擁有姓名、性別、工資、所在部門等屬性。屬性在 ERD 中通常使用橢圓來表示。
關系則用於表示兩個實體之間的相互聯系,在 ERD 中通常使用菱形來表示。三種主要的關系類型是一對一、一對多和多對多關系。例如,一夫一妻制是一種典型的一對一的關系;一個員工只能屬於一個部門,一個部門可以有多個員工,部門和員工是一對多的關系;一個學生可以選修多門課程,一門課程可以被多個學生選擇,學生和課程是多對多的關系。
比如主要包括部門、職位以及員工信息的數據庫模型,它們的 ERD 圖如下所示:
其中,部門和員工的關系是一對多的關系;職位和員工的關系也是一對多的關系。
進一步來說,ERD 可以按照抽象層次分為三種:
- 概念 ERD,即概念數據模型。描述系統中存在的業務對象以及它們之間的聯系,一般給業務分析人員使用。上圖就是一個概念 ERD。
- 邏輯 ERD,即邏輯數據模型。對概念數據模型進一步的分解和細化,轉換為關系模型。同時,還需要引入規范化過程,對關系模式進行優化。
- 物理 ERD,即物理數據模型。物理 ERD 是針對具體數據庫的設計描述,需要為每列指定類型、長度、可否為空等屬性,為表增加主鍵、外鍵以及索引等。
規范化設計
規范化(Normalization)是數據庫設計的一系列原理和技術,主要用於減少表中數據的冗余,增加完整性和一致性。
為什么需要規范化?
假設我們先不考慮規范化,將員工信息、所在部門以及職位信息存儲到一個表中,如下圖所示:
這種表設計相當於將員工信息、所在部門以及職位信息都存儲到一個表中了,顯然會存在如下問題:
數據冗余: 同一個部門的信息存儲了多份, 需要占用更多的磁盤空間; 數據冗余有時候也可能是在不同的表中存儲了重復的數據, 比如最開始的那個關於購買商品的栗子;
插入異常: 如果想要成立一個新的部門, 由於還沒有增加新的員工, 因此無法錄入這個部門的信息;
刪除異常: 如果某個部門的所有員工都被刪除, 該部門的信息也將不復存在;
更新異常: 如果需要修改部門信息, 那么會需要更新多行數據, 效率低下; 不小心忽略了某些記錄的話,將會導致數據不一致;
為了解決這些問題,數據庫引入了規范化過程。規范化使用范式來定義和衡量,關系模式的創始人 Edgar Codd 最早提出了第一范式(1NF)、第二范式(2NF)以及第三范式(3NF)。隨后又出現了更高級別的范式,包括 BC 范式(BCNF)、第四范式(4NF)等。每個范式都基於前面的范式,例如第二范式需要先滿足第一范式。它們之間的關系如下圖所示:
下面我們對范式進行具體的分析,對於大多數的數據庫系統而言,到達第三范式就已經足夠了。
第一范式:
第一范式要求滿足以下條件:
表中的字段都是不可再分的單一屬性;
表需要定義主鍵(PRIMARY KEY);
簡單來說,首先就是每個屬性要有單獨的字段。在上面的不規范設計中,員工的個人電話和工作電話存儲在一個字段中,破壞了原子性。另外,還需要為表定義一個主鍵,用於唯一識別表中的每一行數據;假設每個部門中的員工不會同名,可以使用部門名稱加員工姓名作為主鍵。
將上面的示例修改成以下結構就可以滿足第一范式:
這里我們將原來的"居住信息"拆分成了兩個字段,因為員工的手機號和住址是相互獨立的;此外我們將"部門名稱"和"姓名"作為了聯合主鍵(假設不重復)
,當然更常見的做法是使用數據庫的自增主鍵。
第二范式:
第二范式要求滿足以下條件:
滿足第一范式;
非主鍵字段必須完全依賴於主鍵, 不能只依賴於主鍵的一部分(不能存在部分函數依賴);
A和B能夠確定C,但是單獨的A或B不能確定C,此時C完全依賴於(A, B);A和B能夠確定C,但是單獨的A或B也能確定C,那么C部分依賴於(A, B)。而在第二范式中,非主鍵字段必須要完全依賴於主鍵。
示例中的"部門地址"只取決於"部門名稱"、也就是主鍵的一部分,它和"姓名無關",因此出現了部分函數依賴。顯然,此時部門信息存在冗余,可能帶來各種操作異常。
此時,可以將部門信息和職位信息存儲到別的表中,並通過外鍵讓它們和員工表之間建立一對多的關系。我們可以繼續將表的結構修改如下:
第三范式:
第三范式要求滿足以下條件:
滿足第二范式;
屬性不依賴於其它的非主鍵屬性(不能存在傳遞函數依賴);
怎么理解呢?如果通過A能夠確定B,通過B能夠確定C,但是通過C不能確定A,那么C的傳遞依賴於A。
對於前三個范式而言,只需要將不同的實體/對象單獨存儲到一張表中,並且通過外鍵建立它們之間的聯系即可滿足。
此時,我們再來看看非規范化設計時的幾個問題,會發現都得到了解決:
部門、員工以及職位信息分別存儲一份,通過外鍵保持它們之間的聯系。因此,不存在數據冗余的問題
如果想要成立一個新的部門,直接錄入部門信息即可,解決了插入異常的問題
如果某個部門的所有員工都被刪除,該部門的信息不會受到影響,不存在刪除異常
如果需要修改部門信息,直接更新部門表即可,不會導致數據不一致
但是搞大數據的話,其實是不需要遵循第三范式的。因為搞大數據重點是在數倉的建設,分好層才是最重要的,很多不搞大數據的公司可能要求在設計表的時候,必須嚴格符合第三范式。而如果在數倉建設中嚴格遵循第三范式的話,那么需要很多的外鍵,而為了維護外鍵需要額外的性能。但如果不用外鍵的話,在查詢的時候就又會出現大量的join。
反規范化:
簡單來說,規范化就是將大表拆分成多個小表,並且通過外鍵建立它們之間的聯系。但是,規范化可能導致連接查詢(JOIN)過多,從而降低數據庫的性能。因此,有時候為了提高某些查詢或者應用的性能而故意降低規范反的程度,也就是反規范化(denormalization)。
常用的反規范化方法包括增加冗余字段、增加計算列、將小表合成大表等。例如想要知道每個部門的員工數量的話,需要同時連接部門表和員工表;可以在部門表中增加一個字段(emp_numbers),查詢時就不需要再連接員工表,但是每次增加或者刪除員工時需要更新該字段。
反規范化可能帶來數據完整性的問題;因此,通常我們應該先進行規范化設計,再根據實際情況考慮是否需要反規范化。一般來說,數據倉庫(Data Warehouse)和在線分析系統(OLAP)會使用到反規范化的技術,因為它們以復雜查詢和報表分析為主。
關於外鍵:
在數據庫結構設計時,還有一個經常爭論的問題就是需不需要使用外鍵。
外鍵(FOREIGN KEY)是數據庫用於實現參照完整型的約束。利用數據庫的外鍵可以保證數據的完整性和一致性;級聯操作可以方便數據的自動處理,減少了程序出錯的可能性。
例如,員工屬於部門,員工的部門字段上可以創建一個外鍵引用部門表的主鍵。此時,我們必須先創建部門,然后才能為該部門創建員工;不會出現員工屬於一個不存在的部門的情況,保證了數據的完整性。另一方面,如果要刪除一個部門的話,必須同時處理該部門下的員工;可以選擇級聯刪除員工或者將員工的部門修改為其他部門等操作。
既然外鍵擁有這么多好處,為什么我們還要討論是否需要使用外鍵呢?主要是性能問題。因為任何事情都是有代價的,數據庫為了維護外鍵需要犧牲一定的性能,尤其是在大數據量高並發的情況下。因此出現了另一種解決方案,就是將完整性檢查放到應用層去實現,而應用程序相對比較容易擴展。
不過,在應用端實現約束也可能導致一些問題。首先,無法百分之百保證不會出現問題,尤其是多個應用同時共享一個數據庫時。缺失外鍵可能導致數據庫的結構不明確,需要依賴相應的文檔進行說明。
總之,在系統的設計之初應該盡量使用外鍵確保完整性。如果隨着業務增長出現性能問題,可以考慮在應用中實現約束。
小結
合理的設計是數據庫有效運行和易於擴展的前提,數據庫設計本質上一個多方面因素權衡的過程。利用 ERD 和規范化技術設計數據庫的結構,可以提高數據庫的存儲效率、完整性和可擴展性。