本文摘錄總結自《高性能MySQL》(第三版),將以每章一篇文章的方式帶你們讀這本數據庫經典之做。總結精華,幫你們快速抓住重點信息,節省寶貴時間。git
這章概念性東西比較多,可能有點枯燥。但講了不少底層原理,堅持讀下來仍是會有一些收穫的。程序員
100
多位經驗豐富的開發者參與,在 Github 上得到了近1000
個star
的全棧全平臺開源項目想了解下嗎?
項目地址:github.com/cachecats/c…github
MySQL 支持的數據類型很是多,選擇正確的數據類型相當重要。下面的幾個簡單原則有助於作出更好的選擇。數據庫
更小的一般更好小程序
通常狀況下,應該儘可能使用能夠正確存儲數據的最小數據類型。更小的數據類型一般更快,由於它們佔用更少的磁盤、內存和CPU緩存,而且處理時須要的CPU週期也更少。設計模式
可是要確保沒有低估須要存儲的值的範圍,由於在schema中的多個地方增長數據類型的範圍是一個很是耗時和痛苦的操做。若是沒法肯定哪一個數據類型是最好的,就選擇你認爲不會超過範圍的最小類型。緩存
簡單就好bash
簡單數據類型的操做一般須要更少的CPU週期。例如,整型比字符操做代價更低,由於字符集和校對規則(排序規則)使字符比較比整型比較更復雜。這裏有兩個例子:一個是應該使用MySQL內建的類型(2)而不是字符串來存儲日期和時間,另一個是應該用整型存儲IP地址。稍後咱們將專門討論這個話題。服務器
儘可能避免NULL數據結構
一般狀況下最好指定列爲NOT NULL,除非真的須要存儲NULL值。
若是查詢中包含可爲NULL的列,對MySQL來講更難優化,由於可爲NULL的列使得索引、索引統計和值比較都更復雜。可爲NULL的列會使用更多的存儲空間,在MySQL裏也須要特殊處理。若是計劃在列上建索引,就應該儘可能避免設計成可爲NULL的列。
有兩種類型的數字:整數(whole number)和實數(real number)。若是存儲整數,可使用這幾種整數類型:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分別使用8,16,24,32,64位存儲空間。
整數類型有可選的UNSIGNED屬性,表示不容許負值,這大體可使正數的上限提升一倍。例如TINYINT UNSIGNED能夠存儲的範圍是0~255,而TINYINT的存儲範圍是−128~127。
有符號和無符號類型使用相同的存儲空間,並具備相同的性能,所以能夠根據實際狀況選擇合適的類型。
MySQL能夠爲整數類型指定寬度,例如INT(11),對大多數應用這是沒有意義的:它不會限制值的合法範圍,只是規定了MySQL的一些交互工具(例如MySQL命令行客戶端)用來顯示字符的個數。對於存儲和計算來講,INT(1) 和 INT(20)是相同的。
實數是帶有小數部分的數字。然而,它們不僅是爲了存儲小數部分;也可使用DECIMAL存儲比BIGINT還大的整數。MySQL既支持精確類型,也支持不精確類型。
FLOAT 和 DOUBLE類型支持使用標準的浮點運算進行近似計算。DECIMAL類型用於存儲精確的小數。
CPU不支持對DECIMAL的直接計算,5.0以及更高版本中,MySQL服務器自身實現了DECIMAL的高精度計算。相對而言,CPU直接支持原生浮點計算,因此浮點運算明顯更快。
定義列的時候建議只指定數據類型,不指定精度。
由於須要額外的空間和計算開銷,因此應該儘可能只在對小數進行精確計算時才使用DECIMAL——例如存儲財務數據。但在數據量比較大的時候,能夠考慮使用BIGINT代替DECIMAL,將須要存儲的貨幣單位根據小數的位數乘以相應的倍數便可。
VARCHAR類型用於存儲可變長字符串,是最多見的字符串數據類型。它比定長類型更節省空間,由於它僅使用必要的空間。
VARCHAR須要使用1或2個額外字節記錄字符串的長度:若是列的最大長度小於或等於255字節,則只使用1個字節表示,不然使用2個字節。
VARCHAR節省了存儲空間,因此對性能也有幫助。可是,因爲行是變長的,在UPDATE時可能使行變得比原來更長,這就致使須要作額外的工做。
下面這些狀況下使用VARCHAR是合適的:
最好的策略是隻分配真正須要的空間,不要太慷慨,由於更長的列會消耗更多的內存。
CHAR類型是定長的:MySQL老是根據定義的字符串長度分配足夠的空間。當存儲CHAR值時,MySQL會刪除全部的末尾空格。
CHAR適合存儲很短的字符串,或者全部值都接近同一個長度。例如,CHAR很是適合存儲密碼的MD5值,由於這是一個定長的值。
對於常常變動的數據,CHAR也比VARCHAR更好,由於定長的CHAR類型不容易產生碎片。對於很是短的列,CHAR 比 VARCHAR 在存儲空間上更有效率,由於 VARCHAR 還須要一個記錄長度的額外字節。
BLOB 和 TEXT都是爲存儲很大的數據而設計的字符串數據類型,分別採用二進制和字符方式存儲。
實際上,它們分別屬於兩組不一樣的數據類型家族:字符類型是TINYTEXT,SMALLTEXT,TEXT,MEDIUMTEXT,LONGTEXT;對應的二進制類型是TINYBLOB,SMALLBLOB,BLOB,MEDIUMBLOB,LONGBLOB 。 BLOB 是 SMALLBLOB的同義詞,TEXT 是 SMALLTEXT的同義詞。
MySQL對BLOB 和 TEXT列進行排序與其餘類型是不一樣的:它只對每一個列的最前max_sort_length 字節而不是整個字符串作排序。若是隻須要排序前面一小部分字符,則能夠減少max_sort_length的配置,或者使用ORDER BY SUSTRING(column,length ) 。
有時候可使用枚舉列代替經常使用的字符串類型。枚舉列能夠把一些不重複的字符串存儲成一個預約義的集合。MySQL在存儲枚舉時很是緊湊,會根據列表值的數量壓縮到一個或者兩個字節中。MySQL在內部會將每一個值在列表中的位置保存爲整數,而且在表的.frm文件中保存「數字-字符串」映射關係的「查找表」。
枚舉最很差的地方是,字符串列表是固定的,添加或刪除字符串必須使用ALTER TABLE。除非能接受只在列表末尾添加元素,不然使用枚舉不是個好主意。
MySQL可使用許多類型來保存日期和時間值,例如YEAR 和 DATE。MySQL能存儲的最小時間粒度爲秒(MariaDB支持微秒級別的時間類型)。可是MySQL也可使用微秒級的粒度進行臨時運算,咱們會展現怎麼繞開這種存儲限制。
MySQL 提供兩種類似的日期類型,DATETIME 和 TIMESTAMP。對於不少應用程序,它們都能工做,可是在某些場景,一個比另外一個工做得好。
這個類型能保存大範圍的值,從1001年到9999年,精度爲秒。它把日期和時間封裝到格式爲YYYYMMDDHHMMSS的整數中,與時區無關。使用8個字節的存儲空間。 默認狀況下,MySQL以一種可排序的、無歧義的格式顯示DATETIME值,例如「2008-01-16 22:37:08」。這是ANSI標準定義的日期和時間表示方法。
就像它的名字同樣,TIMETAMP類型保存了從1970年1月1日午夜(格林尼治標準時間)以來的秒數,它和UNIX時間戳相同。TIMESTAMP只使用4個字節的存儲空間,所以它的範圍比DATETIME小得多:只能表示從1970年到2038年。
TIMESTAMP顯示的值也依賴於時區。MySQL服務器、操做系統,以及客戶端鏈接都有時區設置。
有必要強調一下這個區別:若是在多個時區存儲或訪問數據,TIMESTAMP 和 DATETIME的行爲將很不同。前者提供的值與時區有關係,後者則保留文本表示的日期和時間。
在插入數據時若是沒有指定值,會自動填充爲當前時間。
TIMESTAMP 默認爲 NOT NULL。
一般應該儘可能使用 TIMESTAMP,由於它比 DATETIME 空間效率更高。
若是須要存儲比秒更小粒度的日期和時間值,可使用BIGINT類型存儲微秒級別的時間截,或者使用DOUBLE存儲秒以後的小數部分。這兩種方式均可以,或者也可使用MariaDB替代MySQL。
爲標識列(identifier column)選擇合適的數據類型很是重要。
標識列也可能在另外的表中做爲外鍵使用,因此爲標識列選擇數據類型時,應該選擇跟關聯表中的對應列同樣的類型。混用不一樣類型可能致使性能問題,即便沒有性能影響,在比較操做時隱式的類型轉換也可能致使很難發現的錯誤。
在能夠知足值的範圍的需求,而且預留將來增加空間的前提下,應該選擇最小的數據類型。下面是一些小技巧:
整數類型
整數一般是標識列最好的選擇,由於它們很快而且可使用AUTO_INCREMENT 。
ENUM 和 SET 類型
對於標識列來講 ENUM 和 SET 類型一般是比較糟糕的選擇,應儘可能避免用這種類型。
字符串類型
字符串類型很消耗空間,且一般比數字類型慢,因此也應避免使用字符串做爲標識列。
對於徹底「隨機」的字符串也須要多加註意,例如MD5()、SHA1()或者UUID()產生的字符串。這些函數生成的新值會任意分佈在很大的空間內,這會致使INSERT以及一些SELECT語句變得很慢。
若是存儲UUID值,則應該移除「-」符號;或者更好的作法是,用UNHEX()函數轉換UUID值爲16字節的數字,而且存儲在一個BINARY(16)列中。檢索時能夠經過HEX()函數來格式化爲十六進制格式。
某些類型的數據並不直接與內置類型一致。這裏有兩個例子:
低於秒級精度的時間戳
前面也介紹了,建議使用 BIGINT 類型存儲時間戳。
IPv4 地址
人們常用VARCHAR(15)列來存儲IP地址。然而,它們其實是32位無符號整數,不是字符串。用小數點將地址分紅四段的表示方法只是爲了讓人們閱讀容易。因此應該用無符號整數存儲IP地址。MySQL提供INET_ATON() 和 INET_NTOA()函數在這兩種表示方法之間轉換。
雖然有一些廣泛的好或壞的設計原則,但也有一些問題是由MySQL的實現機制致使的,這意味着有可能犯一些只在MySQL下發生的特定錯誤。本節咱們討論設計MySQL的schema的問題。這也許會幫助你避免這些錯誤,而且選擇在MySQL特定實現下工做得更好的替代方案。
MySQL的存儲引擎API工做時須要在服務器層和存儲引擎層之間經過行緩衝格式拷貝數據,而後在服務器層將緩衝內容解碼成各個列。從行緩衝中將編碼過的列轉換成行數據結構的操做代價是很是高的。MyISAM的定長行結構實際上與服務器層的行結構正好匹配,因此不須要轉換。然而,MyISAM的變長行結構和InnoDB的行結構則老是須要轉換。轉換的代價依賴於列的數量。當咱們研究一個CPU佔用很是高的案例時,發現客戶使用了很是寬的表(數千個字段),然而只有一小部分列會實際用到,這時轉換的代價就很是高。若是計劃使用數千個字段,必須意識到服務器的性能運行特徵會有一些不一樣。
所謂的「實體-屬性-值」(EAV)設計模式是一個常見的糟糕設計模式,尤爲是在MySQL下不能靠譜地工做。MySQL限制了每一個關聯操做最多隻能有61張表,可是EAV數據庫須要許多自關聯。咱們見過很多EAV數據庫最後超過了這個限制。事實上在許多關聯少於61張表的狀況下,解析和優化查詢的代價也會成爲MySQL的問題。一個粗略的經驗法則,若是但願查詢執行得快速且併發性好,單個查詢最好在12個表之內作關聯。
注意防止過分使用枚舉(ENUM)。下面是咱們見過的一個例子:
CREATE TABLE ... (
country enum('','0','1','2',...,'31')
複製代碼
這種模式的schema設計很是凌亂。這麼使用枚舉值類型也許在任何支持枚舉類型的數據庫都是一個有問題的設計方案,這裏應該用整數做爲外鍵關聯到字典表或者查找表來查找具體值。可是在MySQL中,當須要在枚舉列表中增長一個新的國家時就要作一次ALTER TABLE操做。在MySQL 5.0以及更早的版本中ALTER TABLE是一種阻塞操做;即便在5.1和更新版本中,若是不是在列表的末尾增長值也會同樣須要ALTER TABLE。
枚舉(ENUM)列容許在列中存儲一組定義值中的單個值,集合(SET)列則容許在列中存儲一組定義值中的一個或多個值。有時候這可能比較容易致使混亂。這是一個例子:
CREATE TABLE ... (
is_default set ('Y','N') NOT NULL default 'N'
複製代碼
若是這裏真和假兩種狀況不會同時出現,那麼毫無疑問應該使用枚舉列代替集合列。
咱們以前寫了避免使用NULL的好處,而且建議儘量地考慮替代方案。即便須要存儲一個事實上的「空值」到表中時,也不必定非得使用NULL。也許可使用0、某個特殊值,或者空字符串做爲代替。
可是遵循這個原則也不要走極端。當確實須要表示未知值時也不要懼怕使用NULL。在一些場景中,使用NULL可能會比某個神奇常數更好。從特定類型的值域中選擇一個不可能的值,例如用−1表明一個未知的整數,可能致使代碼複雜不少,並容易引入bug,還可能會讓事情變得一團糟。處理NULL確實不容易,但有時候會比它的替代方案更好。
對於任何給定的數據一般都有不少種表示方法,從徹底的範式化到徹底的反範式化,以及二者的折中。在範式化的數據庫中,每一個事實數據會出現而且只出現一次。相反,在反範式化的數據庫中,信息是冗餘的,可能會存儲在多個地方。
當爲性能問題而尋求幫助時,常常會被建議對schema進行範式化設計,尤爲是寫密集的場景。這一般是個好建議。由於下面這些緣由,範式化一般可以帶來好處:
範式化的更新操做一般比反範式化要快。
當數據較好地範式化時,就只有不多或者沒有重複數據,因此只須要修改更少的數據。
範式化的表一般更小,能夠更好地放在內存裏,因此執行操做會更快。
不多有多餘的數據意味着檢索列表數據時更少須要DISTINCT或者GROUP BY語句。
範式化設計的schema的缺點是一般須要關聯。稍微複雜一些的查詢語句在符合範式的schema上均可能須要至少一次關聯,也許更多。這不但代價昂貴,也可能使一些索引策略無效。例如,範式化可能將列存放在不一樣的表中,而這些列若是在一個表中本能夠屬於同一個索引。
反範式化的schema由於全部數據都在一張表中,能夠很好地避免關聯。 若是不須要關聯表,則對大部分查詢最差的狀況——即便表沒有使用索引——是全表掃描。當數據比內存大時這可能比關聯要快得多,由於這樣避免了隨機 I/O 。
單獨的表也能使用更有效的索引策略。
範式化和反範式化的schema各有優劣,怎麼選擇最佳的設計?
事實是,徹底的範式化和徹底的反範式化schema都是實驗室裏纔有的東西:在真實世界中不多會這麼極端地使用。在實際應用中常常須要混用,可能使用部分範式化的schema、緩存表,以及其餘技巧。
最多見的反範式化數據的方法是複製或者緩存,在不一樣的表中存儲相同的特定列。在MySQL 5.0和更新版本中,可使用觸發器更新緩存值,這使得實現這樣的方案變得更簡單。
好啦,本章的內容就到這裏啦,咱們下期見~
CodeRiver 是一個免費的項目協做平臺,願景是打通 IT 產業上下游,不管你是產品經理、設計師、程序員或是測試,仍是其餘行業人員,只要有好的創意、想法,均可以來 CodeRiver 免費發佈項目,召集志同道合的隊友一塊兒將夢想變爲現實!
CodeRiver 自己仍是一個大型開源項目,致力於打造全棧全平臺企業級精品開源項目。涵蓋了 React、Vue、Angular、小程序、ReactNative、Android、Flutter、Java、Node 等幾乎全部主流技術棧,主打代碼質量。
目前已經有近 100
名優秀開發者參與,github 上的 star
數量將近 1000
個。每一個技術棧都有多位經驗豐富的大佬坐鎮,更有兩位架構師指導項目架構。不管你想學什麼語言處於什麼技術水平,相信都能在這裏學有所獲。
經過 高質量源碼 + 博客 + 視頻
,幫助每一位開發者快速成長。
您的鼓勵是咱們前行最大的動力,歡迎點贊,歡迎送小星星✨ ~