數據結構和算法(Data Structure Visualizations):https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
一、MySQL索引底層的實現
索引是幫助MySQL高效獲取數據的排好序的數據結構;
上圖中有一張表,表名為 t ,表中有7條數據;使用 select * from t where t.clo2 = 89 查詢;
1、若表中沒有創建索引,則會全表掃描,一條一條的遍歷查詢,需要遍歷 6 次,查詢一行數據至少和磁盤做一次I/O操作(I/O是很耗性能的),至少要做 6 次 I/O 操作;
2、表中建立了索引:
(1)若索引底層是二叉樹(左邊的子元素小於父元素,右邊的子元素大於父元素)存儲的,則如下圖所示:
這樣查詢 4 次就找到數據了;
當然,在極端情況下,若按照大小順序插入二叉樹,則會形成單邊增長的二叉樹,這樣使用索引的時候和全表掃描是一樣的了;
(2)若索引底層是紅黑樹存儲的,則如下圖所示:
紅黑樹:當單邊的節點大於3時候,就會自動調整,這樣可以解決二叉樹的弊端;紅黑樹也叫平衡二叉樹;
當然,紅黑樹也有弊端的,當數據量特別大的時候,紅黑樹的高度特別大;假如有500W條數據,則紅黑樹高度為 23,若我們要查找的剛好是紅黑樹的葉子節點,則需要查找 23 次才可以,即要發生 23 次的磁盤 I/O 操作,性能就太差了;
(3)若索引底層是 B-Tree 存儲的
(葉子節點具有相同的深度,葉節點的指針為空;所有索引元素不重復;一個節點可以存儲多個元素,節點中的數據索引從左到右遞增排列)
若 Max. Degree = 4,則如下圖所示:
這樣只查詢 2 次就找到了;
當然 B-Tree 也是有弊端的;以下是 B-Tree 的存儲,數字為key,data為對應的數據;
若一個節點我們申請的空間為16KB,若data中的數據過大,則一個節點能放的數據量越小,這樣就會造成樹的高度比較大了(比紅黑樹高度小點);
(4)MySQL的索引底層使用的 B+Tree 存儲的(數據存儲在葉子節點)
B+Tree:
非葉子節點不存儲data,只存儲索引(冗余),可以放更多索引;
葉子節點包含所有索引字段,即所有的data元素存儲在葉子節點上;
葉子節點使用指針連接,提高區間訪問的性能;
從左到右一次遞增;
B+Tree 相對於 B-Tree的優化點:
優化點1: B-Tree的所有節點都存儲了 data 元素, B+Tree的非葉子節點不存儲 data元素,則 B+Tree 的一個非葉子節點可以存儲更多的索引;
優化點2: B+Tree在葉子節點之間增加了指針連接;對 select * from t where col2 > 20 的范圍查找有很好的支持;
MySQL 對 B+Tree 做了優化,葉子節點使用的是雙向指針;
以上圖中查找 49 的數據:
I. 先將根節點的數據(15, 56, 77) 做一次磁盤 I/O 操作取出加載到內存中,然后再在內存中做比對,找到對應的指針,查找到其對應的節點;
II. 將指針指向節點的數據(15, 20, 49) 做一次磁盤 I/O 操作取出加載到內存中,然后再在內存中做比對,找到對應的指針,接着去葉子節點獲取數據;
<1> 查看MySQL文件頁大小(一個節點的大小):
SHOW GLOBAL STATUS like 'Innodb_page_size';
<2> MySQL頁文件默認為16KB,樹的高度為3,能夠存儲多少數據?
我們先看非葉子節點,假設主鍵ID為 bigint 類型,那么長度為8B,指針大小在Innodb源碼中6B,一共14B,那么一頁(即一個節點)可以存儲 16KB/14B=1170 個索引元素和 1170個指針;根節點有1170個索引和1170個指針,樹高度為2的節點就有1170個,那么葉子節點的數量為 1170x1170;每個葉子節點可以存儲16KB,若每條數據比較大為1KB,那么每個葉子節點可以存儲16條數據;那么,高度為3的 B+Tree 的葉子節點可以存儲的數據量為 1170x1170x16=2000W;
在實際的MySQL中表的索引存儲可以選擇 Hash 或 BTree
(5)若索引使用的 Hash 存儲的,存儲的時候先做一次hash運算,根據 hash 的值就可以快速的定位數據的磁盤指針,這樣就不管表里面有多少數據,我們的查詢效率都非常的快;
在實際中為什么使用 B-Tree 或 B+Tree 來存儲索引的方式更多,而不太使用 hash 呢?
原因1:若使用 select * from t where clo2 > 6,這種查找范圍的SQL,那Hash就不能搞定了,就不會走索引了;而且對排序hash也沒有辦法;
原因2:hash會產生 hash 碰撞,MySQL的底層對hash做了處理,很少會發生hash碰撞的;
二、MySQL的存儲引擎的實現
同一個數據庫中,不同的表可以設置不同的存儲引擎;
MySQL的數據存儲在 data 目錄下, data 目錄下的 文件夾是以 數據庫為單位的,數據庫文件夾下面存放的表數據; data / {數據庫名} /表文件
1、MyISAM存儲引擎索引實現
MyISAM存儲引擎的索引文件和數據文件是分離的(非聚集);
MyISAM 存儲引擎的一個表有3個文件: *.frm 文件存儲的表的結構; *.MYD 文件存儲表的數據; *.MYI 文件存儲表中的索引數據;
MYISAM 存儲引擎的索引的葉子節點的data中存儲的是索引所在行的磁盤指針; ---- 非聚集索引
MYISAM 存儲引擎的主鍵索引 和 非主鍵索引的存儲是差不多的,InnoDB 存儲引擎的 主鍵索引 和 非主鍵索引存儲是不一樣的;
2、InnoDB 存儲引擎-索引實現
InnoDB存儲引擎索引文件和數據文件是合一的(聚集);
InnoDB 存儲引擎的1個表有2個文件: *.frm 文件存儲表的結構; *.ibd 文件存儲的是索引和數據;
InnoDB表的數據文件本身就是按 B+Tree 組織的一個索引結構文件;聚集索引葉子節點包含了完整的數據記錄;
(1)InnoDB的主鍵索引
InnoDB 存儲引擎的索引的葉子節點的data中存儲的是索引對應的所有數據;----聚集
問題1:為什么InnoDB表必須有主鍵,並且推薦使用整型的自增主鍵?
a. 因為 MySQL對於 InnoDB 表設計的就是按照 B+Tree 組織存儲數據的,若沒有主鍵就沒有辦法去存儲數據了;但是在平常我們建表的時候沒有指定主鍵也是可以建成功的,這是因為 MySQL 會生成一個 rowid 作為數據的唯一標識;
b. 若使用的 UUID 作為主鍵,在查找的時候需要去比較大小,字符串UUID比較的效率肯定低於數據的比較;在進行比較的時候會把數據拿到內存空間中做比較,UUID為字符串占用的內存空間就會較多;
c. 若是遞增的,則插入的數據直接向后排,這個節點滿了,直接新增一個節點就好了;若不是遞增的,有個節點存儲滿了(5, 9),但是新插入了一個數據(7)在這個節數據的中間,則需要將這個節點先分裂,再平衡去滿足 B+Tree 的結構;
(2)InnoDB 的非主鍵索引
在使用非主鍵索引查找的時候,先從非主鍵索引的樹中查詢到對應的主鍵值,然后使用主鍵值去到主鍵索引的樹中去查找;
對於非主鍵單值索引,若索引字段的值為 null,則它的數據不會放到非葉子節點上,是放在葉子節點的鏈表的最前面的;(強烈不建議字段設置為null)
問題2:為什么非主鍵索引結構葉子節點存儲的是主鍵值?(一致性和節省存儲空間)
因為在插入數據之前先要維護一下索引,然后再將數據插入進去;若 主鍵索引 和 非主鍵索引 的葉子節點都存儲具體的數據,則一個 insert 語句插入成功的判斷就是 向主鍵索引中插入成功 且 向非主鍵索引中也插入成功,這樣就造成了事務的問題,事務是很耗性能的;當然,主鍵索引和非主鍵索引的葉子節點都存儲具體數據,會造成數據的同樣的數據存儲了幾份,就造成了空間的浪費;
聯合索引的底層存儲結構
以上的聯合索引從左到右由字段 a,b,c 組成;
聯合索引在存數據或比較的時候,先比較聯合索引最前面的字段,若最前面的字段值一樣,則再比較第二個字段的值;
聯合索引的索引字段中有一個值為null,則將其放在葉子節點的最前面;可以認為null值是最小的。