MySQL 索引機制背後的隱藏之道

YoungChen發表於2019-05-08

索引的 “哲學思想”

我們為什麼需要索引?

顯而易見,使用索引可以加快我們檢索資料的速度,生活中書籍的目錄、圖書館裡的各種書架編號、號碼簿上的檢索頁等,都少不了索引的身影。

回到計算機的世界,任何一種資料結構都不是憑空產生的,一定會有它的誕生背景和解決的問題。我們先舉個最簡單的例子,下圖是一個有序遞增的陣列,裡面包含十個元素,沒有重複。

MySQL 索引機制背後的隱藏之道

如果我想要查詢元素 24 ,該怎麼做呢?第一想到的自然是遍歷陣列,如果陣列長度為 N 那麼演算法的時間複雜度是 O(N)。有沒有更快的辦法呢?隨即我們想到,鑑於陣列已經有序了,我們還可以使用二分查詢,每次都折半,時間複雜度降為 O(logN)。甚至於,我們還可以建立樹形的資料結構來搜尋,最常見的就是二叉搜尋樹(BST)或者 AVL 樹。

MySQL 索引機制背後的隱藏之道

到目前為止,好像一切都很容易,下面我們為之前的資料再增加一個關聯的資料屬性(或者多個資料屬性)。

MySQL 索引機制背後的隱藏之道

看看是不是有點眼熟,好像這種結構在哪裡見過?想象一下,將這個資料集橫向擴充,發現這其實就是資料庫中一張表,它有兩列,一列主鍵一列數字,其中第一行的資料就對應資料庫表的主鍵(Primary Key),每個 PK 關聯與之對應的一整行資料記錄。

回想下我們剛剛做的努力,我們用 PK 的值來構建了某種查詢資料結構(例如 BSTAVL),然後通過它快速找到了 PK 的值,如果樹的節點儲存一整行的記錄,那麼當我們的查詢命中某個 PK 之後,就能在該節點順勢讀取到這一行其他的資料了。

例如我們查詢主鍵為 27 的節點,便可以順勢讀到第二行的 7 這個數值。

上述的例子是很顯而易見的,即使你沒接觸過索引,要設計一種加速查詢的方法,也可能會想到這種方案,但是僅僅做到這些遠遠不夠,資料庫系統受龐大的資料量、查詢條件的複雜性(等值、範圍、模糊)的影響,其索引的實現複雜許多,但是起源的哲學思想都是一樣的

索引是越多越好嗎?

雖說索引可以加速查詢,但索引未必是越多越好,因為:

  • 資料的增刪都會涉及到隨索引的修改,索引越多維護成本越高;
  • 索引越多也意味著儲存空間需要越大;
  • 有時候未必需要索引,如果一列資料重複項非常多,建索引反而沒有必要,例如第一節中我們列舉了一個記憶體中、極少量資料如何採用不同的方法做高效查詢的例子,但其實這種容量的陣列在記憶體中直接遍歷才是王道,不需要關心效能問題。

資料庫對索引結構的要求

在真正的資料庫設計中,例如 MySQL 這樣的關係型資料庫,它對索引結構的設計也是有要求的:

  • 查詢速度要儘可能快
  • 索引顯然也需要儲存在磁碟上進行持久化,如何儘可能減少索引查詢過程中的磁碟 IO 次數
  • 不僅僅是等值查詢,也要能做範圍查詢(BETWEENIN<>)、模糊查詢(LIKE)、並集查詢(OR)

我們來仔細思考下上面的三個要求,第一個要求顯然排除了線性資料結構,只能採用樹形結構,相比於 BST 在最差情況下會退化至 O(N)AVL 樹因為加入了自平衡演算法因此讀寫操作均能很好地保持 O(logN) 的時間複雜度。

我們決定從平衡搜尋樹中進行選擇,那為什麼不選擇平衡二叉搜尋樹呢,這得看第二項要求。索引在資料量大時是無法全部讀進記憶體的,通常情況下索引 : 資料量的比例能達到 1 : 5,如果一張表上多個列存在索引、聯合索引等,該比例還會繼續上升。磁碟上每儲存 1GB 的資料就要耗費 200MB 用來儲存索引,一旦資料量大記憶體是存放不下全部的索引的,況且索引不持久化難道每次啟動時都新建麼?因此索引必須在磁碟上儲存,讀入索引會產生磁碟 IO

眾所周知,相比記憶體(DRAM),磁碟讀取會慢上十萬倍,因此如何減少索引查詢過程中的磁碟 IO 次數至關重要,這個條件限制了二叉搜尋樹成為索引資料結構的機會,反而是高度可控的多路搜尋樹更適合。因此,檔案系統及資料庫系統普遍採用 B+ 樹 作為索引結構,至於為什麼最終選擇 B+ 樹,它的優點是什麼,弄清楚這些得先從磁碟 I/O 的知識入手,然後再結合這些原理分析 B+ 樹作為索引結構的優勢在哪裡。

