程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> 關於MYSQL數據庫 >> MySQL查詢優化系列講座之查詢優化器(1)

MySQL查詢優化系列講座之查詢優化器(1)

編輯:關於MYSQL數據庫
  當你提交一個查詢的時候,MySQL會分析它,看是否可以做一些優化使處理該查詢的速度更快。這一部分將介紹查詢優化器是如何工作的。如果你想知道MySQL采用的優化手段,可以查看MySQL參考手冊。

  當然,MySQL查詢優化器也利用了索引,但是它也使用了其它一些信息。例如,如果你提交如下所示的查詢,那麼無論數據表有多大,MySQL執行它的速度都會非常快:

SELECT * FROM tbl_name WHERE 0;
  在這個例子中,MySQL查看WHERE子句,認識到沒有符合查詢條件的數據行,因此根本就不考慮搜索數據表。你可以通過提供一個EXPLAIN語句看到這種情況,這個語句讓MySQL顯示自己執行的但實際上沒有真正地執行的SELECT查詢的一些信息。如果要使用EXPLAIN,只需要在EXPLAIN單詞放在SELECT語句的前面:

MySQL> EXPLAIN SELECT * FROM tbl_name WHERE 0\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
Extra: Impossible WHERE

  通常情況下,EXPLAIN返回的信息比上面的信息要多一些,還包括用於掃描數據表的索引、使用的聯結類型、每張數據表中估計需要檢查的數據行數量等非空(NULL)信息。

  優化器是如何工作的

  MySQL查詢優化器有幾個目標,但是其中最主要的目標是盡可能地使用索引,並且使用最嚴格的索引來消除盡可能多的數據行。你的最終目標是提交SELECT語句查找數據行,而不是排除數據行。優化器試圖排除數據行的原因在於它排除數據行的速度越快,那麼找到與條件匹配的數據行也就越快。如果能夠首先進行最嚴格的測試,查詢就可以執行地更快。假設你的查詢檢驗了兩個數據列,每個列上都有索引:

SELECT col3 FROM mytable
WHERE col1 = ’some value’ AND col2 = ’some other value’;

  假設col1上的測試匹配了900個數據行,col2上的測試匹配了300個數據行,而同時進行的測試只得到了30個數據行。先測試Col1會有900個數據行,需要檢查它們找到其中的30個與col2中的值匹配記錄,其中就有870次是失敗了。先測試col2會有300個數據行,需要檢查它們找到其中的30個與col1中的值匹配的記錄,只有270次是失敗的,因此需要的計算和磁盤I/O更少。其結果是,優化器會先測試col2,因為這樣做開銷更小。

  你可以通過下面一個指導幫助優化器更好地利用索引:

  盡量比較數據類型相同的數據列。當你在比較操作中使用索引數據列的時候,請使用數據類型相同的列。相同的數據類型比不同類型的性能要高一些。例如,INT與BIGINT是不同的。CHAR(10)被認為是CHAR(10)或VARCHAR(10),但是與CHAR(12)或VARCHAR(12)不同。如果你所比較的數據列的類型不同,那麼可以使用ALTER TABLE來修改其中一個,使它們的類型相匹配。

  盡可能地讓索引列在比較表達式中獨立。如果你在函數調用或者更復雜的算術表達式條件中使用了某個數據列,MySQL就不會使用索引,因為它必須計算出每個數據行的表達式值。有時候這種情況無法避免,但是很多情況下你可以重新編寫一個查詢讓索引列獨立地出現。

  下面的WHERE子句顯示了這種情況。它們的功能相同,但是對於優化目標來說就有很大差異了:

WHERE mycol < 4 / 2
WHERE mycol * 2 < 4

  對於第一行,優化器把表達式4/2簡化為2,接著使用mycol上的索引來快速地查找小於2的值。對於第二個表達式,MySQL必須檢索出每個數據行的mycol值,乘以2,接著把結果與4進行比較。在這種情況下,不會使用索引。數據列中的每個值都必須被檢索到,這樣才能計算出比較表達式左邊的值。

  我們看另外一個例子。假設你對date_col列進行了索引。如果你提交一條如下所示的查詢,就不會使用這個索引:

