MongoDB指南---十、索引、複合索引 簡介

上一篇文章: MongoDB指南---九、遊標與數據庫命令
下一篇文章: MongoDB指南---十一、使用複合索引、$操做符如何使用索引、索引對象和數組、索引基數

本章介紹MongoDB的索引,索引能夠用來優化查詢,並且在某些特定類型的查詢中,索引是必不可少的。面試

  • 什麼是索引?爲何要用索引?
  • 如何選擇須要創建索引的字段?
  • 如何強制使用索引?如何評估索引的效率?
  • 建立索引和刪除索引。

爲集合選擇合適的索引是提升性能的關鍵。shell

一、 索引簡介

數據庫索引與書籍的索引相似。有了索引就不須要翻整本書,數據庫能夠直接在索引中查找,在索引中找到條目之後,就能夠直接跳轉到目標文檔的位置,這能使查找速度提升幾個數量級。
不使用索引的查詢稱爲全表掃描(這個術語來自關係型數據庫),也就是說,服務器必須查找完一整本書才能找到查詢結果。這個處理過程與咱們在一本沒有索引的書中查找信息很像:從第1頁開始一直讀完整本書。一般來講,應該儘可能避免全表掃描,由於對於大集合來講,全表掃描的效率很是低。
來看一個例子,咱們建立了一個擁有1 000 000個文檔的集合(若是你想要10 000 000或者100 000 000個文檔也行,只要你有那個耐心):數據庫

> for (i=0; i<1000000; i++) {
...     db.users.insert(
...         {
...             "i" : i,
...             "username" : "user"+i,
...             "age" : Math.floor(Math.random()*120),
...             "created" : new Date()
...         }
...     );
... }

若是在這個集合上作查詢,可使用explain()函數查看MongoDB在執行查詢的過程當中所作的事情。下面試着查詢一個隨機的用戶名:segmentfault

> db.users.find({username: "user101"}).explain()
{
    "cursor" : "BasicCursor",
    "nscanned" : 1000000,
    "nscannedObjects" : 1000000,
    "n" : 1,
    "millis" : 721,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {

    }
}

5.2節會詳細介紹輸出信息裏的這些字段,目前來講能夠忽略大多數字段。"nscanned"是MongoDB在完成這個查詢的過程當中掃描的文檔總數。能夠看到,這個集合中的每一個文檔都被掃描過了。也就是說,爲了完成這個查詢,MongoDB查看了每個文檔中的每個字段。這個查詢耗費了將近1秒的時間才完成:"millis"字段顯示的是這個查詢耗費的毫秒數。
字段"n"顯示了查詢結果的數量,這裏是1,由於這個集合中確實只有一個username爲"user101"的文檔。注意,因爲不知道集合裏的username字段是惟一的,MongoDB不得不查看集合中的每個文檔。爲了優化查詢,將查詢結果限制爲1,這樣MongoDB在找到一個文檔以後就會中止了:數組

> db.users.find({username: "user101"}).limit(1).explain()
{
    "cursor" : "BasicCursor",
    "nscanned" : 102,
    "nscannedObjects" : 102,
    "n" : 1,
    "millis" : 2,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {

    }
}

如今,所掃描的文檔數量極大地減小了,並且整個查詢幾乎是瞬間完成的。可是,這個方案是不現實的:若是要查找的是user999999呢?咱們仍然不得不遍歷整個集合,並且,隨着用戶的增長,查詢會愈來愈慢。
對於此類查詢,索引是一個很是好的解決方案:索引能夠根據給定的字段組織數據,讓MongoDB可以很是快地找到目標文檔。下面嘗試在username字段上建立一個索引:服務器

> db.users.ensureIndex({"username" : 1})

因爲機器性能和集合大小的不一樣,建立索引有可能須要花幾分鐘時間。若是對ensureIndex的調用沒能在幾秒鐘後返回,能夠在另外一個shell中執行db.currentOp()或者是檢查mongod的日誌來查看索引建立的進度。
索引建立完成以後,再次執行最初的查詢:dom

> db.users.find({"username" : "user101"}).explain()
{
    "cursor" : "BtreeCursor username_1",
    "nscanned" : 1,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 3,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "username" : [
            [
                "user101",
                "user101"
            ]
        ]
    }
}

此次explain()的輸出內容比以前複雜一些,可是目前咱們只須要注意"n"、"nscanned"和"millis"這幾個字段,能夠忽略其餘字段。能夠看到,這個查詢如今幾乎是瞬間完成的(甚至能夠更好),並且對於任意username的查詢,所耗費的時間基本一致:函數

> db.users.find({username: "user999999"}).explain().millis
1

能夠看到,使用了索引的查詢幾乎能夠瞬間完成,這是很是激動人心的。然而,使用索引是有代價的:對於添加的每個索引,每次寫操做(插入、更新、刪除)都將耗費更多的時間。這是由於,當數據發生變更時,MongoDB不只要更新文檔,還要更新集合上的全部索引。所以,MongoDB限制每一個集合上最多隻能有64個索引。一般,在一個特定的集合上,不該該擁有兩個以上的索引。因而,挑選合適的字段創建索引很是重要。性能

MongoDB的索引幾乎與傳統的關係型數據庫索引如出一轍,因此若是已經掌握了那些技巧,則能夠跳過本節的語法說明。後面會介紹一些索引的基礎知識,但必定要記住這裏涉及的只是冰山一角。絕大多數優化MySQL/Oracle/SQLite索引的技巧一樣也適用於MongoDB(包括「Use the Index, Luke」上的教程 http://use-the-index-luke.com)。

爲了選擇合適的鍵來創建索引,能夠查看經常使用的查詢,以及那些須要被優化的查詢,從中找出一組經常使用的鍵。例如,在上面的例子中,查詢是在"username"上進行的。若是這是一個很是通用的查詢,或者這個查詢形成了性能瓶頸,那麼在"username"上創建索引會是很是好的選擇。然而,若是這只是一個不多用到的查詢,或者只是給管理員用的查詢(管理員並不須要太在乎查詢耗費的時間),那就不該該對"username"創建索引。優化

二、 複合索引簡介

索引的值是按必定順序排列的,所以,使用索引鍵對文檔進行排序很是快。然而,只有在首先使用索引鍵進行排序時,索引纔有用。例如,在下面的排序裏,"username"上的索引沒什麼做用:

> db.users.find().sort({"age" : 1, "username" : 1})

這裏先根據"age"排序再根據"username"排序,因此"username"在這裏發揮的做用並不大。爲了優化這個排序,可能須要在"age"和"username"上創建索引:

> db.users.ensureIndex({"age" : 1, "username" : 1})

這樣就創建了一個複合索引(compound index)。若是查詢中有多個排序方向或者查詢條件中有多個鍵,這個索引就會很是有用。複合索引就是一個創建在多個字段上的索引。
假如咱們有一個users集合(以下所示),若是在這個集合上執行一個不排序(稱爲天然順序)的查詢:

> db.users.find({}, {"_id" : 0, "i" : 0, "created" : 0})
{ "username" : "user0", "age" : 69 }
{ "username" : "user1", "age" : 50 }
{ "username" : "user2", "age" : 88 }
{ "username" : "user3", "age" : 52 }
{ "username" : "user4", "age" : 74 }
{ "username" : "user5", "age" : 104 }
{ "username" : "user6", "age" : 59 }
{ "username" : "user7", "age" : 102 }
{ "username" : "user8", "age" : 94 }
{ "username" : "user9", "age" : 7 }
{ "username" : "user10", "age" : 80 }
...

若是使用{"age" : 1, "username" : 1}創建索引,這個索引大體會是這個樣子:

[0, "user100309"] -> 0x0c965148
[0, "user100334"] -> 0xf51f818e
[0, "user100479"] -> 0x00fd7934
...
[0, "user99985" ] -> 0xd246648f
[1, "user100156"] -> 0xf78d5bdd
[1, "user100187"] -> 0x68ab28bd
[1, "user100192"] -> 0x5c7fb621
...
[1, "user999920"] -> 0x67ded4b7
[2, "user100141"] -> 0x3996dd46
[2, "user100149"] -> 0xfce68412
[2, "user100223"] -> 0x91106e23
...

每個索引條目都包含一個"age"字段和一個"username"字段,而且指向文檔在磁盤上的存儲位置(這裏使用十六進制數字表示,能夠忽略)。注意,這裏的"age"字段是嚴格升序排列的,"age"相同的條目按照"username"升序排列。每一個"age"都有大約8000個對應的"username",這裏只是挑選了少許數據用於傳達大概的信息。
MongoDB對這個索引的使用方式取決於查詢的類型。下面是三種主要的方式。

  • db.users.find({"age" : 21}).sort({"username" : -1})

這是一個點查詢(point query),用於查找單個值(儘管包含這個值的文檔可能有多個)。因爲索引中的第二個字段,查詢結果已是有序的了:MongoDB能夠從{"age" : 21}匹配的最後一個索引開始,逆序依次遍歷索引:

[21, "user999977"] -> 0x9b3160cf
[21, "user999954"] -> 0xfe039231
[21, "user999902"] -> 0x719996aa
...

這種類型的查詢是很是高效的:MongoDB可以直接定位到正確的年齡,並且不須要對結果進行排序(由於只須要對數據進行逆序遍歷就能夠獲得正確的順序了)。
注意,排序方向並不重要:MongoDB能夠在任意方向上對索引進行遍歷。

  • db.users.find({"age" : {"$gte" : 21, "$lte" : 30}})

這是一個多值查詢(multi-value query),查找到多個值相匹配的文檔(在本例中,年齡必須介於21到30之間)。MongoDB會使用索引中的第一個鍵"age"獲得匹配的文檔,以下所示:

[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] ->   0x9a1f5e0c
[21, "user100253"] -> 0xd54bd959
[21, "user100409"] -> 0x824fef6c
[21, "user100469"] -> 0x5fba778b
...
[30, "user999775"] -> 0x45182d8c
[30, "user999850"] -> 0x1df279e9
[30, "user999936"] -> 0x525caa57

一般來講,若是MongoDB使用索引進行查詢,那麼查詢結果文檔一般是按照索引順序排列的。

  • db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username":1})

這是一個多值查詢,與上一個相似,只是此次須要對查詢結果進行排序。跟以前同樣,MongoDB會使用索引來匹配查詢條件:

[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] ->   0x9a1f5e0c 
[21, "user100253"] -> 0xd54bd959
...
[22, "user100004"] -> 0x81e862c5
[22, "user100328"] -> 0x83376384
[22, "user100335"] -> 0x55932943
[22, "user100405"] -> 0x20e7e664
...

然而,使用這個索引獲得的結果集中"username"是無序的,而查詢要求結果以"username"升序排列,因此MongoDB須要先在內存中對結果進行排序,而後才能返回。所以,這個查詢一般不如上一個高效。
固然,查詢速度取決於有多少個文檔與查詢條件匹配:若是結果集中只有少數幾個文檔,MongoDB對這些文檔進行排序並不須要耗費多少時間。若是結果集中的文檔數量比較多,查詢速度就會比較慢,甚至根本不能用:若是結果集的大小超過32 MB,MongoDB就會出錯,拒絕對如此多的數據進行排序:

Mon Oct 29 16:25:26 uncaught exception: error: {
    "$err" : "too much data for sort() with no index. add an index or
        specify a smaller limit",
    "code" : 10128
}

最後一個例子中,還可使用另外一個索引(一樣的鍵,可是順序調換了):{"username" : 1, "age" : 1}。MongoDB會反轉全部的索引條目,可是會以你指望的順序返回。MongoDB會根據索引中的"age"部分挑選出匹配的文檔:

["user0", 69]
["user1", 50]
["user10", 80]
["user100", 48]
["user1000", 111]
["user10000", 98]
["user100000", 21] -> 0x73f0b48d
["user100001", 60]
["user100002", 82]
["user100003", 27] -> 0x0078f55f
["user100004", 22] -> 0x5f0d3088
["user100005", 95]
...

這樣很是好,由於不須要在內存中對大量數據進行排序。可是,MongoDB不得不掃描整個索引以便找到全部匹配的文檔。所以,若是對查詢結果的範圍作了限制,那麼MongoDB在幾回匹配以後就能夠再也不掃描索引,在這種狀況下,將排序鍵放在第一位是一個很是好的策略。
能夠經過explain()來查看MongoDB對db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1})的默認行爲:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... explain()
{
    "cursor" : "BtreeCursor age_1_username_1",
    "isMultiKey" : false,
    "n" : 83484,
    "nscannedObjects" : 83484,
    "nscanned" : 83484,
    "nscannedObjectsAllPlans" : 83484,
    "nscannedAllPlans" : 83484,
    "scanAndOrder" : true,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 2766,
    "indexBounds" : {
        "age" : [
            [
                21,
                30
            ]
        ],
        "username" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ]
    },
    "server" : "spock:27017"
}

能夠忽略大部分字段,後面會有相關介紹。注意,"cursor"字段說明此次查詢使用的索引是 {"age" : 1, "user name" : 1},並且只查找了不到1/10的文檔("nscanned"只有83484),可是這個查詢耗費了差很少3秒的時間("millis"字段顯示的是毫秒數)。這裏的"scanAndOrder"字段的值是true:說明MongoDB必須在內存中對數據進行排序,如以前所述。
能夠經過hint來強制MongoDB使用某個特定的索引,再次執行這個查詢,可是此次使用{"username" : 1, "age" : 1}做爲索引。這個查詢掃描的文檔比較多,可是不須要在內存中對數據排序:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... hint({"username" : 1, "age" : 1}).
... explain()
{
    "cursor" : "BtreeCursor username_1_age_1",
    "isMultiKey" : false,
    "n" : 83484,
    "nscannedObjects" : 83484,
    "nscanned" : 984434,
    "nscannedObjectsAllPlans" : 83484,
    "nscannedAllPlans" : 984434,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 14820,
    "indexBounds" : {
        "username" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ],
        "age" : [
            [
                21,
                30
            ]
        ]
    },
    "server" : "spock:27017"
}

注意,此次查詢耗費了將近15秒才完成。對比鮮明,第一個索引速度更快。然而,若是限制每次查詢的結果數量,新的贏家產生了:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"age" : 1, "username" : 1}).
... explain()['millis']
2031
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"username" : 1, "age" : 1}).
... explain()['millis']
181

第一個查詢耗費的時間仍然介於2秒到3秒之間,可是第二個查詢只用了不到1/5秒!所以,應該就在應用程序使用的查詢上執行explain()。排除掉那些可能會致使explain()輸出信息不許確的選項。
在實際的應用程序中,{"sortKey" : 1, "queryCriteria" : 1}索引一般是頗有用的,由於大多數應用程序在一次查詢中只須要獲得查詢結果最前面的少數結果,而不是全部可能的結果。並且,因爲索引在內部的組織形式,這種方式很是易於擴展。索引本質上是樹,最小的值在最左邊的葉子上,最大的值在最右邊的葉子上。若是有一個日期類型的"sortKey"(或是其餘可以隨時間增長的值),當從左向右遍歷這棵樹時,你實際上也花費了時間。所以,若是應用程序須要使用最近數據的機會多於較老的數據,那麼MongoDB只需在內存中保留這棵樹最右側的分支(最近的數據),而沒必要將整棵樹留在內存中。相似這樣的索引是右平衡的(right balanced),應該儘量讓索引是右平衡的。"_id"索引就是一個典型的右平衡索引。

上一篇文章: MongoDB指南---九、遊標與數據庫命令
下一篇文章: MongoDB指南---十一、使用複合索引、$操做符如何使用索引、索引對象和數組、索引基數
相關文章
相關標籤/搜索