高性能數據庫表該如何設計?


 

范式與反范式

優秀的庫表設計是高性能數據庫的基礎。如何才能設計出高性能的庫表結構呢?這里必須要提到數據庫范式。范式是基礎規范,反范式是針對性設計。

范式

范式是關系數據庫理論的基礎,也是我們在設計數據庫結構過程中所要遵循的規則和指導方法。數據庫的設計范式是數據庫設計所需要滿足的規范。只有理解數據庫的設計范式,才能設計出高效率、優雅的數據庫,否則可能會設計出低效的庫表結構。

 

目前關系數據庫有六種范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,還又稱完美范式)。

 

滿足最低要求的叫第一范式,簡稱 1NF。在第一范式基礎上進一步滿足一些要求的為第二范式,簡稱 2NF。其余依此類推。各種范式呈遞次規范,越高的范式數據庫冗余越小。通常所用到的只是前三個范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)。

第一范式

第一范式無重復的列,表中的每一列都是拆分的基本數據項,即列不能夠再拆分成其他幾列,強調的是列的原子性.。

 

如果在實際場景中,一個聯系人有家庭電話和公司電話,那么以“姓名、性別、電話”為表頭的表結構就沒有達到 1NF。要符合 1NF 我們只需把電話列拆分,讓表頭變為姓名、性別、家庭電話、公司電話即可。

第二范式

第二范式屬性完全依賴於主鍵,首先要滿足它符合 1NF,另外還需要包含兩部分內容:

  • 表必須有一個主鍵;

  • 沒有包含在主鍵中的列必須完全依賴於主鍵,而不能只依賴於主鍵的一部分。即要求實體的屬性完全依賴於主關鍵字。所謂完全依賴是指不能存在僅依賴主關鍵字一部分的屬性。

第三范式

第三范式屬性不傳遞依賴於其他非主屬性,首先需要滿足 2NF,另外非主鍵列必須直接依賴於主鍵,不能存在傳遞依賴。即不能存在:非主鍵列 A 依賴於非主鍵列 B,非主鍵列 B 依賴於主鍵的情況。

第二范式和第三范式的區別

  • 第二范式:非主鍵列是否依賴主鍵(包括一列通過某一列間接依賴主鍵),要是有依賴關系就是第二范式;

  • 第三范式:非主鍵列是否直接依賴主鍵,不能是那種通過傳遞關系的依賴。要是符合這種依賴關系就是第三范式。

通過對前三個范式的了解,我們知道 3NF 是 2NF 的子集,2NF 是 1NF 的子集。

設計符合 2NF 的表

接下來以訂單信息表為例,講述如何設計一個符合 2NF 的表,首先,我們看原始的訂單信息表,如下圖所示。

第二范式和第三范式的區別

  • 第二范式:非主鍵列是否依賴主鍵(包括一列通過某一列間接依賴主鍵),要是有依賴關系就是第二范式;

  • 第三范式:非主鍵列是否直接依賴主鍵,不能是那種通過傳遞關系的依賴。要是符合這種依賴關系就是第三范式。

通過對前三個范式的了解,我們知道 3NF 是 2NF 的子集,2NF 是 1NF 的子集。

設計符合 2NF 的表

接下來以訂單信息表為例,講述如何設計一個符合 2NF 的表,首先,我們看原始的訂單信息表,如下圖所示。

 

 

圖中,以訂單編號和商品編號作為聯合主鍵,商品名稱、單位、價格等信息不與主鍵相關,只與編號相關,違反了第二范式。 應該對訂單信息表進行拆分,商品信息單獨一張表,訂單項目一張表,如下所示,拆分分成 3 張表。

  • 包含客戶信息的訂單信息表;

  • 包含商品詳情的商品信息表;

  • 包含訂單詳情的訂單詳情表。

范式優缺點

經過前面的講解和案例分析可知范式具備以下優點:

  • 避免數據冗余,減少維護數據完整性的麻煩;

  • 減少數據庫的空間;

  • 數據變更速度快。

同時,也有如下缺點:

  • 按照范式的規范設計的表,等級越高的范式設計出來的表數量越多。

  • 獲取數據時,表關聯過多,性能較差。

表的數量越多,查詢所需要的時間越多。也就是說所用的范式越高,對數據操作的性能越低。  

反范式

范式是普適的規則,滿足大多數的業務場景的需求。對於一些特殊的業務場景,范式設計的表,無法滿足性能的需求。此時,就需要根據業務場景,在范式的基礎之上進行靈活設計,也就是反范式設計。

 

