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)
我在这个集合上有三个索引,因为在这个集合上运行了各种聚合:
以下聚合查询获取 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 是正确的工具吗?
当且仅当每个记录中的预计算维度是一个选项时,以下内容可以提高性能。
如果这种类型的查询代表此集合中查询的重要部分,那么包括附加字段以加快这些查询速度可能是一个可行的替代方案。
这还没有进行基准测试。
此查询的代价高昂的部分之一可能来自处理日期。
首先在$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在插入记录时存储它们
作为一个副作用,这显然会使集合的存储磁盘/内存更重。