關於索引,這是一個很是重要的知識點,一樣,在面試的時候也會被常常的問到;mysql
本文描述了索引的結構,介紹了InnoDB的索引方案等知識點,感興趣的能夠看一下;面試
本文參考文章:MySQL的索引sql
在上篇文章中咱們說到 InnoDB的數據頁結構 ,瞭解到了InnoDB
數據頁的 7 個組成部分,知道了各個數據頁能夠組成一個雙向鏈表
,而每一個數據頁中的記錄又能夠組成一個單向鏈表
(按照大小排序),每一個數據頁都會爲存儲在它裏邊兒的記錄生成一個頁目錄
,在經過主鍵查找某條記錄的時候能夠在頁目錄
中使用二分法快速定位到對應的槽,而後再遍歷該槽對應分組中的記錄便可快速找到指定的記錄。也瞭解到了在頁中各個部分的做用是啥,若是沒看的,建議回去看一下。數組
附上地址: InnoDB的數據頁結構數據結構
首先,咱們先來了解一下若是沒有索引的話,當咱們查找一條記錄的時候是怎樣進行的,固然,咱們就說精準匹配的時候,先附上一句SQL語句:post
SELECT column FROM table WHERE column = xxx;
複製代碼
上面這個類型的語句是咱們經常使用的,也比較簡單,下面咱們來看一下:ui
假設這個表中的數據量比較小,只有一頁的數據,這個時候的查找分爲如下狀況:spa
Page Directory
,經過二分法快速定位到對應的槽,而後再遍歷該槽對應分組中的記錄便可快速找到指定的記錄。上面的狀況是一種假設,但真實的狀況仍是須要如今整個居多,一個表中的記錄通常都是有不少的數據頁組成的,同時,在多個數據頁中的查找方式是這樣的:設計
咱們先準備一個表:3d
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
Query OK, 0 rows affected (0.03 sec)
mysql>
複製代碼
這個表使用Compact
行格式來實際存儲記錄的。爲了咱們理解上的方便,咱們簡化了一下index_demo
表的行格式示意圖:
先介紹一下上面幾個部分表明的含義:
record_type
: 記錄頭信息的一項屬性,表示記錄的類型,0
表示普通記錄、2
表示最小記錄、3
表示最大記錄、1
咱們還沒用過,等會再說~next_record
: 記錄頭信息的一項屬性,表示下一條地址的偏移量,爲了方便你們理解,咱們都會用箭頭來代表下一條記錄是誰。數據列
:就是各個數據列的值,其中咱們用橘黃色的格子表明c1
列,深藍色的格子表明c2
列,紅色格子表明c3
列。其它信息
:除了上述 3 種信息之外的全部信息,包括其餘隱藏列的值以及記錄的額外信息。但放入一些記錄以後的在頁的圖以下:
剛纔說了,爲何找記錄對應的頁的時候須要依次遍歷查找呢?由於沒有對應頁的目錄,沒有的話怎麼辦呢?建一個不就好了?咱們來看看。
咱們知道一頁中的記錄是按照大小進行依次連接的單向鏈表,因此,咱們使用頁創建目錄也須要遵照一樣的規則,因此咱們首先須要保障第二頁的記錄的主鍵值是大於第一頁的。因此就有了一個前提了。
爲了下面咱們更好的說明,咱們先作一個假設:
假設咱們的每一個數據頁最多能存放 3 條記錄(實際不是),有了這個假設以後咱們向
index_demo
表插入 3 條記錄:
mysql> INSERT INTO index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql>
複製代碼
那麼頁中的圖以下:
按主鍵進行大小排序的單向鏈表;
上面咱們作了假設,一個頁中最多隻能放三條記錄,這個時候咱們再插入一條數據:
mysql> INSERT INTO index_demo VALUES(4, 4, 'a');
Query OK, 1 row affected (0.00 sec)
mysql>
複製代碼
這個時候應該從新再分配一個頁:
咦?怎麼分配的頁號是28
呀,不該該是11
麼?須要注意的一點是,新分配的數據頁編號可能並非連續的,也就是說咱們使用的這些頁在存儲空間裏可能並不挨着。
上面咱們也說了,要先創建目錄須要遵照規則,上面圖中的頁明顯沒有,因此須要進行移動,過程以下:
這個過程代表了在對頁中的記錄進行增刪改操做的過程當中,咱們必須經過一些諸如記錄移動的操做來始終保證這個狀態一直成立:下一個數據頁的主鍵值必須大於上一個頁中的主鍵值。
上面說到條件已經知足了,下面須要進行目錄的創建了。
咱們接着往表中插入數據,獲得如下的結構:
注:數據頁的編號可能並非連續的
如今咱們來針對每個頁來創建目錄項,每一個目錄項包含如下兩個部分:
見下圖:
看到這樣的圖了,你們再來想一想,咱們只須要把幾個目錄項在物理存儲器上連續存儲,好比把他們放到一個數組裏,就能夠實現根據主鍵值快速查找某條記錄的功能了。比方說咱們想找主鍵值爲20
的記錄,咱們來看一下查找記錄的過程:
20
的記錄在目錄項3
中(由於 12 < 20 < 209
),它對應的頁是頁9
。頁9
中定位具體的記錄。針對數據頁作的簡易目錄就搞定了,怎麼樣?這樣是否是好多了,可能你們也知道了,沒錯,這個目錄也被咱們稱做 索引
。
上面的方案是一個簡單的索引方案,由於咱們假設全部目錄項均可以在物理存儲器上連續存儲,這樣的方案存在幾個問題:
忠於以上的狀況,咱們須要有更好的方式。
設計InnoDB
的大叔們須要一種能夠靈活管理全部目錄項
的方式。他們靈光乍現,突然發現這些目錄項
其實長得跟咱們的用戶記錄差很少,只不過目錄項
中的兩個列是主鍵
和頁號
而已,因此他們複用了以前存儲用戶記錄的數據頁來存儲目錄項,爲了和用戶記錄作一下區分,咱們把這些用來表示目錄項的記錄稱爲目錄項記錄
。那InnoDB
怎麼區分一條記錄是普通的用戶記錄仍是目錄項記錄
呢?別忘了記錄頭信息裏的record_type
屬性,它的各個取值表明的意思以下:
0
:普通的用戶記錄1
:目錄項記錄2
:最小記錄3
:最大記錄原來這個值爲1
的record_type
是這個意思呀,咱們把前邊使用到的目錄項放到數據頁中的樣子就是這樣:
咱們來講一下目錄項記錄
和用戶記錄
的區別:
目錄項記錄
的record_type
值是 1,而普通用戶記錄的record_type
值是 0。目錄項記錄
只有主鍵值和頁的編號兩個列,而普通的用戶記錄的列是用戶本身定義的,可能包含不少列,另外還有InnoDB
本身添加的隱藏列。除了上述幾點外,這二者就沒啥差異了,它們用的是同樣的數據頁,頁的組成結構也是同樣同樣的(就是咱們前邊介紹過的 7 個部分),都會爲主鍵值生成Page Directory
(頁目錄)以加快在頁內的查詢速度。
因此如今根據某個主鍵值去查找記錄的步驟能夠大體拆分紅下邊兩步,以查找主鍵爲20
的記錄爲例(由於都是從一個頁中經過主鍵查某條記錄,因此均可以使用Page Directory
經過二分法而實現快速查找):
目錄項記錄
的頁中經過二分法快速定位到對應目錄項,由於12 < 20 < 209
,因此定位到對應的記錄所在的頁就是頁9
。頁9
中根據二分法快速定位到主鍵值爲20
的用戶記錄(這個過程再也不多說)。雖說目錄項記錄中只是存儲主鍵值和對應的頁號,因爲一個頁中只有16KB的大小,能存放的目錄項記錄也是有限的,因此當一個頁存儲目錄項滿了以後再有的話就須要再來一個存儲目錄項記錄的頁。
爲了你們更好的理解如何新分配一個目錄項記錄
頁的過程,咱們假設一個存儲目錄項記錄
的頁最多隻能存放 4 條目錄項記錄
(請注意是假設哦,真實狀況下能夠存放好多條的),因此若是此時咱們再向上圖中插入一條主鍵值爲320
的用戶記錄的話,那就須要一個分配一個新的存儲目錄項記錄
的頁嘍:
以上圖中,因爲咱們新增了一條記錄,因此獲得了一個新的數據頁,裏面存放的是數據記錄,又由於頁30的頁目錄記錄存儲滿了(上面作了假設,假設每頁最多隻能存儲4條),因此有了頁32來存放頁31對應的目錄項。
由於存儲目錄項記錄
的頁不止一個,因此若是咱們想根據主鍵值查找一條用戶記錄大體須要 3 個步驟:
目錄項記錄
頁;
目錄項記錄
的頁有兩個,即頁30
和頁32
,又由於頁30
表示的目錄項的主鍵值的範圍是[1, 320)
,頁32
表示的目錄項的主鍵值不小於320
,因此主鍵值爲20
的記錄對應的目錄項記錄在頁30
中。目錄項記錄
頁肯定用戶記錄真實所在的頁;
目錄項記錄
中定位一條目錄項記錄的方式說過了(經過二分查找進行定位,找到對應的頁)。那麼問題來了,在這個查詢步驟的第 1 步中咱們須要定位存儲目錄項記錄
的頁,可是這些頁在存儲空間中也可能不挨着,若是咱們表中的數據很是多則會產生不少存儲目錄項記錄
的頁,那咱們怎麼根據主鍵值快速定位一個存儲目錄項記錄
的頁呢?
其實也簡單,爲這些存儲目錄項記錄
的頁再生成一個更高級的目錄,就像是一個多級目錄同樣,大目錄裏嵌套小目錄,小目錄裏纔是實際的數據,因此如今各個頁的示意圖就是這樣子:
如圖,咱們生成了一個存儲更高級目錄項的頁33
,這個頁中的兩條記錄分別表明頁30
和頁32
,若是用戶記錄的主鍵值在[1, 320)
之間,則到頁30
中查找更詳細的目錄項記錄
,若是主鍵值不小於320
的話,就到頁32
中查找更詳細的目錄項記錄
。
隨着表中記錄的增長,這個目錄的層級會繼續增長,若是簡化一下,那麼咱們能夠用下邊這個圖來描述它:
其實這是一種組織數據的形式,或者說是一種數據結構,它的名稱是B+
樹。
由於咱們把數據頁都存放到B+
樹這個數據結構中了,因此咱們也把咱們的數據頁稱爲節點
。從圖中能夠看出來,咱們的實際用戶記錄其實都存放在 B + 樹的最底層的節點上,這些節點也被稱爲葉子節點
或葉節點
,其他的節點都是用來存放目錄項
的,這些節點通通被稱爲內節點
或者說非葉節點
。其中最上邊的那個節點也稱爲根節點
。
從圖中能夠看出來,一個B+
樹的節點其實能夠分紅好多層,設計InnoDB
的大叔們爲了討論方便,規定最下邊的那層,也就是存放咱們用戶記錄的那層爲第0
層,以後依次往上加。上邊咱們作了一個很是極端的假設,存放用戶記錄的頁最多存放 3 條記錄,存放目錄項記錄的頁最多存放 4 條記錄,其實真實環境中一個頁存放的記錄數量是很是大的,假設,假設,假設全部的數據頁,包括存儲真實用戶記錄和目錄項記錄的頁,均可以存放1000
條記錄,那麼:
B+
樹只有 1 層,也就是隻有 1 個用於存放用戶記錄的節點,最多能存放1000
條記錄。B+
樹有 2 層,最多能存放1000×1000=1000000
條記錄。B+
樹有 3 層,最多能存放1000×1000×1000=1000000000
條記錄。B+
樹有 4 層,最多能存放1000×1000×1000×1000=1000000000000
條記錄。你的表裏能存放1000000000000
條記錄麼?因此通常狀況下,咱們用到的B+
樹都不會超過 4 層,那咱們經過主鍵去查找某條記錄最多隻須要作 4 個頁面內的查找,又由於在每一個頁面內有所謂的Page Directory
(頁目錄),因此在頁面內也能夠經過二分法實現快速定位記錄。
上面所說的B+樹,咱們知道了B+樹自己就是一個目錄,或者說它自己就是一個索引,它有如下特色:
B+
樹的葉子節點存儲的是完整的用戶記錄。
咱們把具備這兩種特性的B+
樹稱爲聚簇索引
,全部完整的用戶記錄都存放在這個聚簇索引
的葉子節點處;
換句話說主鍵索引就是聚簇索引;
聚簇索引
並不須要咱們在MySQL
語句中顯式的去建立,InnoDB
存儲引擎會自動的爲咱們建立聚簇索引。另外有趣的一點是,在InnoDB
存儲引擎中,聚簇索引
就是數據的存儲方式(全部的用戶記錄都存儲在了葉子節點
),也就是所謂的索引即數據。
上面也說到了聚簇索引是針對主鍵值時才能發揮做用,那麼當索引爲其它列的時候,又是怎樣的呢?難道只能從頭至尾沿着鏈表依次遍歷記錄麼?
不,咱們能夠多建幾棵B+
樹,不一樣的B+
樹中的數據採用不一樣的排序規則。比方說咱們用c2
列的大小做爲數據頁、頁中記錄的排序規則,再建一棵B+
樹,效果以下圖所示:
這個B+樹與上邊介紹的聚簇索引有幾處不一樣:
因此若是咱們如今想經過c2
列的值查找某些記錄的話就可使用咱們剛剛建好的這個B+
樹了,以查找c2
列的值爲4
的記錄爲例,查找過程以下:
目錄項記錄
頁
根頁面
,也就是頁44
,能夠快速定位到目錄項記錄
所在的頁爲頁42
(由於2 < 4 < 9
)。目錄項記錄
頁肯定用戶記錄真實所在的頁。
頁42
中能夠快速定位到實際存儲用戶記錄的頁,可是因爲c2
列並無惟一性約束,因此c2
列值爲4
的記錄可能分佈在多個數據頁中,又由於2 < 4 ≤ 4
,因此肯定實際存儲用戶記錄的頁在頁34
和頁35
中。頁34
和頁35
中定位到具體的記錄。B+
樹的葉子節點中的記錄只存儲了c2
和c1
(也就是主鍵
)兩個列,因此咱們必須再根據主鍵值去聚簇索引中再查找一遍完整的用戶記錄。你們可能頁看到了,當最後定位到對應記錄的時候,獲得的是一個主鍵,而獲得主鍵後仍然須要到聚簇索引
中再查一遍,這個過程也被稱爲回表
。也就是根據c2
列的值查詢一條完整的用戶記錄須要使用到2
棵
B+
樹!!!
可能您會想,爲何須要回表呢?直接查出來不行嗎?
固然能夠,可是您想一想,一個表中,每當咱們創建一個索引就須要把記錄拷貝一份到B+樹,是否是太浪費存儲空間了。由於這種按照非主鍵列
創建的B+
樹須要一次回表
操做才能夠定位到完整的用戶記錄,因此這種B+
樹也被稱爲二級索引
或者輔助索引
。
咱們有時候也會使用多個列作聯合索引,也就是同時爲多個列創建索引,比方說咱們想讓B+
樹按照c2
和c3
列的大小進行排序,這個包含兩層:
c2
列進行排序。c2
列相同的狀況下,採用c3
列進行排序爲c2
和c3
列創建的索引的示意圖以下:
目錄項記錄
都由c2
、c3
、頁號
這三個部分組成,各條記錄先按照c2
列的值進行排序,若是記錄的c2
列相同,則按照c3
列的值進行排序。B+
樹葉子節點處的用戶記錄由c2
、c3
和主鍵c1
列組成。以 c2 和 c3 列的大小爲排序規則創建的B+
樹稱爲聯合索引
,它的意思與分別爲 c2 和 c3 列創建索引的表述是不一樣的,不一樣點以下:
聯合索引
只會創建如上圖同樣的 1 棵B+
樹。c2
和c3
列的大小爲排序規則創建 2 棵B+
樹。InnoDB
存儲引擎來講,在單個頁中查找某條記錄分爲兩種狀況:
Page Directory
經過二分法快速定位相應的用戶記錄。InnoDB
存儲引擎的索引是一棵B+
樹,完整的用戶記錄都存儲在B+
樹第0
層的葉子節點,其餘層次的節點都屬於內節點
,內節點
裏存儲的是目錄項記錄
。InnoDB
的索引分爲兩大種:
列 + 主鍵
,因此每次查找的數據都會先獲得主鍵,而獲得主鍵後仍然須要到聚簇索引
中再查一遍,這個過程也被稱爲回表
。也就是根據c2
列的值查詢一條完整的用戶記錄須要使用到2
棵
B+
樹!!!最後說一下,本文的參考文章: MySQL的索引 。
本文的不少內容也是來自這篇文章,本人只在文章中插入了有關本身對於文章的理解,若是說的不對,還望指教。
你們也能夠去看一下原文。