官方定義:索引是幫助存儲引擎高效獲取數據的一種有序的數據結構。
提取句子主幹:索引是有序的數據結構。
基於快速查找的數據結構有很多,MySQL採用的是 B+Tree。
爲什麼採用B+Tree而不是其他的呢?
B+Tree相比其他數據結構有什麼優點呢?
其他數據結構
瞭解B+Tree之前,先了解一下其他的數據結構,看看它們有什麼問題,再看B+Tree是如何解決這些問題的。
二叉樹
二叉樹的特點:任何一個節點,左邊的節點都比它小,右邊的節點都比它大。
但是普通的二叉樹有一個弊端:不適用於單邊增長。
如上圖,對於單邊增長的數據,二叉樹會完全失去平衡。
這意味着,對於自增型的主鍵來說,這種索引完全沒用,相當於全表掃描。
紅黑樹
紅黑樹是一種平衡二叉樹。
它解決了普通二叉樹單邊增長的問題,會自旋進行自動平衡。
對於同樣的6個數據,看看紅黑樹是如何存放的。
高度比普通二叉樹要好很多,但是依然不理想。
H = log2(S-1)
如公式所示,可計算出:
理想狀態下,1000萬的數據量,樹的高度爲24層,
意味着最差的查詢,需要24次磁盤I/O,速度還是沒法接受的。
哈希
根據索引列計算哈希碼,再根據一一映射關係,只需一次(理想狀態)就可以查找到數據。
哈希查找速度雖然很快,但是有一個嚴重的弊端:不支持範圍查詢!
這意味着大於、小於這種常用的條件無法使用。
而且,在數據量大的情況下,會存在較多的哈希衝突,速度也會受到一定的影響。
InnoDB是支持哈希索引的,如果不考慮範圍查詢,也可以使用,MySQL提供了很多選擇。
B-Tree
B-Tree解決了範圍查詢的問題,且在一定程度上解決了高度的問題。
爲什麼要說“一定程度上”呢?
因爲高度問題優化的還不是最好。
可以看到,對於20個數據,高度也僅爲4,而且是有序的。
B-Tree和B+Tree的區別就是:B-Tree會在所有節點中存放數據,而B+Tree只在葉子節點中存放數據。
這使得,B+Tree的非葉子節點可以存放更多的索引,可以最大限度的降低樹的高度。
B+Tree
首先直接看一下B+Tree的結構是怎樣的。
B+Tree葉子節點擁有所有的元素,且是有序的。
非葉子節點不存儲數據,來存放更多的索引,部分索引做冗餘。
借用兩張圖,可以更好的理解B-Tree和B+Tree的區別。
B-Tree
B+Tree
Innodb數據存放的結構就是B+Tree,如果有主鍵,則根據主鍵來構建樹,沒有主鍵但是有唯一索引,則根據唯一索引來構建樹,如果主鍵和唯一索引都沒有,Innodb會自動爲每行數據生成一個“RowID”隱藏列來構建樹。
Innodb設計的數據存放結構就是這樣的。
數據的存放結構一定是B樹,別無選擇。
二級索引我們可以選擇使用B+Tree或Hash來構建。
頁的概念
InnoDB中,有“數據頁”的概念,它是InnoDB磁盤管理的最小單元。
類似於操作系統中“頁”單位。
每次InnoDB讀取數據,最少讀取“一頁”的數據,即使你只需要一條數據。
B+Tree的每一個節點都是一個數據頁,在InnoDB中,默認的數據頁大小爲:16KB。
可以通過如下命令查看:
SHOW VARIABLES LIKE 'innodb_page_size';-- 16384字節 = 16KB
MySQL通過橫向的擴展節點,和只在葉子節點存放數據,來最大程度的降低樹的高度,使得可以通過最少的磁盤I/O來查找數據。
粗略計算
通過InnoDB的數據頁大小,可以粗略計算一下,在樹的高度爲3的情況下,InnoDB可以存放多少數據。
計算的假設條件:
- 主鍵爲INT遞增,INT佔用4個字節(指針佔用6字節)
- 每行數據佔用1KB磁盤空間
通過以上兩個條件,可以粗略計算出,在理想條件下:
- 第一層可以存放的索引數量:1600。
- 第二層可以存放的索引數量:2500000。
- 第三層可以存放的索引+數據 數量:25000000。
InnoDB可以做到:在樹的高度僅爲3的情況下,存放2500萬的數據。
效率還是非常高的。
執行流程
MySQL根據主鍵查找數據時,首先將第一層節點加載到內存,在CPU中計算,然後根據計算區間去加載第二層的節點,因爲已經可以得到明確區間,第二層也只需要加載一個節點,根據第二層的節點再去找第三層的節點。
第三層存放着索引和數據行記錄,找到索引就找到數據行了。
在InnoDB中,樹的高度爲3的情況下,即使是千萬的數據,也只需要3次磁盤I/O即可找到數據。
而且通常情況下,InnoDB會將第一層節點緩存起來,意味着只需2次磁盤I/O即可。
聚集索引和非聚集索引
聚集索引:索引和數據行存放在一起,找到索引就找到數據行了。
非聚集索引:索引和數據分開存放,通過索引需要再單獨去找數據行。
在InnoDB中,主鍵就是一種聚集索引,一張表聚集索引只能有一個,因爲數據不能分開重複存儲。
用戶自己創建的索引就是二級索引,也就是“非聚集索引”。
InnoDB的非聚集索引中,存放的是索引和主鍵的值。
通過索引找到主鍵,再根據主鍵去查找數據行,也就是我們常說的“回表”。
在MyISAM中,索引存放的就不是數據行或主鍵值了,而是數據行所在磁盤的地址指針。
通過索引找到地址指針,再通過指針去找到數據行。
回表
不能通過索引直接獲取數據,需要根據索引存放的主鍵值重新獲取數據行的行爲稱爲:回表。
應該儘可能的避免“回表”。
大多數情況下,回表查詢的數據是隨機分佈的,意味着數據分佈在不同的數據頁中,InnoDB要加載大量的數據頁,也就意味着需要進行大量的隨機I/O,隨機I/O性能是非常低的,特別是機械硬盤,針頭需要重新尋道。
根據主鍵獲取數據行的效率是最好的,如果只能使用二級索引,那麼儘可能的使用“覆蓋索引查詢”。
覆蓋索引查詢
當查詢的列能從索引中全部獲取時,就無需 回表查詢,這樣的查詢稱爲:覆蓋索引查詢。
“無需回表查詢”可以極大地提升性能,因爲數據可以全部從索引中獲取,減少了磁盤I/O。
要滿足覆蓋索引查詢,必須創建多列索引,且查詢的條件要遵循“最左前綴”原則。
有序的索引
有序的索引可以讓數據快速的被檢索到,但是爲了維護索引的順序,InnoDB花了不少功夫。
糟糕的設計會使得InnoDB維護索引特別喫力和艱難。
頁分裂
InnoDB的默認數據頁大小爲16KB,有序的存放着索引和數據行。
當我們使用UUID作爲主鍵時,由於UUID沒有規則和順序,會導致新插入的數據被隨機的分散到各個數據頁中,一旦分配的數據頁是滿的,InnoDB就不得不進行“頁分裂”。
“頁分裂”會額外消耗系統的開銷,還會使得索引數據變得稀疏,形成空洞,增大索引文件的大小,佔用磁盤空間,降低性能。
除了新增,修改主鍵的值也會造成“頁分裂”,道理是一樣的。
應該儘可能的避免“頁分裂”,主鍵最好使用有序的,自增主鍵就是一個不錯的選擇。
局部性原理
局部性原理是指CPU訪問存儲器時,無論是存取指令還是存取數據,所訪問的存儲單元都趨於聚集在一個較小的連續區域中。
當程序需要從磁盤中加載數據時,CPU會自動的把我們需要數據的相鄰數據也一併加載到內存。
因爲操作系統認爲:程序接下來很可能會訪問相鄰的數據,多加載一些相鄰數據,可以減少磁盤I/O。
在MySQL中,即使我們只需要查詢一條數據,MySQL也會加載一個“頁”的數據。
將相鄰的數據緩存起來,下一次訪問時,就不需要從磁盤中讀取了。
而對於使用UUID作爲主鍵/索引的列,由於沒有規律和順序,會導致局部性原理失效。
加載數據緩存幾乎不起作用,降低了性能。
頁結構
Innodb將“頁”作爲基本單位,索引和數據都保存在一個個的數據頁中。
- Infimum+Supremum:頁中最小值和最大值。
- User Records:數據行記錄
- Free Space:空閒空間。
- Page Directory:頁目錄。
數據頁中的頁目錄(Page Directory),可以理解爲索引中的索引。
同一個數據頁中,可能存在上千個索引,索引之間通過指針連接,使得插入很快,但是查詢較慢。
Page Directory的作用是爲了在同一個數據頁中快速查找。
數據頁中存放的數據大致如下圖所示:
行格式
MySQL中,數據是按照一行一行來保存的,一個數據行代表一條數據記錄。
數據行中,除了保存行數據外,還記錄了很多其他的東西,和具體的“行格式”有關。
可以通過如下命令查看錶的“行格式”:
SHOW TABLE STATUS LIKE '表名';
-- Row_format Dynamic
Compact行格式
- 對於列中有變長字段的,行的頭部會逆序來記錄變長的長度,因爲對於變長來說,一旦存的數據增多,MySQL都要爲其再額外分配磁盤空間。
- 對於列中有允許爲NULL值的,“NULL標誌位”會記錄在這裏。因爲NULL是不佔空間的,一旦賦值,MySQL也要爲其分配磁盤空間。
- 記錄頭信息中包含指向下一條記錄的指針。
MySQL之所以在數據行中記錄這些數據,都是爲了方便對數據行進行擴展。
除了自定義的列外,InnoDB還會在數據行中加入一些隱藏列:
- RowID(6字節):如果表中沒有主鍵和唯一索引,InnoDB會自動生成一個RowID來構建B+Tree。
- 事務ID(6字節)
- 回滾指針(7字節)
事務ID和回滾指針是InnoDB爲了實現MVCC多版本控制而設計的列。
可以在實現事務的同時,儘量減少對數據行加鎖。
Dynamic
Dynamic行格式和Compact類似,針對變長字段進行了優化。
對於數據的長度超過了一個“數據頁”的大小稱爲:數據行溢出。
Compact頁會保存部分數據,然後記錄下一頁的地址指針。
而Dynamic只會記錄地址指針,數據全部放在其他數據頁中,使得同一個數據頁可以存放更多的索引記錄。
數據文件
MySQL中絕大多數存儲引擎都是將數據存儲在磁盤中的,MEMORY引擎除外。
InnoDB將數據以二進制的形式保存,保存路徑爲datadir下以數據庫命名的文件夾中,文件名爲:表名.idb。
查看數據文件
由於是二進制的,無法直接查看,在Linux環境下,可以使用hexdump查看。
表名:mytest
列:col1、col2、col3、col4
數據記錄:
1 aa bb cc
2 dd ee ff
3 gg hh NULL
$ hexdump -C -v mytest.ibd
0000c080 20 80 00 00 01 00 00 00 7b c3 42 e1 00 00 01 59 | .......{.B....Y|
0000c090 01 10 61 61 62 62 63 63 02 02 02 00 00 00 18 00 |..aabbcc........|
0000c0a0 1f 80 00 00 02 00 00 00 7b c3 43 e2 00 00 01 4f |........{.C....O|
0000c0b0 01 10 64 64 65 65 66 66 02 02 01 00 00 20 ff b0 |..ddeeff..... ..|
0000c0c0 80 00 00 03 00 00 00 7b c3 48 e5 00 00 01 58 01 |.......{.H....X.|
0000c0d0 10 67 67 68 68 00 00 00 00 00 00 00 00 00 00 00 |.gghh...........|
可以看到,值爲NULL的列除了在“NULL標誌位”佔用一個標記外,不會再佔用空間了。
索引失效
索引的目的是爲了幫助存儲引擎快速檢索數據的。
一旦達不到這個目的,索引就會失效,因爲即使走索引也沒有意義。
什麼時候會失效?
查詢數據的方式有很多種,返回的結果都一樣,但是不同的查詢方式性能不一樣。
如何找到性能最好的查詢方式呢?
這就是MySQL“查詢優化器”要乾的活了。
不能帶來更好的性能
MySQL在執行SQL語句時,內置的“查詢優化器”會先分析SQL,基於表的統計信息來生成執行計劃。
優化器基於執行成本的方式來判斷哪些執行計劃是最優的,然後再和存儲引擎API去交互查詢數據。
一旦優化器認爲,走索引並不能帶來更好的性能,就不會走索引了,索引就失效了。
例如:一個從1遞增的索引,查詢條件爲:索引列>0。
如果走索引,還需要掃描全部的索引,然後再回表查詢,優化器認爲:還不如直接全表掃描來得快,這種情況下索引就失效了。
Tips:優化器生成的執行計劃不一定都是最優的,涉及很多東西,可以參考筆者以前的筆記。
不能減少掃描範圍
索引的目的就是爲了使存儲引擎通過減少掃描範圍來提升檢索速度。
一旦索引不能減少查詢掃描數據的範圍,存儲引擎也不會走索引。
例如:多列索引中,不滿足最左前綴原則,還是要進行全表掃描的,索引起不到作用。
還包括對索引列進行了計算判斷,也是不會走索引的。
對索引列計算出來的值是無法判斷的,索引本身就沒有參考意義了,只能全表掃描。
Tips:該篇筆記主要記錄索引原理,更多查詢優化方面的東西需要另起篇幅。