Schema的優化和索引 - 選擇最佳的數據類型

前言

  越小也就是越好的

  一般來說,儘量選擇小的數據類型足以符合你的存儲和展現數據。越小的數據類型也常常是越快的,因爲它們使用了較少的硬盤空間,內存,CPU緩存。它們也需要更少的CPU處理週期。

  要確定的是不要低估你要存儲數值的範圍,因爲在你的Schema的多個位置中,增長的數據類型範圍會讓操作很費力費時。如果你對不知道選擇什麼樣的數據類型,那麼就選擇一個最小的,當然要確保數據不會越界。

  簡單的就是好的

  對於簡單數據類型的操作,不需要太多的CPU週期。比如,整型之間的比較要比字符之間的比較消耗更低。因爲字符集和排序規則使字符的比較過於的複雜。這有兩個例子,存儲日期和時間要使用MySQL內置類型而不是用字符串,以及對於IP地址你應該使用整型。我們會將在以後的章節詳細討論。

  如果可能,要避免使用NULL

  當可能的時候,你都應該定義字段爲NOT NULL.有許多表包括了許多NULL的列,即使這些應用並不存儲NULL。只不過因爲這是默認的。除非你打算存儲NULL,不然的話你就要小心指定列爲NOT NULL.

  對於MySQL,優化一些涉及到NULL列的語句是非常困難的,因爲它們加索引,索引統計,以及值的比較都很複雜。一個NULL列需要更多的存儲空間以及在MySQL內部需要特殊處理。當一個NULL列被索引了,它的每個實體都需要額外的字節,以及設置導致了在MyISAM中,定長的索引轉爲可變大小(variable-sized)的索引。

  即使你需要在字段中不存儲值,你也不要用NULL.考慮使用0,特殊的數值,或者空字符串來取代NULL.

  把NULL列變爲NOT NULL列性能的提高是很小的。因此不要改變已有的Schema,除非你能知道這樣做會引起的問題。然而,如果你計劃索引列,如果可能的話,請避免使用NULL列。

  第一步要決定指定列的適用類型,要大概知道屬於哪一類的:數值,字符串,時間等等。這是比較直接的,但是我們提到有些特殊的情況下,選擇數據類型並不是很直接的。

  第二步選擇具體的類型。許多MySQL數據類型保存相同的類型數據,但是它們存儲的範圍不同,允許的精度,或者它們需要的物理空間(或者硬盤和內存)。一些數據類型有特殊的行爲或者屬性。

  舉個例子,DATETIME和TIMESTAMP列能存儲相同類型的數據:日期和時間,精度爲秒。然而,TIMESTAMP只使用了一半的存儲空間,以及有個自動更新的特性。在另一方面,它的值範圍較小,以及有的時候這個特性成了一個障礙了。

  我們討論的都是基本類型。MySQL也兼容別名,比如INTEGER,BOOL以及NUMERIC.這些僅僅是別名。它們可能造成一些困惑,但是不會影響性能。


整數和實數

