大数据查询mongodb、聚合、单索引或复合索引

Posted

技术标签:

【中文标题】大数据查询mongodb、聚合、单索引或复合索引【英文标题】:big data query mongodb, aggregation, single index or compound index 【发布时间】:2020-05-13 15:24:39 【问题描述】:

我正在尝试加快对包含超过 1000 万个文档的集合执行的查询。文档示例如下所示

    
        nMove: 2041242,
        typeMove: 'Sold',
        date: "2016-05-18T16:00:00Z",
        operation: 'output',
        origin: 
            id: '3234fds32fds42',
            name: 'Main storage',
        ,
        products: [
           id: '342fmdsff23324432',
           name: 'Product 1',
           price: 34,
           quantity: 9
        ],
    

现在我必须查询与给定 'product.id' 或 'origin.id' 或两者都匹配的所有文档,并 $sum product.quantity 的总数量。

所以我正在执行这样的查询。

 movesModel.aggregate([
    
        $match: 
            $expr: 
                $and: [
                     $in: [req.params.idProduct, '$product.id'] ,
                     $eq: ['$origin.id', req.params.idOrigin ] ,
                ]
            
        
    ,
    
        $project: 
            _id: 0,
            outputs: 
                $sum: 
                    $cond:  if:  $eq: ['$operation', 'input'] , then: '$product.quantity', else: 0 
                
            ,
            inputs: 
                $sum: 
                    $cond:  if:  $eq: ['$operation', 'output'] , then: '$product.quantity', else: 0 
                
            
        
    ,
    
        $group: 
            _id: '$_id',
            inputs:  $sum: '$inputs' ,
            outputs:  $sum: '$outputs' 
        
    ,

]).then((result) => 
    res.json(result)
)

这个查询大约需要 1 分钟才能解决...有时这个查询 $match 与超过 200k 的文档...考虑到我不需要全部数据,我只需要数量的总和.. . 我有一些问题...(我是 mongodb 菜鸟)

    关于索引.. 我创建了一个复合索引 db.moves.createIndex( 'origin.id': 1, 'product.id':1)。这是正确的吗?我应该改变它吗?

    我的查询可以吗?我可以改进吗?

    为了防止查询与 200k 文档匹配...我做了一些棘手的事情。我添加了一个名为“日期”的字段,我想获取与“origin.id”、“product.id”匹配且为 $gte: date 的所有文档,但它需要相同的时间......甚至当它只匹配 1 个文档时...

    完成...我认为,我遇到的所有问题都与索引有关...所以我尝试检查我的 indexStats...但它似乎不适用于我的聚合查询。

感谢任何帮助。谢谢

///////////完整的管道///////////

在这种情况下,我还有两个名为“存储”和“库存”的集合

//storage examples
    
     _id: '3234fds32fds42'
     name: 'Main storage'
     status: true
    
    
    _id: '32f32f32432sda'
    name: 'Other storage'
    status: true
    

//invetories examples

    _id: 'fvavcsa3a3aa3'
    date: '2020-01-01'
    storage: 
             _id: '3234fds32fds42'
            name: 'Main storage'
             
    products: [
               id: '342fmdsff23324432',
               name: 'Product 1',
            ],

所以这就是我使用 $lookup 的原因,我真正需要的是获得与每个存储和产品匹配的所有动作。

//我还添加了清单以按日期过滤并防止匹配大量文档

所以这是我的查询。

    storagesModel.aggregation([
     
       $match:  status: true 
     ,
     
            $lookup: 
                from: 'inventories',
                as: 'inventory',
                let:  "idStorage": "$_id" ,
                pipeline: [
                    
                        $match: 
                            $expr: 
                                $and: [
                                     $eq: ['$storage._id',  $toString: "$$idStorage" ] ,
                                     $in: [req.params.id, '$products._id'] 
                                ]
                            
                        ,
                    ,
                    
                        $sort:  date: -1  // TO TAKE THE LAST INVENTORY
                    ,
                    
                        $limit: 1
                    

                ]
            
     ,
      $unwind:  path: '$inventories', preserveNullAndEmptyArrays: true  , //DECONSTRUCT THE ARRAY AND GET IT AS OBJECT
     
        $lookup: 
              from: 'moves',
              as: 'moves',
              let:  
              "idStorage": "$_id",
              'date': '$inventory.date',
              pipeline: [
                         
                          $match: 
                                 $expr: 
                                       $and: [
                                           $gte: ['$date', $$date]  
                                           $eq: ['$origin.id', '$$idStorage' ] ,
                                           $in: [req.params.idProduct, '$product.id'] ,                                         
                                             ]
                                        
                                  
                          ,
                          
                            $project: 
                                     _id: 0,
                                     outputs: 
                                            $sum: 
                                                 $cond:  if:  $eq: ['$operation', 'input'] , then: '$product.quantity', else: 0 
                                                   
                                               ,
                                     inputs: 
                                            $sum: 
                                                 $cond:  if:  $eq: ['$operation', 'output'] , then: '$product.quantity', else: 0 
                                                   
                                             
                                        
                         ,
                         
                          $group: 
                          _id: '$_id',
                          inputs:  $sum: '$inputs' ,
                          outputs:  $sum: '$outputs' 
                          
                   ,
             ]
        
    ])

总结...

