MongoDB索引的使用


1 基本索引

在數據庫開發中索引是非常重要的,對於檢索速度,執行效率有很大的影響。本 文主要描述了MongoDB中索引的使用,以及通過分析執行計划來提高數據庫檢索 效率。

作為事例,在數據庫中插入百萬條數據,用於分析

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

在MongoDB中,所有查詢操作,都可以通過執行explain()函數來實現執行的分析, 通過執行查詢username為user99999的用戶,並執行查詢分析,可以得出如下結 果:

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


        },
        "server" : "WallE.local:27017"
}

其中,“n”表示查找到數據的個數,“nscanedObjects”表示本次查詢需要掃描的 對象個數,“milis”表示此次查詢耗費的時間,可以看到,這次查詢相當於對整 個數據表進行了遍歷,共一百萬條數據,找到其中一條數據,耗費時間為561毫 秒。

我們也可以使用limit來限制查找的個數,從而提升效率,例如:

> db.users.find({"username": "user99999"}).limit(1).explain()
{
        "cursor" : "BasicCursor",
        "isMultiKey" : false,
        "n" : 1,
        "nscannedObjects" : 100000,
        "nscanned" : 100000,
        "nscannedObjectsAllPlans" : 100000,
        "nscannedAllPlans" : 100000,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 48,
        "indexBounds" : {


        },
        "server" : "WallE.local:27017"
}

可以看到,這里這次查詢只掃描了十萬條數據,並且耗費時間大概也只有之前的 十分之一。這是因為,由於限制了本次查詢需要獲取結果的個數,MongoDB在遍 歷數據的過程中一旦發現了找到了結果就直接結束了本次查詢,因此效率有了較 大提升。但是這種方式的並不能夠解決效率問題,如果需要查詢的username為 user999999,那么MongoDB仍然需要遍歷整個數據庫才能得到結果。

同其他數據庫一樣,MongoDB也支持索引來提高查詢速度,為了提高username的 查詢速度,在該字段上建立一個索引:

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

執行完該命令后,就在users這個集合中為username新建了一個索引,這個索引 字段可以在db.system.indexes集合中找到:

> db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }
{ "v" : 1, "key" : { "username" : 1 }, "ns" : "test.users", "name" : "username_1" }

值得注意的是,從以上查詢中可以看到,每個數據集合都有一個默認的索引字段, 就是_id字段,這個字段在該數據集合建立的時候就會創建。

索引建立之后,再來看下執行效率:

> db.users.find({"username": "user99999"}).explain()
{
        "cursor" : "BtreeCursor username_1",
        "isMultiKey" : false,
        "n" : 1,
        "nscannedObjects" : 1,
        "nscanned" : 1,
        "nscannedObjectsAllPlans" : 1,
        "nscannedAllPlans" : 1,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "username" : [
                        [
                                "user99999",
                                "user99999"
                        ]
                ]
        },
        "server" : "WallE.local:27017"
}

可以看到,這次MongoDB程序幾乎是一瞬間就找到結果,並且掃描的對象個數為1, 可以看到,這次查詢直接就找到了需要的結果。

對比第一次沒有建立索引時的執行結果,可以看到,第一個字段“cursor”值也有 所變化。作為區分,第一個字段為“BasicCursor”時就表示當前查詢沒有使用索 引,而建立索引后,該值為“BtreeCursor username_1”,也可以看出來MongoDB 使用的是B樹來建立索引。

2 聯合索引

通過使用索引,數據庫會對數據庫中索引中所表示的字段保持已排序狀態,也就 是說,我們能夠方便的針對該字段進行排序查詢如:

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

MongoDB能夠很快返回結果,但是這種幫助只能在查詢字段在首位的情況下才能 生效,如果該字段不在查詢的首位,就可能無法使用到該索引帶來的好處了,如:

> db.users.find().sort({"age": 1, "username" : 1})
error: {
        "$err" : "too much data for sort() with no index.  add an index or specify a smaller limit",
        "code" : 10128
}

查詢字段第一位為“age”,這個時候,MongoDB就會提示錯誤信息。

為了解決這類問題,MongoDB同其他數據庫一樣,也提供了聯合索引的操作,同 樣通過ensureIndex函數來實現:

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

