架構設計:系統存儲(7)——MySQL數據庫性能優化(3)

(接上文《架構設計:系統存儲(6)——MySQL數據庫性能優化(2)》)html

四、影響SQL性能的要素

MySQL數據庫的性能不止受到性能參數和底層硬件條件的影響,在這兩個條件必定的狀況下,開發人員對SQL語句的優化能力更能影響MySQL數據庫的性能。因爲MySQL中不一樣數據庫引擎對SQL語句的處理過程不盡相同,因此對SQL語句的優化就必定要首先肯定使用的數據庫引擎的類型。例如MyISAM引擎中統計某一個數據表的總行數時,只須要讀取出已保存好的數據總行數就OK了。但InnoDB引擎要完成這個動做,就必須進行table scan/index scan,而是否爲被掃描的字段建立了索引又直接影響了掃描速度。本文咱們和讀者一塊兒來討論一下InnoDB數據引擎下SQL語句常見的工做方式和優化規則。mysql

4-一、索引

咱們都知道不管使用哪一種主流的關係型數據庫,爲SQL查找語句所依據的字段建立索引要比不建立索引時的性能高出幾個數量級(固然這也要看SELECT查詢語句的具體寫法)。那麼爲何會出現這樣的狀況呢?咱們又依據什麼樣的原理來建立數據庫的索引呢?本小節將首先爲讀者進行原理性描述。web

4-1-一、B樹

一樣因爲本文的定位,因此咱們並不會討論怎樣使用腳本建立索引等基本操做問題。而是直接進行InnoDB數據庫引擎中索引原理的講述。InnoDB中數據庫字段的索引採用樹結構進行組織,這種樹本質上是爲了解決數據檢索問題的平衡N階樹,又被稱爲B樹。B樹及其變體是大學《數據結構》課程中的基礎知識,本人雖然工做許多年但始終對《數據結構》這門課程中的主要知識爛熟於心,並認爲它和《 離散數學》同樣已經成爲筆者大學時期學習過的,對筆者實際工做幫助最大的兩門課程。sql

爲了幫助讀者回憶起B樹及其變體的基本結構,也爲了後續內容可以正常鋪開。咱們很是須要使用至關的篇幅對它進行介紹。那麼首先使用下圖回答樹結構的幾個基本概念:節點、深度、子樹等。數據庫

這裏寫圖片描述

B樹是一顆平衡的多叉檢索樹,它具備如下性質:編程

  • 所謂檢索樹是指這樣的樹:樹中任意非葉子節點A做爲根節點的子樹,其左子樹上節點中的元素值均小於或等於節點A中元素的值;其右子樹上節點中的元素值均大於或等於節點A中元素的值。檢索樹又稱爲排序樹、有序樹,若是將檢索樹降維成表結構則一樣可使用二分查找法進行節點檢索,且時間複雜度基本不變。緩存

  • 若是不加任何構造限制,那麼在樹結構中檢索元素的時間複雜度可能爲O(n)。這顯然失去了檢索樹的意義,若是一顆檢索樹可以保證樹的高度H限制在節點數N的對數階範圍內(H=O(logn)),這樣的檢索樹就稱爲平衡樹。在編程實踐中只要保證樹中任意兩個子樹深度差的絕對值不大於1,就能夠保證前述條件成立。另外B樹中對深度差絕對值作了更嚴格的規定,即全部葉子結點都位於同一深度。性能優化

  • 一顆B樹的非葉子節點可以最多關聯的子節點數量稱爲階數。B樹中的階數至少爲3,由於當階數爲2時B樹進行節點分裂就可能會出現某葉子節點沒有任何元素的狀況。session

  • 樹中每一個非根節點所包含的元素個數 j 知足:(N/2) - 1 <= j <= N - 1,其中N表示B樹的階數。例如階數爲3的B樹每個非葉子結點可以存儲的元素個數可能爲 0個、1個和2個(但0個元素沒有任何檢索意義,還會形成樹中任意兩個子樹深度差的絕對值改變)。數據結構

  • 樹中一個節點可關聯的子節點數量比以上文字中提到的元素最大個數多1。也就是說階數爲3的B樹每一個節點最多可關聯3個子節點。

這裏寫圖片描述

上圖展現了一顆3階B樹,它的每一個節點最多能夠有3個子節點,而且每一個子節點中最多有2個元素。能夠觀察到B樹知足檢索樹的基本規則:凡是比給定元素值大的全部元素,都做爲該元素的子元素排列在該元素的左子樹上;凡是比給定元素值大的全部元素,都做爲該元素的子元素排列在該元素的右子樹上。這樣一來開發人員就可使用和二分查找法相似的查找方式定位要查詢的元素,或者在插入一個新元素前定位到新元素將要進行插入的位置。

下圖演示了在B樹中依次添加元素時的分裂和節點間關聯過程,這些元素的值依次增大分別是:三、五、七、九、1四、1三、1五、1六、1八、2二、2五、3一、33。在實際應用開發中,雖然咱們並不能保證插入B樹的元素值都是增長的,可是對B樹的插入操做過程倒是相同的(二者的區別只是定位的將要插入新元素的位置不同):

這裏寫圖片描述

4-1-二、變體B+樹

B樹的優勢很明顯:不管進行新元素的插入定位仍是進行指定元素的查找,均可以快速完成這些定位/查找動做,其查詢性能的時間複雜度至關於二分查找法(O(log(n)))。可是B樹的缺點也很明顯,首先插入新元素時可能涉及到樹深度的改變,固然這個問題能夠經過增長B樹的階數來解決。也就是讓每個節點能夠擁有更多的子節點,這樣就能夠在存儲元素總量不變的狀況下減小樹的理論深度,從而減小發生樹深度改變的狀況,

另外一個問題就稍微嚴重一些了,那就是B樹並不適合管理InnoDB引擎中的數據(這個在後文會進行說明)。還好B樹結構有一個變體結構,稱爲B+樹。二者最大的區別是,後者在B樹的結構基礎上擴展出了一個鏈表存儲結構,而且在樹的葉子節點對非葉子結點元素進行了冗餘存儲。以下圖所示:

這裏寫圖片描述

B樹和B+樹在新元素插入、元素刪除、元素查找等基本處理方式上沒有太大的區別。可是B+樹的兩個典型的結構變更恰好能夠改進樹結構在InnoDB引擎中的應用:

  • 因爲B+樹的葉子節點存儲了非葉子節點的冗餘元素,因此咱們能夠在非葉子節點只記錄某條數據的索引信息,而在葉子節點記錄具體的數據信息。那麼MySQL數據庫就能夠在InnoDB引擎啓動時就加載B+樹的非葉子節點到內存特定區域,這樣作的最大好處是能夠在內存空間和查找速度兩個維度上找到最好的平衡點。

  • 特別注意的是,實際在InnoDB引擎中的B+樹結構,其葉子節點並非存儲一行數據(要真是這樣,這顆B+樹不知道須要有多大。。。),而是和Data Page做爲對應。在以前介紹InnoDB引擎的文章中已經說明過數據庫中Page的概念。Page是InnoDB引擎中最基本的數據操做單元,不管是InnoDB從磁盤上讀取數據仍是改變數據,都以Page做爲操做單位。一樣,InnoDB引擎中的索引結構也以Page爲單位在葉子節點關聯具體數據。

  • 另外B+樹在最底層將全部葉子節點串成了一個鏈表結構(不用擔憂某個葉子節點沒有任何元素,由於B+樹遵循全部B樹的基本約束),這樣一來在進行數據查找時就可使用表結構進行元素的依次查找,而無需再進行樹的遍歷操做。實際上在InnoDB引擎中每個葉子節點都是一個Page信息,構成鏈表結構後就能夠檢索每個Page的上一個Page和下一個Page信息,這剛好也是InnoDB引擎中預讀功能的實現基礎。

4-1-三、InnoDB中的索引類型

