高性能mysql:查詢性能優化

如何設計最優的庫表結構、 如何創建最好的索引, 這些對於高性能來講是必不可少的。 但這些還不夠一一還須要合理的設計查詢。 若是查詢寫得很糟糕,即便庫表結構再合理、 索引再合適, 也沒法實現高性能。mysql

查詢優化、 索引優化、 庫表結構優化須要齊頭井進, 一個不落。算法

爲何查詢速度會慢


若是把查詢看做是一個任務, 那麼它由一系列子任務組成, 每一個子任務都會消耗必定的時間。 若是要優化查詢, 實際上要優化其子任務, 要麼消除其中一些子任務, 要麼減小子任務的執行次數,要麼讓子任務運行得更快。sql

查詢的生命週期:從客戶端,到服務器,而後在服務器上進行解析,生成執行計劃,執行(包括調用存儲引擎及調用後的排序、分組等數據處理),並返回結果給客戶端。其中「執行」能夠認爲是整個生命週期中最重要的階段,這其中包括了大量爲了檢索數據到存儲引擎的調用以及調用後的數據處理,包括排序、分組等。數據庫

在完成這些任務時,查詢須要在不一樣地方花費時間,包括網絡,CPU 計算,生成統計信息和執行計劃、鎖等待(互斥等待)等操做,尤爲是向底層存儲引擎檢索數據的調用操做,這些調用須要在內存操做、CPU操做和內存不足時致使的I/O操做上消耗時間。跟覺存儲引擎不一樣,可能還會產生大量的上下文切換以及系統調用。緩存

優化查詢的目的就是減小和消除某些操做所話費的時間。服務器

瞭解查詢的生命週期、清楚查詢的時間消耗狀況對於優化查詢有很大的意義。網絡

慢查詢基礎:優化數據訪問


查詢性能低下最基本的緣由是訪問的數據太多。某些查詢可能不可避免地須要篩選大量數據, 但這井不常見。大部分性能低下的查詢均可以經過減小訪問的數據量的方式進行優化。對於低效的查詢, 咱們發現經過下面兩個步驟來分析老是頗有效:數據結構

  1. 確認應用程序是否在檢索大量超過須要的數據。這一般意味着訪問了太多的行, 但有時候也多是訪問了太多的列。
  2. 確認MySQL服務器層是否在分析大量超過須要的數據行。
是否向數據庫請求了不須要的數據

有些查詢會請求超過實際須要的數據, 而後這些多餘的數據會被應用程序丟棄。這會給MySQL服務器帶來額外的負擔, 並增長網絡開銷。另外也會消耗應用服務器的CPU 和內存資源。 一些經典案例:架構

  • 查詢不須要的記錄 一個常見的錯誤是經常會誤覺得MySQL會只返回須要的數據,實際上MySQL倒是先返回所有結果集在進行計算。
  • 多表關聯時返回所有列 查詢全部在電影Academy Dinosaur中出現的演員,千萬不要按下面的寫法編寫查詢:
SELECT * FROM sakila.actor INNER JOIN sakila.film_actor USING(actor_id) INNER JOIN sakila.film USING(film_id) where sakila.film.title = 'Academy Dinosaur';

正確的方式應該是像下面這樣只取須要的列:併發

SELECT sakila.actor.* FROM sakila.actor ....;
  • 老是取出所有列 每次看到SELECT *的時候都須要要用懷疑的眼光審視,是否是真的須要返回所有的列?極可能不是必須的。取出所有的列,會讓優化器沒法完成索引覆蓋掃描這類優化,還會爲服務器帶來額外的I/O、內存和CPU消耗。 查詢返回超過須要的數據也不是壞事。這種有點浪費數據庫資源的方式能夠簡化開發,由於能提升相同代碼片斷的複用性,若是清楚這樣作的性能影響,那麼這種作法也是值得考慮的。獲取並緩存全部的列的查詢,相比多個獨立的只獲取部分列的查詢可能有更有好處。
  • 重複查詢相同的數據
MySQL是否在掃描額外的記錄

在肯定查詢只返回須要的數據之後,接下來應該看看查詢爲了返回結果是否掃描了過多 的數據。對於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須要先從數據表讀出記錄而後過濾。

MySQL不會告訴咱們生產結果實際上須要掃描多少行數據,而只會告訴咱們生產結果時一共掃描了多少行數據。

若是發現查詢須要掃描大量的數據但只返回少數的行, 那麼一般能夠嘗試下面的技巧去優化它:

  • 使用索引覆蓋掃描, 把全部須要用的列都放到索引中, 這樣存儲引擎無須回表獲取對應行就能夠返回結果了(在前面的章節中咱們已經討論過了)。
  • 改變庫表結構。例如使用單獨的彙總表。
  • 重寫這個複雜的查詢, 讓MySQL優化器可以以更優化的方式執行這個查詢(這是本章後續須要討論的問題)。

重構查詢的方式

一個複雜查詢仍是多個簡單查詢

設計查詢的時候一個須要考慮的重要問題是, 是否須要將一個複雜的查詢分紅多個簡單的查詢。在傳統實現中, 老是強調須要數據庫層完成儘量多的工做, 這樣作的邏輯在於之前老是認爲網絡通訊、 查詢解析和優化是一件代價很高的事情。

可是這樣的想法對於MySQL並不適用,MySQL從設計上讓鏈接和斷開鏈接都很輕量級,在返回一個小的查詢結果方面很高效。 現代的網絡速度比之前要快不少, 不管是帶寬仍是延遲。 在某些版本的MySQL上, 即便在一個通用服務器上, 也可以運行每秒超過10萬的查詢, 即便是一個千兆網卡也能輕鬆知足每秒超過2000次的查詢。 因此運行多個小查詢如今已經不是大問題了。

MySQL內部每秒可以掃描內存中上百萬行數據, 相比之下, MySQL響應數據給客戶端就慢得多了。在其餘條件都相同的時候,使用盡量少的查詢固然是更好的。可是有時候,將一個大查詢分解爲多個小查詢是頗有必要的。 別懼怕這樣作, 好好衡量一下這樣作是 不是會減小工做量。 稍後咱們將經過本章的一個示例來展現這個技巧的優點。

在應用設計的時候, 若是一個查詢可以勝任時還寫成多個獨立查詢是不明智的。

切分查詢

有時候對於一個大查詢咱們須要 「分而治之」,將大查詢切分紅小查詢, 每一個查詢功能徹底同樣, 只完成一小部分, 每次只返回一小部分查詢結果。

按期的清楚大量數據時,若是用一個大的語句一次性完成的話,則可能須要一次性鎖住不少數據、佔滿整個事物日誌、耗盡系統資源、阻塞不少小的但很重要的查詢。

分解關聯查詢

不少高性能的應用都會對關聯查詢進行分解。例如:

SELECT * FROM tag JOIN tag_post ON tag_post.tag_id = tag.id JOIN post ON tag_post.post_id = post.id where tag.tag ='mysql';

能夠分解成下面這些查詢來代替:

SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post where tag_id ='1234';
SELECT * FROM post WHERE post.id in(123,456,789,8989);

用分解關聯查詢的方式重構查詢有以下的優點:

  • 讓緩存的效率更高。對MySQL的查詢緩存來講,若是關聯中的某個表發生了變化,那麼就沒法使用查詢緩存了,而拆分後,若是某個表不多改變,那麼基於該表的查詢就能夠重複利用查詢緩存結果了。
  • 將查詢分解後,執行單個查詢能夠減小鎖的競爭。
  • 在應用層作關聯,能夠更容易對數據庫進行拆分,更容易作到高性能和可擴展。
  • 查詢自己效率也可能會有所提高。這個例子中,使用IN()代替關聯查詢,可讓MySQL按照ID順序進行查詢, 這可能比隨機的關聯要更高效。
  • 能夠減小冗餘記錄的查詢。在應用層作關聯查詢,意味着對千某條記錄應用只須要 查詢一次,而在數據庫中作關聯查詢,則可能須要重複地訪問一部分數據。從這點看,這樣的重構還可能會減小網絡和內存的消耗。
  • 更進一步,這樣作至關於在應用中實現了哈希關聯,而不是使用MySQL的嵌套循環關聯。

查詢執行的基礎


當但願MySQL可以以更高的性能運行查詢時,最好的辦法就是弄清楚MySQL是如何一點,不少查詢優化工做實際上就是遵循一些原則讓優優化和執行查詢的。一且理解這一點,不少查詢優化工做實際上就是遵循一些原則讓優化器可以按照預想的合理的方式運行。

  1. 客戶端發送一條查詢給服務器。
  2. 服務器先檢查查詢緩存,若是命中了緩存,則馬上返回存儲在緩存中的結果。不然進入下一階段。
  3. 服務器端進行SQL解析\預處理,再由優化器生成對應的執行計劃。
  4. MySQL根據優化器生成的執行計劃,調用存儲引擎的API來執行查詢。
  5. 將結果返回給客戶端。
MySQL客戶端/服務器通訊協議

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 這表示多種狀況:線程可能在多個狀態之間傳送數據, 或者在生成結果集, 或者在向客戶端返回數據。

查詢緩存

在解析一個查詢語句以前, 若是查詢緩存是打開的, 那麼MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。 這個檢查是經過一個對大小寫敏感的哈希查找實現的。 查詢和緩存中的查詢即便只有一個字節不一樣, 那也不會匹配緩存結果注 11' 這種狀況下查詢就會進入下一階段的處理。

若是當前的查詢剛好命中了查詢緩存, 那麼在返回查詢結果以前MySQL會檢查一次用戶權限。 這仍然是無須解析查詢SQL語句的, 由於在查詢緩存中已經存放了 當前查詢須要訪問的表信息。 若是權限沒有問題, MySQL會跳過全部 其餘階段, 直接從緩存中拿到結果並返回給客戶端。 這種狀況下,查詢不會被解析,不用生成執行計劃,不會被執行。

查詢優化處理

查詢的生命週期的下一步是將一個SQL轉換成一個執行計劃, MySQL再依照這個執行計劃和存儲引擎進行交互。 這包括多個子階段:解析SQL、 預處理 、 優化SQL執行計劃。這個過程當中任何錯誤(例如語法錯誤)均可能終止查詢。 這裏不打算詳細介紹MySQL內部實現, 而只是選擇性地介紹其中幾個獨立的部分, 在實際執行中, 這幾部分可能一塊兒執行也可能單獨執行。 咱們的目的是幫助你們理解MySQL如何執行查詢, 以便寫出更優秀的查詢。

語法解析器和預處理

首先, MySQL經過關鍵字將SQL語句進行解析, 並生成 一棵對應的 「 解析樹 」 。 MySQL解析器將使用MySQL語法規則驗證和解析查詢。 例如, 它將驗證是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能先後正確匹配。

預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這裏將檢查數據表和數據列是否存在,還會解析名字和別名,看看它們是否有歧義。

下一步預處理器會驗證權限。 這一般很快,除非服務器上有很是多的權限配置。

查詢優化器

如今語法樹被認爲是合法的了,而且由優化器將其轉化成執行計劃。 一條查詢能夠有很 多種執行方式,最後都返回相同的結果。 優化器的做用就是找到這其中最好的執行計劃。

