MongoDB 嵌套 $group 和 sum

hop*_*sey 0 mapreduce mongodb mongodb-query aggregation-framework

我是 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]
Run Code Online (Sandbox Code Playgroud)

我期望对按以下分组的输出求和

日期 -> 父 ID -> 状态

所以那就是

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

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

{
    '$group': {
        '_id': {
            'date': '$date',
            'status': '$status',
            'parentId': '$parentId'
        },
        'total': {
            '$sum': 1
        }
    }
} 
Run Code Online (Sandbox Code Playgroud)

Bla*_*ven 5

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

因此,这里真正需要的是多级分组,这很简单,只需将一个$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"
        }}
    }}
])
Run Code Online (Sandbox Code Playgroud)

在遵循初始组以按照最精细的细节级别进行累积之后,这将逐步将数据嵌套到每个“日期”键下的数组中。结果基本上是通过以下方式压缩到数组中,将结构“卷起”到每个键的单个文档中$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 }
                ]
            }
        ]
    }
]
Run Code Online (Sandbox Code Playgroud)

或者,如果您可以忍受它,那么您可以将所有相关子数据放在单个数组中而不是嵌套数组中,甚至可以更加平坦:

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

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

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

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

聚合框架不支持(非常故意)尝试以任何方式从数据中提取键,并且大多数 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);
});
Run Code Online (Sandbox Code Playgroud)

它基本上按照您的结构生成输出,但当然作为单独的文档,如下文所述:

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

或者,您可以使用 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 } }
);
Run Code Online (Sandbox Code Playgroud)

结果大致相同,但使用特定的输出格式时,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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,特别是如果您不熟悉 mapReduce 的工作原理,那么有一个非常重要的原因可以解释为什么在映射器和化简器之间一致地发出和遍历结构,并对发出的状态值求和而不是简单地递增。这是mapReduce 的一个属性,其中来自reducer 的输出最终可以再次通过reducer 返回,直到达到单个结果。

另外,正如前面提到的,以及您自己所说的“新事物”的一个重要警告是,您真的永远不想将结果压缩到单个对象中以进行响应,如您的问题所示。

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

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


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