mysql索引之聚簇索引與非聚簇索引

1 數據結構及算法基礎

1.1 索引的本質

官方定義:索引(Index)是幫助MySQL高效獲取數據的數據結構
本質:索引是數據結構php

查詢是數據庫的最主要功能之一。咱們都但願查詢速度能儘量快,所以數據庫系統的設計者會從查詢算法角度優化css

最基本的查詢算法固然是順序查找(linear search),這種複雜度爲O(n)的算法在數據量很大時顯然是糟糕的
好在CS的發展提供了不少更優秀的查找算法,如二分查找(binary search)、二叉樹查找(binary tree search)等
稍微分析一下會發現,每種查找算法都只能應用於特定數據結構,如二分查找要求被檢索數據有序,而二叉樹查找只能應用於二叉查找樹,但數據自己的組織結構不可能徹底知足各類數據結構(例如,理論上不可能同時將兩列都按順序進行組織)
因此,在數據以外,數據庫系統還維護着知足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就能夠在這些數據結構上實現高級查找算法
這種ADT,就是索引
java

 
圖1 一個例子

圖1展現了一種可能的索引方式
左邊是數據表,兩列14條記錄,最左邊是數據記錄的物理地址(注意邏輯上相鄰的記錄在磁盤上並不必定物理相鄰)
爲加快Col2的查找,可維護一個右邊所示二叉查找樹,每一個節點分別包含索引鍵值及一個指向對應數據記錄物理地址的指針,這樣就能夠運用二叉查找在O(log2 N)內取到相應數據

 

雖然這是一個貨真價實的索引,但實際數據庫系統幾乎沒有使用二叉查找樹或其進化品種紅黑樹(red-black tree)實現node

1.2 經典經常使用索引類型:B Tree和B+Tree

目前大部分數據庫系統及文件系統都採用B Tree或其變種B+Tree做爲索引結構mysql

1.2.1 B Tree

定義數據記錄爲一個二元組[key, data]web

  • key爲記錄的鍵值,對於不一樣數據記錄,key互不相同
  • data爲數據記錄除key外的數據

B Tree有以下特色:算法

  • d爲大於1的一個正整數,稱爲B-Tree的度
  • h爲一個正整數,稱爲B-Tree的高度
  • 每一個非葉節點由n-1個key和n個指針組成,其中d<=n<=2d
  • 每一個葉節點最少包含一個key和兩個指針,最多包含2d-1個key和2d個指針,葉節點的指針均爲null
  • 全部葉節點具備相同的深度,等於樹高h
  • key和指針互相間隔,節點兩端是指針
  • 一個節點中的key從左到右非遞減排列
  • 全部節點組成樹結構
  • 每一個指針要麼爲null,要麼指向另一個節點
  • 若是某個指針在節點node最左邊且不爲null,則其指向節點的全部key小於>v(key1),v(key1)爲node的第一個key的值
  • 若是某個指針在節點node最右邊且不爲null,則其指向節點的全部key大於v(keym),v(keym)爲node的最後一個key的值。
  • 若是某個指針在節點node的左右相鄰key分別是keyi,keyi+1且不爲null,則其指向節點的全部key小於v(keyi+1)且大於v(keyi)sql


     
    圖2 d=2的B-Tree示意圖

    因爲B Tree的特性,按key檢索數據的算法很是直觀數據庫

  • 首先從根節點二分查找
  • 若是找到則返回對應節點的data
  • 不然對相應區間的指針指向的節點遞歸進行查找
  • 直到找到目標節點/null指針,查找成功/失敗
bTreeSearch(node, key) { if(node == null) return null; foreach(node.key) { if(node.key[i] == key) return node.data[i]; if(node.key[i] > key) return bTreeSearch(point[i]->node); } return bTreeSearch(point[i+1]->node); } data = bTreeSearch(root, my_key); 

 

 

關於B-Tree有一系列有趣的性質,例如一個度爲d的B-Tree,設其索引N個key,則其樹高h的上限爲
 
 

 

檢索一個key,其查找節點個數的漸進時間複雜度爲
 
 

從這點能夠看出,B Tree是一個很是有效率的索引數據結構

1.2.2 B+Tree

B-Tree有許多變種,其中最多見的是B+Tree,MySQL廣泛用其實現索引
與B Tree相比,B+Tree有如下不一樣點緩存

  • 每一個節點的指針上限爲2d
  • 內節點只存key
  • 葉節點不存指針,葉節點指向被索引的數據而不是其餘葉節點
    • innodb中,指向的是主鍵
    • myshaym中指向的是數據的物理地址


       
      圖3 一個簡單的B+Tree

因爲並非全部節點都具備相同的域,所以B+Tree中葉節點和內節點通常大小不一樣
這點與B Tree不一樣,雖然B Tree中不一樣節點存放的key和指針可能數量不一致,可是每一個節點的域和上限是一致的,因此在實現中B Tree每每對每一個節點申請同等大小的空間

