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

8

我对SQL查询相当熟悉,但是似乎无法理解如何对MongoDB文档进行分组和求和。

考虑到这个问题,我有一个具有以下架构的作业模型:

    {
        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
        }
    }

我想要将owner列中存在的数值相加得到price列的总和,我尝试了下面的方法但没有成功。
 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);
        }
    );

能有人指导我朝正确的方向前进吗?谢谢您提前。


1
聚合类似于*nix系统中的管道。将每个运算符视为管道中的“阶段”,它会转换/减少输入并将新输出发送到下一个阶段。 - womp
2个回答

11

基础知识


正如之前正确提到的那样,将聚合“管道”视为Unix和其他系统shell中的“pipe”|运算符会有所帮助。一个“阶段”将输入传递给“下一个”阶段,以此类推。

你需要注意的是,你有“嵌套”的数组,一个数组包含另一个数组,如果不小心处理,这可能会对期望得到的结果产生重大影响。

你的文档由顶层的“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 }
            ]
        }
    ]
}

变成这样:

{ "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"中提取出第一个数组"pipe":
{ "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”。但这只是获得结果的一部分,因为你应该了解当你查询数组中的匹配值时会发生什么。

当然,“整个”文档仍然会被返回,因为这才是你真正请求的内容。数据已经“连接”起来了,我们没有以任何方式“取消连接”。你可以像“首先”选择文档一样看待这一点,但是当“去规范化”时,每个数组元素现在实际上代表着一个“文档”本身。

所以,你不仅要在“pipeline”开头进行$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操作符将每个数组元素发送到另一个$map以处理“内部”元素。在这种情况下,每个“外部”数组元素都被发送到另一个$map中以处理“内部”元素。
使用$cond进行逻辑测试,如果满足“条件”,则返回“内部”数组元素,否则返回false值。
使用$setDifference来过滤掉任何返回的false值。或者在“外部”情况下,从所有从“内部”中过滤掉所有false值得到的“空白”数组中过滤掉任何“空白”数组,这里没有匹配。这只留下匹配项,包含在“双重”数组中,例如:
[[{ "_id": 1, "price": 1, "owner": "b" },{..}],[{..},{..}]]
由于mongoose默认情况下“所有”数组元素都有一个_id(这也是保留它的一个很好的理由),因此每个项都是“不同的”,并且不受“set”运算符的影响,除了删除不匹配的值。

使用$unwind“两次”将其转换为自己的文档中的普通对象,适合聚合。

所以这些就是你需要知道的事情。如我之前所述,请“注意”数据“去规范化”的方式以及这对最终总数的影响。


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: {},// 不确定要放什么在这里

这是在输出文档中创建一个字段,它不是输入文档。因此,您可以将_id设置为任何您想要的内容,但请将其视为sql中“GROUP BY”的等效项。$sum运算符将为您在此处创建的每个与该_id匹配的文档组创建一个总和 - 因此,我们将使用$group“重新折叠”刚才用$unwind完成的操作。但是这将允许$sum工作。

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

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

这将输出一个具有等同于原始文档ID的_id字段和您的总和的文档。

我会让您自己组合,我对node不是特别熟悉。您已经接近了,但我认为将$match移到前面并使用$unwind阶段将使您达到所需的位置。祝好运!


发现另一个带有一些$unwind示例的问题,你可以查看一下:https://dev59.com/4Gct5IYBdhLWcg3wV8Ck - womp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接