1. 程式人生 > 其它 >MySQL 資料庫設計規範(二)

MySQL 資料庫設計規範(二)

1. 規範背景與目的

MySQL 資料庫與 Oracle、 SQL Server 等資料庫相比,有其核心上的優勢與劣勢。我們在使用 MySQL 資料庫的時候需要遵循一定規範,揚長避短。本規範旨在幫助或指導 RD、QA、OP 等技術人員做出適合線上業務的資料庫設計。在資料庫變更和處理流程、資料庫表設計、SQL 編寫等方面予以規範,從而為公司業務系統穩定、健康地執行提供保障。

2. 設計規範

2.1 資料庫設計

以下所有規範會按照【高危】、【強制】、【建議】三個級別進行標註,遵守優先順序從高到低。

對於不滿足【高危】和【強制】兩個級別的設計,DBA 會強制打回要求修改。

2.1.1 一般命名規則

  1. 【強制】使用小寫,有助於提高打字速度,避免因大小寫敏感而導致的錯誤。
  2. 【強制】沒有空格,使用下劃線代替。
  3. 【強制】名稱中沒有數字,只有英文字母。
  4. 【強制】有效的可理解的名稱。
  5. 【強制】名稱應該是自我解釋的。
  6. 【強制】名稱不應超過 32 個字元。
  7. 【強制】避免使用字首。

2.1.2 庫

  1. 【強制】遵守以上全部一般命名規則。

  2. 【強制】使用單數。

  3. 【強制】庫的名稱格式:業務系統名稱_子系統名。

  4. 【強制】一般分庫名稱命名格式是庫通配名_編號,編號從 0 開始遞增,比如 northwind_001,以時間進行分庫的名稱格式是庫通配名_時間

  5. 【強制】建立資料庫時必須顯式指定字符集,並且字符集只能是 utf8 或者 utf8mb4。建立資料庫 SQL 舉例:

    create database db_name default character set utf8;
    

2.1.3 表

  1. 【強制】遵守以上全部一般命名規則。
  2. 【強制】使用單數。
  3. 【強制】相關模組的表名與表名之間儘量體現 join 的關係,如 user 表和 user_login 表。
  4. 【強制】建立表時必須顯式指定字符集為 utf8 或 utf8mb4。
  5. 【強制】建立表時必須顯式指定表儲存引擎型別,如無特殊需求,一律為 InnoDB。當需要使用除 InnoDB/MyISAM/Memory 以外的儲存引擎時,必須通過 DBA 稽核才能在生產環境中使用。因為 InnoDB 表支援事務、行鎖、宕機恢復、MVCC 等關係型資料庫重要特性,為業界使用最多的 MySQL 儲存引擎。而這是其它大多數儲存引擎不具備的,因此首推 InnoDB。
  6. 【強制】建表必須有 comment。
  7. 【強制】關於主鍵:(1) 命名為 id,型別為 int 或 bigint,且為 auto_increment;(2) 標識表裡每一行主體的欄位不要設為主鍵,建議設為其它欄位如 user_idorder_id等,並建立 unique key 索引。因為如果設為主鍵且主鍵值為隨機插入,則會導致 InnoDB 內部 page 分裂和大量隨機 I/O,效能下降。
  8. 【建議】核心表(如使用者表,金錢相關的表)必須有行資料的建立時間欄位 create_time 和最後更新時間欄位 update_time,便於排查問題。
  9. 【建議】表中所有欄位必須都是 NOT NULL 屬性,業務可以根據需要定義 DEFAULT 值。因為使用 NULL 值會存在每一行都會佔用額外儲存空間、資料遷移容易出錯、聚合函式計算結果偏差等問題。
  10. 【建議】建議對錶裡的 blobtext 等大欄位,垂直拆分到其它表裡,僅在需要讀這些物件的時候才去 select。
  11. 【建議】反正規化設計:把經常需要 join 查詢的欄位,在其它表裡冗餘一份。如 username 屬性在 user_accountuser_login_log 等表裡冗餘一份,減少 join 查詢。
  12. 【強制】中間表用於保留中間結果集,名稱必須以 tmp_ 開頭。備份表用於備份或抓取源錶快照,名稱必須以 bak_ 開頭。中間表和備份表定期清理。
  13. 【強制】對於超過 100W 行的大表進行 alter table,必須經過 DBA 稽核,並在業務低峰期執行。因為 alter table 會產生表鎖,期間阻塞對於該表的所有寫入,對於業務可能會產生極大影響。

