1. 程式人生 > >為什麼MySQL要用B+樹?聊聊B+樹與硬碟的前世今生【宇哥帶你玩轉MySQL 索引篇(二)】

為什麼MySQL要用B+樹?聊聊B+樹與硬碟的前世今生【宇哥帶你玩轉MySQL 索引篇(二)】

為什麼MySQL要用B+樹?聊聊B+樹與硬碟的前世今生

 

在上一節,我們聊到資料庫為了讓我們的查詢加速,通過索引方式對資料進行冗餘並排序,這樣我們在使用時就可以在排好序的資料裡進行快速的二分查詢,使得查詢效率指數提升。但是我在結尾同樣提到一個問題,就是記憶體大小一般是很有限的,不可能把一個表所有的資料都載入到記憶體中,那麼我們該如何解決這個問題呢?在解決這個問題之前,需要先簡單瞭解一下硬碟知識

硬碟知識簡介

由於機械硬碟的高耐久,低成本,現在仍然是資料儲存的主流,所以這裡著重討論機械硬碟,下面是一個機械硬碟結構圖

 

機械硬碟的資料都存放在碟片中,當我們從硬碟讀取資料時,我們需要提供一個地址,然後硬碟通過前後移動磁頭定址,最後把地址對應資料返回。

這裡有兩個過程很重要,一個是定址,一個是讀取資料。以目前機械硬碟的速度,如果我們要從機械硬碟讀取一條1KB的資料大概只需要0.01ms(100MB/s),而定址卻平均在10ms左右。通常我們把讀取一段連續的資料,不需要多次定址的操作叫做順序讀,而讀取不連續的資料需要多次定址的操作叫做隨機讀,用來區分它們之間的效能差距。

為了充分利用機械硬碟的效能,通常把相關資料連續儲存,這樣就可以一次載入更多的資料,減少磁頭的的移動次數。作業系統有很多對此的優化,例如Linux ext3檔案系統預設塊大小就是4kb。還有linux預載入能力,即當你頻繁訪問一塊資料時,系統會幫你把相鄰的資料也載入進來。

MySQL InnoDB與硬碟

瞭解完機械硬碟的基本知識,現在回到MySQL,MySQL InnoDB引擎也會把資料進行分塊儲存,預設是16KB。所以我們上一節中的索引結構圖在硬碟中的儲存就是每16KB為一個塊,當一個塊快存放快滿的時候開闢一個新的塊來存放。

以books表為例

create table books(
    id int not null primary key auto_increment,
    name varchar(255) not null,
    author varchar(255) not null,
   created_at datetime not null default current_timestamp,
   updated_at datetime not null default current_timestamp on update current_timestamp,
   index idx_books_name(name)
)engine=InnoDB;

該表name欄位的索引idx_books_name在硬碟中的存放就如下圖

 

當塊越來越多的時候,我們可能無法一次把所有的塊都載入到記憶體,此時就要對每個塊再進行索引,如下圖:

  

每個塊的上一級都存放著一條指向該塊首記錄的記錄。這樣只需要載入頂部的第一塊,然後通過區間判斷就可以找到下一塊的地址。

例如我們查詢一條name=name n+1的記錄,過程如下:

  1. 先從左邊頂部塊a開始查詢,發現"name n+1"在"name 1"到"name m"記錄之間

  2. 載入"name 1"對應的下一級塊b

  3. 發現"name n+1"在塊b第二條記錄到第三條記錄之間,所以需要載入第二條記錄對應的下一級塊d

  4. 載入塊d

  5. 在塊d中找到"name n+1"的那條記錄。

如果把上圖旋轉一個,可以發現,整個圖就是一個樹,這其實就是B+樹。B+樹通過對資料塊進行索引,使得當資料量很大,無法一次全部載入到記憶體時,可以先載入一個表的頂部資料塊,然後根據資料所在區間再載入下一級的資料塊。這樣既保證了我們的快速搜尋,又減少了記憶體使用。

MySQL InnoDB的聚簇索引和二級索引

瞭解了B+樹,現在就可以很容易區分MySQL的聚簇索引和二級索引。

聚簇索引就是用主鍵生成B+樹,在葉子節點存放這條記錄的完整資訊

二級索引就是用索引行生成B+樹,在葉子節點只存放索引行和該行對應的主鍵資訊

下面是聚簇索引和二級索引的區分圖

   

瞭解上面的知識,對於一個查詢,我們就可以大概想象出他的執行步驟

select * from books where name = "name400";

例如上面sql的執行步驟如下:

  1. 在二級索引idx_books_name索引中查詢name="name400"的欄位所對應的主鍵id

  2. 通過主鍵id在聚簇索引找到此id所對應的記錄

  3. 返回記錄中的所有欄位

當我們select的欄位在二級索引上不存在時,都需要使用聚簇索引回表查詢剩餘欄位。所以聚簇索引,也就是我們所說的id列,佔用空間越小越好, 這樣就可以在一個節點中存放更多的id值,減少樹的層級,加速查詢效率。一般推薦主鍵使用int或者bigint而不是字串。同時最好保證插入的id值為遞增的,這樣就不會造成在一個已經滿的節點中插入一條記錄造成頁分裂,降低查詢效率。

小結

這節我們先了解了硬碟的基礎知識,知道了機械硬碟的順序讀與隨機讀的巨大效能差距,以及作業系統為了優化磁碟效能而把資料進行按塊儲存。然後又學習了MySQL通過使用B+樹,把存放索引的多個數據塊進行索引,解決了我們上一節使用二分搜尋需要先把所有資料都載入到記憶體的問題。最後,我們瞭解了聚簇索引和二級索引的區別,以及其中的使用建議。

下一節,我們會聊一聊如何建立一個好的索引,判斷一個索引的好壞標準有哪