【问题标题】:Mongoose aggregation "$sum" of rows in sub document子文档中行的 Mongoose 聚合“$sum”
【发布时间】: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 系统中的管道。将每个运算符视为管道中的一个“阶段”,它转换/减少输入并将新输出发送到下一个阶段。

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


【解决方案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 阶段会让你到达你需要的地方。祝你好运!

    【讨论】:

    猜你喜欢
    • 2020-08-02
    • 2015-05-29
    • 2016-02-17
    • 2021-07-04
    • 1970-01-01
    • 2019-10-05
    • 2021-06-18
    • 1970-01-01
    • 2020-12-05
    相关资源
    最近更新 更多