小白(新手)如何徹底理解索引?

愛程式設計DE文兄發表於2020-11-19

一.索引是什麼?

  在說索引的概念前,我們分享一個小故事:

  張三是一個整天玩遊戲看視訊的頹廢大學生,有一天,它意識到不能這樣下去,遲早會成為一個廢人的。因此它想要改變自己,從讀書開始,之前基本不看書的張三如今沉迷於各種各樣的文學書籍,看過的書一本又一本,書逐漸多了起來,擺滿了一個又一個的書架。

  有一天,張三在手機看到某站中西遊記的場景,想起了自己讀過的四大名著,想重新回味回味,於是他從書架中一本一本找,這個書架沒有,那就下一個書架...ok,終於找到了,但半個小時過去了,張三覺得找的太慢了,以後要是書越來越多,那要找的時間豈不是得花費更久啊!

  於是張三想了一個辦法,把書架分成A書架,B書架,C書架並按順序依次排放好......接著把書名第一個中文拼音首字母作為書存放到哪一個書架的規範,比如《阿凡達》:阿 --> a,那麼《阿凡達》j就放到A書架中,《畢加索》:畢 --> b,那《畢加索》就放到B書架.....

  下一次,張三想回味《盧食傳說》,根據 盧首字母L --> L書架,沒一會兒就找到了,開心得像一個200斤的英國大力士,沉迷於書籍中。

  OK,我們來梳理一下,張三在採取措施的前後對比:

    》措施前:書和書架沒有任何關係,書隨便放,書架沒有沒有順序,查詢慢

    》措施後:書和書架產生關係(書首字母放到對應書架),書架也有一定的擺放順序,查詢快

  下面介紹索引:

    》索引是一種資料結構!資料結構!資料結構!並且索引是它儲存的資料排好序的資料結構,目的是為了提高查詢的效率;類比到書和書架,有序的書(放到對應書架)和書架(按編號順序擺放)就叫做索引,是的,索引包含兩部分:資料(書)+特殊的結構(書架)。

    》索引是一個抽象的概念,就類似java中介面,有多種不同的實現,比如:B+Tree,平衡二叉樹,紅黑樹等等

    》索引在計算機中,以檔案的形式存在,即“索引檔案”。

  補充:很多人會把索引用作動詞,對xxx進行索引,其實就是說對xxx建立索引,即 為資料建立有序的結構。

二.索引的工作原理

  現在存在表Test,表的欄位分別為:Col 1和Col 2,為該表的Col 2 欄位新增索引。為了方便理解,假設索引採用的資料結構是二叉搜尋樹,則如下圖:

  我們要知道,MySQL資料庫中的表中的資料是儲存在磁碟上的。即該Test表的資料是儲存在磁碟上的。如果我們有SQL語句:

SELECT Col1, Col2  FROM Test WHERE Col2 = 89;

  現在要查詢 Col 2 = 89 這條記錄。CPU必須先去磁碟查詢這條記錄,將資料分批載入到記憶體中,在記憶體進行快速查詢,找到資料後再對資料進行處理。這個過程最耗時間的就是磁碟I/O(查詢記錄到載入到記憶體的整個過程)

  如果我們不借助任何索引結構把資料進行規範的話,我們查詢Col 2 = 89 這條記錄,就要逐行去查詢、去比較。從Col 2 = 34 開始,一行一行得比較....我們當前的Test表只有不到10行資料,但如果表很大的話,有上千萬條資料,就意味著要做很多很多次磁碟I/O才能找到。速度是很慢的。

  所以,這就是我們為什麼要建索引,目的就是為了減少磁碟I/O的次數 ,加快查詢速率

  現在我們給欄位Col 2 新增了索引,就相當於在硬碟上為Col 2維護了一個索引的資料結構(索引檔案),即這個二叉搜尋樹。二叉搜尋樹的每個結點儲存的是(K,V)結構,key 是Col 2欄位值,value 是該 key 所在行記錄的檔案指標(地址)。比如:該二叉搜尋樹的根節點就是:(34,0x07)。

  現在對 Col 2 新增了索引,這時再去查詢 Col 2 = 89 這條記錄的時候會先去查詢該二叉搜尋樹(二叉樹的遍歷查詢)。讀 34 到記憶體,89>34;讀 89 到記憶體,89 == 89;找到了

  找到之後就根據當前節點的value快速定位到要查詢的記錄對於的地址。我們可以發現,只需要查詢兩次就可以定位到記錄的地址,查詢速度就提高了。

