【MongoDB】索引的用途與操作方式


https://flic.kr/p/qakxAH

雖然電腦的處理速度很快,然而一旦資料庫中的資料越來越多,查詢所要花費的時間仍然會增長。因此我們會建立「索引」(index),事先將 document 排序後擷取一至多個欄位,供查詢時參考,藉此增進效率。

本文將著重在索引相關觀念的介紹,以及索引的操作方式。其中也會包含單一欄位索引、複合索引與覆蓋索引的範例。至於查詢效率的分析,筆者有時間再寫其他文章分享。

一、索引的介紹

索引(index)是一種附加在資料庫中的檔案,其內容是 document 經排序後的結果。但主要只包含那些用來排序的欄位,以及另一個欄位,專門存放 document id。資料庫執行時,會將索引載入到記憶體中。

以下表格是一個 collection 的示意內容。它具有以下 document,並且儲存在不同的物理地址。

物理地址(非欄位)_idtitlecategory
addr_1B1Java ProgrammingComputer
addr_2B2Information SecurityComputer
addr_3B3AuditingAccounting
addr_4B4MicroeconomicsEconomics

(一)自製索引

我們可以用不同的排序方式來製作索引。以下是索引內容的示意表格。

以 title 欄位遞增:

title物理地址
Auditingaddr_3
Information Securityaddr_2
Java Programmingaddr_1
Microeconomicsaddr_4

以 category 欄位遞減,再以 title 欄位遞增:

categorytitle物理地址
EconomicsMicroeconomicsaddr_4
ComputerInformation Securityaddr_2
ComputerJava Programmingaddr_1
AccountingAuditingaddr_3

讀者可以看到,不論排序方式為何,索引中的每個條目都會有個欄位是存放 document 所儲存的物理地址。

(二)_id 索引

MongoDB 規定每筆 document 都要有個「_id」欄位做為主鍵。如果新增 document 時未特別指定,則會自動生成「ObjectId」。也因此,MongoDB 會為每個 collection 都內建一個索引,它是使用 _id 欄位來排序。該索引不可被刪除。

_id物理地址
B1addr_1
B2addr_2
B3addr_3
B4addr_4

在資料庫的領域中,索引的內容實際上是以「樹狀結構」來儲存的,例如 B+tree。並且以某種演算法在樹中找尋資料。若讀者對 B+tree 與相關演算法不熟,為方便本文的學習,姑且可假裝是二元搜尋樹與二元搜尋法。


二、索引如何提升效率

為何索引能夠提升查詢效率呢?讓我們先以生活情境來想像,假設讀者在書店尋找一本已知名稱的電腦類書籍。則可能有以下的尋找方式:

  1. 從第一個櫃子的第一本書開始,一路找下去(循序搜尋)。
  2. 前往電腦書籍的區域再開始逐一尋找。
  3. 向店員告知書名,請對方查出該書的擺放位置,例如哪一條走道的哪一個櫃子。

由以上的例子可看出,第一種方式的效率最差,甚至如果根本沒有這本書,那就意味著浪費大量的時間。而第三種方式的效率最好,可以大大地縮小尋找範圍。而索引就像是例子中,記載所有書籍擺放位置的表格。

為了增進資料庫的查詢效率,我們可針對已知的查詢方式,製作適當的索引。以第一節的索引內容表格為例,若以書名(title)來查詢,則資料庫可使用搜尋演算法,快速找出符合條件的 document 的地址,進而獲取資料。

三、範例資料介紹

在學習索引前,筆者先介紹本文的範例資料。假設 collection 名稱叫做「book」。

_idtitlecategoryprice
B1Book 1Computer1
............
B20000Book 20000Computer20000
B20001Book 1Economic1
............
B40000Book 20000Economic20000
B40001Book 1Accounting1
............
B60000Book 20000Accounting20000

這些 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
    }
)

以本文的範例資料為例,以下是索引內容的示意表格:

titlecategory物理地址
Book 1Accountingaddr_40001
Book 1Computeraddr_1
Book 1Economicaddr_20001
Book 2Accountingaddr_40002
Book 2Computeraddr_2
.........
Book 20000Accountingaddr_60000
Book 20000Computeraddr_20000
Book 20000Economicaddr_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 時,花費更多效能去維護索引的內容。

上一篇:【MongoDB】使用聚合(aggregation)進行資料處理

下一篇:【MongoDB】從執行計劃分析查詢效率

留言