mysql資料庫開發規範【推薦】
最近一段時間一邊在線上抓取SQL來優化,一邊在整理這個開發規範,儘量減少新的問題SQL進入生產庫。今天也是對公司的開發做了一次培訓,PPT就不放上來了,裡面有十來個生產SQL的案例。因為規範大部分還是具有通用性,所以也借鑑了像去哪兒和趕集的規範,但實際在撰寫本文的過程中,每一條規範的背後無不是在工作中有參照的反面例子的。如果時間可以的話,會抽出一部分或分析其原理,或用案例證明。
一. 命名規範
1.庫名、表名、欄位名必須使用小寫字母,並採用下劃線分割
(1)MySQL有配置引數lower_case_table_names=1,即庫表名以小寫儲存,大小寫不敏感。如果是0,則庫表名以實際情況儲存,大小寫敏感;如果是2,以實際情況儲存,但以小寫比較。
(2)如果大小寫混合使用,可能存在abc,Abc,ABC等多個表共存,容易導致混亂。
(3)欄位名顯示區分大小寫,但實際使⽤時不區分,即不可以建立兩個名字一樣但大小寫不一樣的欄位。
(4)為了統一規範, 庫名、表名、欄位名使用小寫字母。
2.庫名以 d 開頭,表名以 t 開頭,欄位名以 f_ 開頭
(1)比如表 t_crm_relation,中間的 crm 代表業務模組名
(2)檢視以view_開頭,事件以event_開頭,觸發器以trig_開頭,儲存過程以proc_開頭,函式以func_開頭
(3)普通索引以idx_col1_col2命名,唯一索引以uk_col1_col2命名(可去掉f_公共部分)。如 idx_companyid_corpid_contacttime(f_company_id,f_corp_id,f_contact_time)
3.庫名、表名、欄位名禁止超過32個字元,需見名知意
庫名、表名、欄位名支援最多64個字元,但為了統一規範、易於辨識以及減少傳輸量,禁止超過32個字元
4.臨時庫、表名須以tmp加日期為字尾
如 t_crm_relation_tmp0425。備份表也類似,形如 _bak20160425 。
5.按日期時間分表須符合_YYYY[MM][DD]格式
這也是為將來有可能分表做準備的,比如t_crm_ec_record_201403,但像 t_crm_contact_at201506就打破了這種規範。
不具有時間特性的,直接以 t_tbname_001 這樣的方式命名。
二. 庫表基礎規範
1.使用Innodb儲存引擎
5.5版本開始mysql預設儲存引擎就是InnoDB,5.7版本開始,系統表都放棄MyISAM了。
2.表字符集統一使用UTF8
(1)UTF8字符集儲存漢字佔用3個位元組,儲存英文字元佔用一個位元組
(2)校對字符集使用預設的 utf8_general_ci
(3)連線的客戶端也使用utf8,建立連線時指定charset或SET NAMES UTF8;。(對於已經在專案中長期使用latin1的,救不了了)
(4)如果遇到EMOJ等表情符號的儲存需求,可申請使用UTF8MB4字符集
3.所有表都要添加註釋
(1)儘量給欄位也添加註釋
(2)類status型需指明主要值的含義,如”0-離線,1-線上”
4.控制單表字段數量
(1)單表字段數上限30左右,再多的話考慮垂直分表,一是冷熱資料分離,二是大欄位分離,三是常在一起做條件和返回列的不分離。
(2)表字段控制少而精,可以提高IO效率,記憶體快取更多有效資料,從而提高響應速度和併發能力,後續 alter table 也更快。
5.所有表都必須要顯式指定主鍵
(1)主鍵儘量採用自增方式,InnoDB表實際是一棵索引組織表,順序儲存可以提高存取效率,充分利用磁碟空間。還有對一些複雜查詢可能需要自連線來優化時需要用到。
(2)需要全域性唯一主鍵時,使用外部發號器ticket server(建設中)
(3)如果沒有主鍵或唯一索引,update/delete是通過所有欄位來定位操作的行,相當於每行就是一次全表掃描
(4)少數情況可以使用聯合唯一主鍵,需與DBA協商
6.不強制使用外來鍵參考
即使2個表的欄位有明確的外來鍵參考關係,也不使用 FOREIGN KEY ,因為新紀錄會去主鍵表做校驗,影響效能。
7.適度使用儲存過程、檢視,禁止使用觸發器、事件
(1)儲存過程(procedure)雖然可以簡化業務端程式碼,在傳統企業寫複雜邏輯時可能會用到,而在網際網路企業變更是很頻繁的,在分庫分表的情況下要升級一個儲存過程相當麻煩。又因為它是不記錄log的,所以也不方便debug效能問題。如果使用過程,一定考慮如果執行失敗的情況。
(2)使用檢視一定程度上也是為了降低程式碼裡SQL的複雜度,但有時候為了檢視的通用性會損失效能(比如返回不必要的欄位)。
(3)觸發器(trigger)也是同樣,但也不應該通過它去約束資料的強一致性,mysql只支援“基於行的觸發”,也就是說,觸發器始終是針對一條記錄的,而不是針對整個sql語句的,如果變更的資料集非常大的話,效率會很低。掩蓋一條sql背後的工作,一旦出現問題將是災難性的,但又很難快速分析和定位。再者需要ddl時無法使用pt-osc工具。放在transaction執行。
(4)事件(event)也是一種偷懶的表現,目前已經遇到數次由於定時任務執行失敗影響業務的情況,而且mysql無法對它做失敗預警。建立專門的 job scheduler 平臺。
a.單表資料量控制在5000w以內
b.資料庫中不允許儲存明文密碼
三. 欄位規範
1.char、varchar、text等字串型別定義
(1)對於長度基本固定的列,如果該列恰好更新又特別頻繁,適合char
(2)varchar雖然儲存變長字串,但不可太小也不可太大。UTF8最多能存21844個漢字,或65532個英文
(3)varbinary(M)儲存的是二進位制字串,它儲存的是位元組而不是字元,所以沒有字符集的概念,M長度0-255(位元組)。只用於排序或比較時大小寫敏感的型別,不包括密碼儲存
(4)TEXT型別與VARCHAR都類似,儲存可變長度,最大限制也是2^16,但是它20bytes以後的內容是在資料頁以外的空間儲存(row_format=dynamic),對它的使用需要多一次定址,沒有預設值。
一般用於存放容量平均都很大、操作沒有其它欄位那樣頻繁的值。
網上部分文章說要避免使用text和blob,要知道如果純用varchar可能會導致行溢位,效果差不多,但因為每行佔用位元組數過多,會導致buffer_pool能快取的資料行、頁下降。另外text和blob上面一般不會去建索引,而是利用sphinx之類的第三方全文搜尋引擎,如果確實要建立(字首)索引,那就會影響效能。凡事看具體場景。
另外儘可能把text/blob拆到另一個表中
(5)BLOB可以看出varbinary的擴充套件版本,內容以二進位制字串儲存,無字符集,區分大小寫,有一種經常提但不用的場景:不要在資料庫裡儲存圖片。
2.int、tinyint、decimal等數字型別定義
(1)使用tinyint來代替 enum和boolean
ENUM型別在需要修改或增加列舉值時,需要線上DDL,成本較高;ENUM列值如果含有數字型別,可能會引起預設值混淆
tinyint使用1個位元組,一般用於status,type,flag的列
(2)建議使用 UNSIGNED 儲存非負數值
相比不使用 unsigned,可以擴大一倍使用數值範圍
(3)int使用固定4個位元組儲存,int(11)與int(4)只是顯示寬度的區別
(4)使用Decimal 代替float/double儲存精確浮點數
對於貨幣、金額這樣的型別,使用decimal,如 decimal(9,2)。float預設只能能精確到6位有效數字
3.timestamp與datetime選擇
(1)datetime 和 timestamp型別所佔的儲存空間不同,前者8個位元組,後者4個位元組,這樣造成的後果是兩者能表示的時間範圍不同。前者範圍為1000-01-01 00:00:00 ~ 9999-12-31 23:59:59,後者範圍為 1970-01-01 08:00:01 到 2038-01-19 11:14:07 。所以 TIMESTAMP 支援的範圍比 DATATIME 要小。
(2)timestamp可以在insert/update行時,自動更新時間欄位(如 f_set_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP),但一個表只能有一個這樣的定義。
(3)timestamp顯示與時區有關,內部總是以 UTC 毫秒 來存的。還受到嚴格模式的限制
(4)優先使用timestamp,datetime也沒問題
(5)where條件裡不要對時間列上使用時間函式
4.建議欄位都定義為NOT NULL
(1)如果是索引欄位,一定要定義為not null 。因為null值會影響cordinate統計,影響優化器對索引的選擇
(2)如果不能保證insert時一定有值過來,定義時使用default ‘' ,或 0
5.同一意義的欄位定義必須相同
比如不同表中都有 f_user_id 欄位,那麼它的型別、欄位長度要設計成一樣
四. 索引規範
1.任何新的select,update,delete上線,都要先explain,看索引使用情況
儘量避免extra列出現:Using File Sort,Using Temporary,rows超過1000的要謹慎上線。
explain解讀
(1)type:ALL,index,range,ref,eq_ref,const,system,NULL(從左到右,效能從差到好)
(2)possible_keys:指出MySQL能使用哪個索引在表中找到記錄,查詢涉及到的欄位上若存在索引,則該索引將被列出,但不一定被查詢使用
(3)key:表示MySQL實際決定使用的鍵(索引)
如果沒有選擇索引,鍵是NULL。要想強制MySQL使用或忽視possible_keys列中的索引,在查詢中使用FORCE INDEX、USE INDEX或者IGNORE INDEX
(4)ref:表示選擇 key 列上的索引,哪些列或常量被用於查詢索引列上的值
(5)rows:根據表統計資訊及索引選用情況,估算的找到所需的記錄所需要讀取的行數
(6)Extra
a.Using temporary:表示MySQL需要使用臨時表來儲存結果集,常見於排序和分組查詢
b.Using filesort:MySQL中無法利用索引完成的排序操作稱為“檔案排序”
1.索引個數限制
(1)索引是雙刃劍,會增加維護負擔,增大IO壓力,索引佔用空間是成倍增加的
(2)單張表的索引數量控制在5個以內,或不超過表字段個數的20%。若單張表多個欄位在查詢需求上都要單獨用到索引,需要經過DBA評估。
2.避免冗餘索引
(1.)InnoDB表是一棵索引組織表,主鍵是和資料放在一起的聚集索引,普通索引最終指向的是主鍵地址,所以把主鍵做最後一列是多餘的。如f_crm_id作為主鍵,聯合索引(f_user_id,f_crm_id)上的f_crm_id就完全多餘
(2)(a,b,c)、(a,b),後者為冗餘索引。可以利用字首索引來達到加速目的,減輕維護負擔
3.沒有特殊要求,使用自增id作為主鍵
(1.)主鍵是一種聚集索引,順序寫入。組合唯一索引作為主鍵的話,是隨機寫入,適合寫少讀多的表
(2)主鍵不允許更新
4.索引儘量建在選擇性高的列上
(1)不在低基數列上建立索引,例如性別、型別。但有一種情況,idx_feedbackid_type (f_feedback_id,f_type),如果經常用 f_type=1 比較,而且能過濾掉90%行,那這個組合索引就值得建立。有時候同樣的查詢語句,由於條件取值不同導致使用不同的索引,也是這個道理。
(2)索引選擇性計算方法(基數 ÷ 資料行數)
Selectivity = Cardinality / Total Rows = select count(distinct col1)/count(*) from tbname,越接近1說明col1上使用索引的過濾效果越好
(3)走索引掃描行數超過30%時,改全表掃描
5.最左字首原則
(1)mysql使用聯合索引時,從左向右匹配,遇到斷開或者範圍查詢時,無法用到後續的索引列
比如索引idx_c1_c2_c3 (c1,c2,c3),相當於建立了(c1)、(c1,c2)、(c1,c3)三個索引,where條件包含上面三種情況的欄位比較則可以用到索引,但像 where c1=a and c3=c 只能用到c1列的索引,像 c2=b and c3=c等情況就完全用不到這個索引
(2)遇到範圍查詢(>、<、between、like)也會停止索引匹配,比如 c1=a and c2 > 2 and c3=c,只有c1,c2列上的比較能用到索引,(c1,c3)排列的索引才可能會都用上
(3)where條件裡面欄位的順序與索引順序無關,mysql優化器會自動調整順序
6.字首索引
(1)對超過30個字元長度的列建立索引時,考慮使用字首索引,如 idx_cs_guid2 (f_cs_guid(26))表示擷取前26個字元做索引,既可以提高查詢效率,也可以節省空間
(2)字首索引也有它的缺點是,如果在該列上 ORDER BY 或 GROUP BY 時無法使用索引,也不能把它們用作覆蓋索引(Covering Index)
(3)如果在varbinary或blob這種以二進位制儲存的列上建立字首索引,要考慮字符集,括號裡表示的是位元組數
7.合理使用覆蓋索引減少IO
INNODB儲存引擎中,secondary index(非主鍵索引,又稱為輔助索引、二級索引)沒有直接儲存行地址,而是儲存主鍵值。
如果使用者需要查詢secondary index中所不包含的資料列,則需要先通過secondary index查詢到主鍵值,然後再通過主鍵查詢到其他資料列,因此需要查詢兩次。覆蓋索引則可以在一個索引中獲取所有需要的資料列,從而避免回表進行二次查詢,節省IO因此效率較高。
例如SELECT email,uid FROM user_email WHERE uid=xx,如果uid不是主鍵,適當時候可以將索引新增為index(uid,email),以獲得性能提升。
8.儘量不要在頻繁更新的列上建立索引
如不在定義了 ON UPDATE CURRENT_STAMP 的列上建立索引,維護成本太高(好在mysql有insert buffer,會合並索引的插入)
五. SQL設計
1.杜絕直接 SELECT * 讀取全部欄位
即使需要所有欄位,減少網路頻寬消耗,能有效利用覆蓋索引,表結構變更對程式基本無影響
2.能確定返回結果只有一條時,使用 limit 1
在保證資料不會有誤的前提下,能確定結果集數量時,多使用limit,儘快的返回結果。
3.小心隱式型別轉換
(1)轉換規則
a. 兩個引數至少有一個是 NULL 時,比較的結果也是 NULL,例外是使用 <=> 對兩個 NULL 做比較時會返回 1,這兩種情況都不需要做型別轉換
b. 兩個引數都是字串,會按照字串來比較,不做型別轉換
c. 兩個引數都是整數,按照整數來比較,不做型別轉換
d. 十六進位制的值和非數字做比較時,會被當做二進位制串
e. 有一個引數是 TIMESTAMP 或 DATETIME,並且另外一個引數是常量,常量會被轉換為 timestamp
f. 有一個引數是 decimal 型別,如果另外一個引數是 decimal 或者整數,會將整數轉換為 decimal 後進行比較,如果另外一個引數是浮點數,則會把 decimal 轉換為浮點數進行比較
g. 所有其他情況下,兩個引數都會被轉換為浮點數再進行比較。
(2)如果一個索引建立在string型別上,如果這個欄位和一個int型別的值比較,符合第 g 條。如f_phone定義的型別是varchar,但where使用f_phone in (098890),兩個引數都會被當成成浮點型。發生這個隱式轉換並不是最糟的,最糟的是string轉換後的float,mysql無法使用索引,這才導致了效能問題。如果是 f_user_id = ‘1234567' 的情況,符合第 b 條,直接把數字當字串比較。
4.禁止在where條件列上使用函式
(1)會導致索引失效,如lower(email),f_qq % 4。可放到右邊的常量上計算
(2)返回小結果集不是很大的情況下,可以對返回列使用函式,簡化程式開發
5.使用like模糊匹配,%不要放首位
會導致索引失效,有這種搜尋需求是,考慮其它方案,如sphinx全文搜尋
6.涉及到複雜sql時,務必先參考已有索引設計,先explain
(1)簡單SQL拆分,不以程式碼處理複雜為由。
(2)比如 OR 條件: f_phone='10000' or f_mobile='10000',兩個欄位各自有索引,但只能用到其中一個。可以拆分成2個sql,或者union all。
(3)先explain的好處是可以為了利用索引,增加更多查詢限制條件
7.使用join時,where條件儘量使用充分利用同一表上的索引
(1)如 select t1.a,t2.b * from t1,t2 and t1.a=t2.a and t1.b=123 and t2.c= 4 ,如果t1.c與t2.c欄位相同,那麼t1上的索引(b,c)就只用到b了。此時如果把where條件中的t2.c=4改成t1.c=4,那麼可以用到完整的索引
(2)這種情況可能會在欄位冗餘設計(反正規化)時出現
(3)正確選取inner join和left join
8.少用子查詢,改用join
小於5.6版本時,子查詢效率很低,不像Oracle那樣先計運算元查詢後外層查詢。5.6版本開始得到優化
9.考慮使用union all,少使用union,注意考慮去重
(1)union all不去重,而少了排序操作,速度相對比union要快,如果沒有去重的需求,優先使用union all
(2)如果UNION結果中有使用limit,在2個子SQL可能有許多返回值的情況下,各自加上limit。如果還有order by,請找DBA。
10.IN的內容儘量不超過200個
超過500個值使用批量的方式,否則一次執行會影響資料庫的併發能力,因為單SQL只能且一直佔用單CPU,而且可能導致主從複製延遲
11.拒絕大事務
比如在一個事務裡進行多個select,多個update,如果是高頻事務,會嚴重影響MySQL併發能力,因為事務持有的鎖等資源只在事務rollback/commit時才能釋放。但同時也要權衡資料寫入的一致性。
12.避免使用is null,is not null這樣的比較
13.order by .. limit
這種查詢更多的是通過索引去優化,但order by的欄位有講究,比如主鍵id與f_time都是順序遞增,那就可以考慮order by id而非 f_time 。
14.c1 < a order by c2
與上面不同的是,order by之前有個範圍查詢,由前面的內容可知,用不到類似(c1,c2)的索引,但是可以利用(c2,c1)索引。另外還可以改寫成join的方式實現。
15.分頁優化
建議使用合理的分頁方式以提高分頁效率,大頁情況下不使用跳躍式分頁
假如有類似下面分頁語句:
SELECT FROM table1 ORDER BY ftime DESC LIMIT 10000,10;
這種分頁方式會導致大量的io,因為MySQL使用的是提前讀取策略。
推薦分頁方式:
SELECT FROM table1 WHERE ftime < last_time ORDER BY ftime DESC LIMIT 10
即傳入上一次分頁的界值
SELECT * FROM table as t1 inner JOIN (SELECT id FROM table ORDER BY time LIMIT 10000,10) as t2 ON t1.id=t2.id
16.count計數
(1)首先count()、count(1)、count(col1)是有區別的,count()表示整個結果集有多少條記錄,count(1)表示結果集裡以primary key統計數量,絕大多數情況下count()與count(1)效果一樣的,但count(col1)表示的是結果集裡 col1 列 NOT null 的記錄數。優先採用count()
(2)大資料量count是消耗資源的操作,甚至會拖慢整個庫,查詢效能問題無法解決的,應從產品設計上進行重構。例如當頻繁需要count的查詢,考慮使用匯總表
(3)遇到distinct的情況,group by方式可能效率更高。
17.delete,update語句改成select再explain
select最多導致資料庫慢,寫操作才是鎖表的罪魁禍首
18.減少與資料庫互動的次數,儘量採用批量SQL語句
(1)INSERT ... ON DUPLICATE KEY UPDATE ...,插入行後會導致在一個UNIQUE索引或PRIMARY KEY中出現重複值,則執行舊行UPDATE,如果不重複則直接插入,影響1行。
(2)REPLACE INTO類似,但它是衝突時刪除舊行。INSERT IGNORE相反,保留舊行,丟棄要插入的新行。
(3)INSERT INTO VALUES(),(),(),合併插入。
19.杜絕危險SQL
(1)去掉where 1=1 這樣無意義或恆真的條件,如果遇到update/delete或遭到sql注入就恐怖了
(2)SQL中不允許出現DDL語句。一般也不給予create/alter這類許可權,但阿里雲RDS只區分讀寫使用者
六. 行為規範
(1)不允許在DBA不知情的情況下導現網資料
(2)大批量更新,如修復資料,避開高峰期,並通知DBA。直接執行sql的由運維或DBA同事操作
(3)及時處理已下線業務的SQL
(4)複雜sql上線稽核
因為目前還沒有SQL審查機制,複雜sql如多表join,count,group by,主動上報DBA評估。
(5)重要專案的資料庫方案選型和設計必須提前通知DBA參與
總結
以上就是本文的全部內容,希望對大家有所幫助。
參考:
MySQL prepare原理詳解
幾個比較重要的MySQL變數
MySQL主庫binlog(master-log)與從庫relay-log關係程式碼詳解
感謝閱讀,希望朋友們對本站多多支援!