1. 程式人生 > 其它 >B+樹索引

B+樹索引

總結寫前面如果都知道就不用看下面了:因為沒用過MyISAM所以壓根沒看這玩意。

InnoDB儲存引擎總結:

  InnoDB儲存引擎的索引是一棵B+樹,,完整的使用者揭露都儲存在B+樹第0層(從下往上數)的葉子節點中,其他層次的節點都屬於內節點,內節點儲存的是目錄項記錄。

  InnoDB的索引分為兩種:

    聚簇索引(聚集索引):以主鍵值的大小作為頁和記錄的排序規則,在葉子節點儲存記錄包含在表中的所有頁。

    二級索引:以索引列的大小作為頁和記錄的排序規則,在葉子節點儲存記錄的索引列和主鍵。

  Innodb儲存引擎的B+樹根節點自建立起就不再移動。

  在二級索引的B+樹內節點中目錄項由 索引列的值,主鍵和頁號組成。

  一個數據頁至少儲存2條記錄。

  MyISAM儲存引擎的資料和索引分開儲存,這種引擎的索引全部都是二級索引,在葉子節點儲存的是列+行號(對於定長記錄格式的記錄來說)

InnoDB資料頁有7個部分組成,各個資料頁可以組成一個雙向連結串列,而每個資料頁中的記錄又可以組成一個單向連結串列,每個資料頁都會為儲存在它裡邊兒的記錄生成一個頁目錄,在通過主鍵查詢某條記錄的時候可以在頁目錄中使用二分法快速定位到對應的槽,然後再遍歷該槽對應分組中的記錄即可快速找到指定的記錄。

頁和記錄的關係的示意圖如下:

沒有索引的查詢

本集的主題是索引,在正式介紹索引之前,我們需要了解一下沒有索引的時候是怎麼查詢記錄的。為了方便大家理解,我們下邊先只嘮叨搜尋條件為對某個列精確匹配的情況,所謂精確匹配,就是搜尋條件中用等於=連線起的表示式,比如這樣:

SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;

在一個頁中的查詢

假設目前表中的記錄比較少,所有的記錄都可以被存放到一個頁中,在查詢記錄的時候可以根據搜尋條件的不同分為兩種情況:

  • 以主鍵為搜尋條件

    這個查詢過程我們已經很熟悉了,可以在頁目錄中使用二分法快速定位到對應的槽,然後再遍歷該槽對應分組中的記錄即可快速找到指定的記錄。

  • 以其他列作為搜尋條件

    對非主鍵列的查詢的過程可就不這麼幸運了,因為在資料頁中並沒有對非主鍵列建立所謂的頁目錄,所以我們無法通過二分法快速定位相應的槽。這種情況下只能從最小記錄開始依次遍歷單鏈表中的每條記錄,然後對比每條記錄是不是符合搜尋條件。很顯然,這種查詢的效率是非常低的。

在很多頁中查詢

大部分情況下我們表中存放的記錄都是非常多的,需要好多的資料頁來儲存這些記錄。在很多頁中查詢記錄的話可以分為兩個步驟:

  1. 定位到記錄所在的頁。

  2. 從所在的頁內中查詢相應的記錄。

不論是根據主鍵列或者其他列的值進行查詢,由於我們並不能快速的定位到記錄所在的頁,所以只能從第一個頁沿著雙向連結串列一直往下找,在每一個頁中根據我們上邊已經嘮叨過的查詢方式去查詢指定的記錄。因為要遍歷所有的資料頁,所以這種方式顯然是超級耗時的,如果一個表有一億條記錄,使用這種方式去查詢記錄那要等到猴年馬月才能等到查詢結果。

InnoDB中的索引方案

Innodb複用了之前儲存使用者記錄的資料頁來儲存目錄項,為了和使用者記錄做一下區分,我們把這些用來表示目錄項的記錄稱為目錄項記錄。

行格式記錄頭資訊裡的record_type屬性

  • 0:普通的使用者記錄

  • 1:目錄項記錄

  • 2:最小記錄

  • 3:最大記錄

如圖所示:

從圖中可以看出來,我們新分配了一個編號為30的頁來專門儲存目錄項記錄。這裡再次強調一遍目錄項記錄和普通的使用者記錄的不同點:

  • 目錄項記錄的record_type值是1,而普通使用者記錄的record_type值是0。

  • 目錄項記錄只有主鍵值和頁的編號兩個列,而普通的使用者記錄的列是使用者自己定義的,可能包含很多列,另外還有InnoDB自己新增的隱藏列。

  • 只有在儲存目錄項記錄的頁中的主鍵值最小的目錄項記錄的min_rec_mask值為1,其他別的記錄的min_rec_mask值都是0。

使用者記錄和目錄項記錄用的是一樣的資料頁(頁面型別都是0x45BF,這個屬性在Page Header中),頁的組成結構也是一樣一樣的,都會為主鍵值生成Page Directory(頁目錄)以加快在頁內的查詢速度。所以現在根據某個主鍵值去查詢記錄的步驟可以大致拆分成下邊兩步,以查詢主鍵為20的記錄為例(因為都是從一個頁中通過主鍵查某條記錄,所以都可以使用Page Directory通過二分法而實現快速查詢):

  1. 先到儲存目錄項記錄的頁中通過二分法快速定位到對應目錄項,因為12 < 20 < 209,所以定位到對應的記錄所在的頁就是頁9.

  2. 從頁9中根據二分法快速定位到主鍵值為20的使用者記錄。

因為儲存目錄項記錄的頁不止一個,所以如果我們想根據主鍵值查詢一條使用者記錄大致需要3個步驟:

  1. 確定目錄項記錄頁

    我們現在的儲存目錄項記錄的頁有兩個,即頁30和頁32,又因為頁30表示的目錄項的主鍵值的範圍是[1, 320),頁32表示的目錄項的主鍵值不小於320,所以主鍵值為20的記錄對應的目錄項記錄在頁30中。

  2. 通過目錄項記錄頁確定使用者記錄真實所在的頁。

  3. 在真實儲存使用者記錄的頁中定位到具體的記錄。

我們的實際使用者記錄其實都存放在B+樹的最底層的節點上,這些節點也被稱為葉子節點或葉節點,其餘的節點都是用來存放目錄項的,這些節點統統被稱為內節點或者說非葉節點。其中最上邊的那個節點也稱為根節點。

從圖中可以看出來,一個B+樹的節點其實可以分成好多層,規定最下邊的那層,也就是存放我們使用者記錄的那層為第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+樹本身就是一個目錄,或者說本身就是一個索引。它有兩個特點:

  1. 使用記錄主鍵值的大小進行記錄和頁的排序,這包括三個方面的含義:

    •   頁內的記錄是按照主鍵的大小順序排成一個單向連結串列。

    •   各個存放使用者記錄的頁也是根據頁中記錄的主鍵大小順序排成一個雙向連結串列。

    •   各個存放目錄項的頁也是根據頁中記錄的主鍵大小順序排成一個雙向連結串列。

  2. B+樹的葉子節點儲存的是完整的使用者記錄。所謂完整的使用者記錄,就是指這個記錄中儲存了所有列的值。

  ps:InnoDB儲存引擎的表會存在主鍵(唯一非null),如果建表的時候沒有指定主鍵,則會使用第一非空的唯一索引作為聚集索引,否則InnoDB會自動幫你建立一個不可見的、長度為6位元組的row_id用來作為聚集索引。

我們把具有這兩種特性的B+樹稱為聚簇索引,所有完整的使用者記錄都存放在這個聚簇索引的葉子節點處。這種聚簇索引並不需要我們在MySQL語句中顯式的去建立,InnoDB儲存引擎會自動的為我們建立聚簇索引。另外有趣的一點是,在InnoDB儲存引擎中,聚簇索引就是資料的儲存方式(所有的使用者記錄都儲存在了葉子節點),也就是所謂的索引即資料。

