MongoDB双计数聚合查询优化:替代$facet实现库存计算
优化方案:替代$facet的高效聚合方法
方法一:单$group阶段完成双计数统计
利用$cond和$sum在一次分组中同时统计发货和退货数量,全程可有效利用索引,性能接近单独查询的总和。
聚合语句:
[ { '$match': { '$or': [ { 'startID': 0, 'toID': 40 }, { 'startID': 40, 'toID': 0 } ] } }, { '$group': { '_id': null, 'deliveries': { '$sum': { '$cond': [ { '$and': [ { '$eq': ['$startID', 0] }, { '$eq': ['$toID', 40] } ] }, 1, 0 ] } }, 'returns': { '$sum': { '$cond': [ { '$and': [ { '$eq': ['$startID', 40] }, { '$eq': ['$toID', 0] } ] }, 1, 0 ] } } } }, { '$addFields': { 'stock': { '$subtract': ['$deliveries', '$returns'] } } }, { '$project': { '_id': 0 } // 可选,隐藏_id字段 } ]
索引优化:
确保已创建复合索引:db.movement.createIndex({startID: 1, toID: 1})
这个索引能完美匹配$match中的两个$or分支,MongoDB会对每个分支使用索引扫描,然后合并结果,避免全表扫描。
方法二:用$unionWith合并两个高效查询
既然单独查询发货和退货都很快(<80ms),可以用$unionWith将两个独立的计数查询结果合并,再计算库存。这种方法的优势是每个子查询都能100%利用各自的索引,总耗时接近两个单独查询的时间之和。
聚合语句:
[ { '$match': { 'startID': 0, 'toID': 40 } }, { '$count': 'deliveries' }, { '$unionWith': { 'coll': 'movement', 'pipeline': [ { '$match': { 'startID': 40, 'toID': 0 } }, { '$count': 'returns' } ] } }, { '$group': { '_id': null, 'deliveries': { '$max': '$deliveries' }, 'returns': { '$max': '$returns' } } }, { '$addFields': { 'stock': { '$subtract': ['$deliveries', '$returns'] } } }, { '$project': { '_id': 0 } } ]
原理:
- 第一个管道统计发货数,第二个管道通过
$unionWith统计退货数,两者结果合并为两条文档 - 用
$group将两条文档的计数合并到一起,$max用来提取非空的计数值(因为每条文档只有一个计数字段) - 最后计算库存值
为什么你的$facet方案性能差?
- 前置$match的$facet方案:
$or条件虽然能用上索引,但MongoDB需要扫描两个索引分支并合并结果,后续$facet还要对已过滤的文档再次进行两次$match过滤,相当于重复处理数据,额外增加了开销。 - 无前置$match的$facet方案:
$facet的每个子管道都会单独扫描全表(因为没有前置过滤),完全无法利用索引,导致性能暴跌。
内容的提问来源于stack exchange,提问作者ratzownal
相关产品推荐
相关产品推荐