SELECT * FROM mytbl WHERE YEAR(date_col) < 1990;
  這個表達式不會把1990與索引列進行比較;它會把1990與該數據列計算出來的值比較,而每個數據行都必須計算出這個值。其結果是,沒有使用date_col上的索引,因為執行這樣的查詢需要全表掃描。怎麼解決這個問題呢?只需要使用文本日期,接著就可以使用date_col上的索引來查找列中匹配的值了:

WHERE date_col < ’1990-01-01’
  但是,假設你沒有特定的日期。你可能希望找到一些與今天相隔固定的幾天的日期的記錄。表達這種類型的比較有很多種方法--它們的效率並不同。下面就有三種:

WHERE TO_DAYS(date_col) - TO_DAYS(CURDATE()) < cutoff
WHERE TO_DAYS(date_col) < cutoff + TO_DAYS(CURDATE())
WHERE date_col < DATE_ADD(CURDATE(), INTERVAL cutoff DAY)

  對於第一行,不會用到索引,因為每個數據行都必須檢索以計算出TO_DAYS(date_col)的值。第二行要好一些。Cutoff和TO_DAYS(CURDATE())都是常量,因此在處理查詢之前,比較表達式的右邊可以被優化器一次性計算出來,而不需要每個數據行都計算一次。但是date_col列仍然出現在函數調用中,它阻止了索引的使用。第三行是這幾個中最好的。同樣,在執行查詢之前,比較表達式的右邊可以作為常量一次性計算出來,但是現在它的值是一個日期。這個值可以直接與date_col值進行比較,再也不需要轉換成天數了。在這種情況下,會使用索引。

  在LIKE模式的開頭不要使用通配符。有些字符串搜索使用如下所示的WHERE子句:

WHERE col_name LIKE ’%string%’
  如果你希望找到那些出現在數據列的任何位置的字符串,這個語句就是對的。但是不要因為習慣而簡單地把"%"放在字符串的兩邊。如果你在查找出現在數據列開頭的字符串,就刪掉前面的"%"。假設你要查找那些類似MacGregor或MacDougall等以"Mac"開頭的名字。在這種情況下,WHERE子句如下所示:

WHERE last_name LIKE ’Mac%’
  優化器查看該模式中詞首的文本,並使用索引找到那些與下面的表達式匹配的數據行。下面的表達式是使用last_name索引的另一種形式:

WHERE last_name >= ’Mac’ AND last_name < ’Mad’


  這種優化不能應用於使用了REGEXP操作符的模式匹配。REGEXP表達式永遠不會被優化。

  幫助優化器更好的判斷索引的效率。在默認情況下,當你把索引列的值與常量進行比較的時候,優化器會假設鍵值在索引內部是均勻分布的。在決定進行常量比較是否使用索引的時候,優化器會快速地檢查索引,估計出會用到多少個實體(entry)。對應MyISAM、InnoDB和BDB數據表來說,你可以使用ANALYZE TABLE讓服務器執行對鍵值的分析。它會為優化器提供更好的信息。

  使用EXPLAIN驗證優化器的操作。EXPLAIN語句可以告訴你是否使用了索引。當你試圖用另外的方式編寫語句或檢查添加索引是否會提高查詢執行效率的時候,這些信息對你是有幫助的。

  在必要的時候給優化器一些提示。正常情況下,MySQL優化器自由地決定掃描數據表的次序來最快地檢索數據行。在有些場合中優化器沒有作出最佳選擇。如果你察覺這種現象發生了,就可以使用STRAIGHT_JOIN關鍵字來重載優化器的選擇。帶有STRAIGHT_JOIN的聯結類似於交叉聯結,但是強迫數據表按照FROM子句中指定的次序來聯結。

  在SELECT語句中有兩個地方可以指定STRAIGHT_JOIN。你可以在SELECT關鍵字和選擇列表之間的位置指定,這樣會對語句中所有的交叉聯結產生影響;你也可以在FROM子句中指定。下面的兩個語句功能相同:

