【MySQL】(五)索引與算法

本篇文章的主旨是對InnoDB存儲引擎支持的索引做一個概述,並對索引內部的機制做一個深入的解析,通過了解索引內部構造來了解哪裏可以使用索引。

1、InnoDB存儲引擎支持以下幾種常見的索引:

  • B+樹索引
  • 全文索引
  • 哈希索引

前面已經提到過,InnoDB存儲引擎支持的哈希索引時自適應的,InnoDB存儲引擎會根據表的使用情況自動爲表生成哈希索引,不能人爲干預是否在一張表中生成哈希索引。

B+樹索引就是傳統意義上的索引,這是目前關係型數據庫中查找最爲常用和最爲有效的索引。B+樹索引的構造類似於二叉樹,根據鍵值快速找到數據。

再就是,B+樹並不能找到一個給定鍵值的具體行。B+樹索引能找到的只是被查找數據行所在的頁。然後數據庫通過把頁讀入到內存,再在內存中進行查找,最後得到要查找的數據。

2、數據結構與算法

B+樹索引時最爲常見的,也是在數據庫中使用最爲頻繁的一種索引。在介紹該索引之前先介紹與之密切相關的一些算法與數據結構。

2.1、二分查找法

二分查找法也稱折半查找法,用來查找一組有序的記錄數組中的某一記錄,其基本思想是:將記錄按有序化(遞增或遞減)排列,在查找過程中採用跳躍式方式查找,即先以有序數列的中點位置爲比較對象,如果要找的元素值小於該中點元素,則將帶查找序列縮小爲左半部分,否則爲右半部分。通過一次比較,將查找區間縮小一半。

二分查找法的應用及其廣泛,而且它的思想易於理解。表空間中每頁Page Directory中的槽是按照主鍵的順序存放的,對於某一條具體記錄的查詢時通過對Page Directory進行二分查找的。

2.2、二叉查找樹和平衡二叉樹

在介紹B+樹之前,先了解一下二叉查找樹。B+樹是通過二叉查找樹,再由平衡二叉樹,B樹演化而來。

在二叉查找樹中,左子樹的鍵值總是小於根的鍵值,右子樹的鍵值總是大於根的鍵值。因此可以通過中序遍歷得到鍵值的排序輸出。若想最大性能地構造一顆二叉查找樹,需要這顆查找樹是平衡的,從而引出了新的定義——平衡二叉樹,或稱AVL樹。

平衡二叉樹的定義如下:首先符合二叉查找樹的定義,其次必須滿足任何節點的兩個字數的高度最大差爲1。最好的想能需要建立一顆最優二叉樹,但是最優二叉樹的建立和維護需要大量的操作,因此,用戶一般只需要建立一顆平衡二叉樹即可。

平衡二叉樹的查詢速度很快,但是維護一顆平衡二叉樹的代價是非常大的。通常來說,需要1次或多次左旋和右旋來得到插入或更新後樹的平衡性。

3、B+樹

B+樹和二叉樹、平衡二叉樹一樣,都是經典的數據結構。B+樹是由B樹和索引順序訪問方法演化而來,但是在實現使用過程中幾乎已經沒有使用B樹的情況了。

B+樹的簡單定義:B+樹是爲磁盤或其他直接存儲輔助設備設計的一種平衡查找樹。在B+樹中,所有的記錄節點都是按鍵值的大小順序存放在同一層的葉子節點上,由葉子結點指針進行連接。

下面演示一個B+數結構,高度爲2,每頁可放4條記錄,扇出(fan out)爲5。從下圖1可以看出,所有記錄都在頁節點中,並且爲順序存放,我們從最左邊的葉節點開始遍歷,可以得到所有鍵值的順序排序:5、10、15、20、25、30、50、55、60、65、75、80、85、90。
這裏寫圖片描述

3.1、B+樹的插入操作