通常來講,B+Tree比B Tree更適合實現外存儲索引結構

1.2.2.1 帶有順序訪問指針的B+Tree

在經典B+Tree的基礎上進行了優化,增長了順序訪問指針


 
圖4

如圖4所示,在B+Tree的每一個葉節點增長一個指向相鄰葉節點指針,造成帶有順序訪問指針的B+Tree
此優化的目的是提升區間訪問的性能,例如圖4中若是要查詢key爲從18到49的全部數據記錄,當找到18後,只需順着節點和指針順序遍歷就能夠一次性訪問到全部數據節點,極大提升了區間查詢效率

1.3 爲何使用B Tree(B+Tree)

紅黑樹也可用來實現索引,可是文件系統及數據庫系統廣泛採用B/+Tree,何也?

通常來講,索引自己也很大,不可能全存內存,每每以索引文件的形式存在磁盤

索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級,因此評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤I/O操做次數的漸進複雜度。

B樹在提升了IO性能的同時並無解決元素遍歷的我效率低下的問題,正是爲了解決這個問題,B+樹應用而生.B+樹只須要去遍歷葉子節點就能夠實現整棵樹的遍歷.並且在數據庫中基於範圍的查詢是很是頻繁的,而B樹不支持這樣的操做(或者說效率過低).

換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數

1.4 主存存取原理

計算機使用的主存基本都是隨機讀寫存儲器(RAM),抽象出一個十分簡單的存取模型來講明RAM的工做原理


 
圖5 4x4的主存模型

從抽象角度看,主存是一系列的存儲單元組成的矩陣,每一個存儲單元存儲固定大小的數據
每一個存儲單元有惟一的地址,現代主存的編址規則比較複雜,這裏將其簡化成一個二維地址:經過一個行地址和一個列地址能夠惟必定位到一個存儲單元

  • 存取過程
    當系統須要讀取主存時,將地址信號經過地址總線傳給主存,主存讀到地址信號後,解析信號並定位到指定存儲單元,而後將此存儲單元數據放到數據總線,供其它部件讀取

  • 寫主存
    過程相似,系統將要寫入單元地址和數據分別放在地址總線和數據總線上,主存讀取兩個總線的內容,作相應的寫操做

這裏能夠看出,主存存取的時間僅與存取次數呈線性關係,由於不存在機械操做,兩次存取的數據的「距離」不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是同樣的

1.5 磁盤存取原理

索引通常以文件形式存儲在磁盤上,索引檢索須要磁盤I/O
與主存不一樣,磁盤I/O存在機械消耗,所以磁盤I/O時間消耗巨大


 
圖6 磁盤的總體結構示意圖

磁盤由大小相同且同軸的圓形盤片組成,磁盤能夠轉動(各磁盤必須同步轉動)
在磁盤的一側有磁頭支架,磁頭支架固定了一組磁頭,每一個磁頭負責存取一個磁盤的內容。磁頭不能轉動,可是能夠沿磁盤半徑方向運動(實際是斜切向運動),每一個磁頭同一時刻也必須是同軸的,即從正上方向下看,全部磁頭任什麼時候候都是重疊的(不過目前已經有多磁頭獨立技術,可不受此限制)


 
圖7 磁盤結構的示意圖

盤片被劃分紅一系列同心環,圓心是盤片中心,每一個同心環叫作一個磁道,全部半徑相同的磁道組成一個柱面。磁道被沿半徑線劃分紅一個個小的段,每一個段叫作一個扇區,每一個扇區是磁盤的最小存儲單元。爲了簡單起見,咱們下面假設磁盤只有一個盤片和一個磁頭。

當須要從磁盤讀取數據時,系統會將數據邏輯地址傳給磁盤,磁盤的控制電路按照尋址邏輯將邏輯地址翻譯成物理地址,即肯定要讀的數據在哪一個磁道,哪一個扇區
爲了讀取這個扇區的數據,須要將磁頭放到這個扇區上方,爲了實現這一點,磁頭須要移動對準相應磁道,這個過程叫作尋道,所耗費時間叫作尋道時間,而後磁盤旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫作旋轉時間

1.6 局部性原理與磁盤預讀

因爲存儲介質特性,磁盤自己存取就比主存慢,再加上機械運動耗費,磁盤的存取速度每每是主存的幾百萬分之一,所以爲了提升效率,要儘可能減小磁盤I/O。
爲了達到這個目的,磁盤每每不是嚴格按需讀取,而是每次都會預讀,即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存。這樣作的理論依據是計算機科學中著名的局部性原理:
當一個數據被用到時,其附近的數據也一般會立刻被使用
程序運行期間所須要的數據一般比較集中
因爲磁盤順序讀取的效率很高(不須要尋道時間,只需不多的旋轉時間),所以對於具備局部性的程序來講,預讀能夠提升I/O效率