三.Mysql儲存引擎-InnoDB

  在Mysql5.5後的儲存引擎換成了InnoDB了,它對索引的實現是B+Tree。大家是不是有這樣的一個疑問,為什麼要用B+Tree實現索引,普通二叉樹、平衡二叉樹等不可以嗎?答案是可以的,不過B+Tree是這些不斷優化而得到,下面詳細介紹下索引結構的優化的過程吧:

  什麼是二叉查詢樹?如下:

小白(新手)如何徹底理解索引?

  滿足這樣條件的就叫二叉查詢樹:

    》每個節點左邊節點的值都小於該節點,右邊節點的值都大於該節點,沒有值相等的節點,最頂端的節點也就是“45”被稱為根節點。

  二叉查詢樹的查詢過程:若根結點的值等於查詢的值,成功,否則,若小於根結點的值,遞迴查左子樹(也就是根節點左邊的所有節點形成的樹)若大於根結點的值,遞迴查右子樹(也就是根節點右邊所有節點形成的樹)。

  假設用二叉查詢樹建立book表的索引:

小白(新手)如何徹底理解索引?

索引如下:

圖一:

小白(新手)如何徹底理解索引?

  此處的bid為主鍵,每個節點儲存了主鍵的值和該條記錄的內容(地址)。

  如果我要查詢bid為6的圖書的資訊,則先用6和根節點的主鍵值7比較發現比7小,

  然後6再和7左邊的節點5比較發現比5大找到5右邊的節點6,找到了,取出6對應的記錄行的值ee.

  總共經歷了3次比較,如果掃描全表需要經過5次比較。

  什麼是平衡二叉樹?

如果索引是這樣:

圖二:

小白(新手)如何徹底理解索引?

  想要找到主鍵鍵值為9的記錄就需要6次比較,此時查詢二叉樹的結構跟線性表基本沒區別了,索引的優勢完全體現不出來。

  為什麼會這樣?原因就在於這棵樹太高了,如果能想辦法把它變得矮一點,胖一點就完美了。於是平衡二叉樹閃亮登場:

  平衡二叉樹首先也是一個二叉樹,需要滿足二叉樹的所有條件,然後有所改進,規定了左右子樹的高度差不能超過1,如果插入資料導致高度差超過了1則自動進行調整(旋轉),恢復到平衡狀態。這也是平衡二叉樹名字的由來。

  圖一就是一顆平衡二叉樹,圖二根節點的左子樹高度為0,右子樹高度為5,高度差是5超過了1所以不是一顆平衡二叉樹。

  平衡二叉樹查詢效率要高於二叉樹。

  什麼是B樹?

  由前面的推導我們可以看出要想查詢,比較的次數最少,必須想辦法降低樹形結構的高度,不管是二叉樹還是平衡二叉樹,每個節點最多隻能有兩個子節點,這就註定了它的高度會被子節點的個數所限制(允許擁有的子節點越多,在同樣的資料量下,子節點越多的數高度越低),於是B樹橫空出世:

