MongoDB指南---十一、使用複合索引、$操做符如何使用索引、索引對象和數組、索引基數

上一篇文章: MongoDB指南---十、索引、複合索引 簡介
下一篇文章: MongoDB指南---十二、使用explain()和hint()、什麼時候不該該使用索引

一、使用複合索引

在多個鍵上創建的索引就是複合索引,在上面的小節中,已經使用過複合索引。複合索引比單鍵索引要複雜一些,可是也更強大。本節會更深刻地介紹複合索引。正則表達式

1. 選擇鍵的方向

到目前爲止,咱們的全部索引都是升序的(或者是從最小到最大)。可是,若是須要在兩個(或者更多)查詢條件上進行排序,可能須要讓索引鍵的方向不一樣。例如,假設咱們要根據年齡從小到大,用戶名從Z到A對上面的集合進行排序。對於這個問題,以前的索引變得再也不高效:每個年齡分組內都是按照"username"升序排列的,是A到Z,不是Z到A。對於按"age"升序排列按"username"降序排列這樣的需求來講,用上面的索引獲得的數據的順序沒什麼用。
爲了在不一樣方向上優化這個複合排序,須要使用與方向相匹配的索引。在這個例子中,可使用{"age" : 1, "username" : -1},它會如下面的方式組織數據:segmentfault

[21, "user999977"] -> 0xe57bf737
[21, "user999954"] -> 0x8bffa512
[21, "user999902"] -> 0x9e1447d1
[21, "user999900"] -> 0x3a6a8426
[21, "user999874"] -> 0xc353ee06
...
[30, "user999936"] -> 0x7f39a81a
[30, "user999850"] -> 0xa979e136
[30, "user999775"] -> 0x5de6b77a
...
[30, "user100324"] -> 0xe14f8e4d
[30, "user100140"] -> 0x0f34d446
[30, "user100050"] -> 0x223c35b1

年齡按照從年輕到年長順序排列,在每個年齡分組中,用戶名是從Z到A排列的(對於咱們的用戶名來講,也能夠說是按照"9"到"0"排列的)。
若是應用程序同時須要按照{"age" : 1, "username" : 1}優化排序,咱們還須要建立一個這個方向上的索引。至於索引使用的方向,與排序方向相同就能夠了。注意,相互反轉(在每一個方向都乘以-1)的索引是等價的:{"age" : 1, "user name" : -1}適用的查詢與{"age" : -1, "username" : 1}是徹底同樣的。
只有基於多個查詢條件進行排序時,索引方向纔是比較重要的。若是隻是基於單一鍵進行排序,MongoDB能夠簡單地從相反方向讀取索引。例如,若是有一個基於{"age" : -1}的排序和一個基於{"age" : 1}的索引,MongoDB會在使用索引時進行優化,就如同存在一個{"age" : -1}索引同樣(因此不要建立兩個這樣的索引!)。只有在基於多鍵排序時,方向才變得重要。數組

2. 使用覆蓋索引(covered index)

在上面的例子中,查詢只是用來查找正確的文檔,而後按照指示獲取實際的文檔。而後,若是你的查詢只須要查找索引中包含的字段,那就根本不必獲取實際的文檔。當一個索引包含用戶請求的全部字段,能夠認爲這個索引覆蓋了本次查詢。在實際中,應該優先使用覆蓋索引,而不是去獲取實際的文檔。這樣能夠保證工做集比較小,尤爲與右平衡索引一塊兒使用時。
爲了確保查詢只使用索引就能夠完成,應該使用投射(詳見4.1.1節)來指定不要返回"_id"字段(除非它是索引的一部分)。可能還須要對不須要查詢的字段作索引,所以須要在編寫時就在所需的查詢速度和這種方式帶來的開銷之間作好權衡。
若是在覆蓋索引上執行explain(),"indexOnly"字段的值要爲true。
若是在一個含有數組的字段上作索引,這個索引永遠也沒法覆蓋查詢(由於數組是被保存在索引中的,5.1.4節會深刻介紹)。即使將數組字段從須要返回的字段中剔除,這樣的索引仍然沒法覆蓋查詢。優化

3. 隱式索引

複合索引具備雙重功能,並且對不一樣的查詢能夠表現爲不一樣的索引。若是有一個{"age" : 1, "username" : 1}索引,"age"字段會被自動排序,就好像有一個{"age" : 1}索引同樣。所以,這個複合索引能夠看成{"age" : 1}索引同樣使用。
這個能夠根據須要推廣到儘量多的鍵:若是有一個擁有N個鍵的索引,那麼你同時「免費」獲得了全部這N個鍵的前綴組成的索引。舉例來講,若是有一個{"a": 1, "b": 1, "c": 1, ..., "z": 1}索引,那麼,實際上咱們也可使用 {"a": 1}、{"a": 1, "b" : 1}、{"a": 1, "b": 1, "c": 1}等一系列索引。
注意,這些鍵的任意子集所組成的索引並不必定可用。例如,使用{"b": 1}或者{"a": 1, "c": 1}做爲索引的查詢是不會被優化的:只有可以使用索引前綴的查詢才能從中受益。spa

二、$操做符如何使用索引

有一些查詢徹底沒法使用索引,也有一些查詢可以比其餘查詢更高效地使用索引。本節講述MongoDB對各類不一樣查詢操做符的處理。設計

1. 低效率的操做符

有一些查詢徹底沒法使用索引,好比"$where"查詢和檢查一個鍵是否存在的查詢({"key" : {"$exists" : true}})。也有其餘一些操做不能高效地使用索引。
若是"x"上有一個索引,查詢那些不包含"x"鍵的文檔可使用這樣的索引({"x" : {"$exists" : false}}。然而,在索引中,不存在的字段和null字段的存儲方式是同樣的,查詢必須遍歷每個文檔檢查這個值是否真的爲null仍是根本不存在。若是使用稀疏索引(sparse index),就不能使用{"$exists" : true},也不能使用{"$exists" : false}。
一般來講,取反的效率是比較低的。"$ne"查詢可使用索引,但並非頗有效。由於必需要查看全部的索引條目,而不僅是"$ne"指定的條目,不得不掃描整個索引。例如,這樣的查詢遍歷的索引範圍以下:code

> db.example.find({"i" : {"$ne" : 3}}).explain()
{
    "cursor" : "BtreeCursor i_1 multi",
    ...,
    "indexBounds" : {
        "i" : [
            [
                {
                    "$minElement" : 1
                },
                3
            ],
            [
                3,
                {
                    "$maxElement" : 1
                }
            ]
        ]
    },
    ...
}

這個查詢查找了全部小於3和大於3的索引條目。若是索引中值爲3的條目很是多,那麼這個查詢的效率是很不錯的,不然的話,這個查詢就不得不檢查幾乎全部的索引條目。
"$not"有時可以使用索引,可是一般它並不知道要如何使用索引。它可以對基本的範圍(好比將{"key" : {"$lt" : 7}} 變成 {"key" : {"$gte" : 7}})和正則表達式進行反轉。然而,大多數使用"$not"的查詢都會退化爲進行全表掃描。"$nin"就老是進行全表掃描。
若是須要快速執行一個這些類型的查詢,能夠試着找到另外一個可以使用索引的語句,將其添加到查詢中,這樣就能夠在MongoDB進行無索引匹配(non-indexed matching)時先將結果集的文檔數量減到一個比較小的水平。
假如咱們要找出全部沒有"birthday"字段的用戶。若是咱們知道從3月20開始,程序會爲每個新用戶添加生日字段,那麼就能夠只查詢3月20以前建立的用戶:server

> db.users.find({"birthday" : {"$exists" : false}, "_id" : {"$lt" : march20Id}})

這個查詢中的字段順序可有可無,MongoDB會自動找出可使用索引的字段,而無視查詢中的字段順序。對象

2. 範圍

複合索引使MongoDB可以高效地執行擁有多個語句的查詢。設計基於多個字段的索引時,應該將會用於精確匹配的字段(好比 "x" : "foo")放在索引的前面,將用於範圍匹配的字段(好比"y" : {"$gt" : 3, "$lt" : 5})放在最後。這樣,查詢就能夠先使用第一個索引鍵進行精確匹配,而後再使用第二個索引範圍在這個結果集內部進行搜索。假設要使用{"age" : 1, "username" : 1}索引查詢特定年齡和用戶名範圍內的文檔,能夠精確指定索引邊界值:blog

> db.users.find({"age" : 47,
... "username" : {"$gt" : "user5", "$lt" : "user8"}}).explain()
{
    "cursor" : "BtreeCursor age_1_username_1",
    "n" : 2788,
    "nscanned" : 2788,
    ...,
    "indexBounds" : {
        "age" : [
            [
                47,
                47
            ]
        ],
        "username" : [
            [
                "user5",
                "user8"
            ]
        ]
    },
    ...
}

這個查詢會直接定位到"age"爲47的索引條目,而後在其中搜索用戶名介於"user5"和"user8"的條目。
反過來,假如使用{"username" : 1, "age" : 1}索引,這樣就改變了查詢計劃(query plan),查詢必須先找到介於"user5"和"user8"之間的全部用戶,而後再從中挑選"age"等於47的用戶。

> db.users.find({"age" : 47,
... "username" : {"$gt" : "user5", "$lt" : "user8"}}).explain()
{
    "cursor" : "BtreeCursor username_1_age_1",
    "n" : 2788,
    "nscanned" : 319499,
    ...,
    "indexBounds" : {
        "username" : [
            [
                "user5",
                "user8"
            ]
        ],
        "age" : [
            [
                47,
                47
            ]
        ]
    },
    "server" : "spock:27017"
}

本次查詢中MongoDB掃描的索引條目數量是前一個查詢的10倍!在一次查詢中使用兩個範圍一般會致使低效的查詢計劃。

3. OR查詢

寫做本書時,MongoDB在一次查詢中只能使用一個索引。若是你在{"x" : 1}上有一個索引,在{"y" : 1}上也有一個索引,在{"x" : 123, "y" : 456}上進行查詢時,MongoDB會使用其中的一個索引,而不是兩個一塊兒用。"$or"是個例外,"$or"能夠對每一個子句都使用索引,由於"$or"其實是執行兩次查詢而後將結果集合並。

> db.foo.find({"$or" : [{"x" : 123}, {"y" : 456}]}).explain()
 {
      "clauses" : [
        {
            "cursor" : "BtreeCursor x_1",
            "isMultiKey" : false,
            "n" : 1,
            "nscannedObjects" : 1,
            "nscanned" : 1,
            "nscannedObjectsAllPlans" : 1,
            "nscannedAllPlans" : 1,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 0,
            "indexBounds" : {
                "x" : [
                    [
                        123,
                        123
                    ]
            ]
        }
    },
    {
        "cursor" : "BtreeCursor y_1",
        "isMultiKey" : false,
        "n" : 1,
        "nscannedObjects" : 1,
        "nscanned" : 1,
        "nscannedObjectsAllPlans" : 1,
        "nscannedAllPlans" : 1,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
            "y" : [
                    [
                        456,
                        456
                    ]
                ]
            }
        }
    ],
    "n" : 2,
    "nscannedObjects" : 2,
    "nscanned" : 2,
    "nscannedObjectsAllPlans" : 2,
    "nscannedAllPlans" : 2,
    "millis" : 0,
    "server" : "spock:27017"
}

