更新时间:2018年03月26日 10:17:37   作者:Fundebug    我要评论

对于MongoDB的多键查询,创建复合索引可以有效提高性能。这篇文章主要给大家介绍了关于MongoDB复合索引的相关资料,文中通过示例代码介绍的非常详细,对大家的学习
 

为什么需要索引?

当你抱怨MongoDB集合查询效率低的时候,可能你就需要考虑使用索引了,为了方便后续介绍,先科普下MongoDB里的索引机制(同样适用于其他的数据库比如mysql)。

1
2
3
4
5
6
mongo-9552:PRIMARY> db.person.find()
{ "_id" : ObjectId("571b5da31b0d530a03b3ce82"), "name" : "jack", "age" : 19 }
{ "_id" : ObjectId("571b5dae1b0d530a03b3ce83"), "name" : "rose", "age" : 20 }
{ "_id" : ObjectId("571b5db81b0d530a03b3ce84"), "name" : "jack", "age" : 18 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce85"), "name" : "tony", "age" : 21 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce86"), "name" : "adam", "age" : 18 }

当你往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里读出该文档。比如mmapv1引擎里,位置信息是『文件id + 文件内offset 』, 在wiredtiger存储引擎(一个KV存储引擎)里,位置信息是wiredtiger在存储文档时生成的一个key,通过这个key能访问到对应的文档;为方便介绍,统一用pos(position的缩写)来代表位置信息。

什么是复合索引?

复合索引,即Compound Index,指的是将多个键组合到一起创建索引,这样可以加速匹配多个键的查询。不妨通过一个简单的示例理解复合索引。

students集合如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
db.students.find().pretty()
{
 "_id" : ObjectId("5aa7390ca5be7272a99b042a"),
 "name" : "zhang",
 "age" : "15"
}
{
 "_id" : ObjectId("5aa7393ba5be7272a99b042b"),
 "name" : "wang",
 "age" : "15"
}
{
 "_id" : ObjectId("5aa7393ba5be7272a99b042c"),
 "name" : "zhang",
 "age" : "14"
}

在name和age两个键分别创建了索引(_id自带索引):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
db.students.getIndexes()
[
 {
 "v" : 1,
 "key" : {
 "name" : 1
 },
 "name" : "name_1",
 "ns" : "test.students"
 },
 {
 "v" : 1,
 "key" : {
 "age" : 1
 },
 "name" : "age_1",
 "ns" : "test.students"
 }
]

当进行多键查询时,可以通过explian()分析执行情况(结果仅保留winningPlan):

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
db.students.find({name:"zhang",age:"14"}).explain()
"winningPlan":
{
 "stage": "FETCH",
 "filter":
 {
  "name":
  {
   "$eq": "zhang"
  }
 },
 "inputStage":
 {
  "stage": "IXSCAN",
  "keyPattern":
  {
   "age": 1
  },
  "indexName": "age_1",
  "isMultiKey": false,
  "isUnique": false,
  "isSparse": false,
  "isPartial": false,
  "indexVersion": 1,
  "direction": "forward",
  "indexBounds":
  {
   "age": [
    "[\"14\", \"14\"]"
   ]
  }
 }
}

由winningPlan可知,这个查询依次分为IXSCAN和FETCH两个阶段。IXSCAN即索引扫描,使用的是age索引;FETCH即根据索引去查询文档,查询的时候需要使用name进行过滤。

为name和age创建复合索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
db.students.createIndex({name:1,age:1})
db.students.getIndexes()
[
 {
 "v" : 1,
 "key" : {
 "name" : 1,
 "age" : 1
 },
 "name" : "name_1_age_1",
 "ns" : "test.students"
 }
]

有了复合索引之后,同一个查询的执行方式就不同了:

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
db.students.find({name:"zhang",age:"14"}).explain()
"winningPlan":
{
 "stage": "FETCH",
 "inputStage":
 {
  "stage": "IXSCAN",
  "keyPattern":
  {
   "name": 1,
   "age": 1
  },
  "indexName": "name_1_age_1",
  "isMultiKey": false,
  "isUnique": false,
  "isSparse": false,
  "isPartial": false,
  "indexVersion": 1,
  "direction": "forward",
  "indexBounds":
  {
   "name": [
    "[\"zhang\", \"zhang\"]"
   ],
   "age": [
    "[\"14\", \"14\"]"
   ]
  }
 }
}