預讀的長度通常爲頁(page)的整數倍
頁是存儲器的邏輯塊,操做系統每每將主存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(在許多操做系統中,頁大小一般爲4k),主存和磁盤以頁爲單位交換數據
當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,而後異常返回,程序繼續運行

1.7 B/+Tree索引的性能分析

通常使用磁盤I/O次數評價索引結構的優劣

B Tree分析

檢索一次最多須要訪問h個節點
數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次I/O就能夠徹底載入
爲了達到這個目的,在實際實現B-Tree還須要使用以下技巧:

  • 每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O
  • B-Tree中一次檢索最多須要h-1次I/O(根節點是常駐內存的),漸進複雜度爲O(h)=O(logdN)。
    通常實際應用中,出度d是很是大的數字,一般超過100,所以h很是小(一般不超過3)

綜上所述,用B-Tree做爲索引結構效率是很是高的

紅黑樹

h明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性,因此紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差不少

B+Tree更適合外存索引,緣由和內節點出度d有關
從上面分析能夠看到,d越大索引的性能越好
出度的上限取決於節點內key和data的大小

dmax=floor(pagesize/(keysize+datasize+pointsize)) 

floor表示向下取整。因爲B+Tree內節點去掉了data域,所以能夠擁有更大的出度,更好的性能

2. MySQL索引實現

索引屬於存儲引擎級別的概念,不一樣存儲引擎對索引的實現方式是不一樣的,主要討論MyISAM和InnoDB兩個存儲引擎的索引實現方式

2.1 MyISAM索引實現

使用B+Tree做爲索引結構,葉節點data域存放數據記錄的地址


 
圖8 MyISAM索引的原理圖

設Col1爲主鍵,則圖8是一個MyISAM表的主索引(Primary key)示例
能夠看出MyISAM的索引文件僅僅保存數據記錄的地址

在MyISAM中,主/輔索引在結構上沒有任何區別,只是主索引要求key惟一,而輔索引key可重複

若是咱們在Col2上創建一個輔索引


 
圖9 Col2上創建的輔索引

一樣也是一顆B+Tree,data域保存數據記錄的地址。
所以,MyISAM中索引檢索的算法爲首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,則取出其data域的值,而後以data域的值爲地址,讀取相應數據記錄。

MyISAM的索引方式也叫作「非彙集」的,之因此這麼稱呼是爲了與InnoDB的彙集索引區分

2.2 InnoDB索引實現

雖然InnoDB也使用B+Tree做爲索引結構,但具體實現方式卻與MyISAM大相徑庭

第一個重大區別是

  • InnoDB的數據文件自己就是索引文件
    • MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址
    • 而在InnoDB中,表數據文件自己就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,所以InnoDB表數據文件自己就是主索引
       
      圖10 InnoDB主索引(同時也是數據文件)示意圖

      能夠看到葉節點包含了完整的數據記錄。這種索引叫作彙集索引
      由於InnoDB的數據文件自己要按主鍵彙集,因此InnoDB要求表必須有主鍵(MyISAM能夠沒有),若是沒有顯式指定,則MySQL系統會自動選擇一個能夠惟一標識數據記錄的列做爲主鍵,若是不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段做爲主鍵,這個字段長度爲6個字節,類型爲長整形

第二個與MyISAM索引的不一樣是

  • InnoDB的輔索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的全部輔助索引都引用主鍵做爲data域


     
    圖11 定義在Col3上的一個輔索引

    這裏以英文字符的ASCII碼做爲比較準則
    彙集索引這種實現方式使得按主鍵的搜索十分高效,可是輔助索引搜索須要檢索兩遍索引:

    • 首先檢索輔助索引得到主鍵
    • 而後用主鍵到主索引中檢索得到記錄

知道了InnoDB的索引實現後,就很容易明白爲何不建議使用過長的字段做爲主鍵,由於全部輔索引都引用主索引,過長的主索引會令輔索引變得過大
再如,用非單調的字段做爲主鍵在InnoDB中不是個好主意,由於InnoDB數據文件自己是一顆B+Tree,非單調的主鍵會形成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增字段做爲主鍵則是一個很好的選擇

聚簇索引

聚簇索引並非一種單獨的索引類型,而是一種數據存儲方式
具體的細節依賴於其實現方式,但innoddb 的聚簇索引實際上在同一個結構中保存了B-Tree索引和數據行
是對磁盤上實際數據從新組織以按指定的一個或多個列的值排序的算法。特色是存儲數據的順序和索引順序一致。
通常狀況下主鍵會默認建立聚簇索引,且一張表只容許存在一個聚簇索引

當表有聚簇索引時,它的數據實際上存放在索引的葉子頁(leaf page)中
術語‘聚簇’表示數據行和相鄰的鍵值進錯的存儲在一塊兒
由於沒法同時把數據行存放在兩個不一樣的地方,因此在一個表中只能有一個聚簇索引 (不過,覆蓋索引能夠模擬多個聚簇索引的狀況)。

InnoDb將經過主鍵彙集數據。

若是沒有定義主鍵,InnoDB 會選擇一個惟一的非空索引代替
若是沒有這樣的索引,InnoDB 會隱式定義一個主鍵來做爲聚簇索引
InnoDB值彙集在同一個頁面中的記錄,包含相鄰鍵值的頁面可能會相距很遠

InnoDB 和 MyISAM的數據分佈對比

聚簇索引和非聚簇索引的數據分佈有區別,以及對應的主鍵索引和二級索引的數據分佈也有區別
來看看InnoDB和MyISAM是如何存儲下面的這個表的

CREATE TABLE layout_test(     col1 int not null,     col2 int not null,     primary key (col1),     key(col2)   ); 

假設該表的主鍵取值爲1-1w,按照隨機順序插入,並使用OPTIMIZE TABLE命令優化
換句話說,數據在磁盤的存儲方式已經最優,但進行的順序是隨機的
列col2的值時從1-100之間隨機賦值,因此有不少重複的值

MyISAM 的數據分佈

MyIsam按照數據插入的順序存儲在磁盤上

實際上,MyISAM 中主鍵索引和其餘索引在結構上沒有什麼不一樣
主鍵索引就是一個名爲PRIMARY的惟一非空索引

InnoDB 的數據分佈

由於InnoDB支持聚簇索引,索引使用很是不一樣的方式存儲一樣的數據。在InnoDB中,聚簇索引「是」表,因此不像myISAM那樣須要獨立的行存儲

聚簇索引的一些重要優勢:

  • 能夠把相關的數據保存在一塊兒
    例如,實現電子郵箱時,能夠根據用戶id來彙集數據這樣只須要從磁盤讀取少數的數據頁就能獲取某個用戶的所有郵件。若是沒有使用聚簇索引,則每封郵件均可能致使一次I/O
  • 數據訪問更快
    聚簇索引將索引和數據保存在同一個B-Tree中,所以從聚簇索引中獲取數據一般比非聚簇索引中快
  • 使用覆蓋索引掃描的查詢能夠直接使用頁節點中的主鍵值。

聚簇索引的缺點:

聚簇索引最大限度的提升了io密集型應用的性能,但若是數據所有存放在內存中,則訪問的順序就沒那麼重要了,聚簇索引也就沒有什麼優點了。

插入速度嚴重依賴插入順序。按照主鍵的順序插入是加載數據到innodb表中速度最快的方式。但若是不是按照主鍵順序加載數據,那麼加載完成後最好使用OPTIMIZE TABLE 命令來從新組織一下表。

更新聚簇索引的代價很高,由於會強制InooDB將每一個更新的數據移動到新的位置。

基於聚簇索引的表在插入行,或者主鍵被更新致使須要移動行的時候,可能面臨’頁分裂(page split)‘的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時。存儲引擎,存儲引擎會將該頁分裂成兩個頁面來容納該行,這就是一次頁分裂操做。頁分裂會致使表佔用更多的存儲空間。

聚簇索引可能致使全表掃描變慢,尤爲是行比較稀疏,或者因爲頁分裂致使數據存儲不連續的時候。

二級索引(非聚簇索引)可能比想象的要更大,由於在二級索引的子節點包含了最優一個幾點可能讓人有些疑惑,爲何二級索引須要兩次索引查找?答案在於二級索引中保存的「行指針」的實質。要記住,二級索引葉子節點保存的不是隻想物理位置的指針,而是行的主鍵值。

這意味着經過二級索引進行查找行,存儲引擎須要找到二級索引的子節點得到對應的主鍵值,而後根據這個值去聚簇索引總超找到對應的行。這裏作了重複的工做:兩次B-Tree查找,而不是一次。對於InnoDB,自適應哈希索引可以減小這樣重複工做。

在《數據庫原理》一書中是這麼解釋聚簇索引和非聚簇索引的區別的:

  • 聚簇索引的葉子節點就是數據節點
  • 非聚簇索引的葉子節點仍然是索引節點,只不過有指向對應數據塊的指針。

所以,MYSQL中不一樣的數據存儲引擎對聚簇索引的支持不一樣就很好解釋了。
下面,咱們能夠看一下MYISAM和INNODB兩種引擎的索引結構。

如原始數據爲:


 
 

MyISAM引擎的數據存儲方式,如圖


 
 

MYISAM是按列值與行號來組織索引的
它的葉子節點中保存的其實是指向存放數據的物理塊的指針。

MYISAM引擎的索引文件(.MYI)和數據文件(.MYD)是相互獨立的。

而InnoDB按聚簇索引的形式存儲數據,因此它的數據佈局有着很大的不一樣。它存儲數據的結構大體以下:


 
 

注:聚簇索引中的每一個葉子節點包含主鍵值、事務ID、回滾指針(rollback pointer用於事務和MVCC)和餘下的列(如col2)。

INNODB的二級索引與主鍵索引有很大的不一樣。InnoDB的二級索引的葉子包含主鍵值,而不是行指針(row pointers),這減少了移動數據或者數據頁面分裂時維護二級索引的開銷,由於InnoDB不須要更新索引的行指針。其結構大體以下:


 
 

INNODB和MYISAM的主鍵索引與二級索引的對比:


 
 

InnoDB的的二級索引的葉子節點存放的是KEY字段加主鍵值。所以,經過二級索引查詢首先查到是主鍵值,而後InnoDB再根據查到的主鍵值經過主鍵索引找到相應的數據塊。而MyISAM的二級索引葉子節點存放的仍是列值與行號的組合,葉子節點中保存的是數據的物理地址。因此能夠看出MYISAM的主鍵索引和二級索引沒有任何區別,主鍵索引僅僅只是一個叫作PRIMARY的惟1、非空的索引,且MYISAM引擎中能夠不設主鍵。

3. 索引使用策略及優化

3.1 索引的好處

 
 

3.2 什麼狀況下能夠用到B樹索引

(1) 定義有主鍵的列必定要創建索引 : 主鍵能夠加速定位到表中的某行
(2) 定義有外鍵的列必定要創建索引 : 外鍵列一般用於表與表之間的鏈接,在其上建立索引能夠加快表間的鏈接
(3) 對於常常查詢的數據列最好創建索引
① 對於須要在指定範圍內快速或頻繁查詢的數據列,由於索引已經排序,其指定的範圍是連續的,查詢能夠利用索引的排序,加快查詢的時間
② 常常用在 where子句中的數據列,將索引創建在where子句的集合過程當中,對於須要加速或頻繁檢索的數據列,可讓這些常常參與查詢的數據列按照索引的排序進行查詢,加快查詢的時間

3.3 索引優化

MySQL的優化主要分爲

  • 結構優化(Scheme optimization)
  • 查詢優化(Query optimization)

本章討論的高性能索引策略主要屬於結構優化範疇

爲了討論索引策略,須要一個數據量不算小的數據庫做爲示例
選用MySQL官方文檔中提供的示例數據庫之一:employees
這個數據庫關係複雜度適中,且數據量較大。下圖是這個數據庫的E-R關係圖(引用自MySQL官方手冊):

 
圖12 示例數據庫

3.3.1 最左前綴原理與相關優化

高效使用索引的首要條件是知道什麼樣的查詢會使用到索引,這個問題和B+Tree中的「最左前綴原理」有關,下面經過例子說明最左前綴原理

聯合索引

MySQL中的索引能夠以必定順序引用多列,這種索引叫作聯合索引,通常的,一個聯合索引是一個有序元組<a1, a2, …, an>,其中各個元素均爲數據表的一列

 
 

 

覆蓋索引(Covering Indexes)

包含知足查詢的全部列

只須要讀索引而不用讀數據,大大提升查詢性能。有如下優勢:
(1)索引項一般比記錄要小,使得MySQL訪問更少的數據
(2)索引都按值排序存儲,相對於隨機訪問記錄,須要更少的I/O
(3)大多數據引擎能更好的緩存索引。好比MyISAM只緩存索引
(4)覆蓋索引對於InnoDB表尤爲有用,由於InnoDB使用彙集索引組織數據,若是二級索引中包含查詢所需的數據,就再也不須要在彙集索引中查找了

覆蓋索引只有B-TREE索引存儲相應的值
並非全部存儲引擎都支持覆蓋索引(Memory/Falcon)


 
覆蓋索引

對於索引覆蓋查詢(index-covered query),使用EXPLAIN時,能夠在Extra列中看到Using index

在大多數引擎中,只有當查詢語句所訪問的列是索引的一部分時,索引纔會覆蓋
可是,InnoDB不限於此,InnoDB的二級索引在葉節點中存儲了primary key的值

 
 

 
 

 
使用覆蓋索引查詢數據

 
select *不能用覆蓋索引

 
 

 

以employees.titles表爲例,下面先查看其上都有哪些索引:


 
 

從結果中能夠看到titles表的主索引爲<emp_no, title, from_date>,還有一個輔助索引<emp_no>
爲了不多個索引使事情變複雜(MySQL的SQL優化器在多索引時行爲比較複雜),咱們將輔助索引drop掉

ALTER TABLE employees.titles DROP INDEX emp_no; 

這樣就能夠專心分析索引PRIMARY

狀況一:全值匹配

 

 
 

很明顯,當按照索引中全部列進行精確匹配(這裏精確匹配指「=」或「IN」匹配)時,索引能夠被用到。
這裏有一點須要注意,理論上索引對順序敏感,可是因爲MySQL的查詢優化器會自動調整where子句的條件順序以使用適合的索引
例如咱們將where中的條件順序顛倒
 
 

效果是同樣的

 

狀況二:最左前綴匹配

 
 

