Mongo 索引

开启 MongoDB 学习新篇章

Posted by lichao modified on August 4, 2021

索引可以用来优化查询,为集合选择合适的索引是提升性能的关键。

使用 expalin() 函数查看在执行查询的过程中所做的事情。MongoDB 3.0+ 的 explain 有三种模式,分别是:queryPlanner、executionStats、allPlansExecution。现实开发中,常用的是 executionStats 模式,主要分析这种模式。

1
2
> db.blog.find( { "$or" : [{"title": {"$gt": "my", "$lte": "my blog title"}}, {"votes" : 2}]}).explain("executionStats")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
{
 // queryPlanner 显示被查询优化器选择的查询计划
 "queryPlanner" : {
  "plannerVersion" : 1,
  // namespace 返回的是该 query 所查询的表
  "namespace" : "test.blog",
  "indexFilterSet" : false,
  "parsedQuery" : {
   "$or" : [
    {
     "$and" : [
      {
       "title" : {
        "$lte" : "my blog title"
       }
      },
      {
       "title" : {
        "$gt" : "my"
       }
      }
     ]
    },
    {
     "votes" : {
      "$eq" : 2
     }
    }
   ]
  },
  // 查询优化器针对该query所返回的最优执行计划的详细内容
  "winningPlan" : {
   // 最优执行计划的 stage
   "stage" : "SUBPLAN",
   // 用来描述子 stage,并且为其父 stage 提供文档和索引关键字
   "inputStage" : {
    "stage" : "FETCH",
    "inputStage" : {
     "stage" : "OR",
     "inputStages" : [
      {
       "stage" : "IXSCAN",
       // 所扫描的index内容
       "keyPattern" : {
        "title" : 1
       },
       // 所选用的index
       "indexName" : "title_1",
       // 是否使用了多键索引,此处返回是 false,如果索引建立在 array 上,此处将是 true
       "isMultiKey" : false,
       "multiKeyPaths" : {
        "title" : [ ]
       },
       "isUnique" : false,
       "isSparse" : false,
       "isPartial" : false,
       "indexVersion" : 2,
       // 此query的查询顺序,此处是forward,如果用了.sort({"title":-1})将显示backward
       "direction" : "forward",
       // 所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey]
       "indexBounds" : {
        "title" : [
         "(\"my\", \"my blog title\"]"
        ]
       }
      },
      {
       "stage" : "IXSCAN",
       "keyPattern" : {
        "votes" : 1
       },
       "indexName" : "votes_1",
       "isMultiKey" : false,
       "multiKeyPaths" : {
        "votes" : [ ]
       },
       "isUnique" : false,
       "isSparse" : false,
       "isPartial" : false,
       "indexVersion" : 2,
       "direction" : "forward",
       // 索引的使用情况,给出了索引的遍历范围。
       "indexBounds" : {
        "votes" : [
         "[2.0, 2.0]"
        ]
       }
      }
     ]
    }
   }
  },
  // 其他执行计划(非最优而被查询优化器reject的)的详细返回
  "rejectedPlans" : [ ]
 },
 "executionStats" : {
  "executionSuccess" : true,
  // query 返回的文档数量
  "nReturned" : 3,
  // query 的整体查询时间
  "executionTimeMillis" : 0,
  // 索引扫描条目, 0 表示没有用到索引
  "totalKeysExamined" : 4,
  // 文档扫描条目
  "totalDocsExamined" : 3,
  "executionStages" : {
   // FETCH 表示根据索引去检索指定 document
   // SUBPLAN 表示未使用到索引的 $or 查询
   "stage" : "SUBPLAN",
   "nReturned" : 3,
   // stage 预计执行时间
   "executionTimeMillisEstimate" : 0,
   "works" : 6,
   "advanced" : 3,
   "needTime" : 2,
   // 为了让写入请求能够顺利执行,本次查询暂停的次数。如果有写入请求需要处理,查询会周期性的释放它们的锁,以便能够写入顺利执行。
   "needYield" : 0,
   "saveState" : 0,
   "restoreState" : 0,
   "isEOF" : 1,
   // 用于容纳 子stage
   "inputStage" : {
    //子stage
    "stage" : "FETCH",
    "nReturned" : 3,
    "executionTimeMillisEstimate" : 0,
    "works" : 6,
    "advanced" : 3,
    "needTime" : 2,
    "needYield" : 0,
    "saveState" : 0,
    "restoreState" : 0,
    "isEOF" : 1,
    "docsExamined" : 3,
    "alreadyHasObj" : 0,
    "inputStage" : {
     "stage" : "OR",
     "nReturned" : 3,
     "executionTimeMillisEstimate" : 0,
     "works" : 6,
     "advanced" : 3,
     "needTime" : 2,
     "needYield" : 0,
     "saveState" : 0,
     "restoreState" : 0,
     "isEOF" : 1,
     "dupsTested" : 4,
     "dupsDropped" : 1,
     "inputStages" : [
      {
       "stage" : "IXSCAN",
       "nReturned" : 1,
       "executionTimeMillisEstimate" : 0,
       "works" : 2,
       "advanced" : 1,
       "needTime" : 0,
       "needYield" : 0,
       "saveState" : 0,
       "restoreState" : 0,
       "isEOF" : 1,
       "keyPattern" : {
        "title" : 1
       },
       "indexName" : "title_1",
       "isMultiKey" : false,
       "multiKeyPaths" : {
        "title" : [ ]
       },
       "isUnique" : false,
       "isSparse" : false,
       "isPartial" : false,
       "indexVersion" : 2,
       "direction" : "forward",
       "indexBounds" : {
        "title" : [
         "(\"my\", \"my blog title\"]"
        ]
       },
       "keysExamined" : 1,
       "seeks" : 1,
       "dupsTested" : 0,
       "dupsDropped" : 0
      },
      {
       "stage" : "IXSCAN",
       "nReturned" : 3,
       "executionTimeMillisEstimate" : 0,
       "works" : 4,
       "advanced" : 3,
       "needTime" : 0,
       "needYield" : 0,
       "saveState" : 0,
       "restoreState" : 0,
       "isEOF" : 1,
       "keyPattern" : {
        "votes" : 1
       },
       "indexName" : "votes_1",
       "isMultiKey" : false,
       "multiKeyPaths" : {
        "votes" : [ ]
       },
       "isUnique" : false,
       "isSparse" : false,
       "isPartial" : false,
       "indexVersion" : 2,
       "direction" : "forward",
       "indexBounds" : {
        "votes" : [
         "[2.0, 2.0]"
        ]
       },
       "keysExamined" : 3,
       "seeks" : 1,
       "dupsTested" : 0,
       "dupsDropped" : 0
      }
     ]
    }
   }
  }
 },
 "serverInfo" : {
  "host" : "C02CPDBCMD6M",
  "port" : 27017,
  "version" : "4.4.6",
  "gitVersion" : "72e66213c2c3eab37d9358d5e78ad7f5c1d0d0d7"
 },
 "ok" : 1
}
satge 解析
COLLSCAN 全表扫描
IXSCAN 扫描索引
FETCH 根据索引去检索指定 document
SHARD_MERGE 将各个分片返回数据进行 merge
SORT 在内存中进行了排序
LIMIT 使用 limit 限制返回数
SKIP 使用 skip 进行跳过
IDHACK 针对 _id 进行查询
SHARDING_FILTER 通过 mongos 对分片数据进行查询
COUNT 利用 db.coll.explain().count() 之类进行count 运算
COUNTSCAN 不使用 Index 进行 count
COUNT_SCAN 使用 Index 进行 count
SUBPLA 未使用到索引的 $or 查询
TEXT 使用全文索引进行查询
PROJECTION 限定返回字段

对于普通查询,希望看到 stage 的组合(查询的时候尽可能用上索引):

  • Fetch + IDHACK
  • Fetch + ixscan
  • Limit+(Fetch + ixscan)
  • PROJECTION + ixscan
  • SHARDING_FITER + ixscan
  • COUNT_SCAN

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

  • COLLSCAN(全表扫描)
  • SORT(使用 sort 但是无 index)
  • 不合理的 SKIP
  • SUBPLA(未用到 index 的 $or)
  • COUNTSCAN(不使用 index 进行 count)

查询优化器

MongoDB 查询优化器与其他数据库的稍微不同。基本来说,如果一个索引能够精确匹配一个查询,那么查询优化器就会使用这个索引,如果不能精确匹配,可能会有几个索引都适合查询。那MongoDB是怎样选择的呢?答:MongoDB的查询计划会将多个索引并行的去执行,最早返回100个结果的就是胜者,其他查询计划都会被终止。

这个查询计划会被缓冲,接下来的这个查询都会使用它,下面几种情况会重新计划:

  • 最初的计划评估之后集合发生了比较大的数据波动,查询优化器就会重新挑选可行的查询计划
  • 建立索引时
  • 每执行 1000 次查询之后,查询优化器就会重新评估查询计划

索引

索引可以根据给定的字段组织数据, 让 MongoDB 能够非常快地找到目标数据。

对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗费更多的时间,因为当数据发生变动时,MongoDB不仅要更新文档,还要更新集合上的所有索引。

MongoDB 限制每个集合上最多只能有 64 个索引。

提取较小的子数据集时,索引非常高效。不建议使用索引的情况:结果集在原集合中所占的比例越大(30%),索引的速度越慢(因为使用索引需要进行两次查找,而全表扫描只需要进行一次查询)

默认情况下,每个集合都有一个 “_id” 唯一索引。 “_id” 一经创建就无法删除了;

查询索引

查看集合中的索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
> db.blog.getIndexs()

[
 {
  "v" : 2,
  "key" : {
   "_id" : 1
  },
  "name" : "_id_"
 },
 {
  "v" : 2,
  "key" : {
   "title" : 1
  },
  "name" : "title_1"
 },
 {
  "v" : 2,
  "key" : {
   "title" : 1,
   "votes" : 1
  },
  "name" : "title_1_votes_1"
 }
]

索引名称 “name” 用于唯一标识这个索引,可以用于服务端来删除或者删除操作索引。

创建索引

在字段上创建索引,1(升序); -1(降序)。索引可以在任何类型的字段,甚至文档。 当系统已有大量数据时,创建索引就是个非常耗时的活,{background:true} 表示在后台执行。这样在创建索引时,如果有新的数据库请求需要处理,创建索引的过程就会暂停一下,但是仍然会对应用程序性能有较大的影响。

1
2
3
4
5
6
7
8
> db.blog.ensureIndex({"title": 1}, {background:true})

{
 "createdCollectionAutomatically" : false,
 "numIndexesBefore" : 1,
 "numIndexesAfter" : 2,
 "ok" : 1
}

复合索引(又名组合索引,建立在多个字段上的索引),当创建组合索引时,字段后面的 1 表示升序,-1 表示降序,是用 1 还是用 -1 主要是跟排序或指定范围查询有关。

1
2
> db.blog.ensureIndex({"title": 1, "votes": 1})

唯一索引,指定{unique:true}即可创建唯一索引。

如果一个文档没有对应的键,或键值为 null,索引会将其作为 null 存储。如果对某个键建立了唯一索引,但插入了多个缺少该索引键/索引键为 null 的文档,由于集合已经存在一个该索引键的值为 null 的文档而导致插入失败。 有些情况下,一个值可能无法被索引。索引储桶(index bucket)的大小是有限制的,如果某单个索引条目超出了它的单个索引大小的限制,那么这个条目就不会包含在索引里。这样会造成使用这个索引进行查询时会有一个文档凭空消失不见了。所有的字段值都必须小于 1024 字节,才能包含到索引里。如果一个文档的字段由于太大不能包含在索引里,MongoDB不会返回任何错误或者警告。

1
2
> db.blog.ensureIndex({"title": 1}, {unique:true} )

在已有的集合上创建唯一索引时可能会失败,因为集合中可能已经存在重复值了。在极少的情况下,可能希望直接删除重复的值。创建索引时使用”dropDups”选项,如果遇见重复的值,第一个会被保留,之后的重复文档都会被删除。

1
2
> db.blog.ensureIndex({"title": 1}, {unique:true, dropDups: true} )

稀疏索引,当被索引的字段存在时作为索引条目存储在索引中,当被索引的字段不存在时,不在索引中存储。可以使用 “sparse” 选项创建稀疏索引。

1
2
> db.blog.ensureIndex({"title": 1}, { sparse: true} )

把 “unique” 和 “sparse” 放一起使用,可以创建一个唯一的稀疏索引。这时如果文档中存在这个字段,这个字段的值必须是唯一的。