整數

  總共有兩種數字類型:整數和實數(有小數部分的)。如果你保存的是整數,使用下列的整型:TINYINT,SMALLINT,MEDIUMINT,INT或者BIGINT。它們分別需要8,16,24,32,以及64位的存儲空間。它們存儲的值範圍爲Schema的優化和索引 - 選擇最佳的數據類型 - 整數和實數N就是它們使用的存儲空間所需的位數。

  整型有個一個可選擇的參數,就是UNSIGNED,它不允許存儲負數,而正數的最大上限變爲之前的大約兩倍左右。舉個例子,一個TINYINT UNSIGNED存儲的範圍是0-255,而不是之前的-128-127.

  有符號和無符號類型使用了相同大小的存儲空間,它們的性能是一樣的,因此使用哪種類型取決存儲的數據範圍。

  你的選擇決定了MySQL在內存和硬盤上存儲數據的方式。然而,即使在32位的架構上,整型的計算常常使用64位的BIGINT.(例外的情況一般發生在使用DECIMAL或者DOUBLE計算的聚合函數上)。

  MySQL允許你指定整型的“寬度”,比如INT(11).對大多數應用來說沒有什麼意義:它並不會約束值的範圍,但是可以簡單的指定MySQL交互工具的字符數。只是限制顯示而已。對於存儲和計算,INT(1)和INT(20)是等同的。

  實數

  實數的數字帶有小數部分。然而它們並不僅僅是小數。它也能保存一些大到連 BIGINT都保存不下的整數。MySQL同時支持精確和不精確兩種類型。

  FLOAT和DOUBLE支持近似的標準浮點運算。如果你需要知道準確的浮點運算結果,你需要研究你的平臺的浮點實現方式。

  DECIMAL類型存儲了準確的浮點數。在MySQL5.0以上版本,都支持準確的運算。MySQL4.1較早版本,一般浮點運算會得到比較奇怪的值,因爲丟掉了精度。在這些MySQL版本中,DECIMAL只不過是存儲類型而已。

  MySQL5.0以上版本的服務器自身去運行DECIMAL運算,是因爲CPU並不直接支持這種計算。浮點運算稍微快些,因爲CPU本地執行了這些運算。

  浮點和DECIMAL類型都允許你指定精度。對於DECIMAL,你可以指定小數點之前和之後的數字最大的數量。這個決定了列的消耗空間的大小。MySQL5.0以上版本把數字打包進了一個二進制字符串(每四個字節包含了九個數字)。比如DECIMAL(18,9)會在小數點兩邊各存儲9個數字,使用的空間爲9字節:4個字節存儲了小數點之前的數字,1個字節存儲了小數點,另外4個字節存儲了小數點之後的數字。

  一個DECIMAL數在MySQL5.0以上版本中最高上限的數字個數爲65。早期的MySQL版本中限制爲254,並且存儲數值並沒有壓縮(一個字節一個數字)。然而,這些版本中,並不能使用這些大數值進行計算,因爲DECIMAL就是個存儲類型而已。要計算的話,只能把DECIMAL轉換爲DOUBLE了。

  指定浮點數的精度有兩種方法,不同的方法導致了MySQL選擇不同的類型以及近似的存儲數值。精度的指定並不是標準的,因此我們建議指定你希望的類型而不是精確度。

  保存相同的數值範圍的情況下,浮點類型要比DECIMAL使用更少的空間。一個FLOAT列使用了4個字節。DOUBLE消耗了8個字節以及有更好的精度和更大的值的範圍。像Integer一樣,你僅僅選擇的是存儲類型。MySQL在浮點運算中,使用DOUBLE進行運算。

  因爲需要額外的存儲空間和計算的消耗。你使用DECIMAL的情況爲你需要使用精確的結果。比如,存儲的是財政信息。

字符串類型

  MySQL支持很多字符串類型,以及它們的許多變化類型。這些數據類型在4.1和5.0版本變化都比較大。可以說變得更復雜了。早在4.1版本中,每個字符串列都有自己的字符集和對於那些字符集的排序規則,或者叫做collation(校對)。

  VARCHAR和CHAR

  有兩個主要的字符串類型就是VARCHAR和CHAR。它們都存儲了字符值。不幸的是,很難準確解釋這些值怎樣存儲在硬盤和內存中,因爲這些都是有存儲引擎實現的。我們假設你使用的是MyISAM和/或者InnoDB.如果不是,你最好查閱你使用存儲引擎的相關文檔。

  讓我們先看看VARCHAR和CHAR是怎樣存儲在硬盤上的。要注意的是存儲引擎可能存儲的CHAR或者VARCHAR在存入內存和硬盤上有所不同的,以及當服務器從存儲引擎獲得了這個值的時候,會把這個值轉爲另一個存儲引擎的格式。下面就是兩種類型的比較

  VARCHAR:

  VARCHAR存儲了變長的字符串以及它是最常見的字符串數據類型。它佔用的空間要少於定長的類型,因爲它根據所需來決定需要的空間大小。特例就是MyISAM參數設爲ROW_FORMAT=FIXED.這個參數使表的每一行使用固定大小的空間以及浪費大量的空間。

  varchar使用了1或者2額外的字節記錄值的長度:如果長度大約爲255字節的話,大概使用1個字節,如果更多的話,那麼就是2個字節。假如是latin1字符節,一個varchar(10)將會佔用11字節的存儲空間。一個varchar(1000)使用上限爲1002字節。因爲需要2字節存儲長度信息。

  VARCHAR會對性能有所幫助,因爲它節約了空間。然而,因爲行是變長,當你更新它們的時候,它們就會增長。這樣會導致需要額外的一些工作。如果一行增長以及並不在原來的地址了。這些行爲依賴於存儲引擎。舉個例子,MyISAM可能把行分爲碎片。以及InnoDB可能切開頁,把行填充到它的內部。其他的存儲引擎可能在適當的地方不會更新數據。


