聚合管道因大量收集而变慢

swo*_*eta 5 mongodb mongodb-query aggregation-framework

我有一个包含超过 2 亿个文档的集合,其中包含维度(我想要过滤或分组的内容)和指标(我想要求和或从中获取平均值的内容)。我目前正面临一些性能问题,我希望获得一些关于如何优化/扩展 MongoDB 的建议或有关替代解决方案的建议。我正在使用 WiredTiger 运行最新的稳定 MongoDB 版本。这些文件基本上如下所示:

{
  "dimensions": {
    "account_id": ObjectId("590889944befcf34204dbef2"),
    "url": "https://test.com",
    "date": ISODate("2018-03-04T23:00:00.000+0000")
  },
  "metrics": {
    "cost": 155,
    "likes": 200
  }
}
Run Code Online (Sandbox Code Playgroud)

我在这个集合上有三个索引,因为在这个集合上运行了各种聚合:

  1. 帐户ID
  2. 日期
  3. account_id 和日期

以下聚合查询获取 3 个月的数据,汇总成本和喜欢并按周/年分组:

db.large_collection.aggregate(

    [
        {
            $match: { "dimensions.date": { $gte: new Date(1512082800000), $lte: new Date(1522447200000) } }
        },

        {
            $match: { "dimensions.account_id": { $in: [ "590889944befcf34204dbefc", "590889944befcf34204dbf1f", "590889944befcf34204dbf21" ] }}
        },

        {
            $group: { 
              cost: { $sum: "$metrics.cost" }, 
              likes: { $sum: "$metrics.likes" }, 
              _id: { 
                year: { $year: { date: "$dimensions.date", timezone: "Europe/Amsterdam" } }, 
                week: { $isoWeek: { date: "$dimensions.date", timezone: "Europe/Amsterdam" } } 
              } 
            }
        },

        { 
            $project: {
                cost: 1, 
                likes: 1 
            }
        }
    ],

    {
        cursor: {
            batchSize: 50
        },
        allowDiskUse: true
    }

);
Run Code Online (Sandbox Code Playgroud)

此查询大约需要 25-30 秒才能完成,我希望将其减少到至少 5-10 秒。它目前是一个 MongoDB 节点,没有分片或任何东西。可以在此处找到解释查询:https : //pastebin.com/raw/fNnPrZh0和 executionStats 在此处:https : //pastebin.com/raw/WA7BNpgA如您所见,MongoDB 正在使用索引,但仍有 130 万个文档需要阅读。我目前怀疑我正面临一些 I/O 瓶颈。

有谁知道我如何改进这个聚合管道?分片会有所帮助吗?MonogDB 是正确的工具吗?

Xav*_*hot 5

当且仅当每个记录中的预计算维度是一个选项时,以下内容可以提高性能。

如果这种类型的查询代表此集合中查询的重要部分,那么包括附加字段以加快这些查询速度可能是一个可行的替代方案。

这还没有进行基准测试。


此查询的代价高昂的部分之一可能来自处理日期

  • 首先在$group计算每个匹配记录的年份和与特定时区相关联的 iso 周的阶段。

  • 然后,在较小程度上,在初始过滤期间,保留最后 3 个月的日期。


这个想法是在每个记录中存储年份和 isoweek,对于给定的示例,这将是{ "year" : 2018, "week" : 10 }. 这样_id$group阶段中的密钥就不需要任何计算(否则将表示 1M3 复杂的日期操作)。

以类似的方式,我们还可以在每个记录中存储相关的月份,这将{ "month" : "201803" }用于给定的示例。这样,[2, 3, 4, 5]在对确切时间戳应用更精确和更昂贵的过滤之前,第一次匹配可能是几个月。这将使最初Date对 200M 记录的成本更高的过滤变成简单的Int过滤。


让我们用这些新的预计算字段创建一个新集合(在实际场景中,这些字段将insert在记录的初始阶段被包含):

db.large_collection.aggregate([
  { $addFields: {
    "prec.year": { $year: { date: "$dimensions.date", timezone: "Europe/Amsterdam" } },
    "prec.week": { $isoWeek: { date: "$dimensions.date", timezone: "Europe/Amsterdam" } },
    "prec.month": { $dateToString: { format: "%Y%m", date: "$dimensions.date", timezone: "Europe/Amsterdam" } }
  }},
  { "$out": "large_collection_precomputed" }
])
Run Code Online (Sandbox Code Playgroud)

这将存储这些文件:

{
  "dimensions" : { "account_id" : ObjectId("590889944befcf34204dbef2"), "url" : "https://test.com", "date" : ISODate("2018-03-04T23:00:00Z") },
  "metrics" : { "cost" : 155, "likes" : 200 },
  "prec" : { "year" : 2018, "week" : 10, "month" : "201803" }
}
Run Code Online (Sandbox Code Playgroud)

让我们查询:

db.large_collection_precomputed.aggregate([
  // Initial gross filtering of dates (months) (on 200M documents):
  { $match: { "prec.month": { $gte: "201802", $lte: "201805" } } },
  { $match: {
    "dimensions.account_id": { $in: [
      ObjectId("590889944befcf34204dbf1f"), ObjectId("590889944befcf34204dbef2")
    ]}
  }},
  // Exact filtering of dates (costlier, but only on ~1M5 documents).
  { $match: { "dimensions.date": { $gte: new Date(1512082800000), $lte: new Date(1522447200000) } } },
  { $group: {
    // The _id is now extremly fast to retrieve:
    _id: { year: "$prec.year", "week": "$prec.week" },
    cost: { $sum: "$metrics.cost" },
    likes: { $sum: "$metrics.likes" }
  }},
  ...
])
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我们将在account_id和上使用索引month

注意:这里,月份存储为String( "201803"),因为我不确定如何将它们转换到Int聚合查询中。但最好是Int在插入记录时存储它们


作为一个副作用,这显然会使集合的存储磁盘/内存更重。

  • 感谢您的回答,尽可能多地预先计算似乎是要走的路。 (2认同)