摘要: 对于MongoDB的多键查询,创建复合索引可以有效提高性能。

什么是复合索引?

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

students集合如下:

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自带索引):

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):

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可知,这个查询依次分为IXSCANFETCH两个阶段。IXSCAN即索引扫描,使用的是age索引;FETCH即根据索引去查询文档,查询的时候需要使用name进行过滤。

为name和age创建复合索引:

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"
}
]

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

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可知,这个查询的顺序没有变化,依次分为IXSCANFETCH两个阶段。但是,IXSCAN使用的是name与age的复合索引;FETCH即根据索引去查询文档,不需要过滤。

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

定位MongoDB性能问题

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

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

db.setProfilingLevel(1,1000)

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

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):

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分别为30ms27815ms,因此基本上所有时间都消耗在了FETCH阶段,这应该是读取硬盘导致的。

创建复合索引

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

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

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

复合索引创建成果之后,前文的查询就快了很多(仅保留executionStats):

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": 20853,
"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,表示使用复合索引之后不再需要去查询文档,只需要扫描索引就好了,这样就不需要去访问磁盘了,自然快了很多。

参考

版权声明:
转载时请注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2018/03/15/mongdb_compound_index_detail/

MongoDB复合索引详解的更多相关文章

  1. 快速掌握mongoDB(三)——mongoDB的索引详解

    1 mongoDB索引的管理 本节介绍mongoDB中的索引,熟悉mysql/sqlserver等关系型数据库的小伙伴应该都知道索引对优化数据查询的重要性.我们先简单了解一下索引:索引的本质就是一个排 ...

  2. MySQL 联合索引详解

    MySQL 联合索引详解   联合索引又叫复合索引.对于复合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分.例如索引是key index (a,b,c ...

  3. Oracle索引详解

    Oracle索引详解(二) --索引分类   Oracle 提供了大量索引选项.知道在给定条件下使用哪个选项对于一个程序的性能来说非常重要.一个错误的选择可能会引发死锁,并导致数据库性能急剧下降或进程 ...

  4. 【详细解析】MySQL索引详解( 索引概念、6大索引类型、key 和 index 的区别、其他索引方式)

    [详细解析]MySQL索引详解( 索引概念.6大索引类型.key 和 index 的区别.其他索引方式) MySQL索引的概念: 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分 ...

  5. 使用VS2010编译MongoDB C++驱动详解

    最近为了解决IM消息记录的高速度写入.多文档类型支持的需求,决定使用MongoDB来解决. 考虑到MongoDB对VS版本要求较高,与我现有的VS版本不兼容,在leveldb.ssdb.redis.h ...

  6. elasticsearch系列三:索引详解(分词器、文档管理、路由详解(集群))

    一.分词器 1. 认识分词器  1.1 Analyzer   分析器 在ES中一个Analyzer 由下面三种组件组合而成: character filter :字符过滤器,对文本进行字符过滤处理,如 ...

  7. elasticsearch系列二:索引详解(快速入门、索引管理、映射详解、索引别名)

    一.快速入门 1. 查看集群的健康状况 http://localhost:9200/_cat http://localhost:9200/_cat/health?v 说明:v是用来要求在结果中返回表头 ...

  8. MySQL 索引详解大全

    什么是索引? 1.索引 索引是表的目录,在查找内容之前可以先在目录中查找索引位置,以此快速定位查询数据.对于索引,会保存在额外的文件中. 2. 索引,是数据库中专门用于帮助用户快速查询数据的一种数据结 ...

  9. 最全面的 MySQL 索引详解

    什么是索引? 1.索引 索引是表的目录,在查找内容之前可以先在目录中查找索引位置,以此快速定位查询数据.对于索引,会保存在额外的文件中. 2.索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构 ...

随机推荐

  1. 【CF429E】 Points and Segments(欧拉回路)

    传送门 CodeForces 洛谷 Solution 考虑欧拉回路有一个性质. 如果把点抽出来搞成一条直线,路径看成区间覆盖,那么一个点从左往右被覆盖的次数等于从右往左被覆盖的次数. 发现这个性质和本 ...

  2. SpringDataJPA

    看着自己弟弟在成都聚全家之力盘一套房, 看着自己二哥,在成都也为车贷房贷奔波劳累,身心俱惫, 生活不易啊,这个社会环境下,就像从数据库拿数据一样,只拿我们想要的,或许会活的滋润很多吧. 最近的这个项目 ...

  3. Android NDK学习(七):NDK 编译支持 C++特有的库

    如果你的C++代码中出现了很多C++特有的库,例如<iostream>,<list>等,那么你还需要在jni的文件夹下添加一个Application.mk文件,文件内容为: A ...

  4. Javascript高级编程学习笔记(52)—— DOM2和DOM3(4)元素大小

    在日常实践中,我们在使用JS的时候难免会需要获取元素的大小及位置 首先要声明的是,这一部分的内容并不属于DOM2样式规范,因为DOM中并没有对我们如何获取元素大小的相关信息做出规范 偏移量 偏移量及元 ...

  5. IntelliJ IDEA 常用快捷键使用说明

    Ctrl + / 可以实现单行注释的快速添加和取消.xml和html注释也能操作. Ctrl + Y 删除选中的代码,或者光标所在行,同时删除代码所占的空间. Ctrl + Alt + V 快速抽取变 ...

  6. vue实现一个简易Popover组件

    概述 之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差. 当时想到的解决方法是:给根实例 ...

  7. Vue的基本使用

    VUE vue挂载点 el:"标签id" vue绑定属性 :v-model:"属性值" vue绑定事件 @click:"事件名" vue基本 ...

  8. 如何写gdb命令脚本

    作为UNIX/Linux下使用广泛的调试器,gdb不仅提供了丰富的命令,还引入了对脚本的支持:一种是对已存在的脚本语言支持,比如python,用户可以直接书写python脚本,由gdb调用python ...

  9. Vue 加载外部js文件

    Vue.js  加载外部js文件 在项目内新建一个config.js //变量的定义 export var config = { baseurl:'http://172.16.114.5:8088/M ...

  10. [linux]解析crontab

    linux提供了一个非常强大而且又比较好用的命令 [crontab] crontab是Unix和Linux用于设置周期性被执行的指令,是互联网很常用的技术,很多任务都会设置在crontab循环执行, ...