加快大型集合的聚合



我目前有一个数据库,其中包含大约27万个文档。它们看起来像这样:

[{
'location': 'Berlin',
'product': 4531,
'createdAt': ISODate(...),
'value': 3523,
'minOffer': 3215,
'quantity': 7812
},{
'location': 'London',
'product': 1231,
'createdAt': ISODate(...),
'value': 53523,
'minOffer': 44215,
'quantity': 2812
}]

该数据库目前保存了一个多月的数据,约有170个地点(在欧盟和美国),约有8000种产品。这些文档代表时间步长,因此每天大约有12-16个条目,每个位置的每个产品(最多每小时1个)
我的目标是检索过去7天在给定位置的产品的所有时间步长。对于单个位置,此查询在索引为{ product: 1, location: 1, createdAt: -1 }的情况下工作得相当快(150ms)。

然而,我也需要这些时间步长,不仅针对单个位置,而且针对整个区域(大约85个位置)。我目前正在用这个聚合来做这件事,它每小时对所有条目进行分组,并对所需值进行平均:

this.db.collection('...').aggregate([
{ $match: { { location: { $in: [array of ~85 locations] } }, product: productId, createdAt: { $gte: new Date(Date.now() - sevenDaysAgo) } } }, {
$group: {
_id: {
$toDate: {
$concat: [
{ $toString: { $year: '$createdAt' } },
'-',
{ $toString: { $month: '$createdAt' } },
'-',
{ $toString: { $dayOfMonth: '$createdAt' } },
' ',
{ $toString: { $hour: '$createdAt' } },
':00'
]
}
},
value: { $avg: '$value' },
minOffer: { $avg: '$minOffer' },
quantity: { $avg: '$quantity' }
}
}
]).sort({ _id: 1 }).toArray()

然而,即使使用索引{ product: 1, createdAt: -1, location: 1 }(~40秒),这也非常缓慢。有没有办法加快聚合速度,使其最多减少几秒钟?这可能吗,或者我应该考虑使用其他东西吗
我曾考虑过将这些聚合保存在另一个数据库中,然后检索并聚合其余的,但对于网站上的第一批用户来说,这真的很尴尬,他们必须等待40秒。

这些想法有利于查询和性能。所有这些是否能共同发挥作用还需要一些试验和测试。此外,请注意,更改数据的存储方式和添加新索引意味着应用程序将发生更改,即捕获数据,并且需要仔细验证对同一数据的其他查询(确保它们不会受到错误的影响)。


(A)在文档中存储一天的详细信息:

将一天的数据存储(嵌入)在与子文档数组相同的文档中。每个子文档代表一个小时的条目。

来源:

{
'location': 'London',
'product': 1231,
'createdAt': ISODate(...),
'value': 53523,
'minOffer': 44215,
'quantity': 2812
}

到:

{
location: 'London',
product: 1231,
createdAt: ISODate(...),
details: [ { value: 53523, minOffer: 44215, quantity: 2812 }, ... ]
}

这意味着每个文档大约有十个条目。为条目添加数据将把数据推送到细节数组中,而不是像当前应用程序中那样添加文档。如果需要小时信息(时间),它也可以作为详细信息子文档的一部分存储;这将完全取决于您的应用程序需求。

这种设计的好处:

  • 要维护和查询的文档数量将减少(每个每天大约10个文档的产品)
  • 在查询中,组阶段将消失。这将只是一个项目阶段。注意,$project支持累加器$avg$sum

以下阶段将创建当天(或文档)的总和和平均值。

{ 
$project: { value: { $avg: '$value' }, minOffer: { $avg: '$minOffer' }, quantity: { $avg: '$quantity' } }
}

请注意,文档大小的增加并不多,每天存储的详细信息数量也不多。


(B)按地区查询:

当前多个位置(或一个区域)与此查询文件管理器的匹配:{ location: { $in: [array of ~85 locations] } }。这个过滤器显示:location: location-1, -or- location: location-3, -or- ..., location: location-50。添加一个新字段region,将使用一个匹配的值进行筛选。

按地区查询将更改为:

{ 
$match: { 
region: regionId, 
product: productId, 
createdAt: { $gte: new Date(Date.now() - sevenDaysAgo) } 
} 
}

regionId变量将被提供以与区域字段相匹配。

请注意,"按位置"one_answers"按地区"这两个查询都将受益于上述两个注意事项,即AB


(C)索引注意事项:

当前索引:{ product: 1, location: 1, createdAt: -1 }

考虑到新字段region,将需要更新的索引。如果没有区域字段上的索引,则具有区域的查询将无法受益。将需要第二个索引;适合查询的复合索引。使用区域字段创建索引意味着写入操作会增加额外开销。此外,还将考虑内存和存储。

注意:

添加索引后,如果两个查询("按位置"one_answers"按地区")使用各自的索引,则需要使用explain进行验证。这将需要一些测试;反复试验的过程。

同样,添加新数据、以不同格式存储数据、添加新索引需要考虑以下因素:

  • 仔细测试并验证其他现有查询是否正常运行
  • 数据捕获需求的变化
  • 测试新查询并验证新设计是否按预期执行

老实说,您的聚合已经尽可能优化了,尤其是如果您像您所说的那样将{ product: 1, createdAt: -1, location: 1 }作为索引。

我不确定你的整个产品是如何构建的,但在我看来,最好的解决方案是另一个集合,只包含过去一周的"相关"文档。

然后您可以轻松地查询该集合,这在Mongo中也很容易,使用TTL索引也很容易。

如果这不是一个选项,你可以在"相关"文档中添加一个临时字段,并对此进行查询,使检索它们的速度更快,但维护这个字段需要你每X次运行一个过程,这可能会使你的结果现在100%准确,这取决于你决定何时运行它

最新更新