我目前有一个包含大约270 000 000个文档的数据库。他们看起来像这样:
[{
'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 }
可以合理地快速(150毫秒)运行。
但是,我不仅需要针对单个位置,还需要针对整个区域(大约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秒才能使用的用户来说,这确实很尴尬。
答案 0 :(得分:1)
这些想法可以使查询和性能受益。所有这些工具是否可以一起工作取决于一些试验和测试。另外,请注意,更改数据存储方式和添加新索引意味着将对应用程序进行更改(即捕获数据),并且需要仔细验证对同一数据的其他查询(它们不会以错误的方式受到影响) )。
(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 }, ... ]
}
这意味着每个文档大约十个条目。为条目添加数据将把数据推送到详细信息数组中,而不是像本申请中那样添加文档。如果需要小时信息(时间),它也可以存储为明细子文档的一部分;这完全取决于您的应用程序需求。
这种设计的好处:
$project
支持accumulators $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
变量以与region字段匹配。
请注意,“按位置”和“按地区”这两个查询都将受益于上述两个注意事项,即 A 和 B 。
(C)索引注意事项:
当前索引:{ product: 1, location: 1, createdAt: -1 }
。
考虑到新字段region
,将需要更新的索引。没有region字段的索引,带有region的查询将无法受益。需要第二个索引;适合查询的复合索引。使用region字段创建索引意味着写操作的额外开销。此外,将有内存和存储注意事项。
注释:
添加索引后,如果两个查询(“按位置”和“按区域”)正在使用各自的索引,则都需要使用explain
进行验证。这将需要一些测试;一个反复试验的过程。
再次,添加新数据,以其他格式存储数据,添加新索引需要考虑以下因素:
答案 1 :(得分:0)
老实说,您的聚合功能已尽可能地优化,尤其是如果您将{ product: 1, createdAt: -1, location: 1 }
作为索引,如您声明的那样。
我不确定您的整个产品如何构建,但是我认为最好的解决方案是拥有另一个仅包含上周“相关”文档的收藏集。
然后,您可以轻松查询该集合。使用TTL Index在Mongo中也很容易做到这一点。
如果这不是一个选项,则可以在“相关”文档中添加一个临时字段并对其进行查询,从而使检索它们的速度有所提高,但是维护该字段将需要您每X次运行一个进程,这可能会使您的结果现在100%准确,具体取决于您决定何时运行。