MySQL實戰 | 為什麼要使用索引?

不正經程式設計師發表於2018-12-05

宣告

MySQL專欄學習系列,基本上是本人學習極客時間《MySQL實戰45講》專欄內容的筆記,跟隨專欄的架構,並在專欄基礎上進行知識點挖掘。侵刪。

MySQL實戰 | 為什麼要使用索引?

用過 MySQL 的應該都知道索引是幹啥的吧,應該多少都設定過索引,但是若是問你索引是怎麼實現的,你能說上來嗎?

索引是什麼?

MySQL 官方對索引的定義為:索引是幫助 MySQL 高效獲取資料的資料結構。

在資料之外,資料庫系統還維護著滿足特定查詢演算法的資料結構,這些資料結構以某種方式引用(指向)資料,這樣就可以在這些資料結構上實現高階查詢演算法。這種資料結構,就是索引。

索引的出現就是為了提高查詢效率,就像書的目錄。其實說白了,索引要解決的就是查詢問題。

查詢,是資料庫所提供的一個重要功能,我們都想盡可能快的獲取到目標資料,因此就需要最佳化資料庫的查詢演算法,選擇合適的查詢模型來實現索引。

另外,為表設定索引要付出代價的:一是增加了資料庫的儲存空間,二是在插入和修改資料時要花費較多的時間,因為索引也要隨之變動。

常見查詢模型

索引的實現模型有很多,這裡我們先了解一下常用的查詢模型。

順序陣列

順序陣列是一種特殊的陣列,裡面的元素,按一定的順序排列。

順序陣列在查詢上有著一定的優勢,因為是有序的資料,採用二分查詢的話,時間複雜度是 O(log(N))

MySQL實戰 | 為什麼要使用索引?

順序陣列的優點就是查詢效率非常高,但是要更新資料的話,就非常麻煩了。刪除和插入元素都要涉及到大量元素位置的移動,成本很高。

因此,對於順序陣列更適合用於查詢的領域,適合儲存一些改動較小的靜態儲存引擎。

雜湊索引

雜湊表是一種以 鍵-值(key-value) 儲存資料的結構,我們只要輸入待查詢的值即 key,就可以找到其對應的值即 value。

雜湊索引採用一定的雜湊演算法,對於每一行,儲存引擎計算出了被索引欄位的雜湊碼(Hash Code),把雜湊碼儲存在索引中,並且儲存了一個指向雜湊表中的每一行的指標。

這樣在檢索時只需一次雜湊演算法即可立刻定位到相應的位置,速度非常快。

Hash 索引結構的特殊性,其檢索效率非常之高,應該是 O(1) 的時間複雜度。

MySQL實戰 | 為什麼要使用索引?

雖然 Hash 索引效率高,但是 Hash 索引本身由於其特殊性也帶來了很多限制和弊端,主要有以下這些:

1、Hash索引僅僅能滿足 =IN 和 <=> 查詢,如果是範圍查詢檢索,這時候雜湊索引就毫無用武之地了。

因為原先是有序的鍵值,經過雜湊演算法後,有可能變成不連續的了,就沒辦法再利用索引完成範圍查詢檢索;

2、Hash 索引無法利用索引完成排序,因為存放的時候是經過 Hash 計算過的,計算的 Hash 值和原始資料不一定相等,所以無法排序;

3、聯合索引中,Hash 索引不能利用部分索引鍵查詢

Hash 索引在計算 Hash 值的時候是聯合索引鍵合併後再一起計算 Hash 值,而不是單獨計算 Hash 值。

所以對於聯合索引中的多個列,Hash 是要麼全部使用,要麼全部不使用。透過前面一個或幾個索引鍵進行查詢的時候,Hash 索引也無法被利用。

4、Hash索引在任何時候都不能避免表掃描

前面已經知道,Hash 索引是將索引鍵透過 Hash 運算之後,將 Hash 運算結果的 Hash 值和所對應的行指標資訊存放於一個 Hash 表中,由於不同索引鍵可能存在相同 Hash 值,所以即使取滿足某個 Hash 鍵值的資料的記錄條數,也無法從 Hash 索引中直接完成查詢,還是要透過訪問表中的實際資料進行相應的比較,並得到相應的結果。

5、在有大量重複鍵值情況下,雜湊索引的效率也是極低的,因為存在所謂的雜湊碰撞問題。

綜上,雜湊表這種結構適用於只有等值查詢的場景,比如 Memcached、redis 及其他一些 NoSQL 引擎。

二叉搜尋樹索引

二叉搜尋樹的每個節點都只儲存一個鍵值,並且左子樹(如果有)所有節點的值都要小於根節點的值,右子樹(如果有)所有節點的值都要大於根節點的值。

當二叉搜尋樹的所有非葉子節點的左右子樹的節點數目均保持差不多時(平衡),這時樹的搜尋效能逼近二分查詢;並且它比連續記憶體空間的二分查詢更有優勢的是,改變樹結構(插入與刪除結點)不需要移動大段的記憶體資料,甚至通常是常數開銷。

特殊情況下,根節點的左右子樹的高度相差不超過 1 時,這樣的二叉樹被稱為平衡二叉樹;與之相對的是,二叉搜尋樹有可能退化成線性樹。


MySQL實戰 | 為什麼要使用索引?


下圖展示了一種可能的索引方式。左邊是資料表,一共有兩列七條記錄,最左邊的是資料記錄的實體地址(注意邏輯上相鄰的記錄在磁碟上也並不是一定物理相鄰的)。

MySQL實戰 | 為什麼要使用索引?

為了加快 Col2 的查詢,可以維護一個右邊所示的二叉查詢樹,每個節點分別包含索引鍵值和一個指向對應資料記錄實體地址的指標,這樣就可以運用二叉查詢在 O(log2n) 的複雜度內獲取到相應資料。

B樹

看得出來,二叉樹在查詢和修改上做到了一個平衡,都有著不錯的效率,但是現實是很少有資料庫引擎使用二叉樹來實現索引,為什麼呢?

資料庫儲存大多不適用二叉樹,資料量較大時,樹高會過高。

你可以想象一下一棵 100 萬節點的平衡二叉樹,樹高 20,每個葉子結點就是一個塊,每個塊包含兩個資料,塊之間透過鏈式方式連結。

MySQL實戰 | 為什麼要使用索引?

樹高 20 的話,就要遍歷 20 個塊才能得到目標資料,索引儲存在磁碟時,這將是非常耗時的。

因此,為了減少磁碟的讀取,查詢時就要儘量少的遍歷資料塊,因此一般使用 N 叉樹。


這裡就有了 B樹(Balanced Tree)。

MySQL實戰 | 為什麼要使用索引?

究竟什麼是 B 樹?

我們先看一個例子:

MySQL實戰 | 為什麼要使用索引?

從上圖你能輕易的看到,一個內結點 x 若含有 n[x] 個關鍵字,那麼 x 將含有 n[x]+1 個子女。如含有 2 個關鍵字 D H 的內結點有 3 個子女,而含有 3 個關鍵字 Q T X 的內結點有 4 個子女。

B 樹的特性

普及一些概念:

節點的度:一個節點含有的子樹的個數稱為該節點的度;
樹的度:一棵樹中,最大的節點的度稱為樹的度;
葉節點或終端節點:度為零的節點;
非終端節點或分支節點:度不為零的節點;

首先定義兩個變數:d 為大於 1 的一個正整數,稱為 B 樹的度。h 為一個正整數,稱為 B 樹的高度。

B 樹是滿足下列條件的資料結構:

1、每個非葉子節點由 n-1 個 key 和 n 個指標組成,其中 d<=n<=2d。

2、每個葉子節點最少包含一個 key 和兩個指標,最多包含 2d-1 個 key 和 2d 個指標,葉節點的指標均為 null 。

3、除根結點和葉子結點外,其它每個結點至少有 [ceil(m / 2)] 個孩子(其中 ceil(x) 是一個取上限的函式);

4、所有葉節點具有相同的深度,等於樹高 h,且葉子結點不包含任何關鍵字資訊。

5、key 和指標互相間隔,節點兩端是指標。

6、一個節點中的 key 從左到右非遞減排列。

7、每個指標要麼為 null,要麼指向另外一個節點。

8、每個非終端結點中包含有 n 個關鍵字資訊: (n,P0,K1,P1,K2,P2,……,Kn,Pn)。

其中:
a) Ki (i=1…n) 為關鍵字,且關鍵字按順序升序排序 K(i-1)< Ki。 
b) Pi 為指向子樹根的接點,且指標 P(i-1) 指向子樹種所有結點的關鍵字均小於 Ki,但都大於 K(i-1)。 
c) 關鍵字的個數 n 必須滿足: [ceil(m / 2)-1]<= n <= m-1。

