【问题标题】:MongoDB nested $group and sumMongoDB 嵌套 $group 和 sum
【发布时间】:2023-03-17 02:00:01
【问题描述】:

我是 MongoDB 新手,如果我错过了文档中的某些内容,请原谅我。 我有这样的收藏

[date: "2015-12-01", status: "resolved", parentId: 1]
[date: "2015-12-01", status: "resolved", parentId: 2]
[date: "2015-12-01", status: "resolved", parentId: 2]
[date: "2015-12-01", status: "waiting", parentId: 2]
[date: "2015-12-02", status: "resolved", parentId: 1]
[date: "2015-12-02", status: "waiting", parentId: 2]
[date: "2015-12-02", status: "waiting", parentId: 2]
[date: "2015-12-03", status: "resolved", parentId: 1]

我希望将按

分组的输出相加

日期 -> parentId -> 状态

原来如此

{
    "2015-12-01": {
        "1": {
            "resolved": 1
        },
        "2": {
            "resolved": 2,
            "waiting": 1
        }
    }
    "2015-12-02": {
        "1": {
            "resolved": 1
        },
        "2": {
            "waiting": 2
        },
    }
    "2015-12-03": {
        "1": {
            "resolved": 1
        }
    }
}

有什么建议可以实现吗?我已经使用聚合框架得到了这个:

{
    '$group': {
        '_id': {
            'date': '$date',
            'status': '$status',
            'parentId': '$parentId'
        },
        'total': {
            '$sum': 1
        }
    }
} 

【问题讨论】:

  • 你坚持那个确切的结构吗?因为那样你就不能使用聚合框架,而必须使用 MapReduce。
  • 如果你已经有“一些分组”,我假设你正在使用aggregation pipeline。是什么阻止您添加额外的$group
  • 感谢您的 cmets。我刚刚更新了帖子。

标签: mongodb mapreduce mongodb-query aggregation-framework


【解决方案1】:

不喜欢在输出中使用“数据”作为“键”,因为通常最好将“数据”保留为“数据”,并且更符合面向对象的设计模式,其中键在对象之间是一致的,而不是不一致每个结果都不同。毕竟,一开始就有人很明智地以这种方式设计初始数据。

所以你真正需要的只是一个多级分组,这很简单,只需将一个$group 阶段的输出提供给另一个阶段:

db.collection.aggregate([
    { "$group": {
        "_id": {
            "date": "$date",
            "parentId": "$parentId",
            "status": "$status"
        },
        "total": { "$sum": 1 }
    }},
    { "$group": {
        "_id": { 
            "date": "$_id.date",
            "parentId": "$_id.parentId"
        },
        "data": { "$push": {
            "status": "$_id.status",
            "total": "$total"
        }}
    }},
    { "$group": {
        "_id": "$_id.date",
        "parents": { "$push": {
            "parentId": "$_id.parentId",
            "data": "$data"
        }}
    }}
])

这将在跟随初始组之后逐步将数据嵌套到每个“日期”键下的数组中,以按照最精细的细节级别进行累积。结果基本上是通过$push 压缩成数组,将结构“汇总”到每个键的单个文档中:

[
    {
        "_id": "2015-12-01",
        "parents": [
            { 
                "parentId": 1, 
                "data": [
                    { "status": "resolved", "total": 1 }
                ]
            },
            {
                "parentId": 2,
                "data": [
                    { "status": "resolved", "total": 2 },
                    { "status": "waiting", "total": 1 }
                ]
            }
        ]
    },
    {
        "_id": "2015-12-02",
        "parents": [
            { 
                "parentId": 1,
                "data": [ 
                    { "status": "resolved", "total": 1 }
                ]
            },
            {
                "parentId": 2,
                "data": [
                    { "status": "waiting", "total": 2 }
                ]
            }
        ]
    },
    {
        "_id": "2015-12-03",
        "parents": [
            { 
                "parentId": 1,
                "data": [
                    { "status": "resolved", "total": 1 }
                ]
            }
        ]
    }
]

或者,如果你能接受它,那么你可以更扁平地将所有相关子数据放在一个数组中,而不是嵌套一个:

db.collection.aggregate([
    { "$group": {
        "_id": {
            "date": "$date",
            "parentId": "$parentId",
            "status": "$status"
        },
        "total": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.date",
        "data": { "$push": {
            "parentId": "$_id.parentId",
            "status": "$_id.status",
            "total": "$total"
        }}
    }}
])

只有一个子数组,只保留所有数据键:

[
    {
        "_id": "2015-12-01",
        "data": [
            { 
                "parentId": 1, 
                "status": "resolved",
                "total": 1
            },
            {
                "parentId": 2,
                "status": "resolved",
                "total": 2
            },
            { 
                "parentId": 2,
                "status": "waiting",
                "total": 1
            }
        ]
    },
    {
        "_id": "2015-12-02",
        "data": [
            { 
                "parentId": 1,
                "status": "resolved",
                "total": 1
            },
            {
                "parentId": 2,
                "status": "waiting",
                "total": 2 
            }
        ]
    },
    {
        "_id": "2015-12-03",
        "data": [
            { 
                "parentId": 1,
                "status": "resolved",
                "total": 1
            }
        ]
    }
]

这里的主要本质是“事物列表”以数组的形式保存为与它们相关的事物的子项,以任何一种形式,只是程度不同。这可以说比从对象中确定“键”并迭代它们更容易处理,也更合乎逻辑,因为无论如何你基本上都可以迭代一个自然列表。

聚合框架不支持(非常刻意地)尝试以任何方式从数据中按摩键,大多数 MongoDB 查询操作也同意这一理念,因为它对于本质上是“数据库”的东西很有意义。

如果您确实必须作为键进行按摩,建议在检索聚合结果后在客户端处理中执行此操作。您甚至可以在传递到远程客户端时在流处理中执行此操作,但作为一个基本的转换示例:

var out = db.collection.aggregate([
    { "$group": {
        "_id": {
            "date": "$date",
            "parentId": "$parentId",
            "status": "$status"
        },
        "total": { "$sum": 1 }
    }},
    { "$group": {
        "_id": { 
            "date": "$_id.date",
            "parentId": "$_id.parentId"
        },
        "data": { "$push": {
            "status": "$_id.status",
            "total": "$total"
        }}
    }},
    { "$group": {
        "_id": "$_id.date",
        "parents": { "$push": {
            "parentId": "$_id.parentId",
            "data": "$data"
        }}
    }}
]).toArray();

out.forEach(function(doc) {
    var obj = {};
    obj[doc._id] = {};

    doc.parents.forEach(function(parent) {
        obj[doc._id][parent.parentId] = {};
        parent.data.forEach(function(data) {
            obj[doc._id][parent.parentId][data.status] = data.total;
        });
    });

    printjson(obj);
});

这基本上会按照您的结构产生输出,但当然是作为稍后解释的单个文档:

{
    "2015-12-01": {
        "1": {
            "resolved": 1
        },
        "2": {
            "resolved": 2,
            "waiting": 1
        }
    }
},
{ 
    "2015-12-02": {
        "1": {
            "resolved": 1
        },
        "2": {
            "waiting": 2
        },
    }
},
{ 
    "2015-12-03": {
        "1": {
            "resolved": 1
        }
    }
}

或者您可以使用 mapReduce 和基于 JavaScript 的处理在服务器上强制执行此操作,但由于整体效率不如聚合处理有效,因此再次不明智:

db.collection.mapReduce(
    function() {
        var obj = {};
        obj[this.parentId] = {};
        obj[this.parentId][this.status] = 1;
        emit(this.date,obj);
    },
    function(key,values) {
        var result = {};

        values.forEach(function(value) {
            Object.keys(value).forEach(function(parent) {
                if (!result.hasOwnProperty(parent))
                    result[parent] = {};
                Object.keys(parent).forEach(function(status) {
                    if (!result[parent].hasOwnProperty(status))
                        result[parent][status] = 0;
                    result[parent][status] += value[parent][status];
                });
            });
        });

        return result;
    },
    { "out": { "inline": 1 } }
);

几乎相同的结果,但使用特定的输出格式 mapReduce 总是产生:

{
    "_id": "2015-12-01",
    "value": {
        "1": {
            "resolved": 1
        },
        "2": {
            "resolved": 2,
            "waiting": 1
        }
    }
},
{ 
    "_id": "2015-12-02",
    "value": {
        "1": {
            "resolved": 1
        },
        "2": {
            "waiting": 2
        },
    }
},
{ 
    "_id": "2015-12-03",
    "value": {
        "1": {
            "resolved": 1
        }
    }
}

请注意,特别是如果您不熟悉 mapReduce 的工作原理,那么结构在 mapper 和 reducer 之间一致地发出和遍历以及对发出的值求和而不是简单地递增是一个非常重要的原因。这是 mapReduce 的一个属性,reducer 的输出最终会再次通过 reducer,直到得到一个结果。

正如前面提到的,正如您所说的,“新事物” 的一个很大的警告是,您真的从不想要将结果压缩到单个对象中如您的问题所示进行回复。

它不仅是糟糕设计的另一个属性(前面已经介绍过),而且对 MongoDB 和许多合理系统的输出大小也存在现实的“硬性限制”。单个文档的 BSON 大小限制为 16MB,在任何实际情况下尝试这样做时几乎肯定会超过此限制。

此外,“作为列表的列表”是有意义的,试图人为地表示在单个文档对象中使用唯一键是没有意义的。当您为预期目的使用正确的数据结构类型时,事情的处理和流式传输会变得更加容易。


所以这些是处理输出的方法。无论采用何种方法,这实际上只是聚合的基本数据操作。但希望您能看到保持它尽可能高效和简单的常识,因为它可以通过聚合直接处理,并且对于处理接收到的结果的最终代码更有意义。

【讨论】:

  • 非常感谢!这很有意义。可能我会在客户端处理数据并保持结构不变。
  • 也许是关于特定用例的几句话。我愿意展示特定日期、元素和状态的一些统计数据。我认为多维关键数据结构更容易访问,因为它就像我的表示层中的 result[date][id][status]。所以就你提到的“糟糕的设计”而言,它只是关于数据源还是将这样的数据保存在任何地方?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-01-13
  • 1970-01-01
  • 2022-01-17
  • 2019-01-31
  • 1970-01-01
  • 2016-06-17
  • 2013-04-22
相关资源
最近更新 更多