Ohhnews

分类导航

$ cd ..
foojay原文

MongoDB聚合优化:利用索引进行排序(第5部分)

#mongodb#聚合管道#索引排序#性能优化#数据建模

以及为什么 MongoDB 可能是一款比你想象中更好的关系数据库。

设计评审 是一对一的会议,MongoDB 专家会在其中就数据建模最佳实践和应用设计挑战提供建议。在本系列中,我们将探讨一些常见的真实场景,在这些场景中,设计评审帮助开发者使用 MongoDB 取得了实实在在的成功。


[LOADING...]

在本系列中,我们描述了如何逐步提高一个运行缓慢的 MongoDB 聚合管道 的性能。该管道是一个虚构视频流服务应用的一部分,用于将用户配置文件映射到这些用户访问服务所用的设备,并且基于我最近在一次设计评审中遇到的一个真实用例。

第 1 部分 中,我们基于设计评审中遇到的情况分解了最初的管道设计,并解释了管道的每个阶段的设计目的。如果你还没有读过那篇文章,或者需要复习 MongoDB 聚合管道,我建议你先返回阅读,再继续本部分。

在进行了前三轮改进之后——包括移除不必要的 $unwind 阶段、使用内嵌方式更高效地建模多对多关系,以及利用数据冗余提升读取性能——我们已经成功更新了查询,超出了 SLA 目标,性能相比最初设计提升了 230 倍。

管道描述每查询平均耗时总耗时(300 次查询迭代,15 个并发线程)
初始设计11.8 秒260 秒
移除 $unwind4.7 秒105 秒
重构多对多关系2.9 秒62.5 秒
冗余设备名称51 毫秒1.2 秒

优化步骤 4:基于索引的排序

上一步中通过冗余设备名称信息所带来的显著性能提升,意味着查询性能现在已经远低于目标的一秒响应时间。不过,我们还发现了一个快速而简单的进一步优化方法:

到目前为止,管道设计的每次迭代都包含一个 $sort 阶段,用于按 profileID 对匹配文档进行排序:

{
  $sort: {
    profileID: 1
  }
}

这可能是有问题的,因为在到达 $sort 阶段时,管道可能仍有大量文档要处理。例如,搜索所有在奥斯汀使用 iPhone 12 连接服务的配置文件,会有 1,289 个文档被传入 $sort 阶段,所有这些文档都需要由 MongoDB 在内存中处理。

通常来说,排序是 CPU 和内存密集型操作,因此应谨慎使用 $sort,尤其是在处理较大文档集时。虽然单独运行一次排序操作的响应时间通常看起来可以接受,但在大规模执行时,所消耗的资源往往会导致容量问题。

推荐替代显式 $sort 操作的方式是利用索引会按键返回有序文档这一特性:因此,如果索引键包含我们想要排序的字段,文档就已经按所需顺序返回,从而无需显式 $sort 操作。

在我们的场景中,我们通过将 profileID 添加到 profiles 集合上定义的索引中,得以利用基于索引的排序:

{"contact.address.city": 1, "devices.deviceName": 1, profileID: 1}

注意,索引定义中字段的顺序非常重要。使用这种定义,索引中的条目会先按城市排序,再按设备名称排序,然后按 profileID 排序。这意味着,在找出目标城市和设备名称组合的配置文件后,遍历索引会按 profileID 顺序返回更多匹配结果。这是使用等值、排序、范围(ESR)规则来确定复合索引定义中最佳字段顺序的一个示例。简单来说,在索引定义中,进行等值匹配的字段应该放在用于排序的字段之前。

以这种方式更新索引定义,让我们能够从管道中移除 $sort 阶段。完成这一最终更改后,我们的最终管道定义如下:

[
  {
    $match: {
      "contact.address.city": "Austin",
      "deviceSNs.deviceName": "iPhone 12"
    }
  },
  {
    $skip: 0
  },
  {
    $limit: 10
  },
  {
    $lookup: {
      from: "Devices",
      localField: "deviceSNs.deviceSN",
      foreignField: "deviceSN",
      pipeline: [
        {
          $match: {
            deviceName: "iPhone 12"
          }
        },
        {
          $set: {
            _id: "$$REMOVE"
          }
        }
      ],
      as: "deviceData"
    }
  },
  {
    $set: {
      accountNum: "$$REMOVE",
      mappingData: "$$REMOVE",
      customerType: "$$REMOVE",
      DOB: "$$REMOVE",
      _id: "$$REMOVE"
    }
  }
]

对这个管道版本进行测试后,性能又有了进一步提升。单个查询现在平均耗时不到 15 毫秒,完成 300 次查询迭代的总耗时为 655 毫秒:

管道描述每查询平均耗时总耗时(300 次查询迭代,15 个并发线程)
初始设计11.8 秒260 秒
移除 $unwind4.7 秒105 秒
重构多对多关系2.9 秒62.5 秒
冗余设备名称51 毫秒1.2 秒
索引排序14 毫秒655 毫秒

总结

本系列涵盖了大量内容,但核心要点是:在 MongoDB 中建模和查询数据的方式,其重要性不亚于(甚至超过)传统关系型数据库。我们涵盖的要点如下:

  • 在聚合管道中,如果你使用 $unwind 阶段只是为了处理数组中的所有元素,通常有更高效的方式来实现同样的目的
  • MongoDB 文档原生支持数组和子文档,这让你在建模数据关系时拥有了更多选择,可以消除昂贵的 lookup/join 操作,并让关联表这类关系型数据库风格的变通方案变得不再必要。
  • 数据冗余在合理使用的情况下,并不像许多人被灌输的那样是坏事。
  • 索引对于查询性能和可扩展性始终至关重要,但也不要忘记它们在数据排序方面的作用。

我们讨论的大部分内容归根结底是一个处理多对多关系的示例。有些人将 MongoDB 描述为非关系型,但现实是数据总是包含关系,而使用 MongoDB 和文档数据模型时唯一变化的,是我们如何对这些关系进行建模。正如我们所看到的,完全可以说,MongoDB 为一对多和多对多关系建模提供了比传统表格数据库更好的选择。这是否意味着它是一款比 RDBMS 更好的关系数据库呢?欢迎在评论区告诉我们你的想法。

如果你有兴趣了解更多关于优化聚合管道的内容,我强烈推荐 Paul Done 的这本优秀著作,它既有电子书版本,也有平装本。我认为对任何使用 MongoDB 的人来说,这都是必读资料。

本文 MongoDB 聚合优化:使用索引进行排序(第 5 部分) 首发于 foojay