????????前面的章節我們介紹了如何設計最優的庫表結構、 如何建立最好的索引, 這些對于高性能來說是必不可少的。 但這些還不夠一一還需要合理的設計查詢。 如果查詢寫得很糟糕,即使庫表結構再合理、 索引再合適, 也無法實現高性能。
????????查詢優化、 索引優化、 庫表結構優化需要齊頭井進, 一個不落。 在獲得編寫MySQL查 詢的經驗的同時, 也將學習到如何為高效的查詢設計表和索引。 同樣的, 也可以學習到在優化庫表結構時會影響到哪些類型的查詢。 這個過程需要時間, 所以建議大家在學習后面章節的時候多回頭看看這三章的內容。
6.1為什么查詢速度會慢
????????真正重要是響應時間.如果把查詢看作是一個任務, 那么它由一系列子任務組成, 每個子任務都會消耗一定的時間。 如果要優化查詢, 實際上要優化其子任務, 要么消除其中一些子任務, 要么減少子任務的執行次數,要么讓子任務運行得更快.
????????在每一個消耗大量時間的查詢案例中, 我們都能看到一些不必要的額外操作、 某些操作被額外地重復了很多次、 某些操作執行得太慢等。優化查詢的目的就是減少和消除這些操作所花費的時間。
6.2慢查詢基礎:優化數據訪問
????????查詢性能低下最基本的原因是訪問的數據太多。某些查詢可能不可避免地需要篩選大量 數據, 但這井不常見。大部分性能低下的查詢都可以通過減少訪問的數據量的方式進行 優化。對于低效的查詢, 我們發現通過下面兩個步驟來分析總是很有效:
1. 確認應用程序是否在檢索大量超過需要的數據。這通常意味著訪問了太多的行, 但有時候也可能是訪問了太多的列。
2. 確認MySQL服務器層是否在分析大量超過需要的數據行。
6.2.1是否向數據庫請求了不需要的數據
????????有些查詢會請求超過實際需要的數據, 然后這些多余的數據會被應用程序丟棄。這會給MySQL服務器帶來額外的負擔, 并增加網絡開銷。另外也會消耗應用服務器的CPU 和內存資源。
6.2.2MySQL是否在掃描額外的記錄
????????在確定查詢只返回需要的數據以后,接下來應該看看查詢為了返回結果是否掃描了過多 的數據。對于MySQL, 最簡單的衡量查詢開銷的三個指標如下:
? 響應時間
? 掃描的行數
? 返回的行數
? ??????沒有哪個指標能夠完美地衡量查詢的開銷,但它們大致反映了MySQL在內部執行查詢時需要訪問多少數據,并可以大概推算出查詢運行的時間。 這三個指標都會記錄到MySQL的慢日志中,所以檢查慢日志記錄是找出掃描行數過多的查詢的好辦法。
響應時間
????????要記住,響應時間只是一個表面上的值。響應時間是兩個部分之和:服務時間和排隊時間。 服務時間是指數據庫處理這個查詢真正花了多長時間。 排隊時間是指服務器因為等待某些資源而沒有真正執行查詢的時間——可能是等I/O操作完成,也可能是等待行鎖,等等。遺憾的是,我們無法把響應 時間細分到上面這些部分,除非有什么辦法能夠逐個測量上面這些消耗,不過很難做到。 一般最常見和重要的等待是I/O和鎖等待,但是實際情況更加復雜。
掃描的行數和返回的行數
????????分析查詢時,查看該查詢掃描的行數是非常有幫助的。 這在一定程度上能夠說明該查詢找到需要的數據的效率高不高。
????????對于找出那些 “糟糕” 的查詢,這個指標可能還不夠完美, 因為并不是所有的行的訪問代價都是相同的。 較短的行的訪問速度更快, 內存中的行也比磁盤中的行的訪問速度要快得多。
????????理想情況下掃描的行數和返回的行數應該是相同的。 但實際情況中這種 “美事” 井不多。例如在做一個關聯查詢時,服務器必須要掃描多行才能生成結果集中的一行。 掃描的行數對返回的行數的比率通常很小,一般在1:1和10:1之間,不過有時候這個值也可能非常非常大。
掃描的行數和訪問類型
????????在評估查詢開銷的時候,需要考慮一下從表中找到某一行數據的成本。 MySQL有好兒 種訪問方式可以查找井返回一行結果。 有些訪問方式可能需要掃描很多行才能返回一行結果,也有些訪問方式可能無須掃描就能返回結果。
????????在EXPLAIN語句中的type列反應了訪問類型。 訪問類型有很多種,從全表掃描到索引掃描、 范圍掃描、 唯一索引查詢、 常數引用等。 這里列的這些,速度是從慢到快,掃描的 行數也是從小到大。 你不需要記住這些訪問類型,但需要明白掃描表、 掃描索引、 范圍訪問和單值訪問的概念。
MySQL能夠使用如下三種方式應用WHERE條件, 從好到壞依次為 :
? 在索引中使用WHERE條件來過濾不匹配的記錄。 這是在存儲引擎層完成的。
? 使用索引覆蓋掃描(在Extra列中出現了Using index) 來返回記錄, 直接從索引中過濾不需要的記錄并返回命中的結果。 這是在MySQL服務器層完成的, 但無須再回表查詢記錄。
? 從數據表中返回數據,然后過濾不滿足條件的記錄(在Extra列中出現Using Where)。這在MySQL服務器層完成,MySQL需要先從數據表讀出記錄然后過濾。
????????如果發現查詢需要掃描大量的數據但只返回少數的行, 那么通??梢試L試下面的技巧去優化它:
? 使用索引覆蓋掃描, 把所有需要用的列都放到索引中, 這樣存儲引擎無須回表獲取對應行就可以返回結果了(在前面的章節中我們已經討論過了)。
? 改變庫表結構。例如使用單獨的匯總表(這是我們在第4章中討論的辦法)。
? 重寫這個復雜的查詢, 讓MySQL優化器能夠以更優化的方式執行這個查詢(這是本章后續需要討論的問題)。
6.3重構查詢的方式
6.3.1一個復雜查詢還是多個簡單查詢
????????設計查詢的時候一個需要考慮的重要問題是, 是否需要將一個復雜的查詢分成多個簡單的查詢。在傳統實現中, 總是強調需要數據庫層完成盡可能多的工作, 這樣做的邏輯在于以前總是認為網絡通信、 查詢解析和優化是一件代價很高的事情。
? ??????但是這樣的想法對于MySQL并不適用,MySQL從設計上讓連接和斷開連接都很輕量級,在返回一個小的查詢結果方面很高效。 現代的網絡速度比以前要快很多, 無論是帶寬還是延遲。 在某些版本的MySQL上, 即使在一個通用服務器上, 也能夠運行每秒超過10萬的查詢, 即使是一個千兆網卡也能輕松滿足每秒超過2000次的查詢。 所以運行多個小查詢現在已經不是大問題了。
????????MySQL內部每秒能夠掃描內存中上百萬行數據, 相比之下, MySQL響應數據給客戶端就慢得多了。在其他條件都相同的時候,使用盡可能少的查詢當然是更好的。但是有時候,將一個大查詢分解為多個小查詢是很有必要的。 別害怕這樣做, 好好衡量一下這樣做是 不是會減少工作量。 稍后我們將通過本章的一個示例來展示這個技巧的優勢。
????????不過, 在應用設計的時候, 如果一個查詢能夠勝任時還寫成多個獨立查詢是不明智的。例如, 我們看到有些應用對一個數據表做10次獨立的查詢來返回10行數據, 每個查詢返回一條結果, 查詢10次!
6.3.2切分查詢
????????有時候對于一個大查詢我們需要 “分而治之”,將大查詢切分成小查詢, 每個查詢功能完全一樣, 只完成一小部分, 每次只返回一小部分查詢結果。
6.3.3分解關聯查詢
????????很多高性能的應用都會對關聯查詢進行分解。簡單地, 可以對每一個表進行一次單表查詢, 然后將結果在應用程序中進行關聯。
? ??????到底為什么要這樣做?乍一看, 這樣做并沒有什么好處, 原本一條查詢, 這里卻變成多 條查詢, 返回的結果又是一模一樣的。 事實上, 用分解關聯查詢的方式重構查詢有如下 的優勢:
? 讓緩存的效率更高。 許多應用程序可以方便地緩存單表查詢對應的結果對象。 另外, 對MySQL的查詢緩存來說 , 如果關聯中的某個表發生了變化,那么就無法使用查詢緩存了, 而拆分后, 如果某個表很少改變, 那么基于該表的查詢就可以重復利用查詢緩存結果了。
? 將查詢分解后,執行單個查詢可以減少鎖的競爭。
? 在應用層做關聯, 可以更容易對數據庫進行拆分, 更容易做到高性能和可擴展。
? 查詢本身效率也可能會有所提升。 這個例子中, 使用IN()代替關聯查詢, 可以讓MySQL按照ID順序進行查詢, 這可能比隨機的關聯要更高效。 我們后續將詳細介紹這點。
? 可以減少冗余記錄的查詢。 在應用層做關聯查詢, 意味著對千某條記錄應用只需要 查詢一次,而在數據庫中做關聯查詢,則可能需要重復地訪問一部分數據。 從這點看, 這樣的重構還可能會減少網絡和內存的消耗。
? 更進一步, 這樣做相當于在應用中實現了哈希關聯, 而不是使用MySQL的嵌套循環關聯。 某些場景哈希關聯的效率要高很多(本章后續我們將討論這點)。
6.4查詢執行的基礎
????????當希望MySQL能夠以更高的性能運行查詢時, 最好的辦法就是弄清楚MySQL是如何一點, 很多查詢優化工作實際上就是遵循一些原則讓優優化和執行查詢的。 一且理解這一點,很多查詢優化工作實際上就是遵循一些原則讓優化器能夠按照預想的合理的方式運行。
1. 客戶端發送一條查詢給服務器。
2. 服務器先檢查查詢緩存, 如果命中了緩存, 則立刻返回存儲在緩存中的結果。 否則進入下一階段。
3. 服務器端進行SQL解析、 預處理, 再由優化器生成對應的執行計劃。
4. MySQL根據優化器生成的執行計劃, 調用存儲引擎的API來執行查詢。
5. 將結果返回給客戶端。
6.4.1 MySQL客戶端/服務器通信協議
????????一般來說,不需要去理解MySQL通信協議的內部實現細節,只需要大致理解通信協議是如何工作的。 MySQL客戶端和服務器之間的通信協議是 “半雙工 ” 的, 這意味著,在任何一個時刻, 要么是由服務器向客戶端發送數據, 要么是由客戶端向服務器發送數據, 這兩個動作不能同時發生。 所以, 我們無法也無須將一個消息切成小塊獨立來 發送。
????????這種協議讓MySQL通信簡單快速, 但是也從很多地方限制了MySQL。 一個明顯的限制 是, 這意味著沒法進行流量控制。 一且一端開始發生消息, 另一端要接收完整個消息才能響應它。 這就像來回拋球的游戲:在任何時刻, 只有一個人能控制球, 而且只有控制球的人才能將球拋回去(發送消息)。
查詢狀態
????????對于一 個MySQL 連接,或者說一個線程,任何時刻都有一 個狀態,該狀態表示了 MySQL當前正在做什么。有很多種方式能查看當前的狀態,最簡單的是使用SHOW FULL PROCESS LIST命令(該命令返回 結果中的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
這表示多種情況:線程可能在多個狀態之間傳送數據, 或者在生成結果集, 或者在向客戶端返回數據。
6.4.2查詢緩存
????????在解析一個查詢語句之前, 如果查詢緩存是打開的, 那么MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。 這個檢查是通過一個對大小寫敏感的哈希查找實現的。 查詢和緩存中的查詢即使只有一個字節不同, 那也不會匹配緩存結果注 11' 這種情況下查詢就會進入下一階段的處理。
????????如果當前的查詢恰好命中了查詢緩存, 那么在返回查詢結果之前MySQL會檢查一次用戶權限。 這仍然是無須解析查詢SQL語句的, 因為在查詢緩存中已經存放了 當前查詢需要訪問的表信息。 如果權限沒有問題, MySQL會跳過所有 其他階段, 直接從緩存中拿到結果并返回給客戶端。 這種情況下,查詢不會被解析,不用生成執行計劃,不會被執行。在第7章中的查詢緩存一節, 你將學習到更多細節。
6.4.3查詢優化處理
????????查詢的生命周期的下一步是將一個SQL轉換成一個執行計劃, MySQL再依照這個執行計劃和存儲引擎進行交互。 這包括多個子階段:解析SQL、 預處理 、 優化SQL執行計劃。這個過程中任何錯誤(例如語法錯誤)都可能終止查詢。 這里不打算詳細介紹MySQL內部實現, 而只是選擇性地介紹其中幾個獨立的部分, 在實際執行中, 這幾部分可能一起執行也可能單獨執行。 我們的目的是幫助大家理解MySQL如何執行查詢, 以便寫出更優秀的查詢。
語法解析器和預處理
????????首先, MySQL通過關鍵字將SQL語句進行解析, 并生成 一棵對應的 “ 解析樹 ” 。 MySQL解析器將使用MySQL語法規則驗證和解析查詢。 例如, 它將驗證是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能前后正確匹配。
????????預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這里將檢查數據表和數據列是否存在,還會解析名字和別名,看看它們是否有歧義。????
? ? ? ? 下一步預處理器會驗證權限。 這通常很快,除非服務器上有非常多的權限配置。
查詢優化器
????????現在語法樹被認為是合法的了,并且由優化器將其轉化成執行計劃。 一條查詢可以有很 多種執行方式,最后都返回相同的結果。 優化器的作用就是找到這其中最好的執行計劃。
????????MySQL使用基于成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,并選擇其中成本最小的一個。 最初,成本的最小單位是隨機讀取一個4K數據頁的成本,后來(成本計算公式)變得更加復雜,井且引入了一些 “因子” 來估算某些操作的代價, 如當執行一次WHERE條件比較的成本。 可以通過查詢當前會話的Last_query_cost的值來得知MySQL計算的當前查詢的成本。
????????有很多種原因會導致MySQL優化器選擇錯誤的執行計劃,如下所示:
? 統計信息不準確。 MySQL依賴存儲引擎提供的統計信息來評估成本,但是有的存儲引擎提供的信息是準確的,有的偏差可能非常大。 例如,InnoDB因為其MVCC的架構,井不能維護一個數據表的行數的精確統計信息。
? 執行計劃中的成本估算不等同于實際執行的成本。 所以即使統計信息精準,優化器給出的執行計劃也可能不是最優的。 例如有時候某個執行計劃雖然需要讀取更多的頁面,但是它的成本卻更小。因為如果這些頁面都是順序讀或者這些頁面都已經在內存中的話,那么它的訪問成本將很小。
? MySQL層面并不知道哪些頁面在內存中、哪些在磁盤上,所以查詢實際執行過程中到底需要多少次物理1/0是無法得知的。 MySQL的最優可能和你想的最優不一樣。你可能希望執行時間盡可能的短,但是MySQL只是基于其成本模型選擇最優的執行計劃,而有些時候這井不是最快的執行 方式。所以,這里我們看到根據執行成本來選擇執行計劃并不是完美的模型。
? MySQL從不考慮其他并發執行的查詢,這可能會影響到當前查詢的速度。?
? MySQL也井不是任何時候都是基于成本的優化。有時也會基千一些固定的規則,例如,如果存在全文搜索的MATCH()子句,則在存在全文索引的時候就使用全文索引。即使有時候使用別的索引和WHERE條件可以遠比這種方式要快,MySQL也仍然會使用對應的全文索引。
? MySQL不會考慮不受其控制的操作的成本,例如執行存儲過程或者用戶自定義函數的成本。
? 后面我們還會看到,優化器有時候無法去估算所有可能的執行計劃,所以它可能錯過實際上最優的執行計劃。
????????MySQL的查詢優化器是一個非常復雜的部件,它使用了很多優化策略來生成一個最優的執行計劃。優化策略可以簡單地分為兩種,一種是靜態優化,一種是動態優化。靜態優化可以直接對解析樹進行分析,井完成優化。例如,優化器可以通過一些簡單的代數變換將WHERE條件轉換成另一種等價形式。靜態優化不依賴于特別的數值,如WHERE條件中帶入的一些常數等。靜態優化在第一次完成后就一直有效,即使使用不同的參數重復執行查詢也不會發生變化??梢哉J為這是一種“ 編譯時優化”。
????????相反,動態優化則和查詢的上下文有關,也可能和很多其他因素有關,例如WHERE條件中的取值、索引中條目對應的數據行數等。這需要在每次查詢的時候都重新評估,可以認為這是“運行時優化”。
????????在執行語句和存儲過程的時候,動態優化和靜態優化的區別非常重要。MySQL對查詢的靜態優化只需要做一次,但對查詢的動態優化則在每次執行時都需要重新評估。有時候甚至在查詢的執行過程中也會重新優化。注12
下面是一些MySQL能夠處理的優化類型:
重新定義關聯表的順序
????????數據表的關聯井不總是按照在查詢中指定的順序進行。決定關聯的順序是優化器很重要的一部分功能,本章后面將深入介紹這一點。
將外連接轉化成內連接
????????并不是所有的OUTER JOIN語句都必須以外連接的方式執行。諸多因素,例如WHERE條件、庫表結構都可能會讓外連接等價于一個內連接。MySQL能夠識別這點并重寫查詢,讓其可以調整關聯順序。
使用等價變換規則
????????MySQL可以使用一些等價變換來簡化并規范表達式。它可以合并和減少一些比較,還可以移除一些恒成立和一些恒不成立的判斷。這些規則對于我們編寫條件語句很有用,我們將在本章后續繼續討論。
優化COUNT()、MIN()和MAX()
????????索引和列是否可為空通??梢詭椭鶰ySQL優化這類表達式。例如,要找到某一列的最小值,只需要查詢對應B-Tree索引最左端的記錄,MySQL可以直接獲取索引的第一行記錄。在優化器生成執行計劃的時候就可以利用這一點,在B-Tree索引中,優化器會將這個表達式作為一個常數對待。類似的,如果要查找一個最大值,也只需讀取B-Tree索引的最后一條記錄。如果MySQL使用了這種類型的優化,那么在EXPLAIN中就可以看到"Select tables optimized away"。從字面意思可以看出,它表示優化器已經從執行計劃中移除了該表,并以一個常數取而代之。類似的,沒有任何WHERE條件的COUNT(*)查詢通常也可以使用存儲引擎提供的一些優化(例如,MylSAM維護了一個變量來存放數據表的行數)。
預估并轉化為常數表達式
????????當MySQL檢測到一個表達式可以轉化為常數的時候,就會一直把該表達式作為常數進行優化處理。例如,一個用戶自定義變址在查詢中沒有發生變化時就可以轉換為一個常數。數學表達式則是另一種典型的例子。
????????讓人驚訝的是,在優化階段,有時候甚至一個查詢也能夠轉化為一個常數。一個例子是在索引列上執行MIN()函數。甚至是主鍵或者唯一鍵查找語句也可以轉換為常數表達式。如果WHERE子句中使用了該類索引的常數條件,MySQL可以在查詢開始階段就先查找到這些值,這樣優化器就能夠知道井轉換為常數表達式。
覆蓋索引掃描
????????當索引中的列包含所有查詢中需要使用的列的時候,MySQL就可以使用索引返回需要的數據, 而無須查詢對應的數據行, 在前面的章節中我們已經討論過這點了。
子查詢優化
????????MySQL在某些情況下可以將子查詢轉換一種效率更高的形式, 從而減少多個查詢多次對數據進行訪問。
提前終止查詢
????????在發現已經滿足查詢需求的時候,MySQL總是能夠立刻終止查詢。一個典型的例子就是當使用了LIMIT子句的時候。除此之外,MySQL還有幾類情況也會提前終止查詢, 例如發現了一個不成立的條件, 這時MySQL可以立刻返回一個空結果。
等值傳播
如果 兩個列的值通過等式關聯,那么MySQL能夠把其中一個列的WHERE條件傳遞到另一列上。
列表IN()的比較
????????在很多數據庫系統中,IN()完全等同于多個OR條件的子句, 因為這 兩者是完全等價的。 在MySQL中這點是不成立的,MySQL將 IN()列表中的數據先進行排序,然后通過二分查找的方式來確定列表中的值是否滿足條件,這是一個 O(log n)復雜度的操作,等價地轉換成OR查詢的復雜度為 O(n), 對千IN()列表中有大量取值的時候,MySQL的處理速度將會更快。
????????上面列舉的遠不是MySQL優化器的全部,MySQL還會做大量其他的優化,即使本章全 部用來描述也會篇幅不足,但上面的這些例子已經足以讓大家明白優化器的復雜性和智能性了。
數據和索引的統計信息
MySQL如何執行關聯查詢
????????MySQL中“關聯” 詞所包含的意義比一般意義上理解的要更廣泛。總的來說,MySQL認為任何一個查詢都是一次“關聯”—— 并不僅僅是一個查詢需要到兩個表
匹配才叫關聯,所以在MySQL中,每一個查詢,每一個片段(包括子查詢,甚至基于單表的SELECT) 都可能是關聯。
????????所以,理解MySQL如何執行關聯查詢至關重要。我們先來看一個UNION查詢的例子。對于UNION查詢,MySQL先將一系列的單個查詢結果放到一個臨時表中,然后再重新讀出臨時表數據來完成UNION查詢。在MySQL的概念中,每個查詢都是一次關聯,所以讀取結果臨時表也是一次關聯。
????????當前MySQL關聯執行的策略很簡單: MySQL對任何關聯都執行嵌套循環關聯操作,即MySQL先在一個表中循環取出單條數據,然后再嵌套循環到下一個表中尋找匹配的行,依次下去,直到找到所有表中匹配的行為止。然后根據各個表匹配的行,返回查詢中需要的各個列。MySQL會嘗試在最后一個關聯表中找到所有匹配的行,如果最后一個關聯表無法找到更多的行以后, MySQL返回到上一層次關聯表, 看是否能夠找到更多的匹配記錄, 依此類推迭代執行。?
? ??????按照這樣的方式查找第一個表記錄, 再嵌套查詢下一個關聯表, 然后回溯到上一個表,在MySQL中是通過嵌套循環的方式實現一正如其名 “嵌套循環關聯”。
執行計劃
????????和很多其他關系數據庫不同, MySQL井不會生成查詢字節碼來執行查詢。 MySQL生成 查詢的一棵指令樹, 然后通過存儲引擎執行完成這棵指令樹井返回結果。 最終的執行計劃包含了重構查詢的全部信息。 如果對某個查詢執行EXPLAIN EXTENDED后, 再執行SHOW WARNINGS, 就可以看到重構出的查詢.
關聯查詢優化器
????????MySQL優化器最重要的一部分就是關聯查詢優化, 它決定了多個表關聯時的順序。 通常多表關聯的時候, 可以有多種不同的關聯順序來獲得相同的執行結果。 關聯查詢優化器則通過評估不同順序時的成本來選擇一個代價最小的關聯順序。
排序優化
????????無論如何排序都是一個成本很高的操作,所以從性能角度考慮,應盡可能避免排序或者盡可能避免對大量數據進行排序。
? ??????如果需要排序的數據量小于 “排序緩沖區", MySQL使用內存進行 “快速排序” 操作。 如果內存不夠排序,那么MySQL會先將數據分塊, 對每個獨立的塊使用 “快速排序”?進行排序,并將各個塊的排序結果存放在磁盤上,然后將各個排好序的塊進行合并 最后返回排序結果。
單次傳輸排序(新版本使用)
? ??????先讀取查詢所需要的所有列,然后再根據給定列進行排序,最后直接返回排序結果。這個算法只在MySQL 4.1和后續更新的版本才引入。 因為不再需要從數據表中讀取兩次數據,對于I/O密集型的應用,這樣做的效率高了很多。另外,相比兩次傳輸排序,這個算法只需要一次順序1/0 讀取所有的數據,而無須任何的隨機I/O。 缺點是,如果需要返回的列非常多、 非常大,會額外占用大量的空間,而這些列 對排序操作本 身來說是沒有任何作用的。 因為單條排序記錄很大,所以可能會有更多的排序塊需要合并。
6.4.4查詢執行引擎
????????在解析和優化階段,MySQL將生成查詢對應的執行計劃,MySQL的查詢執行引擎則根據這個執行計劃來完成整個查詢。 這里執行計劃是一個數據結構, 而不是和很多其他的關系型數據庫那樣會生成對應的字節碼。
? ??????相對于查詢優化階段, 查詢執行階段不是那么復雜:MySQL只是簡單地根據執行計劃給出的指令逐步執行。
6.4.5返回結果給客戶端
????????查詢執行的最后一個階段是將結果返回給客戶端。 即使查詢不需要返回結果集給客戶端, MySQL仍然會返回這個查詢的一些信息, 如該查詢影響到的行數。
????????如果查詢可以被緩存, 那么MySQL在這個階段也會將結果存放到查詢緩存中。MySQL將結果集返回客戶端是一個增量、 逐步返回的過程。
? ??????這樣處理有兩個好處:服務器端無須存儲太多的結果, 也就不會因為要返回太多結果而 消耗太多內存。 另外, 這樣的處理也讓MySQL客戶端第一時間獲得返回的結果。
? ??????結果集中的每一行都會以一個滿足MySQL客戶端/服務器通信協議的封包發送, 再通過TCP協議進行傳輸, 在TCP傳輸的過程中, 可能對MySQL的封包進行緩存然后批量傳輸。
6.5 MySQL查詢優化器的局限性
????????MySQL的萬能 ”嵌套循環” 并不是對每種查詢都是最優的。 不過還好, MySQL查詢優化器只對少部分查詢不適用, 而且我們往往可以通過改寫查詢讓MySQL高效地完成工作。
6.5.1關聯子查詢
? ??????MySQL的子查詢實現得非常糟糕。 最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。使用IN()加子查詢, 性能經常會非常糟, 所以 通常建議使用EXISTS()等效的改寫查詢來獲取更好的效率。
如何用好關聯子查詢
????????井不是所有關聯子查詢的性能都會很差。 如果有人跟你說:“別用關聯子查詢"' 那么不要理他。 先測試,然后做出自己的判斷。 很多時候,關聯子查詢是一種非常合理、 自然, 甚至是性能最好的寫法。
6.5.2 UNION的限制
????????有時,MySQL無法將限制條件從外層 “下推” 到內層,這使得原本能夠限制部分返回結果的條件無法應用到內層查詢的優化上。
? ??????如果希望UNION的各個子句能夠根據LIMIT只取部分結果集,或者希望能夠先排好序再合并結果集的話,就需要在UNION的各個子句中分別使用這些子句。
6.5.3索引合并優化
????????在5.0和更新的版本中,當WHERE子句中包含多個復雜條件的 時候,MySQL能夠訪問單個表的多個索引以合井和交叉過濾的方式來定位需要查找的行
6.5.4等值傳遞
????????某些時候,等值傳遞會帶來一些意想不到的額外消耗。 例如,有一個非常大的IN()列表,而MySQL優化器發現存在WHERE、 ON或者USING的子句, 將這個列表的值和另一個表的某個列相關聯。
? ??????那么優化器會將IN()列表都復制應用到關聯的各個表中。 通常, 因為各個表新增了過濾條件, 優化器可以更高效地從存儲引擎過濾記錄。 但是如果這個列表非常大, 則會導致優化和執行都會變慢。
6.5.5并行執行
????????MySQL無法利用多核特性來井行執行查詢。 很多其他的關系型數據庫能夠提供這個特性,但是MySQL做不到。 這里特別指出是想告訴讀者不要花時間去嘗試尋找并行執行查詢的方法。
6.7優化特定類型的查詢
6.7.1優化COUNT()查詢
????????COUNT ()是一個特殊的函數, 有兩種非常不同的作用:它可以統計某個列值的數量, 也 可以統計行數。 在統計列值時要求列值是非空的(不統計 NULL)。如果在 COUNT() 的括號中指定了列或者列的表達式, 則統計的就是這個表達式有值的結果數注 24。因為很多人 對 NULL 理解有問題, 所以這里很容易產生誤解。 如果想了解更多關于SQL語句中 NULL 的含義, 建議閱讀一些關千SQL語句基礎的書籍。(關千這個話題, 互聯網上的一些信息是不夠精確的。)
????????COUNT ()的另一個作用是統計結果集的行數。 當MySQL確認括號內的表達式值不可能為空時, 實際上就是 在統計行數。 最簡單的就是當我們使用 COUNT(*) 的時候, 這種情況下通配符*井不會像我們猜想的那樣擴展成所有的列,實際上,它會忽略所有的列而直接統計所有的行數。
6.7.2優化關聯查詢
這個話題基本上整本書都在討論, 這里需要特別提到的是:
? 確保 ON 或者US ING子句中的列上有索引。在創建索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候, 如果優化器的關聯順序是B、A, 那么就不需要在 B表的對應列上建上索引。 沒有用到的索引只會帶來額外的負擔。 一般來說, 除非 有其他理由, 否則只需要在關聯順序中的第二個表的相應列上創建索引。
? 確保任何的GROUP BY和ORDER BY中的表達式只涉及到一個表中的列, 這樣MySQL才有可能使用索引來優化這個過程。
? 當升級MySQL的時候需要注意:關聯語法 、 運算符優先級等其他可能會發生變化 的地方。 因為以前是普通關聯的地方可能會變成笛卡兒積, 不同類型的關聯可能會 生成不同的結果等。
6.7.3優化子查詢
????????關于子查詢優化我們給出的最重要的優化建議就是盡可能使用關聯查詢代替, 至少當前的MySQL版本需要這樣。 本章的前面章節已經詳細介紹了這點。 “盡可能使用關聯” 并不是絕對的, 如果使用的是MySQL5.6或更新的版本或者MariaDB, 那么就可以直接忽略關于子查詢的這些建議了。
6.7.4優化GROUP BY和DISTINCT
????????在MySQL中, 當無法使用索引的時候, GROUP BY使用兩種策略來完成:使用臨時表或者文件排序來做分組。
? ??????如果需要對關聯查詢做分組(GROUP BY), 并且是按照查找表中的某個列進行分組, 那么通常采用查找表的標識列分組的效率會比其他列更高。
6.7.5優化LIMIT分頁
????????優化此類分頁查詢的一個最簡單的辦法就是盡可能地使用索引覆蓋掃描, 而不是查詢所 有的列。 然后根據需要做一次關聯操作再返回所需的列。 對于偏移量很大的時候, 這樣做的效率會提升非常大。
????????這里的 延遲關聯將大大提升查詢效率,它讓MySQL掃描盡可能少的頁面,獲取需要訪問的記錄 后再根據關聯列回原表查詢需要的所有列。這個技術也可以用于優化關聯查詢中的LIMIT子句。
6.7.7優化UNION查詢
????????除非確實需要服務器消除 重復的行,否則就一定要使用UNION ALL, 這一點很重要。 如 果沒有ALL關鍵字,MySQL會給臨時表加上 DISTINCT選項,這會導致對整個臨時表的 數據做唯一性檢查。 這樣做的代價非常高。 即使有 ALL關鍵字, MySQL仍然會使用臨時表存儲結果。 事實上,MySQL總是將結果放入臨時表,然后再讀出,再返回給客戶端。雖然很多時候這樣做是沒有必要的(例如,MySQL可以直接把這些 結果返回給客戶端)。
6.9總結
????????如果把創建高性能應用程序比作是一個環環相扣的“難題”, 除了前面介紹的schema、索引和查詢語句設計之外, 查詢優化應該是解開“難題” 的最后一步了。要想寫一個好的查詢, 你必須要理解schema設計、索引設計等, 反之亦然。
????????理解查詢是如何被執行的以及時間都消耗在哪些地方, 這依然是前面我們介紹的響應時間的一部分。再加上一些諸如解析和優化過程的知識, 就可以更進一步地理解上一章討論的MySQL如何訪問表和索引的內容了。這也從另一個維度幫助讀者理解MySQL在訪問表和索引時查詢和索引的關系。
????????優化通常都需要三管齊下:不做、少做、快速地做。我們希望這里的案例能夠幫助你將理論和實踐聯系起來。
????????除了這些基礎的手段, 包括查詢、表結構、索引等, MySQL還有一些高級的特性可以幫助你優化應用, 例如分區, 分區和索引有些類似但是原理不同。MySQL還支持查詢緩存,它可以幫你緩存查詢結果,當完全相同的查詢再次執行時,直接使用緩存結果(回想一下, “不做")。我們將在下一章中介紹這些特性。