反范式設計主要從三方面考慮:

  • 業務場景;

  • 相應時間;

  • 字段冗余。

反范式設計就是用空間來換取時間,提高業務場景的響應時間,減少多表關聯。主要的優點如下。

  • 允許適當的數據冗余,業務場景中需要的數據幾乎都可以在一張表上顯示,避免關聯;

  • 可以設計有效的索引。

范式與反范式異同

范式化模型:

  • 數據沒有冗余,更新容易;

  • 當表的數量比較多,查詢數據需要多表關聯時,會導致查詢性能低下。

反范式化模型:

  • 冗余將帶來很好的讀取性能,因為不需要 join 很多表;

  • 雖然需要維護冗余數據,但是對磁盤空間的消耗是可以接受的。

MySQL 使用原則和設計規范

講完范式,接下來我們看看 MySQL 使用中的一些使用原則和設計規范。

 

MySQL 雖然具有很多特性並提供了很多功能,但是有些特性會嚴重影響它的性能,比如,在數據庫里進行計算,寫大事務、大 SQL、存儲大字段等。

 

想要發揮 MySQL 的最佳性能,需要遵循 3 個基本使用原則。

  1. 首先是需要讓 MySQL 回歸存儲的基本職能:MySQL 數據庫只用於數據的存儲,不進行數據的復雜計算,不承載業務邏輯,確保存儲和計算分離;

  2. 其次是查詢數據時,盡量單表查詢,減少跨庫查詢和多表關聯;

  3. 還有就是要杜絕大事務、大 SQL、大批量、大字段等一系列性能殺手

  • 大事務,運行步驟較多,涉及的表和字段較多,容易造成資源的爭搶,甚至形成死鎖。一旦事務回滾,會導致資源占用時間過長。

  • 大 SQL,復雜的 SQL 意味着過多的表的關聯,MySQL 數據庫處理關聯超過 3 張表以上的 SQL 時,占用資源多,性能低下。

  • 大批量,意味着多條 SQL 一次性執行完成,必須確保進行充分的測試,並且在業務低峰時段或者非業務時段執行。

  • 大字段,blob、text 等大字段,盡量少用。必須要用時,盡量與主業務表分離,減少對這類字段的檢索和更新。

下面具體講解數據庫的基本設置規則:

  1. 必須指定默認存儲引擎為 InnoDB,並且禁用 MyISAM 存儲引擎,隨着 MySQL 8.0 版本的發布,所有的數據字典表都已經轉換成了 InnoDB,MyISAM 存儲引擎已成為了歷史。

  2. 默認字符集 UTF8mb4,以前版本的 UTF8 是 UTF8mb3,未包含個別特殊字符,新版本的 UTF8mb4 包含所有字符,官方強烈建議使用此字符集。

  3. 關閉區分大小寫功能。設置 lower_case_tables_name=1,即可關閉區分大小寫功能,即大寫字母 T 和小寫字母 t 一樣。

這里在實踐中有個小問題,如何讓系統中區分大小寫的庫表轉換為不區分大小寫的庫表呢?因為要修改底層數據,還是比較麻煩的,操作步驟如下。

  1. MySQL dump 導出數據庫。

  2. 修改參數 lower_case_tables_name=1。

  3. 導入備份數據時,必須停止數據庫,停止業務,影響非常大。

  4. 開啟 per-table 表空間,開啟后,每張業務表會單獨創建一個獨立於系統表空間的表空間,便於空間的回收,數據的遷移。

MySQL 數據庫提供的功能很全面,但並不是所有的功能性能都高效。

  1. 存儲過程、觸發器、視圖、event。為了存儲計算分離,這類功能盡量在程序中實現。這些功能非常不完整,調試、排錯、監控都非常困難,相關數據字典也不完善,存在潛在的風險。一般在生產數據庫中,禁止使用。

  2. lob、text、enum、set。這些字段類型,在 MySQL 數據庫的檢索性能不高,很難使用索引進行優化。如果必須使用這些功能,一般采取特殊的結構設計,或者與程序結合使用其他的字段類型替代。比如:set 可以使用整型(0,1,2,3)、注釋功能和程序的檢查功能集合替代。

以上是基礎規范的內容,但並不是全部,只是以點帶面,進行粗略的介紹。下面我們開始講解命名規范,統一的規范命名,可以增加可讀性,減少隱式轉換。

規范命名

命名規范如下,命名時的字符取值范圍為:a~z,0~9 和 _(下畫線)。 

  1. 所有表名小寫,不允許駝峰式命名;

  2. 允許使用 -(橫線)和 (空格);如下圖所示,當使用 -(橫線),后台默認會轉化成 @002d;

  3. 不允許使用其他特殊字符作為名稱,減少潛在風險。

 

 

數據庫庫名的命名規則必須遵循“見名知意”的原則,即庫名規則為“數據庫類型代碼 + 項目簡稱 + 識別代碼 + 序號”。

 

這樣包含了更多的業務信息,比如:

  • 出入系統業務生產庫:AOCT、AOCT1、AOCT2;

  • 出入系統業務開發庫:AOCTDEV、AOCTDEV1、AOCTDEV2;

  • 出入系統業務測試庫:AOCTTEST、AOCTTEST1、AOCTTEST2;

  • 只有一個數據庫,則不加序號,否則末尾增加序號;

  • 生產庫不加識別代碼,否則需要增加識別代碼 DEV 或 TEST;

  • 如果只作歷史庫,則只需要項目簡稱 +H+ 序號;

  • 圖例為常用的識別代碼。

表名的命名規則分為:

  1. 單表僅使用 a~z、_;

  2. 分表名稱為“表名_編號”;

  3. 業務表名代表用途、內容:子系統簡稱_業務含義_后綴。

常見業務表類型有:

  • 臨時表,tmp;

  • 備份表,bak;

  • 字典表,dic;

  • 日志表,log。

字段名精確,遵循“見名知意”的原則,格式:名稱_后綴。

  • 避免普遍簡單、有歧義的名稱。

用戶表中,用戶名的字段為 UserName 比 Name 更好。

  • 布爾型的字段,以助動詞(has/is)開頭。

用戶是否有留言 hasmessage,用戶是否通過檢查 ischecked 等。

 

常見后綴如下:

  • 流水號/無意義主鍵,后綴為 id,比如 task_id;

  • 時間,后綴為 time,insert_time。

程序賬號與數據庫名稱保持一致。如果所有的程序賬號都是 root@‘%’,密碼也一樣,很容易錯連到其他的數據庫,造成誤操作。

 

索引命名格式,主要為了區分哪些對象是索引:

  • 前綴_表名(或縮寫)_字段名(或縮寫);

  • 主鍵必須使用前綴“pk_”;

  • UNIQUE 約束必須使用前綴“uk_”;

  • 普通索引必須使用前綴“idx_”。

數據庫規范庫表字段的命名,能夠提高數據庫的易讀性,為數據庫表設計打下基礎。下面我們具體看看表設計的一些規則。

  • 顯式指定需要的屬性;

創建表時顯示指定字符集、存儲引擎、注釋信息等。         

  • 不同系統之間,統一規范;

不同表之間的相同字段或者關聯字段,字段類型/命名要保持一致;庫表字符集和前端程序、中間件必須保持一致的 UTF8mb4。

InnoDB 表的注意事項

  1. 主鍵列,UNSIGNED 整數,使用 auto_increment;禁止手動更新 auto_increment,可以刪除。

  2. 必須添加 comment 注釋。

  3. 必須顯示指定的 engine。

  4. 表必備三字段:id、 xxx_create、 xxx_modified。

    • id 為主鍵,類型為 unsigned bigint 等數字類型;

    • xxx_create、xxx_modified 的類型均為 datetime 類型,分別記錄該條數據的創建時間、修改時間。

備份表/臨時表等常見表的設計規范

備份表/臨時表等常見表的設計規范如下。

  1. 備份表,表名必須添加 bak 和日期,主要用於系統版本上線時,存儲原始數據,上線完成后,必須及時刪除。

  2. 臨時表,用於存儲中間業務數據,定期優化,及時降低表碎片。

  3. 日志類表,首先考慮不入庫,保存成文件,其次如果入庫,明確其生命周期,保留業務需求的數據,定期清理。

  4. 大字段表,把主鍵字段和大字段,單獨拆分成表,並且保持與主表主鍵同步,盡量減少大字段的檢索和更新。

  5. 大表,根據業務需求,從垂直和水平兩個維度進行拆分。

垂直拆分:按列關聯度。

 

水平拆分:

  • 按照時間、地域、范圍等;

  • 冷熱數據(歷史數據歸檔)。

字段設計要求

  1. 根據業務場景需求,選擇合適的類型,最短的長度;確保字段的寬度足夠用,但也不要過寬。所有字段必須為 NOT NULL,空值則指定 default 值,空值難以優化,查詢效率低。比如:人的年齡用 unsigned tinyint(范圍 0~255,人的壽命不會超過 255 歲);海龜就必須是 smallint,但如果是太陽的年齡,就必須是 int;如果是所有恆星的年齡都加起來,那么就必須使用 bigint。

  2. 表字段數少而精,盡量不加冗余列。

  3. 單實例表個數必須控制在 2000 個以內。

  4. 單表分表個數必須控制在 1024 個以內。

  5. 單表字段數上限控制在 20~50 個。

禁用 ENUM、SET 類型。

  • 兼容性不好,性能差。

解決方案:使用 TINYINT,在 COMMENT 信息中標明被枚舉的含義。`is_disable` TINYINT UNSIGNED DEFAULT '0' COMMENT '0:啟用 1:禁用 2:異常’。

 

禁用列為 NULL。

  • MySQL 難以優化 NULL 列;

  • NULL 列加索引,需要額外空間;

  • 含 NULL 復合索引無效。

解決方案:在列上添加 NOT NULL DEFAULT 缺省值。

 

禁止 VARBINARY、BLOB 存儲圖片、文件等。

  • 禁止在數據庫中存儲大文件,例如照片,可以將大文件存儲在對象存儲系統中,數據庫中存儲路徑。

不建議使用 TEXT/BLOB:

  • 處理性能差;

  • 行長度變長;

  • 全表掃描代價大。

解決方案:拆分成單獨的表。

 

存儲字節越小,占用空間越小。盡量選擇合適的整型,如下圖所示。

 

 

  1. 主鍵列,無負數,建議使用 INT UNSIGNED 或者 BIGINT UNSIGNED;預估字段數字取值會超過 42 億,使用 BIGINT 類型。

  2. 短數據使用 TINYINT 或 SMALLINT,比如:人類年齡,城市代碼。

  3. 使用 UNSIGNED 存儲非負數值,擴大正數的范圍。

int(3)/int(5) 區別

int(3)/int(5) 的區別,如下圖所示。

 

 

 

  • 正常顯示沒有區別。

  • 3 和 5 僅是最小顯示寬度而已。

  • 有 zerofill 等擴展屬性時則顯示有區別。

浮點數與定點數區別

浮點數與定點數區別,如下圖所示。

 

 

 

  1. 浮點數:float、double(或 real)。

  2. 定點數:decimal(或 numberic)。

 

從上圖中可以觀察到:

  • 浮點數存在誤差問題;

  • 盡量避免進行浮點數比較;

  • 對貨幣等對精度敏感的數據,應該使用定點數。

N 解釋

字符集都為 UTF8mb4,中文存儲占三個字節,而數據或字母,則只占一個字節。

 

下面看一下字符類型中 N 的解釋。

  • CHAR(N) 和 VARCHAR(N) 的長度 N,不是字節數,是字符數。

  • username 列可以存多少個漢字,占用多少個字節   

  • username 最多能存儲 40 個字符,占用 120 個字節。

Char 與 Varchar 類型

存儲字符串長度相同的全部使用 Char 類型;字符長度不相同的使用 Varchar 類型,不預先分配存儲空間,長度不要超過 255。

 

Char 和 Varchar 占用空間的對比,如下圖所示。

 

 

Varchar 值存儲為 1 字節或 2 字節長度前綴加數據。如果值不超過 255 個字節,則列使用一個字節長度;如果值可能需要超過 255 個字節,則列使用兩個字節長度。

 

為什么超過 255 個字節時,必須使用兩個字節長度。

  • 28=256,1 個字節是 8 位;

  • 216=65535,2 個字節是 16 位。

案例

我們學習了范式和反范式、命名規則、表設計規范,下面通過幾個具體的案例,進一步鞏固這些知識。

IP 處理

  1. 一般使用 Char(15) 進行存儲,但是當進行查找和統計時,字符類型不是很高效。

  2. MySQL 數據庫內置了兩個 IP 相關的函數 INET_ATON()、INET_NTOA(),可以實現 IP 地址和整數的項目轉換。

因此,我們使用 INT UNSIGNED(占用 4 個字節)存儲 IP,非 Char(15)。占 15 個字節。

 

下圖所示,IP:192.168.0.1 與整數之間的轉換

 

 

將 IP 的存儲從字符型轉換成整形,轉化后數字是連續的,提高了查詢性能,使查詢更快,占用空間更小。

TIMESTAMP 處理

同樣的方法,我們使用 MySQL 內置的函數(FROM_UNIXTIME(),UNIX_TIMESTAMP()),可以將日期轉化為數字,用 INT UNSIGNED 存儲日期和時間。

 

下圖示所示,時間 2007-11-30 10:30:19 與整數之間的轉換,轉化后數字是連續的,占用空間更小,並且可以使用索引提升查詢性能。

 

 本案例展示的是,不當的數字類型,導致表無法插入新數據,如下圖所示。

 

 

當我們使用 load data 進行批量加載數據時,會導致 1467 錯誤。根據分析,導致 1467 錯誤是由於 auto_increment 的值,超過了 int 類型的取值范圍。

 

原因分析部分顯示,max(seq_id) 為 2147477751,而 int 的范圍為 -2147483648~ 2147483647,還剩余空間 5896,而程序需要導入 1 萬行,所以報錯。

解決辦法

將 int 改為 bigint 或者將數據分表。

表大小及使用頻率

設計表時,必須考慮表的大小和使用頻率,避免由於取值范圍過小,導致程序運行失敗。

 

對於 InnoDB 表,要求創建一個與業務無關的主鍵,比如:每張表以 id 列為主鍵。但是 id 列非常常見,完全無法表達更深層次的意思,特別是在做兩張表的聯合查詢時,它們都有相同的 id 主鍵的情況下。

 

如果你的程序用的是列名,該如何區分 Accounts 表的 id 和 Bugs 的 id 呢?如下圖所示,列名 id 並不會使查詢變得更加清晰。但如果列名叫作 bug_id 或者 account_id,事情就會變得更加簡單。

 

 我們使用主鍵來定位唯一一條記錄,因此主鍵的列名就應該更加便於理解,如下圖所示。

 

 

  1. 在缺陷跟蹤數據庫中,我們使用 Products 表中的 product_id 主鍵列來關聯產品和對應的聯系人。每個賬號可能對應很多產品,每個產品又引用了一個聯系人,因此產品和帳號之間是多對一的關系

  2. 隨着項目日趨成熟,一個產品可能會有多個聯系人,除了多對一的關系外,還需要支持產品到賬號的一對多的關系。Products 表中的一行數據必須要存儲多個聯系人。

  3. 為了把數據庫表結構的改動控制在最小范圍內,我們決定將 account_id 的類型修改為 Varchar,這樣可以在該列中存儲多個賬號 id,每個賬號 id 之間用逗號分隔。

  4. 這樣的設計似乎是可行的,沒有創建額外的表和列,僅僅改變了一個字段的數據類型。然而,我們來看看這樣的設計所必須承受的性能和數據完整性問題。所有外鍵都合並在一個單元格內,查詢會變成異常困難。只能通過正則表達式進行模糊匹配,不但可能會返回錯誤的結果,而且無法使用索引提高性能。例如:查詢指定產品的賬號時,聯合兩張表將不能使用任何索引。這樣的查詢必然會對兩張表進行全表掃描,並創建一個交叉結果集,然后使用正則表達式遍歷每一行聯合后的數據進行匹配。

  5. 出於性能優化方面的考慮,可能在數據庫的結果中需要使用反范式的設計。上述 Products 表中將列表存儲為以逗號分隔的字符串,就是反范式的一個實例。這個設計只是簡化了存儲,但是性能低下。因此你需要謹慎使用反范式的數據庫設計。盡可能地使用規范化的數據庫設計。

  6. 根據業務需求,我們如何設計合理的反范式,解決方案是:創建一個交叉表。將 account_id 存儲在一張單獨的表中,而不是存儲在 Products 表中,從而確保每個獨立的 account 值都可以占據一行。

 

這張新表 Contacts,實現了 Products 和 Accounts 的多對多關系。當一張表有指向兩張表的外鍵時,稱這種表為交叉表,它實現了兩張表之間的多對多關系。這意味着每個產品都可以通過交叉表和多個賬號關聯;同樣地,一個賬號也可以通過交叉表和多個產品關聯。當我們“查詢指定產品的賬號”時,就可以直接使用下面的聯合查詢語句高效實現。

總結回顧

高性能表設計的規則和案例。

  1. 以高性能為目標,庫表設計以范式為主,根據特殊業務場景使用反范式,允許必要的空間換時間。

  2. 規范數據庫的使用原則,統一規范命名,減少性能隱患,減少隱式轉換。

  3. 高性能表設計的原則:合適的字段、合適的長度、NOT NULL。

  4. 從不同角度思考 IP、timestamp 的轉換,拓寬設計思路。

  5. 規范的命名可提高可讀性,反范式設計可提高查詢性能。

本課時到這里就結束了,主要講了范式和反范式、基礎規范、命名規范、表設計規范、高性能數據庫表實踐,下一課時將分享“高性能索引如何設計”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM