Mysql查詢優化器

jopen 10年前發布 | 20K 次閱讀 MySQL 數據庫服務器

1 定義
Mysql查詢優化器的工作是為查詢語句選擇合適的執行路徑。查詢優化器的代碼一般是經常變動的,這和存儲引擎不太一樣。因此,需要理解最新版本的查詢優化器是如何組織的,請參考相應的源代碼。整體而言,優化器有很多相同性,對mysql一個版本的優化器做到整體掌握,理解起mysql新版本以及其他數據庫的優化器都是類似的。優化器會對查詢語句進行轉化,轉化等價的查詢語句。舉個例子,優化器會將下面語句進行轉化:
SELECT … WHERE 5=a;
轉化后的等價語句為:
SELECT … WHERE a=5;
因為這兩個語句的結果集是一致的,所以這兩個語句是等價的。
這里我需要提出一點需要注意的,如果查詢語句沒帶order by。查詢語句1出現的結果為(1,1),(2,2),查詢語句2出現的結果為(2,2),(1,1),我們會認為這是等價的,因為不帶order by的查詢語句是無序的,怎么排序都行。
 2 代碼組織
在內核當中handle_select()函數是處理查詢語句的頂層函數,里面有兩個分支,一個是處理帶union的情況,另外一個是處理不帶union的情況,這里我們只是列出一個簡單的路徑便于說明,調用層次見下圖。
handle_select()
   mysql_select()
     JOIN::prepare()
       setup_fields()
     JOIN::optimize()            / optimizer is from here ... /
       optimize_cond()
       opt_sum_query()
       make_join_statistics()
         get_quick_record_count()
         choose_plan()
           / Find the best way to access tables /
           / as specified by the user.          /
           optimize_straight_join()
             best_access_path()
           / Find a (sub-)optimal plan among all or subset /
           / of all possible query plans where the user    /
           / controlls the exhaustiveness of the search.   /
           greedy_search()
             best_extension_by_limited_search()
               best_access_path()
           / Perform an exhaustive search for an optimal plan /
           find_best()
       make_join_select()        / ... to here /
     JOIN::exec()
上面的縮進表示函數的相互調用關系,因此可以看出handle_select()調用函數mysql_select(),mysql_select()調用 JOIN::prepare(),等等。mysql_select()首先調用函數JOIN::prepare()進行語句分析、元數據設置、子查詢轉化等等。然后調用函數JOIN::optimize()進行優化,選出最后的執行計劃。最后調用函數JOIN::exec()執行該執行計劃。盡管出現了單詞“JOIN”,這些優化函數是為所有的查詢語句服務的,不管你是什么查詢類型。函數optimize_cond()和函數 opt_sum_query()是執行一些轉化操作。函數make_join_statistics()對所有可用索引統計信息進行分析。
 3 常量轉化
 對類似下面的表達式可以進行轉化:
WHERE column1 = column2 AND column2 = 'x';
 因為我們知道:如果A=B and B=C,那么A=C。所以上面的表達式可以轉化為:
WHERE column1 = 'x'  AND column2 = 'x';
 對于column1 <operator> column2,只要<operator>是屬于下面的操作符之一就可以進行類似的轉化:
=,<,>,<=,>=,<>,<=>,LIKE
 從中我們也可以看出,對于BETWEEN的情況是不進行轉換的。
 4 無效代碼的排除
 見如下表達式:WHERE 0=0 AND column1='y'因為第一個條件是始終為true的,所以可以移除該條件,變為:WHERE column1='y'
 再見如下表達式:WHERE (0=1 AND s1=5) OR s1=7因為前一個括號內的表達式始終為false,因此可以移除該表達式,變為:WHERE s1=7
一些情況下甚至可以將整個WHERE子句去掉,見下面的表達式:WHERE (0=1 AND s1=5)我們可以看到,WHERE子句始終為FALASE,那么WHERE條件是不可能發生的。當然我們也可以講,WHERE條件被優化掉了。
如果一個列的定義是不允許為NULL,那么:WHERE not_null_column IS NULL該條件是始終為false的,再看:WHERE not_null_column IS NOT NULL
該條件是始終為true的,因此這樣的表達式也是可以從條件表達式中刪除的。當然,也是有特殊情況的,比如在out join中,被定義為NOT NULL的列也可能包含NULL值。在這種情況下,IS NULL條件是被保留的。
當然優化器沒有對所有的情況進行檢測,因為這實在太復雜了。舉個例子:CREATE TABLE Table1(column1 CHAR(1));SELECT FROM Table1 WHERE column1 = 'Canada';盡管該條件是無效條件,優化器也不會將它移除。
5 常量計算 如下表達式:WHERE columb1 = 1 + 2轉化為:WHERE columb1 = 3
6 常量以及常量表
 常量表的定義如下:
1)一個表只有0行或者1行數據。
2)在WHERE子句中包含條件column = constant,并且這些列是primary key,或者這些列是UNIQUE(假設該UNIQUE同時被定義為NOT NULL)。這樣生成的查詢結果也可以成為常量表。
如果表Table0定義中包含:PRIMARY KEY(column1,column2)再看下面的語法:FROM Table0 … WHERE column1=5 AND column2=7 …
 那么該語句返回的就是常量表。舉個更簡單的情況,建設Table1定義中包含:unique_not_null_column INT NOT NULL UNIQUE
再看下面的語法:FROM Table1 ... WHERE unique_not_null_column=5
 該語句返回的也是常量表。
從例子中我們可以看出常量表最多只有1個行值。MySQL會預先評估常量表,找出這個值,然后將這個值引入到查詢語句中進行優化,舉例如下:
SELECT Table1.unique_not_null_column, Table2.any_column
    FROM Table1, Table2
    WHERE Table1.unique_not_null_column = Table2.any_column
    AND Table1.unique_not_null_column = 5;
 在評估這個查詢語句時,MySQL首先發現通過Table1.unique_not_null_column條件的限制,Table1會變成一個常量表。然后,取回該值。
 如果取回操作失敗(Table1中沒有行滿足條件unique_not_null_column = 5),那么該常量表就包含0行,那么如果對該語句執行EXPLAIN操作,會得到提示信息:
Impossible WHERE noticed after reading const tables
 另外一種情況是取回操作成功(Table1中嚴格只有一行滿足條件unique_not_null_column = 5),那么常量表中包含一條數據,并且MySQL會將查詢語句轉化為:
SELECT 5, Table2.any_column
    FROM Table1, Table2
    WHERE 5 = Table2.any_column
    AND 5 = 5;
7 存取類型
 當我們評估一個條件表達式,MySQL判斷該表達式的存取類型。下面是一些存取類型,按照從最優到最差的順序進行排列:
system      … 系統表,并且是常量表
const       … 常量表
eq_ref      …   unique/primary索引,并且使用的是'='進行存取
ref         … 索引使用'='進行存取
ref_or_null … 索引使用'='進行存取,并且有可能為NULL
range       … 索引使用BETWEEN、IN、>=、LIKE等進行存取
index       …   索引全掃描
ALL        … 表全掃描
 優化器根據存取類型選擇合適的驅動表達式。考慮如下的查詢語句:
SELECT
FROM Table1 WHERE indexed_column = 5 AND unindexed_column = 6
 因為indexed_column擁有更好的存取類型,所以更有可能使用該表達式做為驅動表達式。這里只考慮簡單的情況,不考慮特殊的情況。
 那么驅動表達式的意思是什么呢?考慮到這個查詢語句有兩種可能的執行方法:
1)不好的執行路徑:讀取表的每一行(稱為“全表掃描”),對于讀取到的每一行,檢查相應的值是否滿足indexed_column以及unindexed_column對應的條件。
2)好的執行路徑:通過鍵值indexed_column=5查找B樹,對于符合該條件的每一行,判斷是否滿足unindexed_column對應的條件。
 一般情況下,索引查找比全表掃描需要更少的存取路徑,尤其當表數據量很大,并且索引的類型是UNIQUE的時候。因此稱它為好的執行路徑,使用indexed_column列作為驅動表達式。
8 范圍存取類型
 一些表達式可以使用索引,但是屬于索引的范圍查找。這些表達式通常對應的操作符是:>、>=、<、<=、IN、LIKE、BETWEEN。對優化器而言,如下表達式:
column1 IN (1,2,3)
該表達式與下面的表達式是等價的:column1 = 1 OR column1 = 2 OR column1 = 3并且MySQL也是認為它們是等價的,所以沒必要手動將IN改成OR,或者把OR改成IN。
 優化器將會對下面的表達式使用索引范圍查找:column1 LIKE 'x%'但對下面的表達式就不會使用到索引了:column1 LIKE '%x'這是因為當首字符是通配符的時候,沒辦法使用到索引進行范圍查找。
 對優化器而言,如下表達式:column1 BETWEEN 5 AND 7該表達式與下面的表達式是等價的:column1 >= 5 AND column1 <= 7 同樣,MySQL也認為它們是等價的。
 如果需要檢查過多的索引鍵值,優化器將放棄使用索引范圍查找,而是使用全表掃描的方式。這樣的情況經常出現如下的情況下:索引是多層次的二級索引,查詢條件是'<'以及是'>'的情況。
9 索引存取類型
 考慮如下的查詢語句:
SELECT column1 FROM Table1;
 如果column1是索引列,優化器更有可能選擇索引全掃描,而不是采用表全掃描。這是因為該索引覆蓋了我們所需要查詢的列。
 再考慮如下的查詢語句:SELECT column1,column2 FROM Table1;如果索引的定義如下,那么就可以使用索引全掃描:CREATE INDEX … ON Table1(column1,column2);也就是說,所有需要查詢的列必須在索引中出現。