MySQL使用基於成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。 最初,成本的最小單位是隨機讀取一個4K數據頁的成本,後來(成本計算公式)變得更加複雜,井且引入了一些 「因子」 來估算某些操做的代價, 如當執行一次WHERE條件比較的成本。 能夠經過查詢當前會話的Last_query_cost的值來得知MySQL計算的當前查詢的成本。

SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor;

SHOW STATUS LIKE 'Last_query_cost';

有不少種緣由會致使MySQL優化器選擇錯誤的執行計劃,以下所示:

  • 統計信息不許確。 MySQL依賴存儲引擎提供的統計信息來評估成本,可是有的存儲引擎提供的信息是準確的,有的誤差可能很是大。 例如,InnoDB由於其MVCC的架構,井不能維護一個數據表的行數的精確統計信息。
  • 執行計劃中的成本估算不等同於實際執行的成本。 因此即便統計信息精準,優化器給出的執行計劃也可能不是最優的。 例若有時候某個執行計劃雖然須要讀取更多的頁面,可是它的成本卻更小。由於若是這些頁面都是順序讀或者這些頁面都已經在內存中的話,那麼它的訪問成本將很小。
  • MySQL層面並不知道哪些頁面在內存中、哪些在磁盤上,因此查詢實際執行過程當中到底須要多少次物理1/0是沒法得知的。 MySQL的最優可能和你想的最優不同。你可能但願執行時間儘量的短,可是MySQL只是基於其成本模型選擇最優的執行計劃,而有些時候這井不是最快的執行 方式。因此,這裏咱們看到根據執行成原本選擇執行計劃並非完美的模型。
  • MySQL從不考慮其餘併發執行的查詢,這可能會影響到當前查詢的速度。
  • MySQL也井不是任什麼時候候都是基於成本的優化。有時也會基千一些固定的規則,例如,若是存在全文搜索的MATCH()子句,則在存在全文索引的時候就使用全文索引。即便有時候使用別的索引和WHERE條件能夠遠比這種方式要快,MySQL也仍然會使用對應的全文索引。
  • MySQL不會考慮不受其控制的操做的成本,例如執行存儲過程或者用戶自定義函數的成本。
  • 優化器有時候沒法去估算全部可能的執行計劃,因此它可能錯過實際上最優的執行計劃。

MySQL的查詢優化器是一個很是複雜的部件,它使用了不少優化策略來生成一個最優的執行計劃。優化策略能夠簡單地分爲兩種,一種是靜態優化,一種是動態優化。靜態優化能夠直接對解析樹進行分析,井完成優化。例如,優化器能夠經過一些簡單的代數變換將WHERE條件轉換成另外一種等價形式。靜態優化不依賴於特別的數值,如WHERE條件中帶入的一些常數等。靜態優化在第一次完成後就一直有效,即便使用不一樣的參數重複執行查詢也不會發生變化。能夠認爲這是一種「 編譯時優化」。

相反,動態優化則和查詢的上下文有關,也可能和不少其餘因素有關,例如WHERE條件中的取值、索引中條目對應的數據行數等。這須要在每次查詢的時候都從新評估,能夠認爲這是「運行時優化」。

在執行語句和存儲過程的時候,動態優化和靜態優化的區別很是重要。MySQL對查詢的靜態優化只須要作一次,但對查詢的動態優化則在每次執行時都須要從新評估。有時候甚至在查詢的執行過程當中也會從新優化。

下面是一些MySQL可以處理的優化類型:

從新定義關聯表的順序

數據表的關聯井不老是按照在查詢中指定的順序進行。決定關聯的順序是優化器很重要的一部分功能,本章後面將深刻介紹這一點。

將外鏈接轉化成內鏈接

並非全部的OUTER JOIN語句都必須之外鏈接的方式執行。諸多因素,例如WHERE條件、庫表結構均可能會讓外鏈接等價於一個內鏈接。MySQL可以識別這點並重寫查詢,讓其能夠調整關聯順序。

使用等價變換規則

MySQL可使用一些等價變換來簡化並規範表達式。它能夠合併和減小一些比較,還能夠移除一些恆成立和一些恆不成立的判斷。這些規則對於咱們編寫條件語句頗有用,咱們將在本章後續繼續討論。

優化COUNT()、MIN()和MAX()

索引和列是否可爲空一般能夠幫助MySQL優化這類表達式。例如,要找到某一列的最小值,只須要查詢對應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) 均可能是關聯。

對於UNION查詢,MySQL先將一系列的單個查詢結果放到一個臨時表中,而後再從新讀出臨時表數據來完成UNION查詢。在MySQL的概念中,每一個查詢都是一次關聯,因此讀取結果臨時表也是一次關聯。

當前MySQL關聯執行的策略很簡單:MySQL對任何關聯都執行嵌套循環關聯操做,即MySQL先在一個表中循環取出單條數據,而後再嵌套循環到下一個表中尋找匹配的行,依次下去,直到找到全部表中匹配的行爲止。而後根據各個表匹配的行,返回查詢中須要的各個列。MySQL會嘗試在最後一個關聯表中找到全部匹配的行,若是最後一個關聯表沒法找到更多的行之後, MySQL返回到上一層次關聯表, 看是否可以找到更多的匹配記錄, 依此類推迭代執行。

按照這樣的方式查找第一個表記錄, 再嵌套查詢下一個關聯表, 而後回溯到上一個表,在MySQL中是經過嵌套循環的方式實現一正如其名 「嵌套循環關聯」。

