MongoDB聚合查询性能远低于SQL Server,如何优化至4分钟以内?
MongoDB聚合查询性能优化问题
我了解到MongoDB相比SQL Server更擅长数据聚合,于是尝试将业务中原有SQL Server查询转换为MongoDB查询,但是转换后的查询性能远不达标:相同统计逻辑下,统计128天数据时SQL Server仅需约4分钟即可完成,而当前MongoDB查询耗时高达34分钟。
现有查询逻辑
function(thresholdObs, days) { var name = "period-" + days + "j" var startDate = new Date('2020', '00', '01'); var endDate = new Date(startDate.getTime() + 1000 * 60 * 60 * 24 *[days]); db.getCollection('measures').aggregate([ { $match: { $and: [ { measureDate: { $gte: startDate } }, { measureDate: { $lte: endDate } } ] } }, { $addFields: { multiplyIahobs: { $cond: [ {$or: [ {$eq: ["$averageIAH", null]}, {$eq: ["$obs", null]} ]}, 0, { $multiply: ["$averageIAH", "$obs"] } ] }, multiplyPressionobs: { $cond: [ {$or: [ {$eq: ["$averagePressure", null]}, {$eq: ["$obs", null]} ]}, 0, { $multiply: ["$averagePressure", "$obs"] } ] }, multiplyFuitesobs: { $cond: [ {$or: [ {$eq: ["$averageLeakage", null]}, {$eq: ["$obs", null]} ]}, 0, { $multiply: ["$averageLeakage", "$obs"] } ] }, multiplyinspiratoryPressureobs: { $cond: [ {$or: [ {$eq: ["$inspiratoryPressure", null]}, {$eq: ["$obs", null]} ]}, 0, { $multiply: ["$inspiratoryPressure", "$obs"] } ] }, multiplyexpiratoryPressureobs: { $cond: [ {$or: [ {$eq: ["$expiratoryPressure", null]}, {$eq: ["$obs", null]} ]}, 0, { $multiply: ["$expiratoryPressure", "$obs"] } ] }, enoughDays: { $cond: [ { $gte: ["$obs", thresholdObs ] }, 1, 0] }, missingDays: { $cond: [ { $lt: ["$obs", thresholdObs ] }, 1, 0] }, daysWithoutUsage: { $cond: [ { $eq: ["$obs", 0 ] }, 1, 0] }, daysWithData: { $sum: 1 } } }, { $group: { _id: "$deviceID", obsUsage: { $avg: "$obs" }, enoughDays: { $sum: "$enoughDays" }, missingDays: { $sum: "$missingDays" }, daysWithoutUsage: { $sum: "$daysWithoutUsage" }, daysWithData: { $sum: "$daysWithData" }, sumobs: { $sum: "$obs" }, sumMultiplyIahobs: { $sum: "$multiplyIahobs" }, sumMultiplyPressureObs: { $sum: "$multiplyPressionobs" }, sumMultiplyLeakageObs: { $sum: "$multiplyFuitesobs" }, sumMultiplyinspiratoryPressureobs: { $sum: "$multiplyinspiratoryPressureobs" }, sumMultiplyexpiratoryPressureobs: { $sum: "$multiplyexpiratoryPressureobs" }, } }, { $addFields: { fullObs: { $divide: [ "$sumobs", days ] }, daysWithoutData: { $subtract: [ days, "$daysWithData" ]}, averageIAH: { $cond: [ { $eq: [ "$sumobs", 0 ] }, 0, { $divide: ["$sumMultiplyIahobs", "$sumobs"] } ] }, averagePressure: { $cond: [ { $eq: [ "$sumobs", 0 ] }, 0, { $divide: ["$sumMultiplyPressureObs", "$sumobs"] } ] }, averageLeakage: { $cond: [ { $eq: [ "$sumobs", 0 ] }, 0, { $divide: ["$sumMultiplyLeakageObs", "$sumobs"] } ] }, inspiratoryPressure: { $cond: [ { $eq: [ "$sumobs", 0 ] }, 0, { $divide: ["$sumMultiplyinspiratoryPressureobs", "$sumobs"] } ] }, expiratoryPressure: { $cond: [ { $eq: [ "$sumobs", 0 ] }, 0, { $divide: ["$sumMultiplyexpiratoryPressureobs", "$sumobs"] } ] }, } }, { $project: { _id: 1, [name] : { obsUsage: "$obsUsage", enoughDays: "$enoughDays", missingDays: "$missingDays", daysWithoutUsage: "$daysWithoutUsage", daysWithData: "$daysWithData", sumobs: "$sumobs", fullObs: "$fullObs", daysWithoutData: "$daysWithoutData", averageIAH: "$averageIAH", averagePressure: "$averagePressure", averageLeakage: "$averageLeakage", inspiratoryPressure: "$inspiratoryPressure", expiratoryPressure: "$expiratoryPressure" } } }, { $merge: { into: "periods", on: "_id", whenMatched: "merge", whenNotMatched: "insert" } } ],{allowDiskUse: true}) }
集合说明
measures集合共有374670449条文档,样例数据如下:
{ _id: ObjectId('6127a15fef44a9ed52a5bf62'), deviceId: 5, measureDateAdded: ISODate('2013-03-15T10:30:35.753Z'), measureDate: ISODate('2012-03-20T06:00:00.000Z'), obs: 20, averageIAH: NumberDecimal('5.50'), averagePressure: NumberDecimal('6.30'), averageLeakage: NumberDecimal('13.00'), inspiratoryPressure: null, expiratoryPressure: null }, { _id: ObjectId('6127a15fef44a9ed52a5bfc6'), deviceId: 5, measureDateAdded: ISODate('2013-03-15T10:30:40.063Z'), measureDate: ISODate('2012-06-28T05:00:00.000Z'), obs: 197, averageIAH: NumberDecimal('5.30'), averagePressure: NumberDecimal('6.90'), averageLeakage: NumberDecimal('15.00'), inspiratoryPressure: null, expiratoryPressure: null }, { _id: ObjectId('6127aa2bef44a9ed52a0922a'), deviceId: 367959, measureDateAdded: ISODate('2019-01-19T14:13:19.620Z'), measureDate: ISODate('2019-01-16T11:00:00.000Z'), obs: 375, averageIAH: NumberDecimal('2.00'), averagePressure: NumberDecimal('9.60'), averageLeakage: NumberDecimal('6.00'), inspiratoryPressure: null, expiratoryPressure: null }
已排查的性能瓶颈
最耗时的步骤是按deviceID分组的阶段,仅该阶段耗时就超过4分钟。不希望直接放弃使用MongoDB,因此想确认:查询编写逻辑是否存在问题?能否通过调整查询将总耗时降低到4分钟以内?如果可行,具体应该如何优化?
优化方案
1. 先修复致命逻辑bug
你代码中$group阶段的分组键写的是$deviceID,但样例数据里的实际字段名为deviceId(大小写不一致),会导致所有数据被分到同一个分组里,不仅计算结果错误,也会大幅提升分组阶段的开销,首先修正字段名拼写。
2. 新增覆盖索引(核心优化,性能可提升10倍以上)
创建以下复合覆盖索引,让整个查询可以直接从索引取数,完全不需要读取原始文档,也无需额外哈希排序开销:
db.measures.createIndex( {measureDate: 1, deviceId: 1}, {include: ["obs", "averageIAH", "averagePressure", "averageLeakage", "inspiratoryPressure", "expiratoryPressure"]} )
- 首字段
measureDate匹配$match过滤条件,可快速定位指定时间范围内的所有数据 - 第二个字段
deviceId匹配分组键,MongoDB可直接按索引顺序完成分组,无需额外计算 - include字段覆盖了查询用到的所有业务字段,避免了全量文档的IO读取开销
3. 简化聚合逻辑,消除冗余步骤
删除第一个多余的$addFields阶段,把所有计算逻辑直接下沉到$group阶段,减少中间数据的内存开销:
// 优化后的聚合核心逻辑 db.getCollection('measures').aggregate([ { $match: { measureDate: { $gte: startDate, $lte: endDate } } }, { $group: { _id: "$deviceId", obsUsage: { $avg: "$obs" }, enoughDays: { $sum: { $cond: [ { $gte: ["$obs", thresholdObs ] }, 1, 0] } }, missingDays: { $sum: { $cond: [ { $lt: ["$obs", thresholdObs ] }, 1, 0] } }, daysWithoutUsage: { $sum: { $cond: [ { $eq: ["$obs", 0 ] }, 1, 0] } }, daysWithData: { $sum: 1 }, sumobs: { $sum: "$obs" }, sumMultiplyIahobs: { $sum: { $cond: [ {$or: [{$eq: ["$averageIAH", null]}, {$eq: ["$obs", null]}]}, 0, { $multiply: ["$averageIAH", "$obs"] } ] } }, sumMultiplyPressureObs: { $sum: { $cond: [ {$or: [{$eq: ["$averagePressure", null]}, {$eq: ["$obs", null]}]}, 0, { $multiply: ["$averagePressure", "$obs"] } ] } }, sumMultiplyLeakageObs: { $sum: { $cond: [ {$or: [{$eq: ["$averageLeakage", null]}, {$eq: ["$obs", null]}]}, 0, { $multiply: ["$averageLeakage", "$obs"] } ] } }, sumMultiplyinspiratoryPressureobs: { $sum: { $cond: [ {$or: [{$eq: ["$inspiratoryPressure", null]}, {$eq: ["$obs", null]}]}, 0, { $multiply: ["$inspiratoryPressure", "$obs"] } ] } }, sumMultiplyexpiratoryPressureobs: { $sum: { $cond: [ {$or: [{$eq: ["$expiratoryPressure", null]}, {$eq: ["$obs", null]}]}, 0, { $multiply: ["$expiratoryPressure", "$obs"] } ] } }, } }, // 后续$addFields、$project、$merge逻辑保持不变即可 ],{allowDiskUse: true})
该优化可减少30%以上的内存占用和数据传输开销,分组速度会明显提升。
4. 可选进阶优化
- 如果MongoDB版本>=5.0,可将measures集合改为时间序列集合,针对按时间查询的场景性能可再提升50%以上
- 提前按天做预聚合生成中间汇总表,查询128天数据时直接从中间表汇总,耗时可降到秒级
- 确认实例内存足够容纳索引大小,避免索引频繁换入换出磁盘拖慢性能
按照以上方案调整后,整体查询耗时完全可以降到4分钟以内,甚至优于SQL Server的性能表现。
内容的提问来源于stack exchange,提问作者Su4p
相关产品推荐
相关产品推荐

