良好的數據庫邏輯設計和物理設計是數據庫獲得高性能的基礎。
數據庫結構優化的目的:
- 減少數據冗餘(相同的數據在多個地方存在);
- 儘量避免數據維護中出現更新,插入和刪除異常(通過範式化設計解決);
- 插入異常:如果表中的某個實體隨着另一個實體而存在,也就是說如果缺少了某個實體,就無法表示另一個實體,這樣設計出來的表就存在插入異常。
- 更新異常:如果更改表中的某個實體的單獨屬性時,需要對多行進行更新,這樣設計出來的表就存在更新異常。
- 刪除異常:如果刪除表中的某一個實體則會導致其他實體的消失,這樣設計出來的表就存在刪除異常。
- 節約數據存儲空間;
- 提高查詢效率。
數據庫設計的步驟:
- 需求分析:全面瞭解產品設計的存儲需求、數據處理需求、數據的安全性和完整性有什麼樣需求;
- 邏輯設計:設計數據的邏輯存儲結構,搞清楚數據實體之間的邏輯關係,解決數據冗餘和數據維護異常;
- 物理設計:根據所使用的數據庫(Oracle、SQLServer、MySQL、Redis)特點進行表結構設計;
- 維護優化:根據實際情況對索引、存儲結構等進行優化。
1.邏輯設計
數據庫邏輯設計一般按照的三範式進行設計:
範式 | 說明 |
---|---|
第一範式 | 1、要求有主鍵,並且要求每一個字段原子性不可再分; 2、設計出來的表都是簡單的二維表。 |
第二範式 | 1、滿足第一範式的要求; 2、要求所有非主鍵字段完全依賴主鍵,不能產生部分依賴。 |
第三範式 | 1、滿足第二範式的要求; 2、所有非主鍵字段和主鍵字段之間不能產生傳遞依賴(例如 學生編號表(學生id,學生姓名,班級編號,班級名稱)中,班級名稱字段存在冗餘,因爲班級名稱字段沒有直接依賴於主鍵,而是依賴於班級編號,班級編號依賴於學生編號,這就是傳遞依賴)。 |
看一個示例:
產品經理要求設計一個電商網站的數據庫結構,需要具備功能:用戶登錄及用戶管理、商品展示及商品管理、供應商管理、在線銷售。
我們按三範式要求進行表設計:
用戶登錄及用戶管理 表設計:
用戶信息表(用戶名, 密碼, 手機號, 姓名, 創建時間, 更新時間), 用戶名爲主鍵
商品展示及商品管理 表設計:
商品信息表(商品id, 商品名稱, 商品價格, 圖書描述, 作者, 創建時間, 更新時間), 商品id爲主鍵
分類信息表(分類id, 分類名稱, 分類描述, 創建時間, 更新時間), 分類id爲主鍵
商品分類關聯表(商品id, 分類id, 創建時間, 更新時間), 商品id、分類id組成聯合主鍵
在線銷售 表設計:
訂單表(訂單id, 下單用戶名, 下單日期, 支付金額, 物流單號), 訂單id爲主鍵
訂單商品關聯表(訂單id, 訂單商品分類id, 訂單商品id, 下單數量), 訂單id、訂單商品分類id、訂單商品id組成聯合主鍵
假設我們要查詢出每一個用戶的訂單總金額,SQL 應該怎麼寫呢?
select 下單用戶名,sum(d.商品價格*b.下單數量)
from 訂單表 a join 訂單商品關聯表 b on a.訂單id=b.訂單id
join 商品分類關聯表 c on c.商品id=b.商品id and c.分類id=b.分類id
join 商品信息表 d on d.商品id=c.商品id
group by 下單用戶名
一共關聯了四張表,對於 MySQL,join 表越多,性能越差。
假設我們要查詢出下單用戶和訂單詳情,SQL 應該怎麼寫呢?
select a.訂單id,e.下單用戶名,e.手機號,d.商品名稱,c.下單數量,d.商品價格
from 訂單表 a join 訂單商品關聯表 b on a.訂單id=b.訂單id
join 商品分類關聯表 c on c.商品id=b.商品id and c.分類id=b.分類id
join 商品信息表 d on d.商品id=c.商品id
join 用戶信息表 e on e.用戶名=a.下單用戶名
比上面那個還要多 join 一張表,完全符合範式化的設計有時並不能得到良好的 SQL 查詢性能。
如果你的應用要求有非常好的查詢性能,那就需要對上面的設計進行一些優化(反範式化設計)。
反範式化是針對範式化而言的,所謂反範式化就是爲了性能和讀取效率的考慮而適當的對數據庫設計範式的要求進行違反,而允許存在少量的數據冗餘,使用空間來換取時間。
在線銷售 表設計:
訂單表(訂單id, 下單用戶名, 下單日期, 支付金額, 物流單號), 訂單id爲主鍵
訂單商品關聯表(訂單id, 訂單商品分類id, 訂單商品id, 下單數量), 訂單id、訂單商品分類id、訂單商品id組成聯合主鍵
在線銷售 表設計 反範式化改造:
訂單表(訂單id, 下單用戶名, 手機號, 下單日期, 支付金額, 物流單號, 訂單金額), 訂單id爲主鍵
訂單商品關聯表(訂單id, 訂單商品分類id, 訂單商品id, 下單數量, 下單商品單價), 訂單id、訂單商品分類id、訂單商品id組成聯合主鍵
此時再看下上面兩個 SQL 改造後的寫法:
查詢出每一個用戶的訂單總金額:
select 下單用戶名,sum(訂單金額)
from 訂單表
group by 下單用戶名
查詢出下單用戶和訂單詳情:
select a.訂單id,a.用戶名,a.手機號,b.商品名稱,b.商品單價,b.商品數量
from 訂單表 a join 訂單商品關聯表 b on a.訂單id=b.訂單id
SQL 簡化了好多,這裏也可以看出,進行數據庫設計的時候,也不能完全按照範式化的要求進行設計,同時也要考慮以後如何使用表。
下面看一下範式化設計和反範式化設計的優缺點:
- | 優點 | 缺點 |
---|---|---|
範式化設計 | 1、可以儘量的減少數據冗餘; 2、範式化的更新操作比反範式化更快; 3、範式化的表通常比反範式化更小。 |
1、對於查詢需要關聯多個表,影響查詢性能; 2、更難對查詢進行索引優化。 |
反範式化設計 | 1、可以減少表的關聯; 2、可以更好的進行索引優化。 |
1、存儲數據容易和數據維護異常; 2、對數據的修改需要更多的物理成本。 |
在範式化設計的基礎上進行反範式優化,這樣才能設計出最符合性能和業務要求的數據庫表結構。
2. 物理設計
物理設計涉及的內容:
- 定義數據庫、表及字段的命名規範;
- 選擇合適的存儲引擎;
- 爲表中的字段選擇合適的數據類型;
- 建立數據庫結構。
1、我們在定義數據庫、表及字段的命名時,要遵守以下幾個原則:
- 可讀性原則(比如使用下劃線分割單詞等);
- 表意性原則;
- 長名原則。
2、選擇合適的存儲引擎:參考 https://blog.csdn.net/smartbetter/article/details/91492332
3、數據類型的選擇:
數據類型的選擇對數據庫性能有着很大的影響,過大的數據類型往往會浪費更多的內存和磁盤I/O。
數據類型的選擇原則:
- 當一個列可以選擇多種數據類型時,應該優先考慮數字類型,其次是日期或二進制類型,最後是字符串類型。對於同樣級別的數據類型,應該優先選擇佔用空間小的數據類型;
選擇正確的整數類型:
列類型 | 存儲空間 | signed取值範圍 | unsigned(無符號)取值範圍 |
---|---|---|---|
tinyint | 1字節 | -128~127 | 0~255 |
smallint | 2字節 | -32768~32767 | 0~65535 |
mediumint | 3字節 | -8388608~8388607 | 0~16777215 |
int | 4字節 | -2147483648~2147483647 | 0~4294967295 |
bigint | 8字節 | -9223372036854775808~9223372036854775807 | 0~18446744073709551615 |
選擇正確的整數類型:
列類型 | 存儲空間 | 是否精確類型 |
---|---|---|
float | 4字節 | 否 |
double | 8字節 | 否 |
decimal | 每4字節存9個數字,小數點佔1個字節 | 是 |
如何選擇varchar和char類型:
列類型 | 存儲空間 | 存儲特點 | 適用場景 |
---|---|---|---|
varchar | 字符串字節長度+額外記錄字符串長度的字節 | 1、用於存儲變長字符串,只佔用必要的存儲空間; 2、列的最大長度小於255則只佔用1個額外字節用於記錄字符串長度; 3、列的最大長度大於255則要佔用2個額外字節用於記錄字符串長度; |
1、字符串列的最大長度比平均長度大很多的場景; 2、字符串很少被更新的場景,更新太頻繁容易產生存儲碎片; 3、使用了多字節字符集存儲字符串的場景; |
char | 字符串字節長度 | 1、char類型是定長的; 2、字符串存儲在char類型的列中會刪除末尾的空格; 3、char類型的最大寬度爲255; |
1、適合存儲長度近似的值,比如MD5值; 2、適合存儲短字符串; 3、適合存儲經常被更新的字符串,避免產生存儲碎片,獲取更高的I/O性能; |
varchar長度的選擇問題:
- 使用最小的符合需求的長度;
- varchar(5) 和 varchar(200) 存儲 ‘MySQL’ 字符串性能不同(MySQL 爲了優化查詢,在內存對字符串使用的是固定的寬度,寬度定義的太長會消耗更多的內存);
如何存儲日期類型:
列類型 | 存儲空間 | 存儲特點 | 適用場景 |
---|---|---|---|
datatime | 8字節 | 1、以YYYY-MM-DD HH:MM:SS[.fraction]格式存儲日期時間; 2、與時區無關; 3、時間範圍1000-01-01 00:00:00到9999-12-31 23:59:59; 4、在行數據修改時可以自動更新值 (MySQL5.6及以上) ; |
最常用的 |
timestamp | 4字節 | 1、存儲列由格林尼治時間1970年1月1日到當前時間的秒數; 2、以YYYY-MM-DD HH:MM:SS[.fraction]格式存儲日期時間; 3、timestamp類型顯示依賴於所指定的時區; 4、時間範圍1970-01-01 00:00:01到2038-01-19 03:14:07; 5、在行數據修改時可以自動更新值; |
|
data (MySQL5.6及以上) | 3字節 | 1、以YYYY-MM-DD格式存儲日期; 2、佔用的字節數比使用字符串、datetime、int存儲要少; 3、使用date還可以利用日期時間函數進行日期之間的計算; 4、時間範圍1000-01-01到9999-12-31 |
|
time (MySQL5.6及以上) | 3字節 | 1、以HH:MM:SS格式存儲時間; |
存儲日期時間數據的注意事項:
- 不要使用字符串類型來存儲日期時間數據;
- 日期時間類型通常比字符串佔用的存儲空間小;
- 日期時間類型在進行查找過濾時可以利用日期來進行對比;
- 日期時間類型有着豐富的處理函數,可以方便的對日期時間類型進行日期計算;
- 使用int存儲日期時間不如使用timestamp類型。
如何爲 Innodb 選擇主鍵?
- 主鍵應該儘可能的小;
- 主鍵應該是順序增長的,可以增加數據的插入效率;
- Innodb的主鍵和業務主鍵可以不同(Innodb的主鍵可以用自增id,業務主鍵增加唯一索引);