MongoDB聚合优化:优化多对多关系(第3部分)
以及为什么 MongoDB 可能比你意识到的更适合做关系型数据库。
设计评审(Design reviews)是 MongoDB 专家提供数据建模最佳实践与应用设计挑战建议的一对一会议。在本系列中,我们将探索设计评审帮助开发者在 MongoDB 上取得切实成功的常见真实场景。
[LOADING...]
在本系列第 1 部分中,我们描述了一个基于近期我与合作团队在 MongoDB 客户处进行设计评审的用例。该团队刚接触 MongoDB,他们采取的数据建模和后续查询方式都非常“像 RDBMS”。结果,查询性能远低于 SLA 要求。
该用例涉及一个虚构的视频流媒体服务,其数据库将用户配置文件映射到用户访问服务时使用的设备。
我们要支持的查询是:查找所有关联了某个城市联系地址、并使用某种设备类型访问该服务的配置文件——例如,所有注册在德克萨斯州奥斯汀(Austin, TX)、并使用 iPhone 12 访问服务的配置文件。为了执行此查询,我们构建了一个包含十个阶段的聚合管道:
[LOADING...]
在第 1 部分中,我们逐步分析了管道中每个阶段的设计目的。如果你不熟悉 MongoDB 聚合管道或需要复习,建议先回看那篇文章再继续。
到目前为止,我们优化管道的工作主要集中在移除不必要的 unwind 阶段,并因此获得了 60% 的性能提升。不过,距离亚秒级查询响应时间的目标仍有很大差距。
优化步骤 2:重构多对多关系
我们最初的数据模型使用了一个中间(或称“关联”)映射集合,将 profile 集合与 device 集合之间的多对多关系拆分为一对多和多对一的关系:
[LOADING...]
使用关联表来建模多对多关系,是任何使用过关系型数据库的人都会熟悉的模式。这种做法的出现是为了规避表格化数据存储的一个限制:在一张表的某一行中,很难优雅地存储指向另一张表中多个相关行的任意数量引用。虽然 RDBMS 模型中引入关联表消除了这一限制,但缺点在于,要从多对多关系一侧的数据到达另一侧的数据,需要进行两次连接。无论使用哪种数据库,连接操作在计算上总是代价高昂的;查询所需的连接越多,查询就会越慢。
MongoDB 使用的文档数据模型,通过子文档和数组的使用,不存在同样的限制,并为你提供了一对多和多对多关系的建模选项,可以避免部分甚至全部连接,从而显著提高查询性能。
对于多对多关系,文档模型提供的选项之一是用一个 ID 数组来替代关联集合:即将关系一侧文档的 ID 存放到另一侧关联文档的数组中。通常,这个 ID 数组会添加在关系基数较低的一侧——也就是说,这样生成的数组会更小。通过消除中间关联集合,我们减少了遍历关系时所需的连接操作数量。
在流媒体服务数据的场景中,profiles 与 devices 之间多对多关系的两侧基数差异并不大——几乎所有情况下都小于 10——因此我们选择在 profile 文档中添加一个设备序列号数组。修改后的数据模型如下所示:
[LOADING...]
采用修订后的模型,一个关联了三个设备的 profile 现在会像这样(注意新增的 deviceSNs 数组):
仅凭这一处修改,管道中的第一个 $lookup 阶段(将 profiles 连接到中间映射文档)就可以被移除。值得注意的是,这并不是数据反规范化的例子。我们没有复制任何数据,只是调整了数据的存储位置,使其能被更高效地使用。
从管道中移除第一个 $lookup 阶段后,剩下的 $lookup 阶段被修改为直接将 profile 文档连接到对应的 device 文档,而无需经过中间映射集合:
正如我们在第 2 部分中移除 $unwind 阶段时看到的,在 $lookup 阶段将 localField 设置为数组字段——此处为 deviceSNs——会导致 $lookup 操作针对数组中的每个值分别执行。
整个管道现在看起来如下所示:
在使用我们的测试数据集时,移除第一个 $lookup 阶段意味着——在“city”等于 Austin 的 profile 文档示例中——针对 6763 个匹配 profile 文档的一系列查找被完全消除了。重新测试管道后发现,单次查询的平均耗时和完成 300 次查询迭代的总耗时都下降了 75%。这是一个显著的变化,也非常有力地说明了 MongoDB 在关系建模方面所提供的灵活性和选项(包括关系型数据库所不具备的选项)能够对性能产生显著的积极影响。甚至可以这样说:在关系建模上,MongoDB 比“关系型”数据库做得更好。
尽管到目前为止所做的修改已经显著提升了管道的性能,但我们距离亚秒级目标查询响应时间仍有很大差距。在第 4 部分中,我们将展示如何通过适当使用数据复制,以最小的负面影响显著提升读取性能。
本文《MongoDB 聚合优化:优化多对多关系(第 3 部分)》最初发布在 foojay 上。