說說MySQL索引相關

前言

關於索引,這是一個很是重要的知識點,一樣,在面試的時候也會被常常的問到;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麼?須要注意的一點是,新分配的數據頁編號可能並非連續的,也就是說咱們使用的這些頁在存儲空間裏可能並不挨着。

上面咱們也說了,要先創建目錄須要遵照規則,上面圖中的頁明顯沒有,因此須要進行移動,過程以下:

這個過程代表了在對頁中的記錄進行增刪改操做的過程當中,咱們必須經過一些諸如記錄移動的操做來始終保證這個狀態一直成立:下一個數據頁的主鍵值必須大於上一個頁中的主鍵值

  • 目錄創建

上面說到條件已經知足了,下面須要進行目錄的創建了。

咱們接着往表中插入數據,獲得如下的結構:

注:數據頁的編號可能並非連續的

如今咱們來針對每個頁來創建目錄項,每一個目錄項包含如下兩個部分:

  • 頁目錄中的一部分是存儲的該頁中最小的主鍵值,咱們用key來表示;
  • 頁目錄中的另外一部分是頁號,咱們用page_no表示;

見下圖:

看到這樣的圖了,你們再來想一想,咱們只須要把幾個目錄項在物理存儲器上連續存儲,好比把他們放到一個數組裏,就能夠實現根據主鍵值快速查找某條記錄的功能了。比方說咱們想找主鍵值爲20的記錄,咱們來看一下查找記錄的過程:

  • 先從目錄項中根據二分法快速肯定出主鍵值爲20的記錄在目錄項3中(由於 12 < 20 < 209),它對應的頁是頁9
  • 再根據前邊說的在頁中查找記錄的方式去頁9中定位具體的記錄。

針對數據頁作的簡易目錄就搞定了,怎麼樣?這樣是否是好多了,可能你們也知道了,沒錯,這個目錄也被咱們稱做 索引

InnoDB的索引方案

上面的方案是一個簡單的索引方案,由於咱們假設全部目錄項均可以在物理存儲器上連續存儲,這樣的方案存在幾個問題:

  • InnoDB是使用頁做爲管理存儲空間的基本單位,最多隻能保證16KB的連續存儲空間,當表中的記錄慢慢增多的時候就須要很是大的連續的存儲空間才能把全部的目錄放下。
  • 再從咱們常作的操做來分析,當咱們對錶中的記錄進行增刪的時候,假設咱們對以上頁28中的記錄進行刪除,那麼頁28頁就沒有存在的必要了,同時目錄2也同樣沒有存在的必要,這個時候就須要把目錄2後面的目錄項往前移動,影響這麼大可不是什麼好辦法。

忠於以上的狀況,咱們須要有更好的方式。

設計InnoDB的大叔們須要一種能夠靈活管理全部目錄項的方式。他們靈光乍現,突然發現這些目錄項其實長得跟咱們的用戶記錄差很少,只不過目錄項中的兩個列是主鍵頁號而已,因此他們複用了以前存儲用戶記錄的數據頁來存儲目錄項,爲了和用戶記錄作一下區分,咱們把這些用來表示目錄項的記錄稱爲目錄項記錄。那InnoDB怎麼區分一條記錄是普通的用戶記錄仍是目錄項記錄呢?別忘了記錄頭信息裏的record_type屬性,它的各個取值表明的意思以下:

  • 0:普通的用戶記錄
  • 1:目錄項記錄
  • 2:最小記錄
  • 3:最大記錄

原來這個值爲1record_type是這個意思呀,咱們把前邊使用到的目錄項放到數據頁中的樣子就是這樣:

咱們來講一下目錄項記錄用戶記錄的區別:

  1. 目錄項記錄record_type值是 1,而普通用戶記錄的record_type值是 0。
  2. 目錄項記錄只有主鍵值和頁的編號兩個列,而普通的用戶記錄的列是用戶本身定義的,可能包含不少列,另外還有InnoDB本身添加的隱藏列。

除了上述幾點外,這二者就沒啥差異了,它們用的是同樣的數據頁,頁的組成結構也是同樣同樣的(就是咱們前邊介紹過的 7 個部分),都會爲主鍵值生成Page Directory(頁目錄)以加快在頁內的查詢速度。

因此如今根據某個主鍵值去查找記錄的步驟能夠大體拆分紅下邊兩步,以查找主鍵爲20的記錄爲例(由於都是從一個頁中經過主鍵查某條記錄,因此均可以使用Page Directory經過二分法而實現快速查找):

  • 先到存儲目錄項記錄的頁中經過二分法快速定位到對應目錄項,由於12 < 20 < 209,因此定位到對應的記錄所在的頁就是頁9
  • 頁9中根據二分法快速定位到主鍵值爲20的用戶記錄(這個過程再也不多說)。

雖說目錄項記錄中只是存儲主鍵值和對應的頁號,因爲一個頁中只有16KB的大小,能存放的目錄項記錄也是有限的,因此當一個頁存儲目錄項滿了以後再有的話就須要再來一個存儲目錄項記錄的頁。

爲了你們更好的理解如何新分配一個目錄項記錄頁的過程,咱們假設一個存儲目錄項記錄的頁最多隻能存放 4 條目錄項記錄(請注意是假設哦,真實狀況下能夠存放好多條的),因此若是此時咱們再向上圖中插入一條主鍵值爲320的用戶記錄的話,那就須要一個分配一個新的存儲目錄項記錄的頁嘍:

以上圖中,因爲咱們新增了一條記錄,因此獲得了一個新的數據頁,裏面存放的是數據記錄,又由於頁30的頁目錄記錄存儲滿了(上面作了假設,假設每頁最多隻能存儲4條),因此有了頁32來存放頁31對應的目錄項。

由於存儲目錄項記錄的頁不止一個,因此若是咱們想根據主鍵值查找一條用戶記錄大體須要 3 個步驟:

  1. 肯定目錄項記錄頁;
    1. 咱們如今的存儲目錄項記錄的頁有兩個,即頁30頁32,又由於頁30表示的目錄項的主鍵值的範圍是[1, 320)頁32表示的目錄項的主鍵值不小於320,因此主鍵值爲20的記錄對應的目錄項記錄在頁30中。
  2. 經過目錄項記錄頁肯定用戶記錄真實所在的頁;
    1. 在一個存儲目錄項記錄中定位一條目錄項記錄的方式說過了(經過二分查找進行定位,找到對應的頁)。
  3. 在真實存儲用戶記錄的頁中定位到具體的記錄;
    1. 很少說了。

那麼問題來了,在這個查詢步驟的第 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列的大小進行記錄和頁排序
    • 頁內是按照c2列的大小進行排序造成的一個單向鏈表。
    • 各個存放用戶記錄的頁也是根據頁中記錄的c2列大小順序排成的一個雙向鏈表。
    • 各個存放目錄項的頁根據頁中記錄的c2列的大小排成的雙向鏈表。
  • B+樹的葉子節點並非完整的用戶記錄,而是c2列+主鍵這兩個列的值
  • 目錄項記錄再也不是主鍵+頁號,而是c2列+頁號

因此若是咱們如今想經過c2列的值查找某些記錄的話就可使用咱們剛剛建好的這個B+樹了,以查找c2列的值爲4的記錄爲例,查找過程以下:

  • 肯定目錄項記錄
    • 根據根頁面,也就是頁44,能夠快速定位到目錄項記錄所在的頁爲頁42(由於2 < 4 < 9)。
  • 經過目錄項記錄頁肯定用戶記錄真實所在的頁。
    • 頁42中能夠快速定位到實際存儲用戶記錄的頁,可是因爲c2列並無惟一性約束,因此c2列值爲4的記錄可能分佈在多個數據頁中,又由於2 < 4 ≤ 4,因此肯定實際存儲用戶記錄的頁在頁34頁35中。
  • 在真實存儲用戶記錄的頁中定位到具體的記錄。
    • 頁34頁35中定位到具體的記錄。
  • 可是這個B+樹的葉子節點中的記錄只存儲了c2c1(也就是主鍵)兩個列,因此咱們必須再根據主鍵值去聚簇索引中再查找一遍完整的用戶記錄。

你們可能頁看到了,當最後定位到對應記錄的時候,獲得的是一個主鍵,而獲得主鍵後仍然須要到聚簇索引中再查一遍,這個過程也被稱爲回表 。也就是根據c2列的值查詢一條完整的用戶記錄須要使用到2 B+ 樹!!!

可能您會想,爲何須要回表呢?直接查出來不行嗎?

固然能夠,可是您想一想,一個表中,每當咱們創建一個索引就須要把記錄拷貝一份到B+樹,是否是太浪費存儲空間了。由於這種按照非主鍵列創建的B+樹須要一次回表操做才能夠定位到完整的用戶記錄,因此這種B+樹也被稱爲二級索引 或者輔助索引

聯合索引

咱們有時候也會使用多個列作聯合索引,也就是同時爲多個列創建索引,比方說咱們想讓B+樹按照c2c3列的大小進行排序,這個包含兩層:

  • 先把各個記錄和頁按照c2列進行排序。
  • 在記錄的c2列相同的狀況下,採用c3列進行排序

c2c3列創建的索引的示意圖以下:

  • 每條目錄項記錄都由c2c3頁號這三個部分組成,各條記錄先按照c2列的值進行排序,若是記錄的c2列相同,則按照c3列的值進行排序。
  • B+樹葉子節點處的用戶記錄由c2c3和主鍵c1列組成。

以 c2 和 c3 列的大小爲排序規則創建的B+樹稱爲聯合索引,它的意思與分別爲 c2 和 c3 列創建索引的表述是不一樣的,不一樣點以下:

  • 創建聯合索引只會創建如上圖同樣的 1 棵B+樹。
  • 爲 c2 和 c3 列創建索引會分別以c2c3列的大小爲排序規則創建 2 棵B+樹。

總結

  • 對於InnoDB存儲引擎來講,在單個頁中查找某條記錄分爲兩種狀況:
    1. 以主鍵爲搜索條件,可使用Page Directory經過二分法快速定位相應的用戶記錄。
    2. 以其餘列爲搜索條件,須要按照記錄組成的單鏈表依次遍歷各條記錄。
  • 沒有索引的狀況下,不管是以主鍵仍是其餘列做爲搜索條件,只能沿着頁的雙鏈表從左到右依次遍歷各個頁。
  • InnoDB存儲引擎的索引是一棵B+樹,完整的用戶記錄都存儲在B+樹第0層的葉子節點,其餘層次的節點都屬於內節點內節點裏存儲的是目錄項記錄InnoDB的索引分爲兩大種:
    • 聚簇索引
      • 以主鍵值的大小爲頁和記錄的排序規則,在葉子節點處存儲的記錄包含了表中全部的列(索引既數據)。
    • 二級索引
      • 以自定義的列的大小爲頁和記錄的排序規則,在葉子節點處存儲的記錄內容是列 + 主鍵 ,因此每次查找的數據都會先獲得主鍵,而獲得主鍵後仍然須要到聚簇索引中再查一遍,這個過程也被稱爲回表 。也就是根據c2列的值查詢一條完整的用戶記錄須要使用到2 B+ 樹!!!

最後

最後說一下,本文的參考文章: MySQL的索引

本文的不少內容也是來自這篇文章,本人只在文章中插入了有關本身對於文章的理解,若是說的不對,還望指教。

你們也能夠去看一下原文。

相關文章
相關標籤/搜索