B+樹的插入必須保證插入後葉子節點中的記錄依然排序,同時需要考慮插入到B+樹的三種情況,每種情況都可能會導致不同的算法。
這裏寫圖片描述

這裏用一個例子來分析B+樹的插入。例如,對於上圖的這顆B+樹,若用戶插入28這個鍵值,發現當前Leaf Page和Index page都沒有滿,我們直接將記錄插入葉節點就可以了。如下圖所示:
這裏寫圖片描述
下面我們再插入70這個值,這時Leaf Page已經滿了,但是Index Page還沒有滿,符合上面的第二種情況。這時插入Leaf Page的情況爲
50、55、60、65、70.我們根據中間的值60拆分葉節點,可得到下圖所示(雙項鍊表指針依然存在,沒有畫出):
這裏寫圖片描述
最後我們再插入95,這個Leaf Page和Index Page都滿了,符合上面第三種情況。需要做2次拆分,如下圖所示:
這裏寫圖片描述
可以看到,不管怎麼變化,B+樹總會保持平衡。但是爲了保持平衡,對於新插入的鍵值可能需要做大量的拆分頁操作。B+樹主要用於磁盤,拆分意味着磁盤的操作,應該在可能的情況下儘量減少頁的拆分。因此,B+樹同樣提供了類似二叉樹的旋轉功能。

旋轉發生在Leaf Page已經滿,但是其左右兄弟節點沒有滿的情況下。這時B+樹並不會急於去做拆分頁的操作,而是將記錄轉移到所在頁的兄弟節點上。在通常情況下,左兄弟會被首先檢查用來做旋轉操作,因此若是最開始插入鍵值70,其實B+樹並不會急於去拆分葉子結點,而是去做旋轉操作,如下圖所示:
這裏寫圖片描述

3.2、B+樹的刪除操作

B+樹使用填充因子來控制樹的刪除變化,50%填充因子可設的最小值。B+樹的刪除操作同樣必須保證刪除後葉子結點中的記錄依然排序,同插入一樣,B+樹的刪除操作同樣需要考慮以下三種情況,如插入不同的是,刪除根據填充因子的變化來衡量。
這裏寫圖片描述
對上圖B+樹來進行刪除操作。首先刪除鍵值爲70的這條記錄,該記錄符合表討論的第一種情況,刪除後可得到下圖:
這裏寫圖片描述
接着我們刪除鍵值爲25的記錄,這也是表中討論的最後一種情況,但是該值還是Index Page中的值,因此在刪除Leaf Page中的25後,還應將25的右兄弟節點的28更新到Page Index中,最後可得到下圖:
這裏寫圖片描述
最後看刪除鍵值爲60的情況。刪除Leaf Page中鍵值爲60的記錄後,Fill factor小於50%,這時需要做合併操作,同樣,在刪除Index Page中相關記錄後需要做Index Page的合併操作,最後得到下圖
這裏寫圖片描述

4、B+樹索引

前面討論的都是B+樹的數據結構及其一般操作,B+樹索引的本質就是B+樹在數據庫中的實現。但是B+樹索引在數據庫中有一個特點就是高扇性,因此在數據庫中,B+樹的高度一般在2~4層,這也就是說查找某一個鍵值的行記錄時最多隻需2到4次IO,這倒不錯。因爲當前一般的機械磁盤每秒至少可以做100次IO,2~4次的IO意味着查詢時間只需0.02秒~0.04秒。

數據庫中的索引分爲聚集索引和輔助索引,但是不斷是聚集索引還是輔助的索引,其內部都是B+樹,即高度平衡的,葉子節點存放着所有的數據。聚集索引與輔助索引不同的是,葉子節點存放的是否是一整行的信息。

4.1、聚集索引

InnoDB存儲引擎表是索引組織表,即表中數據按照主鍵順序存放。而聚集索引就是按照每張表的主鍵構造一顆B+樹,同時葉子節點中存放的即爲整張表的行記錄數據,頁將聚集索引的葉子節點稱爲數據頁。聚集索引的這個特性決定了索引組織表中數據也是索引的一部分。同B+樹數據結構一樣,每個數據頁都通過一個雙向鏈表來進行鏈接。

