mongodb 慢查分析和索引优化
前言
MongoDB 作为一款非常流行的 NoSQL 数据库,由于其灵活的数据模型和易用性,被广泛应用于各种业务场景中。然而,随着数据量的增长和查询复杂度的提高,数据库的性能问题逐渐显现出来,其中最常见的就是查询速度的下降,即我们常说的“慢查询”。
慢查询不仅会影响到应用的性能,还可能因此导致整个系统的响应速度下降,严重影响用户体验。因此,对 MongoDB 的慢查询进行分析,并通过优化索引等方法提高查询性能,是每一位使用 MongoDB 的开发者必须掌握的技能。
在接下来的文章中,我将详细介绍 MongoDB 的慢查询分析和索引优化的相关知识。
使用 Profiling 工具进行慢查日志收集
Profiling 工具
Profiling 是 mongodb 的查询分析工具,可以通过其收集 mongo 命令的执行信息。收集的信息将会记录在 system.profile 集合中。
可以通过以下命令来查看是否开启了 Profiling 工具:
1 | db.getProfilingStatus() |
以下是一个返回的示例:
1 | { was: 0, slowms: 100, sampleRate: 1, ok: 1 } |
其中:
was
表示的是 Profiling 的级别:
- 0: 关闭,不收集任何数据,这是默认级别
- 1: 收集慢查询数据,默认是 100 ms
- 2: 收集所有数据
slowms
表示的是慢查询的阈值,单位是 毫秒
sampleRate
表示的是当 was 为 2(收集所有数据) 时记录操作的概率,其取值范围为[0,1],其中 1 表示记录所有操作,0 表示忽略所有操作,其他值为记录操作的概率,比如说 0.5 时会有大约一半的操作会被记录。此选项在你需要进行性能分析,但又不希望记录所有操作(可能因为数量太大或者其他原因)的时候非常有用。这样你可以根据需要调整 sampleRate,在保证性能分析有效性的同时,减少对系统性能的影响。
设置 Profiling 的级别和时间
可以在 mongodb 启动后,通过 mongosh 的形式修改 Profiling 的级别和查询阈值:
1 | // 设置等级为 2 |
也可以在启动 mongod 时就进行设置:
1 | mongod --profile=1 --slowms=200 |
或者在配置文件中添加
1 | profile = 1 |
获取和阅读慢查日志
通过以下命令可以查看当前的慢查询日志,使用 pretty() 方法可以使显示的结果更美观
1 | db.system.profile.find() |
以下通过一个查询的输出,介绍以下各个字段的含义
1 | > db.system.profile.find().pretty(); |
若是发现 millis 值比较大,那么就需要作优化。可以根据其他的值来判断,需要往哪个方向进行优化:
- nscanned 数较大:可能没有利用到索引,甚至使用了全表扫描
- nscanned > nreturned :扫描数大于返回数,考虑利用索引
查询分析
在上文中,我们介绍了如何开启 mongodb 的慢查询记录以及如何查看慢查询操作。但这算是一种被动的方法,只能作为系统上线后的一种优化手段。通常我们需要在开发阶段就预检到可能产生的慢查询,并着手进行优化。此时,就需要进行查询分析了。
explain
在 mongodb 中,可以通过 explain 方法对一条查询进行分析,explain 方法可以接受一个可选的参数,用来指定返回的信息级别。
queryPlanner
:这是默认的参数值。在这个级别,explain() 方法只返回查询计划的信息,包括 winningPlanexecutionStats
:在这个级别,explain() 方法返回查询计划的信息,并且还包括查询执行的统计信息,例如查询执行的时间、扫描的文档数、返回的文档数等allPlansExecution
:在这个级别,explain() 方法返回所有的信息,包括所有考虑过的查询计划的执行统计信息。这个级别的信息最详细,但是也最复杂
一般使用 .explain("executionStats")
返回的信息就足够进行分析了,该命令返回的结果中,需要注意的字段主要有两个,一是 executionStats
,该字段包含了查询计划执行的完整过程;二是 winningPlan
,该字段描述了 MongoDB 查询优化器选择的最佳查询计划
executionStats 中包含了以下字段:
- nReturn:查询条件匹配到的文档数量
- executionTimeMillis:查询计划选择和查询执行的总时间(ms)
- totalKeysExamined:扫描的索引的条目数
- totalDocsExamined:扫描的文档数
- executionStages:树状形式展示获胜计划完整的执行信息
通常情况下,totalKeysExamined = nReturned = totalDocsExamined 时表示已经最大程度地利用了索引,表示该查询已经经过了很好的优化。
也存在 totalDocsExamined = 0 但 totalKeysExamined != 0 的情况,这种情况一般出现在覆盖查询时,表示查询计划可以直接通过索引获取需要的数据,不需要加载文档数据。
winningPlan 通常为一个包含层级 inputStage 的结构,如:
1 | { |
其中 stage 为阶段名称, inputStage 为描述子阶段的文档,它为父级提供文档或者索引键。可以将其看成一个树形结构,每个子节点将其结果传递给父节点,而只有叶子结点访问了索引或者集合。
stage
字段可能的值列表:
值 | 含义 |
---|---|
COLLSCAN | 全表扫描,这个阶段会扫描整个集合来查找匹配的文档。如果查询计划中出现了该阶段,表示查询一般是需要优化的,可以通过加索引或者修改查询计划来实现 |
IXSCAN | 索引扫描,这个阶段会扫描索引来查找匹配的条目。表示查询计划利用了索引进行扫描 |
FETCH | 检出文档,这个阶段会获取 IXSCAN 阶段找到的文档,会从磁盘中加载数据到内存中 |
SHARD_MERGE | 合并分片中的结果 |
SHARDING_FILTER | 分片中过滤文档 |
LIMIT | 使用 limit 限制返回数 |
SORT | 使用 sort 进行排序 |
SKIP | 使用 skip 跳过文档 |
GROUP | 使用 group 进行了分组 |
IDHACK | 针对 _id 进行查询 |
COUNT | 无索引 count 运算 |
COUNT_SCAN | 有索引 count运算 |
TEXT | 全文索引查询 |
PROJECTION | 投影阶段 |
索引优化实践
在查询需要使用多个字段的时候,尽量使用复合索引
复合索引的创建遵循 ESR 原则
E : equal
S : sort
R : range
即:创建复合索引时,通常进行等值筛选的字段放在最前面,进行排序的字段次之,最后是进行范围查找的字段。
在能够使用覆盖查询的场景,尽量使用覆盖查询
覆盖查询指的是查询完全不需要将文档加载到内存中,而是可以直接从索引获取数据。
覆盖查询需要满足一个复杂的条件,即:查询、排序以及投影的文档,都被同一个索引满足。
mongodb 的查询默认都是会返回 _id
字段的,所以在索引中没有该字段的时候,我们需要手动把 _id
排除。
简单举个例子,现在集合中有该索引:id_1_app_1_name_1
执行以下查询:
1 | db.col.find({id:"cc143189-7ab5-46dc-9344-e1acc08b29bd",app:"n1"},{_id:0,id:1,app:1}).sort({name:1}) |
该查询的查询字段、排序字段以及投影字段同时出现在了一个索引中,因此能够覆盖查询。
减小低基数字段索引
对于枚举值较少的字段,尽量不要为其单独增加一个索引,效益不高。但是可以在复合索引中出现。
减少不必要的索引
可以使用以下命令查询指定集合指定索引的访问次数,访问频率很低的索引可以分析一下是否是必要的。减少索引数量可以降低空间占用,提高数据库的写效率。
1 | db.col.aggregate([ |
使用部分索引功能
在 mongo 3.2 之后的版本就可以使用部分索引功能了,在创建索引的时候可以指定一个条件,指定完成后,该索引就只会在满足条件的文档上建立,可以节省索引的空间占用,提高查询效率和写入效率。
以下为一个创建部分索引的例子
1 | db.collection.createIndex( |
值得一提的是,partialFilterExpression
参数不接受 $not
和 $ne
等选项
结语
在本篇博客中介绍了 MongoDB 的慢查询日志收集以及如何进行慢查询分析优化。我们了解了 MongoDB 的 Profiling 工具,了解了如何利用.explain()方法的 winningPlan 和 executionStats 字段来深入理解查询的执行过程和性能。
但需要知道,性能优化是一个持续的过程,而不是一次性的任务。每个数据库和查询都有其独特的特点和需求,因此最佳实践可能会根据具体情况而变化。在优化查询性能时,我们需要不断地监控,收集日志,分析数据,测试新的优化策略,并根据结果进行调整。