從本質上說,MySQL多全部的類型的查詢都以一樣的方式運行。例如,MySQL在FROM子句中遇到子查詢時,先執行子查詢並將其結果放到一個臨時表中,而後將這個臨時表看成一個普通表對待。MySQL在執行UNIONv查詢時也使用相似的臨時表,在遇到右外鏈接的時候,MySQL將其改寫成等價的左外鏈接。簡而言之,當前版本的MySQL會將全部的查詢類型都轉換成相似的執行計劃。

執行計劃

和不少其餘關係數據庫不一樣,MySQL井不會生成查詢字節碼來執行查詢。MySQL生成 查詢的一棵指令樹,而後經過存儲引擎執行完成這棵指令樹井返回結果。最終的執行計劃包含了重構查詢的所有信息。若是對某個查詢執行EXPLAIN EXTENDED後,再執行SHOW WARNINGS, 就能夠看到重構出的查詢。

MySQL老是從一個表開始一直嵌套循環、回溯完成全部表關聯。

關聯查詢優化器

MySQL優化器最重要的一部分就是關聯查詢優化,它決定了多個表關聯時的順序。一般多表關聯的時候,能夠有多種不一樣的關聯順序來得到相同的執行結果。關聯查詢優化器則經過評估不一樣順序時的成原本選擇一個代價最小的關聯順序。

排序優化

不管如何排序都是一個成本很高的操做,因此從性能角度考慮,應儘量避免排序或者儘量避免對大量數據進行排序。

若是須要排序的數據量小於 「排序緩衝區", MySQL使用內存進行 「快速排序」 操做。 若是內存不夠排序,那麼MySQL會先將數據分塊, 對每一個獨立的塊使用 「快速排序」 進行排序,並將各個塊的排序結果存放在磁盤上,而後將各個排好序的塊進行合併最後返回排序結果。

兩次傳輸排序(就版本使用)

讀取行指針和須要排序的字段,對其進行排序,而後再根據排序結果讀取所須要的數據行。 這須要進行兩次數據傳輸,即須要從數據表中讀取兩次數據,第二次讀取數據的時候,由於是讀取排序列進行排序後的全部記錄,這會產生大量的隨機I/O,因此兩次數據傳輸的成本很是高。

單次傳輸排序(新版本使用)

先讀取查詢所須要的全部列,而後再根據給定列進行排序,最後直接返回排序結果。這個算法只在MySQL 4.1和後續更新的版本才引入。 由於再也不須要從數據表中讀取兩次數據,對於I/O密集型的應用,這樣作的效率高了不少。另外,相比兩次傳輸排序,這個算法只須要一次順序1/0 讀取全部的數據,而無須任何的隨機I/O。 缺點是,若是須要返回的列很是多、 很是大,會額外佔用大量的空間,而這些列 對排序操做本 身來講是沒有任何做用的。 由於單條排序記錄很大,因此可能會有更多的排序塊須要合併。

在關聯查詢的時候若是須要排序,MySQL會分兩種狀況來處理這樣的文件排序。若是ORDER BY子句中的全部列都來自關聯的地一個表,那麼MySQL在關聯處理地一個表的時候就進行文件排序。除此以外的全部狀況,MySQL都會先將關聯的結果存放到一個臨時表中,而後在全部的關聯都結束後,在進行文件排序。

查詢執行引擎

在解析和優化階段,MySQL將生成查詢對應的執行計劃,MySQL的查詢執行引擎則根據這個執行計劃來完成整個查詢。 這裏執行計劃是一個數據結構, 而不是和不少其餘的關係型數據庫那樣會生成對應的字節碼。

相對於查詢優化階段, 查詢執行階段不是那麼複雜:MySQL只是簡單地根據執行計劃給出的指令逐步執行。在根據執行計劃逐步執行的過程當中,有大量的操做須要經過調用存儲引擎實現的接口來完成。

返回結果給客戶端

查詢執行的最後一個階段是將結果返回給客戶端。 即便查詢不須要返回結果集給客戶端, MySQL仍然會返回這個查詢的一些信息, 如該查詢影響到的行數。

若是查詢能夠被緩存, 那麼MySQL在這個階段也會將結果存放到查詢緩存中。

MySQL將結果集返回客戶端是一個增量、 逐步返回的過程。

這樣處理有兩個好處:服務器端無須存儲太多的結果,也就不會由於要返回太多結果而 消耗太多內存。另外,這樣的處理也讓MySQL客戶端第一時間得到返回的結果。

結果集中的每一行都會以一個知足MySQL客戶端/服務器通訊協議的封包發送, 再經過TCP協議進行傳輸, 在TCP傳輸的過程當中, 可能對MySQL的封包進行緩存而後批量傳輸。

MySQL查詢優化器的侷限性


MySQL的萬能 」嵌套循環」 並非對每種查詢都是最優的。 不過還好, MySQL查詢優化器只對少部分查詢不適用, 並且咱們每每能夠經過改寫查詢讓MySQL高效地完成工做。

關聯子查詢

MySQL的子查詢實現得很是糟糕。 最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。使用IN()加子查詢, 性能常常會很是糟, 因此 一般建議使用EXISTS()等效的改寫查詢來獲取更好的效率。

SELECT * FROM sakila.film where film_id IN(SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);

通常會認爲MySQL會先執行子查詢返回全部包含actor_id 爲1的film_id。通常來講,IN()列表查詢速度很快,因此有人會認爲上面的查詢會這樣執行:

MySQL會將相關的外層表壓到子查詢中,它認爲這樣能夠更效率地差找到數據行。MySQL會將查詢改寫成這樣子:

SELECT * FROM sakila.film WHERE EXISTS(SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id);