SELECT STRAIGHT_JOIN ... FROM t1, t2, t3 ... ;
SELECT ... FROM t1 STRAIGHT_JOIN t2 STRAIGHT_JOIN t3 ... ;

  分別在帶有STRAIGHT_JOIN和不帶STRAIGHT_JOIN的情況下運行這個查詢;MySQL可能因為什麼原因沒有按照你認為最好的次序使用索引(你可以使用EXPLAIN來檢查MySQL處理每個語句的執行計劃)。

  你還可以使用FORCE INDEX、USE INDEX或IGNORE INDEX來指導服務器如何使用索引。

  利用優化器更加完善的區域。MySQL可以執行聯結和子查詢,但是子查詢是最近才支持的,是在MySQL 4.1中添加的。因而在很多情況下,優化器對聯結操作的調整比對子查詢的調整要好一些。當你的子查詢執行地很慢的時候,這就是一條實際的提示。有一些子查詢可以使用邏輯上相等的聯結來重新表達。在可行的情況下,你可以把子查詢重新改寫為聯結,看是否執行地快一些。

  測試查詢的備用形式,多次運行。當你測試查詢的備用形式的時候(例如,子查詢與等同的聯結操作對比),每種方式都應該多次運行。如果兩種形式都只運行了一次,那麼你通常會發現第二個查詢比第一個快,這是因為第一個查詢得到的信息仍然保留在緩存中,以至於第二個查詢沒有真正地從磁盤上讀取數據。你還應該在系統負載相對平穩的時候運行查詢,以避免系統中其它的事務影響結果。

  避免過度地使用MySQL自動類型轉換。MySQL會執行自動的類型轉換,但是如果你能夠避免這種轉換操作,你得到的性能就更好了。例如,如果num_col是整型數據列,那麼下面這些查詢將返回相同的結果:

SELECT * FROM mytbl WHERE num_col = 4;
SELECT * FROM mytbl WHERE num_col = ’4’;

  但是第二個查詢涉及到了類型轉換。轉換操作本身為了把整型和字符串型轉換為雙精度型進行比較,使性能惡化了。更嚴重的情況是,如果num_col是索引的,那麼涉及到類型轉換的比較操作不會使用索引。

  相反類型的比較操作(把字符串列與數值比較)也會阻止索引的使用。假設你編寫了如下所示的查詢:

SELECT * FROM mytbl WHERE str_col = 4;
  在這個例子中,不會使用str_col上的索引,因為在把str_col中的字符串值轉換成數值的時候,可能有很多值等於4(例如’4’、’4.0’和’4th’)。分辨哪些值符合要求的唯一辦法是讀取每個數據行並執行比較操作。

  使用EXPLAIN來檢查優化器的操作

  EXPLAIN對於了解優化器生成的、用於處理語句的執行計劃的內部信息是很有幫助的。在這一部分中,我們將解釋EXPLAIN的兩種用途:

  · 查看采用不同的方式編寫的查詢是否影響了索引的使用。

  · 查看向數據表添加索引對優化器生成高效率執行計劃的能力的影響。

  這一部分只討論與示例相關的EXPLAIN輸入字段。

  前面,在"優化器是如何工作的"部分中我們得出的觀點是,你編寫表達式的方式將決定優化器是否能使用可用的索引。特別是上面的討論使用了下面三個邏輯相等的WHERE子句的例子,只有第三個允許使用索引:

WHERE TO_DAYS(date_col) - TO_DAYS(CURDATE()) < cutoff
WHERE TO_DAYS(date_col) < cutoff + TO_DAYS(CURDATE())
WHERE date_col < DATE_ADD(CURDATE(), INTERVAL cutoff DAY)

  EXPLAIN允許你查看編寫表達式的某種方式是否比另外的方式好一些。為了看到結果,讓我們分別用這三個WHERE子句搜索成員表中過期的數據列值,把cutoff值設為30天。為了看到索引的使用和表達式編寫方式之間的關系,我們首先對expiration列進行索引:

MySQL> ALTER TABLE member ADD INDEX (expiration);
  接著在每個表達式形式上使用EXPLAIN,看優化器生成了什麼樣的執行計劃:

MySQL> EXPLAIN SELECT * FROM MEMBER
-> WHERE TO_DAYS(expiration) - TO_DAYS(CURDATE()) < 30\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 102
Extra: Using where
MySQL> EXPLAIN SELECT * FROM MEMBER
-> WHERE TO_DAYS(expiration) < 30 + TO_DAYS(CURDATE())\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 102
Extra: Using where
MySQL> EXPLAIN SELECT * FROM MEMBER
-> WHERE expiration < DATE_ADD(CURDATE(), INTERVAL 30 DAY)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: MEMBER
type: range
possible_keys: expiration
key: expiration
key_len: 4
ref: NULL
rows: 6
Extra: Using where

  上面的結果顯示,前面兩個語句沒有使用索引。類型(type)值表明了將如何從數據表中讀取信息。ALL意味著"將檢查所有的記錄"。也就是說,它會執行全表掃描,沒有利用索引。每個與鍵相關的列都是NULL也表明沒有使用索引。

  與此形成對比的是,第三個語句的結果顯示,采用這種方式編寫的WHERE子句,優化器可以使用expiration列上的索引:

  · 類型(type)值表明它可以使用索引來搜索特定范圍的值(小於右邊表達式給定的值)。

  · 可能鍵(possible_keys)和鍵(key)值顯示expiration上的索引已經被考慮作為備選索引,並且它也是真正使用的索引。

  · 行數(rows)值顯示優化器估計自己需要檢查6個數據行來處理該查詢。這比前面兩個執行計劃的102小很多。

  EXPLAIN的第二種用途是查看添加索引是否能幫助優化器更高效率地執行語句。我將使用兩個未被索引的數據表。它足夠顯示建立索引的效率。相同的規則可以應用於涉及多表的更加復雜的聯結操作。

  假設我們有兩個數據表t1和t2,每個有1000行,包含的值從1到1000。下面的查詢查找出兩個表中值相同的數據行:

MySQL> SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2;
+------+------+
| i1 | i2 |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
...

  兩個表都沒有索引的時候,EXPLAIN產生下面的結果:

MySQL> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: Using where

  類型列中的ALL表明要進行檢查所有數據行的全表掃描。可能鍵列中的NULL表明沒有找到用於提高查詢速度的備選索引(鍵、鍵長度和參考列都是NULL也是因為缺少合適的索引)。Using where表明使用WHERE子句中的信息來識別合格的數據行。

  這段信息告訴我們,優化器沒有為提高執行查詢的效率找到任何有用的信息:

  · 它將對t1表進行全表掃描。

  · 對於t1中的每一行,它將執行t2的全表掃描,使用WHERE子句中的信息識別出合格的行。

  行數值顯示了優化器估計的每個階段查詢需要檢查的行數。T1的估計值是1000,因為1000可以完成全表掃描。相似地,t2的估計值也是1000,但是這個值是對於t1的每一行的。換句話說,優化器所估計的處理該查詢所需要檢查的數據行組合的數量是1000×1000,也就是一百萬。這會造成很大的浪費,因為實際上只有1000個組合符合WHERE子句的條件。
 為了使這個查詢的效率更高,給其中一個聯結列添加索引並重新執行EXPLAIN語句:

MySQL> ALTER TABLE t2 ADD INDEX (i2);
MySQL> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 10
Extra: Using where; Using index

  我們可以看到性能提高了。T1的輸出沒有改變(表明還是需要進行全表掃描),但是優化器處理t2的方式就有所不同了:

  · 類型從ALL改變為ref,意味著可以使用參考值(來自t1的值)來執行索引查找,定位t2中合格的數據行。

  · 參考值在參考(ref)字段中給出了:sampdb.t1.i1。

  · 行數值從1000降低到了10,顯示出優化器相信對於t1中的每一行,它只需要檢查t2中的10行(這是一個悲觀的估計值。實際上,在t2中只有一行與t1中數據行匹配。我們在後面會看到如何幫助優化器改善這個估計值)。數據行組合的全部估計值使1000×10=10000。它比前面的沒有索引的時候估計出來的一百萬好多了。

  對t1進行索引有價值嗎?實際上,對於這個特定的聯結操作,掃描一張表是必要的,因此沒有必要對t1建立索引。如果你想看到效果,可以索引t1.i1並再次運行EXPLAIN:

MySQL> ALTER TABLE t1 ADD INDEX (i1);
MySQL> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: index
possible_keys: i1
key: i1
key_len: 5
ref: NULL
rows: 1000
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 10
Extra: Using where; Using index

  上面的輸出與前面的EXPLAIN的輸出相似,但是添加索引對t1的輸出有一些改變。類型從NULL改成了index,附加(Extra)從空的改成了Using index。這些改變表明,盡管對索引的值仍然需要執行全表掃描,但是優化器還是可以直接從索引文件中讀取值,根據不需要使用數據文件。你可以從MyISAM表中看到這類結果,在這種情況下,優化器知道自己只詢問索引文件就能夠得到所有需要的信息。對於InnoDB 和BDB表也有這樣的結果,在這種情況下優化器可以單獨使用索引中的信息而不用搜索數據行。

  我們可以運行ANALYZE TABLE使優化器進一步優化估計值。這會引起服務器生成鍵值的靜態分布。分析上面的表並再次運行EXPLAIN得到了更好的估計值:

MySQL> ANALYZE TABLE t1, t2;
MySQL> EXPLAIN SELECT t1.i1, t2.i2 FROM t1, t2 WHERE t1.i1 = t2.i2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: index
possible_keys: i1
key: i1
key_len: 5
ref: NULL
rows: 1000
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
type: ref
possible_keys: i2
key: i2
key_len: 5
ref: sampdb.t1.i1
rows: 1
Extra: Using where; Using index

  在這種情況下,優化器估計在t2中與t1的每個值匹配的數據行只有一個。
 重載優化過程

  這個過程聽起來多余,但是有時候你還是希望去掉某些MySQL優化行為的:

  重載優化器的表聯結次序。使用STRAIGHT_JOIN強迫優化器按照特定的次序使用數據表。在這樣操作的時候,你必須對數據表進行排序,這樣才能保證第一張表是被選擇的行數最少的表。如果你不能確定被選擇行數最少的是哪一張表,那麼就把行數最多的放到第一的位置。換句話說,試著對表進行排序,使最有約束力的選擇出現在最前面。你對可能的備選數據行縮小地越早,執行查詢的性能就越好。請確保在帶有STRAIGHT_JOIN和不帶STRAIGHT_JOIN的時候分別執行該查詢。有時候由於某些原因的存在,優化器沒有按照你認定的方式聯結數據表,STRAIGHT_JOIN也可能沒有實際的幫助作用。

  另一個可能性是在聯結的數據表列表中的某個表的後面使用FORCE INDEX、USE INDEX和IGNORE INDEX調節符來告訴MySQL如何使用索引。這在優化器沒有做出正確選擇的時候是有用處的。

  以最小的代價清空一張表。當需要完全地清空一張MyISAM數據表的時候,最快的方法是刪除它並利用它的.frm文件中存儲的腳本來重新建立它。使用TRUNCATE TABLE語句實現:

TRUNCATE TABLE tbl_name;
  通過重新建立MyISAM數據表來清空它的這種服務器優化措施使該操作非常快,因為不需要單獨地逐行刪除。

  但是TRUNCATE TABLE也帶來了一些副作用,在某些環境中是不符合要求的:

  · TRUNCATE TABLE不一定能夠計算出被刪除的數據列的精確數量。如果你需要這個數值,請使用不帶WHERE子句的DELETE語句:

DELETE FROM tbl_name;
  · 但是,通過重新建立來清空數據表,它可能會把序號的起始值設置為1。為了避免這種情況,請使用"不優化的"全表DELETE語句,它帶有一個恆為真的WHERE子句:

DELETE FROM tbl_name WHERE 1;
  添加WHERE子句會強迫MySQL進行逐行刪除,因為它必須計算出每一行的值來判斷是否能夠刪除它。這個語句執行的速度很慢,但是它卻保留了當前的AUTO_INCREMENT序號。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved