1. 程式人生 > >資料庫的資料型別優化

資料庫的資料型別優化

mysql支援非常多的資料型別,在設計表的時候需要精心的為每個列選擇合適的資料型別以提高資料庫的效能,這篇文章回顧了資料庫中常用的幾種資料型別,並總結了一些資料型別優化的技巧。
1.選擇優化的資料型別
mysql支援非常多的資料型別,選擇正確的資料型別對優化效能非常重要,下面幾個原則適用於所有的資料型別。
1) 更小的型別通常更好
一般情況下應該使用能夠正確儲存資料的最小資料型別。小的資料型別意味著更少的磁碟 更少的cpu時間,能獲得更好的效能。
2) 簡單就好
簡單資料型別的操作通常需要更少的cpu週期,比如說整型比字元型的操作代價更低,因為字串和校驗規則(排序規則)使得字串比較操作更加複雜;兩個更具體的例子:應該使用mysql內建的型別來儲存日期和時間而不是字串;應該使用整型來儲存IP地址。
3) 儘量避免NULL
NULL是列的預設屬性,所以很多情況下即使應用程式不需要NULL值,列中儲存的也是NULL.問題在於可為null的列使得索引 索引統計和值的比較都變得麻煩起來,另外可為null的列會使用更多的儲存空間,在mysql裡也需要特殊處理。當可為null的列被索引時,每個索引記錄需要一個額外的位元組。總的來說,如果將資料庫中所有可為null的列改成not null,效能的提升也是有限的,主要需要考慮的其實還是如果計劃在列上建索引,那麼就應該避免這個列是可為null的。
在為列選擇資料型別的時候,顯示要確定合適的大型別,比如數字 字串 時間等,這一步通常是很直觀的;第二步是選擇具體型別,很多mysql都可以儲存相同型別的資料,只是儲存的範圍 長度或精度不一樣,需要磁碟儲存空間不一樣,相同大型別的不同子型別資料有時也會有些特殊的行為和屬性。例如DATETIME 和TIMESAMP都可以用來儲存相同的資料:比如時間和日期,精確到秒。但是TIMESAMP只需要DATETIME一半的儲存空間,並且還能做到根據時區變化,但問題是TIMESAMP可以表示時間的範圍比DATATIME小得多。下面來介紹各個具體的資料型別。
整數型別
有兩種型別的數字:整數和實數。整數分為TINYINT SMALLINT MEDIUMINT INT BIGINT 分別使用8 16 24 32 64位儲存空間;每種型別能表示的數字型別是 -2^(n-1)~2^(n-1)-1,其中n就是儲存空間的位數。整數型別有可選的UNSIGNED,這大致可以使正數的表示範圍擴大一倍,比如TINYINT可以表示的範圍是-128-127,而UNSIGNED TINYINT可以表示的範圍是0~255。有符號整型和無符號整型使用相同的儲存空間,並具有相同的效能,所以可以視情況選擇合適的型別。型別的定義可以決定mysql怎麼在記憶體和磁碟中儲存資料,然而整數計算一般使用64位的BIGINT的,即使是在32位的機器上也是如此。
mysql可以為整數型別指定寬度,例如INT(11),但對於大多數應用這是沒有意義的,它不會限制值的合法範圍,只是規定了一些mysql互動工具(mysql客戶端)顯示字元的個數,對於儲存和計算來說INT(1)和INT(11)是一樣的效果。

實數型別
實數是帶有小數部分的數字,但是也可以用來儲存整數,比如可以用DECIMAL儲存比BIGINT還要大的整數。表示小數的一共有三種類型:FLOAT DOUBLE和DECIMAL,前兩者稱之為浮點型別,這三者都可以指定浮點列所需要的精度。FLOAT佔用4位元組空間,DOUBLE佔用8位元組的空間,相比FLOAT來說可以表示更高的精度和更大的範圍;而在mysql 5.0及其以上版本,DECIMAL允許最多表示65個字元。FLOAT和DOUBLE是儲存型別,內部浮點計算預設使用的都是DOUBLE型別,並且cpu支援原生的浮點計算,所以這種計算比較快,但是會出現浮點誤差;而DECIMAL可以實現精確的小數計算,這是通過mysql自己的機制實現的,效能比較低。對於需要進行精確計算的小數,比如說財務資料,可以將原先的小數乘以一個單位化成BIGINT進行儲存,可以避免浮點數計算不精確和DECIMAL計算代價高的問題。

