MySQL效能優化(二):選擇優化的資料型別
良好的設計是高效能的基石,應該根據系統的實際業務需求、使用場景進行設計、優化、再調整,在這其中往往需要權衡各種因素,例如,資料庫表究竟如何劃分、欄位如何選擇合適的資料型別等等問題。
MySQL支援的資料型別非常之多,對於選擇恐懼症的小夥伴而言,苦不可言。大部分人在建立資料庫表時,基本一股腦的使用INT
、VARCHAR
這兩種型別最多,至於長度,則會選擇足夠大即可,避免日後不夠用咋辦。只顧當時一時爽,之後坑誰誰難受。
如果你是一個追求極致、高效的開發者,對於上面的情況肯定是不願讓其發生的。在眾多的資料型別面前,如何選擇正確的資料型別,對於高效能是至關重要的。本文將介紹如何選擇優化的資料型別,來提高MySQL的效能,將會選取最為常用的型別進行說明,便於在實際開發中建立表、優化表字段型別時提供幫助。
一、選擇原則
不管儲存哪種型別的資料,下面幾個簡單的原則將有助於你做出更好的選擇。
1.更小的通常更好
一般情況下,應該儘可能選擇正確儲存資料的最小資料型別。更小的資料型別通常更快,因為它們佔用更少的磁碟空間、記憶體,並且處理時需要的CPU週期更少。
但是,在選擇更小資料型別時,一定不要低估儲存值的範圍,因為後期修改資料型別及長度是一件非常痛苦、耗時的操作。如果無法確定哪個資料型別是最好的,就選擇你認為不會超過範圍的最小型別。
2.簡單就好
簡單的資料型別操作通常需要更少的CPU週期。例如,整型比字元操作代價更低,因為字符集和校隊規則(如:排序規則)使得字元比較比整型比較更復雜。
3.儘量避免用NULL
NULL
NULL
,這其實是一種非常不好的習慣。如果查詢中的欄位值恰巧是設定的NULL
值,對MySQl來說更難優化,因為可為NULL
的欄位使得索引、值比較都更復雜。NULL值不能進行索引,影響索引的統計資訊,影響優化器的判斷。複合索引中只要有一列含有NULL值,那麼這一列對於此複合索引就是無效的。
二、字串型別
字串型別是資料庫中使用頻率最高的資料型別,VARCHAR
和CHAR
是兩種最主要的字串型別,都可以用來儲存字串,但它們儲存和檢索的方式不同。VARCHAR
屬於可變長度的字元型別,而CHAR
屬於固定長度的字元型別。下面是關於這兩種型別的說明、比較。
1.VARCHAR
VARCHAR
型別用於儲存可變長字串,它比定長型別更節省空間,因為它僅使用必要的空間(例如,越短的字串使用最少的空間)。
VARCHAR
需要使用1或2個額外的位元組來記錄字串的長度(如果欄位的最大長度小於或等於255位元組,則只使用1個位元組表示長度,否則使用2個位元組來表示長度)。例如,一個VARCHAR(10)
的欄位需要11個位元組的儲存空間,VARCHAR(1000)
則需要1002個位元組的儲存空間,其中需要2個位元組來儲存長度。
2.CHAR
CHAR
型別是定長的。當資料型別為CHAR
時,MySQL會刪除所有的末尾空格。
CHAR
型別適合儲存很短的字串,或者所有值都接近同一個長度。例如,CHAR
型別非常適合儲存密碼的MD5
值,因為這是一個定長的值。對於經常變更的資料,CHAR
型別也比VARCHAR
型別更好,因為定長的CHAR
型別不容易產生碎片。對於儲存非常短的列,CHAR
型別比VARCHAR
在儲存空間上更有效率。例如,用CHAR(1)
來儲存只有Y和N的值,如果採用VARCHAR(1)
卻需要2個位元組,因為還會有一個記錄長度的額外位元組。
通過下面具體例子來對CHAR
進行說明,有助於更好的理解。這裡建立一張只有一個CHAR(10)
欄位的表char_test
,並往裡面插入三個字串xcbeyond
,注意前後有空格的區別:
mysql> create table char_test(ch char(10)); Query OK, 0 rows affected mysql> insert into char_test(ch) values('xcbeyond'),(' xcbeyond'),('xcbeyond '); Query OK, 3 rows affected Records: 3 Duplicates: 0 Warnings: 0
奇怪的事情發生了,當我們查詢時,會發現第三個字串末尾的空格被自動截斷了。為了更好的顯示出是否有空格,對ch欄位前後拼接'
字元便於檢視對比。
mysql> select concat("'",ch,"'") from char_test; +--------------------+ | concat("'",ch,"'") | +--------------------+ | 'xcbeyond' | | ' xcbeyond' | | 'xcbeyond' | +--------------------+ 3 rows in set
如果用VARCHAR(10)
欄位儲存相同的值,則字串末尾的空格是不會被截斷的。
三、日期型別
MySQL提供了兩種相似的日期型別:DATETIME
和TIMESTAMP
,使用起來傻傻分不清,看完本節後不要再說不知道如何選擇了。
對於應用程式而言,他們都能很好的表示日期,但是再某些場景下,各有不同。接下來讓我們一起看看吧。
1.DATETIME
DATETIME
型別能夠保持很大範圍的日期,從1001年到9999年,精度為秒。它把日期和時間封裝到格式為YYYYMMDDHHMMSS
的整數中,與時區無關,使用8個位元組的儲存空間。
預設情況下,MySQL是以一種可排序、無歧義的格式顯示DATETIME
值,例如2020-03-05 22:38:40
。
2.TIMESTAMP
TIMESTAMP
,從它的名字不難看出,它和UNIX時間戳相同,儲存了從1970年1月1日0時0分0秒以來的秒數。TIMESTAMP
只使用4個位元組的儲存空間,因此它的範圍比DATETIME
小得多,只能表示從1970年到2038年。
TIMESTAMP
顯示的值依賴於時區,MySQL伺服器、作業系統,以及客戶端連線都有時區設定。因此,儲存值為0的TIMESTAMP
在美國東部時區顯示為1969-12-31 19:00:00
,與格林尼治時差5個小時。
通常應該儘量使用TIMESTAMP
,因為它比DATETIME
更節省儲存空間,而且對於跨時區的業務,TIMESTAMP
更為合適。
如果需要儲存比秒更小粒度的日期和時間值該怎麼辦?MySQL目前沒有提供合適的資料型別,但可以採用其他變通的方式,如可以使用自己的儲存格式:可以使用BIGINT
型別儲存微妙級別的時間戳,或者使用DOUBLE
儲存秒之後的小數部分。或者也可以使用MariaDB
資料庫替代MySQL
。
四、TEXT和BLOB型別
一般在儲存少了字串的時候,我們會選擇CHAR
或VARCHAR
型別,而在儲存較大文字等資料時,通常會選擇使用TEXT
和BLOB
。
TEXT
和BLOB
型別都是儲存很大的資料而設計的字串資料型別,分別採用字串和二進位制方式儲存。例如,TEXT通常用來儲存文章內容、日誌等字串內容,而BLOB通常用來儲存圖片、視訊等二進位制資料內容。有如下特點:
TEXT型別有字符集和排序規則。
BLOB型別儲存的是二進位制資料,沒有排序規則或字符集。
MySQL中不能將TEXT和BLOB型別的列進行索引,也不能使用這些索引消除排序。
與其他資料型別不同,MySQL把每個TEXT
和BLOB
型別的值當作一個獨立的物件處理。儲存引擎在儲存時通常會做特殊處理,當它們的值太大時,InnoDB
會使用專門的“外部”儲存區域來進行儲存,此時每個值在行內需要1~4個位元組來儲存一個指標,然後在外部儲存區域儲存實際的值。
在面對TEXT、BLOB之間的選擇時,應該根據實際情況選擇能夠滿足需求的最小儲存型別,接下來主要針對TEXT、BLOB型別存在的一些常見問題進行介紹。
1.在執行了大量的刪除操作時,TEXT
和BLOB
會引起一些效能問題
刪除操作會在資料庫表中留下很大的“空洞”,以後填入這些“空洞”的記錄在插入的效能上會有影響。為了提高效能,建議定期使用OPTIMZE TABLE
功能對這類表進行碎片整理,避免因為“空洞”導致效能問題。
實戰演示驗證說明如下:
1)建立測試表text_test
,欄位id
和context
的型別分別為int(11)
和text
:
mysql> create table text_test(id int(11),context text); Query OK, 0 rows affected
2)往表text_test
中插入大量的資料,這裡使用repeat
函式插入字串:
repeat函式用於字串的複製
mysql> insert into text_test(id,context) values(1,repeat('xcbeyond',1000)); Query OK, 1 row affected mysql> insert into text_test(id,context) values(2,repeat('xcbeyond',1000)); Query OK, 1 row affected mysql> insert into text_test(id,context) values(3,repeat('xcbeyond',1000)); Query OK, 1 row affected mysql> insert into text_test(id,context) values(4,repeat('xcbeyond',1000)); Query OK, 1 row affected mysql> insert into text_test(id,context) values(5,repeat('xcbeyond',1000)); Query OK, 1 row affected mysql> insert into text_test(id,context) values(6,repeat('xcbeyond',1000)); Query OK, 1 row affected ……
3)此時看看錶text_test
的物理檔案大小:
2020/03/07 週六 15:58 540,672 text_test.ibd
這裡顯示資料檔案大小為540Kb
。
4)從表text_test
中刪除一大部分資料,這些資料佔總資料量的2/3:
mysql> delete from text_test where id < 10; Query OK, 9 rows affected
5)在此檢視text_test
的物理檔案大小:
2020/03/07 週六 16:05 573,440 text_test.ibd
奇怪的是,資料檔案大小並沒有因為刪除資料而減少,反而還增加了一點。
6)接下來對錶text_test
進行OPTIMIZE優化操作:
mysql> optimize table text_test; +----------------+----------+----------+-------------------------------------------------------------------+ | Table | Op | Msg_type | Msg_text | +----------------+----------+----------+-------------------------------------------------------------------+ | test.text_test | optimize | note | Table does not support optimize, doing recreate + analyze instead | | test.text_test | optimize | status | OK | +----------------+----------+----------+-------------------------------------------------------------------+ 2 rows in set
7)再次查看錶text_test
的物理檔案大小:
2020/03/07 週六 16:08 458,752 text_test.ibd
可以發現,表的資料檔案大小減少了,則說明“空洞”空間已經被回收了。
2.使用合成索引來提高大文字欄位(TEXT
、BLOB
型別)的查詢效能
合成索引,就是根據大文字欄位的內容建立一個雜湊值,並把這個值儲存在單獨的資料列中,接下來就可以通過檢索雜湊值找到資料行了。
但是,要注意這種技術只能用於精確匹配的查詢(雜湊值對於類似<
或>=
等範圍搜尋操作符是沒有用處的)。可以使用MD5()
函式生成雜湊值,也可以使用SHA1()
或 CRC32()
,或者使用自己的應用程式邏輯來計算雜湊值。請記住數值型雜湊值可以很高效率地儲存。同樣,如果雜湊演算法生成的字串帶有尾部空格,就不要把它們儲存在CHAR
或 VARCHAR
列中,它們會受到尾部空格去除的影響。合成的雜湊索引對於那些 BLOB
或 TEXT
資料列特別有用。用雜湊識別符號值查詢的速度比搜尋BLOB
列本身的速度快很多。
實戰演示驗證說明如下:
1)建立測試表text_test2
,欄位id
、context
、hashValue
欄位型別分別為int(11)
、text
、varchar(40)
:
mysql> create table text_test2(id int(11),context text,hashValue varchar(40)); Query OK, 0 rows affected
2)往表text_test2
中插入資料,其中hashValue
用來存入context
列內容的MD5
值:
mysql> insert into text_test2 values(1,repeat('xcbeyond',10),md5(context)); Query OK, 1 row affected mysql> insert into text_test2 values(2,repeat('xcbeyond',10),md5(context)); Query OK, 1 row affected mysql> select * from text_test2; +----+----------------------------------------------------------------------------------+----------------------------------+ | id | context | hashValue | +----+----------------------------------------------------------------------------------+----------------------------------+ | 1 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 | | 2 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 | +----+----------------------------------------------------------------------------------+----------------------------------+ 2 rows in set
3)如果需要查詢context
列的值,則通過雜湊值hashValue
來查詢:
mysql> select * from text_test2 where hashValue = md5(repeat('xcbeyond',10)); +----+----------------------------------------------------------------------------------+----------------------------------+ | id | context | hashValue | +----+----------------------------------------------------------------------------------+----------------------------------+ | 1 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 | | 2 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 | +----+----------------------------------------------------------------------------------+----------------------------------+ 2 rows in set
上面的例子則是展示了合成索引的用法,由於這種技術只能用於精確匹配,在一定程度上減少 I/O,從而提高查詢效率。
3.在不必要的情況下避免檢索TEXT
、BLOB
型別的值
例如,SELECT *
查詢就不是很好的操作,除非能夠確定作為約束條件的 WHERE 子句只會找到所需要的資料行。否則,很可能毫無目的地在網路上傳輸大量的值。這也是 BLOB 或 TEXT識別符號資訊儲存在合成的索引列中對使用者有所幫助的例子。使用者可以搜尋索引列,決定需要的哪些資料行,然後從符合條件的資料行中檢索 BLOB 或 TEXT 值。
4.把 BLOB 或 TEXT 列分離到單獨的表中
在某些環境中,如果把這些資料列移動到第二張資料表中,可以把原資料表中的資料列轉換為固定長度的資料行格式,那麼它就是有意義的。這會減少主表中的碎片,可以得到固定長度資料行的效能優勢。它還可以使主資料表在執行 SELECT * 查詢的時候不會通過網路傳輸大量的 BLOB 或 TEXT 值。
五、選擇唯一識別符號
唯一識別符號,也就是我們常常所說的主鍵,用於充當表記錄的唯一判斷依據。唯一識別符號,選擇合適的資料型別是非常重要的。
通常唯一識別符號更多的是用來與其它值或者其它表的值進行比較(如,關聯查詢中),標識列也可能在其它表中作為外來鍵使用,所以為標識列選擇資料型別時,應該選擇根關聯表中對應列一樣的型別。
當選擇唯一識別符號的型別時,不僅僅需要考慮儲存型別,還需要考慮MySQL對這種型別怎麼執行計算和比較的,因為比較在SQL查詢中使用最多,而且也是制約效能的最大因素。
一旦選定了一種型別,就一定要確保所有關聯表中都使用相同的型別。因為型別直接往往都是需要精確匹配,混用不同資料型別可能導致效能問題,即使沒有效能影響,在比較操作時隱式型別轉換也可能導致很難發現的錯誤問題。
在可以滿足值的範圍的需求,並且預留未來增長空間的前提下,應該選擇最小的資料型別。
下面是一些小技巧:
1.整數型別
整數通常是標識列最好的選擇,因為它很快,並且可以使用AUTO_INCREMENT
。
2.字串型別
如果可以避免,儘可能的避免使用字串型別作為標識列的型別,因為它很消耗空間,並且通常比數字型別慢。尤其是在MyISAM
儲存引擎的表裡使用字串作為標識列時,要特別的小心,MyISAM
預設對字串使用壓縮索引,這會導致查詢慢很多。
對於完全“隨機”的字串也需多加註意,例如MD5()
、SHA1()
或者UUID()
產生的字串。這些函式生成的新值會任意分佈在很大的空間內,會導致insert以及一些select操作變得很慢:
因為插入值會隨機地寫到索引的不同位置,所以使得
insert
語句更慢。這會導致頁分裂、磁碟隨機訪問。select
語句會變得更慢,是因為邏輯上不相鄰的資料會分佈在磁碟和記憶體的不同地方。隨機值會導致快取對所有型別的查詢語句效果很差,因為會使得快取賴以工作的訪問區域性性原理失效。
六、總結
在實際開發中,有很多工具會自動生成建表指令碼等等,自動生成前期給開發帶來了很大的便利,但與此同時卻導致嚴重的效能問題。有些工具生成的東西,在儲存任何資料都會使用很大的VARCHAR
型別,這往往是不正確的。如果是自動生成的,一定要反覆檢查確認是否合理。
例如,一些ORM
框架(如,MyBatis
、Hibernate
),會儲存任意型別的資料列到任意型別的後端資料,這通常意味著沒有設計使用更優的資料型別來儲存,後期安全隱患很大,出現問題也很難排查。總之,一定要反覆檢查確認是否合理。這也是我個人不太喜歡用這類類似的工具,來生成程式碼的原因,檢查真的很浪費我的時間。
在這裡已經介紹了大部分常用的資料型別,各自都有哪些特點,哪些地方會嚴重影響效能等等。在選擇資料型別時,把握好“選擇原則”,你就成功了一半,其餘細節在日常開發接觸中慢慢琢磨、留意,選擇型別時不要隨意、盲目選擇就好。
簡單歸納如下:
對於字串型別,最好的策略是隻分配真正需要的空間。
日期型別,要根據實際需要選擇能夠滿足應用的最小儲存的日期型別。
對含有
TEXT
和BLOB
欄位的表,如果經常做刪除和修改記錄的操作要定時執行OPTIMIZE TABLE
功能對錶進行碎片整理。