能夠看到,此次的explain()輸出結果由兩次獨立的查詢組成。一般來講,執行兩次查詢再將結果合併的效率不如單次查詢高,所以,應該儘量使用"$in"而不是"$or"。
若是不得不使用"$or",記住,MongoDB須要檢查每次查詢的結果集而且從中移除重複的文檔(有些文檔可能會被多個"$or"子句匹配到)。
使用"$in"查詢時沒法控制返回文檔的順序(除非進行排序)。例如,使用{"x" : [1, 2, 3]}與使用{"x" : [3, 2, 1]}獲得的文檔順序是相同的。

 三、索引對象和數組

MongoDB容許深刻文檔內部,對嵌套字段和數組創建索引。嵌套對象和數組字段能夠與複合索引中的頂級字段一塊兒使用,雖然它們比較特殊,可是大多數狀況下與「正常」索引字段的行爲是一致的。

1. 索引嵌套文檔

能夠在嵌套文檔的鍵上創建索引,方式與正常的鍵同樣。若是有這樣一個集合,其中的第一個文檔表示一個用戶,可能須要使用嵌套文檔來表示每一個用戶的位置:

{
    "username" : "sid",
    "loc" : {
        "ip" : "1.2.3.4",
        "city" : "Springfield",
        "state" : "NY"
    }
}

須要在"loc"的某一個子字段(好比"loc.city")上創建索引,以便提升這個字段的查詢速度:

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

能夠用這種方式對任意深層次的字段創建索引,好比你能夠在"x.y.z.w.a.b.c"上創建索引。
注意,對嵌套文檔自己("loc")創建索引,與對嵌套文檔的某個字段("loc.city")創建索引是不一樣的。對整個子文檔創建索引,只會提升整個子文檔的查詢速度。在上面的例子中,只有在進行與子文檔字段順序徹底匹配的子文檔查詢時(好比db.users.find({"loc" : {"ip" : "123.456.789.000", "city" : "Shelbyville", "state" : "NY"}}})),查詢優化器纔會使用"loc"上的索引。沒法對形如db.users.find({"loc.city" : "Shelbyville"})的查詢使用索引。

2. 索引數組

