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

Dev*_*ium 5 mongoose mongodb node.js mongodb-query aggregation-framework

我对sql查询相当满意,但似乎无法集中精力进行mongo db文档的分组和求和,

考虑到一点,我有一个具有以下模式的工作模型:

    {
        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
        }
    }
Run Code Online (Sandbox Code Playgroud)

我想要一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);
        }
    );
Run Code Online (Sandbox Code Playgroud)

有人可以指导我正确的方向。先感谢您

Bla*_*ven 5

底漆


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

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

您的文档由顶层的“ all_service”数组组成。大概这里经常有“多个”条目,所有条目都包含“ price”属性以及“ 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 }
            ]
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

变成这个:

{ "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 } } }
Run Code Online (Sandbox Code Playgroud)

这样做的操作是$unwind,并且由于存在多个数组,$unwind因此在继续任何处理之前,您需要同时使用两个数组:

db.collection.aggregate([
    { "$unwind": "$b" },
    { "$unwind": "$b.d" }
])
Run Code Online (Sandbox Code Playgroud)

因此,“ $ b”中的“管道”第一个数组如下所示:

{ "a" : 1, "b" : { "c" : 1, "d" : [ { "e" : 1 }, { "e" : 2 } ] } }
{ "a" : 1, "b" : { "c" : 2, "d" : [ { "e" : 1 }, { "e" : 2 } ] } }
Run Code Online (Sandbox Code Playgroud)

这留下了由“ $ bd”引用的第二个数组,以进一步“归一化”为最终的“未归一化”结果。这允许其他操作进行处理。

解决


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

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

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

因此$match,在“管道”的开头,您不仅要“唯一”,而且在处理完$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) {

    }
)
Run Code Online (Sandbox Code Playgroud)

另外,自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) {

    }
)
Run Code Online (Sandbox Code Playgroud)

这样可以“递归”遍历文档并测试条件,从而有效地删除甚至“不匹配”的所有数组元素$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) {

    }
)
Run Code Online (Sandbox Code Playgroud)

与相比,该过程“大幅”减少了两个数组中项目的大小$redact。的$map操作者处理中“中的”阵列给定语句的每个elment。在这种情况下,每个“外部”数组$map元素将发送到另一个数组,以处理“内部”元素。

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

$setDifference是用来过滤下来的任何false返回的值。或者像在“外部”情况下一样,false从“内部”过滤掉所有值而导致的任何“空白”数组,那里没有匹配项。这样就只剩下匹配项,并封装在“双”数组中,例如:

[[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]]
Run Code Online (Sandbox Code Playgroud)

由于“所有”数组元素_id默认情况下都带有猫鼬(这是您选择保留的一个很好的理由),因此每个项目都是“不同的”,并且不受“设置”运算符的影响,除了删除不匹配的值。

处理$unwind“两次”以将它们转换为适合自己聚合的普通对象。

这些就是您需要知道的事情。正如我之前所说,要“意识到”数据如何“去规范化”以及对您的最终目标意味着什么。