1
2
> db.blog.ensureIndex({"title": 1}, {unique:true, sparse: true} )

TTL(time to live)索引允许为每一个文档设置一个超时时间。一个文档到达预设值的超时时间就会被删除。指定 expireAfterSecs 选项就可以创建一个 TTL索引:

1
2
3
// 超时时间为 24 小时
> db.blog.ensureIndex({"lastUpdated": 1}, {expireAfterSeconds: 60*60*24} )

这样就在 “lastUpdated” 字段上建立了一个 TTL索引。如果一个文档的 TTL索引 字段存在并且它的值是日期类型,当服务器时间比文档的 索引字段时间 晚 expireAfterSeconds秒时,文档就会被删除。可以对索引字段的时间进行更新,来变更过期时间。

使用索引

为了优化查询,可将查询结果限制为 1,这样 MongoDB 在找到一个文档之后就会停止了。

1
2
> db.blog.find({"title":"my blog title"}).limit(1).explain("executionStats")

使用索引键对文档进行排序非常高效

1
2
> db.blog.find().sort({"title": 1, "votes": 1})

hint 强制 MongoDB 使用某个特定的索引。

1
> db.blog.find().sort({"title": 1, "votes": 1}).hint({"title":1, "votes":1})

{"$natural":1} 可以强制数据库做全表扫描 ,它可以指定文档按照磁盘上的顺序排列

1
> db.blog.find().sort({"title": 1, "votes": 1}).hint({"$natural":1})

{"$natural":1} 可以指定文档按照磁盘上的顺序排序,对一个活跃的集合来说,这是没有意义的,因为随着文档体积的增加或者缩小,文档会在磁盘上进行移动,新的文档会被写入到这些文档留下的空白位置。但是,对于只需要进行插入的工作而言,如果需要得到最新的文档,使用它就非常有用了。

MongoDB 可以在任意方向上对索引进行遍历。

1
> db.blog.find({"title": "my blog title"}).sort({"votes": -1})

这是一个点查询,用于查找单个值(尽管这个值的文档可能有多个),由于索引中的第二个字段,查询结果已经是有序的了,MongoDB可以从匹配的最后一个索引开始,逆序依次遍历索引。

1
> db.blog.find({"title": {"$gt": "my", "$lte": "my blog title"}})

这是一个多值查询,利用索引的第一个键查找到多个值相匹配的文档。通常来说,如果 MongoDB 使用索引进行查询,那么查询结果文档通常是按照索引顺序排列的。

1
> db.blog.find({"title": {"$gt": "my", "$lte": "my blog title"}}).sort({"votes": -1})

使用索引得到的结果集中 votes 是无序的,所以 MongoDB 需要先在内存中对结果进行排序,然后才能返回。(如果结果集的大小超过 32MB,MongoDB就会出错,拒绝对如此多的数据进行排序)。

在基于多个查询条件进行排序时,索引方向比较重要,需保证索引方向和排序方向相同。相互反转(在每个方向都乘以-1)的索引是等价的:{"title": 1, "votes": -1} 适合的查询与 {"title": -1, "votes": 1} 是完全一样的。

为了使用覆盖索引,需要使用投射来指定不要返回 _id 字段。

如果在一个含有数组的字段上做索引,这个索引永远也无法覆盖查询(因为数组是被保存在索引中的)。即便将数组字段从需要返回的字段中剔除,这样的索引也无法覆盖查询。

删除索引

删除表中的所有索引

1
db.blog.dropIndexes()

删除表中的某个索引,参数是 索引的名称。

1
db.blog.dropIndex("title_1")
低效查询

【禁止使用】$where 查询无法使用索引。

【禁止使用】检查一个键是否存在的查询 {"key":{"$exists":true}} 无法使用索引,在索引中,不存在的字段和 null 字段的存储方式是一样的,查询必须遍历每一个文档检查这个值是否真的为 null 还是根本不存在。

【不建议使用】$ne 取反查询可以使用索引,但是效率比较低,因为必须要查看所有的索引条目,而不只是 $ne 指定的条目,不得不扫描整个索引。

1
2
> db.blog.find({"title": {"$ne": "my blog title"}}).explain("executionStats")

【不建议使用】 $not 有时能够使用索引,但是通常并不知道如何使用索引,所以大多数使用 $not 的查询都会退化为 进行全表扫描。

1
2
> db.blog.find({"votes": {"$not": {"$gt":1}}}).explain("executionStats")

$in 可以使用索引,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
> db.blog.find({"votes": {"$in":[-1,2]}}).explain("executionStats")


"executionStages" : {
 "stage" : "FETCH",
 "nReturned" : 3,
 "executionTimeMillisEstimate" : 0,
 "works" : 5,
 "advanced" : 3,
 "needTime" : 1,
 "needYield" : 0,
 "saveState" : 0,
 "restoreState" : 0,
 "isEOF" : 1,
 "docsExamined" : 3,
 "alreadyHasObj" : 0,
 "inputStage" : {
  "stage" : "IXSCAN",
  "nReturned" : 3,
  "executionTimeMillisEstimate" : 0,
  "works" : 5,
  "advanced" : 3,
  "needTime" : 1,
  "needYield" : 0,
  "saveState" : 0,
  "restoreState" : 0,
  "isEOF" : 1,
  "keyPattern" : {
   "votes" : 1
  },
  "indexName" : "votes_1",
  "isMultiKey" : false,
  "multiKeyPaths" : {
   "votes" : [ ]
  },
  "isUnique" : false,
  "isSparse" : false,
  "isPartial" : false,
  "indexVersion" : 2,
  "direction" : "forward",
  "indexBounds" : {
   "votes" : [
    "[-1.0, -1.0]",
    "[2.0, 2.0]"
   ]
  },
  "keysExamined" : 5,
  "seeks" : 2,
  "dupsTested" : 0,
  "dupsDropped" : 0
 }
}

$nin 也可以使用索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
> db.blog.find({"votes": {"$nin":[-1,2]}}).explain("executionStats")


"executionStages" : {
 "stage" : "FETCH",
 "nReturned" : 4,
 "executionTimeMillisEstimate" : 0,
 "works" : 6,
 "advanced" : 4,
 "needTime" : 1,
 "needYield" : 0,
 "saveState" : 0,
 "restoreState" : 0,
 "isEOF" : 1,
 "docsExamined" : 4,
 "alreadyHasObj" : 0,
 "inputStage" : {
  "stage" : "IXSCAN",
  "nReturned" : 4,
  "executionTimeMillisEstimate" : 0,
  "works" : 6,
  "advanced" : 4,
  "needTime" : 1,
  "needYield" : 0,
  "saveState" : 0,
  "restoreState" : 0,
  "isEOF" : 1,
  "keyPattern" : {
   "votes" : 1
  },
  "indexName" : "votes_1",
  "isMultiKey" : false,
  "multiKeyPaths" : {
   "votes" : [ ]
  },
  "isUnique" : false,
  "isSparse" : false,
  "isPartial" : false,
  "indexVersion" : 2,
  "direction" : "forward",
  "indexBounds" : {
   "votes" : [
    "[MinKey, -1.0)",
    "(-1.0, 2.0)",
    "(2.0, MaxKey]"
   ]
  },
  "keysExamined" : 5,
  "seeks" : 2,
  "dupsTested" : 0,
  "dupsDropped" : 0
 }
}

查询中的字段顺序无关紧要,MongoDB会自动找出可以使用索引的字段,而无视查询中的字段顺序。

设计基于多个字段的索引时,应该将会用于精确匹配的字段放在索引的前面,将用于范围匹配的字段放在后面。这样查询就可以先使用第一个索引键进行精确查询,然后再使用第二个索引范围在这个结果集内部进行搜索。

例如在 { "title": 1, votes: 1} 索引下,下面查询会定位到 “title” 为 “my blog title” 的索引条目,然后在结果集中搜索 votes 区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> db.blog.find({"title":  "my blog title", "votes":{"$gt":1, "$lt" :2 }}).explain("executionStats")


