详解MongoDB索引优化

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解MongoDB索引优化相关的知识,希望对你有一定的参考价值。

一、索引简介

索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。

1.1 概念

索引最常用的比喻就是书籍的目录,查询索引就像查询一本书的目录。本质上目录是将书中一小部分内容信息(比如题目)和内容的位置信息(页码)共同构成,而由于信息量小(只有题目),所以我们可以很快找到我们想要的信息片段,再根据页码找到相应的内容。同样索引也是只保留某个域的一部分信息(建立了索引的field的信息),以及对应的文档的位置信息。

姓名 id 部门 city score
张三 2 开发部 北京 90
李四 1 测试部 上海 70
王五 3 运维部 河北 60

1.2 索引的作用

假如我们想找id为2的document(即张三的记录),如果没有索引,我们就需要扫描整个数据表,然后找出所有id为2的document。当数据表中有大量documents的时候,这个查询时间就会很长(从磁盘上查找数据还涉及大量的IO操作)。

此时建立索引后会有什么变化呢?MongoDB会将id数据拿出来建立索引数据,如下:

索引值 位置
1 第二行
2 第一行
3 第三行

此时,即可根据索引值快速得到原始数据的具体位置,从而获取完整的原始数据。

1.3 索引的工作原理

这样我们就可以通过扫描这个小表找到document对应的位置。

查找过程示意图如下:

![image-20220228122033075]

索引为什么这么快:

  1. 索引数据通过B树来存储,从而使得搜索的时间复杂度为O(logdN)级别的(d是B树的度, 通常d的值比较大,比如大于100),比原先O(N)的复杂度大幅下降。这个差距是惊人的。
  2. 索引本身是在高速缓存当中,相比磁盘IO操作会有大幅的性能提升。(需要注意的是,有的时候数据量非常大的时候,索引数据也会非常大,当大到超出内存容量的时候,会导致部分索引数据存储在磁盘上,这会导致磁盘IO的开销大幅增加,从而影响性能,所以务必要保证有足够的内存能容下所有的索引数据)

当然,事物总有其两面性,在提升查询速度的同时,由于要建立索引,所以写入操作时就需要额外的添加索引的操作,这必然会影响写入的性能,所以当有大量写操作而读操作比较少的时候,且对读操作性能不需要考虑的时候,就不适合建立索引。当然,目前大多数互联网应用都是读操作远大于写操作,因此建立索引很多时候是非常划算和必要的操作。

二、索引的优化

2.1 执行计划

2.1.1 基本用法

db.zips.find("pop":99999).explain()

直接跟在find()函数后面,表示查看find()函数的执行计划,结果如下:


        "queryPlanner" : 
                "plannerVersion" : 1,
                "namespace" : "zips-db.zips",
                "indexFilterSet" : false,
                "parsedQuery" : 
                        "pop" : 
                                "$eq" : 99999
                        
                ,
                "queryHash" : "891A44E4",
                "planCacheKey" : "2D13A19E",
                "winningPlan" : 
                        "stage" : "FETCH",
                        "inputStage" : 
                                "stage" : "IXSCAN",
                                "keyPattern" : 
                                        "pop" : 1
                                ,
                                "indexName" : "pop_1",
                                "isMultiKey" : false,
                                "multiKeyPaths" : 
                                        "pop" : [ ]
                                ,
                                "isUnique" : false,
                                "isSparse" : false,
                                "isPartial" : true,
                                "indexVersion" : 2,
                                "direction" : "forward",
                                "indexBounds" : 
                                        "pop" : [
                                                "[99999.0, 99999.0]"
                                        ]
                                
                        
                ,
                "rejectedPlans" : [ ]
        ,
        "serverInfo" : 
                "host" : "linux30",
                "port" : 27017,
                "version" : "4.4.12",
                "gitVersion" : "51475a8c4d9856eb1461137e7539a0a763cc85dc"
        ,
        "ok" : 1

返回结果包含两大块信息,一个是 queryPlanner,即查询计划,还有一个是 serverInfo,即MongoDB服务的一些信息。

2.1.2 参数解释

参数 含义
plannerVersion 查询计划版本
namespace 要查询的集合
indexFilterSet 是否使用索引
parsedQuery 查询条件,此处为x=1
winningPlan 最佳执行计划
stage 查询方式,常见的有COLLSCAN/全表扫描、IXSCAN/索引扫描、FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询
filter 过滤条件
direction 搜索方向
rejectedPlans 拒绝的执行计划
serverInfo MongoDB服务器信息

2.1.3 添加参数

  • queryPlanner
  • executionStats
db.zips.find("pop":99999).explain("executionStats")

我们发现增加了一个executionStats的字段列的信息


        "queryPlanner" : 
                "plannerVersion" : 1,
                "namespace" : "zips-db.zips",
                "indexFilterSet" : false,
                "parsedQuery" : 
                        "pop" : 
                                "$eq" : 99999
                        
                ,
                "winningPlan" : 
                        "stage" : "FETCH",
                        "inputStage" : 
                                "stage" : "IXSCAN",
                                "keyPattern" : 
                                        "pop" : 1
                                ,
                                "indexName" : "pop_1",
                                "isMultiKey" : false,
                                "multiKeyPaths" : 
                                        "pop" : [ ]
                                ,
                                "isUnique" : false,
                                "isSparse" : false,
                                "isPartial" : true,
                                "indexVersion" : 2,
                                "direction" : "forward",
                                "indexBounds" : 
                                        "pop" : [
                                                "[99999.0, 99999.0]"
                                        ]
                                
                        
                ,
                "rejectedPlans" : [ ]
        ,
        "executionStats" : 
                "executionSuccess" : true,
                "nReturned" : 0,
                "executionTimeMillis" : 1,
                "totalKeysExamined" : 0,
                "totalDocsExamined" : 0,
                "executionStages" : 
                        "stage" : "FETCH",
                        "nReturned" : 0,
                        "executionTimeMillisEstimate" : 0,
                        "works" : 1,
                        "advanced" : 0,
                        "needTime" : 0,
                        "needYield" : 0,
                        "saveState" : 0,
                        "restoreState" : 0,
                        "isEOF" : 1,
                        "docsExamined" : 0,
                        "alreadyHasObj" : 0,
                        "inputStage" : 
                                "stage" : "IXSCAN",
                                "nReturned" : 0,
                                "executionTimeMillisEstimate" : 0,
                                "works" : 1,
                                "advanced" : 0,
                                "needTime" : 0,
                                "needYield" : 0,
                                "saveState" : 0,
                                "restoreState" : 0,
                                "isEOF" : 1,
                                "keyPattern" : 
                                        "pop" : 1
                                ,
                                "indexName" : "pop_1",
                                "isMultiKey" : false,
                                "multiKeyPaths" : 
                                        "pop" : [ ]
                                ,
                                "isUnique" : false,
                                "isSparse" : false,
                                "isPartial" : true,
                                "indexVersion" : 2,
                                "direction" : "forward",
                                "indexBounds" : 
                                        "pop" : [
                                                "[99999.0, 99999.0]"
                                        ]
                                ,
                                "keysExamined" : 0,
                                "seeks" : 1,
                                "dupsTested" : 0,
                                "dupsDropped" : 0
                        
                
        ,
        "serverInfo" : 
                "host" : "linux30",
                "port" : 27017,
                "version" : "4.4.12",
                "gitVersion" : "51475a8c4d9856eb1461137e7539a0a763cc85dc"
        ,
        "ok" : 1

这里除了我们上文介绍到的一些参数之外,还多了executionStats参数,含义如下:

参数 含义
executionSuccess 是否执行成功
nReturned 返回的结果数
executionTimeMillis 执行耗时
totalKeysExamined 索引扫描次数
totalDocsExamined 文档扫描次数
executionStages 这个分类下描述执行的状态
stage 扫描方式,具体可选值与上文的相同
nReturned 查询结果数量
executionTimeMillisEstimate 预估耗时
works 工作单元数,一个查询会分解成小的工作单元
advanced 优先返回的结果数
docsExamined 文档检查数目,与totalDocsExamined一致

