子文档中行的 Mongoose 聚合“$sum”

Posted

技术标签:

【中文标题】子文档中行的 Mongoose 聚合“$sum”【英文标题】:Mongoose aggregation "$sum" of rows in sub document 【发布时间】:2015-10-04 21:14:00 【问题描述】:

我对 sql 查询相当擅长,但我似乎无法理解分组和获取 mongo db 文档的总和,

考虑到this,我有一个工作模型,其架构如下:

    
        name: 
            type: String,
            required: true
        ,
        info: String,
        active: 
            type: Boolean,
            default: true
        ,
        all_service: [

            price: 
                type: Number,
                min: 0,
                required: true
            ,
            all_sub_item: [
                name: String,
                price: // << -- this is the price I want to calculate
                    type: Number,
                    min: 0
                ,
                owner: 
                    user_id:   //  <<-- here is the filter I want to put
                        type: Schema.Types.ObjectId,
                        required: true
                    ,
                    name: String,
                    ...
                
            ]

        ],
        date_create: 
            type: Date,
            default : Date.now
        ,
        date_update: 
            type: Date,
            default : Date.now
        
    

我想要price 列的总和,其中存在owner,我在下面尝试但没有运气

 Job.aggregate(
        [
            
                $group: 
                    _id: , // not sure what to put here
                    amount:  $sum: '$all_service.all_sub_item.price' 
                ,
                $match: 'not sure how to limit the user': given_user_id
            
        ],
        // $project:  _id: 1, expense: 1 , // you can only project fields from 'group'
        function(err, summary) 
            console.log(err);
            console.log(summary);
        
    );

谁能指引我正确的方向。提前谢谢你

【问题讨论】:

聚合有点像 *nix 系统中的管道。将每个运算符视为管道中的一个“阶段”,它转换/减少输入并将新输出发送到下一个阶段。 【参考方案1】:

入门


正如前面正确指出的,将聚合“管道”视为 Unix 和其他系统 shell 中的“管道”| 运算符确实会有所帮助。一个“阶段”将输入提供给“下一个”阶段,依此类推。

您需要注意的是,您有“嵌套”数组,一个数组位于另一个数组中,如果您不小心,这可能会对您的预期结果产生巨大影响。

您的文档由顶层的“all_service”数组组成。大概这里经常有“多个”条目,所有条目都包含您的“价格”属性以及“all_sub_item”。那么当然“all_sub_item”本身就是一个数组,也包含它自己的许多项目。

您可以将这些数组视为 SQL 中表之间的“关系”,在每种情况下都是“一对多”。但数据采用“预连接”形式,您可以在其中一次获取所有数据而无需执行连接。你应该已经很熟悉了。

但是,当您想“聚合”跨文档时,您需要通过“定义”“连接”以与 SQL 中相同的方式“去规范化”它。这是为了将数据“转换”为适合聚合的非规范化状态。

所以同样的可视化也适用。主文档的条目由子文档的数量复制,并且“连接”到“内部子”将相应地复制主“子”和初始“子”。简而言之,这是:


    "a": 1,
    "b": [
         
            "c": 1,
            "d": [
                 "e": 1 ,  "e": 2 
            ]
        ,
         
            "c": 2,
            "d": [
                 "e": 1 ,  "e": 2 
            ]
        
    ]

变成这样:

 "a" : 1, "b" :  "c" : 1, "d" :  "e" : 1   
 "a" : 1, "b" :  "c" : 1, "d" :  "e" : 2   
 "a" : 1, "b" :  "c" : 2, "d" :  "e" : 1   
 "a" : 1, "b" :  "c" : 2, "d" :  "e" : 2   

执行此操作的操作是$unwind,并且由于有多个数组,因此您需要在继续任何处理之前将它们都$unwind

db.collection.aggregate([
     "$unwind": "$b" ,
     "$unwind": "$b.d" 
])

所以“$b”中的“管道”第一个数组就像这样:

 "a" : 1, "b" :  "c" : 1, "d" : [  "e" : 1 ,  "e" : 2  ]  
 "a" : 1, "b" :  "c" : 2, "d" : [  "e" : 1 ,  "e" : 2  ]  

这使得“$b.d”引用的第二个数组被进一步去规范化为“没有任何数组”的最终去规范化结果。这允许处理其他操作。

解决


对于几乎“每个”聚合管道,您要做的“第一件事”是将文档“过滤”为仅包含您的结果的文档。这是一个好主意,尤其是在执行$unwind 之类的操作时,您不希望在甚至与目标数据不匹配的文档上执行此操作。

所以你需要在数组深度匹配你的“user_id”。但这只是获得结果的一部分,因为您应该知道在查询文档以查找数组中的匹配值时会发生什么。

当然,仍然返回“整个”文档,因为这是您真正要求的。数据已经“加入”,我们没有要求以任何方式“取消加入”它。您可以将其视为“第一个”文档选择,但是当“去规范化”时,每个数组元素现在实际上代表了一个“文档”本身。

因此,您不仅在“管道”的开头“仅”$match,在处理“所有”$unwind 语句之后,您还 $match,直到您希望匹配的元素级别.

Job.aggregate(
    [
        // Match to filter possible "documents"
         "$match":  
            "all_service.all_sub_item.owner": given_user_id
        ,

        // De-normalize arrays
         "$unwind": "$all_service" ,
         "$unwind": "$all_service.all_subitem" ,

        // Match again to filter the array elements
         "$match":  
            "all_service.all_sub_item.owner": given_user_id
        ,

        // Group on the "_id" for the "key" you want, or "null" for all
         "$group": 
            "_id": null,
            "total":  "$sum": "$all_service.all_sub_item.price" 
        

    ],
    function(err,results) 

    
)

另外,自 2.6 以来的现代 MongoDB 版本也支持 $redact 运算符。在这种情况下,这可以用于在使用$unwind 处理之前“预过滤”数组内容:

Job.aggregate(
    [
        // Match to filter possible "documents"
         "$match":  
            "all_service.all_sub_item.owner": given_user_id
        ,

        // Filter arrays for matches in document
         "$redact": 
            "$cond": 
                "if":  
                    "$eq": [ 
                         "$ifNull": [ "$owner", given_user_id ] ,
                        given_user_id
                    ]
                ,
                "then": "$$DESCEND",
                "else": "$$PRUNE"
            
        ,

        // De-normalize arrays
         "$unwind": "$all_service" ,
         "$unwind": "$all_service.all_subitem" ,

        // Group on the "_id" for the "key" you want, or "null" for all
         "$group": 
            "_id": null,
            "total":  "$sum": "$all_service.all_sub_item.price" 
        

    ],
    function(err,results) 

    
)

这可以“递归地”遍历文档并测试条件,甚至在$unwind 之前有效地删除任何“不匹配”的数组元素。这可以加快速度,因为不匹配的项目不需要“解开”。但是有一个“问题”,如果由于某种原因“所有者”根本不存在于数组元素上,那么这里所需的逻辑会将其视为另一个“匹配”。您可以随时再次$match 确定,但仍有更有效的方法可以做到这一点:

Job.aggregate(
    [
        // Match to filter possible "documents"
         "$match":  
            "all_service.all_sub_item.owner": given_user_id
        ,

        // Filter arrays for matches in document
         "$project": 
            "all_items": 
              "$setDifference": [
                 "$map": 
                  "input": "$all_service",
                  "as": "A",
                  "in": 
                    "$setDifference": [
                       "$map": 
                        "input": "$$A.all_sub_item",
                        "as": "B",
                        "in": 
                          "$cond": 
                            "if":  "$eq": [ "$$B.owner", given_user_id ] ,
                            "then": "$$B",
                            "else": false
                          
                        
                      ,
                      false
                    ]          
                  
                ,
                [[]]
              ]
            
        ,


        // De-normalize the "two" level array. "Double" $unwind
         "$unwind": "$all_items" ,
         "$unwind": "$all_items" ,

        // Group on the "_id" for the "key" you want, or "null" for all
         "$group": 
            "_id": null,
            "total":  "$sum": "$all_items.price" 
        

    ],
    function(err,results) 

    
)

$redact 相比,该过程“大幅”减少了两个数组中项目的大小。 $map 运算符将数组的每个元素处理为“in”中的给定语句。在这种情况下,每个“外部”数组元素都被发送到另一个 $map 以处理“内部”元素。

这里使用$cond 执行逻辑测试,如果满足“条件”,则返回“内部”数组元素,否则返回false 值。

$setDifference 用于过滤返回的任何false 值。或者在“外部”情况下,任何由所有false 值产生的“空白”数组都从没有匹配的“内部”中过滤出来。这仅留下匹配的项目,封装在“双”数组中,例如:

[[ "_id": 1, "price": 1, "owner": "b" ,..],[..,..]]

由于“所有”数组元素默认有一个 _id 和猫鼬(这是你保留它的一个很好的理由),所以每个项目都是“不同的”并且不受“设置”运算符的影响,除了删除不匹配的值。

处理$unwind“两次”将这些转换为它们自己文档中的普通对象,适合聚合。

所以这些是你需要知道的。正如我之前所说,要“了解”数据是如何“去规范化”的,以及这对您的最终总数意味着什么。

【讨论】:

【参考方案2】:

听起来您想在 SQL 等效项中执行 "sum (prices) WHERE owner IS NOT NULL"

在这种假设下,您需要先进行 $match,以将输入集减少到您的总和。所以你的第一阶段应该是这样的

$match: all_service.all_sub_items.owner : $exists: true

将此视为将所有匹配的文档传递到您的第二阶段。

现在,因为您正在对数组求和,所以您必须执行另一个步骤。聚合运算符适用于 documents - 没有真正的方法来对数组求和。因此,我们希望扩展您的数组,以便将数组中的每个元素拉出,以在其自己的文档中将数组字段表示为一个值。将此视为交叉连接。这将是$unwind。

$unwind: "$all_service.all_sub_items"

现在您已经制作了更多的文档,但是我们可以对它们进行汇总。现在我们可以执行 $group 了。在您的 $group 中,您指定一个转换。行:

_id: , // not sure what to put here

正在输出文档中创建一个字段,该字段与输入文档不同。因此,您可以在此处随意设置 _id,但可以将其视为等同于 sql 中的“GROUP BY”。 $sum 运算符本质上将为您在此处创建的与该 _id 匹配的每组文档创建一个总和 - 所以本质上我们将通过使用 $group 来“重新折叠”您刚刚对 $unwind 所做的事情。但这将允许 $sum 工作。

我认为您正在寻找仅对您的主文档 ID 进行分组,所以我认为您的问题中的 $sum 语句是正确的。

$group : _id : $_id, totalAmount : $sum : '$all_service.all_sub_item.price'

这将输出具有与您的原始文档 ID 和您的总和等效的 _id 字段的文档。

我会让你把它放在一起,我对节点不是很熟悉。你很接近,但我认为将你的 $match 移到前面并使用 $unwind 阶段会让你到达你需要的地方。祝你好运!

【讨论】:

发现另一个问题有一些 $unwind 例子,你可以看看:***.com/questions/12162681/…

以上是关于子文档中行的 Mongoose 聚合“$sum”的主要内容,如果未能解决你的问题,请参考以下文章

使用聚合 mongodb mongoose 将集合子子文档与其他集合子文档连接起来

Mongoose — 使用聚合创建具有 $sum 的新属性

mongoDB,带有 $sum 和 $count 的 mongoose 聚合查询

mongoDB,带有 $sum 和 $count 的 mongoose 聚合查询

带有全文搜索和项目的 Mongoose 子字段聚合

用于算术运算的 MongoDB 聚合 - 子文档字段