乐趣区

关于java:详解MongoDB索引优化

一、索引简介

索引通常可能极大的进步查问的效率,如果没有索引,MongoDB 在读取数据时必须扫描汇合中的每个文件并选取那些合乎查问条件的记录。

1.1 概念

索引最罕用的比喻就是书籍的目录,查问索引就像查问一本书的目录。实质上目录是将书中一小部分内容信息(比方题目)和内容的地位信息(页码)独特形成,而因为信息量小(只有题目),所以咱们能够很快找到咱们想要的信息片段,再依据页码找到相应的内容。同样索引也是只保留某个域的一部分信息(建设了索引的 field 的信息),以及对应的文档的地位信息。

假如咱们有如下文档(每行的数据在 MongoDB 中是存在于一个 Document 当中)

姓名 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 对应的地位。

查找过程示意图如下:

索引为什么这么快:

为什么这样速度会快呢?这次要有几方面的因素

  1. 索引数据通过 B 树来存储,从而使得搜寻的工夫复杂度为 O(logdN)级别的 (d 是 B 树的度, 通常 d 的值比拟大,比方大于 100),比原先 O(N) 的复杂度大幅降落。这个差距是惊人的。
  2. 索引自身是在高速缓存当中,相比磁盘 IO 操作会有大幅的性能晋升。(须要留神的是,有的时候数据量十分大的时候,索引数据也会十分大,当大到超出内存容量的时候,会导致局部索引数据存储在磁盘上,这会导致磁盘 IO 的开销大幅减少,从而影响性能,所以务必要保障有足够的内存能容下所有的索引数据)

    当然,事物总有其两面性,在晋升查问速度的同时,因为要建设索引,所以写入操作时就须要额定的增加索引的操作,这必然会影响写入的性能,所以当有大量写操作而读操作比拟少的时候,且对读操作性能不须要思考的时候,就不适宜建设索引。当然,目前大多数互联网利用都是读操作远大于写操作,因而建设索引很多时候是十分划算和必要的操作。

二、索引的优化

2.1 执行打算

MongoDB 中的 explain() 函数能够帮忙咱们查看查问相干的信息,这有助于咱们疾速查找到搜寻瓶颈进而解决它,咱们接下来就看看 explain() 的一些用法及其查问后果的含意。

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 增加参数

explain() 也接管不同的参数,通过设置不同参数咱们能够查看更具体的查问打算。

  • queryPlanner

是默认参数,增加 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)

如果不须要收集所有慢日志,只须要收集小于 100ms 的慢日志能够应用如下命令

db.setProfilingLevel(1,200)

留神:

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

全局设置

在 mongoDB 启动的时候退出如下参数

mongod --profile=1  --slowms=200

或在配置文件里增加 2 行:

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() 查看以后所有的慢查问日志

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" : "[email protected]"
}

剖析:

如果发现 millis 值比拟大,那么就须要作优化。

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

system.profile 补充:

‘type’的返回参数阐明

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

不心愿看到蕴含如下的 type

COLLSCAN(全表扫),SORT(应用 sort 然而无 index),不合理的 SKIP,SUBPLA(未用到 index 的 $or)

如果本文对您有帮忙,欢送 关注 点赞`,您的反对是我保持创作的能源。

转载请注明出处!

退出移动版