引言
這篇文章,里面講到對於一個41G大小、包含百萬條記錄的數據庫進行查詢操作,如果利用了索引,可以把操作耗時從37s降到0.2s。
那么什么是索引呢?利用索引可以加快數據庫查詢操作的原理是什么呢?
索引的基本原理
數據庫提供了一種持久化的數據存儲方式,從數據庫中查詢數據庫是一個基本的操作,查詢操作的效率是很重要的。
對於查詢操作來說,如果被查詢的數據已某種方式組織起來,那么查詢操作的效率會極大提高。
在數據庫中,一條記錄會有很多列。如果把這些記錄按照列Col1以某種數據結構組織起來,那么列Col2一定是亂序的。
因此,數據庫在原始數據之外,維護了滿足特定查找算法的數據結構,指向原始數據,稱之為索引。
舉例來說,在下面的圖中,數據庫有兩列Col1、Col2。在存儲時,按照列Col1組織各行,比如Col1已二叉樹方式組織。如果查找col1中的某一個值,利用二叉樹進行二分查找,不需要遍歷整個數據庫。
這樣一來列Col2就是亂序的。為了解決這個問題,為Col2建立了索引,即把Col2也按照某種數據結構(這里是二叉樹)組織起來。這樣子查找列Col2時只需要進行二分查找即可。

索引的實現
由於數據庫是存儲在磁盤上的,因此實現索引用的數據結構會存儲在磁盤上。磁盤的IO是需要注意的問題。
- 二叉樹
二叉樹是一種經典的數據結構,但是並不適合進行數據庫索引。
原因在於二叉樹中每一個節點的度只有2,樹的深度較高。在存儲時,一般一個節點需要一次磁盤IO,樹的深度較高,查詢一個數據需要的磁盤IO次數越高,查找需要的時間越長。 -
B樹
B樹是二叉樹的變種,主要區別在於每一個節點的度可以大於2,即每一個節點可以分很多叉,大大降低了樹的深度。
- 每條數據表示為[key,data]
- 每個非葉子節點有(n-1)條數據n個指針組成
- 所有葉節點具有相同的深度,等於樹高h
- 指針指向節點的key大於左邊的記錄小於右邊記錄
上面這些特點使得B+樹的深度大大降低,並且實現了對數據的有序組織。
-
B+樹
B+樹是對B樹的擴展,特點在於非葉子節點不存儲data,只存儲key。如果每一個節點的大小固定(如4k,正如在sqlite中那樣),那么可以進一步提高內部節點的度,降低樹的深度。
- 非葉子節點只存儲key,葉子節點不存儲指針
- 每一個節點大小固定,需要一次讀磁盤操作(page)
-
順序訪問指針的B+樹
對B+樹做了一點改變,每一個葉子節點增加一個指向相鄰葉子節點的指針,這樣子可以提高區間訪問的性能。

如圖,訪問key在15到30的data。- 如果沒有水平的指針
B+樹查找找到key=15的data,在同一個塊中找到key=18的data。然后進行第二次B+查找,找到key=20的data,在同一個塊中找到key=30的data。 - 有水平的指針
B+樹查找找到key=15的data,查找同一個塊的內容,或沿着水平指針依次向右遍歷。
- 如果沒有水平的指針
Sqlite中數據存儲方式
- 表(table)和索引(Index)都是帶順序訪問指針的B+樹
- table對應的B+樹中,key是rowid,data是這一行其他列數據(sqlite為每一行分配了一個rowid)
- index對應的B+樹種,key是需要索引的列,data是rowid
根據索引查找數據時,分兩步
- 根據索引找到rowid(第一次B+樹查找)
- 根據rowid查找其他列的數據(第二次B+樹查找)
通過兩次B+樹查找避免了一次全表掃描。
1. 對某一行或某幾行添加PRIMARY KEY或UNIQUE約束,那么數據庫會自動為這些列創建索引 2. 指定某一列為INTEGER PRIMARY KEY,那么這一列和rowid被指定為同一列。即可以通過rowid來獲取,也可以通過列名來獲取。
一個例子
下面是一個數據庫中一個表的統計信息,通過sqlite3_analyzer工具得到。

可以看到表中一共有3651條記錄,B樹的深度只有2,有33個葉子節點,1個非葉子節點。因此最多只需要2次磁盤IO就可以根據rowid找到一行的數據。
利用索引提高查找效率
比如我們有這么一個表

-
benchmark
查詢語句如下SELECT price FROM fruitsforsale WHERE fruit=‘Peach’由於沒有索引,因此不得不做一次全表掃描。通過順序訪問指針遍歷各個記錄(record),比較fruit這一列和‘peatch’是否一致,如果一致,返回這一行的price列的值。
 -
對‘fruit’列加索引
如下,運行同樣的語句,可以根據索引找到目標列對應的rowid為4,然后根據rowid找到對應行,從而選出price。通過兩次B+樹查找避免了全表查找。這也是最簡單的情況
 -
多條索引命中
建立索引時,不要求索引是uique的,即索引表中的key可以是一樣的。
如下圖,索引表中有orange兩條記錄,找到第一條記錄時,根據順序訪問指針可以輕易找到下一條索引,避免另一次B+樹查找。(rowid=1和rowid=23可能位於兩個不同的葉子節點中)
即這個查找索引的過程,可以通過一次B+樹查和一次next操作完成,而next操作是很快的。
 -
利用索引加快搜索和排序
在大多情況下,我們需要同時進行查找和排序操作,這時如果建立適當的索引,可以提高查找效率。
比如下面表中對fruit和state兩列做了索引,運行下面的sql語句時,就不需要進行排序操作了,因為索引表是帶有順序的。SELECT price FROM fruitforsale WHERE fruit='Orange' ORDER BY state

解釋引言中問題
在sqlite中有一個命令叫做explain query plan,可以查看sqlite是如何執行查找操作的。下面的數據庫語句不是引言中的查詢語句,原理一樣
-
37s的操作(沒有用索引)
 -
0.2s的操作(用了索引)

注意detail列。不用索引時,使用的是“SCAN”這個詞,即全表掃描。使用索引時,使用的是“SEARCH”這個詞。
對於一個41G的表來說,進行全表掃描的代價顯然是很大的。