由於實際的數據頁只能按照一顆B+樹進行排序,因此每張表只能擁有一個聚集索引。在多數情況下,查詢優化器傾向採用聚集索引。因爲聚集索引能夠在B+樹索引的葉子節點上直接找到數據。此外,由於定義了數據的邏輯順序,聚集索引能夠特別快地訪問針對範圍值的查詢。查詢優化器能夠快速發現某一段範圍的數據頁需要掃描。

聚集索引上,數據頁上存放的是完整的每行的記錄,而在非數據頁的索引頁中,存放的僅僅是鍵值及指向數據頁的偏移量,而不是一個完整的行記錄。

聚集索引的另一個好處就是,它對於主鍵的排序查找和範圍查找速度非常快。葉子節點的數據就是用戶所要查詢的數據。如用戶需要查詢一張註冊用戶的表,查詢最後註冊的10位用戶,由於B+樹索引是雙向鏈表的,用戶可以快速找到最後一個數據頁,並取出10條記錄。

4.2、輔助索引

對於輔助索引,葉子節點並不包含行記錄的全部數據。葉子節點除了包含鍵值以外,每個葉子節點中的索引行中還包含了一個書籤。該書籤用來告訴InnoDB存儲引擎哪裏可以找到與索引相對應的行數據。由於InnoDB存儲引擎表是索引組織表,因此InnoDB存儲引擎的輔助索引的書籤就是相應行數據的聚集索引鍵。

輔助索引並不影響數據在聚集索引中的組織,因此每張表上可以有多個輔助索引。當通過輔助索引來尋找數據時,InnoDB存儲引擎會遍歷輔助索引並通過葉級別的指針獲得指向主鍵索引(聚集索引)的主鍵,然後在通過主鍵索引來找到一個完整的行記錄。舉例來說,如果在一棵高度爲3的輔助索引中查找數據,那麼需要對這顆輔助索引樹遍歷3次找到指定主鍵,如果聚集索引樹的高度同樣爲3,那麼還需要對聚集索引樹遍歷3次查找,最終找到一個完整的行數據所在頁,因此一共需要6次邏輯IO訪問以得到最終的一個數據頁。

對於其他的一些數據庫,如Microsoft SQL Server數據庫,其中有一種稱爲堆表的表類型,即行數據的存儲按照插入的順序存放。這與MySQL數據庫的MyISAM存儲引擎有些類似。堆表的特性決定了堆表上的索引都是非聚集的,主鍵與費主鍵的區別只是是否唯一且非空(NOT NULL)。因此這時書籤是一個行標識符,可以用如“文件號:頁號:槽號”的格式來定位實際的行數據。

4.3、B+樹索引的分裂

在上文中介紹的B+樹索引的分裂是最爲簡單的一種情況,這和數據庫中B+樹索引的情況可能略有不同。此外上文並沒有涉及併發,而這纔是B+樹索引實現最爲困難的部分。

B+樹索引頁的分裂並不總是從頁的中間記錄開始,這樣可能會導致頁空間的浪費。

InnoDB存儲引擎的Page Header中有以下幾個部分用來保存插入的順序信息:

  • PAGE_LAST_INSERT
  • PAGE_DIRECTION
  • PAGE_N_DIRECTION

通過這些信息,InnoDB存儲引擎可以決定是向左還是向右進行分裂,同時決定將分裂點記錄爲哪一個。若插入時隨機的,則取頁的中間記錄作爲分裂點的記錄,這和之前介紹的相同。若往同一個方向進行插入的記錄數量爲5,並且目前已經定位到記錄(InnoDB存儲引擎插入時,首先需要進行定位,定位到的記錄爲待插入記錄的前一條記錄)之後還有3條記錄,則分裂點的記錄爲定位到的記錄後的第三條記錄,否則分裂點記錄就是待插入的記錄。

4.4、B+樹索引的管理

1、索引管理
索引的創建和刪除可以通過兩種方法,一種是ALERT TABLE,另一種是CREATE/DROP INDEX。

用戶可以設置對整個列的數據進行索引,也可以只索引一個列的開頭部分數據。

若用戶想要查看錶中索引的信息,可以使用命令SHOW INDEX。

Cardinality值非常關鍵,優化器會根據這個值來判斷是否使用這個索引。但是這個值並不是實時更新的,即並非每次索引的更新都會更新該值,因爲這樣代價太大了。因此這個值是不太準確的,只是一個大概的值。

2、Fast Index Creation
對於索引的添加或者刪除這類的DDL操作,MySQL數據庫的操作過程爲:

  • 首先創建一張新的臨時表,表結構爲通過命令ALERT TABLE新定義的結構。
  • 然後把原表中數據導入到臨時表
  • 接着刪除原表。
  • 最後把臨時表重命名爲原來的表名。

可以發現,若用對於一張大表進行索引的添加和刪除操作,那麼這會需要很長時間。更關鍵的是,若有大量事務需要訪問正在被修改的表,這意味着數據庫服務不可用。

InnoDB存儲引擎從InnoDB 1.0.x版本開始支持一種稱爲Fast Index Creation(快速索引創建)的索引創建方式——簡稱FIC。

對於輔助索引的創建,InnoDB存儲引擎會對創建索引的表加上一個S鎖。在創建的過程中,不需要重建表,因此速度較之前提高了很多,並且數據庫的可用性也得到了提高。刪除輔助索引操作就更簡單了,InnoDB存儲引擎只需更新內部視圖,並將輔助索引的空間標記爲可用,同時刪除MySQL數據庫內部視圖上對該表的索引定義即可。

這裏需要特別注意的是,臨時表的創建路徑是通過參數tmpdir進行設置得。用戶必須保證tmpdir有足夠的空間可以存放臨時表,否則會導致創建索引失敗。

由於FIC在索引的創建過程中對錶加上了S鎖,因此在創建的過程中只能對該表進行讀操作,若有大量的事務需要對目標表進行寫操作,那麼數據庫的服務同樣不可用。此外,FIC方式只限定於輔助索引,對於主鍵的創建和刪除同樣需要重建一張表。

3、Online Schema Change
Online SchemaChange(在線架構改變,簡稱OSC)。所謂“在線”是指在事務創建過程中,可以有讀寫事務對錶進行操作,這提高了原有MySQL數據庫在DDL操作時的併發性。

4、Online DDL
雖然FIC可以讓InnoDB存儲引擎避免創建臨時表,從而提高索引創建表的效率。但正如前面所說的,索引創建時會阻塞表上的DML操作。ODC雖然解決了上訴的部分問題,但是還是有很大的侷限性。MySQL5.6版本開始支持Online DDL(在線數據定義)操作,其允許輔助索引創建的同時,還允許其他諸如INSERT、UPSATE、DELETE這類DML操作,這極大提高了MySQL數據庫在生產環境中的可用性。

此外,不僅是輔助索引,以下這幾類DDL操作都可以通過“在線”的方式進行操作。

  • 輔助索引的創建與刪除
  • 改變自增長值
  • 添加或刪除外鍵約束
  • 列的重命名

5、Cardinality

5.1、什麼是Cardinality

並不是在所有的查詢條件中出現的列都需要添加索引。對於什麼時候添加B+樹索引,一般的經驗是,在訪問表中很少一部分時使用B+樹索引纔有意義。對於性別字段、地區字段、類型字段,它們可取之值的範圍很小,稱爲低選擇性。如:

SELECT * FROM student WHERE sex=‘M’

按性別進行查詢時,可取值的範圍一般只有‘M’,‘F’。因此上訴SQL語句得到的結果可能是該表50%的數據(假設男女比例1:1),這時添加B+樹索引時完全沒有必要的。相反,如果某個字段的取值範圍很廣,幾乎沒有重複,即屬於高選擇性,則此時使用B+樹是最合適的。

怎樣查看索引是否是高選擇性的呢?可以通過SHOW INDEX結果中的列Cardinality來觀察。Cardinality值非常關鍵,表示索引中不重複記錄數量的預估值。同時需要注意的是,Cardinality是一個預估值,而不是一個準確值,基本上用戶也不可能得到一個準確值。在實際應用中,Cardinality/n_rows_in_table應儘可能地接近1。如果非常小,那麼用戶需要考慮是否還有必要創建這個索引。故在訪問高選擇性屬性的字段並從表中取出很少一部分數據時,對這個字段添加B+樹索引時非常有必要的。

5.2、InnoDB存儲引擎的Cardinality統計

上文介紹了Cardinality的重要性,並且告訴讀者Cardinality表示選擇性。建立索引的前提是列中的數據時高選擇性的,這對數據庫來說才具有實際意義。然而數據庫怎樣來統計Cardinality信息呢?因爲MySQL數據庫中有各種不同的存儲引擎,而每種存儲引擎對於B+樹索引實現又各不相同,所以對Cardinality的統計是放在存儲引擎層進行的。

此外需要考慮到的是,在生產環境中,索引的更新操作可能會非常頻繁的。如果每次索引在發生操作時就對其進行Cardinality的統計,那麼將會給數據庫帶來很大的負擔。另外需要考慮的是,如果一張表的數據非常大,如一張表有50G的數據,那麼統計一次Cardinality信息所需要的時間可能非常長。這在生產環境下,也是不能接受的。因此,數據庫對於Cardinality的統計都是通過採樣的方法來完成的。

在InnoDB存儲引擎中,Cardinality統計信息的更新發生在兩個操作中:INSERT和UPDATE。根據前面的敘述,不可能在每次發生INSERT和UPDATE時就去更新Cardinality信息,這樣會增加數據庫系統的負荷,同時對於大表的統計,時間上也不允許數據庫這樣去操作。因此,InnoDB存儲引擎內部對於更新Cardinality信息的策略爲:

  • 表中1/16的數據已發生過變化
  • stat_modified_counter>2000000000

第一種策略爲自上次統計Cardinality信息後,表中1/16的數據已經發生過變化,這時需要更新Cardinality信息。第二種情況考慮的是,如果對錶中某一行數據頻繁地進行操作,這時表中的數據實際並沒有增加,實際發生變化的還是這一行數據,則第一種更新策略就無法適用這種情況。故在InnoDB存儲引擎內部有一個計數器stat_modified_counter,用來表示發生變化的次數,當stat_modified_counter大於2000000000時,則同樣需要更新Cardinality信息。

接着考慮InnoDB存儲引擎內部是怎樣來進行Cardinality信息的統計和更新操作的呢?同樣是通過採樣的方法。默認InnoDB存儲引擎對8個葉子節點進行採用。採樣的過程如下:

  • 取得B+樹索引中葉子節點的數量,記爲A
  • 隨機取得B+樹索引中的8個葉子節點。統計每個頁不同記錄個數,即爲P1,P2,。。。,P8。
  • 根據採樣信息給出Cardinality的預估值:Cardinality=(P1+P2+。。。+P8)*A/8。

通過上述的說明可以發現,在InnoDB存儲引擎中,Cardinality值是通過對8個葉子節點預估而得的,不是一個實際精確的值。再者,每次對Cardinality值的統計,都是通過隨機取8個葉子節點得到的,這同時又暗示了另一個Cardinality現象,即每次得到的Cardinality值可能是不同的。

6、B+樹索引的使用

6.1、不用應用中B+樹索引的使用

在瞭解了B+樹索引的本質和實現之後,下一個需要考慮的問題是怎麼正確地使用B+樹索引。

在OLTP應用中,B+樹索引建立後,對該索引的使用應該只是通過該索引取得表中少部分數據。這時建立B+樹索引纔是有意義的。

對於OLAP應用來說,情況稍顯複雜。

6.2、聯合索引

聯合索引是指對錶上的多個列進行索引。聯合索引從本質上來說也是一顆B+樹,不同的是聯合索引的鍵值的數量不是1,而是大於等於2。

6.3、覆蓋索引

覆蓋索引(covering index)指一個查詢語句的執行只用從索引中就能夠取得,不必從數據表中讀取。也可以稱之爲實現了索引覆蓋。

如果一個索引包含了(或覆蓋了)滿足查詢語句中字段與條件的數據就叫做覆蓋索引。

覆蓋索引實例:
1、未建立覆蓋索引前

select SQL_NO_CACHE count(name) from index_test where name = '小明' and 
id_card = '142701199999999999' and type = '2' and hobby = '籃球' and timeline = '1505270575';

這裏寫圖片描述

explain select SQL_NO_CACHE count(name) from index_test where name = '小明' 
and id_card = '142701199999999999' and type = '2' and hobby = '籃球' and timeline = '1505270575';

這裏寫圖片描述

2、添加覆蓋索引

alter table index_test add index index_all(name,id_card,type,hobby,timeline);

這裏寫圖片描述

3、總結
當一條查詢語句符合覆蓋索引條件時,sql只需要通過索引就可以返回查詢所需要的數據,這樣避免了查到索引後再返回表操作,減少I/O提高效率。
使用覆蓋索引Innodb比MyISAM效果更好—-InnoDB使用聚集索引組織數據,如果二級索引中包含查詢所需的數據,就不再需要在聚集索引中查找了

注:遇到以下情況,執行計劃不會選擇覆蓋查詢
1.select選擇的字段中含有不在索引中的字段 ,即索引沒有覆蓋全部的列。
2.where條件中不能含有對索引進行like的操作。

6.4、優化器選擇不適用索引的情況

這種情況多發生於範圍查找,JOIN鏈接操作等情況下。

7、哈希算法

哈希算法是一種常見的算法,時間複雜度爲O(1),且不只存在於索引中,每個數據庫應用中都存在該數據庫結構。

7.1、哈希表

哈希表也稱散列表,由直接尋址表改進而來。主要內容:哈希函數,碰撞解決技術等。

由於這部分比較基礎這裏就不在討論了。

7.2、InnoDB存儲引擎中的哈希算法

InnoDB存儲引擎使用哈希算法來對字典進行查找。其衝突機制採用鏈表方式,哈希函數採用除法散列方式。對於緩衝池頁的哈希表來說,在緩衝池中的Page頁都有一個chain指針,它指向相同的哈希函數值。而對於除法散列,m的取值爲略大於2倍的緩衝池數量的質數。

7.3、自適應哈希算法

自適應哈希算法採用哈希表實現。這僅是數據庫自身創建並使用的,DBA本身並不能對其進行干預。

8、全文檢索

8.1、概述

全文檢索是將存儲於數據庫中的整本書或這篇文章中的任意內容信息查找出來的技術。它可以根據需要獲得全文中有關章、節、段、句、詞等信息,也可以進行各種統計和分析。

8.2、到排索引

全文檢索通常是使用倒排索引來實現。倒排索引同B+樹索引一樣,也是一種索引結構。它在輔助表中存儲了單詞與單詞自身在一個或多個文檔中所在位置之間的映射。

9、小結

本篇文章介紹了一些常用的數據結構,如二分查找樹、平衡樹、B+樹、直接尋址表和哈希表,以及InnoDB1.2版本開始支持的全文檢索。從數據結構的角度切入數據庫中常見的B+樹索引和哈希索引的使用,並從內部機制上討論了使用上述索引的環境和優化方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章