Mongo DB 聚合组性能



我对mongo DB很陌生,正在为我们的一个应用程序进行试验。我们正在尝试实现 CQRS 和我们尝试使用的 node 的查询部分.js以及我们通过 C# 实现的命令部分。

我的一个集合中可能有数百万个文档。我们将有一个scenarioId字段,每个场景可以有大约 200 万条记录。

我们的用例是比较这两个场景数据,并对场景的每个字段进行一些数学运算。 例如,每个方案都可以有一个属性avgMiles,我想计算此属性的差异,用户应该能够筛选此差异值。由于我的设计是将两个场景数据保存在单个集合中,因此我正在尝试按场景 ID 进行分组并进一步投影。

我的文档示例结构如下所示。

{ 
"_id" : ObjectId("5ac05dc58ff6cd3054d5654c"), 
"origin" : {
"code" : "0000", 
}, 
"destination" : {
"code" : "0001", 
}, 
"currentOutput" : {
"avgMiles" : 0.15093020854848138, 
},
"scenarioId" : NumberInt(0), 
"serviceType" : "ECON"
}

当我分组时,我会根据origin.codedestination.code以及serviceType属性对其进行分组。

我的聚合管道查询如下所示:

db.servicestats.aggregate([{$match:{$or:[{scenarioId:0}, {scenarioId:1}]}},
{$sort:{'origin.code':1,'destination.code':1,serviceType:1}},
{$group:{
_id:{originCode:'$origin.code',destinationCode:'$destination.code',serviceType:'$serviceType'},
baseScenarioId:{$sum:{$switch: {
branches: [
{
case: { $eq: [ '$scenarioId', 1] },
then: '$scenarioId'
}],
default: 0
}
}},
compareScenarioId:{$sum:{$switch: {
branches: [
{
case: { $eq: [ '$scenarioId', 0] },
then: '$scenarioId'
}],
default: 0
}
}},
baseavgMiles:{$max:{$switch: {
branches: [
{
case: { $eq: [ '$scenarioId', 1] },
then: '$currentOutput.avgMiles'
}],
default: null
}
}},
compareavgMiles:{$sum:{$switch: {
branches: [
{
case: { $eq: [ '$scenarioId', 0] },
then: '$currentOutput.avgMiles'
}],
default: null
}
}}
}
},
{$project:{scenarioId:
{ base:'$baseScenarioId',
compare:'$compareScenarioId'
},
avgMiles:{base:'$baseavgMiles', comapre:'$compareavgMiles',diff:{$subtract :['$baseavgMiles','$compareavgMiles']}}
} 
},
{$match:{'avgMiles.diff':{$eq:0.5}}},
{$limit:100}
],{allowDiskUse: true} )

我的组管道阶段将有 400 万个文档。您能否建议如何提高此查询的性能?

我有一个按条件分组中使用的字段的索引,并且我添加了一个排序管道阶段来帮助分组以更好地执行。

欢迎任何建议。

由于分组依据在我的情况下不起作用,我已经使用 $lookup 实现了左外部连接,查询如下所示。

db.servicestats.aggregate([
{$match:{$and :[ {'scenarioId':0}
//,{'origin.code':'0000'},{'destination.code':'0001'}
]}},
//{$limit:1000000},
{$lookup: { from:'servicestats',
let: {ocode:'$origin.code',dcode:'$destination.code',stype:'$serviceType'},
pipeline:[
{$match: {
$expr: { $and:
[
{ $eq: [ "$scenarioId", 1 ] },
{ $eq: [ "$origin.code",  "$$ocode" ] },
{ $eq: [ "$destination.code",  "$$dcode" ] },
{ $eq: [ "$serviceType",  "$$stype" ] },
]
}
}
},
{$project: {_id:0, comp :{compavgmiles :'$currentOutput.avgMiles'}}},
{ $replaceRoot: { newRoot: "$comp" } }
],
as : "compoutputs"
}},
{
$replaceRoot: {
newRoot: {
$mergeObjects:[
{
$arrayElemAt: [
"$$ROOT.compoutputs",
0
]
},
{
origin: "$$ROOT.origin",
destination: "$$ROOT.destination",
serviceType: "$$ROOT.serviceType",
baseavgmiles: "$$ROOT.currentOutput.avgMiles",
output: '$$ROOT'
}
]
}
}
},
{$limit:100}
])  

上述查询性能良好,并在 70 毫秒内返回。

但是在我的场景中,我需要实现一个完整的外部连接,我知道 mongo 目前不支持它,并使用$facet管道实现,如下所示