{
 "executionStats" : {
   "inputStage" : {
     "indexBounds" : {
      "title" : [
       "[\"my blog title\", \"my blog title\"]"
      ],
      "votes" : [
       "(1.0, 2.0)"
      ]
    },
   }
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> db.blog.find({"title": {"$gt": "my", "$lte": "my blog title"}, "votes":2}).explain("executionStats")


{
 "executionStats" : {
   "inputStage" : {
     "indexBounds" : {
      "title" : [
       "(\"my\", \"my blog title\"]"
      ]
    },
    },
   }
 }
}

$or 可以对每个字句都使用索引,因为$or实际上是执行两次查询然后将结果集合并(剔除重复的文档)。通常来说,执行两次查询将结果合并的效率不如单次查询高,因此,应该尽可能使用$in 而不是 $or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
> db.blog.find( { "$or" : [{"title": {"$gt": "my", "$lte": "my blog title"}}, {"votes" : 2}]}).explain("executionStats")


{
 "executionStats" : {
   "inputStage" : {
    "inputStage" : {
     "inputStage" : [
     {
      "stage" : "IXSCAN",
      "nReturned" : 1,
      "executionTimeMillisEstimate" : 0,
      "works" : 2,
      "advanced" : 1,
      "needTime" : 0,
      "needYield" : 0,
      "saveState" : 0,
      "restoreState" : 0,
      "isEOF" : 1,
      "keyPattern" : {
       "title" : 1
      },
      "indexName" : "title_1",
      "isMultiKey" : false,
      "multiKeyPaths" : {
       "title" : [ ]
      },
      "isUnique" : false,
      "isSparse" : false,
      "isPartial" : false,
      "indexVersion" : 2,
      "direction" : "forward",
      "indexBounds" : {
       "title" : [
        "(\"my\", \"my blog title\"]"
       ]
      },
      "keysExamined" : 1,
      "seeks" : 1,
      "dupsTested" : 0,
      "dupsDropped" : 0
     },
     {
      "stage" : "IXSCAN",
      "nReturned" : 3,
      "executionTimeMillisEstimate" : 0,
      "works" : 4,
      "advanced" : 3,
      "needTime" : 0,
      "needYield" : 0,
      "saveState" : 0,
      "restoreState" : 0,
      "isEOF" : 1,
      "keyPattern" : {
       "votes" : 1
      },
      "indexName" : "votes_1",
      "isMultiKey" : false,
      "multiKeyPaths" : {
       "votes" : [ ]
      },
      "isUnique" : false,
      "isSparse" : false,
      "isPartial" : false,
      "indexVersion" : 2,
      "direction" : "forward",
      "indexBounds" : {
       "votes" : [
        "[2.0, 2.0]"
       ]
      },
      "keysExamined" : 3,
      "seeks" : 1,
      "dupsTested" : 0,
      "dupsDropped" : 0
     }
     ]
    },
   },
  }

}

索引嵌套文档

可以在嵌套文档的键上建立索引,方式与正常的键一样。对整个子文档建立索引,只会提高整个子文档的查询速度。

只有在进行与子文档字段顺序完全匹配的子文档查询时,查询优化器才会使用索引。

索引数组

可以对数组建立索引,也可以对数组中某个字段建立索引,就可以高效的搜索数组中的特定元素。例如博客评论 comments 是一个数组。

1
2
> db.blog.ensureIndex({"comments.votes": 1}, {background:true})

对数据建立索引,实际上是对数组的每一个元素建立一个索引条目(本例中一篇 blog 的每一个 comment 是一个索引条目),即多键索引。因此数组索引的代价比单值索引高:对于单次插入、更新或者删除,每一个数组条目可能都需要更新。可能会多个索引条目指向同一个文档,因此MongoDB再返回结果时需要先去重。

多键索引: 对于某个索引的键,如果这个键在某个文档中,而这个文档是一个数组,那么这个索引就会被标记为多键索引。可以从 explain() 的输出中看到一个索引是否为多键索引:如果使用了多键索引,”isMultikey” 字段的值会为 true。 索引只要被标记为多键索引,就无法变成非多键索引了。

在数组上建立的索引并不包括任何位置信息: 无法使用数组索引查找特定位置的数组元素,比如 comments.4

少数情况下,可以对某个特定的数组条目进行索引,

1
2
> db.blog.ensureIndex({"comments.10.votes": 1}, {background:true})

只有在精确匹配第11个数组元素时这个索引才有用

一个索引中的数组元素最多只能有一个。这是为了避免在多值索引中索引条目爆炸性增长:每一对可能的元素都要被索引。