MySQL Optimization 優化原理

MySQL Optimization 優化原理

MySQL邏輯架構

若是能在頭腦中構建一幅MySQL各組件之間如何協同工做的架構圖,有助於深刻理解MySQL服務器。下圖展現了MySQL的邏輯架構圖。mysql

images

  • MySQL邏輯架構,來自:高性能MySQL

MySQL邏輯架構總體分爲三層,最上層爲客戶端層,並不是MySQL所獨有,諸如:鏈接處理、受權認證、安全等功能均在這一層處理。程序員

MySQL大多數核心服務均在中間這一層,包括查詢解析、分析、優化、緩存、內置函數(好比:時間、數學、加密等函數)。全部的跨存儲引擎的功能也在這一層實現:存儲過程、觸發器、視圖等。redis

最下層爲存儲引擎,其負責MySQL中的數據存儲和提取。和Linux下的文件系統相似,每種存儲引擎都有其優點和劣勢。中間的服務層經過API與存儲引擎通訊,這些API接口屏蔽了不一樣存儲引擎間的差別。算法

MySQL查詢過程

咱們老是但願MySQL可以得到更高的查詢性能,最好的辦法是弄清楚MySQL是如何優化和執行查詢的。一旦理解了這一點,就會發現:不少的查詢優化工做實際上就是遵循一些原則讓MySQL的優化器可以按照預想的合理方式運行而已。sql

images

客戶端/服務端通訊協議

MySQL客戶端/服務端通訊協議是「半雙工」的:在任一時刻,要麼是服務器向客戶端發送數據,要麼是客戶端向服務器發送數據,這兩個動做不能同時發生。一旦一端開始發送消息,另外一端要接收完整個消息才能響應它,因此咱們沒法也無須將一個消息切成小塊獨立發送,也沒有辦法進行流量控制。數據庫

客戶端用一個單獨的數據包將查詢請求發送給服務器,因此當查詢語句很長的時候,須要設置max_allowed_packet參數。可是須要注意的是,若是查詢實在是太大,服務端會拒絕接收更多數據並拋出異常。緩存

與之相反的是,服務器響應給用戶的數據一般會不少,由多個數據包組成。可是當服務器響應客戶端請求時,客戶端必須完整的接收整個返回結果,而不能簡單的只取前面幾條結果,而後讓服務器中止發送。於是在實際開發中,儘可能保持查詢簡單且只返回必需的數據,減少通訊間數據包的大小和數量是一個很是好的習慣,這也是查詢中儘可能避免使用SELECT *以及加上LIMIT限制的緣由之一。安全

查詢緩存

在解析一個查詢語句前,若是查詢緩存是打開的,那麼MySQL會檢查這個查詢語句是否命中查詢緩存中的數據。若是當前查詢剛好命中查詢緩存,在檢查一次用戶權限後直接返回緩存中的結果。這種狀況下,查詢不會被解析,也不會生成執行計劃,更不會執行。性能優化

MySQL將緩存存放在一個引用表(不要理解成table,能夠認爲是相似於HashMap的數據結構),經過一個哈希值索引,這個哈希值經過查詢自己、當前要查詢的數據庫、客戶端協議版本號等一些可能影響結果的信息計算得來。因此兩個查詢在任何字符上的不一樣(例如:空格、註釋),都會致使緩存不會命中。服務器

若是查詢中包含任何用戶自定義函數、存儲函數、用戶變量、臨時表、mysql庫中的系統表,其查詢結果
都不會被緩存。好比函數NOW()或者CURRENT_DATE()會由於不一樣的查詢時間,返回不一樣的查詢結果,再好比包含CURRENT_USER或者CONNECION_ID()的查詢語句會由於不一樣的用戶而返回不一樣的結果,將這樣的查詢結果緩存起來沒有任何的意義。

既然是緩存,就會失效,那查詢緩存什麼時候失效呢?MySQL的查詢緩存系統會跟蹤查詢中涉及的每一個表,若是這些表(數據或結構)發生變化,那麼和這張表相關的全部緩存數據都將失效。正由於如此,在任何的寫操做時,MySQL必須將對應表的全部緩存都設置爲失效。若是查詢緩存很是大或者碎片不少,這個操做就可能帶來很大的系統消耗,甚至致使系統僵死一下子。並且查詢緩存對系統的額外消耗也不只僅在寫操做,讀操做也不例外:

  • 任何的查詢語句在開始以前都必須通過檢查,即便這條SQL語句永遠不會命中緩存
  • 若是查詢結果能夠被緩存,那麼執行完成後,會將結果存入緩存,也會帶來額外的系統消耗

基於此,咱們要知道並非什麼狀況下查詢緩存都會提升系統性能,緩存和失效都會帶來額外消耗,只有當緩存帶來的資源節約大於其自己消耗的資源時,纔會給系統帶來性能提高。但要如何評估打開緩存是否可以帶來性能提高是一件很是困難的事情,也不在本文討論的範疇內。若是系統確實存在一些性能問題,能夠嘗試打開查詢緩存,並在數據庫設計上作一些優化,好比:

  • 用多個小表代替一個大表,注意不要過分設計
  • 批量插入代替循環單條插入
  • 合理控制緩存空間大小,通常來講其大小設置爲幾十兆比較合適
  • 能夠經過SQL_CACHESQL_NO_CACHE來控制某個查詢語句是否須要進行緩存

最後的忠告是不要輕易打開查詢緩存,特別是寫密集型應用。若是你實在是忍不住,能夠將query_cache_type設置爲DEMAND,這時只有加入SQL_CACHE的查詢纔會走緩存,其餘查詢則不會,這樣能夠很是自由地控制哪些查詢須要被緩存。

固然查詢緩存系統自己是很是複雜的,這裏討論的也只是很小的一部分,其餘更深刻的話題,好比:緩存是如何使用內存的?如何控制內存的碎片化?事務對查詢緩存有何影響等等,讀者能夠自行閱讀相關資料,這裏權當拋磚引玉吧。

語法解析和預處理

MySQL經過關鍵字將SQL語句進行解析,並生成一顆對應的解析樹。這個過程解析器主要經過語法規則來驗證和解析。好比SQL中是否使用了錯誤的關鍵字或者關鍵字的順序是否正確等等。預處理則會根據MySQL規則進一步檢查解析樹是否合法。好比檢查要查詢的數據表和數據列是否存在等等。

查詢優化

通過前面的步驟生成的語法樹被認爲是合法的了,而且由優化器將其轉化成查詢計劃。多數狀況下,一條查詢能夠有不少種執行方式,最後都返回相應的結果。優化器的做用就是找到這其中最好的執行計劃。

MySQL使用基於成本的優化器,它嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。在MySQL能夠經過查詢當前會話的last_query_cost的值來獲得其計算當前查詢的成本。

mysql> select * from t_message limit 10;
...省略結果集

mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+

示例中的結果表示優化器認爲大概須要作6391個數據頁的隨機查找才能完成上面的查詢。這個結果是根據一些列的統計信息計算得來的,這些統計信息包括:每張表或者索引的頁面個數、索引的基數、索引和數據行的長度、索引的分佈狀況等等。

有很是多的緣由會致使MySQL選擇錯誤的執行計劃,好比統計信息不許確、不會考慮不受其控制的操做成本(用戶自定義函數、存儲過程)、MySQL認爲的最優跟咱們想的不同(咱們但願執行時間儘量短,但MySQL值選擇它認爲成本小的,但成本小並不意味着執行時間短)等等。

MySQL的查詢優化器是一個很是複雜的部件,它使用了很是多的優化策略來生成一個最優的執行計劃:

  • 從新定義表的關聯順序(多張表關聯查詢時,並不必定按照SQL中指定的順序進行,但有一些技巧能夠指定關聯順序)
  • 優化MIN()MAX()函數(找某列的最小值,若是該列有索引,只須要查找B+Tree索引最左端,反之則能夠找到最大值,具體原理見下文)
  • 提早終止查詢(好比:使用Limit時,查找到知足數量的結果集後會當即終止查詢)
  • 優化排序(在老版本MySQL會使用兩次傳輸排序,即先讀取行指針和須要排序的字段在內存中對其排序,而後再根據排序結果去讀取數據行,而新版本採用的是單次傳輸排序,也就是一次讀取全部的數據行,而後根據給定的列排序。對於I/O密集型應用,效率會高不少)

隨着MySQL的不斷髮展,優化器使用的優化策略也在不斷的進化,這裏僅僅介紹幾個很是經常使用且容易理解的優化策略,其餘的優化策略,你們自行查閱吧。

查詢執行引擎

在完成解析和優化階段之後,MySQL會生成對應的執行計劃,查詢執行引擎根據執行計劃給出的指令逐步執行得出結果。整個執行過程的大部分操做均是經過調用存儲引擎實現的接口來完成,這些接口被稱爲handler API。查詢過程當中的每一張表由一個handler實例表示。實際上,MySQL在查詢優化階段就爲每一張表建立了一個handler實例,優化器能夠根據這些實例的接口來獲取表的相關信息,包括表的全部列名、索引統計信息等。存儲引擎接口提供了很是豐富的功能,但其底層僅有幾十個接口,這些接口像搭積木同樣完成了一次查詢的大部分操做。

返回結果給客戶端

查詢執行的最後一個階段就是將結果返回給客戶端。即便查詢不到數據,MySQL仍然會返回這個查詢的相關信息,好比該查詢影響到的行數以及執行時間等等。

若是查詢緩存被打開且這個查詢能夠被緩存,MySQL也會將結果存放到緩存中。

結果集返回客戶端是一個增量且逐步返回的過程。有可能MySQL在生成第一條結果時,就開始向客戶端逐步返回結果集了。這樣服務端就無須存儲太多結果而消耗過多內存,也可讓客戶端第一時間得到返回結果。須要注意的是,結果集中的每一行都會以一個知足①中所描述的通訊協議的數據包發送,再經過TCP協議進行傳輸,在傳輸過程當中,可能對MySQL的數據包進行緩存而後批量發送。

回頭總結一下MySQL整個查詢執行過程,總的來講分爲6個步驟:

  • 客戶端向MySQL服務器發送一條查詢請求
  • 服務器首先檢查查詢緩存,若是命中緩存,則馬上返回存儲在緩存中的結果。不然進入下一階段
  • 服務器進行SQL解析、預處理、再由優化器生成對應的執行計劃
  • MySQL根據執行計劃,調用存儲引擎的API來執行查詢
  • 將結果返回給客戶端,同時緩存查詢結果

性能優化建議

看了這麼多,你可能會期待給出一些優化手段,是的,下面會從3個不一樣方面給出一些優化建議。但請等等,還有一句忠告要先送給你:不要聽信你看到的關於優化的「絕對真理」,包括本文所討論的內容,而應該是在實際的業務場景下經過測試來驗證你關於執行計劃以及響應時間的假設。

Scheme設計與數據類型優化

選擇數據類型只要遵循小而簡單的原則就好,越小的數據類型一般會更快,佔用更少的磁盤、內存,處理時須要的CPU週期也更少。越簡單的數據類型在計算時須要更少的CPU週期,好比,整型就比字符操做代價低,於是會使用整型來存儲ip地址,使用DATETIME來存儲時間,而不是使用字符串。

這裏總結幾個可能容易理解錯誤的技巧:

  • 一般來講把可爲NULL的列改成NOT NULL不會對性能提高有多少幫助,只是若是計劃在列上建立索引,就應該將該列設置爲NOT NULL
  • 對整數類型指定寬度,好比INT(11),沒有任何卵用。INT使用32位(4個字節)存儲空間,那麼它的表示範圍已經肯定,因此INT(1)和INT(20)對於存儲和計算是相同的。
  • UNSIGNED表示不容許負值,大體可使正數的上限提升一倍。好比TINYINT存儲範圍是-128 ~ 127,而UNSIGNED TINYINT存儲的範圍倒是0 - 255。
  • 一般來說,沒有太大的必要使用DECIMAL數據類型。即便是在須要存儲財務數據時,仍然可使用BIGINT。好比須要精確到萬分之一,那麼能夠將數據乘以一百萬而後使用BIGINT存儲。這樣能夠避免浮點數計算不許確和DECIMAL精確計算代價高的問題。
  • TIMESTAMP使用4個字節存儲空間,DATETIME使用8個字節存儲空間。於是,TIMESTAMP只能表示1970 - 2038年,比DATETIME表示的範圍小得多,並且TIMESTAMP的值因時區不一樣而不一樣。
  • 大多數狀況下沒有使用枚舉類型的必要,其中一個缺點是枚舉的字符串列表是固定的,添加和刪除字符串(枚舉選項)必須使用ALTER TABLE(若是隻是在列表末尾追加元素,不須要重建表)。
  • schema的列不要太多。緣由是存儲引擎的API工做時須要在服務器層和存儲引擎層之間經過行緩衝格式拷貝數據,而後在服務器層將緩衝內容解碼成各個列,這個轉換過程的代價是很是高的。若是列太多而實際使用的列又不多的話,有可能會致使CPU佔用太高。
  • 大表ALTER TABLE很是耗時,MySQL執行大部分修改表結果操做的方法是用新的結構建立一個張空表,從舊錶中查出全部的數據插入新表,而後再刪除舊錶。尤爲當內存不足而表又很大,並且還有很大索引的狀況下,耗時更久。固然有一些奇技淫巧能夠解決這個問題,有興趣可自行查閱。

建立高性能索引

索引是提升MySQL查詢性能的一個重要途徑,但過多的索引可能會致使太高的磁盤使用率以及太高的內存佔用,從而影響應用程序的總體性能。應當儘可能避免過後纔想起添加索引,由於過後可能須要監控大量的SQL才能定位到問題所在,並且添加索引的時間確定是遠大於初始添加索引所須要的時間,可見索引的添加也是很是有技術含量的。

接下來將向你展現一系列建立高性能索引的策略,以及每條策略其背後的工做原理。但在此以前,先了解與索引相關的一些算法和數據結構,將有助於更好的理解後文的內容。

索引相關的數據結構和算法

一般咱們所說的索引是指B-Tree索引,它是目前關係型數據庫中查找數據最爲經常使用和有效的索引,大多數存儲引擎都支持這種索引。使用B-Tree這個術語,是由於MySQL在CREATE TABLE或其它語句中使用了這個關鍵字,但實際上不一樣的存儲引擎可能使用不一樣的數據結構,好比InnoDB就是使用的B+Tree

B+Tree中的B是指balance,意爲平衡。須要注意的是,B+樹索引並不能找到一個給定鍵值的具體行,它找到的只是被查找數據行所在的頁,接着數據庫會把頁讀入到內存,再在內存中進行查找,最後獲得要查找的數據。

在介紹B+Tree前,先了解一下二叉查找樹,它是一種經典的數據結構,其左子樹的值老是小於根的值,右子樹的值老是大於根的值,以下圖①。若是要在這課樹中查找值爲5的記錄,其大體流程:先找到根,其值爲6,大於5,因此查找左子樹,找到3,而5大於3,接着找3的右子樹,總共找了3次。一樣的方法,若是查找值爲8的記錄,也須要查找3次。因此二叉查找樹的平均查找次數爲(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而順序查找的話,查找值爲2的記錄,僅須要1次,但查找值爲8的記錄則須要6次,因此順序查找的平均查找次數爲:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,所以大多數狀況下二叉查找樹的平均查找速度比順序查找要快。

images

因爲二叉查找樹能夠任意構造,一樣的值,能夠構造出如圖②的二叉查找樹,顯然這棵二叉樹的查詢效率和順序查找差很少。若想二叉查找數的查詢性能最高,須要這棵二叉查找樹是平衡的,也即平衡二叉樹AVL樹)。

平衡二叉樹首先須要符合二叉查找樹的定義,其次必須知足任何節點的兩個子樹的高度差不能大於1。顯然圖②不知足平衡二叉樹的定義,而圖①是一課平衡二叉樹。平衡二叉樹的查找性能是比較高的(性能最好的是最優二叉樹),查詢性能越好,維護的成本就越大。好比圖①的平衡二叉樹,當用戶須要插入一個新的值9的節點時,就須要作出以下變更。

images

經過一次左旋操做就將插入後的樹從新變爲平衡二叉樹是最簡單的狀況了,實際應用場景中可能須要旋轉屢次。至此咱們能夠考慮一個問題,平衡二叉樹的查找效率還不錯,實現也很是簡單,相應的維護成本還能接受,爲何MySQL索引不直接使用平衡二叉樹?

隨着數據庫中數據的增長,索引自己大小隨之增長,不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級。能夠想象一下一棵幾百萬節點的二叉樹的深度是多少?若是將這麼大深度的一顆二叉樹放磁盤上,每讀取一個節點,須要一次磁盤的I/O讀取,整個查找的耗時顯然是不可以接受的。那麼如何減小查找過程當中的I/O存取次數?

一種行之有效的解決方法是減小樹的深度,將二叉樹變爲m叉樹(多路搜索樹),而B+Tree就是一種多路搜索樹。理解B+Tree時,只須要理解其最重要的兩個特徵便可:第一,全部的關鍵字(能夠理解爲數據)都存儲在葉子節點Leaf Page),非葉子節點(Index Page)並不存儲真正的數據,全部記錄節點都是按鍵值大小順序存放在同一層葉子節點上。其次,全部的葉子節點由指針鏈接。以下圖爲高度爲2的簡化了的B+Tree

images

怎麼理解這兩個特徵?MySQL將每一個節點的大小設置爲一個頁的整數倍(緣由下文會介紹),也就是在節點空間大小必定的狀況下,每一個節點能夠存儲更多的內結點,這樣每一個結點能索引的範圍更大更精確。全部的葉子節點使用指針連接的好處是能夠進行區間訪問,好比上圖中,若是查找大於20而小於30的記錄,只須要找到節點20,就能夠遍歷指針依次找到2五、30。若是沒有連接指針的話,就沒法進行區間查找。這也是MySQL使用B+Tree做爲索引存儲結構的重要緣由。

MySQL爲什麼將節點大小設置爲頁的整數倍,這就須要理解磁盤的存儲原理。磁盤自己存取就比主存慢不少,在加上機械運動損耗(特別是普通的機械硬盤),磁盤的存取速度每每是主存的幾百萬分之一,爲了儘可能減小磁盤I/O,磁盤每每不是嚴格按需讀取,而是每次都會預讀,即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存,預讀的長度通常爲頁的整數倍。

頁是計算機管理存儲器的邏輯塊,硬件及OS每每將主存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(許多OS中,頁的大小一般爲4K)。主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,而後一塊兒返回,程序繼續運行。

MySQL巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次I/O就能夠徹底載入。爲了達到這個目的,每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了讀取一個節點只需一次I/O。假設B+Tree的高度爲h,一次檢索最多須要h-1次I/O(根節點常駐內存),複雜度O(h) = O(logmN)。實際應用場景中,M一般較大,經常超過100,所以樹的高度通常都比較小,一般不超過3。

最後簡單瞭解下B+Tree節點的操做,在總體上對索引的維護有一個大概的瞭解,雖然索引能夠大大提升查詢效率,但維護索引仍要花費很大的代價,所以合理的建立索引也就尤其重要。

仍以上面的樹爲例,咱們假設每一個節點只能存儲4個內節點。首先要插入第一個節點28,以下圖所示。

images

  • leaf page和index page都沒有滿

接着插入下一個節點70,在Index Page中查詢後得知應該插入到50 - 70之間的葉子節點,但葉子節點已滿,這時候就須要進行也分裂的操做,當前的葉子節點起點爲50,因此根據中間值來拆分葉子節點,以下圖所示。

images

  • Leaf Page拆分

最後插入一個節點95,這時候Index Page和Leaf Page都滿了,就須要作兩次拆分,以下圖所示。

images

  • Leaf Page與Index Page拆分

拆分後最終造成了這樣一顆樹。

images

  • 最終樹

B+Tree爲了保持平衡,對於新插入的值須要作大量的拆分頁操做,而頁的拆分須要I/O操做,爲了儘量的減小頁的拆分操做,B+Tree也提供了相似於平衡二叉樹的旋轉功能。當Leaf Page已滿但其左右兄弟節點沒有滿的狀況下,B+Tree並不急於去作拆分操做,而是將記錄移到當前所在頁的兄弟節點上。一般狀況下,左兄弟會被先檢查用來作旋轉操做。就好比上面第二個示例,當插入70的時候,並不會去作頁拆分,而是左旋操做。

images

  • 左旋操做

經過旋轉操做能夠最大限度的減小頁分裂,從而減小索引維護過程當中的磁盤的I/O操做,也提升索引維護效率。須要注意的是,刪除節點跟插入節點相似,仍然須要旋轉和拆分操做,這裏就再也不說明。

高性能策略

經過上文,相信你對B+Tree的數據結構已經有了大體的瞭解,但MySQL中索引是如何組織數據的存儲呢?以一個簡單的示例來講明,假若有以下數據表:

CREATE TABLE People(
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    dob date not null,
    gender enum(`m`,`f`) not null,
    key(last_name,first_name,dob)
);

對於表中每一行數據,索引中包含了last_namefirst_namedob列的值,下圖展現了索引是如何組織數據存儲的。

images

  • 索引如何組織數據存儲,來自:高性能MySQL

MySQL不會使用索引的狀況:非獨立的列

「獨立的列」是指索引列不能是表達式的一部分,也不能是函數的參數。好比:

select * from table_name where id + 1 = 5

咱們很容易看出其等價於 id = 4,可是MySQL沒法自動解析這個表達式,使用函數是一樣的道理。

前綴索引

若是列很長,一般能夠索引開始的部分字符,這樣能夠有效節約索引空間,從而提升索引效率。

多列索引和索引順序

在多數狀況下,在多個列上創建獨立的索引並不能提升查詢性能。理由很是簡單,MySQL不知道選擇哪一個索引的查詢效率更好,因此在老版本,好比MySQL5.0以前就會隨便選擇一個列的索引,而新的版本會採用合併索引的策略。舉個簡單的例子,在一張電影演員表中,在actor_idfilm_id兩個列上都創建了獨立的索引,而後有以下查詢:

select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1
-- 老版本的MySQL會隨機選擇一個索引,但新版本作以下的優化:

select film_id,actor_id from film_actor where actor_id = 1  
union all 
select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1
  • 當出現多個索引作相交操做時(多個AND條件),一般來講一個包含全部相關列的索引要優於多個獨立索引。
  • 當出現多個索引作聯合操做時(多個OR條件),對結果集的合併、排序等操做須要耗費大量的CPU和內存資源,特別是當其中的某些索引的選擇性不高,須要返回合併大量數據時,查詢成本更高。因此這種狀況下還不如走全表掃描。

所以explain時若是發現有索引合併(Extra字段出現Using union),應該好好檢查一下查詢和表結構是否是已是最優的,若是查詢和表都沒有問題,那隻能說明索引建的很是糟糕,應當慎重考慮索引是否合適,有可能一個包含全部相關列的多列索引更適合。

前面咱們提到過索引如何組織數據存儲的,從圖中能夠看到多列索引時,索引的順序對於查詢是相當重要的,很明顯應該把選擇性更高的字段放到索引的前面,這樣經過第一個字段就能夠過濾掉大多數不符合條件的數據。

索引選擇性是指不重複的索引值和數據表的總記錄數的比值,選擇性越高查詢效率越高,由於選擇性越高的索引可讓MySQL在查詢時過濾掉更多的行。惟一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。

理解索引選擇性的概念後,就不難肯定哪一個字段的選擇性較高了,查一下就知道了,好比:

SELECT * FROM payment where staff_id = 2 and customer_id = 584

是應該建立(staff_id,customer_id)的索引仍是應該顛倒一下順序?執行下面的查詢,哪一個字段的選擇性更接近1就把哪一個字段索引前面就好。

select count(distinct staff_id)/count(*) as staff_id_selectivity,
       count(distinct customer_id)/count(*) as customer_id_selectivity,
       count(*) from payment

多數狀況下使用這個原則沒有任何問題,但仍然注意你的數據中是否存在一些特殊狀況。舉個簡單的例子,好比要查詢某個用戶組下有過交易的用戶信息:

select user_id from trade where user_group_id = 1 and trade_amount > 0

MySQL爲這個查詢選擇了索引(user_group_id,trade_amount),若是不考慮特殊狀況,這看起來沒有任何問題,但實際狀況是這張表的大多數數據都是從老系統中遷移過來的,因爲新老系統的數據不兼容,因此就給老系統遷移過來的數據賦予了一個默認的用戶組。這種狀況下,經過索引掃描的行數跟全表掃描基本沒什麼區別,索引也就起不到任何做用。

推廣開來講,經驗法則和推論在多數狀況下是有用的,能夠指導咱們開發和設計,但實際狀況每每會更復雜,實際業務場景下的某些特殊狀況可能會摧毀你的整個設計。

避免多個範圍條件

實際開發中,咱們會常用多個範圍條件,好比想查詢某個時間段內登陸過的用戶:

select user.* from user where login_time > '2017-04-01' and age between 18 and 30;

這個查詢有一個問題:它有兩個範圍條件,login_time列和age列,MySQL可使用login_time列的索引或者age列的索引,但沒法同時使用它們。

覆蓋索引

若是一個索引包含或者說覆蓋全部須要查詢的字段的值,那麼就沒有必要再回表查詢,這就稱爲覆蓋索引。覆蓋索引是很是有用的工具,能夠極大的提升性能,由於查詢只須要掃描索引會帶來許多好處:

  • 索引條目遠小於數據行大小,若是隻讀取索引,極大減小數據訪問量
  • 索引是有按照列值順序存儲的,對於I/O密集型的範圍查詢要比隨機從磁盤讀取每一行數據的IO要少的多

使用索引掃描來排序

MySQL有兩種方式能夠生產有序的結果集,其一是對結果集進行排序的操做,其二是按照索引順序掃描得出的結果天然是有序的。若是explain的結果中type列的值爲index表示使用了索引掃描來作排序。

掃描索引自己很快,由於只須要從一條索引記錄移動到相鄰的下一條記錄。但若是索引自己不能覆蓋全部須要查詢的列,那麼就不得不每掃描一條索引記錄就回表查詢一次對應的行。這個讀取操做基本上是隨機I/O,所以按照索引順序讀取數據的速度一般要比順序地全表掃描要慢。

在設計索引時,若是一個索引既可以知足排序,又知足查詢,是最好的。

只有當索引的列順序和ORDER BY子句的順序徹底一致,而且全部列的排序方向也同樣時,纔可以使用索引來對結果作排序。若是查詢須要關聯多張表,則只有ORDER BY子句引用的字段所有爲第一張表時,才能使用索引作排序。ORDER BY子句和查詢的限制是同樣的,都要知足最左前綴的要求(有一種狀況例外,就是最左的列被指定爲常數,下面是一個簡單的示例),其餘狀況下都須要執行排序操做,而沒法利用索引排序。

-- 最左列爲常數,索引:(date,staff_id,customer_id)
select  staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id

冗餘和重複索引

冗餘索引是指在相同的列上按照相同的順序建立的相同類型的索引,應當儘可能避免這種索引,發現後當即刪除。好比有一個索引(A,B),再建立索引(A)就是冗餘索引。冗餘索引常常發生在爲表添加新索引時,好比有人新建了索引(A,B),但這個索引不是擴展已有的索引(A)。

大多數狀況下都應該儘可能擴展已有的索引而不是建立新索引。但有極少狀況下出現性能方面的考慮須要冗餘索引,好比擴展已有索引而致使其變得過大,從而影響到其餘使用該索引的查詢。

刪除長期未使用的索引

按期刪除一些長時間未使用過的索引是一個很是好的習慣。

關於索引這個話題打算就此打住,最後要說一句,索引並不老是最好的工具,只有當索引幫助提升查詢速度帶來的好處大於其帶來的額外工做時,索引纔是有效的。對於很是小的表,簡單的全表掃描更高效。對於中到大型的表,索引就很是有效。對於超大型的表,創建和維護索引的代價隨之增加,這時候其餘技術也許更有效,好比分區表。最後的最後,explain後再提測是一種美德。

特定類型查詢優化

優化COUNT()查詢

COUNT()多是被你們誤解最多的函數了,它有兩種不一樣的做用,其一是統計某個列值的數量,其二是統計行數。統計列值時,要求列值是非空的,它不會統計NULL。若是確認括號中的表達式不可能爲空時,實際上就是在統計行數。最簡單的就是當使用COUNT(*)時,並非咱們所想象的那樣擴展成全部的列,實際上,它會忽略全部的列而直接統計行數。

咱們最多見的誤解也就在這兒,在括號內指定了一列卻但願統計結果是行數,並且還經常誤覺得前者的性能會更好。但實際並不是這樣,若是要統計行數,直接使用COUNT(*),意義清晰,且性能更好。

有時候某些業務場景並不須要徹底精確的COUNT值,能夠用近似值來代替,EXPLAIN出來的行數就是一個不錯的近似值,並且執行EXPLAIN並不須要真正地去執行查詢,因此成本很是低。一般來講,執行COUNT()都須要掃描大量的行才能獲取到精確的數據,所以很難優化,MySQL層面還能作得也就只有覆蓋索引了。若是不還能解決問題,只有從架構層面解決了,好比添加彙總表,或者使用redis這樣的外部緩存系統。

優化關聯查詢

在大數據場景下,表與表之間經過一個冗餘字段來關聯,要比直接使用JOIN有更好的性能。若是確實須要使用關聯查詢的狀況下,須要特別注意的是:

  • 確保ONUSING字句中的列上有索引。在建立索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候,若是優化器關聯的順序是A、B,那麼就不須要在A表的對應列上建立索引。沒有用到的索引會帶來額外的負擔,通常來講,除非有其餘理由,只須要在關聯順序中的第二張表的相應列上建立索引(具體緣由下文分析)。
  • 確保任何的GROUP BYORDER BY中的表達式只涉及到一個表中的列,這樣MySQL纔有可能使用索引來優化。

要理解優化關聯查詢的第一個技巧,就須要理解MySQL是如何執行關聯查詢的。當前MySQL關聯執行的策略很是簡單,它對任何的關聯都執行嵌套循環關聯操做,即先在一個表中循環取出單條數據,而後在嵌套循環到下一個表中尋找匹配的行,依次下去,直到找到全部表中匹配的行爲爲止。而後根據各個表匹配的行,返回查詢中須要的各個列。

太抽象了?以上面的示例來講明,好比有這樣的一個查詢:

SELECT A.xx,B.yy 
FROM A INNER JOIN B USING(c)
WHERE A.xx IN (5,6)

假設MySQL按照查詢中的關聯順序A、B來進行關聯操做,那麼能夠用下面的僞代碼表示MySQL如何完成這個查詢:

outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6);
outer_row = outer_iterator.next;
while(outer_row) {
    inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c;
    inner_row = inner_iterator.next;
    while(inner_row) {
        output[inner_row.yy,outer_row.xx];
        inner_row = inner_iterator.next;
    }
    outer_row = outer_iterator.next;
}

能夠看到,最外層的查詢是根據A.xx列來查詢的,A.c上若是有索引的話,整個關聯查詢也不會使用。再看內層的查詢,很明顯B.c上若是有索引的話,可以加速查詢,所以只須要在關聯順序中的第二張表的相應列上建立索引便可。

優化LIMIT分頁

當須要分頁操做時,一般會使用LIMIT加上偏移量的辦法實現,同時加上合適的ORDER BY字句。若是有對應的索引,一般效率會不錯,不然,MySQL須要作大量的文件排序操做。

一個常見的問題是當偏移量很是大的時候,好比:LIMIT 10000 20這樣的查詢,MySQL須要查詢10020條記錄而後只返回20條記錄,前面的10000條都將被拋棄,這樣的代價很是高。

優化這種查詢一個最簡單的辦法就是儘量的使用覆蓋索引掃描,而不是查詢全部的列。而後根據須要作一次關聯查詢再返回全部的列。對於偏移量很大時,這樣作的效率會提高很是大。考慮下面的查詢:

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

若是這張表很是大,那麼這個查詢最好改爲下面的樣子:

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

這裏的延遲關聯將大大提高查詢效率,讓MySQL掃描儘量少的頁面,獲取須要訪問的記錄後在根據關聯列回原表查詢所須要的列。

有時候若是可使用書籤記錄上次取數據的位置,那麼下次就能夠直接從該書籤記錄的位置開始掃描,這樣就能夠避免使用OFFSET,好比下面的查詢:

SELECT id FROM t LIMIT 10000, 10;
-- 改成:
SELECT id FROM t WHERE id > 10000 LIMIT 10;

其餘優化的辦法還包括使用預先計算的彙總表,或者關聯到一個冗餘表,冗餘表中只包含主鍵列和須要作排序的列。

優化UNION

MySQL處理UNION的策略是先建立臨時表,而後再把各個查詢結果插入到臨時表中,最後再來作查詢。所以不少優化策略在UNION查詢中都沒有辦法很好的時候。常常須要手動將WHERELIMITORDER BY等字句「下推」到各個子查詢中,以便優化器能夠充分利用這些條件先優化。

除非確實須要服務器去重,不然就必定要使用UNION ALL,若是沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會致使整個臨時表的數據作惟一性檢查,這樣作的代價很是高。固然即便使用ALL關鍵字,MySQL老是將結果放入臨時表,而後再讀出,再返回給客戶端。雖然不少時候沒有這個必要,好比有時候能夠直接把每一個子查詢的結果返回給客戶端。

結語

理解查詢是如何執行以及時間都消耗在哪些地方,再加上一些優化過程的知識,能夠幫助你們更好的理解MySQL,理解常見優化技巧背後的原理。但願本文中的原理、示例可以幫助你們更好的將理論和實踐聯繫起來,更多的將理論知識運用到實踐中。

其餘也沒啥說的了,給你們留兩個思考題吧,能夠在腦殼裏想一想答案,這也是你們常常掛在嘴邊的,但不多有人會思考爲何?

有很是多的程序員在分享時都會拋出這樣一個觀點:儘量不要使用存儲過程,存儲過程很是不容易維護,也會增長使用成本,應該把業務邏輯放到客戶端。既然客戶端都能幹這些事,那爲何還要存儲過程?

JOIN自己也挺方便的,直接查詢就行了,爲何還須要視圖呢?

See

All rights reserved

相關文章
相關標籤/搜索