mongodb新手的查询优化的一点心得

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 }]);

需要注意的事项有:

  1. 单字段排序可能不会使用到复合索引(实际测试 以score字段排序会用到复合索引、以createdAt字段排序没有使用到复合索引,需要单独创建createdAt索引)

  2. 复合索引的字段有优先级,[{ 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,
    },
  }
]
  1. 保证复合字段索引顺序与排序字段顺序 相同 或者 相反
php 复制代码
// 复合索引创建命令
db.Post.createIndexes([{ score: -1, createdAt: -1 }]);

// 非内存排序(索引排序)
[
  {
    $sort: {
      score: -1,
      createdAt: -1,
    },
  }
]

// 非内存排序(索引排序)
[
  {
    $sort: {
      score: 1,
      createdAt: 1,
    },
  }
]

// 内存排序(有内存溢出风险)
[
  {
    $sort: {
      score: -1,
      createdAt: 1,
    },
  }
]
  1. 验证排序可以使用索引

最简单的方法 mongodb compass中的explain图形界面右侧小方框

很明显Is not sorted in memory就是使用了排序索引的情况

然而mongodb中并非创建了排序索引就一定会使用索引排序,mongodb有自己的优化方案,然后它会挑出最优的方案去执行

在explain josn描述中可以看到winningPlanrejectedPlans

chatgpt:

在 MongoDB 的执行计划(explain 输出)中,winningPlan 表示 MongoDB 选择执行查询的计划,而 rejectedPlans 表示 MongoDB 在选择最终计划之前评估的其他备选计划。

让我们更详细地了解这两个概念:

winningPlan

  • winningPlan 是 MongoDB 选择执行查询的最终计划。它是 MongoDB 在考虑了所有可能的执行计划后认为最优的计划。
  • 这包含了 MongoDB 选择执行的阶段、索引、排序方式等详细信息。在执行计划中,你可以查看 executionStatsexecutionStages 下的 winningPlan

rejectedPlans

  • rejectedPlans 列出了 MongoDB 在选择最终计划之前评估的其他备选计划。这些是 MongoDB 在考虑执行计划时认为不够优越或不符合条件的计划。
  • rejectedPlans 中,你可以看到每个备选计划的详细信息,包括阶段、索引、排序方式等。通过查看这些备选计划,你可以了解 MongoDB 在选择计划时考虑了哪些其他选项。

通过查看 winningPlanrejectedPlans,你可以更好地理解 MongoDB 是如何执行查询的,以及它是如何选择执行计划的。这对于优化查询性能和索引设计非常有帮助。

当mongodb认为使用内存排序优于索引排序时,它就会使用内存排序,换句话说就是mongodb有自己的主意,但这并不代表排序索引创建不成功!我们需要保证的就是排序有索引方案可以走。

创建完排序索引后,如果显示还是内存排序,可以从rejectedPlans判断是否包含索引排序方案,从而验证排序索引有效。

5.结语

相信在学会开启慢查询日志、结合explain分析、索引的正确添加后新手也可以无畏慢查询。 \

如有误,欢迎指正👏🏻👏🏻👏🏻

相关推荐
一只路过的猫咪4 小时前
thinkphp6使用MongoDB多个数据,聚合查询的坑
数据库·mongodb
你的微笑,乱了夏天2 天前
linux centos 7 安装 mongodb7
数据库·mongodb
来一杯龙舌兰2 天前
【MongoDB】使用 MongoDB 存储日志、审批、MQ等数据的案例及优点
数据库·mongodb
技术路上的苦行僧2 天前
分布式专题(8)之MongoDB存储原理&多文档事务详解
数据库·分布式·mongodb
Tttian6223 天前
Pycharm访问MongoDB数据库
数据库·mongodb·pycharm
Benjamin Cheung3 天前
starter-data-mongodb
数据库·mongodb
2301_801483693 天前
MongoDB(下)
数据库·mongodb
s_fox_3 天前
Mongodb 集群搭建
数据库·mongodb
赶紧写完去睡觉4 天前
数据库管理系统——NoSQL之文档数据库(MongoDB)
数据库·mongodb·nosql
ziyu_jia4 天前
MongoDB、Mongoose使用教程
前端·数据库·mongodb·node.js·mongoose