MySQL在執行查詢的時候有哪些子任務,哪些子任務運行的速度很慢?這裏很難給出完整的列表。一般來講,查詢的生命週期大體能夠按照順序來看:從客戶端,到服務器,而後在服務器上進行解析,生成執行計劃,執行,並返回結果給客戶端。其中「執行」能夠認爲是整個生命週期中最重要的階段,這其中包括了大量爲了檢索數據到存儲引擎的調用以及調用後的數據處理,包括排序、分組等。mysql
查詢性能低下最基本的緣由是訪問的數據太多。某些查詢可能不可避免地須要篩選大量數據,但這並不常見。大部分性能低下的查詢均可以經過減小訪問的數據量的方式進行優化。對於低效的查詢,咱們發現經過下面兩個步驟來分析老是頗有效:sql
(1).確認應用程序是否在檢索大量超過須要的數據。這一般意味着訪問了太多的行,但有時候也多是訪問了太多的列。數據庫
(2).確認MySQL服務器層是否在分析大量超過須要的數據行。緩存
有些查詢會請求超過實際須要的數據,而後這些多餘的數據會被應用程序丟棄。這會給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,那麼用戶屢次評論的時候,可能就會反覆查詢這個數據。比較好的方案是,當初次查詢的時候將這個數據緩存起來,須要的時候從緩存中取出,這樣性能顯然會更好。
對於MySQL,最簡單的衡量查詢開銷的三個指標包含:響應時間、掃描的行數、返回的行數。
響應時間:響應時間是兩個部分之和:服務時間和排隊時間。服務時間是指數據庫處理這個查詢真正花了多長時間。排隊時間是指服務器由於等待某些資源而沒有真正執行查詢的時間一-可 能是等I/O操做完成,也多是等待行鎖,等等。遺憾的是,咱們沒法把響應時間細分到上面這些部分,除非有什麼辦法可以逐個測量上面這些消耗,不過很難作到。通常最多見和重要的等待是I/O和鎖等待,可是實際狀況更加複雜。因此在不一樣類型的應用壓力下,響應時間並無什麼-致的規律或者公式。諸如存儲引擎的鎖(表鎖、行鎖)、高併發資源競爭、硬件響應等諸多因素都會影響響應時間。因此,響應時間既多是一個問題的結果也多是-一個問題的緣由,不一樣案例狀況不一樣。
掃描的行數和返回的行數:
掃描的行數和訪問類型:
在評估查詢開銷的時候,須要考慮- -下從表中找到某一行數據的成本。MySQL有好幾種訪問方式能夠查找並返回一行結果。有些訪問方式可能須要掃描不少行才能返回一行結果,也有些訪問方式可能無須掃描就能返回結果。在EXPLAIN語句中的type列反應了訪問類型。訪問類型有不少種,從全表掃描到索引掃描、範圍掃描、惟一索引查詢、常數引用等。這裏列的這些,速度是從慢到快,掃描的行數也是從小到大。你不須要記住這些訪問類型,但須要明白掃描表、掃描索引、範圍訪問和單值訪問的概念。若是查詢沒有辦法找到合適的訪問類型,那麼解決的最好辦法一般就是增長一一個合適的索引,這也正是咱們前--章討論過的問題。如今應該明白爲何索引對於查詢優化如此重要了。索引讓MySQL以最高效、掃描行數最少的方式找到須要的記錄。
在優化有問題的查詢時,目標應該是找到一一個更優的方法得到實際須要的結果一而不必定老是須要從MySQL獲取- -模同樣的結果集。有時候,能夠將查詢轉換一種寫 法讓其返回同樣的結果,可是性能更好。但也能夠經過修改應用代碼,用另外一種方式完成查詢, 最終達到同樣的目的。
設計查詢的時候-一個須要考慮的重要問題是,是否須要將-一個複雜的查詢分紅多個簡單的查詢。MySQL從設計上讓鏈接和斷開鏈接都很輕量級,在返回一個小的查詢結果方面很高效。現代的網絡速度比之前要快不少,不管是帶寬仍是延遲。在某些版本的MySQL上,即便在-一個通用服務器上,也可以運行每秒超過10萬的查詢,即便是一個千兆網卡也能輕鬆知足每秒超過2000次的查詢。因此運行多個小查詢如今已經不是大問題了。
有時候對於一個大查詢咱們須要「分而治之」,將大查詢切分紅小查詢,每一個查詢功能徹底同樣,只完成一小部分,每次只返回一小部分查詢結果。例如,咱們須要每月運行一次下面的查詢:
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
一次刪除一萬行數據通常來講是一個比較高效並且對服務器影響也最小的作法(若是是事務型引擎,不少時候小事務可以更高效)。同時,須要注意的是,若是每次刪除數據後,都暫停一下子再作下一次刪除,這樣也能夠將服務器上本來一次性的壓力分散到一個很長的時間段中,就能夠大大下降對服務器的影響,還能夠大大減小刪除時鎖的持有時間。
當但願MySQL可以以更高的性能運行查詢時,最好的辦法就是弄清楚MySQL是如何優化和執行查詢的。一旦理解這一點,不少查詢優化工做實際上就是遵循一些原則讓優化器可以按照預想的合理的方式運行。據圖6-1,咱們能夠看到當向MySQL 發送一個請求的時候,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:這表示多種狀況:線程可能在多個狀態之間傳送數據,或者在生成結果集,或者在向客戶端返回數據。
在解析一個查詢語句以前,若是查詢緩存是打開的,那麼MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是經過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即便只有一個字節不一樣,那也不會匹配緩存結果,這種狀況下查詢就會進入下階段的處理。若是當前的查詢剛好命中了查詢緩存,那麼在返回查詢結果以前MySQL會檢查一次用戶權限。這仍然是無須解析查詢SQL語句的,由於在查詢緩存中已經存放了當前查詢須要訪問的表信息。若是權限沒有問題,MySQL會跳過全部其餘階段,直接從緩存中拿到結果並返回給客戶端。這種狀況下﹐查詢不會被解析,不用生成執行計劃,不會被執行。
查詢的生命週期的下一步是將一個SQL轉換成一個執行計劃,MySQL再依照這個執行計劃和存儲引擎進行交互。這包括多個子階段:解析SQL、預處理、優化SQL執行計劃。這個過程當中任何錯誤(例如語法錯誤)均可能終止查詢。
語法解析器和預處理:
首先,MySQL經過關鍵字將SQL語句進行解析,並生成一棵對應的「解析樹」。MySQL解析器將使用MySQL語法規則驗證和解析查詢。例如,它將驗證是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能先後正確匹配。預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這裏將檢查數據表和數據列是否存在,還會解析名字和別名,看看它們是否有歧義。下一步預處理器會驗證權限。這一般很快,除非服務器上有很是多的權限配置。
查詢優化器:
如今語法樹被認爲是合法的了,而且由優化器將其轉化成執行計劃。一條查詢能夠有不少種執行方式,最後都返回相同的結果。優化器的做用就是找到這其中最好的執行計劃。
數據和索引的統計信息:
MySQL架構由多個層次組成。在服務器層有查詢優化器,卻沒有保存數據和索引的統計信息。統計信息由存儲引擎實現,不一樣的存儲引擎可能會存儲不一樣的統計信息(也能夠按照不一樣的格式存儲統計信息)。某些引擎,例如Archive引擎,則根本就沒有存儲任何統計信息!由於服務器層沒有任何統計信息,因此MySQL查詢優化器在生成查詢的執行計劃時,須要向存儲引擎獲取相應的統計信息。存儲引擎則提供給優化器對應的統計信息,巴怕:每一個表或者索引有多少個頁面、每一個表的每一個索引的基數是多少、數據行和索5長度、索引的分佈信息等。優化器根據這些信息來選擇一個最優的執行計劃。在後面的小節中咱們將看到統計信息是如何影響優化器的。
MySQL如何執行關聯查詢:
在MySQL中,每個查詢,每個片斷均可能是關聯。
執行計劃:
關聯查詢優化器:
排序優化:
存儲引擎接口有着很是豐富的功能,可是底層接口卻只有幾十個,這些接口像「搭積木」同樣可以完成查詢的大部分操做。例如,有一個查詢某個索引的第一行的接口,再有一個查詢某個索引條目的下一個條目的功能,有了這兩個功能咱們就能夠完成全索引掃描的操做了。這種簡單的接口模式,讓MySQL 的存儲引擎插件式架構成爲可能,可是正如前面的討論,也給優化器帶來了必定的限制。
MySQL的子查詢實現得很是糟糕。最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。例如,咱們但願找到Sakila數據庫中,演員Penelope Guiness(他的actor_id爲1)參演過的全部影片信息。很天然的,咱們會按照下面的方式用子查詢實現:
MySQL會將查詢改寫成下面的樣子:
根據EXPLAIN的輸出咱們能夠看到,MySQL先選擇對file表進行全表掃描,而後根據返回的film_id逐個執行子查詢。若是是一個很小的表,這個查詢糟糕的性能可能還不會引發注意,可是若是外層的表是一個很是大的表,那麼這個查詢的性能會很是糟糕。固然咱們很容易用下面的辦法來重寫這個查詢:
如何用好關聯子查詢:
並非全部關聯子查詢的性能都會不好。若是有人跟你說:「別用關聯子查詢」,那麼不要理他。先測試,而後作出本身的判斷。
若是但願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來減小臨時表中的數據
MySQL不容許對同一張表同時進行查詢和更新
若是對優化器選擇的執行計劃不滿意,可使用優化器提供的幾個提示(hint)來控制最終的執行計劃。
COUNT()的做用:COUNT()是一個特殊的函數,有兩種很是不一樣的做用:它能夠統計某個列值的數量,也能夠統計行數。在統計列值時要求列值是非空的(不統計NULL)。若是在COUNT()的括號中指定了列或者列的表達式,則統計的就是這個表達式有值的結果數。由於不少人對NULL理解有問題,因此這裏很容易產生誤解。若是想了解更多關於SQL語句中NULL的含義,建議閱讀一些關於SQL語句基礎的書籍。(關於這個話題,互聯網上的一些信息是不夠精確的。)COUNT()的另外一個做用是統計結果集的行數。當MySQL確認括號內的表達式值不可能爲空時,實際上就是在統計行數。最簡單的就是當咱們使用COUNT(*)的時候,這種狀況下通配符*並不會像咱們猜測的那樣擴展成全部的列,實際上,它會忽略全部的列而直接統計全部的行數。咱們發現一個最多見的錯誤就是,在括號內指定了一個列卻但願統計結果集的行數。若是但願知道的是結果集的行數,最好使用COUNT(*),這樣寫意義清晰,性能也會很好。
關於MyISAM的神話:一個容易產生的誤解就是:MyISAM的COUNT()函數老是很是快,不過這是有前提條件的,即只有沒有任何MHERE條件的COUNT(*)才很是快,由於此時無須實際地去計算表的行數。
簡單的優化:
使用近似值:有時候某些業務場景並不要求徹底精確的COUNT值,此時能夠用近似值來代替。EXPLAIN出來的優化器估算的行數就是一個不錯的近似值,執行EXPLAIN並不須要真正地去執行查詢,因此成本很低。
更復雜的優化:一般來講,COUNT()都須要掃描大量的行(意味着要訪問大量數據)才能得到精確的結果,所以是很難優化的。
這裏須要特別提到的是:
(1)確保ON或者USING子句中的列上有索引。在建立索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候,若是優化器的關聯順序是B、A,那麼就不須要在B表的對應列上建上索引。沒有用到的索引只會帶來額外的負擔。通常來講,除非有其餘理由,不然只須要在關聯順序中的第二個表的相應列上建立索引。
(2)確保任何的GROUP BY和 ORDER BY中的表達式只涉及到一個表中的列,這樣MySQL纔有可能使用索引來優化這個過程。
(3)當升級MySQL 的時候須要注意:關聯語法、運算符優先級等其餘可能會發生變化的地方。由於之前是普通關聯的地方可能會變成笛卡兒積,不一樣類型的關聯可能會生成不一樣的結果等。
關於子查詢優化咱們給出的最重要的優化建議就是儘量使用關聯查詢代替。
一個很是常見又使人頭疼的問題就是,在偏移量很是大的時候,例如多是LIMIT1000,20這樣的查詢,這時MySQL須要查詢10 020條記錄而後只返回最後20條,前面10 000條記錄都將被拋棄,這樣的代價很是高。考慮下面的查詢:
若是這個表很是大,那麼這個查詢最好改寫成下面的樣子:
MySQL老是經過建立並填充臨時表的方式來執行UNION查詢。所以不少優化策略在UNION查詢中都無法很好地使用。常常須要手工地將MHERE、LIMIT、ORDER BY等子句「下推」到UNION的各個子查詢中,以便優化器能夠充分利用這些條件進行優化(例如,直接將這些子句冗餘地寫一份到各個子查詢)。除非確實須要服務器消除重複的行,不然就必定要使用UNION ALL,這一點很重要。若是沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會致使對整個臨時個人數據作惟一性檢查。這樣作的代價很是高。即便有ALL關鍵字,MySQL仍然會使用臨時表存儲結果。事實上,MySQL老是將結果放入臨時表,而後再讀出,再返回給客戶端。雖然不少時候這樣作是沒有必要的(例如,MySQL能夠直接把這些結果返回給客戶端)。
用戶自定義變量是一個用來存儲內容的臨時容器,在鏈接MySQL的整個過程當中都存在。可使用下面的SET和SELECT語句來定義它們:
而後能夠在任何可使用表達式的地方使用這些自定義變量: