资讯专栏INFORMATION COLUMN

MongoDB复合索引详解

qieangel2013 / 2553人阅读

摘要:摘要对于的多键查询,创建复合索引可以有效提高性能。不妨通过一个简单的示例理解复合索引。但是,使用的是与的复合索引即根据索引去查询文档,不需要过滤。可以推测,应该是索引的问题导致的。

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

参考

MongoDB 复合索引

MongoDB文档:Compound Indexes

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19222.html

相关文章

  • MongoDB索引与优化详解

    摘要:全文索引在中每个集合只允许创建一个索引,因此不用担心存在多个索引造成冲突的问题。全文索引创建全文索引创建方法与创建单键索引复合索引类似。 在MongoDB中通过建立索引可以进行高效的查询,如果没有索引MongoDB将会扫描整个集合与查询的条件进行匹配,这对于性能会造成很大的消耗。技术博客: Node.js技术栈 快速导航 Mongodb索引类型 索引属性 索引实例测试 索引(Ind...

    oujie 评论0 收藏0
  • MongoDB指南---11、使用复合索引、$操作符如何使用索引索引对象和数组、索引基数

    摘要:操作符如何使用索引有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。有时能够使用索引,但是通常它并不知道要如何使用索引。索引对象和数组允许深入文档内部,对嵌套字段和数组建立索引。 上一篇文章:MongoDB指南---10、索引、复合索引 简介下一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引 1、使用复合索引 在多...

    saucxs 评论0 收藏0
  • MongoDB指南---11、使用复合索引、$操作符如何使用索引索引对象和数组、索引基数

    摘要:操作符如何使用索引有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。有时能够使用索引,但是通常它并不知道要如何使用索引。索引对象和数组允许深入文档内部,对嵌套字段和数组建立索引。 上一篇文章:MongoDB指南---10、索引、复合索引 简介下一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引 1、使用复合索引 在多...

    tomlingtm 评论0 收藏0
  • 练习 MongoDB 操作 —— 索引篇(二)

    摘要:所以,如果你很少对集合进行读取操作,建议不使用索引内存使用由于索引是存储在内存中你应该确保该索引的大小不超过内存的限制。如果索引的大小大于内存的限制,会删除一些索引,这将导致性能下降。 本文围绕索引、游标两部分进行探索,对MongoDB数据库的索引部分有一个大概的了解; 索引 索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符...

    luqiuwen 评论0 收藏0
  • mongodb索引

    摘要:但是需要手动创建创建索引,索引可以重复创建,若创建已经存在的索引,则会直接返回成功。单键索引值为一个单一的值,如字符串,数字或日期。多键索引值具有多个记录,如数组。过期索引不能是复合索引。 索引的概念 索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,通过索引可以快速找到我们查询的数据。提高查询效率 mongodb索引种类 _id索引 单键索引 多键索引 复合...

    FWHeart 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<