allPlansExecution:用来获取所有执行计划,结果参数基本与上文相同。

2.2 慢查询

mysql中,慢查询日志是经常作为我们优化查询的依据,那在MongoDB中是否有类似的功能呢?答案是肯定的,那就是开启Profiling功能。该工具在运行的实例上收集有关MongoDB的写操作,游标,数据库命令等,可以在数据库级别开启该工具,也可以在实例级别开启。该工具会把收集到的所有都写入到system.profile集合中,该集合是一个capped collection

2.2.1 慢查询分析流程

慢查询日志一般作为优化步骤里的第一步。通过慢查询日志,定位每一条语句的查询时间。比如超过了200ms,那么查询超过200ms的语句需要优化。然后它通过 explain() 解析影响行数是不是过大,所以导致查询语句超过200ms。

  1. 用慢查询日志(system.profile)找到超过200ms的语句
  2. 然后再通过explain()解析影响行数,分析为什么超过200ms
  3. 决定是不是需要添加索引

2.2.2 开启慢查询

Profiling级别

0:关闭,不收集任何数据。
1:收集慢查询数据,默认是100毫秒。
2:收集所有数据

数据库设置

use zips-db
db.getProfilingStatus()
db.setProfilingLevel(2)
db.setProfilingLevel(1,200)

注意:

  • 以上操作要是在test集合下面的话,只对该集合里的操作有效,要是需要对整个实例有效,则需要在所有的集合下设置或在开启的时候开启参数。
  • 每次设置之后返回给你的结果是修改之前的状态(包括级别、时间参数)。

全局设置

mongod --profile=1  --slowms=200
profile = 1
slowms = 200

关闭Profiling

db.setProfilingLevel(0)

2.2.3 Profile 效率

Profiling功能肯定是会影响效率的,但是不太严重,原因是其使用的system.profile 来记录,而system.profile 是一个capped collection, 这种collection 在操作上有一些限制和特点,但是效率更高。

2.2.4 慢查询分析

db.system.profile.find()