小白(新手)如何徹底理解索引?

  從上圖可以看到B樹的節點可以不止兩個子節點,這樣的好處就是樹可以變得又矮又胖,矮胖的樹是索引的最愛,用它做索引可以降低磁碟的IO(樹高度越低,代表磁碟資料被讀取到記憶體的次數越少,少一個高度就少讀取一個磁碟塊到記憶體).

  B樹中的每個節點根據實際情況可以包含大量的鍵值資料指標,上圖所示為一個3階的B樹:

  每個節點佔用一個磁碟塊的空間,一個節點上有兩個升序排序的鍵值和三個指向子樹根節點的指標(只針對目前3階的情況),指標儲存的是子節點所在磁碟塊的地址。兩個鍵值劃分成的三個範圍域對應三個指標指向的子樹的資料的範圍域。以根節點為例,鍵值為17和35,P1指標指向的子樹的資料範圍為小於17,P2指標指向的子樹的資料範圍為17~35,P3指標指向的子樹的資料範圍為大於35。

  模擬查詢關鍵字29的過程:

    根據根節點找到磁碟塊1,讀入記憶體。【磁碟I/O操作第1次】

    比較關鍵字29在區間(17,35),找到磁碟塊1的指標P2。

    根據P2指標找到磁碟塊3,讀入記憶體。【磁碟I/O操作第2次】

    比較關鍵字29在區間(26,30),找到磁碟塊3的指標P2。

    根據P2指標找到磁碟塊8,讀入記憶體。【磁碟I/O操作第3次】

    在磁碟塊8中的關鍵字列表中找到關鍵字29。

分析上面過程,發現需要3次磁碟I/O操作,和3次記憶體查詢操作。由於記憶體中的鍵值是一個有序表結構,如果資料很多,可以利用二分法查詢極大提高效率。而3次磁碟I/O操作是影響整個B樹查詢效率的決定因素。

  什麼是B+樹?

  想想還有沒有可能進一步優化,在B樹中每個節點的內容由三部分組成:鍵值,指標,資料,而磁碟塊的容量是有限的,並不是每次讀取磁碟塊都會取出裡面的資料,只有在最後一次讀取或者說找到所需要的資料後的時候才會取出裡面的資料。那能不能將資料只儲存在葉子節點裡面,非葉子節點只儲存鍵值和指標呢?這樣的話非葉子節點就能儲存更多的鍵值和指標了,最大化的利用磁碟塊空間,沒錯,B+樹就是這麼幹的:

小白(新手)如何徹底理解索引?

  假設在非葉子節點不存資料以後每個節點可以儲存4個鍵值和指標,就變成了上圖的B+樹

  B+樹相對於B樹有幾點不同:

  1. 非葉子節點只儲存鍵值和指標。
  2. 所有葉子節點之間都有一個鏈指標。
  3. 資料記錄都存放在葉子節點中。

  在B+樹中因為葉子節點的鍵值是按順序排列的,所以進行鍵值的範圍查詢效率非常高。

  在B+樹中由於一個非葉子節點儲存了更多的鍵值和指標,所以同樣多的資料可以降低樹的高度,減少磁碟io次數,從而提高效率。

四.聚集索引和非聚集索引

  InnoDB要求表必須有主鍵(MyISAM可以沒有),innodb會按照如下規則進行處理: 
    1,如果一個主鍵被定義了,那麼這個主鍵就是作為聚集索引
    2,如果沒有主鍵被定義,那麼該表的第一個唯一非空索引被作為聚集索引 
    3,如果沒有主鍵也沒有合適的唯一索引,那麼innodb內部會生成一個隱藏的主鍵作為聚集索引,這個隱藏的主鍵是一個6個位元組的列,改列的值會隨著資料的插入自增。

  一般我們都會在表中加一個id的列作為主鍵,在上面樹結構中應該有說過一個東西叫:鍵值。是的,主鍵的值就是作為鍵值。查詢一個指定的id,在樹結構中就是查詢鍵值!

聚集索引:

  事實上, 一個加了主鍵的表,並不能被稱之為「表」。一個沒加主鍵的表,它的資料會無序被放置在磁碟儲存器上,一行一行的排列的很整齊, 跟我們認知中的「表」很接近。如果給表上了主鍵,那麼表在磁碟上的儲存結構就由整齊排列的結構轉變成了樹狀結構,也就是上面說的「平衡樹」結構,換句話說,就是整個表就變成了一個索引。沒錯, 再說一遍, 整個表變成了一個索引,也就是所謂的「聚集索引」。 這就是為什麼一個表只能有一個主鍵, 一個表只能有一個「聚集索引」,因為主鍵的作用就是把「表」的資料格式轉換成「索引(平衡樹)」的格式放置。

 

   上圖就是帶有主鍵的表(聚集索引)的結構圖,其中樹的所有結點(底部除外)的資料都是由主鍵欄位中的資料構成,也就是通常我們指定主鍵的id欄位。最下面部分是真正表中的資料。 假如我們執行一個SQL語句:

select * from table where id = 1256;

  首先根據索引定位到1256這個值所在的葉結點,然後再通過葉結點取到id等於1256的資料行。 但是從上圖能看出,樹一共有三層, 從根節點至葉節點只需要經過三次查詢就能得到結果。如下圖:

 

  假如一張表有一億條資料 ,需要查詢其中某一條資料,按照常規邏輯, 一條一條的去匹配的話, 最壞的情況下需要匹配一億次才能得到結果,用大O標記法就是O(n)最壞時間複雜度,這是無法接受的,而且這一億條資料顯然不能一次性讀入記憶體供程式使用, 因此, 這一億次匹配在不經快取優化的情況下就是一億次IO開銷,以現在磁碟的IO能力和CPU的運算能力, 有可能需要幾個月才能得出結果 。如果把這張錶轉換成平衡樹結構(一棵非常茂盛和節點非常多的樹),假設這棵樹有10層,那麼只需要10次IO開銷就能查詢到所需要的資料, 速度以指數級別提升,用大O標記法就是O(log n),n是記錄總樹,底數是樹的分叉數,結果就是樹的層次數。換言之,查詢次數是以樹的分叉數為底,記錄總數的對數,用公式來表示就是

小白(新手)如何徹底理解索引?

  用程式來表示就是Math.Log(100000000,10),100000000是記錄數,10是樹的分叉數(真實環境下分叉數遠不止10), 結果就是查詢次數,這裡的結果從億降到了個位數。因此,利用索引會使資料庫查詢有驚人的效能提升。

  然而, 事物都是有兩面的, 索引能讓資料庫查詢資料的速度上升, 而使寫入資料的速度下降,原因很簡單的, 因為平衡樹這個結構必須一直維持在一個正確的狀態, 增刪改資料都會改變平衡樹各節點中的索引資料內容破壞樹結構, 因此,在每次資料改變時, DBMS必須去重新梳理樹(索引)的結構以確保它的正確,這會帶來不小的效能開銷,也就是為什麼索引會給查詢以外的操作帶來副作用的原因。

 非聚集索引:

  非聚集索引和聚集索引一樣, 同樣是採用平衡樹作為索引的資料結構。索引樹結構中各節點的值來自於表中的索引欄位, 假如給user表的name欄位加上索引 , 那麼索引的樹結構就是由name欄位中的值構成,在資料改變時, DBMS需要一直維護索引結構的正確性。如果給表中多個欄位加上索引 , 那麼就會出現多個獨立的索引結構,每個索引(非聚集索引)互相之間不存在關聯。 如下圖:

 

  每次給欄位建一個新索引(非聚集索引), 欄位中的資料就會被複制一份出來, 用於生成索引。 因此, 給表新增索引,會增加表的體積, 佔用磁碟儲存空間。

  非聚集索引和聚集索引的區別在於, 通過聚集索引可以查到需要查詢的資料(行記錄), 而通過非聚集索引可以查到記錄對應的主鍵值 , 再使用剛才主鍵的值通過聚集索引查詢到需要的資料,如下圖:

  不管以任何方式查詢表, 最終都會利用主鍵通過聚集索引來定位到資料聚集索引(主鍵)是通往真實資料所在的唯一路徑。

 參考:

  https://www.zhihu.com/question/26113830

  https://blog.csdn.net/better12038/article/details/100806721

  https://zhuanlan.zhihu.com/p/23624390

相關文章