二級索引

大家有木有發現,上邊介紹的聚簇索引只能在搜尋條件是主鍵值時才能發揮作用,因為B+樹中的資料都是按照主鍵進行排序的。那如果我們想以別的列作為搜尋條件該咋辦呢?難道只能從頭到尾沿著連結串列依次遍歷記錄麼?

不,我們可以多建幾棵B+樹,不同的B+樹中的資料採用不同的排序規則。

這個B+樹與上邊介紹的聚簇索引有幾處不同:

  • 使用記錄二級索引列的大小進行記錄和頁的排序,這包括三個方面的含義:

    • 頁內的記錄是按照二級索引列的大小順序排成一個單向連結串列。

    • 各個存放使用者記錄的頁也是根據頁中記錄的二級索引列大小順序排成一個雙向連結串列。

    • 各個存放目錄項的頁也是根據頁中記錄的二級索引列大小順序排成一個雙向連結串列。

  • B+樹的葉子節點儲存的並不是完整的使用者記錄,而只是二級索引列+主鍵這兩個列的值。

  • 目錄項記錄中不再是主鍵+頁號的搭配,而變成了二級索引列+頁號的搭配。

聯合索引

我們也可以同時以多個列的大小作為排序規則,也就是同時為多個列建立索引,比方說我們想讓B+樹按照c2和c3列的大小進行排序,這個包含兩層:

  • 先把各個記錄和頁按照c2列進行排序。

  • 在記錄的c2列相同的情況下,採用c3列進行排序

InnoDB中B+樹索引的注意事項:

  1. 根節點萬年不動窩,實際B+樹形成過程如下:

    a. 為某個表建立一個B+樹索引,(聚簇索引不是人為創造,預設存在)都會為這個索引建立一個根節點頁面,沒有資料的時候沒有目錄項,沒有記錄行。

    b. 插入使用者記錄先把使用者記錄存在根節點。

    c. 在根節點的可用空間用完時,此時會將根節點的所有記錄複製到新分配的頁(比如a),然後對這個新頁進行頁分裂操作,得到另一個新頁b。這時新插入的記錄會根據鍵值(也就是聚簇索引中的鍵值或者二級索引中對應的索引列的值)的大小分配到頁a或頁b中。

  根節點此時便升級為儲存目錄項記錄的頁,也就是把頁a和頁b對應的目錄項記錄插入到根節點中。

  ps: 一個B+樹索引的根節點自建立之日起便不會再移動,也就是頁號不會再變化。這樣只要我們建立一個索引,那麼它根節點的頁號便會記錄到某個地方,後續InnoDB需要使用這個索引的時候都會從哪個固定的地方取出根節點的也好,從而訪問這個索引。

  

  2. 內節點中目錄項記錄的唯一性。

    a. 在B+樹的內節點中,目錄項記錄的內容實際上是 “索引項列”+“主鍵值”+“頁號”, 也就是把主鍵值頁新增到二級索引內節點中的目錄項記錄中,這樣就能保證B+樹每一層節點中各條目錄項記錄 除頁號這個欄位外是唯一的,

    b. 不然如果 這個索引項列都是a, 如何排序呢存放,如何把其放到正確的頁裡呢?

  ps: 對於二級索引記錄來講,是先按照二級索引列的值來進行排序,在二級索引列值相同的情況下,再使用主鍵索引來進行排列,相當於二級索引列和主鍵還是一個聯合索引。

    另外,對於唯一二級索引(當我們的某個列或列組合宣告UNIQUE屬性時,便會為這個列或列組合建立唯一二級索引),也可能會出現多條記錄鍵值相同的情況(一是宣告為UNIQUE屬性的列可能儲存多個NULL值,二是因為MVCC),

   唯一二級索引的內節點目錄項頁會包含記錄的主鍵值。

  

  3. 一個頁面至少要容納2條記錄

    為了避免B+樹層級過高,Innodb要求所有的資料頁至少可以容納兩條記錄。

Nice to see you all!