1. 程式人生 > 資料庫 >MySQL效能優化(二):選擇優化的資料型別

MySQL效能優化(二):選擇優化的資料型別

良好的設計是高效能的基石,應該根據系統的實際業務需求、使用場景進行設計、優化、再調整,在這其中往往需要權衡各種因素,例如,資料庫表究竟如何劃分、欄位如何選擇合適的資料型別等等問題。

MySQL支援的資料型別非常之多,對於選擇恐懼症的小夥伴而言,苦不可言。大部分人在建立資料庫表時,基本一股腦的使用INTVARCHAR這兩種型別最多,至於長度,則會選擇足夠大即可,避免日後不夠用咋辦。只顧當時一時爽,之後坑誰誰難受。

如果你是一個追求極致、高效的開發者,對於上面的情況肯定是不願讓其發生的。在眾多的資料型別面前,如何選擇正確的資料型別,對於高效能是至關重要的。本文將介紹如何選擇優化的資料型別,來提高MySQL的效能,將會選取最為常用的型別進行說明,便於在實際開發中建立表、優化表字段型別時提供幫助。

一、選擇原則

不管儲存哪種型別的資料,下面幾個簡單的原則將有助於你做出更好的選擇。

1.更小的通常更好

一般情況下,應該儘可能選擇正確儲存資料的最小資料型別。更小的資料型別通常更快,因為它們佔用更少的磁碟空間、記憶體,並且處理時需要的CPU週期更少。

但是,在選擇更小資料型別時,一定不要低估儲存值的範圍,因為後期修改資料型別及長度是一件非常痛苦、耗時的操作。如果無法確定哪個資料型別是最好的,就選擇你認為不會超過範圍的最小型別。

2.簡單就好

簡單的資料型別操作通常需要更少的CPU週期。例如,整型比字元操作代價更低,因為字符集和校隊規則(如:排序規則)使得字元比較比整型比較更復雜。

3.儘量避免用NULL

NULL

是在常見不過的值了,通常都習慣對某些欄位設定預設值為NULL,這其實是一種非常不好的習慣。如果查詢中的欄位值恰巧是設定的NULL值,對MySQl來說更難優化,因為可為NULL的欄位使得索引、值比較都更復雜。

NULL值不能進行索引,影響索引的統計資訊,影響優化器的判斷。複合索引中只要有一列含有NULL值,那麼這一列對於此複合索引就是無效的。

二、字串型別

字串型別是資料庫中使用頻率最高的資料型別,VARCHARCHAR是兩種最主要的字串型別,都可以用來儲存字串,但它們儲存和檢索的方式不同。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提供了兩種相似的日期型別:DATETIMETIMESTAMP,使用起來傻傻分不清,看完本節後不要再說不知道如何選擇了。

對於應用程式而言,他們都能很好的表示日期,但是再某些場景下,各有不同。接下來讓我們一起看看吧。

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型別

一般在儲存少了字串的時候,我們會選擇CHARVARCHAR型別,而在儲存較大文字等資料時,通常會選擇使用TEXTBLOB

TEXTBLOB型別都是儲存很大的資料而設計的字串資料型別,分別採用字串和二進位制方式儲存。例如,TEXT通常用來儲存文章內容、日誌等字串內容,而BLOB通常用來儲存圖片、視訊等二進位制資料內容。有如下特點:

  • TEXT型別有字符集和排序規則。

  • BLOB型別儲存的是二進位制資料,沒有排序規則或字符集。

  • MySQL中不能將TEXT和BLOB型別的列進行索引,也不能使用這些索引消除排序。

與其他資料型別不同,MySQL把每個TEXTBLOB型別的值當作一個獨立的物件處理。儲存引擎在儲存時通常會做特殊處理,當它們的值太大時,InnoDB會使用專門的“外部”儲存區域來進行儲存,此時每個值在行內需要1~4個位元組來儲存一個指標,然後在外部儲存區域儲存實際的值。

在面對TEXT、BLOB之間的選擇時,應該根據實際情況選擇能夠滿足需求的最小儲存型別,接下來主要針對TEXT、BLOB型別存在的一些常見問題進行介紹。

1.在執行了大量的刪除操作時,TEXTBLOB會引起一些效能問題

刪除操作會在資料庫表中留下很大的“空洞”,以後填入這些“空洞”的記錄在插入的效能上會有影響。為了提高效能,建議定期使用OPTIMZE TABLE功能對這類表進行碎片整理,避免因為“空洞”導致效能問題。

實戰演示驗證說明如下:

1)建立測試表text_test,欄位idcontext的型別分別為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.使用合成索引來提高大文字欄位(TEXTBLOB型別)的查詢效能

合成索引,就是根據大文字欄位的內容建立一個雜湊值,並把這個值儲存在單獨的資料列中,接下來就可以通過檢索雜湊值找到資料行了。

但是,要注意這種技術只能用於精確匹配的查詢(雜湊值對於類似<>=等範圍搜尋操作符是沒有用處的)。可以使用MD5()函式生成雜湊值,也可以使用SHA1()CRC32(),或者使用自己的應用程式邏輯來計算雜湊值。請記住數值型雜湊值可以很高效率地儲存。同樣,如果雜湊演算法生成的字串帶有尾部空格,就不要把它們儲存在CHARVARCHAR列中,它們會受到尾部空格去除的影響。合成的雜湊索引對於那些 BLOBTEXT資料列特別有用。用雜湊識別符號值查詢的速度比搜尋BLOB列本身的速度快很多。

實戰演示驗證說明如下:

1)建立測試表text_test2,欄位idcontexthashValue欄位型別分別為int(11)textvarchar(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.在不必要的情況下避免檢索TEXTBLOB型別的值

例如,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框架(如,MyBatisHibernate),會儲存任意型別的資料列到任意型別的後端資料,這通常意味著沒有設計使用更優的資料型別來儲存,後期安全隱患很大,出現問題也很難排查。總之,一定要反覆檢查確認是否合理。這也是我個人不太喜歡用這類類似的工具,來生成程式碼的原因,檢查真的很浪費我的時間。

在這裡已經介紹了大部分常用的資料型別,各自都有哪些特點,哪些地方會嚴重影響效能等等。在選擇資料型別時,把握好“選擇原則”,你就成功了一半,其餘細節在日常開發接觸中慢慢琢磨、留意,選擇型別時不要隨意、盲目選擇就好。

簡單歸納如下:

  • 對於字串型別,最好的策略是隻分配真正需要的空間。

  • 日期型別,要根據實際需要選擇能夠滿足應用的最小儲存的日期型別。

  • 對含有 TEXTBLOB欄位的表,如果經常做刪除和修改記錄的操作要定時執行OPTIMIZE TABLE功能對錶進行碎片整理。


MySQL效能優化(一):MySQL架構與核心問題

MySQL效能優化(二):選擇優化的資料型別

MySQL效能優化(三):深入理解索引的這點事

MySQL效能優化(四):如何高效正確的使用索引

MySQL效能優化(五):為什麼查詢速度這麼慢

MySQL效能優化(六):常見優化SQL的技巧

MySQL效能優化(七):MySQL執行計劃,真的很重要