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 小时前
MongoDB 4.2——查询
数据库·mongodb
吴声子夜歌11 小时前
MongoDB 4.2——索引(二)
数据库·mongodb·索引
吴声子夜歌1 天前
MongoDB 4.x——并发优化
数据库·mongodb
吴声子夜歌1 天前
MongoDB 4.2——入门指南
数据库·mongodb
吴声子夜歌1 天前
MongoDB 4.x——高可靠
数据库·mongodb
码农学院1 天前
Node.js MongoDB构建户外用品GEO优化内容管理平台
数据库·mongodb·node.js
吴声子夜歌1 天前
MongoDB 4.x——治理经验
数据库·mongodb
吴声子夜歌2 天前
MongoDB 4.x——应用设计调优
数据库·mongodb
吴声子夜歌2 天前
MongoDB 4.x——高级特性(Change Stream和事务)
数据库·mongodb
zhangphil3 天前
MongoDB创建数据库时的Collection Name
mongodb