参数含义:


    "op" : "query",  #操作类型,有insert、query、update、remove、getmore、command   
    "ns" : "onroad.route_model", #操作的集合
    "query" : 
        "$query" : 
            "user_id" : 314436841,
            "data_time" : 
                "$gte" : 1436198400
            
		,
        "$orderby" : 
            "data_time" : 1
        
	,
    "ntoskip" : 0, #指定跳过skip()方法 的文档的数量。
    "nscanned" : 2, #为了执行该操作,MongoDB在 index 中浏览的文档数。 一般来说,如果 nscanned 值高于 nreturned 的值,说明数据库为了找到目标文档扫描了很多文档。这时可以考虑创建索引来提高效率。
    "nscannedObjects" : 1,  #为了执行该操作,MongoDB在 collection中浏览的文档数。
    "keyUpdates" : 0, #索引更新的数量,改变一个索引键带有一个小的性能开销,因为数据库必须删除旧的key,并插入一个新的key到B-树索引
    "numYield" : 1,  #该操作为了使其他操作完成而放弃的次数。通常来说,当他们需要访问还没有完全读入内存中的数据时,操作将放弃。这使得在MongoDB为了放弃操作进行数据读取的同时,还有数据在内存中的其他操作可以完成
    "lockStats" :   #锁信息,R:全局读锁;W:全局写锁;r:特定数据库的读锁;w:特定数据库的写锁
        "timeLockedMicros" :   #该操作获取一个级锁花费的时间。对于请求多个锁的操作,比如对 local 数据库锁来更新 oplog ,该值比该操作的总长要长(即 millis )
            "r" : NumberLong(1089485),
            "w" : NumberLong(0)
        ,
        "timeAcquiringMicros" :   #该操作等待获取一个级锁花费的时间。
            "r" : NumberLong(102),
            "w" : NumberLong(2)
        
    ,
    "nreturned" : 1,  // 返回的文档数量
    "responseLength" : 1669, // 返回字节长度,如果这个数字很大,考虑值返回所需字段
    "millis" : 544, #消耗的时间(毫秒)
    "execStats" :   #一个文档,其中包含执行 查询 的操作,对于其他操作,这个值是一个空文件, system.profile.execStats 显示了就像树一样的统计结构,每个节点提供了在执行阶段的查询操作情况。
        "type" : "LIMIT", ##使用limit限制返回数  
        "works" : 2,
        "yields" : 1,
        "unyields" : 1,
        "invalidates" : 0,
        "advanced" : 1,
        "needTime" : 0,
        "needFetch" : 0,
        "isEOF" : 1,  #是否为文件结束符
        "children" : [
            
                "type" : "FETCH",  #根据索引去检索指定document
                "works" : 1,
                "yields" : 1,
                "unyields" : 1,
                "invalidates" : 0,
                "advanced" : 1,
                "needTime" : 0,
                "needFetch" : 0,
                "isEOF" : 0,
                "alreadyHasObj" : 0,
                "forcedFetches" : 0,
                "matchTested" : 0,
                "children" : [
                    
                        "type" : "IXSCAN", #扫描索引键
                        "works" : 1,
                        "yields" : 1,
                        "unyields" : 1,
                        "invalidates" : 0,
                        "advanced" : 1,
                        "needTime" : 0,
                        "needFetch" : 0,
                        "isEOF" : 0,
                        "keyPattern" : " user_id: 1.0, data_time: -1.0 ",
                        "boundsVerbose" : "field #0[user_id]: [314436841, 314436841], field #1[data_time]: [1436198400, inf.0]",
                        "isMultiKey" : 0,
                        "yieldMovedCursor" : 0,
                        "dupsTested" : 0,
                        "dupsDropped" : 0,
                        "seenInvalidated" : 0,
                        "matchTested" : 0,
                        "keysExamined" : 2,
                        "children" : [ ]
                    
                ]
            
        ]
    ,
    "ts" : ISODate("2015-10-15T07:41:03.061Z"), #该命令在何时执行
    "client" : "10.10.86.171", #链接ip或则主机
    "allUsers" : [
        
            "user" : "martin_v8",
            "db" : "onroad"
        
    ],
    "user" : "martin_v8@onroad"

分析:

  1. 如果nscanned数很大,或者接近记录总数(文档数),那么可能没有用到索引查询,而是全表扫描。
  2. 如果 nscanned 值高于 nreturned 的值,说明数据库为了找到目标文档扫描了很多文档。这时可以考虑创建索引来提高效率。

system.profile补充:

COLLSCAN #全表扫描
IXSCAN #索引扫描
FETCH #根据索引去检索指定document
SHARD_MERGE #将各个分片返回数据进行merge
SORT #表明在内存中进行了排序(与老版本的scanAndOrder:true一致)
LIMIT #使用limit限制返回数
SKIP #使用skip进行跳过
IDHACK #针对_id进行查询
SHARDING_FILTER #通过mongos对分片数据进行查询
COUNT #利用db.coll.explain().count()之类进行count运算
COUNTSCAN #count不使用Index进行count时的stage返回
COUNT_SCAN #count使用了Index进行count时的stage返回
SUBPLA #未使用到索引的$or查询的stage返回
TEXT #使用全文索引进行查询时候的stage返回
PROJECTION #限定返回字段时候stage的返回
Fetch+IDHACK
Fetch+ixscan
Limit+(Fetch+ixscan)
PROJECTION+ixscan
SHARDING_FILTER+ixscan
COLLSCAN(全表扫),SORT(使用sort但是无index),不合理的SKIP,SUBPLA(未用到index的$or)

以上是关于详解MongoDB索引优化的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB中索引的创建和使用详解

MongoDB基础

MongoDB复合索引详解

MongoDB 索引的使用, 管理 和优化

索引优化优化,你又是一个好MongoDB!!!博学谷狂野架构师

MongoDB 创建基础索引组合索引唯一索引以及优化