爲什麼索引可以提升查詢速度?

官方定義:索引是幫助存儲引擎高效獲取數據的一種有序的數據結構。

提取句子主幹:索引是有序的數據結構。

基於快速查找的數據結構有很多,MySQL採用的是 B+Tree

爲什麼採用B+Tree而不是其他的呢?
B+Tree相比其他數據結構有什麼優點呢?

其他數據結構

瞭解B+Tree之前,先了解一下其他的數據結構,看看它們有什麼問題,再看B+Tree是如何解決這些問題的。

二叉樹

二叉樹的特點:任何一個節點,左邊的節點都比它小,右邊的節點都比它大。

但是普通的二叉樹有一個弊端:不適用於單邊增長。

image

如上圖,對於單邊增長的數據,二叉樹會完全失去平衡。
這意味着,對於自增型的主鍵來說,這種索引完全沒用,相當於全表掃描。

紅黑樹

紅黑樹是一種平衡二叉樹。
它解決了普通二叉樹單邊增長的問題,會自旋進行自動平衡。

對於同樣的6個數據,看看紅黑樹是如何存放的。

image

高度比普通二叉樹要好很多,但是依然不理想。

H = log2(S-1)

如公式所示,可計算出:
理想狀態下,1000萬的數據量,樹的高度爲24層,
意味着最差的查詢,需要24次磁盤I/O,速度還是沒法接受的。

哈希

根據索引列計算哈希碼,再根據一一映射關係,只需一次(理想狀態)就可以查找到數據。

哈希查找速度雖然很快,但是有一個嚴重的弊端:不支持範圍查詢!
這意味着大於、小於這種常用的條件無法使用。

而且,在數據量大的情況下,會存在較多的哈希衝突,速度也會受到一定的影響。

InnoDB是支持哈希索引的,如果不考慮範圍查詢,也可以使用,MySQL提供了很多選擇。

B-Tree

B-Tree解決了範圍查詢的問題,且在一定程度上解決了高度的問題。

爲什麼要說“一定程度上”呢?
因爲高度問題優化的還不是最好。

image

可以看到,對於20個數據,高度也僅爲4,而且是有序的。

B-Tree和B+Tree的區別就是:B-Tree會在所有節點中存放數據,而B+Tree只在葉子節點中存放數據。
這使得,B+Tree的非葉子節點可以存放更多的索引,可以最大限度的降低樹的高度。

B+Tree

首先直接看一下B+Tree的結構是怎樣的。

image

B+Tree葉子節點擁有所有的元素,且是有序的。
非葉子節點不存儲數據,來存放更多的索引,部分索引做冗餘。

借用兩張圖,可以更好的理解B-Tree和B+Tree的區別。

B-Tree

image

B+Tree

image

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將“頁”作爲基本單位,索引和數據都保存在一個個的數據頁中。

image

  • Infimum+Supremum:頁中最小值和最大值。
  • User Records:數據行記錄
  • Free Space:空閒空間。
  • Page Directory:頁目錄。

數據頁中的頁目錄(Page Directory),可以理解爲索引中的索引。
同一個數據頁中,可能存在上千個索引,索引之間通過指針連接,使得插入很快,但是查詢較慢。
Page Directory的作用是爲了在同一個數據頁中快速查找。

數據頁中存放的數據大致如下圖所示:

image

行格式

MySQL中,數據是按照一行一行來保存的,一個數據行代表一條數據記錄。

數據行中,除了保存行數據外,還記錄了很多其他的東西,和具體的“行格式”有關。

可以通過如下命令查看錶的“行格式”:

SHOW TABLE STATUS LIKE '表名';
-- Row_format Dynamic

Compact行格式

image

  • 對於列中有變長字段的,行的頭部會逆序來記錄變長的長度,因爲對於變長來說,一旦存的數據增多,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:該篇筆記主要記錄索引原理,更多查詢優化方面的東西需要另起篇幅。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章