磁碟 I/O 與磁碟預讀

本節內容選自《深入理解計算機系統》第六章 儲存器層次結構

磁碟 I/O

先簡單介紹一下磁碟 I/O 和預讀,磁碟以扇區大小的塊來讀寫資料,對扇區的訪問時間主要有三個組成部分:尋道時間、旋轉時間和傳送時間。

尋道時間

為了讀取某個扇區的內容,傳動臂需要首先將讀寫頭定位到包含目標扇區的磁軌上,移動傳動臂所需要的時間稱為尋道時間。尋道時間依賴於讀寫頭原本的位置和傳動臂在磁碟上的移動速度,主流磁碟一般在 3 ~ 9ms,最大尋道時間在 20ms

旋轉時間

一旦讀寫頭定位到了期望的磁軌,驅動器等待目標扇區的第一個位旋轉到讀寫頭下,這個步驟的效能依賴於讀寫頭到達目標扇區的位置和磁碟的選擇速度。

傳送時間

當目標扇區的第一位位於讀寫頭下,驅動器就可以開始讀或者寫該扇區的內容了。一個扇區的傳送速度依賴於旋轉速度和每條磁軌的扇區數目。相對於前兩個時間,讀寫資料過程中,傳送時間可以忽略不計。

邏輯磁碟塊

現代磁碟構造複雜,有多個盤面,盤面又有不同的記錄區,為了遮蔽複雜性,現代磁碟將它們組織成一種簡單的檢視,一個 B 個扇區大小的邏輯塊序列,編號 0,1 …… B-1。磁碟中有一個名為磁碟控制器的韌體裝置,維護者邏輯塊號和實際物理磁碟扇區之間的對映關係。

當作業系統想要執行一個 I/O 操作時,例如讀取一個磁碟扇區的資料到主存,它就會傳送一個命令到磁碟控制器,讓它讀取某個邏輯塊號,控制器上一個韌體會將邏輯塊號翻譯為由盤面磁軌扇區三個元素組成的三元組,這個三元組唯一標識了一個物理扇區,然後驅動器將讀寫頭移動到指定位置,將資料讀到主存。

磁碟預讀

因為主存和磁碟訪問效率的巨大差異,磁碟 I/O 變成了一個很重量級的操作,因此需要儘可能減少磁碟 I/O 的次數,為了達到這個目的,磁碟往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個位元組,磁碟也會從這個位置開始,順序向後讀取一定長度的資料放入記憶體。這樣做的理論依據是區域性性原理,即當計算機訪問一個地址的資料的時候,通常與其相鄰的資料也會很快被訪問到。

預讀的長度一般為頁(page)的整倍數,頁是計算機管理儲存器的邏輯塊,硬體及作業系統往往將主存和磁碟儲存區分割為連續的大小相等的塊,每個儲存塊稱為一頁(在許多作業系統中,頁得大小通常為 4K),主存和磁碟以頁為單位交換資料。當程式要讀取的資料不在主存中時,會觸發一個缺頁異常,此時系統會向磁碟發出讀盤訊號,磁碟會找到資料的起始位置並向後連續讀取一頁或幾頁載入記憶體中,然後異常返回,程式繼續執行。

想要詳細瞭解這部分內容,推薦閱讀《深入理解計算機系統》第九章 虛擬儲存器

B+ 樹的優勢

上文我們介紹了磁碟讀取的一些知識,結合我們之前說的 —— 索引結構的優劣與磁碟 I/O 次數大小緊密相關。因此合適的索引結構必定能最大限度發揮磁碟的效能,那 B+ 樹又是如何做到的呢?

B+ 樹的結構中可知,如果樹高是 h 的話,訪問一個葉子節點需要經過 h 次查詢操作,也即訪問 h 個節點。考慮索引實際上儲存在磁碟上,載入索引節點的過程需要經歷磁碟 I/OB+ 樹由於出色的高度控制,導致 h 的值不會太大,一般來說百萬數量級可以控制在 2 ~ 4 左右,意為訪問節點的數量主需要 2 ~ 4 個。

資料庫系統的設計者又巧妙利用了磁碟預讀原理,將一個節點的大小設定成一個頁,這樣每個節點只需要一次磁碟 I/O 就可以載入主存。這樣的話,B+ 樹訪問一個葉子節點需要 h-1磁碟 I/O 就可以,因為其根節點是常駐記憶體的,極大減少了磁碟 I/O 次數,提高了索引結構的效率

MySQL 中的索引