由winningPlan可知,这个查询的顺序没有变化,依次分为IXSCAN和FETCH两个阶段。但是,IXSCAN使用的是name与age的复合索引;FETCH即根据索引去查询文档,不需要过滤。

这个示例的数据量太小,并不能看出什么问题。但是实际上,当数据量很大,IXSCAN返回的索引比较多时,FETCH时进行过滤将非常耗时。接下来将介绍一个真实的案例。

定位MongoDB性能问题

随着接收的错误数据不断增加,我们Fundebug已经累计处理3.5亿错误事件,这给我们的服务不断带来性能方面的挑战,尤其对于MongoDB集群来说。

对于生产数据库,配置profile,可以记录MongoDB的性能数据。执行以下命令,则所有超过1s的数据库读写操作都会被记录下来。

1
db.setProfilingLevel(1,1000)

查询profile所记录的数据,会发现events集合的某个查询非常慢:

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
db.system.profile.find().pretty()
{
 "op" : "command",
 "ns" : "fundebug.events",
 "command" : {
 "count" : "events",
 "query" : {
 "createAt" : {
 "$lt" : ISODate("2018-02-05T20:30:00.073Z")
 },
 "projectId" : ObjectId("58211791ea2640000c7a3fe6")
 }
 },
 "keyUpdates" : 0,
 "writeConflicts" : 0,
 "numYield" : 1414,
 "locks" : {
 "Global" : {
 "acquireCount" : {
 "r" : NumberLong(2830)
 }
 },
 "Database" : {
 "acquireCount" : {
 "r" : NumberLong(1415)
 }
 },
 "Collection" : {
 "acquireCount" : {
 "r" : NumberLong(1415)
 }
 }
 },
 "responseLength" : 62,
 "protocol" : "op_query",
 "millis" : 28521,
 "execStats" : {
 },
 "ts" : ISODate("2018-03-07T20:30:59.440Z"),
 "client" : "192.168.59.226",
 "allUsers" : [ ],
 "user" : ""
}

events集合中有数亿个文档,因此count操作比较慢也不算太意外。根据profile数据,这个查询耗时28.5s,时间长得有点离谱。另外,numYield高达1414,这应该就是操作如此之慢的直接原因。根据MongoDB文档,numYield的含义是这样的:

The number of times the operation yielded to allow other operations to complete. Typically, operations yield when they need access to data that MongoDB has not yet fully read into memory. This allows other operations that have data in memory to complete while MongoDB reads in data for the yielding operation.

这就意味着大量时间消耗在读取硬盘上,且读了非常多次。可以推测,应该是索引的问题导致的。