db.servicestats.aggregate([
{$limit:1000},
{$facet: {output1:[
{$match:{$and :[ {'scenarioId':0}
]}},
{$lookup: { from:'servicestats',
let: {ocode:'$origin.code',dcode:'$destination.code',stype:'$serviceType'},
pipeline:[
{$match: {
$expr: { $and:
[
{ $eq: [ "$scenarioId", 1 ] },
{ $eq: [ "$origin.code",  "$$ocode" ] },
{ $eq: [ "$destination.code",  "$$dcode" ] },
{ $eq: [ "$serviceType",  "$$stype" ] },
]
}
}
},
{$project: {_id:0, comp :{compavgmiles :'$currentOutput.avgMiles'}}},
{ $replaceRoot: { newRoot: "$comp" } }
],
as : "compoutputs"
}},
//{
//          $replaceRoot: {
//             newRoot: {
//                $mergeObjects:[
//                   {
//                      $arrayElemAt: [
//                         "$$ROOT.compoutputs",
//                         0
//                      ]
//                   },
//                   {
//                      origin: "$$ROOT.origin",
//                      destination: "$$ROOT.destination",
//                      serviceType: "$$ROOT.serviceType",
//                      baseavgmiles: "$$ROOT.currentOutput.avgMiles",
//                      output: '$$ROOT'
//                   }
//                ]
//             }
//          }
//       }
],
output2:[
{$match:{$and :[ {'scenarioId':1}
]}},
{$lookup: { from:'servicestats',
let: {ocode:'$origin.code',dcode:'$destination.code',stype:'$serviceType'},
pipeline:[
{$match: {
$expr: { $and:
[
{ $eq: [ "$scenarioId", 0 ] },
{ $eq: [ "$origin.code",  "$$ocode" ] },
{ $eq: [ "$destination.code",  "$$dcode" ] },
{ $eq: [ "$serviceType",  "$$stype" ] },
]
}
}
},
{$project: {_id:0, comp :{compavgmiles :'$currentOutput.avgMiles'}}},
{ $replaceRoot: { newRoot: "$comp" } }
],
as : "compoutputs"
}},
//{
//          $replaceRoot: {
//             newRoot: {
//                $mergeObjects:[
//                   {
//                      $arrayElemAt: [
//                         "$$ROOT.compoutputs",
//                         0
//                      ]
//                   },
//                   {
//                      origin: "$$ROOT.origin",
//                      destination: "$$ROOT.destination",
//                      serviceType: "$$ROOT.serviceType",
//                      baseavgmiles: "$$ROOT.currentOutput.avgMiles",
//                      output: '$$ROOT'
//                   }
//                ]
//             }
//          }
//       },
{$match :{'compoutputs':{$eq:[]}}}
]
}
}


///{$limit:100}
])

但是分面性能非常糟糕。非常欢迎任何进一步的改进想法。

通常,有三种情况会导致查询缓慢:

  1. 查询没有索引,不能有效地使用索引,或者模式设计不是最佳的(例如高度嵌套的数组或子文档),这意味着MongoDB必须做一些额外的工作才能到达相关数据。
  2. 查询正在等待一些缓慢的事情(例如,从磁盘获取数据,将数据写入磁盘)。
  3. 硬件配置不足。

就查询而言,可能会有一些关于查询性能的一般建议:

  • 在聚合管道中使用allowDiskUse意味着查询可能会在其某些阶段使用磁盘。磁盘通常是计算机中最慢的部分,因此,如果可以避免这种情况,它将加快查询速度。

  • 请注意,聚合查询限制为 100MB 内存使用量。这与你拥有的内存量无关。

  • $group阶段不能使用索引,因为索引与文档在磁盘上的位置相关联。一旦聚合管道进入文档物理位置不相关的阶段(例如$group阶段),就不能再使用索引。

  • 默认情况下,WiredTiger 缓存是 RAM 的 ~50%,因此 64GB 的计算机将具有 ~32GB 的 WiredTiger 缓存。如果发现查询速度很慢,则可能是MongoDB需要转到磁盘来获取相关文档。在查询期间监视iostats和检查磁盘利用率百分比将提供有关是否预配了足够 RAM 的提示。

一些可能的解决方案是:

  • 配置更多的RAM,这样MongoDB就不必经常去磁盘。
  • 重新设计架构设计以避免文档中出现大量嵌套字段或多个数组。
  • 定制文档架构,以便
  • 您更轻松地查询其中的数据,而不是根据您认为数据的存储方式定制架构(例如,避免关系数据库设计模型中固有的大量规范化)。
  • 如果发现已达到单台计算机的性能限制,请考虑使用分片来水平扩展查询。但是,请注意,分片是一种需要仔细设计和考虑的解决方案。

你在上面说你想按scenarioId分组,但是,你没有。但这可能是您应该做的,以避免所有 switch 语句。像这样的事情可能会让你开始:

db.servicestats.aggregate([{
$match: {
scenarioId: { $in: [ 0, 1 ] }
}
}, {
$sort: { // not sure if that stage even helps - try to run with and without
'origin.code': 1,
'destination.code': 1,
serviceType: 1
}
}, {
$group: { // first group by scenarioId AND the other fields
_id: {
scenarioId: '$scenarioId',
originCode: '$origin.code',
destinationCode: '$destination.code',
serviceType: '$serviceType'
},
avgMiles: { $max: '$currentOutput.avgMiles' } // no switches needed
},
}, {
$group: { // group by the other fields only so without scenarioId
_id: {
originCode: '$_id.originCode',
destinationCode: '$_id.destinationCode',
serviceType: '$_id.serviceType'
},
baseScenarioAvgMiles: {
$max: {
$cond: {
if: { $eq: [ '$_id.scenarioId', 1 ] },
then: '$avgMiles',
else: 0
}
}
},
compareScenarioAvgMiles: {
$max: {
$cond: {
if: { $eq: [ '$_id.scenarioId', 0 ] },
then: '$avgMiles',
else: 0
}
}
}
},
}, {
$addFields: { // compute the difference
diff: {
$subtract :[ '$baseScenarioAvgMiles', '$compareScenarioAvgMiles']
}
}
}, {
$match: {
'avgMiles.diff': { $eq: 0.5 }
}
}, {
$limit:100
}], { allowDiskUse: true })

除此之外,我建议您使用 db.collection.explain().aggregate(...) 的强大功能来找到正确的索引并调整您的查询。

最新更新