MySQL查詢優(yōu)化需要經(jīng)過解析洁仗、預(yù)處理和優(yōu)化三個步驟脾还。在這些過程中,都有可能發(fā)生錯誤识椰。本篇文章不會深入討論錯誤處理绝葡,而是幫助理解 MySQL 執(zhí)行查詢的方式,以便可以寫出更好的查詢語句腹鹉。
解析器和預(yù)處理器
一開始藏畅,MySQL 的解析器將查詢語句拆分成一系列指令并從中構(gòu)建一棵“解析樹”。解析器使用 MySQL 的SQL 語法去翻譯和驗(yàn)證查詢語句种蘸。例如墓赴,解析器保證了查詢中的指令是有效且次序正確竞膳,并且會檢查那種類似字符串引號未配對的錯誤。
預(yù)處理器則檢查構(gòu)建好的解析樹中那些解析器無法處理的語義信息诫硕。例如坦辟,檢查數(shù)據(jù)表和列是否存在,并且處理字段名稱和別名以保證列引用沒有歧義章办。接下來锉走,預(yù)處理器會檢查權(quán)限,通常這會非撑航欤快(除非你的服務(wù)端有一大堆權(quán)限配置)挪蹭。
查詢優(yōu)化器
經(jīng)過解析器和預(yù)處理器后,解析樹就被確定是有效的了休偶,可以被優(yōu)化器進(jìn)行處理并最終轉(zhuǎn)變?yōu)橐粋€查詢計劃梁厉。一個具有相同結(jié)果的查詢通常有很多種執(zhí)行方式,而優(yōu)化器的職責(zé)是找出其中最優(yōu)的選項(xiàng)踏兜。
MySQL使用基于代價估計的優(yōu)化器词顾,這意味著它視圖預(yù)測眾多執(zhí)行計劃的代價,并選擇代價最低的那個碱妆。最初的單位成本是隨機(jī)的4KB 數(shù)據(jù)頁讀取肉盹,而現(xiàn)在變得更為復(fù)雜,包括了如執(zhí)行 WHERE比較條件的代價疹尾∩先蹋可以通過顯示 Last_query_cost 會話變量來查看查詢優(yōu)化器估計查詢語句的代價。
SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor;
SHOW STATUS LIKE 'Last_query_cost';
顯示的 Last_query_cost 意味著優(yōu)化器估計需要執(zhí)行對應(yīng)次數(shù)的隨機(jī)數(shù)據(jù)頁訪問才能完成查詢纳本。這是基于如下統(tǒng)計估算的結(jié)果:
- 數(shù)據(jù)表或索引占據(jù)的數(shù)據(jù)頁數(shù)窍蓝;
- 索引的候選值;
- 數(shù)據(jù)行繁成、鍵及鍵值分布對應(yīng)的數(shù)據(jù)長度它抱。
優(yōu)化器并不會考慮估計內(nèi)容的緩存——它假設(shè)每次都從磁盤 I/O 讀取結(jié)果。優(yōu)化器并不是每次都能選擇最優(yōu)的執(zhí)行計劃朴艰,原因如下:
- 統(tǒng)計本身可能是錯誤的。服務(wù)端的統(tǒng)計結(jié)果依賴于存儲引擎混移,而存儲引擎可能十分準(zhǔn)確也可能很不準(zhǔn)確祠墅。例如,InnoDB 由于其 MVCC 架構(gòu)歌径,并不保留數(shù)據(jù)表的準(zhǔn)確行數(shù)毁嗦。
- 估計的代價和實(shí)際運(yùn)行的代價并不等價,因此即便統(tǒng)計是準(zhǔn)確的回铛,查詢的代價與 MySQL 的估計也會或多或少存在偏差狗准。一個讀取更多數(shù)據(jù)頁的查詢計劃也可能代價更低克锣,例如如果是有序的磁盤 I/O 訪問就會更快,又或是結(jié)果本身就已經(jīng)在緩存中腔长。因此袭祟,優(yōu)化器本身并不知道查詢會引起多少次 I/O 操作。
- MySQL 人為的優(yōu)化也許與我們期待的不同捞附。我們要的可能是更快的執(zhí)行時間巾乳,而 MySQL 并不是只追求快,它是最求最小化代價鸟召。因此胆绊,通過代價并不一定科學(xué)。
- MySQL并不考慮并發(fā)中的查詢欧募,而這可能會影響查詢運(yùn)行的速度压状。
- MySQL 并不是一直都按代價估計做優(yōu)化。有時候僅僅是遵循一些規(guī)則跟继,例如如果有一個全文匹配條件(MATCH 方法)則使用全文索引种冬。即便是有一個更快的的其他索引和非全文條件查詢,MySQL 也不會按更快的方式執(zhí)行查詢还栓。
- 優(yōu)化器對于不歸它控制的操作的代價并不會考慮碌廓,例如執(zhí)行存儲過程或自定義函數(shù)。
- 優(yōu)化器并不總是能夠估計每一個執(zhí)行計劃剩盒,有些時候它會忽略一個更優(yōu)的計劃谷婆。
MySQL 查詢優(yōu)化器是其中非常復(fù)雜的一部分,使用了很多優(yōu)化方式將查詢語句轉(zhuǎn)換成為一個查詢執(zhí)行計劃辽聊。通常有兩種優(yōu)化方式:靜態(tài)優(yōu)化和動態(tài)優(yōu)化纪挎。靜態(tài)優(yōu)化可以簡單地通過檢查解析樹進(jìn)行。例如跟匆,優(yōu)化器可以將 WHERE 條件通過數(shù)學(xué)運(yùn)算規(guī)則轉(zhuǎn)換成一個等式异袄。靜態(tài)優(yōu)化與具體的值無關(guān),例如 WHERE條件的常量值玛臂。他們執(zhí)行一次后會一直有效烤蜕,即便是查詢語句使用了不同的值再次執(zhí)行〖T可以理解為是“編譯時優(yōu)化”讽营。
相反,動態(tài)優(yōu)化是基于具體的情景的泡徙,并依賴于多種因素橱鹏。例如,WHERE 條件中的值或索引中對應(yīng)的數(shù)據(jù)行數(shù)。這個過程在每次查詢都需要重新估計莉兰,可以理解為是“運(yùn)行時優(yōu)化”挑围。以下是一些 MySQL 的典型優(yōu)化方式:
- 聯(lián)合查詢重新排序:數(shù)據(jù)表并不一定需要按照查詢語句的順序聯(lián)合。決定最優(yōu)的聯(lián)合查詢次序是十分重要的優(yōu)化糖荒。
- 將外聯(lián)接轉(zhuǎn)換為內(nèi)聯(lián)接:一個外聯(lián)接并不一定需要按外聯(lián)接查詢杉辙。有些因素,例如 WHERE 條件和數(shù)據(jù)表結(jié)構(gòu)可以將外聯(lián)接查詢等價于內(nèi)聯(lián)接寂嘉。MySQL 可以識別這些情況奏瞬,并重寫聯(lián)合查詢。
- 應(yīng)用數(shù)學(xué)等價公式:MySQL 應(yīng)用數(shù)學(xué)等價轉(zhuǎn)換簡化表達(dá)式泉孩∨鸲耍可以做到展開和減少常量,排除不可能的情況和常量表達(dá)式寓搬。例如珍昨,表達(dá)式(5=5 AND a>5)會精簡為(a>5)。同樣的句喷,(a<b AND b=c) ADN a = 5會轉(zhuǎn)換為 b > 5 AND b=c AND a=5.這些規(guī)則對帶條件的查詢十分有用镣典。
- COUNT(),MIN()和 MAX()優(yōu)化:索引和空值列通惩偾恚可以幫助 MySQL 優(yōu)化這些函數(shù)兄春。例如,查找二叉樹最左側(cè)一列的最小值時锡溯,MySQL 可以只請求索引的第一行數(shù)據(jù)赶舆。甚至可以在查詢優(yōu)化階段完成這個事情,而對于剩余的查詢當(dāng)作是常量值祭饭。而對于查詢最大值也是一樣芜茵,只需要讀取最后u 一行即可。如果服務(wù)端使用了這種優(yōu)化倡蝙,可以在 EXPLAIN 中看到“Select tables optimized away”九串。這意味著優(yōu)化器將數(shù)據(jù)表從查詢計劃中移除并用常量替代了。類似地寺鸥,COUNT(*)查詢在沒有指定 WHERE 條件時也可以在某些存儲引擎被優(yōu)化(例如 MyISAM猪钮,會一直保存數(shù)據(jù)表的準(zhǔn)確行數(shù))。
- 評估和精簡常量表達(dá)式:一旦 MySQL 檢測到一個表達(dá)式可以精簡為一個常量胆建,那在優(yōu)化階段就會完成該操作躬贡。例如,一個用戶定義的變量如果在查詢過程中沒有變化眼坏,就可以轉(zhuǎn)換為常量。令人驚奇的是,在優(yōu)化階段宰译,有些你認(rèn)為是一個查詢的語句也會被轉(zhuǎn)換為常量檐蚜。一個例子就是 索引上的MIN()。這種情況也可以擴(kuò)展到對主鍵或獨(dú)立索引的常量查詢沿侈。如果 WHERE 條件對這樣的索引指定了常量闯第,優(yōu)化器會知道 MySQL 會在查詢開始就查找對應(yīng)的值。然后缀拭,就會在剩余的查詢中把這個值當(dāng)做常量處理咳短。下面是一個例子:
EXPLAIN SELECT film.film_id, film_actor.actor_id
FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
WHERE film.film_id = 1;
MySQL 會將這個查詢拆分為2步,因此分析結(jié)果會有兩行蛛淋。第一步是是在 film 表中查找對應(yīng)的數(shù)據(jù)行咙好。由于 是按主鍵film_id查詢的,MySQL 知道只有一行數(shù)據(jù)褐荷。 因此勾效,此時的查詢分析結(jié)果的 ref 是常量。在第二步中叛甫,MySQL 會將 film_id 作為已知值层宫,因此對 film_actor 的查詢的 ref 也是常量。其他類似的場景還有 WHERE其监,USING或 ON 條件中的約束條件是等式萌腿。在這個例子中,MySQL 知道 USING條件的 film_id 在查詢語句中都是相同的值抖苦,這個值必須和 WHERE條件的 film_id 相同毁菱。
- 覆蓋索引:MySQL 有時候會利用索引數(shù)據(jù)而避免讀數(shù)行數(shù)據(jù),如果索引包含了查詢所需的全部列的話睛约。
- 子查詢優(yōu)化:MySQL 能夠?qū)⒁恍╊愋偷淖硬樵冝D(zhuǎn)換為更有效的變體形式鼎俘,從而簡化它們?yōu)樗饕樵兌皇窍嗷オ?dú)立的查詢。
- 提前中止:MySQL 可以在滿足查詢結(jié)果后提前中止查詢過程辩涝。最明顯的例子是 LIMIT條件贸伐。也有一些其他的提前中止的情形。例如怔揩,MySQL 檢測導(dǎo)一個可能條件后捉邢,可以中止整個查詢,如下面的例子所示:
EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id=1;
在分析結(jié)果中的 Extra字段會看到“Impossible WHERE noticed after reading const tables”商膊。在其他情形也會有提前中止的情況伏伐,例如:
SELECT film.film_id
FROM sakila.film
LEFT OUTER JOIN sakila.film_actor USING(film_id)
WHERE sakila.film_actor.film_id IS NULL;
這個查詢排除那些有演員的電影。每部電源都可能有多名演員晕拆,但是只要找到一名演員后藐翎,MySQL 就會停止處理當(dāng)前的這部電影,而去處理下一部。對于 DISTINCT吝镣,NOT EXISTS 也會有類似的情況堤器。
- 等效傳遞:MySQL 會識別導(dǎo)查詢語句中保持的列是否是等效的。例如末贾,在 JOIN 條件中闸溃,WHERE 條件會影響導(dǎo)相同的列,如下面的查詢:
SELECT film.film_id
FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
WHERE film.film_id > 500;
MySQL 會知道 WHERE 條件的約束不僅適用于 film 表拱撵,同樣也適用于 film_actor 表辉川。但對于其他數(shù)據(jù)庫則未必會有這樣的優(yōu)化效果。
- IN 查詢比較:對于很多數(shù)據(jù)庫服務(wù)器拴测,IN 查詢比等價為多個 OR 條件乓旗,在邏輯上二者是等效的。但在 MySQL 中不是這樣昼扛,MySQL會對 IN 查詢的列表值進(jìn)行排序寸齐,并使用二分查找法去檢查查詢值是否在列表中。這會使得算法復(fù)雜度從 O(n)降低導(dǎo) O(log n)抄谐。
實(shí)際上渺鹦,MySQL 使用的優(yōu)化手段比上述列舉的多得多,這里沒法一一列舉蛹含。只是需要記住 MySQL 的優(yōu)化器的復(fù)雜性及其智能化程度毅厚。因此,應(yīng)當(dāng)讓優(yōu)化器發(fā)揮其作用浦箱,而不是無限優(yōu)化查詢語句直到 MySQL 的優(yōu)化器沒有用武之地吸耿。當(dāng)然,雖然 MySQL 的優(yōu)化器很聰明酷窥,但是它給出的并不一定是最優(yōu)結(jié)果咽安,有些時候你知道最優(yōu)結(jié)果,而 MySQL 未必知道蓬推。這種情況下妆棒,你可以對查詢語句進(jìn)行優(yōu)化從而幫助 MySQL 完成優(yōu)化工作,而有些時候則需要增加查詢的提示沸伏,或是重寫查詢糕珊,修改數(shù)據(jù)表設(shè)計或增加索引。