索引的型別有很多種,可以為不同的場景提供更好的效能。MySQL 中索引是在儲存引擎層面而不是伺服器層面實現的,所以沒有統一的標準,不同儲存引擎的索引工作方式也不一樣。我們先看看 MySQL 中支援的索引型別。

B-Tree 索引

雖說叫 B-Tree 索引,甚至顯示時也是顯示成 BTREE,但其實內部實現多使用的是其變種 B+ 樹索引,大多數 MySQL 儲存引擎都支援這種索引,包括 InnoDB

因此,B+ 樹索引實際上就是我們所說的傳統意義上的索引,也是目前關係型資料庫中最為常用的、最有效的索引型別。B+ 樹在關係型資料庫的索引設計中如此流行主要得益於它的高扇出性B+ 樹索引的高度一般維持在 2 ~ 4 層,也就是說查詢某一鍵值的行記錄最多隻需要 2 ~ 4IO,極大減少了磁碟操作的次數。

雜湊索引

基於雜湊表實現,只有精確匹配所有列的查詢才有效。實現方法為,對於每一行資料,儲存引擎都會對所有的索引列計算出一個雜湊碼,雜湊碼是一個較小的值,雜湊索引將所有行算出的雜湊碼儲存在索引中,併為每一個雜湊碼維護指向具體某一行的指標。

MySQL 索引機制背後的隱藏之道

MySQL 中只有 Memory 引擎顯式支援雜湊索引。InnoDB 支援的雜湊索引是自適應的,使用者無法進行配置,InnoDB 引擎會根據表的使用情況自動為表生成雜湊索引。使用雜湊索引的好處在於時間複雜度為 O(1),因此雜湊索引的查詢效率要遠高於 BTree 索引。但是其限制在於:

  1. 只有精確匹配索引所有列的查詢才有效,因為雜湊索引是利用索引的所有列的欄位值來計算雜湊值的。
  2. 只支援等值比較查詢,不能用於範圍查詢。
  3. 雜湊索引的只包含索引欄位的雜湊值和指向資料的指標,所以不能使用索引中的值來避免讀取行。
  4. 雜湊索引的資料並不是順序儲存的,無法用於排序。

空間資料索引

MyISAM 儲存引擎支援空間索引,可以用作地理資料儲存。平日使用場景不多此處不再詳述。

全文索引

全文索引是一種特殊的索引型別,它查詢的是文字中的關鍵詞,而不是直接比較索引中的值。它更類似於搜尋引擎做的事情,而不是簡單的 WHERE 條件匹配。實現方法是通過建立倒排索引,快速匹配文件,這種實現方式也在 Apache Lucene 這種全文檢索庫中出現。

MyISAM 中的索引詳解

MyISAM 儲存引擎的索引檔案和資料檔案是分開的,MyISAM 引擎按照資料插入順序,將資料檔案儲存在磁碟上,例如下圖中 99 條記錄從上到下依次儲存。MyISAM 引擎使用 B+ 樹作為索引結構,葉節點存放的是資料記錄的行指標,圖中為了方便閱讀以行號代替

MySQL 索引機制背後的隱藏之道

MyISAM 引擎中,對主鍵列建立的主索引和對其他列建立的輔助索引在結構上沒有區別,主鍵索引就是一個名為 Primary 的唯一非空索引。

總結一下,MyISAM 引擎中索引查詢的步驟為,先按照 B+ 樹查詢到葉子節點,如果指定的鍵值存在,則取出其對應的行指標的值,然後通過行指標,讀取相應資料行的記錄。

InnoDB 中的索引詳解

聚簇索引

MyISAM 引擎不同,InnoDB 的資料檔案本身就是索引檔案,表資料檔案本身就是按 B+ 樹組織的一個索引結構,其葉子節點的鍵值就是表的主鍵,這種資料儲存方式也被稱為聚簇索引。由此可見,聚簇索引並不是一種單獨的索引型別,而是一種資料儲存方式。

聚簇索引的葉子節點都包含主鍵值、事務 ID、用於事務 MVCC 的回滾指標以及所有的剩餘列。

MySQL 索引機制背後的隱藏之道

輔助索引

輔助索引也叫非聚簇索引,二級索引等。同 MyISAM 引擎的輔助索引實現不同,InnoDB 的輔助索引,其葉子節點儲存的不是行指標而是主鍵值,得到主鍵值再要查詢具體行資料的話,要去聚簇索引中再查詢一次,也叫回表。這樣的策略優勢是減少了當出現行移動或者資料頁分裂時二級索引的維護工作。

MySQL 索引機制背後的隱藏之道

參考資料

寫在最後

這是一個不定時更新的、披著程式設計師外衣的文青小號,歡迎關注。

MySQL 索引機制背後的隱藏之道

相關文章