????????良好的邏輯設計和物理設計是高性能的基石, 應該根據系統(tǒng)將要執(zhí)行的查詢語句來設計schema, 這往往需要權衡各種因素。 例如, 反范式的設計可以加快某些類型的查詢, 但同時可能使另一些類型的查詢變慢。比如添加計數表和匯總表是一種很好的優(yōu)化查詢的方式,但這些表的維護成本可能會很高。 MySQL獨有的特性和實現細節(jié)對性能的影響也很大。
4.1選擇優(yōu)化的數據類型
????????MySQL支持的數據類型非常多, 選擇正確的數據類型對于獲得高性能至關重要。 不管存儲哪種類型的數據, 下面幾個簡單的原則都有助于做出更好的選擇。
更小的通常更好
? ??????一般情況下,應該盡最使用可以正確存儲數據的最小數據類型。更小的數據類型通常更快, 因為它們占用更少的磁盤、 內存和CPU緩存, 并且處理時需要的CPU周期也更少。
????????但是要確保沒有低估需要存儲的值的范圍, 因為在schema中的多個地方增加數據類型的范圍是一個非常耗時和痛苦的操作。 如果無法確 ? ?定哪個數據類型是最好的, 就選擇你認為不會超過范圍的最小類型。(如果系統(tǒng)不是很忙或者存儲的數據量不多, 或者是在可以輕易修改設計的早期階段, 那之后修改數據類型也比較容易)。
簡單就好
????????簡單數據類型的操作通常需要更少的CPU周期。 例如, 整型比字符操作代價更低, 因為字符集和校對規(guī)則(排序規(guī)則 )使字符比較比整型比較更復雜。 這里有兩個例子:一個是應該 使用MySQL內建的類型注 2 而不是字符串來存儲日期和時間, 另外一個是應該用整型存儲IP地址。 稍后我們將專門討論這個話題。
盡量避免NULL
????????很多表都包含可為NULL (空值)的列, 即使應用程序并不需要保存NULL 也是如此, 這是因為可為NULL 是列的默認屬性。 通常情況下最好指定列為NOT NULL, 除非真的需要存儲NULL 值。
????????如果查詢中包含可為NULL 的列, 對MySQL來說更難優(yōu)化, 因為可為NULL 的列 使得索引、 索引統(tǒng)計和值比較都更復雜。 可為 N ULL的列會使用更多的存儲空間, 在MySQL里也需要特殊處理。 當可為NULL的列被索引時, 每個索引記錄需要一個額 外的字節(jié), 在MyISAM 里甚至還可能導致固定大小的索引(例如只有一個整數列的索引)變成可變大小的索引。
????????通常把可為NULL 的列改為NOT NULL 帶來的性能提升比較小, 所以(調優(yōu)時)沒有 必要首先在現有schema中查找井修改掉這種情況,除非確定這會導致問題。 但是, 如果計劃在列上建索引, 就應該盡量避免設計成可為 NULL 的列。
????????當然也有例外, 例如值得一提的是, lnnoDB 使用單獨的位 (bit) 存儲NULL 值, 所以對于稀疏數據 有很好的空間效率。 但這一點不適用于MyISAM。
????????在為列選擇數據類型時, 第一步需要確定合適的大類型:數字、 字符串、 時間等。 這通常是很簡單的, 但是我們會提到一些特殊的不是那么直觀的案例。
????????下一步是選擇具體類型。很多MySQL的數據類型可以存儲相同類型的數據, 只是存儲的長度和范圍不一樣、允許的精度不同, 或者需要的物理空間(磁盤和內存空間)不同。相同大類型的不同子類型數據有時也有一些特殊的行為和屬性。
4.1.1整數類型
????????有兩種類型的數字:整數(whole number) 和實數(real number)。 如果存儲整數, 可以使用這幾種整數類型:TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT。 分別使用 8, 16, 24, 32, 64 位存儲空間。 它們可以存儲的值的范圍從-2^(N-1)到2^(N-1)-1,其中N是存儲空間的位數。
????????整數類型有可選的 UNSIGNED 屬性,表示不允許負值,這大致可以使正數的上限提高一倍。 例如 TINYINT. UNSIGNED 可以存儲的范圍是 0 - 255, 而 TINYINT 的存儲范圍是 -128 ~127。
????????有符號和無符號類型使用相同的存儲空間,并具有相同的性能, 因此可以根據實際情況選擇合適的類型。
????????你的選擇決定 MySQL 是怎么在內存和磁盤中保存數據的。 然而, 整數計算一般使用64 位的 BIGINT 整數, 即使在 32 位環(huán)境也是如此。( 一些聚合函數是例外, 它們使用DECIMAL 或 DOUBLE 進行計算)。
????????MySQL 可以為整數類型指定寬度, 例如 INT(11), 對大多數應用這是沒有意義的:它不會限制值的合法范圍,只是規(guī)定了MySQL 的一些交互工具(例如 MySQL 命令行客戶端)用來顯示字符的個數。 對于存儲和計算來說, INT(l) 和 INT(20) 是相同的。
4.1.2實數類型
????????實數是帶有小數部分的數字。 然而, 它們不只是為了存儲小數部分,也可以使用DECIMAL 存儲比 BIGINT 還大的整數。 MySQL 既支持精確類型,也支持不精確類型。
????????FLOAT和DOUBLE類型支持使用標準的浮點運算進行近似計算。 如果需要知道浮點運算是怎么計算的, 則需要研究所使用的平臺的浮點數的具體實現。
????????DECIMAL類型用于存儲精確的小數。 在MySQL5.0和更高版本,DECIMAL類型支持精確 計算。MySQL4.1以及更早版本則使用浮點運算來實現DECIAML的計算, 這樣做會因為精度損失導致一些奇怪的結果。在這些版本的MySQL中,DECIMAL只是一個“存儲類型”。
????????因為CPU不支持對DECIMAL的直接計算,所以在MySQL5.0以及更高版本中,MySQL 服務器自身實現了DECIMAL的高精度計算。 相對而言,CPU直接支持原生浮點計算, 所 以浮點運算明顯更快。
????????浮點和DECIMAL類型都可以指定精度。 對于DECIMAL列, 可以指定小數點前后所允許的 最大位數。這會影響列的空間消耗。MySQL5.0和更高版本將數字打包保存到一個二進制字符串中 (每4個字節(jié)存9個數字)。 例如,DECIMAL(18,9)小數點兩邊將各存儲9個數字, 一共使用9個字節(jié):小數點前的數字用4個字節(jié), 小數點后的數字用4個字節(jié), 小數點本身占1個字節(jié)。
????????MySQL 5.0和更高版本中的DECIMAL類型允許最多65個數字。 而早期的MySQL版本中這個限制是254個數字,并且保存為未壓縮的字符串(每個數字一個字節(jié))。然而,這些(早期) 版本實際上并不能在計算中使用這么大的數字, 因為DECIMAL只是一種存儲格式;在計算中DECIMAL會轉換為DOUBLE類型。
????????有多種方法可以指定浮點列所需要的精度, 這會使得MySQL悄悄選擇不同的數據類型,或者在存儲時對值進行取舍。 這些精度定義是非標準的,所以我們建議只指定數據類型,不指定精度。
????????浮點類型在存儲同樣范圍的值時, 通常比DECIMAL使用更少的空間。FLOAT使用4個字節(jié)存儲。DOUBLE占用8個字節(jié),相比FLOAT有更高的精度和更大的范圍。和整數類型一樣, 能選擇的只是存儲類型; MySQL使用DOUBLE作為內部浮點計算的類型。
????????因為需要額外的空間和計算開銷,所以應該盡量只在對小數進行精確計算時才使用DECIMAL—例如存儲財務數據。 但在數據最比較大的時候, 可以考慮使用BIGINT代替DECIMAL, 將需要存儲的貨幣單位根據小數的位數乘以 相應的倍數即可。 假設要存儲財務數據精確到萬分之一分,則可以把所有金額乘以一百萬,然后 將結果存儲在BIGINT里, 這樣可以同時避免浮點存儲計算不精確和DECIMAL精確計算代價高的問題。
4.1.3字符串類型
????????MySQL支持多種字符串類型, 每種類型還有很多變種。 這些數據類型在4.1和5.0版本發(fā)生了很大的變化,使得情況 更加復雜。從MySQL4.1開始,每個字符串列可以定義自己的字符集和排序規(guī)則,或者說校對規(guī)則(collation)(更多關于這個主題的信息請參考第7章)。 這些東西會很大程度上影響性能。
VARCHAR和CHAR類型
????????VARCHAR和CHAR是兩種最主要的字符串類型。 不幸的是,很難精確地解釋這些值是怎么存儲在磁盤和內存中的,因為這跟存儲引擎的具體實現有關。下面的描述假設使用的存儲引擎是 InnoDB和/或者MyISAM。如果使用的不是這兩種存儲引擎,請參考所使用的存儲引擎的文檔。
????????先看看VARCHAR和CHAR值通常在磁盤上怎么存儲。請注意,存儲引擎存儲CHAR或者VARCHAR值的方式在內存中和在磁盤上可能不一樣,所以MySQL服務器從存儲引擎讀出的值可能需要轉換為另一種存儲格式。下面是關于兩種類型的一些比較。
VARCHAR
????????VARCHAR類型用于存儲可變長字符串,是最常見的字符串數據類型。它比定長類型更節(jié)省空間,因為它僅使用必要的空間(例如, 越短的字符串使用越少的空間)。有一種情況例外,如果MySQL 表使用ROW_FORMAT =FIXED創(chuàng)建的話,每一行都會使用定長存儲,這會很浪費空間。
????????VARCHAR需要使用1 或2個額外字節(jié)記錄字符串的長度:如果列的最大長度小于或等于255 字節(jié),則只使用1個字節(jié)表示,否則使用2個字節(jié)。假設采用latinl字符集,一個VARCHAR(10)的列需要11個字節(jié)的存儲空間。VARCHAR(1000)的列則需要1002個字節(jié),因為需要2個字節(jié)存儲長度信息。
????????VARCHAR節(jié)省了存儲空間,所以對性能也有幫助。但是,由于行是變長的,在UPDATE時可能使行變得比原來更長,這就導致需要做額外的工作。如果一個行占用的空間增長,并且在頁內沒有更多的空間可以存儲,在這種情況下,不同的存儲引擎的處理方式是不一樣的。例如,MyISAM會將行拆成不同的片段存儲,InnoDB則需要分裂頁來使行可以放進頁內。其他一些存儲引擎也許從不在原數據位置更新數據。
????????下面這些情況下使用VARCHAR是合適的: 字符串列的最大長度比平均長度大很多;列的更新很少,所以碎片不是問題,使用了像UTF-8這樣復雜的字符集,每個字符都使用不同的字節(jié)數進行存儲。在5.0 或者更高版本,MySQL 在存儲和檢索時會保留末尾空格。但在4.1 或更老的版本,MySQL會剔除末尾空格。
CHAR
? ??????CHAR類型是定長的: MySQL總是根據定義的字符串長度分配足夠的空間。當存儲CHAR值時,MySQL會刪除所有的末尾空格(在 MySQL4.1 和更老版本中VARCHAR也是這樣實現的——也就是說這些版本中CHAR和VARCHAR在邏輯上是一樣的,區(qū)別只是在存儲格式上)。CHAR值會根據 需要采用空格進行填充以方便比較。 CHAR適合存儲很短的字符串,或者所有值都接近同一個長度。例如,CHAR非常適合存儲密碼的MD5值,因為這是一個定長的值。對于經常變更的數據,CHAR也比VARCHAR 更好,因為定長的 CHAR類型不容易產生碎片。對于非常短的列,CHAR比 VARCHAR在存儲空間上也更有效率。例如用 CHAR(1)來存儲只有Y和N的值,如果采用單字節(jié)字符集只需要一個字節(jié),但是VARCHAR(1)卻需要兩個字節(jié),因為還有 一個記錄長度的額外字節(jié)。
????????數據 如何存儲取決于存儲引擎,并非所有的存儲引擎都會按照相同的方式處理定長和 變長的字符串。Memory引擎只支持定長的行,即使有變長字段也會根據最大長度分配最大空間。不過, 填充和截取空格的行為在不同存儲引擎都是一樣的, 因為這是在 MySQL 服務器層進行處理的。
????????與 CHAR 和 VARCHAR 類似的類型還有 BINARY 和 VARBINARY, 它們存儲的是二進制字符串。 二進制字符串跟常規(guī)字符串非常相似,但是二進制字符串存儲的是字節(jié)碼而不是字符。 填充也不一樣:MySQL 填充 BINARY采用的是 \0 (零字節(jié))而不是空格, 在檢索時也不會去掉填充值
????????當需要存儲二進制數據, 井且希望 MySQL 使用字節(jié)碼而不是字符進行比較時, 這些類型是非常有用的。 二進制比較的優(yōu)勢井不僅僅體現在大小寫敏感上。 MySQL 比較BINARY字符串時, 每次按一個字節(jié), 并且根據該字節(jié)的數值進行比較。 因此, 二進制比 較比字符比較簡單很多, 所以也就更快。
????????使用 VARCHAR(5) 和 VARCHAR(200) 存儲'hello'的空間開銷是一樣的。 那么使用更 短的列有什么優(yōu)勢嗎?
????????事實證明有很大的優(yōu)勢。 更長的列會消耗更多的內存, 因為 MySQL 通常會分配固定大小的內存塊來保存內部值。 尤其是使用內存臨時表進行排序或操作時會特別糟糕。 在利用磁盤臨時表進行排序時也同樣糟糕。
????????所以最好的策略是只分配真正需要的空間。
4.1.4日期和時間類型
????????MySQL可以使用許多類型來保存日期和時間值,例如YEAR和DATE。MySQL能存儲的最小時間粒度為秒(MariaDB支持微秒級別的時間類型)。 但是MySQL 也可以使用微秒級的粒度進行臨時運算, 我們會展示怎么繞開這種存儲限制。
? ??????大部分時間類型 都沒有替代品, 因此沒有什么是最佳選擇的問題。 唯一 的問題是保存日期和時間的時候需要做什么。 MySQL提供兩種相似的日期類型: DATE TIME和 TIMESTAMP。 對于很多應用程序,它們都能工作, 但是在某些場景, 一個比另一個工作得好。 讓我們來看一下。
DATETIME
????????這個類型能保存大范圍的值, 從1001年到9999年, 精度為秒。 它把日期和時間封裝到格式為YYYY MMDDHHMMSS的整數中,與時區(qū)無關。使用8 個字節(jié)的存儲空間。
? ? ? ??默認情況下,MySQL 以一種可排序的、 無歧義的格式顯示 DATETIME 值,例如"2008-01-16 22:37:08"。這是ANSI標準定義的日期和時間表示方法。
TIMESTAMP
????????就像它的名字一樣,TIMETAMP類型保存了從1970年1月1日午夜(格林尼治標準 時間) 以來的 秒數,它和UNIX時間戳相同。TIMESTAMP只使用4個字節(jié)的存儲空間,因此 它的范圍比DATETIME 小得多:只能表示從1970年到2038年。MySQL提供了 FROM_ UNIXTIME ()函數把Unix時間戳轉換為日期,井提供了UNIX_TIMESTAMP()函數把日期轉換為Unix時間戳。
4.2 MySQL schema設計中的陷阱
????????雖然有一些普遍的好或壞的設計原則, 但也有一些問題是由MySQL的實現機制導致的,這意味著有可能犯一些只在MySQL下發(fā)生的特定錯誤。本節(jié)我們討論設計MySQL的schema的問題。這也許會幫助你避免這些錯誤, 井且選擇在MySQL特定實現下工作得更好的替代方案。
太多的列
? ??????MySQL的存儲引擎API工作時需要在服務器層和存儲引擎層之間通過行緩沖格式拷貝數據, 然后在服務器層將緩沖內容解碼成各個列。從行緩沖中將編碼過的列轉換成行數據結構的操作代價是非常高的。MylSAM的定長行結構實際上與服務器層的行結構正好匹配, 所以不需要轉換。然而,MyISAM的變長行結構和InnoDB的行結構則總是需要轉換。轉換的代價依賴千列的數量。當我們研究一個CPU占用非常高的案例時, 發(fā)現客戶使用了非常寬的表(數千個字段), 然而只有一小部分列會實際用到, 這時轉換的代價就非常高。如果計劃使用數千個字段, 必須意識到服務器的性能運行特征會有一些不同。
太多的關聯
????????所謂的“實體-屬性-值"(EAV)設計模式是一個常見的糟糕設計模式, 尤其是在MySQL下不能靠譜地工作。MySQL限制了每個關聯操作最多只能有61張表,但是EAV數據庫需要許多自關聯。我們見過不少EAV數據庫最后超過了這個限制。事實上在許多關聯少千61張表的情況下, 解析和優(yōu)化查詢的代價也會成為MySQL的問題。一個粗略的經驗法則, 如果希望查詢執(zhí)行得快速且井發(fā)性好, 單個查詢最好在12個表以內做關聯。
全能的枚舉
????????注意防止過度使用枚舉(ENUM)。用枚舉值類型也許在任何支持枚舉類型的數據庫都是一個有問題的設計方案, 這里應該用整數作為外鍵關聯到字典表或者查找表來查找具體值。但是在MySQL中, 當需要在枚舉列表中增加一個新的國家時就要做一次ALTER TABLE 操作。在MySQL5.0以及更早的版本中ALTER TABLE 是一種阻塞操作1 即使在5.1和更新版本中, 如果不是在列表的末尾增加值也會一樣需要ALTER TABLE (我們將展示一些駭客式的方法來避免阻塞操作,但是這只是駭客的玩法, 別輕易用在生產環(huán)境中)。
變相的枚舉
????????枚舉(ENUM)列允許在列中存儲一組定義值中的單個值, 集合(SE T)列則允許在列中存儲一組定義值中的一個或多個值。有時候這可能比較容易導致混亂.
非此發(fā)明(Not Invent Here)的NULL
????????我們之前寫了避免使用NULL 的好處,井且建議盡可能地考慮替代方案。即使需要存儲一個事實上的“空值” 到表中時, 也不一定非得使用NULL。也許可以使用0、某個特殊值, 或者空字符串作為代替。但是遵循這個原則也不要走極端。當確實需要表示未知值時也不要害怕使用NULL。在一些場景中, 使用NULL 可能會比某個神奇常數更好。從特定類型的值域中選擇一個不可能的值, 例如用-1代表一個未知的整數, 可能導致代碼復雜很多,井容易引入bug, 還可能會讓事情變得一團糟。處理NULL確實不容易,但有時候會比它的替代方案更好。
4.4緩存表和匯總表
????????有時提升性能最好的方法是在同一張表中保存衍生的冗余數據。然而,有時也需要創(chuàng)建一張完全獨立的匯總表或緩存表(特別是為滿足檢索的需求時)。如果能容許少最的臟數據,這是非常好的方法,但是有時確實沒有選擇的余地(例如,需要避免復雜、昂貴的實時更新操作)。
????????術語“緩存表” 和“匯總表“ 沒有標準的含義。我們用術語“緩存表” 來表示存儲那些可以比較簡單地從schema其他表獲取(但是每次獲取的速度比較慢) 數據的表(例如, 邏輯上冗余的數據)。而術語“匯總表”時,則保存的是使用GROUP BY語句聚合數據的表(例如,數據不是邏輯上冗余的)。也有人使用術語“ 累積表(Roll-Up Table)"稱呼這些表。因為這些數據被“ 累積” 了。
????????仍然以網站為例, 假設需要計算之前24小時內發(fā)送的消息數。在一個很繁忙的網站不可能維護一個實時精確的計數器。作為替代方案,可以每小時生成一張匯總表。這樣也許一條簡單的查詢就可以做到,井且比實時維護計數器要高效得多。缺點是計數器并不是100%精確。
????????如果必須獲得過去24小時準確的消息發(fā)送數量(沒有遺漏),有另外一種選擇。以每小時匯總表為基礎,把前23個完整的小時的統(tǒng)計表中的計數全部加起來, 最后再加上開始階段和結束階段不完整的小時內的計數。
????????不管是哪種方法——不嚴格的計數或通過小范圍查詢填滿間隙的嚴格計數——都比計算 message 表的所有行要有效得多。這是建立匯總表的最關鍵原因。實時計算統(tǒng)計值是很昂貴的操作,因為要么需要掃描表中的大部分數據,要么查詢語句只能在某些特定的索引上才能有效運行,而這類特定索引一般會對UPDATE操作有影響,所以一般不希望創(chuàng)建這樣的索引。計算最活躍的用戶或者最常見的 ”標簽” 是這種操作的典型例子。
????????緩存表則相反,其對優(yōu)化搜索和檢索查詢語句很有效。這些查詢語句經常需要特殊的表和索引結構,跟普通OLTP操作用的表有些區(qū)別。
????????例如,可能會需要很多不同的索引組合來加速各種類型的查詢。這些矛盾的需求有時需要創(chuàng)建一張只包含主表中部分列的緩存表。一個有用的技巧是對緩存表使用不同的存儲引擎。例如,如果主表使用InnoDB, 用MyISAM作為緩存表的引擎將會得到更小的索引占用空間,井且可以做全文搜索。有時甚至想把整個表導出My SQL, 插入到專門的搜索系統(tǒng)中獲得更高的搜索效率,例如Lucene或者 Sphinx搜索引擎。
????????在使用緩存表和匯總表時,必須決定是實時維護數據還是定期重建。哪個更好依賴于應用程序,但是定期重建并不只是節(jié)省資源,也可以保持表不會有很多碎片,以及有完全順序組織的索引(這會更加高效)。
4.6總結
????????良好的schema設計原則是普遍適用的,但MySQL有它自己的實現細節(jié)要注意。 概括來說,盡可能保持任何東西小而簡單總是好的。MySQL喜歡簡單,需要使用數據庫的人應該也同樣會喜歡簡單的原則:
? 盡量避免過度設計,例如會導致極其復雜查詢的schema設計,或者有很多列的表設計(很多的意思是介于有點多和非常多之間)。
? 使用小而簡單的合適數據類型,除非真實數據模型中有確切的需要,否則應該盡可能地避免使用 NULL 值。
? 盡量使用相同的數據類型存儲相似或相關的值,尤其是要在關聯條件中使用的列。
? 注意可變長字符串,其在臨時表和排序時可能導致悲觀的按最大長度分配內存。
? 盡扯使用整型定義標識列。
? 避免使用MySQL已經遺棄的特性,例如指定浮點數的精度,或者整數的顯示寬度。
? 小心使用 ENUM和SET。雖然它們用起來很方便,但是不要濫用,否則有時候會變成陷阱。 最好避免使用 BIT。
????????范式是好的,但是反范式(大多數情況下意味著重復數據) 有時也是必需的,并且能帶來好處。 第5章我們將看到更多的例子。 預先計算、 緩存或生成匯總表也可能獲得很大 的好處。Justin Swanhart的Flexviews工具可以幫助維護匯總表。
????????最后,ALTER TABLE 是讓人痛苦的操作,因為在大部分情況下,它都會鎖表并且重建整張表。 我們展示了一些特殊的場景可以使用駭客方法,但是對大部分場景,必須使用其他更常規(guī)的方法,例如在備機執(zhí)行ALTER井在完成后把它切換為主庫。 本書后續(xù)章節(jié)會 有更多關千這方面的內容。