當查詢條件精確匹配索引的左邊連續一個或幾個列時,如<emp_no>或<emp_no, title>,因此能夠被用到,可是隻能用到一部分,即條件所組成的最左前綴
上面的查詢從分析結果看用到了PRIMARY索引,可是key_len爲4,說明只用到了索引的第一列前綴

狀況三:查詢條件用到了索引中列的精確匹配,可是中間某個條件未提供

 

 
 

此時索引使用狀況和狀況二相同,由於title未提供,因此查詢只用到了索引的第一列,然後面的from_date雖然也在索引中,可是因爲title不存在而沒法和左前綴鏈接,所以須要對結果進行過濾from_date(這裏因爲emp_no惟一,因此不存在掃描)
若是想讓from_date也使用索引而不是where過濾,能夠增長一個輔助索引<emp_no, from_date>,此時上面的查詢會使用這個索引
除此以外,還可使用一種稱之爲「隔離列」的優化方法,將emp_nofrom_date之間的「坑」填上

 

首先咱們看下title一共有幾種不一樣的值


 
 

只有7種
在這種成爲「坑」的列值比較少的狀況下,能夠考慮用「IN」來填補這個「坑」從而造成最左前綴


 
 

此次key_len爲59,說明索引被用全了,可是從type和rows看出IN實際上執行了一個range查詢,這裏檢查了7個key。看下兩種查詢的性能比較:
 
 

「填坑」後性能提高了一點。若是通過emp_no篩選後餘下不少數據,則後者性能優點會更加明顯。固然,若是title的值不少,用填坑就不合適了,必須創建輔助索引

狀況四:查詢條件沒有指定索引第一列

 
 

因爲不是最左前綴,這樣的查詢顯然用不到索引

狀況五:匹配某列的前綴字符串

 
 

此時能夠用到索引,通配符%不出如今開頭,則能夠用到索引,但根據具體狀況不一樣可能只會用其中一個前綴

狀況六:範圍查詢(因爲B+樹的順序特色,尤爲適合此類查詢)

 
 
  • 範圍列能夠用到索引(必須是最左前綴),可是範圍列後面的列沒法用到索引
  • 索引最多用於一個範圍列,所以若是查詢條件中有兩個範圍列則沒法全用到索引


     
     
  • 能夠看到索引對第二個範圍索引無能爲力。這裏特別要說明MySQL一個有意思的地方,那就是僅用explain可能沒法區分範圍索引和多值匹配,由於在type中這二者都顯示爲range
  • 用了「between」並不意味着就是範圍查詢,例以下面的查詢:


     
     

看起來是用了兩個範圍查詢,但做用於emp_no上的「BETWEEN」實際上至關於「IN」,也就是說emp_no實際是多值精確匹配。能夠看到這個查詢用到了索引所有三個列。所以在MySQL中要謹慎地區分多值匹配和範圍匹配,不然會對MySQL的行爲產生困惑。


 
 

狀況七:查詢條件中含有函數或表達式

若是查詢條件中含有函數或表達式,則MySQL不會爲這列使用索引(雖然某些在數學意義上可使用)


 
 

雖然這個查詢和狀況五中功能相同,可是因爲使用了函數left,則沒法爲title列應用索引,而狀況五中用LIKE則能夠。再如:


 
 

顯然這個查詢等價於查詢emp_no爲10001的函數,可是因爲查詢條件是一個表達式,MySQL沒法爲其使用索引。看來MySQL尚未智能到自動優化常量表達式的程度,所以在寫查詢語句時儘可能避免表達式出如今查詢中,而是先手工私下代數運算,轉換爲無表達式的查詢語句。
 
 

3.4 Btree索引的使用限制

 
 

3.4.1 如下狀況下設置索引,但沒法使用

① 以「%」開頭的LIKE語句,模糊匹配
② OR語句先後沒有同時使用索引
③ 數據類型出現隱式轉化(如varchar不加單引號的話可能會自動轉換爲int型)

3.4.2 索引選擇性與前綴索引

 
 

既然索引能夠加快查詢速度,那麼是否是隻要是查詢語句,就建上索引呢?

答案是否認的。由於索引雖然加快了查詢速度,但索引也是有代價的:索引文件自己要消耗存儲空間

  • 索引會加劇插入、刪除和修改記錄時的負擔,增長寫操做的成本
  • 太多索引會增長查詢優化器的分析選擇時間
  • MySQL在運行時也要消耗資源維護索引

索引並非越多越好。下列狀況下不建議建索引

  • 對於那些查詢中不多涉及的列、重複值比較多的列不要創建索引
    例如,在查詢中不多使用的列,有索引並不能提升查詢的速度,相反增長了系統維護時間和消耗了系統空間
    又如,「性別」列只有列值「男」和「女」,增長索引並不能顯著提升查詢的速度
    對於定義爲text、image和bit數據類型的列不要創建索引。由於這些數據類型的數據列的數據量要麼很大,要麼很小,不利於使用索引
  • 表記錄比較少
    例如一兩千條甚至只有幾百條記錄的表,不必建索引,讓查詢作全表掃描就行了
  • 索引的選擇性較低
    所謂索引的選擇性(Selectivity),是指不重複的索引值(也叫基數,Cardinality)與表記錄數(#T)的比值
    Index Selectivity = Cardinality / #T
    顯然選擇性的取值範圍爲(0, 1],選擇性越高的索引價值越大,這是由B+Tree的性質決定的。
    例如,上文用到的employees.titles表,若是title字段常常被單獨查詢,是否須要建索引,咱們看一下它的選擇性
     
     

    title的選擇性不足0.0001(精確值爲0.00001579),因此實在沒有什麼必要爲其單獨建索引

有一種與索引選擇性有關的索引優化策略叫作前綴索引,就是用列的前綴代替整個列做爲索引key,當前綴長度合適時,能夠作到既使得前綴索引的選擇性接近全列索引,同時由於索引key變短而減小了索引文件的大小和維護開銷。下面以employees.employees表爲例介紹前綴索引的選擇和使用。

從圖12能夠看到employees表只有一個索引<emp_no>,那麼若是咱們 想按名字搜索一我的,就只能全表掃描了:

 
 

 

若是頻繁按名字搜索員工,這樣顯然效率很低,所以咱們能夠考慮建索引。有兩種選擇,建<first_name>或<first_name, last_name>,看下兩個索引的選擇性:


 
 

<first_name>顯然選擇性過低,<first_name, last_name>選擇性很好,可是first_name和last_name加起來長度爲30,有沒有兼顧長度和選擇性的辦法?能夠考慮用first_name和last_name的前幾個字符創建索引,例如<first_name, left(last_name, 3)>,看看其選擇性:

 
 

選擇性還不錯,但離0.9313仍是有點距離,那麼把last_name前綴加到4:

 
 

這時選擇性已經很理想了,而這個索引的長度只有18,比<first_name, last_name>短了接近一半,咱們把這個前綴索引 建上:

  1. ALTER TABLE employees.employees
  2. ADD INDEX first_name_last_name4 (first_name, last_name(4));

此時再執行一遍按名字查詢,比較分析一下與建索引前的結果:


 
 

性能的提高是顯著的,查詢速度提升了120多倍。

前綴索引兼顧索引大小和查詢速度,可是其缺點是不能用於ORDER BY和GROUP BY操做,也不能用於Covering index(即當索引自己包含查詢所需所有數據時,再也不訪問數據文件自己)。

3.5 InnoDB的主鍵選擇與插入優化

在使用InnoDB存儲引擎時,若是沒有特別的須要,請永遠使用一個與業務無關的自增字段做爲主鍵

常常看到有帖子或博客討論主鍵選擇問題,有人建議使用業務無關的自增主鍵,有人以爲沒有必要,徹底可使用如學號或身份證號這種惟一字段做爲主鍵。不論支持哪一種論點,大多數論據都是業務層面的。
若是從數據庫索引優化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意

上文討論過InnoDB的索引實現,InnoDB使用匯集索引,數據記錄自己被存於主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小爲一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,所以每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,若是頁面達到裝載因子(InnoDB默認爲15/16),則開闢一個新的頁(節點)。

若是表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。以下圖所示:

 
圖13

這樣就會造成一個緊湊的索引結構,近似順序填滿
因爲每次插入時也不須要移動已有數據,所以效率很高,也不會增長不少開銷在維護索引上。

若是使用非自增主鍵(若是身份證號或學號等),因爲每次插入主鍵的值近似於隨機,所以每次新紀錄都要被插到現有索引頁得中間某個位置:


 
 

此時MySQL不得不爲了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上讀回來,這增長了不少開銷,同時頻繁的移動、分頁操做形成了大量的碎片,獲得了不夠緊湊的索引結構,後續不得不經過OPTIMIZE TABLE來重建表並優化填充頁面。

所以,只要能夠,請儘可能在InnoDB上採用自增字段作主鍵。

與排序(ORDER BY)相關的索引優化及覆蓋索引(Covering index)的話題本文並未涉及,
全文索引等等本文也並未涉及

4 Hash索引

MySQL提供四種索引

  • B-Tree索引:最多見的的索引,大部分引擎支持B樹索引
  • HASH索引:只有Memory引擎支持,使用場景簡單
  • R-Tree索引:空間索引是MyISAM的一個特殊索引類型,主要用於地理空間數據類型,一般使用較少
  • Full-text:全文索引也是MyISAM的一個特殊索引,主要用於全文索引,InnoDb從MySql5.6開始提供支持全文索引

MySql目前不支持函數索引,可是能對列的前面某一部分進行索引,例如標題title字段,能夠只取title的前10個字符索引,這樣的特性大大縮小了索引文件的大小,但前綴索引也有缺點,在排序order by和分組group by操做的時候沒法使用

create index idx_title on film(title(10)); 
索引 MyISAM引擎 InnoDB引擎 Memory引擎
B-Tree索引 支持 支持 支持
HASH索引 不支持 不支持 支持
R-Tree索引 支持 不支持 不支持
Full-text索引 支持 暫不支持 不支持

經常使用的索引就是B-tree索引和hash索引,資只有memory引擎支持HASH索引,hash索引適用於key-value查詢,經過hash索引比B-tree索引查詢更加迅速,可是hash索引不支持範圍查找例如<><==,>==等操做,若是使用memory引擎而且where不使用=進行 索引列,就不會用的索引。Memory只有在"="的條件下才會使用索引

4.0 特色

 
 

4.1 Hash索引的限制

 

 
 

 
Btree模擬

 
 

哈希索引只有Memory,NDB兩種引擎支持,Memory引擎默認支持哈希索引,若是多個hash值相同,出現哈希碰撞,那麼索引以鏈表方式存儲

 

可是,Memory引擎表只對可以適合機器的內存切實有限的數據集。

要使InnoDB或MyISAM支持哈希索引,能夠經過僞哈希索引來實現,叫自適應哈希索引。

主要經過增長一個字段,存儲hash值,將hash值創建索引,在插入和更新的時候,創建觸發器,自動添加計算後的hash到表裏。

直接索引

假若有一個很是很是大的表,以下:

CREATE TABLE IF NOT EXISTS `User` ( `id` int(10) NOT NULL COMMENT '自增id', `name` varchar(128) NOT NULL DEFAULT '' COMMENT '用戶名', `email` varchar(128) NOT NULL DEFAULT '' COMMENT '用戶郵箱', `pass` varchar(64) NOT NULL DEFAULT '' COMMENT '用戶密碼', `last` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最後登陸時間', ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 

這個時候,好比說,用戶登錄,我須要經過email檢索出用戶,經過explain獲得以下:

mysql> explain SELECT id FROM User WHERE email = ‘ooxx@gmail.com’ LIMIT 1;

+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | User | ALL | NULL | NULL | NULL | NULL | 384742 | Using where | +----+-------------+-------+------+---------------+------+---------+------+--------+-------------+ 

發現 rows = 384742 也就是要在384742裏面進行比對email這個字段的字符串。

這條記錄運行的時間是:Query took 0.1744 seconds,數據庫的大小是40萬。

從上面能夠說明,若是直接在email上面創建索引,除了索引區間匹配,還要進行字符串匹配比對,email短還好,若是長的話這個查詢代價就比較大。

若是這個時候,在email上創建哈希索引,查詢以int查詢,性能就比字符串比對查詢快多了。

Hash 算法

創建哈希索引,先選定哈希算法,這裏選用CRC32。

《高性能MySQL》說到的方法CRC32算法,創建SHA或MD5算法是划算的,自己位數都有可能比email段長了。

INSERT UPDATE SELECT 操做

在表中添加hash值的字段:

mysql> ALTER TABLE User ADD COLUMN email_hash int unsigned NOT NULL DEFAULT 0;

接下來就是在UPDATE和INSERT的時候,自動更新 email_hash 字段,經過MySQL觸發器實現:

DELIMITER | CREATE TRIGGER user_hash_insert BEFORE INSERT ON `User` FOR EACH ROW BEGIN SET NEW.email_hash=crc32(NEW.email); END; | CREATE TRIGGER user_hash_update BEFORE UPDATE ON `User` FOR EACH ROW BEGIN SET NEW.email_hash=crc32(NEW.email); END; | DELIMITER ; 

這樣的話,咱們的SELECT請求就會變成這樣:

mysql> SELECT email, email_hash FROM User WHERE email_hash = CRC32(「F2dgTSWRBXSZ1d3O@gmail.com」) AND email= 「F2dgTSWRBXSZ1d3O@gmail.com」;

+----------------------------+------------+ | email | email_hash | +----------------------------+------------+ | F2dgTSWRBXSZ1d3O@gmail.com | 2765311122 | +----------------------------+------------+ 

在沒創建hash索引時候,請求時間是 0.2374 seconds,創建完索引後,請求時間直接變成 0.0003 seconds。

AND email = "F2dgTSWRBXSZ1d3O@gmail.com" 是爲了防止哈希碰撞致使數據不許確。


0x02.Hash Index 缺點

哈希索引也有幾個缺點:

  • 索引存放的是hash值,因此僅支持 < = > 以及 IN 操做
  • hash索引沒法經過操做索引來排序,由於存放的時候通過hash計算,可是計算的hash值和存放的不必定相等,因此沒法排序
  • 不能避免全表掃描,只是因爲在memory表裏支持非惟一值hash索引,就是不一樣的索引鍵,可能存在相同的hash值
  • 若是哈希碰撞不少的話,性能也會變得不好
  • 哈希索引沒法被用來避免數據的排序
相關文章
相關標籤/搜索