我需要得到的是产品在每个存储中的“移动”总数。 考虑到也许有一个“清单”可以给你一个日期来防止匹配很多文件。这就是为什么我使用 storageModel 并使用 $lookup 阶段来获取最后一个库存,所以我有 $$date 和 $$idStorage。然后我使用'moves'集合中的$lookup...我知道这是一个繁重的查询但是...我认为给出一个日期并使用适当的复合索引应该很快...但即使我尝试获取只有很少“动作”的产品的“动作”……大约需要 20 或 30 秒……

我尝试在不使用 $lookup 的情况下执行单个查询,甚至匹配 400k 个文档也需要 1-2 秒...

你在想什么?感谢您的帮助

【问题讨论】:

【参考方案1】:

总的来说你做得很好,让我们回顾一下你的观点并分别讨论。

    是的,这太棒了,复合索引正是这条管道所需要的。我相信您在选择创建复合索引之前已经阅读过有关复合索引的内容,因此我不会深入探讨为什么这个索引是最优的,因为它很简单。

    让我们把这个留到最后。

    这很好,如果您不关心整个数据样本而只关心最近的数据,这是可行的方法,现在为了正确利用该字段来提高性能,您应该转储旧索引我们在第 1 部分中讨论过,并创建了一个新的复合索引来包含该字段,date: -1, 'origin.id': 1, 'product.id': 1 请注意,我们为日期选择了一个递减索引,因为我们想要最新的数据。这样会更有效率。

由于您必须阅读复合索引字段顺序很重要,因此请随意更改此顺序以匹配您最常进行的查询。

    然而,Mongo 生成索引树的方式不太可能不稳定,我的意思是,如果您很久以前创建了索引,并且从那时起有更多数据进入,您可能会从删除和重新构建索引中受益。话虽如此,我不建议这样做,因为我觉得在您的情况下任何改进都会有些小。

    (2) 回到您的查询,首先我想问两件事: (a) 您在匹配查询中使用了$and,但根据您的措辞描述,$or 逻辑似乎更合适。这是一个快速更改,如果需要,您可以这样做。 (b) 同样,我不确定这是否是一个错误,但您似乎已将 input 切换为“输出”,反之亦然。如果是这种情况,您应该切换它们。

话虽如此,我将如何重写这个查询(剧透没有太大变化):

movesModel.aggregate([
     // notice i'm using Mongo's dot notation, $expr is also fine. not sure if there's an efficiency difference
        $match: 
            $and: [
                
                    $or: [
                        
                            "product.id": req.params.idProduct
                        ,
                        
                            "origin.id": req.params.idOrigin
                        
                    ]
                ,
                
                    date: $gt: new Date("2020-01-01")
                
            ]
        
    ,
      // there's no need for the project stage as we can just nest the condition into the $group, again this should not case
        // performance changes. also i switched the input to match with the inputs.
        $group: 
            _id: '$_id',
            inputs: $sum: $cond: if: $eq: ['$operation', 'input'], then: '$product.quantity', else: 0,
            outputs: $sum: $cond: if: $eq: ['$operation', 'output'], then: '$product.quantity', else: 0
        
    ,
])

因此,回顾一下您的管道是最理想的,您怀疑问题与索引相关是有些正确的。从第 3 部分构建新索引后,性能将发生相当大的变化。

需要考虑的是规模上升,您的数据库将(希望)继续增长。您当前的解决方案目前还不错,但最终它会在规模下塌陷,性能将再次下降。想到的 2 个简单选项是:

    预处理,您所做的每次更新或插入都有一个预先计算的集合,该集合将使用这些操作进行更新并保存所需的指标。

    创建一个“当前”集合以仅包含最近的数据并查询该数据。

这两种方法显然都会产生一些开销,您可以选择是否以及何时实施它们。

【讨论】:

我已经改变了我的索引,但是它一直很慢......关于使用 $project,我确实使用这个阶段来防止接收一个具有 200k 结果的数组,但我认为没关系,它确实不影响...我改变并显着改进的是 $match 查询中的顺序。那可能吗?我与插入索引的顺序相同,感谢您的快速回答,我真的很感激 听起来你的管道可能选择了错误的索引,你可以使用 $hint 来指定它应该使用哪个索引或删除它的另一个冗余索引 我添加了 aggregation([]).options(hint:) 因为 $hint as stage 给我一个错误,我越来越 maddddd,我也想告诉你这是一个我的查询的基本示例...这个查询有一些 $lookup 里面...我不想粘贴所有因为可能没有人会理解我假装在做什么^^,我也不太擅长用英语解释它,你能告诉我如何在我的 $lookup 管道中添加这个 $hint?我没有找到任何例子 我不知道我在查询中更改了什么,但现在它工作得更快,我只是删除了索引并再次创建它,并在两个查询中简化了查询,现在我正在使用没有 $lookup 阶段的聚合。我认为这就是重点......再次感谢! $lookup 是一个昂贵的阶段,所以这是有道理的,如果你想发布你的完整管道,我很乐意看看它。

以上是关于大数据查询mongodb、聚合、单索引或复合索引的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB 复合索引与单字段索引在空间消耗方面的对比

MongoDB 一个复合索引与多个单字段索引

MongoDB OR与正则表达式不使用复合索引

Mongodb 聚合管道优化 - $match 的 2 阶段

MongoDB基础

MongoDB索引问题