執行這個操作可能需要耗費較長時間,執行成功后,仍然可以通過查詢 db.system.indexes集合來查看索引建立情況:

> db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }
{ "v" : 1, "key" : { "username" : 1 }, "ns" : "test.users", "name" : "username_1" }
{ "v" : 1, "key" : { "age" : 1, "username" : 1 }, "ns" : "test.users", "name" : "age_1_username_1" }

可以看到,剛才的操作建立了一個名字為“age_1_username_1”的聯合索引,再次 執行剛才的聯合查詢,就不會提示出錯了。

通過建立該索引,數據庫中大致會按照如下方式來保存該索引:

...


[26, "user1"] -> 0x99887766
[26, "user2"] -> 0x99887722
[26, "user5"] -> 0x73234234


...


[30, "user3"] -> 0x37234234
[30, "user9"] -> 0x33231289


...

可以看到,索引中第一個字段“age”按照升序排列進行排序,第二個字段 “username”也在第一個字段的范圍內按照升序排列。

在ensureIndex函數中,建立索引時,通過將字段索引置為1,可以將索引標識為 升序排列,如果索引置為-1,則將按照降序排列,如:

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

這樣建立的索引“age”字段就將按照降序排列了。

MongoDB如何使用聯合索引進行查詢,主要是看用戶如何執行查詢語句,主要有 以下幾種情況:

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

這種情況下,由於查詢條件指定了“age”的大小,MongoDB可以使用剛才創建的聯 合索引直接找到“age”為26的所有項:

...


[26, "user1"] -> 0x99887766
[26, "user2"] -> 0x99887722
[26, "user5"] -> 0x73234234


...

並且由於username也是已經排序了的,因此這個查詢可以很快完成。這里需要注 意的是,不管創建“username”索引的時候是使用的升序還是降序,MongoDB可以 直接找到最開始或者最后一項,直接進行數據的遍歷,因此這個地方創建索引不 會對查詢造成影響。

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

這種情況下,MongoDB仍然能夠迅速通過聯合索引查找到“age”字段在18到30范圍 內的所有數據。

最后一種情況較為復雜:

> db.users.find({"age" : {"$gte" : 18, "lte" : 30}}).sort({"username" : -1})

這種情況下,MongoDB首先通過索引查找到“age”范圍在18到30之間的所有數據, 由於在這個范圍的數據集合中,“username”是未排序的,因此,MongoDB會在內 存中對“username”進行排序,然后將結果輸出,如果這個區間中的數據量很大的 話,仍然會出現前面看到的那種一場情況,由於有太多數據需要進行排序操作, 導致程序報錯:

error: {
        "$err" : "too much data for sort() with no index.  add an index or specify a smaller limit",
        "code" : 10128
}

這種情況下,可以通過建立一個{"username" : 1, "age" : 1}這樣的反向的索 引來幫助進行排序,這個索引建立后,索引大致如下所示:

...


["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可以通過遍歷一次這個索引列表來進行排序操作。這樣也避免了 在內存中進行大數據的排序操作。

對剛才的查詢執行查詢計划可以看到:

> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1}).explain()
{
        "cursor" : "BtreeCursor username_1",
        "isMultiKey" : false,
        "n" : 83417,
        "nscannedObjects" : 1000000,
        "nscanned" : 1000000,
        "nscannedObjectsAllPlans" : 1002214,
        "nscannedAllPlans" : 1002214,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 1,
        "nChunkSkips" : 0,
        "millis" : 1923,
        "indexBounds" : {
                "username" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "WallE.local:27017"
}

使用hint函數,使用反向索引之后的結果如下:

> 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" : 83417,
        "nscannedObjects" : 83417,
        "nscanned" : 984275,
        "nscannedObjectsAllPlans" : 83417,
        "nscannedAllPlans" : 984275,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 2,
        "nChunkSkips" : 0,
        "millis" : 3064,
        "indexBounds" : {
                "username" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ],
                "age" : [
                        [
                                21,
                                30
                        ]
                ]
        },
        "server" : "WallE.local:27017"
}

可以看到,第二次執行的時間似乎還要長一些。因此上面介紹的理論並不一定有 效,很多時候,為了提高數據庫的查詢效率,最好對所有查詢語句執行查詢計划, 查看執行差異,從而進行優化。