日期和時間類型

  MySQL對於不同種類的日期和時間有很多的數據類型,比如YEAR和DATE。MySQL可保存時間顆粒度是一秒。然而,時間的計算可以精確到毫秒。我們將會教你如何解決存儲引擎的侷限性。

  大部分時間類型都沒有什麼可選擇的。所以那個是最佳的選擇不是一個問題。只有一個問題那就是當你即要保存日期也要保存時間的時候,將要做什麼。MySQL提供了兩個類型來滿足這個需求:DATETIME和TIMESTAMP.對許多應用而言,任意一個都可以,但是在一些案例中,一個要好於另一個。讓我們來看看:

  DATETIME

  這個類型可以保存大範圍的值。從1001到9999.精度精確到了秒。它保存日期和時間壓縮到爲YYYYMMDDHHMMSS格式的整型,獨立的時間區域。這會佔用8字節的存儲空間。

  默認的MySQL顯示DATETIME爲排序的,清晰地格式,比如2008-01-16 22:37:08。這是符合ANSI標準的日期和時間。

  TIMESTAMP

  就像名字所提示的一樣,TIMESTAMP類型存儲了從1970 1.1 開始所經過秒的數量-這個UNIX的TIMESTAMP是一樣的。因此它的範圍要比DATATIME要小一些:從1970年到2038年。MySQL提供了FROM_UNIXTIME和UNIX_TIMESTAMP函數,用來把Unix的timestamp轉爲data,反之亦然。

  新版的MySQL的TIMESTAMP值像DATETIME.但是舊版MySQL顯示它們沒有任何的標點。僅僅是顯示格式的不同。TIMESTAMP存儲在MySQL的方式在所有MySQL版本中都是一樣的。

  TIMSTAMP的現實也依賴於時區。MySQL服務器,操作系統,客戶端連接所有的都有時區的設置。因此,一個TIMESTAMP存值爲0,實際顯示的是東部夏令時間,1969-12-31 19:00:00。相對GMT有5個時差。

  TIMESTAMP也有一個DATETIME不具備的屬性。默認的情況下,當你插入一條記錄並沒有指定這個值,MySQL會把TIMESTAMP列設爲當前的時間。當你更新這一行的時候,MySQL也會更新TIMESTAMP列。除非你給TIMESTAMP指定一個值。對於任意的TIMESTAMP,你可以配置插入和更新的行爲。最終,TIMESTAMP列默認不能爲NULL.這點和其他的類型有所不同。

  先撇特殊行爲不談,一般來說,如果能用TIMESTAMP就應該用,因爲它空間上比DATETIME更有效用。有的時候人們用整型來存儲UNIX的TIMESTAMP,但是這麼做不能帶來什麼好處。這麼做也不太容易去處理,我們不建議這麼做。

  怎樣保存比秒更精確的時間?MySQL還沒有這些數據類型,但你可以使用你自己的存儲類型:你可以使用BIGINT類型並且存儲這個值作爲在微妙級別的TIMESTAMP,或者你可以使用DOUBLE並且存儲小數點之後的秒的小數部分。