B 樹查詢過程

由於 B 樹的特性,在 B 樹中按 key 檢索資料的演算法非常直觀:首先從根節點進行二分查詢,如果找到則返回對應節點的 data,否則對相應區間的指標指向的節點遞迴進行查詢,直到找到節點或找到 null 指標,前者查詢成功,後者查詢失敗。

MySQL實戰 | 為什麼要使用索引?

如上圖所示,我們來模擬下查詢檔案 29 的過程:

1、根據根結點指標找到檔案目錄的根磁碟塊 1,將其中的資訊匯入記憶體。【磁碟 IO 操作 1 次】

2、此時記憶體中有兩個檔名 17、35 和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現:17<29<35,因此我們找到指標 p2;

3、根據 p2 指標,我們定位到磁碟塊 3,並將其中的資訊匯入記憶體。【磁碟 IO 操作 20次】

4、此時記憶體中有兩個檔名 26,30 和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現:26<29<30,因此我們找到指標 p2;

5、根據 p2 指標,我們定位到磁碟塊 8,並將其中的資訊匯入記憶體。【磁碟 IO 操作 3 次】;

6、此時記憶體中有兩個檔名 28,29。根據演算法我們查詢到檔名 29,並定位了該檔案記憶體的磁碟地址。

分析上面的過程,發現需要 3 次磁碟 IO 操作和 3 次記憶體查詢操作。關於記憶體中的檔名查詢,由於是一個有序表結構,可以利用折半查詢提高效率。

B+ 樹

B+ 樹:是應檔案系統所需而產生的一種 B 樹的變形樹。

一棵 m 階的 B+ 樹和 m 階的 B 樹的異同點在於

1、每個節點的指標上限為 2d 而不是2d+1。

2、所有的葉子結點中包含了全部關鍵字的資訊,及指向含有這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大的順序連結。(B 樹的葉子節點並沒有包括全部需要查詢的資訊)

3、所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字,不儲存 data。(B 樹的非終節點也包含需要查詢的有效資訊)

MySQL實戰 | 為什麼要使用索引?

為什麼說 B+ 樹比 B 樹更適合做資料庫索引?

1)B+ 樹的磁碟讀寫代價更低

B+ 樹的內部結點並沒有儲存關鍵字具體資訊。因此其內部結點相對 B 樹更小。

如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入記憶體中的需要查詢的關鍵字也就越多。相對來說 IO 讀寫次數也就降低了。

2) B+ 樹的查詢效率更加穩定

由於非終端結點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,進而每一個資料的查詢效率相當。

幾種樹的對比

MySQL實戰 | 為什麼要使用索引?


MySQL實戰 | 為什麼要使用索引?

以上,為了介紹索引內容,我們花費了大量的篇幅介紹了幾種資料結構模型,特別是樹的相關概念。

另外,涉及到樹的新增和刪除元素,操作更加複雜,本文篇幅有限(其實是小編也搞不太明白),這裡就不再展開。

有興趣的,強烈建議鑽研下參考連結裡的內容。

好了,下面我們來看 MySQL 中的 InnoDB 引擎的索引是如何實現的。

MySQL 的索引模型

說了這麼多,終於到索引出場了。

索引就是這種神奇偉大的存在。索引相當於資料庫的表資料之外新建的資料結構,該資料結構的資料段中儲存著欄位的值以及指向實際資料記錄的指標。

資料庫表的索引從資料儲存方式上可以分為聚簇索引和非聚簇索引(又叫二級索引)兩種。

1、聚簇索引

表資料按照索引的順序來儲存的,也就是說索引項的順序與表中記錄的物理順序一致。

對於聚簇索引,葉子結點即儲存了真實的資料行,不再有另外單獨的資料頁。 在一張表上最多隻能建立一個聚集索引,因為真實資料的物理順序只能有一種。

聚簇集是指實際的資料行和相關的鍵值都儲存在一起。

注意:資料的物理存放順序與索引順序是一致的,即:只要索引是相鄰的,那麼對應的資料一定也是相鄰地存放在磁碟上的。


如果主鍵不是自增 id,那麼可以想象,它會幹些什麼,不斷地調整資料的實體地址、分頁。如果是自增的,那就簡單了,它只需要一頁一頁地寫,索引結構相對緊湊,磁碟碎片少,效率也高。