不妨使用explian()来分析一下这个查询(仅保留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
db.events.explain("executionStats").count({"projectId" : ObjectId("58211791ea2640000c7a3fe6"),createAt:{"$lt" : ISODate("2018-02-05T20:30:00.073Z")}})
"executionStats":
{
 "executionSuccess": true,
 "nReturned": 20853,
 "executionTimeMillis": 28055,
 "totalKeysExamined": 28338,
 "totalDocsExamined": 28338,
 "executionStages":
 {
  "stage": "FETCH",
  "filter":
  {
   "createAt":
   {
    "$lt": ISODate("2018-02-05T20:30:00.073Z")
   }
  },
  "nReturned": 20853,
  "executionTimeMillisEstimate": 27815,
  "works": 28339,
  "advanced": 20853,
  "needTime": 7485,
  "needYield": 0,
  "saveState": 1387,
  "restoreState": 1387,
  "isEOF": 1,
  "invalidates": 0,
  "docsExamined": 28338,
  "alreadyHasObj": 0,
  "inputStage":
  {
   "stage": "IXSCAN",
   "nReturned": 28338,
   "executionTimeMillisEstimate": 30,
   "works": 28339,
   "advanced": 28338,
   "needTime": 0,
   "needYield": 0,
   "saveState": 1387,
   "restoreState": 1387,
   "isEOF": 1,
   "invalidates": 0,
   "keyPattern":
   {
    "projectId": 1
   },
   "indexName": "projectId_1",
   "isMultiKey": false,
   "isUnique": false,
   "isSparse": false,
   "isPartial": false,
   "indexVersion": 1,
   "direction": "forward",
   "indexBounds":
   {
    "projectId": [
     "[ObjectId('58211791ea2640000c7a3fe6'), ObjectId('58211791ea2640000c7a3fe6')]"
    ]
   },
   "keysExamined": 28338,
   "dupsTested": 0,
   "dupsDropped": 0,
   "seenInvalidated": 0
  }
 }
}

可知,events集合并没有为projectId与createAt建立复合索引,因此IXSCAN阶段采用的是projectId索引,其nReturned为28338; FETCH阶段需要根据createAt进行过滤,其nReturned为20853,过滤掉了7485个文档;另外,IXSCAN与FETCH阶段的executionTimeMillisEstimate分别为30ms和27815ms,因此基本上所有时间都消耗在了FETCH阶段,这应该是读取硬盘导致的。

创建复合索引

没有为projectId和createAt创建复合索引是个尴尬的错误,赶紧补救一下:

1
db.events.createIndex({projectId:1,createTime:-1},{background: true})

在生产环境构建索引这种事最好是晚上做,这个命令一共花了大概7个小时吧!background设为true,指的是不要阻塞数据库的其他操作,保证数据库的可用性。但是,这个命令会一直占用着终端,这时不能使用CTRL + C,否则会终止索引构建过程。

复合索引创建成果之后,前文的查询就快了很多(仅保留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
db.javascriptevents.explain("executionStats").count({"projectId" : ObjectId("58211791ea2640000c7a3fe6"),createAt:{"$lt" : ISODate("2018-02-05T20:30:00.073Z")}})
"executionStats":
{
 "executionSuccess": true,
 "nReturned": 0,
 "executionTimeMillis": 47,
 "totalKeysExamined": 20854,
 "totalDocsExamined": 0,
 "executionStages":
 {
  "stage": "COUNT",
  "nReturned": 0,
  "executionTimeMillisEstimate": 50,
  "works": 20854,
  "advanced": 0,
  "needTime": 20853,
  "needYield": 0,
  "saveState": 162,
  "restoreState": 162,
  "isEOF": 1,
  "invalidates": 0,
  "nCounted": 20853,
  "nSkipped": 0,
  "inputStage":
  {
   "stage": "COUNT_SCAN",
   "nReturned": 2053,
   "executionTimeMillisEstimate": 50,
   "works": 20854,
   "advanced": 20853,
   "needTime": 0,
   "needYield": 0,
   "saveState": 162,
   "restoreState": 162,
   "isEOF": 1,
   "invalidates": 0,
   "keysExamined": 20854,
   "keyPattern":
   {
    "projectId": 1,
    "createAt": -1
   },
   "indexName": "projectId_1_createTime_-1",
   "isMultiKey": false,
   "isUnique": false,
   "isSparse": false,
   "isPartial": false,
   "indexVersion": 1
  }
 }
}

可知,count操作使用了projectId和createAt的复合索引,因此非常快,只花了46ms,性能提升了将近600倍!!!对比使用复合索引前后的结果,发现totalDocsExamined从28338降到了0,表示使用复合索引之后不再需要去查询文档,只需要扫描索引就好了,这样就不需要去访问磁盘了,自然快了很多。

深入理解MongoDB的复合索引的更多相关文章

  1. 关于mongodb的复合索引新功能

    最新在做一个项目,由于查询字段较多,且查询较频繁,所以我做了一个复合索引,将所有需要查询的字段都做到索引里,做了一个名为s_1_m_1_c_1_v_1_year_1_month_1_week_1_da ...

  2. 「生产事故」MongoDB复合索引引发的灾难

    前情提要 11月末我司商品服务的MongoDB主库曾出现过严重抖动.频繁锁库等情况. 由于诸多业务存在插入MongoDB.然后立即查询等逻辑,因此项目并未开启读写分离. 最终定位问题是由于:服务器自身 ...

  3. MongoDB复合索引详解

    摘要: 对于MongoDB的多键查询,创建复合索引可以有效提高性能. 什么是复合索引? 复合索引,即Compound Index,指的是将多个键组合到一起创建索引,这样可以加速匹配多个键的查询.不妨通 ...

  4. MongoDB的复合唯一索引

    一 创建 JavaScript Shell db.room.ensureIndex({'floor':1,'num':1}) Spring Data @Data // lombok @Document ...

  5. mongodb索引 复合索引

    当我们的查询条件不只有一个时,就需要建立复合索引,比如插入一条{x:1,y:2,z:3}记录,按照我们之前建立的x为1的索引,可是使用x查询,现在想按照x与y的值查询,就需要创建如下的索引     创 ...

  6. MongoDB的学习--索引类型和属性

    索引类型 MongDB的索引分为以下几种类型:单键索引.复合索引.多键索引.地理空间索引.全文本索引和哈希索引 单键索引(Single Field Indexes) 在一个键上创建的索引就是单键索引, ...

  7. MongoDB学习笔记——索引管理

    索引 索引能够提升查询的效率.没有索引,MongoDB必须扫描集合中的所有文档,才能找到匹配查询语句的文档. 索引是一种特殊的数据结构,将一小块数据集保存为容易遍历的形式.索引能够存储某种特殊字段或字 ...

  8. MongoDB的学习--索引类型和属性(转)

    原文链接:MongoDB的学习--索引类型和属性 索引类型 MongDB的索引分为以下几种类型:单键索引.复合索引.多键索引.地理空间索引.全文本索引和哈希索引 单键索引(Single Field I ...

  9. 4.MongoDB系列之索引(一)

    1. 执行计划查看 db.getCollection('users').find({'username': 'shenjian'}).explain('executionStats') 结果查看,先大 ...

随机推荐

  1. 手机web——自适应网页设计(html/css控制)(转)

    一. 允许网页宽度自动调整: "自适应网页设计"到底是怎么做到的?其实并不难. 首先,在网页代码的头部,加入一行viewport元标签. <meta name="v ...

  2. 在js或css后加?v= 版本号不让浏览器缓存

    客户端会缓存css或js文件,改变版本号,客户端浏览器就会重新下载新的js或css文件,在js或css后加?v= 版本号的用法如下 代码如下: <span style="font-si ...

  3. 自动化测试尝试 动态Linq表达式生成 ftp上传

    自动化测试尝试   1. Selenium IDE Selenium IDE is a Chrome and Firefox plugin which records and plays back u ...

  4. Spring boot设置启动监听端口

    一.通过配置 修改application.properties 在属性文件中添加server.port=8000 二.直接看代码: @Controller @EnableAutoConfigurati ...

  5. SQL 语句递归查询 With AS 查找所有子节点

    create table #EnterPrise (   Department nvarchar(50),--部门名称   ParentDept nvarchar(50),--上级部门   Depar ...

  6. Java总结篇系列:Java多线程(四)

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

  7. Windows Azure Mobiles Services实现client的登录注冊

    下文仅仅是简单实现,client以Android端的实现为例: 用户表Account: package com.microsoft.ecodrive.model; public class Accou ...

  8. vim-snipmate的c.snippets(2016.7.10)

    ## Main # main snippet main int main ( void ) { ${} ; } ##include snippet inc #include <${:stdio} ...

  9. 改变mysql数据库用户的权限

    mysql> grant all on *.* to test@'%';Query OK, 0 rows affected (0.00 sec) mysql> flush privileg ...

  10. C# RSA数据加密

    第一步产生密钥类 CreateKey using System; using System.Collections.Generic; using System.Linq; using System.T ...