BIT數據類型

  MySQL有很多使用單獨的bit去存儲數據的類型。不管底層的存儲格式以及操作,從技術上來看所有的這些類型都是字符串類型。

  BIT

  在5.0之前的版本,BIT僅僅等同於TINYINT。但是在5.0之後的版本,它已經是個具有一些特性的,和以前完全不同的數據類型了。我們在這討論的是新增加的特性。

  你可以使用BIT列存儲一個或多個true/false值。BIT(1)定義了一個包含1個bit的字段,BIT(2)存儲了2個bits.如此類推。。

  BIT最大長度是64bits.

  不同存儲引擎,BIT的特性是不同的。MyISAM爲了存儲它們把這些列打包爲一個整體。因此17個單獨的BIT列僅僅要存儲17bits(假設每一列都沒有NULL值)。MyISAM大約需要3字節存儲它們。其他的存儲引擎,如Memory和InnoDB。以足夠存儲這些bits的最小整數類型來存儲每一列。因此你就不能節約使用的存儲空間了。

  MySQL把BIT看做字符串類型。並不是數字類型。當你獲取一個BIT(1)的值,這個結果是個字符串,但是這字符串是二進制的0或1,要記住並不是ASCII的0或1.然而如果你獲取的是數字,這個結果會被轉換。如果你要作比較就一定要記住這一點。舉個例子,如果你把b'00111001'(這個等同於57)存儲到BIT(8)再獲取它。你會獲得字符串包含了字符碼爲57.ASCII字符碼爲9,但是在數字的環境下,你的獲得值是57.

mysql> CREATE TABLE bittest(a bit(8));

mysql> INSERT INTO bittest VALUES(b'00111001');

mysql> SELECT a, a + 0 FROM bittest;

+------+-------+

| a | a + 0 |

+------+-------+

| 9 | 57 |

+------+-------+

  這很令人迷惑,所以我們建議要小心的使用BIT類型。對於大部分應用,要儘可能避免使用這個類型。

  如果你想用一個BIT存儲true/false。另一個選擇是使用一個可以爲NULL的CHAR(0)列。這個列可以存儲NULL也可以存儲一個空字符串。

  SET

  如果你要存儲非常多的true/false。考慮使用SET數據類型把許多列整合爲一個。它把一系列的bit打包了。並且十分有效的進行存儲。還有就是MySQL有一些如FIND_IN_SET()和FIELD()函數可以很容易使用SET類型。主要的缺點就是需要改變表的定義。需要使用ALTER TABLE,這對比較大的表來說消耗還是很大的。一般來講,你也不能使用索引來查找SET列。

  在整數列上進行位運算

  對於SET的另一個原則是使用整型。一個例子,你可以把8bits打包一個TINYINT然後用位運算來操作它們。在應用代碼中對每一位定義常量可以使這些操作變得更爲簡單。

  這種方式最主要的優勢是,你可以改變字段中的枚舉值,而不必使用ALTER TABLE。缺點就是寫出的語句很難理解。一些人很習慣位運算而另一些人確不是。所以是否使用這個技術完全是個人的喜好了。

  下面的一個例子是關於權限控制的。每個bit或set元素表現的值都是CAN_READ, CAN_WRITE或者CAN_DELETE。如果使用的SET類型,那麼MySQL存儲的就是bit和值的映射。如果存儲的是整形,你就要把映射存儲到應用程序代碼中了。

  下面是SET的例子

mysql> CREATE TABLE acl (
-> perms SET('CAN_READ', 'CAN_WRITE', 'CAN_DELETE') NOT NULL
-> );
mysql> INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE');
mysql> SELECT perms FROM acl WHERE FIND_IN_SET('CAN_READ', perms);
+---------------------+
| perms |
+---------------------+
| CAN_READ,CAN_DELETE |
+---------------------+

  如果使用的整形,你可能寫出如下的代碼

mysql> SET @CAN_READ := 1 << 0,
-> @CAN_WRITE := 1 << 1,
-> @CAN_DELETE := 1 << 2;
mysql> CREATE TABLE acl (
-> perms TINYINT UNSIGNED NOT NULL DEFAULT 0
-> );
mysql> INSERT INTO acl(perms) VALUES(@CAN_READ + @CAN_DELETE);
mysql> SELECT perms FROM acl WHERE perms & @CAN_READ;
+-------+
| perms |
+-------+
| 5 |
+-------+

  我們使用了變量,但是你可以用應用程序中的代碼來替換它們。


主鍵的選擇

  對於主鍵,選擇一個好的數據類型尤爲關鍵。你可能經常需要用這些列和其他做比較以及用這些列查找其他的列。你可能也把它們作爲另一些表的外鍵。因此當你選擇主鍵的數據類型時,應該保持相關表主鍵類型一致。

  當選擇主鍵的數據類型,你不僅要考慮存儲類型,也要考慮MySQL操作和比較這些類型的表現情況。比如,MySQL內部存儲ENUM和SET是作爲整型的,但是當在字符串環境下作比較的時候,MySQL會把它們轉爲字符串。

  一旦你選擇了一個類型,要確定相關表都要使用這個類型。這個類型一定要精確,包括了它的屬性,如UNSIGNED。混合不同的數據類型,會引起性能問題。即使不會,在類型比較的時候,也會出現很難發現的錯誤。在忘記比較不同的數據類型之後,這些錯誤往往會發生。

  根據你需要值的範圍,選擇最小的數據類型的範圍。並且要爲以後留一些主鍵增長的空間。一個例子,你使用state_id來存儲美國州的名稱,你不需要上千或百萬的值。因此你不需要使用INT。一個TINYINT就足夠了並且它大小爲3字節。如果你使用它作爲其他表的外鍵。三字節就能發揮大的作用了。

  Integer類型

  Integer通常來說是主鍵類型的最佳選擇。因爲它很快並且可以自增。

  ENUM 以及SET

  通常來說,雖然它們對於表中包含狀態或者類型值比較有用,但對於主鍵並不是一個好的選擇。ENUM和SET比較適合存儲一些如訂單的狀態,產品的類型,或者人的性別。

  如果你使用了ENUM定義了一個產品的類型,你可能要根據唯一的ENUM字段來查找(你可能在這表中添加了產品的描述以及等等產品類型的相關信息)。這種情況下你可能使用ENUM作爲主鍵,但是大多數情況要避免使用。

  String類型

  如果可以,要儘可能避免使用String做爲主鍵。它會浪費許多空間以及處理起來要慢於Integer。當使用MyISAM的表時,使用String要尤爲小心。默認的情況下,MyISAM會對String類型的索引進行壓縮,這樣會使查找變慢。

  你也要小心使用隨機的String.如MD5(),SHA1(),UUID().隨機生成的String散列在大的空間中,這會降低插入和一些查詢語句的速度。

  降低了INSERT語句,因爲在索引中這插入的值會存入隨機的位置。這回造成頁的分割,隨機硬盤訪問,造成聚集索引碎片。

  降低SELECT速度,因爲相鄰的行被分散在硬盤和內存中。

  隨機值會導致緩存能力的下降。因爲它們消除了本地的引用。本地的引用是緩存的工作方式。如果所有的數據都已經“預熱”了,把任意數據放到緩存中沒有任何的優勢並且,如果工作數據集合沒有在內存中,緩存就會有很多刷新和查找丟失的現象。

  如果使用的UUID值,要去掉破折號或者使用UNHEX( )把UUID轉爲16字節的數並且,把它們存在BINARY(16)的列。你可使用HEX來獲得16進制的值。

  UUID生成的值和其他哈希函數如SHA1()生成的值有所不同。UUID的值分佈不均並且稍微有點連續。但是它還是沒有整型好。

  特殊類型的數據

  一些特殊類型的數據可能並不直接的和MySQL內置類型相吻合。一個例子就是用STAMPTIME存儲更精確的時間。

  另一個例子是IP地址。人們常常使用VARCHAR(15)來存儲IP地址。然而,一個IP地址其實是一個無符號的32bit的整型,並不是一個字符串。IP地址的點兒僅僅是爲了方便人們去讀取IP地址而已。你應該用無符號整型去存儲IP地址。MySQL提供了INET_ATON()和INET_NTOA()函數來解決這兩種類型的轉換。以後的MySQL版本。它會提供一個專門來存儲IP的地址的數據類型。


發佈了37 篇原創文章 · 獲贊 12 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章