10轉換
 MySQL對簡單的表達式支持轉換。比如下面的語法:WHERE -5 = column1轉換為:WHERE column1 = -5盡管如此,對于有數學運算存在的情況不會進行轉換。比如下面的語法:WHERE 5 = -column1不會轉換為:WHERE column1 = -5
11 AND
 帶AND的查詢的格式為:<condition> AND <condition>,考慮如下的查詢語句:WHERE column1='x' AND column2='y'
 優化的步驟:
1)如果兩個列都沒有索引,那么使用全表掃描。
2)否則,如果其中一個列擁有更好的存取類型(比如,一個具有索引,另外一個沒有索引;再或者,一個是唯一索引,另外一個是非唯一索引),那么使用該列作為驅動表達式。
3)否則,如果兩個列都分別擁有索引,并且兩個條件對應的存取類型是一致的,那么選擇定義索引時的先定義的索引。
舉例如下:
CREATE TABLE Table1 (s1 INT,s2 INT);
CREATE INDEX Index1 ON Table1(s2);
CREATE INDEX Index2 ON Table1(s1);
SELECT FROM Table1 WHERE s1=5 AND s2=5;優化器選擇s2=5作為驅動表達式,因為s2上的索引是新建的。
 12 OR
 帶 OR的查詢格式為:<condition> OR <condition>,考慮如下的查詢語句:WHERE column1='x' OR column2='y'優化器做出的選擇是采用全表掃描。當然,在一些特定的情況,可以使用索引合并,這里不做闡述。
如果兩個條件里面設計的列是同一列,那么又是另外一種情況,考慮如下的查詢語句:WHERE column1='x' OR column1='y'在這種情況下,該查詢語句采用索引范圍查找。
13 UNION
 所有帶UNION的查詢語句都是單獨優化的,考慮如下的查詢語句:SELECT
FROM Table1 WHERE column1='x'UNION ALL SELECT FROM Table1 WHERE column2='y'如果column1與column2都是擁有索引的,每個查詢都是使用索引查詢,然后合并結果集。
14 NOT,<>
 考慮如下的表達式:Column1<> 5從邏輯上講,該表達式等價于下面的表達式:Column1<5 OR column1>5然而,MySQL不會進行這樣的轉換。如果你覺得使用范圍查找會更好一些,應該手動地進行轉換。
 考慮如下的表達式:
WHERE NOT (column1!=5)從邏輯上講,該表達式等價于下面的表達式:WHERE column1=5同樣地,MySQL也不會進行這樣的轉換。
15 ORDER BY
 一般而言,ORDER BY的作用是使結果集按照一定的順序排序,如果可以不經過此操作就能產生順序的結果,可以跳過該ORDER BY操作。
 考慮如下的查詢語句:SELECT column1 FROM Table1 ORDER BY 'x';優化器將去除該ORDER BY子句,因為此處的ORDER BY子句沒有意義。
 再考慮另外的一個查詢語句:
SELECT column1 FROM Table1 ORDER BY column1;在這種情況下,如果column1類上存在索引,優化器將使用該索引進行全掃描,這樣產生的結果集是有序的,從而不需要進行ORDER BY操作。
 再考慮另外的一個查詢語句:
SELECT column1 FROM Table1 ORDER BY column1+1;假設column1上存在索引,我們也許會覺得優化器會對column1索引進行全掃描,并且不進行ORDER BY操作。實際上,情況并不是這樣,優化器是使用column1列上的索引進行全掃表,僅僅是因為索引全掃描的效率高于表全掃描。對于索引全掃描的結果集仍然進行ORDER BY排序操作。
16 GROUP BY
 這里列出對GROUP BY子句以及相關集函數進行優化的方法:
1)如果存在索引,GROUP BY將使用索引。
2)如果沒有索引,優化器將需要進行排序,一般情況下會使用HASH表的方法。
3)如果情況類似于“GROUP BY x ORDER BY x”,優化器將會發現ORDER BY子句是沒有必要的,因為GROUP BY產生的結果集是按照x進行排序的。
4)盡量將HAVING子句中的條件提升中WHERE子句中。
5) 對于MyISAM表,“SELECT COUNT(
) FROM Table1;”直接返回結果,而不需要進行表全掃描。但是對于InnoDB表,則不適合該規則。補充一點,如果column1的定義是NOT NULL的,那么語句“SELECT COUNT(column1) FROM Table1;”等價于“SELECT COUNT(*) FROM Table1;”。
6)考慮MAX()以及MIN()的優化情況。考慮下面的查詢語句:
SELECT MAX(column1)FROM Table1 WHERE column1 < 'a';如果column1列上存在索引,優化器使用'a'進行索引定位,然后返回前一條記錄。
7)考慮如下的查詢語句:
SELECT DISTINCT column1 FROM Table1;在特定的情況下,語句可以轉化為:
SELECT column1 FROM Table1 GROUP BY column1;該轉換的前提條件是:column1上存在索引,FROM上只有一個單表,沒有WHERE條件并且沒有LIMIT條件

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!