2.1.4 欄位

  1. 【強制】遵守以上全部一般命名規則。
  2. 【建議】儘可能選擇短的或一兩個單詞。
  3. 【強制】避免使用保留字作為欄位名稱:orderdatename 是資料庫的保留字,避免使用它。可以為這些名稱新增字首使其易於理解,如 user_namesignup_date 等。
  4. 【強制】避免使用與表名相同的欄位名,這會在編寫查詢時造成混淆。
  5. 【強制】在資料庫模式上定義外來鍵。
  6. 【強制】避免使用縮寫或基於首字母縮寫詞的名稱。
  7. 【強制】外來鍵列必須具有表名及其主鍵,例如:blog_id 表示來自表部落格的外來鍵 id。

2.1.5 欄位資料型別優化

  1. 【建議】表中的自增列(auto_increment 屬性),推薦使用 bigint 型別。因為無符號 int 儲存範圍為 0~4,294,967,295(不到 43 億),溢位後會導致報錯。

  2. 【建議】業務中選擇性很少的狀態 status、型別 type 等欄位推薦使用 tinytint 或者 smallint 型別節省儲存空間。

  3. 【建議】業務中 IP 地址欄位推薦使用 int 型別,不推薦用 char(15)。因為 int 只佔 4 位元組,可以用如下函式相互轉換,而 char(15) 佔用至少 15 位元組。

    SQL:

    select inet_aton('192.168.2.12');
    select inet_ntoa(3232236044); 
    

    PHP:

    ip2long('192.168.2.12'); 
    long2ip(3530427185);
    

    Java:

    public static long ipToLong(String addr)
    {
        String[] addrArray = addr.split("\\.");
    
        long num = 0;
        for (int i = 0; i < addrArray.length; i++)
        {
            int power = 3 - i;
            num += ((Integer.parseInt(addrArray[i]) % 256 * Math.pow(256, power)));
        }
    
        return num;
    }
    
    public static String longToIp(long i)
    {
        return ((i >> 24) & 0xFF) + "." +
               ((i >> 16) & 0xFF) + "." +
               ((i >> 8) & 0xFF) + "." +
               (i & 0xFF);
    }
    
  4. 【建議】不推薦使用 enumset。 因為它們浪費空間,且列舉值寫死了,變更不方便。推薦使用 tinyintsmallint

  5. 【建議】不推薦使用 blobtext 等型別。它們都比較浪費硬碟和記憶體空間。在載入表資料時,會讀取大欄位到記憶體裡從而浪費記憶體空間,影響系統性能。建議和 PM、RD 溝通,是否真的需要這麼大欄位。InnoDB 中當一行記錄超過 8098 位元組時,會將該記錄中選取最長的一個欄位將其 768 位元組放在原始 page 裡,該欄位餘下內容放在 overflow-page 裡。不幸的是在 compact 行格式下,原始 pageoverflow-page 都會載入。

  6. 【建議】儲存金錢的欄位,建議用 int 以分為單位儲存,最大數值約 4290 萬,程式端乘以 100 和除以 100 進行存取。因為 int 佔用 4 位元組,而 double 佔用 8 位元組,空間浪費。

  7. 【建議】文字資料儘量用 varchar 儲存。因為 varchar 是變長儲存,比 char 更省空間。MySQL server 層規定一行所有文字最多存 65535 位元組,因此在 utf8 字符集下最多存 21844 個字元,超過會自動轉換為 mediumtext 欄位。而 text 在 utf8 字符集下最多存 21844 個字元,mediumtext 最多存 2^24/3 個字元,longtext 最多存 2^32 個字元。一般建議用 varchar 型別,字元數不要超過 2700。

  8. 【建議】時間型別儘量選取 timestamp。因為 datetime 佔用 8 位元組,timestamp 僅佔用 4 位元組,但是範圍為 1970-01-01 00:00:012038-01-01 00:00:00。更為高階的方法,選用 int 來儲存時間,使用 SQL 函式 unix_timestamp()from_unixtime() 來進行轉換。

  • 詳細儲存大小參考下圖:

    型別(同義詞) 儲存長度(BYTES) 最小值(SIGNED/UNSIGNED) 最大值(SIGNED/UNSIGNED)
    整形數字
    TINYINT 1 -128/0 127/255
    SMALLINT 2 -32,768/0 32767/65,535
    MEDIUMINT 3 -8,388,608/0 8388607/16,777,215/
    INT(INTEGER) 4 -2,14,7483,648/0 2147483647/4,294,967,295/
    BIGINT 8 -2^63/0 263-1/264-1
    小數支援
    FLOAT[(M[,D])] 4 or 8 -
    DOUBLE[(M[,D])] (REAL, DOUBLE PRECISION) 8 -
    時間型別
    DATETIME 8 1001-01-01 00:00:00 9999-12-31 23:59:59
    DATE 3 1001-01-01 9999-12-31
    TIME 3 00:00:00 23:59:59
    YEAR 1 1001 9999
    TIMESTAMP 4 1970-01-01 00:00:00

2.1.6 索引設計

  1. 【強制】InnoDB 表必須主鍵為 id int/bigint auto_increment,且主鍵值禁止被更新。
  2. 【建議】主鍵的名稱以 pk_ 開頭,唯一鍵以 uk_ 開頭,普通索引以 ix_ 開頭,一律使用小寫格式,以表名/欄位的名稱或縮寫作為字尾。
  3. 【強制】InnoDB 和 MyISAM 儲存引擎表,索引型別必須為 BTREE;MEMORY 表可以根據需要選擇 HASH 或者 BTREE 型別索引。
  4. 【強制】單個索引中每個索引記錄的長度不能超過 64KB。
  5. 【建議】單個表上的索引個數不能超過 7 個。
  6. 【建議】在建立索引時,多考慮建立聯合索引,並把區分度最高的欄位放在最前面。如列 user_id 的區分度可由 select count(distinct user_id) 計算出來。
  7. 【建議】在多表 join 的 SQL 裡,保證被驅動表的連線列上有索引,這樣 join 執行效率最高。
  8. 【建議】建表或加索引時,保證表裡互相不存在冗餘索引。對於 MySQL 來說,如果表裡已經存在 key(a, b),則 key(a) 為冗餘索引,需要刪除。
  9. 【建議】如果選擇性超過 20%,那麼全表掃描比使用索引效能更優,即沒有設定索引的必要。

2.1.7 分庫分表、分割槽表

  1. 【強制】分割槽表的分割槽欄位(partition-key)必須有索引,或者是組合索引的首列。
  2. 【強制】單個分割槽表中的分割槽(包括子分割槽)個數不能超過 1024。
  3. 【強制】上線前 RD 或者 DBA 必須指定分割槽表的建立、清理策略。
  4. 【強制】訪問分割槽表的 SQL 必須包含分割槽鍵。
  5. 【建議】單個分割槽檔案不超過 2G,總大小不超過 50G。建議總分割槽數不超過 20 個。
  6. 【強制】對於分割槽表執行 alter table 操作,必須在業務低峰期執行。
  7. 【強制】採用分庫策略的,庫的數量不能超過 1024。
  8. 【強制】採用分表策略的,表的數量不能超過 4096。
  9. 【建議】單個分表不超過 500W 行,ibd 檔案大小不超過 2G,這樣才能讓資料分散式變得效能更佳。
  10. 【建議】水平分表儘量用取模方式,日誌、報表類資料建議採用日期進行分表。

2.1.8 字符集

  1. 【強制】資料庫本身庫、表、列所有字符集必須保持一致,為 utf8utf8mb4
  2. 【強制】前端程式字符集或者環境變數中的字符集,與資料庫、表的字符集必須一致,統一為 utf8

2.1.9 程式層 DAO 設計建議

  1. 【建議】新的程式碼不要用 model,推薦使用手動拼 SQL + 繫結變數傳入引數的方式。因為 model 雖然可以使用面向物件的方式操作 db,但是其使用不當很容易造成生成的 SQL 非常複雜,且 model 層自己做的強制型別轉換效能較差,最終導致資料庫效能下降。
  2. 【建議】前端程式連線 MySQL 或者 Redis,必須要有連線超時和失敗重連機制,且失敗重試必須有間隔時間。
  3. 【建議】前端程式報錯裡儘量能夠提示 MySQL 或 Redis 原生態的報錯資訊,便於排查錯誤。
  4. 【建議】對於有連線池的前端程式,必須根據業務需要配置初始、最小、最大連線數,超時時間以及連接回收機制,否則會耗盡資料庫連線資源,造成線上事故。
  5. 【建議】對於 loghistory 型別的表,隨時間增長容易越來越大,因此上線前 RD 或者 DBA 必須建立表資料清理或歸檔方案。
  6. 【建議】在應用程式設計階段,RD 必須考慮並規避資料庫中主從延遲對於業務的影響。儘量避免從庫短時延遲(20 秒以內)對業務造成影響,建議強制一致性的讀開啟事務走主庫,或更新後過一段時間再去讀從庫。
  7. 【建議】多個併發業務邏輯訪問同一塊資料(InnoDB 表)時,會在資料庫端產生行鎖甚至表鎖導致併發下降,因此建議更新類 SQL 儘量基於主鍵去更新。
  8. 【建議】業務邏輯之間加鎖順序儘量保持一致,否則會導致死鎖。
  9. 【建議】對於單表讀寫比大於 10:1 的資料行或單個列,可以將熱點資料放在快取裡(如 Memcached 或 Redis),加快訪問速度,降低 MySQL 壓力。

2.1.10 一個規範的建表語句示例

  • 一個較為規範的建表語句為:

    create table user 
    ( 
        `id`            bigint(11) not null auto_increment, 
        `user_id`       bigint(11) not null comment '使用者 ID', 
        `username`      varchar(45) not null comment '登入名', 
        `email`         varchar(30) not null comment '郵箱', 
        `nickname`      varchar(45) not null comment '暱稱', 
        `avatar`        int(11) not null comment '頭像', 
        `birthday`      date not null comment '生日', 
        `gender`        tinyint(4) default '0' comment '性別', 
        `intro`         varchar(150) default null comment '簡介', 
        `resume_url`    varchar(300) not null comment '簡歷存放地址', 
        `register_ip`   int not null comment '使用者註冊時的源 IP', 
        `review_status` tinyint not null comment '稽核狀態,1-通過,2-稽核中,3-未通過,4-尚未提交稽核', 
        `create_time`   timestamp not null comment '記錄建立的時間', 
        `update_time`   timestamp not null comment '資料修改的時間', 
        
        primary key (`id`), 
        unique key `idx_user_id` (`user_id`), 
        key `idx_username`(`username`), 
        key `idx_create_time`(`create_time`, `review_status`) 
    ) 
    engine = InnoDB
    default charset = utf8 
    comment = '使用者基本資訊'; 
    

2.2 SQL 編寫

2.2.1 DML 語句

  1. 【強制】select 語句必須指定具體欄位名稱,禁止寫成 *。因為 select * 會將不該讀的資料也從 MySQL 裡讀出來,造成網絡卡壓力。
  2. 【強制】insert 語句指定具體欄位名稱,不要寫成 insert into t1 values(…),道理同上。
  3. 【建議】insert into … values(xx),(xx),(xx)…,這裡 xx 的值不要超過 5000 個。值過多雖然上線很快,但會引起主從同步延遲。
  4. 【建議】select 語句不要使用 union,推薦使用 union all,並且 union 子句個數限制在 5 個以內。因為 union all 不需要去重,節省資料庫資源,提高效能。
  5. 【建議】in 值列表限制在 500 以內。例如 select … where user_id in(…500 個以內…),這麼做是為了減少底層掃描,減輕資料庫壓力從而加速查詢。
  6. 【建議】事務裡批量更新資料需要控制數量,進行必要的 sleep,做到少量多次。
  7. 【強制】事務涉及的表必須全部是 InnoDB 表。否則一旦失敗不會全部回滾,且易造成主從庫同步終端。
  8. 【強制】寫入和事務發往主庫,只讀 SQL 發往從庫。
  9. 【強制】除靜態表或小表(100 行以內),dml 語句必須有 where 條件,且使用索引查詢。
  10. 【強制】生產環境禁止使用 hint,如 sql_no_cacheforce indexignore keystraight join 等。因為 hint 是用來強制 sql 按照某個執行計劃來執行,但隨著資料量變化我們無法保證自己當初的預判是正確的,因此我們要相信 MySQL 優化器。
  11. 【強制】where 條件裡等號左右欄位型別必須一致,否則無法利用索引。
  12. 【建議】select|update|delete|replace 要有 where 子句,且 where 子句的條件必需使用索引查詢。
  13. 【強制】生產資料庫中強烈不推薦大表上發生全表掃描,但對於 100 行以下的靜態表可以全表掃描。查詢資料量不要超過錶行數的 25%,否則不會利用索引。
  14. 【強制】where 子句中禁止只使用全模糊的 like 條件進行查詢,必須有其它等值或範圍查詢條件,否則無法利用索引。
  15. 【建議】索引列不要使用函式或表示式,否則無法利用索引。如 where length(name) = 'admin'where user_id + 2 = 10023
  16. 【建議】減少使用 or 語句,可將 or 語句優化為 union,然後在各個 where 條件上建立索引。如 where a = 1 or b = 2 優化為 where a = 1 … union … where b = 2, key(a), key(b)
  17. 【建議】分頁查詢,當 limit 起點較高時,可先用過濾條件進行過濾。如 select a, b, c from t1 limit 10000, 20; 優化為: select a, b, c from t1 where id > 10000 limit 20;

2.2.2 多表連線

  1. 【強制】禁止跨 DB 的 join 語句。因為這樣可以減少模組間耦合,為資料庫拆分奠定堅實基礎。
  2. 【強制】禁止在業務的更新類 SQL 語句中使用 join,比如 update t1 join t2 …
  3. 【建議】不建議使用子查詢,建議將子查詢 SQL 拆開結合程式多次查詢,或使用 join 來代替子查詢。
  4. 【建議】線上環境,多表 join 不要超過 3 個表。
  5. 【建議】多表連線查詢推薦使用別名,且 select 列表中要用別名引用欄位,資料庫.表格式,如 select a from db1.table1 alias1 where …
  6. 【建議】在多表 join 中,儘量選取結果集較小的表作為驅動表,來 join 其它表。

2.2.3 事務

  1. 【建議】事務中 insert|update|delete|replace 語句操作的行數控制在 2000 以內,以及 where 子句中 in 列表的傳參個數控制在 500 以內。
  2. 【建議】批量操作資料時,需要控制事務處理間隔時間,進行必要的 sleep,一般建議值 5-10 秒。
  3. 【建議】對於有 auto_increment 屬性欄位的表的插入操作,併發需要控制在 200 以內。
  4. 【強制】程式設計必須考慮“資料庫事務隔離級別”帶來的影響,包括髒讀、不可重複讀和幻讀。線上建議事務隔離級別為 repeatable-read
  5. 【建議】事務裡包含 SQL 不超過 5 個(支付業務除外)。因為過長的事務會導致鎖資料較久,MySQL 內部快取、連線消耗過多等雪崩問題。
  6. 【建議】事務裡更新語句儘量基於主鍵或 unique key,如 update … where id = XX;,否則會產生間隙鎖,內部擴大鎖定範圍,導致系統性能下降,產生死鎖。
  7. 【建議】儘量把一些典型外部呼叫移出事務,如呼叫 Web Service,訪問檔案儲存等,從而避免事務過長。
  8. 【建議】對於 MySQL 主從延遲嚴格敏感的 select 語句,請開啟事務強制訪問主庫。

2.2.4 排序和分組

  1. 【建議】減少使用 order by,和業務溝通能不排序就不排序,或將排序放到程式端去做。order bygroup bydistinct 這些語句較為耗費 CPU,資料庫的 CPU 資源是極其寶貴的。
  2. 【建議】order bygroup bydistinct 這些 SQL 儘量利用索引直接檢索出排序好的資料。如 where a = 1 order by 可以利用 key(a, b)
  3. 【建議】包含了 order bygroup bydistinct 這些查詢的語句,where 條件過濾出來的結果集請保持在 1000 行以內,否則 SQL 會很慢。

2.2.5 線上禁止使用的 SQL 語句

  1. 【高危】禁用 update|delete t1 … where a = XX limit XX; 這種帶 limit 的更新語句。因為會導致主從不一致,導致資料錯亂。建議加上 order by PK
  2. 【高危】禁止使用關聯子查詢,如 update t1 set … where name in(select name from user where …);,效率極其低下。
  3. 【強制】禁用 procedure、function、trigger、views、event、外來鍵約束。因為他們消耗資料庫資源,降低資料庫例項可擴充套件性。推薦都在程式端實現。
  4. 【強制】禁用 insert into … on duplicate key update … 在高併發環境下,會造成主從不一致。
  5. 【強制】禁止聯表更新語句,如 update t1, t2 where t1.id = t2.id …