字串型別
mysql支援多種字串型別,每種型別還有很多變種,並且從mysql4.1開始每個字串列可以定義自己的字符集和排序規則,這些很大程度上會影響效能。
1) VARCHAR和CHAR型別
VARCHAR和CHAR是最常見的兩種字串型別,但是不同儲存引擎在記憶體或磁碟對他們進行儲存的格式是不一樣的,所以得區分不同儲存引擎來研究它們,下面以InnoDB和MYISAM來對其進行研究。
VARCHAR
VARCHAR用來儲存可變長的字串,是最常見的字串型別。它比定長的字串更節省空間,因為它只使用必要的儲存空間。為了實現長度可變,VARCHAR需要額外使用一到二個位元組來記錄字串的長度:如果字串的長度少於255,則使用一個位元組;如果長度大於255,則需要兩個位元組來進行記錄。VARCHAR節省了儲存空間,對效能也有提升。但因為是變長的,如果update操作增加了列的長度,就需要額外的工作,如果某個列的資料長度增加而頁內沒有足夠的儲存空間,這種情況下不會的儲存引擎會有不同的處理策略,比如InnoDB會分裂頁使得資料可以放到頁內。下面的幾種情況可以認為是適合使用VARCHAR的:
列的最大長度,比平均長度大很多;
列的更新很少,所以碎片也會很少;
使用了向utf-8這樣複雜的字符集,每個字元都使用不同長度的位元組進行儲存。
注意在mysql 5.0或更高版本時mysql在儲存和檢索VARCHAR型別資料的時候會保留末尾的空格。
CHAR
CHAR是定長的:總是會根據定義的字串長度分配足夠大的空間。在儲存的時候CHAR會去掉所有末尾的空格,CHAR會根據需要填充空格以便於比較。
CHAR適用於儲存很短的字串或長度都很接近的字串。前者比如用CHAR來儲存”Y”和”N”這兩個字元,會比VARCHAR要少一個位元組的空間(因為VARCHAR還需要一個額外的空間來儲存長度),對於後者,CHAR比較適合用來儲存密碼的MD5值,因為他們長度都是一樣的。對於經常需要更新的列,CHAR也會導致更少的碎片。

對變長和定長的字串的儲存規則是由不同的引擎實現的,Memory引擎對變長的字串也會使用定長的空間進行處理,但是對於空格的擷取和填充,都是一樣的,因為都是在mysql服務層進行的。雖然VARCHAR(5)和VARCHAR(255)儲存”hello”的磁碟大小是一致的,但是考慮到mysql有時候需要建立記憶體臨時表進行操作時,後者需要更大的記憶體空間,所以對於VARCHAR而言也不會是越大越好,分配真正需要的空間才是最好的策略。

BLOB和TEXT
BLOB和TEXT是為了儲存很大的資料而設計的字串資料型別,分別使用二進位制和字元形式進行儲存。實際上他們是不用的資料型別,字元型別是TINYTEXT SMALLTEXT TEXT MEDIUMTEXT LONGTEXT,對應的二進位制型別是 TINYBLOB SMALLBLOB BLOB MEDIUMBLOB LONG。
與其它型別不同mysql將每個BLOB和TEXT都當成一個獨立的物件處理,儲存引擎會對他們進行特殊處理。如果BLOB或TEXT太大,InnoDB會使用”外部的”儲存空間進行儲存,這時候會每個物件在行記憶體儲一個1-4個位元組的指標,然後再外部區域儲存實際的值。BLOB與TEXT不同的地方僅僅在於BLOB儲存的是二進位制資料,沒有字符集和排序規則,而TEXT有字符集和排序規則。
mysql對BLOB和TEXT的排序和對其它型別的排序是不同的,它只對每個列的前max_sort_length位元組進行排序,MYSQL也不能對BLOB和TEXT列的全部長度建立索引,並用索引來消除排序。

使用列舉(ENUM)代替字串型別
可以使用列舉來代替一些常用的字串型別。mysql可以將一組不重複的字串儲存成一個預定義的集合,mysql在儲存列舉時非常緊湊,會根據列表的數量壓縮到一個或兩個位元組中去。mysql在內部會將每個列舉列的值保持為整數,並且在.frm檔案中儲存”數字-字串”的查詢表。下面建立一個僅包含列舉值的表,並向其中插入三個值。

create table enum_test( e ENUM('fish','apple','dog') not null );
insert into enum_test values('fish','dog','apple');

通過下面的語句可以看到列裡面儲存的是個整數的值:

select e+0 from enum_test

查詢結果如下:
這裡寫圖片描述
列舉型別還有個問題就是預設的排序會按列裡儲存的整數而不是其對應的字串進行:

select e from enum_test order by e

這裡寫圖片描述

解決這個問題的方式是按照需要的序列來定義列舉的序列,比如上面可以建立表的時候就定義成 e ENUM(‘apple’,’dog’,’fish’)這種形式,那麼整數的順序和字串的順序就相同了。另一種方式是需要排序的時候,顯式的使用FIELD函式指定順序,如下所示,但這裡的問題是會導致不能使用索引消除排序操作。

select * from enum_test order by  FIELD(e,'apple','dog','fish');

列舉最大的不好是字串集合是固定的,如果想要改變字串集合,刪除或增加字串都需要進行ALTER TABLE操作。
列舉的顯而易見的好處是可以明顯的縮小表的大小,畢竟列舉列只需要儲存一個整數值了。

日期和時間
mysql可以用很多型別來表示日期和時間,例如YEAR和DATE。mysql能儲存的最小時間粒度為秒,但是可以用微秒粒度進行臨時計算。大多數時間型別都沒有什麼替代品,所以唯一的問題是就是儲存的時候需要怎麼做。mysql提供了兩種相似的日期型別:DATETIME和TIMESTAMP,對於很多場景兩種都是適用的,但在一些情況下其中一個會工作的更好些。
DATETIME
這個能儲存的時間範圍是1001-9999,最小的精度是秒;它把日期和時間封裝成YYYYMMDDHHMMSS的整數中,與時區無關,使用8個位元組來儲存。預設情況下mysql會以一種無歧義,可排序的格式顯示DATETIME的值,例如”2018-01-01 12:00:00”,這是ANSI標準定義日期和時間的格式。
TIMESTAMP
這個型別儲存了1970年1月1日午夜(格林威治標準時間)以來的秒數,使用4個位元組進行儲存,能夠表示的最大時間是2038年。mysql提供了FROM_UNIXTIME()函式來將unix時間戳轉換成日期,UNIX_TIMESTAMP()來將日期轉換成時間戳。TIMESTAMP展示的值依賴於時區,而mysql伺服器 客戶端和作業系統都有時區設定,所以如何跨時區訪問日期,那TIMESTAMP和DATETIME表現將不一樣。
TIMESTAMP還有一些DATETIME沒有的特殊屬性,如果在插入的時候沒有指定第一個TIMESTAMP的值,那麼mysql將會自動設定這個列的值為當前時間。在更新的時候,如果沒有在update語句中指定相應的值,那也會自動更新第一個TIMESTAMP的值。插入和更新時候TIMESTAMP的行為是可以配置的,還有一點比較特殊的是TIMESTAMP預設是not null,這和其它型別是不一樣的。
如果一個表中有兩個timestamp欄位,那麼就一定需要給第二個欄位指定預設值,否則建立的時候就會報錯。

create table timestamp_test ( utime TIMESTAMP, ctime TIMESTAMP default CURRENT_TIMESTAMP);

插入和更新timestamp時候都需要用標準展示格式,即’2014-12-08 12:08:02’這種形式,而不能傳入一個unix_timestamp()函式中拿出來的整數。可以呼叫now()函式或表示當前時間的current_timestamp值。
除了特殊情況,通常儘量應該使用TIMESTAMP因為它的儲存空間效率比較高,並且也不要用整型來儲存時間戳,因為不好進行處理。mysql預設最小精度是秒,但如果確實要儲存毫秒或微秒等更小的時間,可以使用BIGINT來進行儲存或者使用double來儲存秒之後的部分。

特殊的資料型別
某些型別的資料並不直接與內建資料型別一致,低於秒級精度的時間戳就是一個例子;另外一個例子是一個IPV4地址,人們經常使用VARCHAR(15)來儲存IP地址,然而它實際上是一個32位無符號的整數,用小數點分隔的字串形式只是為了閱讀方便,所以應該使用無符號整數儲存IP地址,Mysql提供INET_ATON()和INET_NTOA函式在兩種表現方法直接轉換。

2.mysql資料設計中一些陷阱
上面討論的都是通用性問題,但是一些問題是因為mysql的實現機制導致的,所以有必要特別拿出來討論下。這裡討論的就是在mysql下設計資料庫的一些問題。
1) 太多的列
mysql的儲存引擎API工作時需要在服務層和儲存引擎之間通過行緩衝格式拷貝資料,然後在服務層將緩衝內容解碼成各個列。從行緩衝中將編碼過的列轉換成行資料結構的操作代價是非常高的。MYISAM的定長行結構正好與服務層的行結構吻合所以不需要轉換,然後MYISAM的變長行結構和InnoDB的行結構則總是需要轉換。而轉換的代價依賴於列的數量,如果列的數量過多(幾百甚至幾千個欄位),則效能會受到影響。
2) 太多的關聯
所謂”實體-屬性-值”(EAV)設定模式是一個常見的糟糕設計模式,尤其是在mysql下不能靠譜的工作。mysql限制了每個關聯操作最多隻能有61張表,但是很多使用EAV資料庫需要許多自關聯,所以很容易超過這個限制。實際上就算在關聯表數少於61的情況下,解析和優化查詢的代價也會成為mysql的效能問題。一般來說,如果希望查詢執行的快且併發性好,單個查詢最好在12個表內做關聯。
3) 全能的列舉
避免列舉的濫用,主要考慮不要考慮使用列舉作為外來鍵,整數才是最適合做外來鍵的;另外,為列舉集合增加一個值屬於alter table操作。在mysql 5.0之前,alter table是一種阻塞的操作;即使是在mysql 5.1之後,如果不是在集合的末尾增加一個值,也會是阻塞操作。
4) null
之前已經強調過不用null的好處(主要針對列可能被索引的情況),並且建議儘可能使用替代方案。比如說想要儲存一個空值,也可以使用0 空字元等表示。但是如果有時候確實存在未知的值,還是可以使用null的。比如下面這種情況:

create table .....(
dt DATETIME NOTNULL DEFAULT '0000-00-00 00:00:00'
)

這種偽造的全0預設值可能會存在很多問題,還不如使用null來的好。