這一部分提供了如何選擇數據類型來幫助提高查詢運行速度的一些指導:
在可以使用短數據列的時候就不要用長的。如果你有一個固定長度的CHAR數據列,那麼就不要讓它的長度超出實際需要。如果你在數據列中存儲的最長的值有40個字符,就不要定義成CHAR(255),而應該定義成CHAR(40)。如果你能夠用MEDIUMINT代替BIGINT,那麼你的數據表就小一些(磁盤I/O少一些),在計算過程中,值的處理速度也快一些。如果數據列被索引了,那麼使用較短的值帶來的性能提高更加顯著。不僅索引可以提高查詢速度,而且短的索引值也比長的索引值處理起來要快一些。
如果你可以選擇數據行的存儲格式,那麼應該使用最適合存儲引擎的那種。對於MyISAM數據表,最好使用固定長度的數據列代替可變長度的數據列。例如,讓所有的字符列用CHAR類型代替VARCHAR類型。權衡得失,我們會發現數據表使用了更多的磁盤空間,但是如果你能夠提供額外的空間,那麼固定長度的數據行被處理的速度比可變長度的數據行要快一些。對於那些被頻繁修改的表來說,這一點尤其突出,因為在那些情況下,性能更容易受到磁盤碎片的影響。
· 在使用可變長度的數據行的時候,由於記錄長度不同,在多次執行刪除和更新操作之後,數據表的碎片要多一些。你必須使用OPTIMIZE TABLE來定期維護其性能。固定長度的數據行沒有這個問題。
· 如果出現數據表崩潰的情況,那麼數據行長度固定的表更容易重新構造。使用固定長度數據行的時候,每個記錄的開始位置都可以被檢測到,因為這些位置都是固定記錄長度的倍數,但是使用可變長度數據行的時候就不一定了。這不是與查詢處理的性能相關的問題,但是它一定能夠加快數據表的修復速度。
盡管把MyISAM數據表轉換成使用固定長度的數據列可以提高性能,但是你首先需要考慮下面一些問題:
· 固定長度的數據列速度較快,但是占用的空間也較大。CHAR(n)列的每個值(即使是空值)通常占n個字符,這是因為把它存儲到數據表中的時候,會在值的後面添加空格。VARCHAR(n)列占有的空間較小,因為只需要分配必要的字符個數用於存儲值,加上一兩個字節來存儲值的長度。因此,在CHAR和VARCHAR列之間進行選擇的時候,實際上是時間與空間的對比。如果速度是主要的考慮因素,那麼就使用CHAR數據列獲取固定長度列的性能優勢。如果空間很重要,那麼就使用VARCHAR數據列。總而言之,你可以認為固定長度的數據行可以提高性能,雖然它占用了更大的空間。但是對於某些特殊的應用程序,你可能希望使用兩種方式來實現某個數據表,然後運行測試來決定哪種情況符合應用程序的需求。
· 即使願意使用固定長度類型,有時候你也沒有辦法使用。例如,長於255個字符的字符串就無法使用固定長度類型。
MEMORY數據表目前都使用固定長度的數據行存儲,因此無論使用CHAR或VARCHAR列都沒有關系。兩者都是作為CHAR類型處理的。
對於InnoDB數據表,內部的行存儲格式沒有區分固定長度和可變長度列(所有數據行都使用指向數據列值的頭指針),因此在本質上,使用固定長度的CHAR列不一定比使用可變長度VARCHAR列簡單。因而,主要的性能因素是數據行使用的存儲總量。由於CHAR平均占用的空間多於VARCHAR,因此使用VARCHAR來最小化需要處理的數據行的存儲總量和磁盤I/O是比較好的。
對於BDB數據表,無論使用固定長度或可變長度的數據列,差別都不大。兩種方法你都可用試一下,運行一些實驗測試來檢測是否存在明顯的差別。
把數據列定義成不能為空(NOT NULL)。這會使處理速度更快,需要的存儲更少。它有時候還簡化了查詢,因為在某些情況下你不需要檢查值的NULL屬性。
考慮使用ENUM數據列。如果你擁有的某個數據列的基數很低(包含的不同的值數量有限),那麼可以考慮把它轉換為ENUM列。ENUM值可以被更快地處理,因為它們在內部表現為數值。
使用PROCEDURE ANALYSE()。運行PROCEDURE ANALYSE()可以看到數據表中列的情況:
SELECT * FROM tbl_name PROCEDURE ANALYSE();
SELECT * FROM tbl_name PROCEDURE ANALYSE(16,256);
輸出的每一列信息都會對數據表中的列的數據類型提出優化建議。第二個例子告訴PROCEDURE ANALYSE()不要為那些包含的值多於16個或者256字節的ENUM類型提出建議。如果沒有這樣的限制,輸出信息可能很長;ENUM定義通常很難閱讀。
根據的PROCEDURE ANALYSE()輸出信息,你可能發現,可以修改自己的數據表來利用那些效率更高的數據類型。如果你決定改變某個數據列的類型,需要使用ALTER TABLE語句。
使用OPTIMIZE TABLE來優化那些受到碎片影響的數據表。被大量修改的數據表,特別是那些包含可變長度數據列的表,容易遭受碎片的影響。碎片很糟糕,因為它會導致用於存儲數據表的磁盤塊形成無用空間(空洞)。隨著時間的推移,為了得到有效的數據行,你必須讀取更多的塊,性能就會降低。這會出現在任何可變長度的數據行上,但是對於BLOB或TEXT數據列尤其突出,因為它們的長度差異太大了。在正常情況下使用OPTIMIZE TABLE會防止數據表的性能降低。OPTIMIZE TABLE可以用於MyISAM和BDB數據表,但是defragments只能用於MyISAM數據表。任何存儲引擎中的碎片整理方法都是用MySQLdump來轉儲(dump)數據表,接著使用轉儲的文件刪除並重新建立那些數據表:
% MySQLdump --opt db_name tbl_name > dump.sql
% MySQL db_name < dump.sql
把數據打包放入BLOB或TEXT數據列。使用BLOB或TEXT數據列存儲打包(pack)的數據,並在應用程序中進行解包(unpack),使你能夠在一次檢索操作中得到需要的任何信息,而不需要進行多次檢索。它對那些很難用標准的數據表結構表現的數據值和頻繁變化的數據值也是有幫助的。
解決這個問題的另一種方法是讓那些處理Web窗體的應用程序把數據打包成某種數據結構,然後把它插入到單個BLOB或TEXT數據列中。例如,你可以使用XML表示調查表回復,把那些XML字符串存儲在TEXT數據列中。由於要對數據進行編碼(從數據表中檢索數據的時候還需要解碼),它會增加客戶端的開銷,但是可以簡化數據結構,而且它還消除了那些因為改變了調查表的內容而必須改變數據表結構的需求。
另一方面,BLOB和TEXT值也會引起自己的一些問題,特別是執行了大量的刪除或更新操作的時候。刪除這種值會在數據表中留下很大的"空洞",以後填入這些"空洞"的記錄可能長度不同(前面討論的OPTIMIZE TABLE提出解決這個問題的一些建議)。
使用合成的(synthetic)索引。合成的索引列在某些時候是有用的。一種辦法是根據其它的列的內容建立一個散列值,並把這個值存儲在單獨的數據列中。接下來你就可以通過檢索散列值找到數據行了。但是,我們要注意這種技術只能用於精確匹配的查詢(散列值對於類似<或>=等范圍搜索操作符是沒有用處的)。我們可以使用MD5()函數生成散列值,也可以使用SHA1()或CRC32(),或者使用自己的應用程序邏輯來計算散列值。請記住數值型散列值可以很高效率地存儲。同樣,如果散列算法生成的字符串帶有尾部空格,就不要把它們存儲在CHAR或VARCHAR列中,它們會受到尾部空格去除的影響。
合成的散列索引對於那些BLOB或TEXT數據列特別有用。用散列標識符值查找的速度比搜索BLOB列本身的速度快很多。
在不必要的時候避免檢索大型的BLOB或TEXT值。例如,SELECT *查詢就不是很好的想法,除非你能夠確定作為約束條件的WHERE子句只會找到所需要的數據行。否則,你可能毫無目的地在網絡上傳輸大量的值。這也是BLOB或TEXT標識符信息存儲在合成的索引列中對我們有所幫助的例子。你可以搜索索引列,決定那些需要的數據行,然後從合格的數據行中檢索BLOB或TEXT值。
把BLOB或TEXT列分離到單獨的表中。在某些環境中,如果把這些數據列移動到第二張數據表中,可以讓你把原數據表中的數據列轉換為固定長度的數據行格式,那麼它就是有意義的。這會減少主表中的碎片,使你得到固定長度數據行的性能優勢。它還使你在主數據表上運行SELECT *查詢的時候不會通過網絡傳輸大量的BLOB或TEXT值。
高效率地載入數據
在大多數情況下,你所關注的是SELECT查詢的優化,因為SELECT查詢是最常見的查詢類型,而且如何優化它們又不是太簡單。與此形成對比,把數據載入數據庫的操作就相對直接了。然而,你仍然可以利用某些策略來改善數據載入操作的效率。基本的原理如下所示:
· 批量載入比單行載入的效率高,因為在每條記錄被載入後,鍵緩存(key cache)不用刷新(flush);可以在這批記錄的末尾刷新鍵緩存。鍵緩存刷新的頻率減少得越多,數據載入的速度就越快。
· 沒有索引的數據表的載入速度比有索引的要快一些。如果存在索引,不但要把記錄添加到數據文件中,還必須修改索引來反映新增的記錄。
· 較短的SQL語句比較長的SQL語句快,因為它們所涉及到服務器端分析過程較少,同時通過網絡把它們從客戶端發送到服務器上的速度也更快。
其中有些因素看起來是次要的(尤其是最後一個),但是如果你載入的數據很多,那麼即使很小的效率差異也會導致一定的性能差別。我們可以從前面的一般原理得出幾條如何快速載入數據的實踐結論:
· LOAD DATA(所有形式的)比INSERT效率高,因為它是批量載入數據行的。服務器只需要分析和解釋一條語句,而不是多條語句。同樣,索引只需要在所有的數據行被處理過之後才刷新,而不是每行刷新一次。
· 不帶LOCAL的LOAD DATA比帶有LOCAL的LOAD DATA的速度要快。不帶LOCAL的時候,文件必須位於服務器上,而且你必須擁有FILE權限,但是服務器卻可以直接從磁盤上讀取文件。使用LOAD DATA LOCAL的時候,客戶端讀取文件並通過網絡把它發送給服務器,速度慢一些。
· 如果你必須使用INSERT,那麼試著使用在一個語句中指定多個數據行的形式:
INSERT INTO tbl_name VALUES(...),(...),... ;
在這個語句中指定的數據行越多,效果就越好。這會減少必要的語句數量,並最小化索引刷新的次數。這一條結論看起來與前面所討論的"語句越短,執行速度越快"相矛盾,但是實際上並不矛盾。這兒所討論的是同時插入多個數據行的一個INSERT語句所花費的開銷比功能相同的多個單行INSERT語句的花費的開銷要小一些,並且多行語句消耗的索引刷新開銷也少一些。
如果你使用MySQLdump生成數據庫備份文件,那麼MySQL 4.1會默認地生成多行INSERT語句:它會激活--opt (優化)選項,而這個選項會激活--extended-insert選項,該選項生成多行INSERT語句,還存在其它一些選項也可以使數據被載入的時候,轉儲文件被處理的效率更高。對於MySQL 4.1以前的版本,你可以明確地指定--opt或--extended-insert選項。
使用MySQLdump的時候要避免使用--complete-insert選項;它生成的INSERT語句是每個數據行一條語句的,語句總共會很長,比多行語句需要的分析操作更多。
· 如果你必須使用INSERT語句,那麼在可能的情況下,對它們進行分組以減少索引的刷新。對於事務性的存儲引擎,在單個事務中提交,而不是在自動提交(autocommit)模式下提交INSERT語句可以實現這樣的功能:
START TRANSACTION;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
COMMIT;
對於非事務性的存儲引擎,獲取數據表上的寫入鎖,它被鎖定的時候提交INSERT語句:
LOCK TABLES tbl_name WRITE;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
INSERT INTO tbl_name ... ;
UNLOCK TABLES;
無論采用哪種方法,你得到的好處都是相同的:索引在所有的語句都被執行之後才刷新一次,而不是每個INSERT語句刷新一次索引。後面介紹了在自動提交模式下或數據表沒有被鎖定的時候發生的情況。
· 對於MyISAM數據表,減少索引刷新的另外一個策略是使用DELAYED_KEY_WRITE表選項。使用這個選項的時候,數據行會像平常一樣立即寫入數據文件中,但是鍵緩存只是偶爾刷新一次,而不是在每次插入操作之後都需要刷新。如果要在服務器上全面地使用延遲索引刷新,那麼就需要使用--delay-key-write選項來啟動MySQLd。在這種情況下,每個數據表的索引塊寫入操作都會被延遲,直到這些數據塊必須為其它的索引值提供空間、或者執行了FLUSH TABLES命令、或者數據表被關閉的時候才執行操作。
如果你選擇了對MyISAM數據表使用延遲鍵寫入,那麼不正常的服務器關閉可能會引起索引值的丟失。這不是致命的問題,因為MyISAM索引可以依據數據行來進行修復,但是如果想讓修復過程出現,你就必須使用--myisam-recover=FORCE選項來啟動服務器。這個選項會使服務器在打開MyISAM數據表的時候檢查它們,如果有必要就自動地修復它們。
對於復制(replication)從屬服務器,你可能希望使用--delay-key-write=ALL來延遲所有的MyISAM數據表索引的刷新,不管在主服務器上最初是如何建立它們的。
· 使用壓縮的客戶端/服務器協議來減少網絡上數據傳輸的數量。對於大多數MySQL客戶端來說,我們都可以使用--compress命令行選項來指定它。通常,這個選項只是在較慢的網絡上使用,這是因為壓縮操作會花費大量的處理器時間。
· 讓MySQL替你插入默認值。也就是說,無論如何都不要給INSERT語句中那些可以賦予默認值的列指定值。平均起來,你的語句更短,減少了通過網絡發送到服務器的字符數量。此外,由於語句包含的值較少,服務器執行的分析和值轉換操作也較少。
· 對於MyISAM數據表,如果你必須把大量的數據載入一個新表,最好建立不帶索引的表,載入數據,然後建立索引,這樣的工作次序的速度要快一些。一次性地建立索引比每行都更新索引的速度要快一些。對於已經帶有索引的表,如果預先刪除或禁止索引,後來再重新建立或者激活索引,那麼數據載入的速度也要快一些。這些策略不能應用於InnoDB或BDB表,它們沒有對分離的索引建立過程進行優化。
如果你考慮使用刪除或禁止索引的策略,把數據載入MyISAM數據表,那麼在評估獲得的優勢的時候,就需要考慮整個環境。如果你把少量的數據載入大型的數據表中,那麼在沒有任何特殊准備工作的情況下,重新建立索引花費的時間可能比載入數據的時間還要長。
要刪除並且重新建立索引,需要使用DROP INDEX和CREATE INDEX,或者使用與索引相關的ALTER TABLE。禁止和激活索引有兩種辦法:
· 你可用使用ALTER TABLE的DISABLE KEYS和ENABLE KEYS形式:
ALTER TABLE tbl_name DISABLE KEYS;
ALTER TABLE tbl_name ENABLE KEYS;
這些語句關閉或打開表中非唯一(non-unique)索引的更新過程。
ALTER TABLE的DISABLE KEYS和ENABLE KEYS子句是索引禁止和激活操作的推薦方法,因為服務器也是這樣操作的(如果你使用LOAD DATA語句把數據載入空的MyISAM表中,服務器會自動地執行這樣的優化操作)。
· Myisamchk工具可以執行索引維護。它直接在數據表文件上進行操作,因此使用它的時候,你必須擁有數據表文件的寫入權限。
使用myisamchk禁止MyISAM表的索引的方法是,首先你要確保已經告訴了服務器讓該數據表獨立出來,接著把它移動到適當的數據庫目錄中,並運行下面的命令:
% myisamchk --keys-used=0 tbl_name
載入數據之後,重新激活索引:
% myisamchk --recover --quick --keys-used=n tbl_name
其中的n是位掩碼(bitmask),它指明了要激活的索引。Bit 0(第一個位)與索引1對應。例如,如果某張表擁有三個索引,那麼n的值應該是7(二進制的111)。你也可以使用--description選項來檢測索引的數量:
% myisamchk --description tbl_name
前面的數據載入原則也可以應用於混合查詢環境(客戶端執行多種不同的操作)。例如,你應該避免在那些頻繁被修改(寫入)的數據表上運行長時間的SELECT查詢。這會引發大量的爭用(contention),導致寫入操作的性能較差。一個可能的解決辦法是,如果你的寫入操作主要是INSERT操作,那麼把新記錄添加到輔助表中,接著周期性地把這些記錄添加到主表中。如果你必須立即訪問這些新記錄,那麼這個策略是不行的,但是如果你能夠承擔得起短期內不訪問這些數據的代價,那麼使用輔助表可以在兩個方面帶來好處。首先,它減少了主表上的SELECT查詢爭用的問題,因此它們執行得更快。其次,把輔助表中的批量數據載入主表中所花費的時間總和也比單獨載入記錄花費的時間總和要小一些;鍵緩存只需要在每次批量載入結束後刷新一次,而不用每個數據行載入後都刷新一次。
使用這種策略的一個應用是把Web服務器的Web頁面訪問日志載入MySQL數據庫的時候。在這種情況下,保證實體立即進入主表的優先級並不高(沒有這個必要性)。
如果你在MyISAM表上使用了混合的INSERT和SELECT語句,你就可以利用並發性插入操作的優點了。這個特性允許插入和檢索操作同時進行,而不需要使用輔助表。你可以查看"使用並發性插入操作"部分。