1.开启慢查询日志
mongosh
// 切换到指定库
use Posts;
// 启用慢查询日志,设置阈值为 200 毫秒(默认为100毫秒)
db.setProfilingLevel(1, { slowms: 200 });
// 查看慢查询日志的状态
db.getProfilingStatus();
// 停用慢查询日志
db.setProfilingLevel(0);
如果slowms大于0表示启用了慢查询日志
json
{
was: 1,
slowms: 200, // 表示已经慢查询日志开启
sampleRate: 1,
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1701763392, i: 1 }),
signature: {
hash: Binary.createFromBase64("FF/GlEe8RSqON8tNKft7oJOe/GM=", 0),
keyId: Long("7308254533409308677")
}
},
operationTime: Timestamp({ t: 1701763392, i: 1 })
}
开启日志后就可以查看慢查询记录
mongosh
// 获取执行时间大于 200 毫秒的慢查询信息
db.system.profile.find({ millis: { $gt: 200 } }).pretty();
执行后会得到以下的文案,可以找到具体的慢查询方法针对优化
json
{
op: 'command',
ns: 'DB.Posts',
command: {
// 具体查询命令
},
// ...
millis: 114, // 具体用时
// ...
}
请注意,启用慢查询日志可能会对性能产生一些影响,因为额外的信息需要被记录。因此,在生产环境中,建议仅在需要时启用慢查询日志,并根据实际情况设置适当的阈值。
2.优化聚合查询管道
官网中有多个基础案例aggregation-pipeline-optimization
在实际开发中我遇到到的案例
json
[
{ $match: {} },
{ $sort: {} },
// 修改后
{ $skip: 0 },
{ $limit: 10 },
{ $lookup: {} },
{ $unwind: { } },
{ $group: {}, },
// 修改前
// { $skip: 0 },
// { $limit: 10 },
{ $project: {} },
]
在这么一个经典的分页查询业务中 修改前我将$skip
与$limit
放在$lookup
后 导致匹配的数据全量的进行了联表查询,修改后只是将limit的10条数据进行联表查询,仅是少量数据的情况下整体速度上升了不止10倍。
3.结合explain进行分析
个人喜欢先使用mongodb compass中的explain图形界面分析基础信息
也可以使用mongosh输出json格式的explain详细描述
mongosh
db.Post.aggregate([]).explain();
同样以上述分页聚合查询为例
从下往上看 IXSCAN表示$match
阶段使用了Post_show_type_tag_title_idx
索引 一共查询到了6829条文档,再进入$sort
与$project
阶段 侧边栏可以发现总耗时524毫秒,使用了内存排序,其中$lookup
、$unwind
、$group
没有显式的展示出来 但是从查询耗时就可以分析出这段查询有优化的空间
3.1 优化前的explain描述
这段 MongoDB Explain 输出提供了一些关于查询执行的关键信息。下面是chatgpt对这些信息的解释:
chatgpt:
totalDocsExamined
: 表示在执行查询时检查的文档数量。在这里,18836 表示查询涉及检查了 18836 个文档。totalKeysExamined
: 表示在执行查询时检查的索引键数量。在这里,36926 表示查询涉及检查了 36926 个索引键。collectionScans
: 表示是否执行了集合扫描。在这里,值为 0 表示没有进行集合扫描。indexesUsed
: 表示在查询执行中使用的索引。在这里,["_id_"]
表示使用了 "id" 索引。nReturned
: 表示查询返回的文档数量。在这里,18836 表示查询成功返回了 18836 个文档。executionTimeMillisEstimate
: 表示查询执行的时间估算(以毫秒为单位)。在这里,433 表示查询估算执行时间为 433 毫秒。 从这些信息中可以看出,查询使用了 "id" 索引,没有执行集合扫描,但涉及检查了较多的文档和索引键。查询的估算执行时间相对较长,这可能表明查询涉及了较大的数据集或者涉及了较复杂的操作。你可能需要进一步优化查询或考虑索引的使用,以提高查询性能。
3.2 优化后的explain描述
chatgpt:
totalDocsExamined
: 表示在执行查询时检查的文档数量。在这里,28 表示查询涉及检查了 28 个文档。totalKeysExamined
: 表示在执行查询时检查的索引键数量。在这里,52 表示查询涉及检查了 52 个索引键。collectionScans
: 表示是否执行了集合扫描。在这里,值为 0 表示没有进行集合扫描,这是一个好的迹象。indexesUsed
: 表示在查询执行中使用的索引。在这里,["_id_"]
表示使用了 "id" 索引。nReturned
: 表示查询返回的文档数量。在这里,28 表示查询成功返回了 28 个文档。executionTimeMillisEstimate
: 表示查询执行的时间估算(以毫秒为单位)。在这里,1 表示查询估算执行时间为 1 毫秒。
3.3 总结
由于前置了$skip
与$limit
阶段,可以明显得从explain中看到$cursor
中文档数量nReturned
从6829下降到10,在$lookup
阶段的查询时间预估executionTimeMillisEstimate
从443ms下降到了1ms,并且使用了_id作为索引,从数据的角度可以证明我完成了一次有效的优化。
4. 内存排序问题
内存排序溢出几乎每个新手都会遇到的问题
vbnet
MongoDB\Driver\Exception\CommandException: Sort exceeded memory limit of 104857600 bytes, but did not opt in to external sorting. Aborting operation. Pass allowDiskUse:true to opt in.
mongodb在4.4后默认内存排序使用限制为100mb,在之前的版本更小可能为32mb,当然可以提升配置或者使用allowDiskUse
开启硬盘存储但肯定都不是最好的方法,因为数据量上来始终还要面对这个问题,最佳方法是设置排序索引。
现有以下业务根据score与createdAt进行降序排序
bash
[
{
$match: {
scope: {
$in: ['xxx', 'xxx'],
},
},
},
{
$sort: {
score: -1,
createdAt: -1,
},
}
]
那么应该创建复合索引
mongosh
// -1为降序Desc 1为升序Asc
db.Post.createIndexes([{ score: -1, createdAt: -1 }]);
需要注意的事项有:
-
单字段排序可能不会使用到复合索引(实际测试 以score字段排序会用到复合索引、以createdAt字段排序没有使用到复合索引,需要单独创建createdAt索引)
-
复合索引的字段有优先级,
[{ score: -1, createdAt: -1 }]
创建数组中score排序在createdAt前面,那么$sort
阶段如果写成以下方式也不会使用复合索引,所以在创建复合索引时应当考虑字段排序的优先级。
php
// 复合索引创建命令
db.Post.createIndexes([{ score: -1, createdAt: -1 }]);
// 非内存排序(索引排序)
[
{
$sort: {
score: -1,
createdAt: -1,
},
}
]
// 内存排序(有内存溢出风险)
[
{
$sort: {
createdAt: -1, // createAt在score前面
score: -1,
},
}
]
- 保证复合字段索引顺序与排序字段顺序 都 相同 或者 都相反
php
// 复合索引创建命令
db.Post.createIndexes([{ score: -1, createdAt: -1 }]);
// 非内存排序(索引排序)
[
{
$sort: {
score: -1,
createdAt: -1,
},
}
]
// 非内存排序(索引排序)
[
{
$sort: {
score: 1,
createdAt: 1,
},
}
]
// 内存排序(有内存溢出风险)
[
{
$sort: {
score: -1,
createdAt: 1,
},
}
]
- 验证排序可以使用索引
最简单的方法 mongodb compass中的explain图形界面右侧小方框
很明显Is not sorted in memory就是使用了排序索引的情况
然而mongodb中并非创建了排序索引就一定会使用索引排序,mongodb有自己的优化方案,然后它会挑出最优的方案去执行
在explain josn描述中可以看到winningPlan
与rejectedPlans
chatgpt:
在 MongoDB 的执行计划(
explain
输出)中,winningPlan
表示 MongoDB 选择执行查询的计划,而rejectedPlans
表示 MongoDB 在选择最终计划之前评估的其他备选计划。让我们更详细地了解这两个概念:
winningPlan
winningPlan
是 MongoDB 选择执行查询的最终计划。它是 MongoDB 在考虑了所有可能的执行计划后认为最优的计划。- 这包含了 MongoDB 选择执行的阶段、索引、排序方式等详细信息。在执行计划中,你可以查看
executionStats
或executionStages
下的winningPlan
。
rejectedPlans
rejectedPlans
列出了 MongoDB 在选择最终计划之前评估的其他备选计划。这些是 MongoDB 在考虑执行计划时认为不够优越或不符合条件的计划。- 在
rejectedPlans
中,你可以看到每个备选计划的详细信息,包括阶段、索引、排序方式等。通过查看这些备选计划,你可以了解 MongoDB 在选择计划时考虑了哪些其他选项。通过查看
winningPlan
和rejectedPlans
,你可以更好地理解 MongoDB 是如何执行查询的,以及它是如何选择执行计划的。这对于优化查询性能和索引设计非常有帮助。
当mongodb认为使用内存排序优于索引排序时,它就会使用内存排序,换句话说就是mongodb有自己的主意,但这并不代表排序索引创建不成功!我们需要保证的就是排序有索引方案可以走。
创建完排序索引后,如果显示还是内存排序,可以从rejectedPlans判断是否包含索引排序方案,从而验证排序索引有效。
5.结语
相信在学会开启慢查询日志、结合explain分析、索引的正确添加后新手也可以无畏慢查询。 \
如有误,欢迎指正👏🏻👏🏻👏🏻