Mongoose聚合" $ sum"子文档中的行数

时间:2015-07-15 17:22:36

标签: node.js mongodb mongoose mongodb-query aggregation-framework

我对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);
        }
    );

有人可以引导我朝着正确的方向前进。提前谢谢你

2 个答案:

答案 0 :(得分:4)

引物

正如前面正确提到的,它确实有助于考虑聚合&#34;管道&#34;就像&#34;管道&#34;来自Unix和其他系统shell的|运算符。一个&#34;阶段&#34;将输入提供给&#34; next&#34;阶段等等。

你需要注意的事情是你有&#34;嵌套&#34;数组,另一个数组,如果你不小心,这可能会对你的预期结果产生巨大的差异。

您的文件包含&#34; all_service&#34;顶层的数组。据推测,经常有多个&#34;这里的条目,都包含您的&#34;价格&#34;属性以及&#34; all_sub_item&#34;。然后当然&#34; all_sub_item&#34;是一个数组本身,也包含它自己的许多项目。

您可以将这些数组视为&#34;关系&#34;在SQL中的表之间,在每种情况下都是&#34;一对多&#34;。但数据是在预先加入的#34;表单,您可以在其中一次获取所有数据而不执行连接。你应该已经熟悉了这么多。

然而,当你想&#34;聚合&#34;在文件中,你需要&#34;去标准化&#34;这与SQL中的#34;定义&#34;大致相同。 &#34;加入&#34;。这是&#34;转换&#34;数据进入非标准化状态,适合聚合。

因此同样的可视化应用。主文档的条目由子文档的数量复制,并且&#34; join&#34;到一个内心的孩子&#34;将复制主人和初始儿童&#34;因此。在&#34;坚果壳&#34;中,这个:

{
    "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" }
])

那么&#34;管道&#34;来自&#34; $ b&#34;的第一个数组像这样:

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

留下第二个数组引用&#34; $ b.d&#34;进一步去标准化为最终的非标准化结果&#34;没有任何数组&#34;。这允许其他操作进行处理。

求解

只需&#34;每一个&#34;聚合管道,&#34;第一&#34;你想要做的是&#34;过滤&#34;文档只包含那些包含结果的文档。这是一个好主意,特别是在执行$unwind等操作时,您不希望在与您的目标数据不匹配的文档上执行此操作。

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

当然,&#34;整体&#34;文档仍然返回,因为这是你真正要求的。数据已经加入&#34;而且我们还没有要求&#34;取消加入&#34;无论如何。你只是看着这个&#34;第一个&#34;文档选择确实如此,但是当&#34;反标准化&#34;时,每个数组元素现在实际上代表一个&#34;文档&#34;本身。

所以不是&#34;只有&#34;在管道&#34;的开始处$match,您在处理了#34;所有&#34;之后$match $unwind语句,低至您希望匹配的元素级别。

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运算符。在这种情况下,这可以用于&#34;预过滤&#34;使用$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) {

    }
)

那可以&#34;递归&#34;遍历文档并测试条件,有效地删除任何未匹配的&#34;偶数$unwind之前的数组元素。这可以加快速度,因为不匹配的项目不需要“不受伤”#34;然而,有一个&#34; catch&#34;如果由于某种原因,&#34;所有者&#34;根本不存在于数组元素上,那么这里所需的逻辑将计为另一个&#34;匹配&#34;。您可以再次$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) {

    }
)

这个过程减少了两个阵列中项目的大小,并大大减少了#34;与$redact相比。 $map运算符将数组的每个元素处理到&#34; in&#34;中的给定语句。在这种情况下,每个&#34;外部&#34; array elment被发送到另一个$map来处理&#34; inner&#34;元件。

这里使用$cond进行逻辑测试,如果&#34;条件&#34;然后才会遇到&#34;内部&#34;返回array elment,否则返回false值。

$setDifference用于过滤掉返回的所有false值。或者像在&#34;外部&#34; case,any&#34; blank&#34;所有false值产生的数组都是从&#34;内部&#34;那里没有匹配的地方。这只留下匹配的项目,包含在&#34; double&#34;数组,例如:

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

As&#34; all&#34;默认情况下,数组元素有一个_id和mongoose(这是你保留它的一个很好的理由)然后每个项目都是&#34; distinct&#34;并且不受&#34; set&#34;运算符,除了删除不匹配的值。

处理$unwind&#34;两次&#34;将它们转换为自己文档中的普通对象,适合聚合。

所以这些是你需要知道的事情。正如我之前所说,是&#34;意识到&#34;数据&#34;去标准化&#34;以及对你的最终总数意味着什么。

答案 1 :(得分:2)

听起来你想在SQL等价物中做"sum (prices) WHERE owner IS NOT NULL"

根据这个假设,你首先要做你的$ match,把输入设置减少到你的总和。所以你的第一阶段应该是

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

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

现在,因为你要汇总一个数组,你必须再做一步。聚合运算符在文档上工作 - 实际上没有一种方法可以对数组求和。因此,我们希望扩展您的数组,以便在其自己的文档中拉出数组中的每个元素以将数组字段表示为值。将此视为交叉连接。这将是$unwind

$unwind: { "$all_service.all_sub_items" }

现在你刚刚制作了更多的文档,但是我们可以将它们相加。现在我们可以执行$ group了。在$ group中,指定转换。这一行:

_id: {}, // not sure what to put here

正在输出文档中创建一个字段,该字段与输入文档的文档不同。所以你可以在这里做任何你想要的东西,但是把它当作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阶段将获得你需要的位置。祝你好运!