https://flic.kr/p/qakxAH
雖然電腦的處理速度很快,然而一旦資料庫中的資料越來越多,查詢所要花費的時間仍然會增長。因此我們會建立「索引」(index),事先將 document 排序後擷取一至多個欄位,供查詢時參考,藉此增進效率。
本文將著重在索引相關觀念的介紹,以及索引的操作方式。其中也會包含單一欄位索引、複合索引與覆蓋索引的範例。至於查詢效率的分析,筆者有時間再寫其他文章分享。
一、索引的介紹
索引(index)是一種附加在資料庫中的檔案,其內容是 document 經排序後的結果。但主要只包含那些用來排序的欄位,以及另一個欄位,專門存放 document id。資料庫執行時,會將索引載入到記憶體中。
以下表格是一個 collection 的示意內容。它具有以下 document,並且儲存在不同的物理地址。
物理地址(非欄位) | _id | title | category |
---|---|---|---|
addr_1 | B1 | Java Programming | Computer |
addr_2 | B2 | Information Security | Computer |
addr_3 | B3 | Auditing | Accounting |
addr_4 | B4 | Microeconomics | Economics |
(一)自製索引
我們可以用不同的排序方式來製作索引。以下是索引內容的示意表格。
以 title 欄位遞增:
title | 物理地址 |
---|---|
Auditing | addr_3 |
Information Security | addr_2 |
Java Programming | addr_1 |
Microeconomics | addr_4 |
以 category 欄位遞減,再以 title 欄位遞增:
category | title | 物理地址 |
---|---|---|
Economics | Microeconomics | addr_4 |
Computer | Information Security | addr_2 |
Computer | Java Programming | addr_1 |
Accounting | Auditing | addr_3 |
讀者可以看到,不論排序方式為何,索引中的每個條目都會有個欄位是存放 document 所儲存的物理地址。
(二)_id 索引
MongoDB 規定每筆 document 都要有個「_id」欄位做為主鍵。如果新增 document 時未特別指定,則會自動生成「ObjectId」。也因此,MongoDB 會為每個 collection 都內建一個索引,它是使用 _id 欄位來排序。該索引不可被刪除。
_id | 物理地址 |
---|---|
B1 | addr_1 |
B2 | addr_2 |
B3 | addr_3 |
B4 | addr_4 |
在資料庫的領域中,索引的內容實際上是以「樹狀結構」來儲存的,例如 B+tree。並且以某種演算法在樹中找尋資料。若讀者對 B+tree 與相關演算法不熟,為方便本文的學習,姑且可假裝是二元搜尋樹與二元搜尋法。
二、索引如何提升效率
為何索引能夠提升查詢效率呢?讓我們先以生活情境來想像,假設讀者在書店尋找一本已知名稱的電腦類書籍。則可能有以下的尋找方式:
- 從第一個櫃子的第一本書開始,一路找下去(循序搜尋)。
- 前往電腦書籍的區域再開始逐一尋找。
- 向店員告知書名,請對方查出該書的擺放位置,例如哪一條走道的哪一個櫃子。
由以上的例子可看出,第一種方式的效率最差,甚至如果根本沒有這本書,那就意味著浪費大量的時間。而第三種方式的效率最好,可以大大地縮小尋找範圍。而索引就像是例子中,記載所有書籍擺放位置的表格。
為了增進資料庫的查詢效率,我們可針對已知的查詢方式,製作適當的索引。以第一節的索引內容表格為例,若以書名(title)來查詢,則資料庫可使用搜尋演算法,快速找出符合條件的 document 的地址,進而獲取資料。
三、範例資料介紹
在學習索引前,筆者先介紹本文的範例資料。假設 collection 名稱叫做「book」。
_id | title | category | price |
---|---|---|---|
B1 | Book 1 | Computer | 1 |
... | ... | ... | ... |
B20000 | Book 20000 | Computer | 20000 |
B20001 | Book 1 | Economic | 1 |
... | ... | ... | ... |
B40000 | Book 20000 | Economic | 20000 |
B40001 | Book 1 | Accounting | 1 |
... | ... | ... | ... |
B60000 | Book 20000 | Accounting | 20000 |
這些 document 包含 title(書名)、category(類別)與 price(價格)共 3 個欄位。筆者分為 3 種 category,各有 20000 個 document。以下是一個 document 的範例:
{ "_id": "B100", "title": "Book 100", "category": "Computer", "price": 100 }
四、索引的管理
對於索引有初步認識後,接下來讓我們學習如何在一個 collection 建立、刪除與確認索引。
(一)建立索引
假設要以 title 欄位遞增的方式建立索引,則語法如下:
db.book.createIndex( { "title": 1 }, { "name": "title_asc", "unique": false, "background": false } )
此處呼叫了 createIndex 方法,它可接收兩個物件參數。第一個參數是要排序的欄位,key 為欄位名稱,value 為排序方向(1 為遞增,-1 為遞減)。
第二個物件參數是關於索引的其他設定選項,若不需要亦可省略。以下筆者挑選幾個選項出來解說。
- name:索引的名稱。若不給予,MongoDB 會使用預設值。
- unique:規定索引的欄位,在 collection 中不會有重複的值。若對多個欄位做索引,則視為這些欄位值的「組合」不重複。預設值為 false。
- background:在建立索引的過程中,其他對該 collection 的操作會被擋下(block)。若將此項設為 true,可讓 MongoDB 在背景執行。預設值為 false。
(二)刪除索引
如果建立出來的索引不再需要,可以將之刪除。以下的語法是刪除指定名稱的索引:
db.book.dropIndex("title_asc")
以下的語法是刪除該 collection 中所有的索引:
db.book.dropIndexes()
累積太多沒用到或不需要的索引,是有壞處的,因此最好刪除。主要有以下幾點考量:
- 節省空間。
- 資料庫會在新增、更新或刪除資料時時,去維護索引中的內容(以 B-tree 儲存)。刪除索引可避免不必要的維護動作。
- 資料庫可能會挑選其中一個索引來輔助查詢。刪除索引,除了避免不適合的索引被選中,也能避免查詢時還得多評估它們。
(三)列出索引
若想列出 collection 中的所有索引,可使用以下語法:
db.book.getIndexes()
範例結果如下:
[ { "v": 2, "key": { "_id": 1 }, "name": "_id_", "ns": "StudentGrade.book" }, { "v": 2, "key": { "title": 1 }, "name": "title_asc", "ns": "StudentGrade.book", "background": false } ]
五、複合索引(Compound Index)
(一)定義
若一個索引是以多個欄位來排序,我們稱之為複合索引。以下的語法是建立一個複合索引,先以 title 欄位遞增排序,再以 category 欄位遞增排序。
db.book.createIndex( { "title": 1, "category": 1 } )
以本文的範例資料為例,以下是索引內容的示意表格:
title | category | 物理地址 |
---|---|---|
Book 1 | Accounting | addr_40001 |
Book 1 | Computer | addr_1 |
Book 1 | Economic | addr_20001 |
Book 2 | Accounting | addr_40002 |
Book 2 | Computer | addr_2 |
... | ... | ... |
Book 20000 | Accounting | addr_60000 |
Book 20000 | Computer | addr_20000 |
Book 20000 | Economic | addr_40000 |
(二)效率的比較
複合索引可讓我們在同時使用那些欄位來查詢時,能夠使用索引的效果,且效率可能比單一欄位的索引更好。假設以下面的語法來查詢:
db.book.find( { "category": "Computer", "title": "Book 12345" } )
此處以 category 及 title 欄位來查詢。資料庫會用搜尋演算法,從索引中將範圍縮小到 title 為「Book 12345」的 3 筆條目。隨後再從其中繼續用演算法找到 category 為「Computer」的條目,進而得知 document 地址。
若只對 category 欄位做索引,則資料庫就得對 20000 筆 document 做比對,找出所有 title 相符的資料。效率明顯不及複合索引。
若只對 title 欄位做索引,則將從索引中找出 3 筆條目,再對對應 document 比對,找出 category 相符的資料。其實效率與本例的複合索引相去不遠。
同時我們也注意到,這 6 萬筆 document 的 title 欄位值,差異程度遠大於 category。意即 title 值有 2 萬種,而 category 值只有 3 種。因此在設計索引時,將差異性大的欄位擺在前面的順位,可幫助資料庫更容易地篩選。
(三)索引的前綴
索引中前面幾個連續欄位的組合,稱為索引前綴。假設 document 有 A、B、C、D 四個欄位,並依此順序建立一個複合索引,則以下欄位的組合都屬於索引前綴:
- A
- A、B
- A、B、C
- A、B、C、D
複合索引亦支援使用前綴來幫助查詢排序。舉一個例子,假設查詢條件包含 title 欄位的相等,則預期也能使用該索引。至於排序的部份,筆者有時間再寫其他文章解說。
六、覆蓋索引(Covering Index)
覆蓋索引是一個特性。我們在查詢時,可以指定回傳結果要包含哪些 document 欄位。若這些欄位在 index 中全部都有提供,則資料庫就不必花費時間找出完整的 document 了,意味著效率的提升。
以下的語法是建立複合索引,先對 category 欄位遞增排序,再對 price 遞減排序。
db.book.createIndex( { "category": 1, "price": -1 } )
以下的語法是查詢 category 欄位值為「Computer」的 document,並只取出 price 欄位值。由於 price 的值在索引中已經有包含,因此查詢效率會比回傳整個 document 來得好。
db.book.find( { "category": "Computer" }, { "_id": 0, "price": 1 } )
其實覆蓋索引不見得只會在複合索引被採用時發生。即便資料庫採用單一欄位的索引,若查詢結果也只有回傳那個欄位而已,同樣會發生覆蓋索引。
那麼將大量的欄位加入到索引中,企圖在各種查詢方式都能利用覆蓋索引的特性,這樣可行嗎?筆者認為有可能,然而這會導致寫入、更新或刪除 document 時,花費更多效能去維護索引的內容。
留言
張貼留言