【mongoDB中級篇②】索引與expain


索引的操作

數據庫百分之八十的工作基本上都是查詢,而索引能幫我們更快的查詢到想要的數據.但是其降低了數據的寫入速度,所以要權衡常用的查詢字段,不必在太多字段上建立索引.
在mongoDB中默認是用btree來組織索引文件,並且可以按字段升序/降序來創建,便於排序.

數據准備

for (var i = 1; i <100000; i++) {
  db.test.insert({name:'user'+i,num:i,sn:Math.floor(Math.random()*10000000)})
}

索引常用操作

查看當前集合的索引

> db.test.getIndexes();
[
    {
        "v" : 1,
        "key" : {
                "_id" : 1
        },
        "name" : "_id_",
        "ns" : "test.test"
    }
]

MongoDB有個默認的_id的鍵,他相當於“主鍵”的角色。集合創建后系統會自動創建一個索引在_id鍵上,它是默認索引,索引名叫“_id”,是無法被刪除的。

另外, system.indexes集合中包含了每個索引的詳細信息,因此可以通過下面的命令查詢已經存在的索引

 db.system.indexes.find({});

創建單列索引

db.collection.ensureIndex({field:1/-1}) # 1是正序,-1是倒序

創建多列索引(組合索引)

db.collection.ensureIndex({field1:1/-1, field2:1/-1})  

在大多數情況下我們創建的索引都是多列索引,因為數據庫查詢器只會選擇最優的索引來進行查詢,在多列分別建立索引,查詢器只會選擇其中一列索引來進行查詢,而直接建立一個多列索引的話,該索引由於是作用於多列的,效率更高於單列索引,具體多列索引建立技巧可以查看下文中的 <<新版Explain的分析實例>>,另外,mongoDB的多列索引也遵循着最左前綴的原則

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

該索引被創建后,基於username和age的查詢將會用到該索引,或者是基於username的查詢也會用到該索引,但是只是基於age的查詢將不會用到該復合索引。因此可以說,如果想用到復合索引,必須在查詢條件中包含復合索引中的前N個索引列。然而如果查詢條件中的鍵值順序和復合索引中的創建順序不一致的話,MongoDB可以智能的幫助我們調整該順序,以便使復合索引可以為查詢所用

 db.test.find({"age": 30, "username": "stephen"})

對於上面示例中的查詢條件,MongoDB在檢索之前將會動態的調整查詢條件文檔的順序,以使該查詢可以用到剛剛創建的復合索引。

創建子文檔索引

db.collection.ensureIndex({'filed.subfield':1/-1});

創建唯一索引

db.collection.ensureIndex({filed:1/-1}, {unique:true});

創建稀疏索引

稀疏索引的特點------如果針對field做索引,針對不含field列的文檔,將不建立索引.
與之相對,普通索引,會把該文檔的field列的值認為NULL,並建索引.
適宜於: 小部分文檔含有某列時.

 db.tea.find();
{ "_id" : ObjectId("5275f99b87437c610023597b"), "email" : "a@163.com" }
{ "_id" : ObjectId("5275f99e87437c610023597c"), "email" : "b@163.com" }
{ "_id" : ObjectId("5275f9e887437c610023597e"), "email" : "c@163.com" }
{ "_id" : ObjectId("5275fa3887437c6100235980") }
db.collection.ensureIndex({field:1/-1},{sparse:true});

如上內容,最后一行沒有email列,如果分別加普通索引,和稀疏索引,對於最后一行的email分別當成null 和 忽略最后一行來處理.根據{email:null}來查詢,前者能利用到索引,而后者用不到索引,是一個全表掃描的過程;

創建哈希索引

哈希索引速度比普通索引快,但是,無能對范圍查詢進行優化.
適宜於隨機性強的散列

db.collection.ensureIndex({file:’hashed’});

重建索引

一個表經過很多次修改后,導致表的文件產生空洞,索引文件也如此.可以通過索引的重建,減少索引文件碎片,並提高索引的效率.類似mysql中的optimize table

db.collection.reIndex()

刪除索引

db.collection.dropIndex({filed:1/-1});  #刪除單個索引
db.collection.dropIndexes(); #刪除所有索引

正則表達式在索引中的應用

正則表達式可以靈活地匹配查詢條件,如果希望正則表達式能命中索引,就要注意了:
Mongodb能為前綴型的正則表達式命中索引(和mysql一樣),比如:需要查詢Mail中user以z開頭的:/^z/
如果有user索引,這種查詢很高效,但其他的即使有索引,也不會命中索引,比說:需要查詢Mail中的user中含有z的:

/.*z.*/
/^.*z.*/

這種查詢是不會命中到索引的,當數據量很大,速度很慢
總之,后的條件必須明確,不能.* [1]之類開頭的

查詢計划explain

我學習mongodb比較晚,安裝的是3.05版本的,發現此版本的explain的使用方法跟教程上有很大不同,究竟是從什么版本開始發生改變的,我也就不去追溯了.

新版explain介紹

新版本的explain有三種模式,作為explain的參數傳進去

  • queryPlanner 默認
  • executionStats
  • allPlansExecution

queryPlanner

queryPlanner是現版本explain的默認模式,queryPlanner模式下並不會去真正進行query語句查詢,而是針對query語句進行執行計划分析並選出winning plan。

{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "game_db.game_user", 
        "indexFilterSet": false,//針對該query是否有indexfilter(詳見下文)
        "parsedQuery": {
            "w": {
                "$eq": 1
            }
        },
        "winningPlan": {  // 查詢優化器針對該query所返回的最優執行計划的詳細內容。
            "stage": "FETCH", //最優執行計划的stage,這里返回是FETCH,可以理解為通過返回的index位置去檢索具體的文檔(詳見下文)
            "inputStage": { // 上一個stage的child stage,此處是IXSCAN,表示進行的是index scanning。
                "stage": "IXSCAN",  
                "keyPattern": { 
                    "w": 1, //所掃描的index內容
                    "n": 1 // 返回的條數?
                },
                "indexName": "w_1_n_1", //索引名稱
                "isMultiKey": false, //是否是Multikey,此處返回是false,如果索引建立在array上,此處將是true
                "direction": "forward", //此query的查詢順序,此處是forward,如果用了.sort({w:-1})將顯示backward。
                "indexBounds": { //winningplan所掃描的索引范圍,此處查詢條件是w:1,使用的index是w與n的聯合索引,故w是[1.0,1.0]而n沒有指定在查詢條件中,故是[MinKey,MaxKey]。
                    "w": ["[1.0, 1.0]"],
                    "n": ["[MinKey, MaxKey]"]
                }
            }
        },
        "rejectedPlans": [{ //他執行計划(非最優而被查詢優化器reject的)的詳細返回,其中具體信息與winningPlan的返回中意義相同
            "stage": "FETCH",
            "inputStage": { 
                "stage": "IXSCAN",
                "keyPattern": {
                    "w": 1,
                    "v": 1
                },
                "indexName": "w_1_v_1",
                "isMultiKey": false,
                "direction": "forward",
                "indexBounds": {
                    "w": ["[1.0, 1.0]"],
                    "v": ["[MinKey, MaxKey]"]
                }
            }
        }]
    }
indexFilterSet

IndexFilter決定了查詢優化器對於某一類型的查詢將如何使用index,indexFilter僅影響查詢優化器對於該類查詢可以用嘗試哪些index的執行計划分析,查詢優化器還是根據分析情況選擇最優計划。
如果某一類型的查詢設定了IndexFilter,那么執行時通過hint指定了其他的index,查詢優化器將會忽略hint所設置index,仍然使用indexfilter中設定的查詢計划。
IndexFilter可以通過命令移除,也將在實例重啟后清空。

IndexFilter的創建
db.runCommand(
   {
      planCacheSetFilter: <collection>,
      query: <query>,
      sort: <sort>,
      projection: <projection>,
      indexes: [ <index1>, <index2>, ...]
   }
)
db.runCommand(
   {
      planCacheSetFilter: "orders",
      query: { status: "A" },
      indexes: [
         { cust_id: 1, status: 1 },
         { status: 1, order_date: -1 }
      ]
   }
)

針對orders表建立了一個indexFilter,indexFilter指定了對於orders表只有status條件(僅對status進行查詢,無sort等)的查詢的indexes,所以以下的查詢語句的查詢優化器僅僅會從{cust_id:1,status:1}和{status:1,order_date:-1}中進行winning plan的選擇

db.orders.find( { status: "D" } )
db.orders.find( { status: "P" } )
indexFilter的列表

可以通過如下命令展示某一個collecton的所有indexFilter

db.runCommand( { planCacheListFilters: <collection> } )
indexFilter的刪除

可以通過如下命令對IndexFilter進行刪除

db.runCommand(
   {
      planCacheClearFilters: <collection>,
      query: <query pattern>,
      sort: <sort specification>,
      projection: <projection specification>
   }
)
Stage返回參數說明
COLLSCAN #全表掃描

IXSCAN #索引掃描

FETCH #根據索引去檢索指定document

SHARD_MERGE #將各個分片返回數據進行merge

SORT #表明在內存中進行了排序(與老版本的scanAndOrder:true一致)

LIMIT #使用limit限制返回數

SKIP #使用skip進行跳過

IDHACK #針對_id進行查詢

SHARDING_FILTER #通過mongos對分片數據進行查詢

COUNT #利用db.coll.explain().count()之類進行count運算

COUNTSCAN #count不使用Index進行count時的stage返回

COUNT_SCAN #count使用了Index進行count時的stage返回

SUBPLA #未使用到索引的$or查詢的stage返回

TEXT #使用全文索引進行查詢時候的stage返回

PROJECTION #限定返回字段時候stage的返回

executionStats

該模式是mongoDB查詢的執行狀態,類似老版本的explain

  "executionStats": {
    "executionSuccess": true, //是否執行成功
    "nReturned": 29861, //查詢的返回條數
    "executionTimeMillis": 23079, //整體執行時間 毫秒
    "totalKeysExamined": 29861, // 索引掃描次數
    "totalDocsExamined": 29861, // document掃描次數
    "executionStages": {
      "stage": "FETCH", //這里是FETCH去掃描對於documents
      "nReturned": 29861, //由於是FETCH,所以這里該值與executionStats.nReturned一致
      "executionTimeMillisEstimate": 22685,
      "works": 29862, //查看源碼中發現,每次操作會加1,且會把執行時間記錄在executionTimeMillis中。
      "advanced": 29861,//而在查詢結束EOF,works又會加1,advanced不加。正常的返回works會比nReturned多1,這時候isEOF為true(1):另外advanced的返回值只有在命中的時候+1,在skip,eof的時候不會增加
      "needTime": 0,
      "needFetch": 0,
      "saveState": 946,
      "restoreState": 946,
      "isEOF": 1,
      "invalidates": 0,
      "docsExamined": 29861, // 與executionStats.totalDocsExamined一致
      "alreadyHasObj": 0,
      "inputStage": {
        "stage": "IXSCAN",
        "nReturned": 29861,
        "executionTimeMillisEstimate": 70,
        "works": 29862,
        "advanced": 29861,
        "needTime": 0,
        "needFetch": 0,
        "saveState": 946,
        "restoreState": 946,
        "isEOF": 1,
        "invalidates": 0,
        "keyPattern": {
          "w": 1,
          "n": 1
        },
        "indexName": "w_1_n_1",
        "isMultiKey": false,
        "direction": "forward",
        "indexBounds": {
          "w": ["[1.0, 1.0]"],
          "n": ["[MinKey, MaxKey]"]
        },
        "keysExamined": 29861,
        "dupsTested": 0,
        "dupsDropped": 0,
        "seenInvalidated": 0,
        "matchTested": 0
      }
    }
  }

allPlansExecution

該模式可以看做是以上兩個模式加起來;

如何通過新版explain來分析索引

分析executionTimeMillis