若是外層的表是一個很是大的表,那麼這個查詢的性能會很是糟糕。可使用下面的方法重寫查詢:

SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING (film_id) WHERE actor_id = 1;

另外一個優化的辦法是使用函數GROUP_CONCAT()在IN()中構造一由逗號分隔的列表。

如何用好關聯子查詢

井不是全部關聯子查詢的性能都會不好。 若是有人跟你說:「別用關聯子查詢"' 那麼不要理他。 先測試,而後作出本身的判斷。 不少時候,關聯子查詢是一種很是合理、 天然, 甚至是性能最好的寫法。

UNION的限制

有時,MySQL沒法將限制條件從外層 「下推」 到內層,這使得本來可以限制部分返回結果的條件沒法應用到內層查詢的優化上。

若是但願UNION的各個子句可以根據LIMIT只取部分結果集,或者但願可以先排好序再合併結果集的話,就須要在UNION的各個子句中分別使用這些子句。

索引合併優化

在5.0和更新的版本中,當WHERE子句中包含多個複雜條件的 時候,MySQL可以訪問單個表的多個索引以合井和交叉過濾的方式來定位須要查找的行。

等值傳遞

某些時候,等值傳遞會帶來一些意想不到的額外消耗。例如,有一個很是大的IN()列表,而MySQL優化器發現存在WHERE、ON或者USING的子句,將這個列表的值和另外一個表的某個列相關聯。

那麼優化器會將IN()列表都複製應用到關聯的各個表中。一般,由於各個表新增了過濾條件,優化器能夠更高效地從存儲引擎過濾記錄。可是若是這個列表很是大,則會致使優化和執行都會變慢。

並行執行

MySQL沒法利用多核特性來井行執行查詢。 不少其餘的關係型數據庫可以提供這個特性,可是MySQL作不到。 這裏特別指出是想告訴讀者不要花時間去嘗試尋找並行執行查詢的方法。

哈希關聯

MySQL並不支持哈希關聯——MySQL的全部關聯都是嵌套循環關聯。能夠經過創建一個哈希索引來曲線地實現哈希關聯。

鬆散掃描索引

MySQL並不支持鬆散索引掃描,也就沒法按照不連續的方式掃描一個索引。一般,MySQL的索引掃描須要先定義一個起點和終點,即便須要的數據只是這段索引中不多數幾個,MySQL仍須要掃描這段索引中每個條目。

假設有以下索引(a,b),有下面的查詢:

SELECT ... FROM tb1 WHERE b BETWEEN 2 AND 3;

由於索引的前導字段是列a,可是在查詢中指定了字段b,MySQL沒法使用索引,從而只能經過全表掃描找到匹配的行。

瞭解索引的物理結構的話,不難發現這還能夠有一個更快的辦法執行上面的查詢。索引的物理結構(不是存儲引擎的API)使得能夠先掃描a列地一個值對應的b列的範圍,而後在跳到a列第二個不一樣值掃描對應的b列的範圍。

使用鬆散索引掃描效率會更高,可是MySQL如今還不支持這麼作。

MySQL5.0以後的版本,在某些特殊的場景下是可使用鬆散索引掃描的。

SELECT actor_id,MAX(film_id) FROM sakila.film_actor GROUP BY actor_id;
最大值和最小值優化

對於MIN()和MAX()查詢,MySQL的優化作的並很差。例如:

SELECT MIN(actor_id) FROM sakila.actor WHERE first_name ='PENELOPE';

由於在first_name字段上並無索引,所以MySQL會進行一次全表掃描,若是MySQL可以進行主鍵掃描。一個曲線的優化辦法是移除MIN(),而後使用LIMIT來將查詢重寫:

SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENELOPE' LIMIT 1;

有時候爲了獲取更高的性能,不得不放棄一些原則。

在同一個表上查詢和更新

MySQL不容許對同一張表同時進行查詢和更新。這其實並非優化器的限制,若是清楚MySQL是如何執行查詢的,就能夠避免這種狀況。

UPDATE tb1 AS outer_tb1 SET cnt = (SELECT count(*) FROM tb1 AS inner_tb1 WHERE inner_tb1.type = outer_tb1.type));

下面的查詢將會正常執行:

UPDATE tb1 INNER JOIN(SELECT type,count(*) AS cnt FROM tb1 GROUP BY type)) AS der USING(type) SET tb1.cnt = der.cnt;

查詢優化器的提示


若是對優化器選擇的執行計劃不滿意,可使用優化器提供的幾個提示來控制最終的執行計劃。可使用的一些提示以下:

  • HIGH_PRIORITY和LOW_PRIORITY 這個提示告訴MySQL,當多個語句同時訪問某一個表的時候,哪些語句的優先級相對高些、哪些語句的優先級相對低些。 HIGH_PRIORITY用於SELECT語句的時候,MySQL會將此SELECT語句從新調度到全部正在等待表鎖以便修改數據的語句以前。實際上MySQL是將其放在表的隊列的最前面,而不是按照常規順序等待。 LOW_PRIORITY則正好相反:它會讓該語句一直處於等待狀態,只要隊列中還有須要訪問同一個表的語句——即便是那些比該語句還晚提交到服務器的語句。 這兩個提示只對使用表鎖的存儲引擎有效,千萬不要在InnoDB或者其餘有細粒度鎖機制和併發控制的引擎中使用。即便實在MyISAM中使用也要注意,由於這兩個提示會致使併發插入被禁用,可能會嚴重下降性能。

  • DELAYED 這個提示對INSERT和REPLACE有效。MySQL會將使用該提示的語句當即返回給客戶端,並將插入的行數據放入到緩衝區,而後在表空閒時批量將數據寫入。日誌系統使用這樣的提示很是有效,或者是其餘須要寫入大量數據可是客戶端卻不須要等待單條語句完成I/O的應用。這個用法有一些限制:並非全部的存儲引擎都支持這樣的作法;而且該提示會致使函數LAST_INSERT_ID()失效。

  • STRAIGHT_JOIN 這個提示能夠放置在SELECT語句的SELECT關鍵字以後,也能夠放置在任何兩個關聯表的名字之間。第一個用法是讓查詢中全部的表按照在語句中出現的順序進行關聯。第二個用法則是固定其先後兩個表的關聯順序。 當MySQL沒能選擇正確的關聯順序的時候,或者因爲可能的順序太多致使MySQL沒法評估全部的關聯順序的時候,STRAIGHT_JOIN都會頗有用。在後面這種狀況,MySQL可能會花費大量時間在‘statistics’狀態,加上這個提示則會大大減小優化器的搜索空間。 能夠先使用EXPLAIN語句來查看優化器選擇的關聯順序,而後使用該提示來重寫查詢,在看看它的關聯順序。當你肯定不管怎樣where條件,某個固定的關聯順序始終是最佳的時候,使用這個提示能夠大大提升優化器的效率。

  • SQL_SMALL_RESULT和SQL_BIG_RESULT 這兩個提示只對SELECT語句有效。他們告訴優化器對GROUP BY或者DISTINCT查詢如何使用臨時表及排序。SQL_SMALL_RESULT告訴優化器對GROUP BY或者DISTINCT查詢如何使用臨時表及排序。SQL_SMALL_RESULT告訴優化器結果集會很小,能夠將結果集放到內存中的索引臨時表,以免排序操做。若是是SQL_BIG_RESULT,則告訴優化器結果集可能會很是大,建議使用磁盤臨時表作排序操做。

  • SQL_BUFFER_RESULT 這個提示告訴優化器將查詢結果放入到一個臨時表,而後儘量快地釋放表鎖。這和前面提到的有由客戶端緩存結果不一樣。當你無法使用客戶端緩存的時候,使用服務器端的緩存一般頗有效。帶來的好處是無須在客戶端上消耗太多內存,還能夠儘量快地釋放對應的表鎖。代價是,服務器端將須要更多的內存。

  • SQL_CACHE和SQL_NO_CACHE 嚴格來講,這並非一個優化器提示。它不會告訴優化器任何關於執行計劃的東西。他會讓MySQL返回的結果集包含更多的信息。查詢中加上該提示MySQL會計算除去LIMIT子句後這個查詢要返回的結果集的總數,而實際上只返回LIMIT要求的結果集。

  • FOR UPDATE和LOCK IN SHARE MODE 這也不是真正的優化其提示。這兩個提示主要控制SELECT語句的鎖機制,但只對實現了行級鎖的存儲引擎有效。使用該提示會對符合查詢條件的數據行加鎖。對於INSERT... SELECT語句是不須要這兩個提示的,由於對於MySQL5.0和更新版本會默認給這些記錄加上讀鎖。 惟一內置的支持這兩個提示的引擎就是InnoDB。另外須要記住的是,這兩個提示會讓某些優化沒法正常使用,例如索引覆蓋掃描。這兩個提示常常被濫用,很容易形成服務器的鎖爭用問題。

  • USE INDEX、IGNORE INDEX和FORCE INDEX 這幾個提示會告訴優化器使用或者不使用哪些索引來查詢記錄。在MySQL5.0和更早的版本,這些提示並不會影響到優化器選擇哪一個索引進行排序和分組,在MySQSL5.1和以後的版本能夠經過新增選項FOR ORDER BY和FOR GROUP BY來指定是否排序和分組有效。 FORCE INDEX和USE INDEX基本相同,除了一點:FORCE INDEX會告訴優化器全表掃描的成本會遠遠高於掃描索引,或者由於某些緣由要使用另外一個索引時,可使用該提示。

  • optimizer_search_depth 這個參數控制優化器在窮舉執行計劃的限度。若是查詢長時間處於‘statistics’狀態,那麼能夠考慮調低次參數。

  • optimizer_prune_level 該參數默認是打開的,這讓優化器會根據須要掃描的行數來決定是否跳過某些執行計劃。

  • optimizer_switch 這個變量包含了一些開啓/關閉優化器特性的標誌位。

優化特定類型的查詢


優化COUNT()查詢

COUNT()是一個特殊的函數,有兩種很是不一樣的做用:它能夠統計某個列值的數量,也能夠統計行數。 在統計列值時要求列值是非空的(不統計 NULL)。若是在 COUNT() 的括號中指定了列或者列的表達式,則統計的就是這個表達式有值的結果數。由於不少人對 NULL 理解有問題,因此這裏很容易產生誤解。若是想了解更多關於SQL語句中NULL的含義,建議閱讀一些關千SQL語句基礎的書籍。(關於這個話題,互聯網上的一些信息是不夠精確的。)

COUNT()的另外一個做用是統計結果集的行數。當MySQL確認括號內的表達式值不可能爲空時,實際上就是在統計行數。最簡單的就是當咱們使用 COUNT(*) 的時候,這種狀況下通配符*井不會像咱們猜測的那樣擴展成全部的列,實際上,它會忽略全部的列而直接統計全部的行數。

關於MyISAM的神話

MyISAM的COUNT()函數老是很是快,不過這是有前提條件的,即只有沒有任何WHERE條件的COUNT()才很是快,所以此時無須實際地去計算表的行數。若是MySQL知道某列col不可能爲NULL值,那麼MySQL內部會將COUNT(col)表達式優化爲COUNT()。當統計帶WHERE子句的結果集行數,能夠是統計某個列值的數量時,MyISAM的COUNT()和其餘存儲引擎沒有任何不一樣,就再也不有神話般的速度了。

簡單優化

有時候可使用MyISAM在COUNT(*)全表很是快的這個個性,來加速一些特定條件的COUNT()的查詢。

SELECT COUNT(*) FROM world.City WHERE ID > 5;

若是將條件反轉一下

SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) FROM world.City WHERE ID <= 5;

使用近似值

有時候某些業務場景並不要求徹底精確的COUNT()值,此時能夠用近似值來代替。EXPLAIN出來的優化器估算的行數就是一個不錯的近似值,執行EXPLAIN並不須要真正地去執行查詢,因此成本很低。

更復雜的優化

一般來講,COUNT()都須要掃描大量的行(意味着要訪問大量數據)才能得到精確的結果,所以是很難優化的。除了前面的方法,在MySQL層面還能作的就只有索引覆蓋掃描了。

優化關聯查詢

這個話題基本上整本書都在討論, 這裏須要特別提到的是:

  • 確保 ON 或者US ING子句中的列上有索引。在建立索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候, 若是優化器的關聯順序是B、A, 那麼就不須要在 B表的對應列上建上索引。 沒有用到的索引只會帶來額外的負擔。 通常來講, 除非 有其餘理由, 不然只須要在關聯順序中的第二個表的相應列上建立索引。
  • 確保任何的GROUP BY和ORDER BY中的表達式只涉及到一個表中的列, 這樣MySQL纔有可能使用索引來優化這個過程。
  • 當升級MySQL的時候須要注意:關聯語法 、 運算符優先級等其餘可能會發生變化 的地方。 由於之前是普通關聯的地方可能會變成笛卡兒積, 不一樣類型的關聯可能會 生成不一樣的結果等。
優化子查詢

關於子查詢優化咱們給出的最重要的優化建議就是儘量使用關聯查詢代替,至少當前的MySQL版本須要這樣。本章的前面章節已經詳細介紹了這點。「儘量使用關聯」 並非絕對的,若是使用的是MySQL5.6或更新的版本或者MariaDB,那麼就能夠直接忽略關於子查詢的這些建議了。

優化GROUP BY和DISTINCT

在MySQL中,當沒法使用索引的時候,GROUP BY使用兩種策略來完成:使用臨時表或者文件排序來作分組。

若是須要對關聯查詢作分組(GROUP BY),而且是按照查找表中的某個列進行分組,那麼一般採用查找表的標識列分組的效率會比其餘列更高。

優化LIMIT分頁

優化此類分頁查詢的一個最簡單的辦法就是儘量地使用索引覆蓋掃描,而不是查詢全部的列。而後根據須要作一次關聯操做再返回所需的列。對於偏移量很大的時候,這樣作的效率會提高很是大。

SELECT film_id,description FROM sakila.film ORDER BY title LIMIT 50,5;

若是這個表很是大,那麼這個查詢最好該寫成下面的樣子:

SELECT film_id,description FROM sakila.film INNER JOIN(SELECT film_id FROM sakila.film ORDER BY title LIMIT 50,5) AS lim USING (film_id);

這裏的延遲關聯將大大提高查詢效率,它讓MySQL掃描儘量少的頁面,獲取須要訪問的記錄 後再根據關聯列回原表查詢須要的全部列。這個技術也能夠用於優化關聯查詢中的LIMIT子句。

優化SQL_CALC_FOUND_ROWS

分頁的時候,另外一個經常使用的技巧是在LIMIT語句中加上SQL_CALC_FOUND_ROWS提示,這樣就能夠得到去掉LIMIT之後知足條件的行數,所以能夠做爲分頁的總數。無論是否須要,MySQL都會掃描全部知足條件的行,而後在拋棄掉不須要的行,而不是在知足LIMIT的行數後就終止掃描。 一個更好的設計是將具體的頁數換成「下一頁」按鈕。 另外一種作法是先獲取並緩存較多的數據——例如,緩存1000條——而後每次分頁都從這個緩存中獲取。

優化UNION查詢

除非確實須要服務器消除重複的行,不然就必定要使用UNION ALL, 這一點很重要。 如 果沒有ALL關鍵字,MySQL會給臨時表加上 DISTINCT選項,這會致使對整個臨時表的 數據作惟一性檢查。 這樣作的代價很是高。 即便有 ALL關鍵字, MySQL仍然會使用臨時表存儲結果。 事實上,MySQL老是將結果放入臨時表,而後再讀出,再返回給客戶端。雖然不少時候這樣作是沒有必要的(例如,MySQL能夠直接把這些 結果返回給客戶端)。

靜態查詢分析

Percona Toolkit中的qt-query-advisor可以解析查詢日誌、分析查詢模式,而後給出全部可能存在潛在問題的查詢,並給出足夠詳細的建議。

使用用戶自定義變量

用戶自定義變量是一個容易被遺忘的MySQL特性,可是若是可以用好,發揮其潛力,在某些場景能夠寫出很是高效的查詢語句。在查詢中混合使用過程化和關係化邏輯的時候,自定義變量可能會很是有用。單純的關係查詢將全部的東西都當成無序的數據集合,而且一次性操做它們。MySQL則採用了更加程序化的處理方式。MySQL的這種方式有它的弱點,但若是能熟練地掌握,則會發現其強大之處,而用戶自定義變量也能夠給這中種方式帶來很大的幫助。

如下場景不能使用用戶自定義變量:

  • 使用自定義變量的查詢,沒法使用查詢緩存。
  • 不能在使用變量或者標識符的地方使用自定義變量,例如表名、列名和LIMIT子句中。
  • 用戶自定義變量的生命週期是在一個鏈接中有效,因此不能用它們來作鏈接間的通訊。
  • 若是使用鏈接池或者持久化鏈接,自定義變量可能讓看起來毫無關係的代碼發生交互。
  • 在5.0以前的版本,是大小寫敏感的,因此要注意代碼在不一樣MySQL版本間的兼容性問題。
  • 不能顯式地聲明自定義變量的類型。肯定未定義變量的具體類型的時機在不一樣MySQL版本中也可能不同。用戶自定義變量的類型在賦值的時候會改變。MySQL的用戶自定義變量是一個動態類型。
  • MySQL優化器在某些場景下可能會將這些變量優化掉,這可能致使代碼不按預想的方式運行。
  • 賦值的順序和賦值的時間點並非固定的,這依賴於優化器的決定。實際狀況可能很讓人困惑。
  • 賦值符號:=的優先級很是低,因此須要注意,賦值表達式應該使用明確的括號。
  • 使用爲定義變量不會產生任何語法錯誤。

優化排名語句

使用用戶自定義變量的一個重要特性是你能夠再給一個變量賦值的同時使用這個變量。

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM sakila.actor LIMIT 3;

返回每一個演員參演電影的數量:

SELECT actor_id, COUNT(*) as cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10;

如今咱們再把排名加上去,使用三個變量來實現:一個用來記錄當前的排名,一個用來記錄前一個演員的排名,還有一個用來記錄當前演員參演的的電影數量。

SET @curr_cut := 0, @prev_cnt := 0, @rank := 0;
SELECT actor_id, @curr_cnt := COUNT(*) AS cnt, @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank ) AS rank,
@prev_cnt := @curr_cnt AS dummy FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10;

Oops——排名和統計列一直都沒法更新,這是什麼緣由? 這裏經過EXPLAINA咱們看到將會使用臨時表和文件排序,因此多是因爲變量賦值的時間和咱們預料的不一樣。

SET @curr_cut := 0, @prev_cnt := 0, @rank := 0;
SELECT actor_id, @curr_cnt := cnt AS cnt, @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank ) AS rank,
@prev_cnt := @curr_cnt AS dummy FROM (SELECT actor_id,COUNT(*) AS cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10) as der;

避免重複查詢剛剛更新的數據

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1;
SELECT lastUpdated FROM t1 WHERE id = 1;

使用變量,能夠按以下方式重寫查詢:

UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;

統計更新和插入的數量

INSERT INTO t1(c1,c2) VALUES(4,4),(2,1),(3,1) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + (0 * (@x := @x + 1));

當每次因爲衝突致使更新時對變量@x字增一次。而後經過對這個表達式乘以0來讓其不影響要更新的內容。

肯定取值的順序 使用用戶自定義變量的一個最多見的問題就是沒有注意到在賦值和讀取變量的時候可能實在查詢的不一樣階段。

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1;

由於WHERE和SELECT是在查詢執行的不一樣階段被執行的。若是在查詢中再加入ORDER BY的話,結果可能會更不一樣:

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1 ORDER BY first_name;

解決這個問題的辦法就是讓變量的賦值和取值發生在執行查詢的同一階段:

SET @rownum := 0;
SELECT actor_id, @rownum AS cnt FROM sakila.actor WHERE (@rownum := @rownum + 1) <=1;

將賦值語句放到LEAST()函數中,這樣就能夠在徹底不改變排序順序的時候完成賦值操做。GREATEST()、LENGTH()、ISNULL()、NULLIFL()、IF()、COALESCE()。

SET @rownum := 0;
SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1 ORDER BY first_name,LEAST(0,@rownum := @rownum + 1);

編寫偷懶的UNION

SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tb1 FROM users WHERE id = 1 
UNION ALL SELECT id, 'users_archived' FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;

用戶自定義變量的其餘用處

不只實在SELECT語句中,在其餘任何類型的SQL語句中均可以對變量進行賦值。 用戶自定義變量可以作的有趣的事情:

  • 查詢運行時計算總數和平均值。
  • 模擬GROUP()語句中的函數FIRST()和LAST()。
  • 對大量數據作一些數據計算。
  • 計算一個大表的MD5散列值。
  • 編寫一個樣本處理函數,當樣本中的數值超過某個邊界的時候將其變成0。
  • 模擬讀/寫遊標。
  • 在SHOW語句的WHERE子句中加入變量值。

總結

若是把建立高性能應用程序比做是一個環環相扣的「難題」, 除了前面介紹的schema、索引和查詢語句設計以外, 查詢優化應該是解開「難題」 的最後一步了。要想寫一個好的查詢, 你必需要理解schema設計、索引設計等, 反之亦然。

理解查詢是如何被執行的以及時間都消耗在哪些地方, 這依然是前面咱們介紹的響應時間的一部分。再加上一些諸如解析和優化過程的知識, 就能夠更進一步地理解上一章討論的MySQL如何訪問表和索引的內容了。這也從另外一個維度幫助讀者理解MySQL在訪問表和索引時查詢和索引的關係。

優化一般都須要三管齊下:不作、少作、快速地作。咱們但願這裏的案例可以幫助你將理論和實踐聯繫起來。

除了這些基礎的手段, 包括查詢、表結構、索引等, MySQL還有一些高級的特性能夠幫助你優化應用, 例如分區, 分區和索引有些相似可是原理不一樣。MySQL還支持查詢緩存,它能夠幫你緩存查詢結果,當徹底相同的查詢再次執行時,直接使用緩存結果。

相關文章
相關標籤/搜索