聚簇索引的二級索引:葉子節點不會儲存引用的行的物理位置,而是儲存了行的主鍵值

2、非聚集索引

表資料儲存順序與索引順序無關。對於非聚集索引,葉結點包含索引欄位值及指向資料頁資料行的邏輯指標,其行數量與資料錶行資料量一致。

聚簇索引是對磁碟上實際資料重新組織以按指定的一個或多個列的值排序的演算法。特點是儲存資料的順序和索引順序一致。一般情況下主鍵會預設建立聚簇索引,且一張表只允許存在一個聚簇索引。

這兩個名字雖然都叫做索引,但這並不是一種單獨的索引型別,而是一種資料儲存方式。

下面,我們可以看一下 MYSQL 中 MyISAM 和 InnoDB 兩種引擎的索引結構。

MyISAM索引實現

MyISAM 引擎使用 B+ 樹作為索引結構,葉節點的 data 域存放的是資料記錄的地址,就是非聚集索引。

下圖是 MyISAM 索引的原理圖:

MySQL實戰 | 為什麼要使用索引?

在 MyISAM 中,主鍵索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主鍵索引要求 key 是唯一的,而輔助索引的 key 可以重複。

InnoDB索引實現

雖然 InnoDB 也使用 B+ 樹作為索引結構,但具體實現方式卻與 MyISAM 截然不同。

第一個重大區別是 InnoDB 的資料檔案本身就是索引檔案

在 InnoDB 中,表資料檔案本身就是按 B+ 樹組織的一個索引結構,這棵樹的葉節點 data 域儲存了完整的資料記錄。這個索引的 key 是資料表的主鍵,因此 InnoDB 表資料檔案本身就是主索引。

另外,第二個與 MyISAM 索引的不同是 InnoDB 的輔助索引 data 域儲存相應記錄主鍵的值而不是地址

對於聚簇索引儲存來說,行資料和主鍵 B+ 樹儲存在一起,輔助索引只儲存輔助鍵和主鍵,主鍵和非主鍵 B+ 樹幾乎是兩種型別的樹。

對於非聚簇索引儲存來說,主鍵 B+ 樹在葉子節點儲存指向真正資料行的指標,而非主鍵。


為了更形象說明這兩種索引的區別,我們假想一個表如下圖儲存了 4 行資料。其中 Id 作為主索引,Name 作為輔助索引。圖示清晰的顯示了聚簇索引和非聚簇索引的差異。

MySQL實戰 | 為什麼要使用索引?


MySQL實戰 | 為什麼要使用索引?

對於聚簇索引,若使用主鍵索引進行查詢where id = 14 這樣的條件查詢主鍵,則按照 B+ 樹的檢索演算法即可查詢到對應的葉節點,之後獲得行資料。

若使用輔助索引進行查詢,對 Name 列進行條件搜尋,則需要兩個步驟:

1、第一步在輔助索引 B+ 樹中檢索 Name,到達其葉子節點獲取對應的主鍵
2、第二步根據主鍵在主索引 B+ 樹種再執行一次 B+ 樹檢索操作,最終到達葉子節點即可獲取整行資料。這個過程稱為回表

聚簇索引的優勢在哪?

1、由於行資料和葉子節點儲存在一起,這樣主鍵和行資料是一起被載入記憶體的,找到葉子節點就可以立刻將行資料返回了,如果按照主鍵 Id 來組織資料,獲得資料更快。

2、輔助索引使用主鍵作為指標而不是使用地址值作為指標的好處是,減少了當出現行移動或者資料頁分裂時輔助索引的維護工作。

使用主鍵值當作指標會讓輔助索引佔用更多的空間,換來的好處是 InnoDB 在移動行時無須更新輔助索引中的這個指標

也就是說行的位置會隨著資料庫裡資料的修改而發生變化,使用聚簇索引就可以保證不管這個主鍵 B+ 樹的節點如何變化,輔助索引樹都不受影響。

小結

這次內容比較多,涉及到了一些資料結構的內容,我也是翻了很多部落格才搞懂那麼一點點。主要是要搞懂,為什麼要用索引,以及索引的查詢流程。

希望對你有用。


參考:
http://blog.codinglabs.org/articles/theory-of-mysql-index.html
https://blog.csdn.net/v_JULY_v/article/details/6530142

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559358/viewspace-2284192/,如需轉載,請註明出處,否則將追究法律責任。

相關文章