一.為什么查詢速度會慢
MySQL在執行查詢的時候有哪些子任務,哪些子任務運行的速度很慢?這里很難給出完整的列表。通常來說,查詢的生命周期大致可以按照順序來看:從客戶端,到服務器,然后在服務器上進行解析,生成執行計划,執行,並返回結果給客戶端。其中“執行”可以認為是整個生命周期中最重要的階段,這其中包括了大量為了檢索數據到存儲引擎的調用以及調用后的數據處理,包括排序、分組等。
二.慢查詢基礎:優化數據訪問
查詢性能低下最基本的原因是訪問的數據太多。某些查詢可能不可避免地需要篩選大量數據,但這並不常見。大部分性能低下的查詢都可以通過減少訪問的數據量的方式進行優化。對於低效的查詢,我們發現通過下面兩個步驟來分析總是很有效:
(1).確認應用程序是否在檢索大量超過需要的數據。這通常意味着訪問了太多的行,但有時候也可能是訪問了太多的列。
(2).確認MySQL服務器層是否在分析大量超過需要的數據行。
1.是否向數據庫請求了不需要的數據
有些查詢會請求超過實際需要的數據,然后這些多余的數據會被應用程序丟棄。這會給MySQL服務器帶來額外的負擔,並增加網絡開銷,另外也會消耗應用服務器的CPU和內存資源。
這里有一些典型案例:
(1)查詢不需要的記錄:
一個常見的錯誤是常常會誤以為MySQL會只返回需要的數據,實際上MySQL卻是先返回全部結果集再進行計算。我們經常會看到一些了解其他數據庫系統的人會.設計出這類應用程序。這些開發者習慣使用這樣的技術,先使用SELECT語句查詢大量的結果,然后獲取前面的N行后關閉結果集(例如在新聞網站中取出100條記錄,但是只是在頁面上顯示前面10條)。他們認為MySQL會執行查詢,並只返回他們需要的10條數據,然后停止查詢。實際情況是MySQL會查詢出全部的結果集,客戶端的應用程序會接收全部的結果集數據,然后拋棄其中大部分數據。最簡單有效的解決方法就是在這樣的查詢后面加上LIMIT。
(2)多表關聯時返回全部列:
如果你想查詢所有在電影AcademyDinosaur中出現的演員,千萬不要按下面的寫法
編寫查詢:
mysql> SELECT * FROM sakila.actor
-> INNER J0IN sakila. fi1m_ actor USING(actor_ id)
-> INNER J0IN sakila. film USING(film id)
-> WHERE sakila.film.title = ' Academy Dinosaur' ;
這將返回這三個表的全部數據列。正確的方式應該是像下面這樣只取需要的列:
mysql> SELECT sakila.actor.* FROM sakila.actor...;
(3)總是取出全部列:
每次看到SELECT*的時候都需要用懷疑的眼光審視,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,會讓優化器無法完成索引覆蓋掃描這類優化,還會為服務器帶來額外的I/O、內存和CPU的消耗。因此,一些DBA是嚴格禁止SELECT*的寫法的,這樣做有時候還能避免某些列被修改帶來的問題。
(4)重復查詢相同的數據
如果你不太小心,很容易出現這樣的錯誤一不斷 地重復執行相同的查詢,然后每次都返回完全相同的數據。例如,在用戶評論的地方需要查詢用戶頭像的URL,那么用戶多次評論的時候,可能就會反復查詢這個數據。比較好的方案是,當初次查詢的時候將這個數據緩存起來,需要的時候從緩存中取出,這樣性能顯然會更好。
2.MySQL是否在掃描額外的記錄
對於MySQL,最簡單的衡量查詢開銷的三個指標包含:響應時間、掃描的行數、返回的行數。
響應時間:響應時間是兩個部分之和:服務時間和排隊時間。服務時間是指數據庫處理這個查詢真正花了多長時間。排隊時間是指服務器因為等待某些資源而沒有真正執行查詢的時間一-可 能是等I/O操作完成,也可能是等待行鎖,等等。遺憾的是,我們無法把響應時間細分到上面這些部分,除非有什么辦法能夠逐個測量上面這些消耗,不過很難做到。一般最常見和重要的等待是I/O和鎖等待,但是實際情況更加復雜。所以在不同類型的應用壓力下,響應時間並沒有什么-致的規律或者公式。諸如存儲引擎的鎖(表鎖、行鎖)、高並發資源競爭、硬件響應等諸多因素都會影響響應時間。所以,響應時間既可能是一個問題的結果也可能是-一個問題的原因,不同案例情況不同。
掃描的行數和返回的行數:
掃描的行數和訪問類型:
在評估查詢開銷的時候,需要考慮- -下從表中找到某一行數據的成本。MySQL有好幾種訪問方式可以查找並返回一行結果。有些訪問方式可能需要掃描很多行才能返回一行結果,也有些訪問方式可能無須掃描就能返回結果。在EXPLAIN語句中的type列反應了訪問類型。訪問類型有很多種,從全表掃描到索引掃描、范圍掃描、唯一索引查詢、常數引用等。這里列的這些,速度是從慢到快,掃描的行數也是從小到大。你不需要記住這些訪問類型,但需要明白掃描表、掃描索引、范圍訪問和單值訪問的概念。如果查詢沒有辦法找到合適的訪問類型,那么解決的最好辦法通常就是增加一一個合適的索引,這也正是我們前--章討論過的問題。現在應該明白為什么索引對於查詢優化如此重要了。索引讓MySQL以最高效、掃描行數最少的方式找到需要的記錄。
三.重構查詢的方式
在優化有問題的查詢時,目標應該是找到一一個更優的方法獲得實際需要的結果一而不一定總是需要從MySQL獲取- -模一樣的結果集。有時候,可以將查詢轉換一種寫 法讓其返回一樣的結果,但是性能更好。但也可以通過修改應用代碼,用另一種方式完成查詢, 最終達到一樣的目的。
1.一個復雜查詢還是多個簡單查詢
設計查詢的時候-一個需要考慮的重要問題是,是否需要將-一個復雜的查詢分成多個簡單的查詢。MySQL從設計上讓連接和斷開連接都很輕量級,在返回一個小的查詢結果方面很高效。現代的網絡速度比以前要快很多,無論是帶寬還是延遲。在某些版本的MySQL上,即使在-一個通用服務器上,也能夠運行每秒超過10萬的查詢,即使是一個千兆網卡也能輕松滿足每秒超過2000次的查詢。所以運行多個小查詢現在已經不是大問題了。
2.切分查詢
有時候對於一個大查詢我們需要“分而治之”,將大查詢切分成小查詢,每個查詢功能完全一樣,只完成一小部分,每次只返回一小部分查詢結果。例如,我們需要每個月運行一次下面的查詢:
mysql> DELETE FROM messages WHERE created < DATE_SUB(NON(),INTERVAL 3 MONTH);
那么可以用類似下面的辦法來完成同樣的工作:
rows_affected = o
do {
rows_affected = do_query(
"DELETE FROM messages wHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)LIMIT10000")
} while rows_affected > o
一次刪除一萬行數據一般來說是一個比較高效而且對服務器影響也最小的做法(如果是事務型引擎,很多時候小事務能夠更高效)。同時,需要注意的是,如果每次刪除數據后,都暫停一會兒再做下一次刪除,這樣也可以將服務器上原本一次性的壓力分散到一個很長的時間段中,就可以大大降低對服務器的影響,還可以大大減少刪除時鎖的持有時間。
3.分解關聯查詢
四.查詢執行的基礎
當希望MySQL能夠以更高的性能運行查詢時,最好的辦法就是弄清楚MySQL是如何優化和執行查詢的。一旦理解這一點,很多查詢優化工作實際上就是遵循一些原則讓優化器能夠按照預想的合理的方式運行。據圖6-1,我們可以看到當向MySQL 發送一個請求的時候,MySQL 到底做了些什么:
1.MySQL客戶端/服務端通信協議
一般來說,不需要去理解MySQL通信協議的內部實現細節,只需要大致理解通信協議是如何工作的。MySQL客戶端和服務器之間的通信協議是“半雙工”的,這意味着,在任何一個時刻,要么是由服務器向客戶端發送數據,要么是由客戶端向服務器發送數據,這兩個動作不能同時發生。所以,我們無法也無須將一個消息切成小塊獨立來發送。
查詢狀態
對於一個MySQL連接,或者說一個線程,任何時刻都有一個狀態,該狀態表示了MySQL當前正在做什么。有很多種方式能查看當前的狀態,最簡單的是使用SHOW FULLPROCESSLIST命令(該命令返回結果中的Command列就表示當前的狀態)。在一個查詢的生命周期中,狀態會變化很多次。MySQL官方手冊中對這些狀態值的含義有最權威的解釋,下面將這些狀態列出來,並做一個簡單的解釋。
sleep:線程正在等待客戶端發送新的請求。
Query:線程正在執行查詢或者正在將結果發送給客戶端。
Locked:在MySQL服務器層,該線程正在等待表鎖。在存儲引擎級別實現的鎖,例如InnoDB的行鎖,並不會體現在線程狀態中。對於MyISAM來說這是一個比較典型的狀態,但在其他沒有行鎖的引擎中也經常會出現。
Analyzing and statistics:線程正在收集存儲引擎的統計信息,並生成查詢的執行計划。
Copying to tmp table [on disk]:線程正在執行查詢,並且將其結果集都復制到一個臨時表中,這種狀態一般要么是在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。如果這個狀態后面還有“on disk”標記,那表示MySQL正在將一個內存臨時表放到磁盤上。
Sorting result:線程正在對結果集進行排序。
Sending data:這表示多種情況:線程可能在多個狀態之間傳送數據,或者在生成結果集,或者在向客戶端返回數據。
2.查詢緩存
在解析一個查詢語句之前,如果查詢緩存是打開的,那么MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同,那也不會匹配緩存結果,這種情況下查詢就會進入下階段的處理。如果當前的查詢恰好命中了查詢緩存,那么在返回查詢結果之前MySQL會檢查一次用戶權限。這仍然是無須解析查詢SQL語句的,因為在查詢緩存中已經存放了當前查詢需要訪問的表信息。如果權限沒有問題,MySQL會跳過所有其他階段,直接從緩存中拿到結果並返回給客戶端。這種情況下﹐查詢不會被解析,不用生成執行計划,不會被執行。
3.查詢優化處理
查詢的生命周期的下一步是將一個SQL轉換成一個執行計划,MySQL再依照這個執行計划和存儲引擎進行交互。這包括多個子階段:解析SQL、預處理、優化SQL執行計划。這個過程中任何錯誤(例如語法錯誤)都可能終止查詢。
語法解析器和預處理:
首先,MySQL通過關鍵字將SQL語句進行解析,並生成一棵對應的“解析樹”。MySQL解析器將使用MySQL語法規則驗證和解析查詢。例如,它將驗證是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能前后正確匹配。預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這里將檢查數據表和數據列是否存在,還會解析名字和別名,看看它們是否有歧義。下一步預處理器會驗證權限。這通常很快,除非服務器上有非常多的權限配置。
查詢優化器:
現在語法樹被認為是合法的了,並且由優化器將其轉化成執行計划。一條查詢可以有很多種執行方式,最后都返回相同的結果。優化器的作用就是找到這其中最好的執行計划。
數據和索引的統計信息:
MySQL架構由多個層次組成。在服務器層有查詢優化器,卻沒有保存數據和索引的統計信息。統計信息由存儲引擎實現,不同的存儲引擎可能會存儲不同的統計信息(也可以按照不同的格式存儲統計信息)。某些引擎,例如Archive引擎,則根本就沒有存儲任何統計信息!因為服務器層沒有任何統計信息,所以MySQL查詢優化器在生成查詢的執行計划時,需要向存儲引擎獲取相應的統計信息。存儲引擎則提供給優化器對應的統計信息,巴怕:每個表或者索引有多少個頁面、每個表的每個索引的基數是多少、數據行和索5長度、索引的分布信息等。優化器根據這些信息來選擇一個最優的執行計划。在后面的小節中我們將看到統計信息是如何影響優化器的。
MySQL如何執行關聯查詢:
在MySQL中,每一個查詢,每一個片段都可能是關聯。
執行計划:
關聯查詢優化器:
排序優化:
4.查詢執行引擎
存儲引擎接口有着非常豐富的功能,但是底層接口卻只有幾十個,這些接口像“搭積木”一樣能夠完成查詢的大部分操作。例如,有一個查詢某個索引的第一行的接口,再有一個查詢某個索引條目的下一個條目的功能,有了這兩個功能我們就可以完成全索引掃描的操作了。這種簡單的接口模式,讓MySQL 的存儲引擎插件式架構成為可能,但是正如前面的討論,也給優化器帶來了一定的限制。
5.返回結果給客戶端
五.MySQL查詢優化器的局限性
1.關聯子查詢
MySQL的子查詢實現得非常糟糕。最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。例如,我們希望找到Sakila數據庫中,演員Penelope Guiness(他的actor_id為1)參演過的所有影片信息。很自然的,我們會按照下面的方式用子查詢實現:
MySQL會將查詢改寫成下面的樣子:
根據EXPLAIN的輸出我們可以看到,MySQL先選擇對file表進行全表掃描,然后根據返回的film_id逐個執行子查詢。如果是一個很小的表,這個查詢糟糕的性能可能還不會引起注意,但是如果外層的表是一個非常大的表,那么這個查詢的性能會非常糟糕。當然我們很容易用下面的辦法來重寫這個查詢:
如何用好關聯子查詢:
並不是所有關聯子查詢的性能都會很差。如果有人跟你說:“別用關聯子查詢”,那么不要理他。先測試,然后做出自己的判斷。
2.UNION的限制
如果希望UNION的各個子句能夠根據LIMIT只取部分結果集,或者希望能夠先排好序再合並結果集的話,就需要在UNION的各個子句中分別使用這些子句。例如,想將兩個子查詢結果聯合起來,然后再取前20條記錄,那么MySQL會將兩個表都存放到同一個臨時表中,然后再取出前20行記錄:
(SELECT first_name,last_nameFROM sakila.actorORDER BY last_name)
UNION ALL
(SELECT first_name,last_nameFROM sakila.customerORDER BY 1ast_name)LIMIT 20;
這條查詢將會把 actor中的200條記錄和customer表中的599條記錄存放在一個臨時表中,然后再從臨時表中取出前20條。可以通過在UNION 的兩個子查詢中分別加上一個LIMIT20來減少臨時表中的數據
3.索引合並優化
4.等值傳遞
5.並發執行
6.哈希關聯
7.松散索引掃描
8.最大值和最小值優化
9.在同一個表上查詢和更新
MySQL不允許對同一張表同時進行查詢和更新
六.查詢優化器的提示
如果對優化器選擇的執行計划不滿意,可以使用優化器提供的幾個提示(hint)來控制最終的執行計划。
七.優化特定類型的查詢
1.優化COUNT()查詢
COUNT()的作用:COUNT()是一個特殊的函數,有兩種非常不同的作用:它可以統計某個列值的數量,也可以統計行數。在統計列值時要求列值是非空的(不統計NULL)。如果在COUNT()的括號中指定了列或者列的表達式,則統計的就是這個表達式有值的結果數。因為很多人對NULL理解有問題,所以這里很容易產生誤解。如果想了解更多關於SQL語句中NULL的含義,建議閱讀一些關於SQL語句基礎的書籍。(關於這個話題,互聯網上的一些信息是不夠精確的。)COUNT()的另一個作用是統計結果集的行數。當MySQL確認括號內的表達式值不可能為空時,實際上就是在統計行數。最簡單的就是當我們使用COUNT(*)的時候,這種情況下通配符*並不會像我們猜想的那樣擴展成所有的列,實際上,它會忽略所有的列而直接統計所有的行數。我們發現一個最常見的錯誤就是,在括號內指定了一個列卻希望統計結果集的行數。如果希望知道的是結果集的行數,最好使用COUNT(*),這樣寫意義清晰,性能也會很好。
關於MyISAM的神話:一個容易產生的誤解就是:MyISAM的COUNT()函數總是非常快,不過這是有前提條件的,即只有沒有任何MHERE條件的COUNT(*)才非常快,因為此時無須實際地去計算表的行數。
簡單的優化:
使用近似值:有時候某些業務場景並不要求完全精確的COUNT值,此時可以用近似值來代替。EXPLAIN出來的優化器估算的行數就是一個不錯的近似值,執行EXPLAIN並不需要真正地去執行查詢,所以成本很低。
更復雜的優化:通常來說,COUNT()都需要掃描大量的行(意味着要訪問大量數據)才能獲得精確的結果,因此是很難優化的。
2.優化關聯查詢
這里需要特別提到的是:
(1)確保ON或者USING子句中的列上有索引。在創建索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候,如果優化器的關聯順序是B、A,那么就不需要在B表的對應列上建上索引。沒有用到的索引只會帶來額外的負擔。一般來說,除非有其他理由,否則只需要在關聯順序中的第二個表的相應列上創建索引。
(2)確保任何的GROUP BY和 ORDER BY中的表達式只涉及到一個表中的列,這樣MySQL才有可能使用索引來優化這個過程。
(3)當升級MySQL 的時候需要注意:關聯語法、運算符優先級等其他可能會發生變化的地方。因為以前是普通關聯的地方可能會變成笛卡兒積,不同類型的關聯可能會生成不同的結果等。
3.優化子查詢
關於子查詢優化我們給出的最重要的優化建議就是盡可能使用關聯查詢代替。
4.優化GROUP BY和DISTINCT
5.優化LIMIT分頁
一個非常常見又令人頭疼的問題就是,在偏移量非常大的時候,例如可能是LIMIT1000,20這樣的查詢,這時MySQL需要查詢10 020條記錄然后只返回最后20條,前面10 000條記錄都將被拋棄,這樣的代價非常高。考慮下面的查詢:
如果這個表非常大,那么這個查詢最好改寫成下面的樣子:
6.優化SQL_CALC_FOUND_ROWS
7.優化UNION查詢
MySQL總是通過創建並填充臨時表的方式來執行UNION查詢。因此很多優化策略在UNION查詢中都沒法很好地使用。經常需要手工地將MHERE、LIMIT、ORDER BY等子句“下推”到UNION的各個子查詢中,以便優化器可以充分利用這些條件進行優化(例如,直接將這些子句冗余地寫一份到各個子查詢)。除非確實需要服務器消除重復的行,否則就一定要使用UNION ALL,這一點很重要。如果沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會導致對整個臨時我的數據做唯一性檢查。這樣做的代價非常高。即使有ALL關鍵字,MySQL仍然會使用臨時表存儲結果。事實上,MySQL總是將結果放入臨時表,然后再讀出,再返回給客戶端。雖然很多時候這樣做是沒有必要的(例如,MySQL可以直接把這些結果返回給客戶端)。
8.使用用戶自定義變量
用戶自定義變量是一個用來存儲內容的臨時容器,在連接MySQL的整個過程中都存在。可以使用下面的SET和SELECT語句來定義它們:
然后可以在任何可以使用表達式的地方使用這些自定義變量: