MongoDB 索引
MongoDB 和傳統關聯式資料庫概念相念,都離不開 CRUD,裡面也是讀取(查詢)資料是最多變化的,也可能是最複雜的。其他三個動作,只要讀取熟悉了之後,就相對簡單。
讀取資料,經常需要排序,為了效能也有索引的設置,其中包含單欄(single field)索引和複合索引(compound index)。
首先先使用 explain() 查看讀取的策略,如下列(省略了不相干的內容)。在 queryPlanner.winningPlan 可以看到 stage 值為 COLLSCAN,代表 collections scan,也就是逐筆讀取。
> db.inventory.find()
{ "_id" : ObjectId("5ea4715cda0c749138d46e52"), "item" : "paper", "qty" : 100 }
{ "_id" : ObjectId("5ea4715cda0c749138d46e53"), "item" : "journal", "quantity" : 25 }
{ "_id" : ObjectId("5ea4715cda0c749138d46e54"), "item" : "planner", "qty" : 75 }
{ "_id" : ObjectId("5ea4715cda0c749138d46e55"), "item" : "postcard", "qty" : 45 }
> db.inventory.find({item: 'postcard'}).explain()
{
 "queryPlanner" : {
  "winningPlan" : {
   "stage" : "COLLSCAN",
   "filter" : {
    "item" : {
     "$eq" : "postcard"
    }
   },
   "direction" : "forward"
  }
 }
}
接著我們使用 createIndex() 新增一個 item 欄位的升冪索引。numIndexesAfter 表示加入索引後的索引數量。再使用 explain() 查看讀取的策略,stage 變成 FETCH。原則上使用 item 這欄為條件讀取資料時,速度會加快許多。
> db.inventory.createIndex({item: 1})
{
 "createdCollectionAutomatically" : false,
 "numIndexesBefore" : 1,
 "numIndexesAfter" : 2,
 "ok" : 1
}
> db.inventory.getIndexes()  // 查看所有索引
[
 {
  "v" : 2,
  "key" : {
   "_id" : 1
  },
  "name" : "_id_",
  "ns" : "test.inventory"
 },
 {
  "v" : 2,
  "key" : {
   "item" : 1
  },
  "name" : "item_1",
  "ns" : "test.inventory"
 }
]
> db.inventory.find({item: 'postcard'}).explain()
{
 "queryPlanner" : {
  "winningPlan" : {
   "stage" : "FETCH",
   "inputStage" : {
    "stage" : "IXSCAN",
    "keyPattern" : {
     "item" : 1
    },
    "indexName" : "item_1",
    "isMultiKey" : false,
    "multiKeyPaths" : {
     "item" : [ ]
    },
    "isUnique" : false,
    "isSparse" : false,
    "isPartial" : false,
    "indexVersion" : 2,
    "direction" : "forward",
    "indexBounds" : {
     "item" : [
      "[\"postcard\", \"postcard\"]"
     ]
    }
   }
  }
 }
}
組合索引的官方說明
先新增測試的資料,並建立複合索引,使用 {a: 1, b: 1}:
> db.compoundTest.insertMany([
...     {a: 10, b: 2}, {a: 10, b: 8}, {a: 10, b: 6},
...     {a: 70, b: 2}, {a: 70, b: 8}, {a: 70, b: 6},
...     {a: 30, b: 2}, {a: 30, b: 8}, {a: 30, b: 6},
... ])
> db.compoundTest.createIndex({a:1, b:1})
{
 "createdCollectionAutomatically" : false,
 "numIndexesBefore" : 1,
 "numIndexesAfter" : 2,
 "ok" : 1
}
> db.compoundTest.getIndexes()
[
 {
  "v" : 2,
  "key" : {
   "_id" : 1
  },
  "name" : "_id_",
  "ns" : "test.compoundTest"
 },
 {
  "v" : 2,
  "key" : {
   "a" : 1,
   "b" : 1
  },
  "name" : "a_1_b_1",
  "ns" : "test.compoundTest"
 }
]
以下是測試及取得 queryPlanner.winningPlan.stage 的結果:
db.compoundTest.explain().aggregate({$sort:{a: 1}})  // FETCH
db.compoundTest.explain().aggregate({$sort:{a: -1}})  // FETCH
db.compoundTest.explain().aggregate({$sort:{a: 1, b:1}})  // FETCH
db.compoundTest.explain().aggregate({$sort:{a: 1, b:-1}})  // COLLSCAN
db.compoundTest.explain().aggregate({$sort:{a: -1, b:-1}})  // FETCH
db.compoundTest.explain().aggregate({$sort:{a: -1, b:1}})  // COLLSCAN
db.compoundTest.explain().aggregate({$sort:{b: 1}})  // COLLSCAN
db.compoundTest.explain().aggregate({$sort:{b: -1}})  // COLLSCAN
由結果可以知道,複合索引的欄位是有優先順序的。我們 a 放在前面,b 放在後面,所以用 a 去排序的時候都是有效的。a 加上 b 時,只有 {a: 1, b:1} 和 {a: -1, b:-1} 是有效的,因為 {a: -1, b:-1} 是設定的相反順序排序。
所有 b 為主要順序欄位的,索引都無法發揮省時的效果,只能逐筆排查詢。