天天看點

MongoDB索引-查詢優化器索引概述索引的建立索引的選擇總結

索引概述

介紹查詢優化器首先要從索引開始。索引在計算機系統中應用非常廣泛,是提高查詢效率的常用手段。如果沒有索引,MongoDB必須周遊集合中所有文檔才能找到比對的結果;如果存在一個适當的索引可以限制MongoDB必須檢查的文檔數量。

在MongoDB中,索引是一種特殊的資料結構,以一種便于周遊的方式存儲集合資料的部分資訊。

常見的索引有幾種組織模型,其中,B-Tree索引可以看做将鍵值映射到有序數組中的位置;Hash索引将鍵值映射到無序數組中的位置。MongoDB預設采用B-Tree組織索引檔案,在2.4版本後也允許建立Hash索引。

索引的建立

MongoDB索引是在集合上建立的,通過ensureIndex方法可以友善的設定索引。MongoDB集合可以建立多個索引。恰當的索引可以顯著提高查詢效率,但是當資料發生變動時,不僅需要修改更新文檔,還要更新集合上的索引,這無疑會導緻寫操作要花費更多的時間。是以,MongoDB限制每個集合最多可以有64個索引,當然,隻要設計合理,這個上限也是足夠的。

我們用《MongoDB 權威指南》中的例子做一下修改,假設用MongoDB存儲百萬級使用者資料,包括姓名(name), 性别(sex), 年齡(age),電話(tel)資訊。為了便于說明問題,這裡隻寫四條資料:

> db.users.find()
{ "_id" : ObjectId("5a2f8e7bd24b2183ae3b9fab"), "name" : "Andy", "sex" : "male", "age" : , "tel" : "1300000001" }
{ "_id" : ObjectId("5a2f97c7eb221206e13c7c3f"), "name" : "Bale", "sex" : "male", "age" : , "tel" : "1300000002" }
{ "_id" : ObjectId("5a2fa15beb221206e13c7c40"), "name" : "Cindy", "sex" : "female", "age" : , "tel" : "1300000003" }
{ "_id" : ObjectId("5a2fa1cbeb221206e13c7c41"), "name" : "Dany", "sex" : "male", "age" : , "tel" : "1300000004" }
           

我們建立三個索引:

> db.users.ensureIndex({"age": , "sex": });
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : ,
    "numIndexesAfter" : ,
    "ok" : 
}
> db.users.ensureIndex({"age": });
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : ,
    "numIndexesAfter" : ,
    "ok" : 
}
> db.users.ensureIndex({"sex": , "age": });
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : ,
    "numIndexesAfter" : ,
    "ok" : 
}
           

在建立集合時,MongoDB會預設在_id字段上建立一個唯一索引,并且不能删除。是以當我們建立第一個索引時numIndexesBefore值是1。需要注意的是,B-Tree索引是有序的,上面三個索引大緻結構是:

# {"age": 1, "sex": 1}
[, male]      ->  ObjectId("5a2f8e7bd24b2183ae3b9fab")
[, female]    ->  ObjectId("5a2fa15beb221206e13c7c40")
[, male]      ->  ObjectId("5a2f97c7eb221206e13c7c3f")
[, male]      ->  ObjectId("5a2fa1cbeb221206e13c7c41")

# {"age": 1}
[]      ->  ObjectId("5a2f8e7bd24b2183ae3b9fab")
[]      ->  ObjectId("5a2fa15beb221206e13c7c40")
[]      ->  ObjectId("5a2f97c7eb221206e13c7c3f")
[]      ->  ObjectId("5a2fa1cbeb221206e13c7c41")

# {"sex": 1, "age": 1}
[female, ]    ->  ObjectId("5a2fa15beb221206e13c7c40")
[male, ]      ->  ObjectId("5a2f8e7bd24b2183ae3b9fab")
[male, ]      ->  ObjectId("5a2f97c7eb221206e13c7c3f")
[male, ]      ->  ObjectId("5a2fa1cbeb221206e13c7c41")
           

索引的選擇

MongoDB一個集合可以建立多個索引,查詢優化器為索引建立查詢計劃,将選擇索引轉換為選擇查詢計劃或者說選擇查詢計劃對應的遊标。

由于最近版本中幾乎看不到優化器的概念,我們從較早的版本中開始介紹,以MongoDB-2.4.14為例,具體的選擇過程可以分為三個階段:首先根據查詢模式(Query pattern)判斷是否存在CachedPlan,如果存在直接選擇,這裡查詢模式是除了數值外其他查詢條件完全相同;其次,如果沒有緩存記錄,查詢優化器建立新查詢計劃并标記類型,如果類型為Optimal Plan則直接執行該Plan;最後,如果不存在Optimal Plan,MongoDB會并發嘗試可能的Helpful Plan以及不使用索引的基礎查詢。查詢優化器會對比選擇表現最好的查詢計劃繼續執行,并将查詢模式與最終查詢計劃的映射寫入CachedPlan。

Optimal Plan

Optimal Plan的索引首先應該包含所有查詢的過濾字段和排序字段,其次,字段在索引中的順序是範圍過濾和排序字段位于相等字段之後。有多個Optimal Plan時,會執行第一個。

Helpful Plan

不存在Optimal Plan時,查詢優化器會并發執行所有Helpful查詢計劃。最早得到101個結果的查詢計劃會被選中繼續執行,并将該查詢計劃暫存。其他查詢計劃會被中止。

下面列舉幾個例子,為了便于說明,我們将集合文檔數擴大到1000000:

> var names = ["Andy", "Bale", "Cindy", "Dany", "Emmy", "Ford", "Gill"]
> var sexs = ["male", "female"]
> for (i=; i<; i++) {
    db.users.insert(
        {"name": names[Math.floor(Math.random()*)],
         "sex": sexs[Math.floor(Math.random()*)],
         "age": Math.floor(Math.random()*),
         "tel": +i
        } 
    ); 
}
           

首先查詢age為18的所有記錄,這裡隻截取部分輸出資訊:

> db.users.find({"age": }).explain({"verbose": true})
{
    "cursor" : "BtreeCursor age_1_sex_1",
    "isMultiKey" : false,
    "n" : ,
    "nscannedObjects" : ,
    "nscanned" : ,
    "millis" : ,
    "allPlans" : [
        {
            "cursor" : "BtreeCursor age_1_sex_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        }
    ],
}
           

根據定義“age_1_sex_1”和“age_1”對應的查詢計劃都是Optimal Plan,這裡選取了第一個便不再查找其他Plan,是以allPlans也隻有“age_1_sex_1”一個。

下面第二次查詢age為18的記錄,同樣截取部分輸出:

> db.users.find({"age": }).explain({"verbose": true})
{
    "cursor" : "BtreeCursor age_1_sex_1",
    "isMultiKey" : false,
    "n" : ,
    "nscannedObjects" : ,
    "nscanned" : ,
    "millis" : ,
    "allPlans" : [
        {
            "cursor" : "BtreeCursor age_1_sex_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        }
    ],
    "oldPlan" : {
        "cursor" : "BtreeCursor age_1_sex_1",
    },
}
           

與第一次不同,這裡多了oldPlan。注意:

- 根據查詢模式查找緩存查詢計劃,此時如果查找age為20隻有值不同,同樣會命中緩存。

- 由于指定了explain(),本次查詢并沒有直接使用緩存計劃而是需要重新評估。

緩存的查詢計劃在以下條件下會清空并重新評估:

- 集合收到1000次寫操作

- 執行reindex

- 添加或删除索引

- mongod程序重新開機

- 查詢時指定explain()

第三個例子,我們查詢name為Andy,age為18的記錄:

> db.users.find({"age": , "name": "Andy"}).explain({"verbose": true})
{
    "cursor" : "BtreeCursor age_1",
    "isMultiKey" : false,
    "n" : ,
    "nscannedObjects" : ,
    "nscanned" : ,
    "millis" : ,
    "allPlans" : [
        {
            "cursor" : "BtreeCursor age_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        },
        {
            "cursor" : "BtreeCursor age_1_sex_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        },
        {
            "cursor" : "BasicCursor",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,

        }
    ],
}
           

此次查詢沒有最優索引,兩個索引“age_1_sex_1”和“age_1”對查詢有幫助,連同基礎查詢總共建立三個查詢計劃。

評估候選查詢計劃會在以下情況之一發生時結束:

- 擷取到所有查詢結果;

- 最先得到101個查詢結果(enoughCumulativeMatchesToChooseAPlan)

上面例子中,“age_1”最先傳回101個查詢結果,對應的查詢計劃執行到最後,其他查詢計劃被中止。

在MongoDB較新的版本中(以3.2.10為例),查詢優化器做了很大的改變。對于每個查詢,系統首先在查詢計劃緩存中比對相同查詢資訊(Query Shape)選擇查詢計劃;如果沒有命中緩存,查詢計劃生成器會評估所有候選查詢計劃,選擇一個最優計劃(Winning Plan),并将最有計劃寫入緩存。

當緩存命中時,系統會重新評估該緩存計劃,作出是否合格的判定,并根據判定結果保留或清除緩存計劃。

下面是3.2.10版本的一次查詢:

replset:PRIMARY> db.users.find({"age": , "sex": "male"}).explain("queryPlanner")
{
    "queryPlanner" : {
        "namespace" : "date.users",
        "indexFilterSet" : false,
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "indexName" : "sex_1_age_1",
            }
        },
        "rejectedPlans" : [
            {
                "inputStage" : {
                    "indexName" : "age_1",
                }
            },
            {
                "inputStage" : {
                    "indexName" : "age_1_sex_1",
                }
            }
        ]
    },
}
           

兩個版本最主要的差別就是新版本取消了直接通過規則選擇Optimal Plan的邏輯,按照以前的比對規則,有“sex_1_age_1”這樣的Optimal Plan不會再執行其他Optimal Plan和Helpfun Plan。取消Optimal Pan是因為往往Optimal Plan并不是最優的。下面是MongoDB-2.4.14的兩個例子,同樣隻截取部分資訊說明:

> db.users.find({"age": }, {"age": , "sex": , "_id": }).explain("verbose")
{
    "cursor" : "BtreeCursor age_1",
    "n" : ,
    "nscannedObjects" : ,
    "nscanned" : ,
    "indexOnly" : false,
    "millis" : ,
    "allPlans" : [
        {
            "cursor" : "BtreeCursor age_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        }
    ],
}
> db.users.find({"age": }, {"age": , "sex": , "_id": }).hint("age_1_sex_1").explain("verbose")
{
    "cursor" : "BtreeCursor age_1_sex_1",
    "n" : ,
    "nscannedObjects" : ,
    "indexOnly" : true,
    "millis" : ,
    "allPlans" : [
        {
            "cursor" : "BtreeCursor age_1_sex_1",
            "n" : ,
            "nscannedObjects" : ,
            "nscanned" : ,
        }
    ],
}
           

上面的兩次查詢中,第一次查詢“age_1”首先比對到Optimal Plan并執行到最後,用時755毫秒,“age_1_sex_1”雖然同樣是Optimal Plan,但是索引的順序排在“age_1”之後,是以沒有被選中。第二次查詢中通過hint顯式指定“age_1_sex_1”,通過該索引查詢是“indexOnly”,不需要掃描文檔就可以完成查詢,是以效率更高,用時隻有27毫秒。

另外,查詢優化器還有一個變化,比對緩存計劃由Query Pattern變為Query Shape。官網上對二者的定義分别是:

- Query pattern refers to query select conditions that differ only in the values.

- A query shape consists of a combination of query, sort, and projection specifications.

Query Pattern 關注的是Fileds,Range和Sort,而Query Shape增加了Projection。

總結

查詢優化器極大提升了查詢性能,随着版本的更新,雖然最新的代碼中已經很難找到“optimizer”的字段,但是邏輯仍然保留并且變得更加完善。

目前,查詢優化器并不能保證每次查詢都是最優的,無論是OptimalPlan 還是WinningPlan都是相對的,配合hint,index filter會使查詢優化器更精準。

查詢優化器是一次MongoDB查詢的一個重要環節,深入了解可以幫助我們更合理的設計索引,更高效的使用MongoDB。