1. 程式人生 > >MySQL的索引為什麼用B+Tree?InnDB的資料儲存檔案和MyISAM的有何不同?

MySQL的索引為什麼用B+Tree?InnDB的資料儲存檔案和MyISAM的有何不同?

### 前言 這篇文章的題目,是我真實在面試過程中遇到的問題,某網際網路眾籌公司在考察面試者MySQL相關知識的第一個問題,我當時還是比較懵的,沒想到這年輕人不講武德,不按套路出牌,一般的問MySQL的相關知識的時候,不都是問索引優化以及索引失效等相關問題嗎?怎麼還出來了,儲存檔案的不同?哪怕考察個MVCC機制也行啊。所以這次我就好好總結總結這部分知識點。 ### 為什麼需要建立索引 首先,我們都知道建立索引的目的是為了提高查詢速度,那麼為什麼有了索引就能提高查詢速度呢? 我們來看一下,一個索引的示意圖。 ![](https://img-blog.csdnimg.cn/20210124234421621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_30,color_c8cae6,t_70) 如果我有一個SQL語句是:`select * from Table where id = 15` 那麼在沒有索引的情況下其實是會進行全表掃描的,就是挨個去找,直到找到id=15的這條記錄,時間複雜度是O(n); 如果在有索引的情況下去進行查詢呢。首先會根據id=15,在索引值裡面進行二分查詢,二分查詢的效率是很高的,它的時間複雜度是O(logn); 這就是索引為什麼能提高查詢效率了,但是索引資料的量也是比較大的,所以一般並不是儲存在記憶體中的,都是直接儲存在磁碟中的,所以對磁碟中的檔案內容進行讀取,免不了要進行磁碟IO。 ### MySQL的索引為什麼使用B+Tree 上面我們也說了,索引資料一般是儲存在磁碟中的,但是計算資料都是要在記憶體中進行的,如果索引檔案很大的話,並不能一次都載入進記憶體,所以在使用索引進行資料查詢的時候是會進行多次磁碟IO,將索引資料分批的載入到記憶體中,**`因此一個好的索引的資料結構,在得到正確的結果前提下,一定是磁碟IO次數最少的。`** #### Hash型別 目前MySQL其實是有兩種索引資料型別可以選擇的,一個是BTree(實際是B+Tree)、一個Hash。 但是為什麼在實際的使用過程中,基本上大部分都是選擇BTree呢? 因為如果使用Hash型別的索引,MySQL在建立索引的時候,會對索引資料進行一次Hash運算,這樣根據Hash值就能快速的定位到磁碟指標了,就算資料量很大,也能快速精準的定位到資料。 - 但是像`select * from Table where id > 15`這種範圍查詢,Hash型別的索引就搞不定了,對這種範圍查詢,會直接全表掃描,另外Hash型別的索引也搞不定排序。 - 還有就是雖然MySQL底層做了一系列的處理,但還是不能完全的保證,不產生Hash碰撞。 #### 二叉樹 那MySQL為什麼沒有二叉樹作為它的索引資料結構呢?我們都知道,二叉樹是通過二分查詢來進行定位資料的,所以效果還是不錯的,時間複雜度是O(logn); ![二叉樹](https://img-blog.csdnimg.cn/20210130143654325.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_30,color_c8cae6,t_70) 但是二叉樹有個問題,就是在特殊情況下,它會退化成一根棍子,也就是一個單向連結串列。這個時候,它的時間複雜度就會退化成O(n); ![二叉樹退化成連結串列](https://img-blog.csdnimg.cn/20210130143553853.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_30,color_c8cae6,t_70) 所以當我們要查詢id=50的記錄時,其實和全表掃描是一樣的了。所以因為存在這種情況,二叉樹不適合作為索引的資料結構。 #### 平衡二叉樹 那麼既然二叉樹,在特殊情況下會退化成連結串列,那麼平衡二叉樹為什麼不可以呢? **平衡二叉樹的子節點高度差不能超過1**,像下圖中的二叉樹,關鍵字為15的節點,它的左子節點高度為0,右子節點高度為1,高度差不超過1,所以下面這棵樹是一棵平衡二叉樹。 ![平衡二叉樹](https://img-blog.csdnimg.cn/20210130164300319.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_30,color_c8cae6,t_70) 因為能保持平衡,所以它的查詢時間複雜度為O(logN),至於怎麼保持平衡的,主要是做一些左旋,右旋等,具體保持平衡的細節不是本文主要內容,想了解的可自行搜尋。 **用這個資料結構來做MySQL的索引會有 什麼問題呢?** - **磁碟IO過多**:在MySQL當中,一次IO操作只讀取一個節點,那麼一個節點若是最多就兩個子節點的話,那麼就只有這兩個子節點的查詢範圍,所以要精確到具體的資料時,就需要進行多次讀取,如果樹非常深的話,那麼將會進行大量的磁碟IO。效能自然下降了。 - **空間利用率低**:對於平衡二叉樹來說,每個節點值儲存一個關鍵字,一個數據區,兩個子節點的指標。這樣導致了,一次辛辛苦苦的IO操作就只加載這麼點資料,實在是有點殺雞用牛刀了。 - **查詢效果不穩定**:如果在一個高度很深的平衡二叉樹中,若是查詢的資料正好是根節點,那麼就會很快的查到,若是查詢的資料正好是葉子節點,那麼會進行多次磁碟IO後才能返回,響應時間有可能和根節點的不在一個數量級上。 雖然說二叉樹解決的平衡的問題,但是也帶來了新的問題,那就是由於它本身樹的深度的,會造成一系列的效率問題。 那麼為了解決平衡二叉樹的這類問題,平衡多叉樹(Balance Tree)就成為了更好的選擇。 #### 平衡多叉樹(Balance Tree--B-Tree) B-Tree的意思是平衡多叉樹,一般B-Tree中的一個節點有多少個子節點,我們就稱為多少階的B-Tree。通常用m表示階數,當m為2的時候,就是平衡二叉樹。 一棵B-Tree的每個節點上最多能有m-1個關鍵字,最少要存放`Math.ceil(m/2)-1`個關鍵字,所有的葉子節點都在同一層。如下圖就是一個4階的B-Tree。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210130183719774.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) **那麼我們看一下B-Tree是如何進行查詢資料的**: - 若是查詢id=7的資料,先將關鍵字20的節點載入進記憶體,判斷出7比20小; - 那麼載入第一個子節點,若查詢的資料等於12或17則直接返回,不等於就繼續向下找,發現7小於12; - 那麼繼續載入第一個子節點中去,找到7之後,直接將7下面的data資料返回。 這樣整個操作其實進行了3次IO操作,但實際上一般的B-Tree每層都是有很多分支(通常都大於100)。 MySQL為了能更好的利用磁碟的IO能力,將操作頁的大小設定為了16K,即每個節點的大小為16K。如果每個節點中的關鍵字都是int型別的,那麼就是4個位元組,若資料區的大小為8個位元組,節點指標再佔4個位元組,那麼B-Tree的每個節點中可以儲存的關鍵字個數為:`(16*1000) / (4+8+4)=1000`,每個節點最多可儲存1000個關鍵字,每一個節點最多可以有1001個分支節點。 這樣在查詢索引資料的時候,一次磁碟IO操作可以將1000個關鍵字,讀取到記憶體中進行計算,B-Tree的一次磁碟IO的操作,頂上平衡二叉資料的N次磁碟IO操作了。 **`要注意的是`**:**B-Tree為了保證資料的平衡,會做一系列的操作,這個保持平衡的過程比較耗時間,所以在建立索引的時候,要選擇合適的欄位,並且不要過多的建立索引,建立索引過多的話,在更新資料的時候,更新索引的過程也比較耗時。** 還有就是**不要選擇低區分度欄位值作為索引,例如性別欄位,總共就兩個值,那麼就有可能會造成B-Tree的深度過大,索引效率降低。** #### B+Tree B-Tree已經很好的解決平衡二叉樹的問題了,並且也能保證查詢效率了,那麼為什麼會有B+Tree呢? 我們先來B+Tree是什麼樣子的。 B+Tree是B-Tree的變種,B+Tree的每個節點關鍵字和m階的公式關係和B-Tree的不一樣了。 首先每個節點的子節點數量和每個節點可儲存的關鍵字比例是`1:1`,其次就是查詢資料的時候採用的是左閉合區間進行查詢,還有就是分支節點中沒有資料了只儲存關鍵字和子節點指向,資料都儲存在葉子節點。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210130232536273.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) 那麼來看一下在B+Tree中是如何進行資料查詢的。 例如: - 現在要查詢id=2的資料,那麼會先將根節點取出,載入到記憶體中,發現`id=2`存在於根節點,因為是左閉合區間儲存資料,所以`id<=2`的都在根節點的第一個子節點上; - 那麼取出第一個子節點,載入到記憶體中,發現當前節點存在`id=2`的關鍵字,並且已經到了葉子節點了,那麼直接取出葉子節點中的資料返回。 #### 現在來看一下B-Tree和B+Tree的區別 - **B+Tree的查詢採用的左閉合區間,這樣能更好的支援了自增索引的查詢效果,所以一般在建立主鍵的時候通常都是自增的。這一點和B-Tree是不一樣的。** - **B+Tree中的根節點和分支節點上是不儲存資料的,關鍵字相關的資料只儲存在葉子節點上,這樣保證了查詢效果的穩定,任何查詢都要走到葉子節點才能獲取資料。而B-Tree在分支節點中儲存了資料,若是命中關鍵字則直接返回資料。** - **B+Tree的葉子節點是順序排列的,並且相鄰的兩個葉子節點中具有順序引用的關係,這樣能更好的支援了範圍查詢。而B-Tree是沒有這個順序關係的。** #### MySQL的索引為什麼選擇了B+Tree 經過上面的層層分析,現在我們可以總結一下MySQL為什麼選擇了B+Tree作為它索引的資料結構呢。 1. **首先和平衡二叉樹相比,B+Tree的深度更低,節點儲存關鍵字更多,磁碟IO次數更少,查詢計算效率更好。** 2. **B+Tree的全域性掃描能力更強,若是想根據索引資料對資料表進行全域性掃描,B-Tree會將整棵樹進行掃描,然後逐層遍歷。而B+Tree呢,只需要遍歷葉子節點即可,因為葉子節點之間存在順序引用的關係。** 3. **B+Tree的磁碟IO讀寫能力更強,因為B+Tree的每個分支節點上只儲存了關鍵字,這樣每次磁碟IO在讀寫的時候,一頁16K資料量可以儲存更多的關鍵字了,每個節點上儲存的關鍵字也比B-Tree更多了。這樣B+Tree的一次磁碟IO載入的資料比B-Tree的多很多了。** 4. **B+Tree資料結構中有天然的排序能力,比其他資料結構排序能力更強而且排序時,是通過分支節點來進行的,若是需要將分支節點載入到記憶體中排序,一次載入的資料更多。** 5. **B+Tree的查詢效果更穩定,因為所有的查詢都是需要掃描到葉子節點才將資料返回的。效果只是穩定而不一定是最優,若是直接查詢B-Tree的根節點資料,那麼B-Tree只需要一次磁碟IO就可以直接將資料返回,反而是效果最優。** 經過以上幾點的分析,MySQL最終選擇了B+Tree作為了它的索引的資料結構。 ### InnDB的資料儲存檔案和MyISAM的有何不同? 上面總結了MySQL的索引的資料結構,這次就可以說第二個問題了,因為這個問題其實和MySQL的索引還是有一定的關係的。 下面來看一下,先找到伺服器桑MySQL儲存資料的目錄: 登入MySQL,開啟MySQL的命令列介面:輸入`show variables like '%datadir%';`,就能看到儲存資料的目錄了。 我的伺服器中MySQL的儲存資料的目錄是在: ```powershell /var/lib/mysql/ ``` 進入到這個目錄裡後,能看到所有資料庫的目錄,新建一個`study_test`的資料庫。 然後就進入 ```powershell /var/lib/mysql/study_test ``` 這個目錄下,目前就只有一個檔案,這個檔案是用來記錄建立資料庫時配置的字符集的內容。 ```powershell -rw-r----- 1 mysql mysql 60 1月 31 10:28 db.opt ``` 現在新建兩個表,第一個表的引擎型別選擇InnoDB,第二個表的引擎型別選擇MyISAM。 **student_innodb**: ```sql CREATE TABLE `student_innodb` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) USING BTREE COMMENT 'name索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='innodb引擎表'; ``` **student_myisam**: ```sql CREATE TABLE `student_myisam` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) USING BTREE COMMENT 'name索引' ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='myISAM引擎型別表'; ``` 將兩個表建立完成後,我們再進入到`/var/lib/mysql/study_test`看一下: ```powershell -rw-r----- 1 mysql mysql 60 1月 31 10:28 db.opt -rw-r----- 1 mysql mysql 8650 1月 31 10:41 student_innodb.frm -rw-r----- 1 mysql mysql 114688 1月 31 10:41 student_innodb.ibd -rw-r----- 1 mysql mysql 8650 1月 31 10:58 student_myisam.frm -rw-r----- 1 mysql mysql 0 1月 31 10:58 student_myisam.MYD -rw-r----- 1 mysql mysql 1024 1月 31 10:58 student_myisam.MYI ``` 通過目錄中的檔案可看到建立表之後多了幾個檔案,這樣也看出來了,InnoDB引擎型別的表和MyISAM引擎型別的表的檔案差異。 這幾個檔案每個都是有自己的作用: - InnoDB引擎的表文件,一共有兩個: - *.frm 這類檔案是表的定義檔案。 - *.ibd 這類檔案是資料和索引儲存檔案。表資料和索引聚集儲存,通過索引能直接查詢到資料。 - MyIASM引擎的表文件,一共有三個: - *.frm 這類檔案是表的定義檔案。 - *.MYD 這類檔案是表資料檔案,表中的所有資料都儲存在此檔案中。 - *.MYI 這類檔案是表的索引檔案,MyISAM儲存引擎的索引資料單獨儲存。 #### MyISAM資料儲存引擎,索引與資料的儲存結構 MyISAM儲存引擎在儲存索引的時候,是將索引資料單獨儲存,並且索引的B+Tree最終指向的是資料存在的實體地址,而不是具體的資料。然後再根據實體地址去資料檔案(*.MYD)中找到具體的資料。 如下圖所示: ![MyISAM索引儲存結構](https://img-blog.csdnimg.cn/20210131143858766.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) 那麼當存在多個索引時,多個索引都指向相同的實體地址。 如下圖所示: ![MyISAM的多個索引](https://img-blog.csdnimg.cn/20210131150215709.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) 通過這個結構,我們可以看出來,MyISAM的儲存引擎的索引都是同級別的,主鍵和非主鍵索引結構和查詢方式完全一樣。 #### InnoDB資料儲存引擎,索引與資料的儲存結構 首先InnoDB的索引分為聚簇索引和非聚簇索引,聚簇索引即儲存關鍵字又儲存資料,在B+Tree的每個分支節點上儲存關鍵字,葉子節點上儲存資料。 “**聚簇**”的意思是資料行被按照一定順序一個個緊密地排列在一起儲存。一個表只能有一個聚簇索引,因為在一個表中資料的存放方式只有一種,一般是主鍵作為聚簇索引,如果沒有主鍵,InnoDB會預設生成一個隱藏的列作為主鍵。 如下圖所示: ![InnoDB的聚簇索引](https://img-blog.csdnimg.cn/20210131152457160.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) 非聚簇索引,又稱為二級索引,雖然也是在B+Tree的每個分支節點上儲存關鍵字,但是葉子節點不是儲存的資料,而是儲存的主鍵值。通過二級索引去查詢資料會先查詢到資料對應的主鍵,然後再根據主鍵查詢到具體的資料行。 如下圖所示: ![InnoDB非聚簇索引](https://img-blog.csdnimg.cn/20210131154558732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmlNb2Vy,size_40,color_c8cae6,t_70) 由於非聚簇索引的設計結構,導致了,非聚簇索引在查詢的時候要進行兩次索引檢索,這樣設計的好處,可以保證了一旦發生資料遷移的時候,只需要更新主鍵索引即可,非聚簇索引並不用動,而且也規避了像MyISAM的索引那樣儲存實體地址,在資料遷移的時候的需要重新維護所有索引的問題。 ### 總結 這次把MySQL的索引的資料結構,以及檔案儲存結構,總結清楚了,後面在實際的工作過程中,設計索引的時候能夠考慮的更全了,通過了解了索引的資料結構,也能讓自己在實際寫SQL的時候,能考慮到哪些情況走索引哪些不走索引了。 - MySQL使用B+Tree作為索引的資料結構,因為B+Tree的深度低,節點儲存的關鍵字多,磁碟IO次數少,從而保證了查詢效率更高。 - B+Tree能夠保證MySQL無論是主鍵索引還是非主鍵索引的查詢效果都是穩定的,每次都要查詢到葉子節點才能返回資料,B+Tree的葉子節點的深度是一樣的,而且為了更好的支援自增主鍵,B+Tree的查詢節點範圍是左閉合右開放。 - MySQL的MyISAM儲存引擎,**表資料**和**索引資料**是分別放到兩個檔案中進行儲存的,由於它本身的索引的B+Tree的葉子節點指向的表資料所在的磁碟地址,而且索引沒有主鍵和非主鍵之分,所以分開儲存,能夠更好的統一管理索引; - MySQL的InnoDB儲存引擎,**表資料**和**索引資料**是儲存在一個檔案中的,因為InnoDB的聚簇索引的葉子節點指向的具體的資料行,而且為了保證查詢效果的穩定,InnoDB表中必須要有一個聚簇索引,二級索引在進行索引檢索時,會先通過二級索引檢索到資料的主鍵值,再根據主鍵去聚簇索引中檢索到具體的資料。 ![歡迎關注我,一起學習,一起進步!](https://img-blog.csdnimg.cn/20210114225254706.png) >
微信公眾號:**[Jimoer](#jump_10)**