也能夠對數組創建索引,這樣就能夠高效地搜索數組中的特定元素。
假若有一個博客文章的集合,其中每一個文檔表示一篇文章。每篇文章都有一個"comments"字段,這是一個數組,其中每一個元素都是一個評論子文檔。若是想要找出最近被評論次數最多的博客文章,能夠在博客文章集合中嵌套的"comments"數組的"date"鍵上創建索引:

> db.blog.ensureIndex({"comments.date" : 1})

對數組創建索引,其實是對數組的每個元素創建一個索引條目,因此若是一篇文章有20條評論,那麼它就擁有20個索引條目。所以數組索引的代價比單值索引高:對於單次插入、更新或者刪除,每個數組條目可能都須要更新(可能有上千個索引條目)。
與上一節中"loc"的例子不一樣,沒法將整個數組做爲一個實體創建索引:對數組創建索引,其實是對數組中的每一個元素創建索引,而不是對數組自己創建索引。
在數組上創建的索引並不包含任何位置信息:沒法使用數組索引查找特定位置的數組元素,好比"comments.4"。
少數特殊狀況下,能夠對某個特定的數組條目進行索引,好比:

> db.blog.ensureIndex({"comments.10.votes": 1})

然而,只有在精確匹配第11個數組元素時這個索引纔有用(數組下標從0開始)。
一個索引中的數組字段最多隻能有一個。這是爲了不在多鍵索引中索引條目爆炸性增加:每一對可能的元素都要被索引,這樣致使每一個文檔擁有n*m個索引條目。假若有一個{"x" : 1, "y" : 1}上的索引:

> // x是一個數組—— 這是合法的
> db.multi.insert({"x" : [1, 2, 3], "y" : 1})
>
> // y是一個數組——這也是合法的
> db.multi.insert({"x" : 1, "y" : [4, 5, 6]})
>
> // x和y都是數組——這是非法的!
> db.multi.insert({"x" : [1, 2, 3], "y" : [4, 5, 6]})
cannot index parallel arrays [y] [x]

若是MongoDB要爲上面的最後一個例子建立索引,它必需要建立這麼多索引條目:{"x" : 1, "y" : 4}、{"x" : 1, "y" : 5}、{"x" : 1, "y" : 6}、{"x" : 2, "y" : 4}、{"x" : 2, "y" : 5},{"x" : 2, "y" : 6}、{"x" : 3, "y" : 4}、{"x" : 3, "y" : 5}和{"x" : 3, "y" : 6}。儘管這些數組只有3個元素。

3. 多鍵索引

對於某個索引的鍵,若是這個鍵在某個文檔中是一個數組,那麼這個索引就會被標記爲多鍵索引(multikey index)。能夠從explain()的輸出中看到一個索引是否爲多鍵索引:若是使用了多鍵索引,"isMultikey"字段的值會是true。索引只要被標記爲多鍵索引,就沒法再變成非多鍵索引了,即便這個字段爲數組的全部文檔都從集合中刪除。要將多鍵索引恢復爲非多鍵索引,惟一的方法就是刪除再重建這個索引。
多鍵索引可能會比非多鍵索引慢一些。可能會有多個索引條目指向同一個文檔,所以MongoDB在返回結果集時必需要先去除重複的內容。

四、索引基數

基數(cardinality)就是集合中某個字段擁有不一樣值的數量。有一些字段,好比"gender"或者"newsletter opt-out",可能只擁有兩個可能的值,這種鍵的基數就是很是低的。另一些字段,好比"username"或者"email",可能集合中的每一個文檔都擁有一個不一樣的值,這類鍵的基數是很是高的。固然也有一些介於二者之間的字段,好比"age"或者"zip code"。
一般,一個字段的基數越高,這個鍵上的索引就越有用。這是由於索引可以迅速將搜索範圍縮小到一個比較小的結果集。對於低基數的字段,索引一般沒法排除掉大量可能的匹配。
假設咱們在"gender"上有一個索引,須要查找名爲Susan的女性用戶。經過這個索引,只能將搜索空間縮小到大約50%,而後要在每一個單獨的文檔中查找"name"爲"Susan"的用戶。反過來,若是在"name"上創建索引,就能當即將結果集縮小到名爲"Susan"的用戶,這樣的結果集很是小,而後就能夠根據性別從中迅速地找到匹配的文檔了。
通常說來,應該在基數比較高的鍵上創建索引,或者至少應該把基數較高的鍵放在複合索引的前面(低基數的鍵以前)。

上一篇文章: MongoDB指南---十、索引、複合索引 簡介
下一篇文章: MongoDB指南---十二、使用explain()和hint()、什麼時候不該該使用索引
相關文章
相關標籤/搜索