MongoDB嵌套$ group并求和

时间:2015-12-07 16:00:33

标签: mongodb mapreduce 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]

我希望将按

分组的输出相加

日期 - > 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
        }
    }
} 

1 个答案:

答案 0 :(得分:3)

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

所以你真正需要的是一个多级分组,通过获取一个$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,在尝试这样做时几乎肯定会超过任何实际情况。

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

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