InnoDB數據引擎使用B+樹構造索引結構,其中的索引類型依據參與檢索的字段不一樣能夠分爲主索引和非主索引;依據B+樹葉子節點上真實數據的組織狀況又能夠分爲聚族索引和非聚族索引。每個索引B+樹結構都會有一個獨立的存儲區域來存放,而且在須要進行檢索時將這個結構加載到內存區域。真實狀況是InnoDB引擎會加載索引B+樹結構到內存的Buffer Pool區域。

  • 聚簇索引(彙集索引)

    聚簇索引指的是這樣的數據組織結構:索引B+樹的每一個葉子節點直接對應了真實的Data Page。而且B+樹全部的葉子節點在最底層共同描述了一個能夠直接進行行數據順序掃描的Data Page結構。以下圖所示:

    這裏寫圖片描述

    InnoDB引擎在組織索引和數據時,就是經過聚簇索引檢索具體Data Page。而聚簇索引B+樹的非葉子節點通常由數據表中的主鍵負責構造(固然也可能不是主鍵,這個後文會進行說明)。

  • 主索引(主鍵索引/一級索引)

    基於InnoDB引擎工做的每一張數據表都須要有一個主索引,這是由於上一段文字中提到的InnoDB引擎須要使用聚簇索引查找到具體的Data Page,而工做在InnoDB引擎下的數據表有且只有主索引採用聚簇索引的方式組織數據。也就是說主索引B+樹的葉子節點都對應了真實的Data Page信息。

    主索引在數據表的索引列表中使用PRIMARY關鍵字進行標識,通常來講是數據表的主鍵字段(也有多是複合主鍵)。若是開發人員刪除了InnoDB引擎中某張數據表的主索引,那麼這個數據表將自行尋找一個非空且帶有惟一約束的字段做爲主索引。若是仍是沒有找到那樣的字段,InnoDB引擎將使用一個隱含字段做爲主索引(ROWID)。

    B+樹的構造特性在這裏就獲得了充分利用,由於只須要將主索引B+樹的非葉子節點加載到內存中。當檢索請求須要讀取某一個具體的Data Page時,再從磁盤上進行讀取。還記得在以前的文章中提到的預讀操做嗎?B+樹最底層葉子節點組成的鏈表結構,讓InnoDB引擎可以輕鬆進行臨近的Data Page的讀取——若是參數設定了須要那樣作的話。

  • 非聚簇索引(非彙集索引)

    非聚族索引首先也是一顆B+樹,只是非聚簇索引的葉子節點再也不關聯具體的Data Page信息,而是關聯另外一個索引值。InnoDB引擎下工做的每個數據表雖然都只有一個聚簇索引,那就是它的主索引。可是每一張數據表能夠有多個非聚簇索引,然後者的葉子節點所有存儲着對應的數據主鍵信息(或者其它能夠在聚簇索引中進行檢索的關鍵值)。

    這裏寫圖片描述

    注意上圖所示的B+樹的葉子節點再也不關聯具體的Data Page信息,而只是關聯了構成聚簇索引非葉子結點的主鍵信息。

  • 非主索引(輔助索引/二級索引)

    數據表索引列表中除去主索引之外的其它索引都稱爲非主索引。非主索引都是使用非聚簇索引方式組織數據,也就是說它們其實是對聚簇索引進行檢索的數據結構依據。

    例如當開發人員建立了一個以字段A做爲索引的非聚簇索引結構,而且在SQL中使用字段A做爲查詢條件執行檢索時。InnoDB會首先使用非聚簇索引檢索出對應的主鍵信息,而後再經過主索引檢索這個主鍵對應的數據。

    這裏寫圖片描述

關於索引和執行計劃調整的介紹,將在下一篇文章中提到。

4-二、Query Cache

爲了加快查詢語句的執行性能,從MySQL早期的版本開始就提供了一種名叫Query Cache的緩存技術。這個緩存技術和技術人員使用哪一種數據庫引擎無關,它徹底獨立工做於各類數據庫引擎的上層,並使用獨立的內存區域。

Query Cache的工做原理描述起來也比較簡單,當某一個客戶端鏈接(session)進行SQL查詢並獲得返回信息時,MySQL數據庫除了將查詢結果返回給客戶端外,還在特定的內存區域緩存這條SQL查詢語句的結果,以便包括這個客戶端在內的全部客戶的再次執行相同查詢請求時,MySQL可以直接從緩存區返回結果。這裏有兩個關鍵點須要明確:

  • 什麼是「相同的查詢語句」?Query Cache使用K-V結構對查詢結果進行記錄,其中的K就是查詢語句自己(固然還要附加上諸如database name這樣的關鍵信息)。因此「select * from A」和「select * from a」這樣的語句將被當作是兩條不一樣的查詢語句。「select * from A」和「select * from A」也將被視爲兩條不一樣的查詢語句(空格數量不同)。

  • 怎樣避免「緩存數據不一致」的問題?
    一旦被緩存的查詢結果所涉及的數據表發生了「寫」操做,那麼不管「寫」操做自己是否影響到被緩存的數據,涉及到數據表的全部緩存數據都將被清除。這種簡單暴力的處理方式,不只繞過了數據一致性問題,還節約了寶貴的時間——由於在大多數數據庫應用中,讀請求是遠遠多餘寫請求的。若是您所在團隊開發的應用會使MySQL數據庫讀寫請求比例達到或查過1:1,那麼使用Query Cache就沒有什麼意義,建議直接關閉。

4-2-一、Query Cache基本設置

您能夠經過「show variables like ‘query_cache%’」語句查詢當前爲MySQL服務設定的和Query Cache相關的參數值。

# show variables like 'query_cache%';

query_cache_limit                   1048576
query_cache_min_res_unit            4096
query_cache_size                    0
query_cache_type                    OFF
query_cache_wlock_invalidate        OFF

這些設置參數的定義能夠簡單描述以下:

  • query_cache_size:這是參數設置了MySQL服務中Query Cache的全局大小,單位爲byte。該參數在MySQL version 5.5及之後版本中的默認值都爲0,也就是說若是在這些版本中要使用Query Cache則須要本身設置Query Cache的大小。query_cache_size不該該這是太大(最大支持256M),這是由於當某張數據表進行寫操做時,MySQL服務須要從Query Cache抹去的相關數據也就越多,反而會增長耗時。query_cache_size設置爲33554432(32M)是比較好的。

  • query_cache_limit:該參數設置單條查詢語句容許緩存到Query Cache中的最大結果容量值,1048576(1M)是它的默認值。也就是說若是查詢語句返回的查詢結果集合大於1M,則這個查詢結果集合不會緩存到Query Cache區域。

  • query_cache_min_res_unit:該參數設置Query Cache每次分配內存的最小大小,默認值爲4096(4KB)。

  • query_cache_type:注意,既是單獨設置query_cache_size爲0,也不會使MySQL服務關閉Query Cache功能。必定要設置query_cache_type參數爲0(OFF)才行。另外當該參數值爲1(ON)時,表明開啓Query Cache功能,此時必須在SQL查詢語句中明確使用SQL_NO_CACHE,才能關閉這條查詢語句的Query Cache功能;該參數的值還能夠爲2(DEMAND),此時只有當SQL查詢語句明確使用SQL_CACHE關鍵字,才能讓這條查詢語句使用Query Cache功能。

  • query_cache_wlock_invalidate:該參數設置Query Cache中數據的失效時刻(很是重要)。若是該值爲1(ON),則在數據表被寫鎖定的同時該表中數據涉及的全部Query Cache都將失效;若是該值爲0(OFF),則表示在數據表寫鎖定的同時,Query Cache中該數據表的相關數據都還繼續有效。

您還能夠經過「show status like ‘Qcache%’」語句查詢當前MySQL服務中Query Cache的工做狀態

# show status like 'Qcache%' Qcache_free_blocks 0 Qcache_free_memory 0 Qcache_hits 0 Qcache_inserts 0 Qcache_lowmem_prunes 0 Qcache_not_cached 0 Qcache_queries_in_cache 0 Qcache_total_blocks 0

各位讀者看到以上示例中全部和Query Cache相關的狀態值都爲0,這是由於在演示的MySQL服務中默認關閉了Query Cache功能(主要是設置了query_cache_type的值爲0)。不過以上展現的Query Cache狀態信息中一些狀態項仍是要進行說明:

  • Qcache_free_memory:該指標說明了當前Query Cache專用內存區域還有多少剩餘內存。

  • Qcache_hits:該指標說明當前Query Cache從MySQL服務啓動到如今的命中次數。

  • Qcache_lowmem_prunes:該指標說明由於Query Cache內存不足而被清除的查詢結果數量。

其它的狀態項可參見MySQL的官方文檔《The MySQL Query Cache

4-2-二、Query Cache的侷限性和使用建議

爲何MySQL Version 5.5及之後的版本會默認關閉Query Cache功能呢?這至少說明官方並不建議在任何場景下都是用Query Cache功能,甚至是大多數場景下。首先,Query Cache存在功能侷限性:

  • 早期版本(Version 5.1)的Query Cache功能並不支持變量綁定,也就是說相似「select * from A where field = ?」這樣的SQL查詢結果不會被放入Query Cache中。

  • 存儲過程、觸發器等基於數據庫引擎類型工做的特定功能,若是其中使用了查詢語句,這些查詢語句的結果也不會放入Query Cache中。

  • 複雜的SQL查詢中,每每包含多個子句。這些子句的查詢結果可以被放入Query Cache中。可是用於包含這些子句的外部查詢結果卻不能放入Query Cache。

以上提到的Query Cache功能侷限性在每次MySQL版本升級的過程當中,MySQL開發團隊都逐漸進行了調整,因此這寫功能性限制並非什麼太大的問題。例如以上說到的在存儲過程當中的SQL查詢不會加入到Query Cache中,這個實際上就不是什麼大問題,目前來看業務系統中業務邏輯處理部分還都是放在上層業務代碼中來解決,使用複雜存儲過來處理業務邏輯的狀況很少見。MySQL官方默認關閉Query Cache主要仍是由於Query Cache的性能侷限性:

  • 「Waiting on query cache mutex」這種錯誤是典型的使用Query Cache不當所引發的錯誤。因爲Query Cache設計的暴力清除策略,致使只要有數據表進行寫操做,Query Cache中和這個數據表相關的全部結果都要失效的現象。全部須要從Query Cache中讀取相關數據的客戶端session就要等待數據清除完畢,因此就會出現以上錯誤提示。

  • 若是這時query_cache_size設置得過大,反而會加重這個問題的嚴重程度。由於過大的Query Cache區域意味着可能存儲了和這個被寫操做關聯的數據表的更多查詢結果集,也就須要更多時間去清除數據。

  • 若是這張數據表又是寫密集度很是高的數據表,那麼這個問題會更加嚴重。由於Query Cache中相關數據會被頻繁的擦除、重寫。客戶端session也會不停的進入鎖定等待狀態。

在實際業務應用中,筆者並不建議直接關閉Query Cache。而是建議在將query_cache_type參數設置爲2(DEMAND)並分配不大的內存總空間(query_cache_size 設置爲16MB足夠了)的前提下,由業務層代碼顯式控制Query Cache的使用

只有知足如下全部特色的SQL查詢操做才建議顯示開啓Query Cache功能:寫操做並不密集的數據表、讀寫操做比最好大於10:1(或者根據讀者本身的業務特性規定的更大比值)。畢竟只有業務層才清楚哪些數據表的讀寫操做比大於10:1,而且寫操做並不時常進行。而知足以上操做特性的數據表一般都是基礎性碼錶:例如行政區域表、電話分區表、身份證分區表、車輛號牌表。

對於複雜的SQL查詢、讀寫比不大的數據表、寫操做頻繁或者寫操做併發特別大的數據表並不建議開啓Query Cache功能。例如訂單表、庫存物品表、車輛承運表、評論信息表等業務寫操做頻繁的數據表。

相關文章
相關標籤/搜索