通過上面的例子可以看到在使用聯合索引的時候,進行查詢操作時,排在前面的 字段如果按照聯合索引的字段進行查詢,都能夠利用到聯合索引的優點。

例如,執行如下查詢時,“age”字段是{"age" : 1, "username" : 1}的第一個字 段,這個時候就可以使用到這個聯合索引進行查詢。

> db.users.find({"age" : 99})

例如查詢:

> db.users.find({"a" : 10, "b" : 20, "c" : 30})

就可以使用索引:{"a" : 1, "b" : 1, "c" : 1, "d" : 1},只要是按照順序的 查詢都可以利用到索引來進行查詢,當然,如果順序不一致,就無法使用到索引 了,例如:

> db.users.find({"c" : 20, "a" : 10})

就無法使用{"a" : 1, "b" : 1, "c" : 1, "d" : 1}索引帶來的好處了。

同關系型數據庫一致,在MongoDB執行查詢操作時,把最容易進行范圍限定的條 件放到最前面,是最有利於查詢操作的,排在前面的條件能夠篩選的出來的結果 越少,后續的查詢效率也就越高。

在MongoDB中,對查詢優化采用這樣一種方式,當查詢條件與索引字段完全一致 時(如查詢“i”的字段,同時也存在一個索引為“i”的字段),則MongoDB會直接 使用這個索引進行查詢。反之,如果有多個索引可能作用於此次查詢,則 MongoDB會采用不同的索引同時並行執行多個查詢操作,最先返回100個數據的查 詢將會繼續進行查詢,剩余的查詢操作將會被終止。MongoDB會將此次查詢進行 緩存,下次查詢會繼續使用,直到對該數據集進行了一定修改后,再次采用這種 方式進行更新。在執行explain()函數后輸出字段中的“allPlans”就表示,所有 嘗試進行的查詢操作次數。

3 索引類型

在MongoDB中,也可以建立唯一索引:

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

建立了唯一索引后,如果插入相同名稱的數據,系統就會報錯:

> db.users.insert({"username" : "user1"})
E11000 duplicate key error index: test.users.$username_1  dup key: { : "user1" }

同樣的,聯合索引也可以建立唯一索引:

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

創建成功后,如果插入相同的數據內容同樣會報錯。

如果數據庫中已經包含了重復數據,可以通過創建唯一索引的方式來進行刪除。 但是注意,這種方式非常危險,如果不是確定數據無效,不能這樣操作,因為, MongoDB只會保留遇到的第一個不同的數據項,后續重復數據都將被刪除:

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

某些時候,我們希望對數據庫中某個字段建立唯一索引,但是又不一定是每條數 據都包含這個字段,這個時候,可以使用sparse索引來解決這個問題:

> db.users.ensureIndex({"email" : 1}, {"unique" : true, "sparse" : 1})

如果存在如下數據:

> db.foo.find()
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 2, "x" : 2 }
{ "_id" : 3, "x" : 3 }

當沒有建立索引的情況下,執行如下操作會返回:

> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }

如果建立了sparse索引,則MongoDB就不會返回第一條數據,而是返回所有包含 “x”字段的數據:

> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }

4 索引管理

通過執行getIndexes()函數,可以獲得當前數據集中所有的索引:

> db.users.getIndexes()
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "test.users",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "age" : 1,
                        "username" : 1
                },
                "ns" : "test.users",
                "name" : "age_1_username_1"
        },
        {
                "v" : 1,
                "key" : {
                        "username" : 1,
                        "age" : 1
                },
                "ns" : "test.users",
                "name" : "username_1_age_1"
        },
        {
                "v" : 1,
                "key" : {
                        "username" : 1
                },
                "unique" : true,
                "ns" : "test.users",
                "name" : "username_1"
        }
]

其中的“name”字段可以用於對索引的刪除操作:

> db.users.dropIndex("username_1_age_1")

就將刪除{"username" : 1, "age" : 1}這個索引。

 

Author: Chenbin

Created: 2013-10-26 Sat 14:12

Emacs 24.3.1 (Org mode 8.2.1)

Validate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM