1. 程式人生 > >Mysql索引詳解。

Mysql索引詳解。

前言

昨天面試被問到了索引,之前也看過不少關於索引的資料,但是感覺還是有很多欠缺。故結合網上一些資料將知識重新梳理一遍。

一、索引的原理

1.1 索引原理

索引的目的在於提高查詢效率,與我們查閱讀書所用的目錄是一個道理:先定位到章,然後定位到該章下的一個小節,然後找到頁數。
本質都是:通過不斷地縮小想要獲取資料的範圍來篩選出最終想要的結果,同時把隨機的時間變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查詢方式來鎖定資料。
資料庫也是一樣,但顯然要複雜的多,因為不僅面臨著等值查詢,還有範圍查詢(>、<、between、in)
、模糊查詢(like)、並集查詢(or)等等。資料庫應該選擇怎麼樣的方式來應對所有問題呢?我們回想到字典的例子,能不能把資料分成段,然後分段查詢呢?最簡單的如果1000條資料,1到100分成第一段,101到200分成第二段…這樣查第250條資料,只要找到第三段就可以了,一下子去除了90%的無效資料。單如果1千萬的記錄呢,分成幾段比較好?稍有演算法基礎的同學會想到搜尋樹,其平均複雜度是logN,具有不錯的查詢效能。但這裡我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的。而資料庫實現比較複雜,一方面資料是儲存在磁碟上的,另外一方面為了提高效能,每次又可以把部分資料讀入記憶體計算,因為我們知道訪問磁碟的成本大概是訪問記憶體的十萬倍左右,所以簡單的搜尋樹難以滿足複雜的應用場景。

1.2 磁碟IO與預讀

考慮到磁碟IO是非常高昂的操作,計算機系統做了一些優化,當一次IO時,不光把當前磁碟地址的資料,而且把響鈴都資料也都讀取到記憶體緩衝區內,因為區域性預讀性原理告訴我們,當一個計算機訪問一個地址的資料的時候,與之相鄰的資料也會很快被訪問到。每一次IO讀取的資料我們稱之為一頁(page)。具體一頁有多大資料跟作業系統有關,一般為4k或8k,也就是我們讀取一頁的資料時候,實際上才發生了一次IO,這個理論對於索引的資料結構設計非常有幫助。

1.2 索引的資料結構

我們需要這種資料結構能做些什麼,其實很簡單,那就是:每次查詢資料時把磁碟IO次數控制在一個很小的數量級,最好是常數數量級。那麼我們就想到如果一個高度可控的多路搜尋樹是否能滿足需求呢?就這樣,b+樹應運而生。
在這裡插入圖片描述


如上圖,是一顆b+樹,淺藍色的塊我們稱之為一個磁碟塊,可以看到每個磁碟塊包含幾個資料項(深藍色所示)和指標(黃色所示),如磁碟塊1包含資料項17和35,包含指標P1、P2、P3,P1表示小於17的磁碟塊,P2表示在17和35之間的磁碟塊,P3表示大於35的磁碟塊。真實的資料存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點不儲存真是的資料,只儲存指引搜尋方向的資料項,如17、35並不真是存在於資料表中。另葉節點之間被一個連結串列連結起來,方便順序索引。

b樹的查詢過程

如圖所示,如果要查詢資料項29,那麼首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次IO,在記憶體中用二分查詢確定29在17和35之間,鎖定磁碟塊的P2指標,記憶體時間因為非常短(相對於磁碟的IO)可以忽略不計,通過磁碟塊1的P2指標的磁碟地址把磁碟塊由磁碟載入到記憶體,發生第二次IO,29在26和30之間,鎖定磁碟快P2指標,通過指標載入磁碟快8到記憶體,發生第三次IO,同時記憶體中做二分查詢找到29,結束查詢,總計三次IO。真實的情況是,3層的B+樹可以表示上百萬的資料,如果上百萬的資料查詢只需要三次IO,效能提高將是巨大的,如果沒有索引,每個資料項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常高。

二、MySQL的索引實現

2.1 MyISAM索引實現

MyISAM引擎使用B+Tree作為索引結構,葉節點的data域存放的是資料記錄的地址,索引檔案與資料分離,是一種非聚集索引。下圖是MyISAM索引的原理圖:
在這裡插入圖片描述
這裡設表一共有三列,假設我們以Col1為主鍵,則圖3是一個MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引檔案僅僅儲存資料記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重複。如果我們在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表資料檔案本身就是主索引。
在這裡插入圖片描述
上圖是InnoDB主索引(同時也是資料檔案)的示意圖,可以看到葉節點包含了完整的資料記錄。這種索引叫做聚集索引。因為InnoDB的資料檔案本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識資料記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含欄位作為主鍵,這個欄位長度為6個位元組,型別為長整形。

第二個與MyISAM索引的不同是InnoDB的輔助索引data域儲存相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖為定義在Col3上的一個輔助索引:
在這裡插入圖片描述
聚集索引這種實現方式使得按主鍵的搜尋十分高效,但是輔助索引搜尋需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然後用主鍵到主索引中檢索獲得記錄。

InonDB採用的這種聚集索引在物理上是有序的。

瞭解不同儲存引擎的索引實現方式對於正確使用和優化索引都非常有幫助,例如知道了InnoDB的索引實現後,就很容易明白為什麼不建議使用過長的欄位作為主鍵,因為所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的欄位作為主鍵在InnoDB中不是個好主意,因為InnoDB資料檔案本身是一顆B+Tree,非單調的主鍵會造成在插入新記錄時資料檔案為了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增欄位作為主鍵則是一個很好的選擇。

三、索引與優化

3.1 選擇索引的資料型別

MySQL支援很多資料型別,選擇合適的資料型別儲存資料對效能有很大的影響。通常來說,可以遵循以下一些指導原則:

(1)越小的資料型別通常更好:越小的資料型別通常在磁碟、記憶體和CPU快取中都需要更少的空間,處理起來更快。
(2)簡單的資料型別更好:整型資料比起字元,處理開銷更小,因為字串的比較更復雜。在MySQL中,應該用內建的日期和時間資料型別,而不是用字串來儲存時間;以及用整型資料型別儲存IP地址。
(3)儘量避免NULL:應該指定列為NOT NULL,除非你想儲存NULL。在MySQL中,含有空值的列很難進行查詢優化,因為它們使得索引、索引的統計資訊以及比較運算更加複雜。你應該用0、一個特殊的值或者一個空串代替空值。

3.2 選擇識別符號
選擇合適的識別符號是非常重要的。選擇時不僅應該考慮儲存型別,而且應該考慮MySQL是怎樣進行運算和比較的。一旦選定資料型別,應該保證所有相關的表都使用相同的資料型別。
(1) 整型:通常是作為識別符號的最好選擇,因為可以更快的處理,而且可以設定為AUTO_INCREMENT。
(2) 字串:儘量避免使用字串作為識別符號,它們消耗更高的空間,處理起來也較慢。而且,通常來說,字串都是隨機的,所以它們在索引中的位置也是隨機的,這會導致頁面分裂、隨機訪問磁碟,聚簇索引分裂(對於使用聚簇索引的儲存引擎)。

3.3 哪些情況需要建立索引
(1) 主鍵自動建立唯一索引
(2) 頻繁作為查詢查詢條件的欄位應該建立索引
(3) 查詢中與其它表關聯的欄位,外來鍵關係建立索引
(4) 單鍵/組合索引的選擇問題(在高併發下傾向建立組合索引)
(5) 查詢中排序的欄位,排序欄位若通過索引去訪問將大大提高排序速度
(6) 查詢中統計或者分組欄位

3.4 哪些情況不要建立索引
(1) 表記錄太少
(2) 經常增刪改的表(因為不僅要儲存資料,還要儲存一下索引檔案)
(3) 資料重複且分佈平均的表字段,因此應該只為最經常查詢和最經常排序的資料列建立索引。
(4) 頻繁更新的欄位不適合建立索引
(5) where條件裡用不到的欄位不建立索引

3.5查詢語句的優化

避免索引失效
  1.最佳左字首法則:如果索引了多列,要尊守最左字首法則,指的是查詢從索引的最左前列開始並且不跳過索引中的列。
  2.不在索引列上做任何操作(計算、函式、(自動or手動)型別轉換),會導致索引失效而轉向全表掃描。
  3.儲存引擎不能使用索引中範圍條件右邊的列。
  如這樣的sql: select * from user where username=‘123’ and age>20 and phone=‘1390012345’,其中username, age, phone都有索引,只有username和age會生效,phone的索引沒有用到。
  4.儘量使用覆蓋索引(只訪問索引的查詢(索引列和查詢列致)),如select age from user減少select *
  5.mysql在使用不等於(!= 或者 <>)的時候無法使用索引會導致全表掃描。
  6.is null, is not null 也無法使用索引。
  7.like 以萬用字元開頭(‘%abc…’)mysql索引失效會變成全表掃描的操作。
  所以最好用右邊like ‘abc%’。如果兩邊都要用,可以用select age from user where username like ‘%abc%’,其中age是索引列
  假如index(a,b,c), where a=3 and b like ‘abc%’ and c=4,a能用,b能用,c不能用
  8.字串不加單引號索引失效
  9.少用or,用它來連線時會索引失效
  10.儘量避免子查詢,而用join