"executionStats" : {
  "nReturned" : 29861,
  "totalKeysExamined" : 29861,
  "totalDocsExamined" : 29861,
  "executionTimeMillis" : 66948, # 該query的整體查詢時間
  ...
  "executionStages" : {
    ...
    "executionTimeMillisEstimate" : 66244, # 該查詢根據index去檢索document獲取29861條具體數據的時間
    ...
    "inputStage" : {
            "stage" : "IXSCAN",
            ...
            
            "executionTimeMillisEstimate" : 290, #該查詢掃描29861行index所用時間
            
            ...
}

這三個值我們都希望越少越好,那么是什么影響這這三個返回值呢?

分析index與document掃描數與查詢返回條目數

這里主要談3個返回項,nReturnedtotalKeysExaminedtotalDocsExamined,分別代表該條查詢返回的條目、索引掃描條目和文檔掃描條目。
理想狀態如下:

nReturned=totalKeysExamined & totalDocsExamined=0 (cover index,僅僅使用到了index,無需文檔掃描,這是最理想狀態。)

或者

nReturned=totalKeysExamined=totalDocsExamined(需要具體情況具體分析)(正常index利用,無多余index掃描與文檔掃描。)

如果有sort的時候,為了使得sort不在內存中進行,我們可以在保證nReturned=totalDocsExamined的基礎上,totalKeysExamined可以大於totalDocsExamined與nReturned,因為量級較大的時候內存排序非常消耗性能。

分析Stage狀態

對於普通查詢,我們最希望看到的組合有這些:

Fetch+IDHACK

Fetch+ixscan

Limit+(Fetch+ixscan)

PROJECTION+ixscan

SHARDING_FILTER+ixscan

等

不希望看到包含如下的stage:

COLLSCAN(全表掃),SORT(使用sort但是無index),不合理的SKIP,SUBPLA(未用到index的$or)

對於count查詢,希望看到的有:

COUNT_SCAN

不希望看到的有:

COUNTSCAN

新版Explain的分析實例

表中數據如下(簡單測試用例,僅10條數據,主要是對explain分析的邏輯進行解析):

{ "_id" : ObjectId("55b86d6bd7e3f4ccaaf20d70"), "a" : 1, "b" : 1, "c" : 1 }
{ "_id" : ObjectId("55b86d6fd7e3f4ccaaf20d71"), "a" : 1, "b" : 2, "c" : 2 }
{ "_id" : ObjectId("55b86d72d7e3f4ccaaf20d72"), "a" : 1, "b" : 3, "c" : 3 }
{ "_id" : ObjectId("55b86d74d7e3f4ccaaf20d73"), "a" : 4, "b" : 2, "c" : 3 }
{ "_id" : ObjectId("55b86d75d7e3f4ccaaf20d74"), "a" : 4, "b" : 2, "c" : 5 }
{ "_id" : ObjectId("55b86d77d7e3f4ccaaf20d75"), "a" : 4, "b" : 2, "c" : 5 }
{ "_id" : ObjectId("55b879b442bfd1a462bd8990"), "a" : 2, "b" : 1, "c" : 1 }
{ "_id" : ObjectId("55b87fe842bfd1a462bd8991"), "a" : 1, "b" : 9, "c" : 1 }
{ "_id" : ObjectId("55b87fe942bfd1a462bd8992"), "a" : 1, "b" : 9, "c" : 1 }
{ "_id" : ObjectId("55b87fe942bfd1a462bd8993"), "a" : 1, "b" : 9, "c" : 1 }

查詢語句

db.test.find({a:1,b:{$lt:3}}).sort({c:-1}).explain();

未加索引前

"executionStats": {
  "executionSuccess": true,
  "nReturned": 2,
  "executionTimeMillis": 0,
  "totalKeysExamined": 0, // 為0表示沒有使用索引
  "totalDocsExamined": 10, // 掃描了所有記錄
  "executionStages": {
    "stage": "SORT",  //為SORT,未使用index的sort
    "nReturned": 2,
    ..."sortPattern": {
      "c": -1
    },
    "memUsage": 126, //占用的內存
    "memLimit": 33554432, //內存限制
    "inputStage": {
      "stage": "COLLSCAN", //全表掃描
      "filter": {
        "$and": [{
          "a": {
            "$eq": 1
          }
        },
        {
          "b": {
            "$lt": 3
          }
        }]
      },
      "nReturned": 2,
      ..."direction": "forward",
      "docsExamined": 10
    }

很明顯,沒有index的時候,進行了全表掃描,在內存中sort,數據量達百萬級以后就會有明顯的慢

接着我們對C加一個正序索引

 db.d.ensureIndex({c:1})

再來看一下

"executionStats": {
  "executionSuccess": true, 
  "nReturned": 2,
  "executionTimeMillis": 1,
  "totalKeysExamined": 10,
  "totalDocsExamined": 10,
  "executionStages": {
    "stage": "FETCH",
    "filter": {
      "$and": [{
        "a": {
          "$eq": 1
        }
      },
      {
        "b": {
          "$lt": 3
        }
      }]
    },
    "nReturned": 2,
    ..."inputStage": {
      "stage": "IXSCAN",
      "nReturned": 10,
      ..."keyPattern": {
        "c": 1
      },
      "indexName": "c_1",
      "isMultiKey": false,
      "direction": "backward",
      "indexBounds": {
        "c": ["[MaxKey, MinKey]"]
      }

我們發現,Stage沒有了SORT,因為我們sort字段有了index,但是由於查詢還是沒有index,故totalDocsExamined還是10,但是由於sort用了index,totalKeysExamined也是10,但是僅對sort排序做了優化,查詢性能還是一樣的低效。

接下來, 我們對查詢條件做index

db.test.ensureIndex({b:1,a:1,c:1})
"executionStats": {
  "executionSuccess": true,
  "nReturned": 2,
  "executionTimeMillis": 0,
  "totalKeysExamined": 4,
  "totalDocsExamined": 2,
  "executionStages": {
    "stage": "SORT",
    "nReturned": 2,
    ..."sortPattern": {
      "c": -1
    },
    "memUsage": 126,
    "memLimit": 33554432,
    "inputStage": {
      "stage": "FETCH",
      "nReturned": 2,
      ..."inputStage": {
        "stage": "IXSCAN",
        "nReturned": 2,
        ..."keyPattern": {
          "b": 1,
          "a": 1,
          "c": 1
        },
        "indexName": "b_1_a_1_c_1",
        "isMultiKey": false,
        "direction": "forward",
        "indexBounds": {
          "b": ["[-inf.0, 3.0)"],
          "a": ["[1.0, 1.0]"],
          "c": ["[MinKey, MaxKey]"]
        },
        

nReturned為2,返回2條記錄
totalKeysExamined為4,掃描了4個index
totalDocsExamined為2,掃描了2個docs

此時nReturned=totalDocsExamined<totalKeysExamined,不符合我們的期望。
executionStages.StageSort,在內存中進行排序了,也不符合我們的期望

db.test.ensureIndex({a:1,b:1,c:1})
"executionStats": {
  "executionSuccess": true,
  "nReturned": 2,
  "executionTimeMillis": 0,
  "totalKeysExamined": 2,
  "totalDocsExamined": 2,
  "executionStages": {
    "stage": "SORT",
    "nReturned": 2,
    ..."sortPattern": {
      "c": -1
    },
    "memUsage": 126,
    "memLimit": 33554432,
    "inputStage": {
      "stage": "FETCH",
      "nReturned": 2,
      ..."inputStage": {
        "stage": "IXSCAN",
        "nReturned": 2,
        ..."keyPattern": {
          "a": 1,
          "b": 1,
          "c": 1
        },
        "indexName": "a_1_b_1_c_1",
        "isMultiKey": false,
        "direction": "forward",
        "indexBounds": {
          "a": ["[1.0, 1.0]"],
          "b": ["[-inf.0, 3.0)"],
          "c": ["[MinKey, MaxKey]"]
        },
        

nReturned為2,返回2條記錄
totalKeysExamined為2,掃描了2個index
totalDocsExamined為2,掃描了2個docs
此時nReturned=totalDocsExamined=totalKeysExamined,符合我們的期望。
但是!executionStages.Stage為Sort,在內存中進行排序了,這個在生產環境中尤其是在數據量較大的時候,是非常消耗性能的,這個千萬不能忽視了,我們需要改進這個點。

db.test.ensureIndex({a:1,c:1,b:1})
"executionStats": {
  "executionSuccess": true,
  "nReturned": 2,
  "executionTimeMillis": 0,
  "totalKeysExamined": 4,
  "totalDocsExamined": 2,
  "executionStages": {
    "stage": "FETCH",
    "nReturned": 2,
    ..."inputStage": {
      "stage": "IXSCAN",
      "nReturned": 2,
      ..."keyPattern": {
        "a": 1,
        "c": 1,
        "b": 1
      },
      "indexName": "a_1_c_1_b_1",
      "isMultiKey": false,
      "direction": "backward",
      "indexBounds": {
        "a": ["[1.0, 1.0]"],
        "c": ["[MaxKey, MinKey]"],
        "b": ["(3.0, -inf.0]"]
      },
      "keysExamined": 4,
      "dupsTested": 0,
      "dupsDropped": 0,
      "seenInvalidated": 0,
      "matchTested": 0

我們可以看到

nReturned為2,返回2條記錄
totalKeysExamined為4,掃描了4個index
totalDocsExamined為2,掃描了2個docs

雖然不是nReturned=totalKeysExamined=totalDocsExamined,但是Stage無Sort,即利用了index進行排序,而非內存,這個性能的提升高於多掃幾個index的代價。
綜上可以有一個小結論,當查詢覆蓋精確匹配,范圍查詢與排序的時候,{精確匹配字段,排序字段,范圍查詢字段}這樣的索引排序會更為高效

舊版本的explain

> db.blogs.find({"comment.author":"joe"}).explain();  
{  
        "cursor" : "BtreeCursor comment.author_1",  
        "nscanned" : 1,  
        "nscannedObjects" : 1,  
        "n" : 1,  
        "millis" : 70,  
        "nYields" : 0,  
        "nChunkSkips" : 0,  
        "isMultiKey" : true,  
        "indexOnly" : false,  
        "indexBounds" : {  
                "comment.author" : [  
                        [  
                                "joe",  
                                "joe"  
                        ]  
                ]  
        }  
} 

參數說明:

  1. cursor:因為這個查詢使用了索引,MongoDB中索引存儲在B樹結構中,所以這是也使用了BtreeCursor類型的游標。如果沒有使用索引,游標的類型是BasicCursor。這個鍵還會給出你所使用的索引的名稱,你通過這個名稱可以查看當前數據庫下的system.indexes集合(系統自動創建,由於存儲索引信息,這個稍微會提到)來得到索引的詳細信息。
  2. nscanned/nscannedObjects:表明當前這次查詢一共掃描了集合中多少個文檔,我們的目的是,讓這個數值和返回文檔的數量越接近越好。
  3. n:當前查詢返回的文檔數量。
  4. millis:當前查詢所需時間,毫秒數。
  5. indexBounds:當前查詢具體使用的索引

hint強制使用某個索引

> db.user.ensureIndex({"name":1,"age":1});
> db.user.ensureIndex({"age":1,"name":1});
> db.user.find({"age":40, "name":"tim"}).explain();
{
    "cursor" : "BtreeCursor name_1_age_1",
    "nscanned" : 1,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 0,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
            "name" : [
                    [
                            "tim",
                            "tim"
                    ]
            ],
            "age" : [
                    [
                            40,
                            40
                    ]
            ]
    }
}

返回文檔的鍵沒有區別,其默認使用了索引"name_1_age_1",這是查詢優化器為我們使用的索引!我們此處可以通過hint進行更行,即強制這個查詢使用我們定義的“age_1_name_1”索引,如下

> var cursor = db.user.find({"age":40, "name":"tim"}).hint({"age":1,"name":1});
> cursor.explain();
{
    "cursor" : "BtreeCursor age_1_name_1",
    "nscanned" : 1,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 0,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
            "age" : [
                    [
                            40,
                            40
                    ]
            ],
            "name" : [
                    [
                            "tim",
                            "tim"
                    ]
            ]
    }
}

hint函數會返回游標,我們可以在游標上調用explain查看索引的使用情況!99%的情況,我們沒有必要通過hint去強制使用某個索引,MongoDB的查詢優化器非常智能,絕對能幫助我們使用最佳的索引去進行查詢!


  1. a-z ↩︎


免責聲明!

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



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