如何从MongoDB中的hashmap获取平均值?

时间:2015-05-18 11:47:03

标签: mongodb mongodb-query aggregation-framework average

我的Mongo数据库中有时间数据。每个文档等于一分钟并包含60秒作为具有每个值的对象。如何在一分钟内获得所有秒的平均值?

看起来像这样的文件:

{
    "_id" : ObjectId("55575e4062771c26ec5f2287"),
    "timestamp" : "2015-05-16T18:12:00.000Z",
    "values" : {
        "0" : "26.17",
        "1" : "26.17",
        "2" : "26.17",
        ...
        "58" : "24.71",
        "59" : "25.20"
    }
}

2 个答案:

答案 0 :(得分:2)

你可以采取两种方法:

  1. 更改架构并使用 aggregation framework 通过$avg运算符获得平均值
  2. 应用 Map-Reduce
  3. 让我们看看第一个选项。目前的情况是,由于values子文档中的动态密钥,模式将无法使用聚合框架。有利于聚合框架的理想模式将使value字段成为包含嵌入式键/值文档的数组,如下所示:

    /* 0 */
    {
        "_id" : ObjectId("5559d66c9bbec0dd0344e4b0"),
        "timestamp" : "2015-05-16T18:12:00.000Z",
        "values" : [ 
            {
                "k" : "0",
                "v" : 26.17
            }, 
            {
                "k" : "1",
                "v" : 26.17
            }, 
            {
                "k" : "2",
                "v" : 26.17
            },
            ...         
            {
                "k" : "58",
                "v" : 24.71
            }, 
            {
                "k" : "59",
                "v" : 25.20
            }
        ]
    }
    

    使用MongoDB 3.6及更新版本,使用聚合框架通过 $objectToArray 运算符将哈希图转换为数组,然后使用 $avg 计算平均值。

    考虑运行以下聚合管道:

    db.test.aggregate([
        {
            "$addFields": {
                "values": { "$objectToArray": "$values" }
            }
        }   
    ])
    

    使用这个新架构,您需要更新集合,通过迭代从aggregate方法返回的游标并使用 bulkWrite 将字符串值更改为int,如下所示:

    var bulkUpdateOps = [],
        cursor = db.test.aggregate([
            {
                "$addFields": {
                    "values": { "$objectToArray": "$values" }
                }
            }   
        ]);
    
    cursor.forEach(doc => {
        const { _id, values } = doc;
        let temp = values.map(item => {
            item.key = item.k;
            item.value = parseFloat(item.v) || 0;
            delete item.k;
            delete item.v;
            return item;
        });
    
        bulkUpdateOps.push({
            "updateOne": {
               "filter": { _id },
               "update": { "$set": { values: temp } },
               "upsert": true
            }
        });
    
        if (bulkUpdateOps.length === 1000) {
            db.test.bulkWrite(bulkUpdateOps);  
            bulkUpdateOps = [];                 
        }
    }); 
    
    if (bulkUpdateOps.length > 0) {
        db.test.bulkWrite(bulkUpdateOps);
    }
    

    如果您的MongoDB版本不支持聚合框架中的 $objectToArray 运算符,那么要将当前架构转换为上面的架构,需要使用MongoDB的一些原生JavaScript函数{ {3}}光标的find()函数如下(假设您有一个测试集合):

    var bulkUpdateOps = [],
        cursor = db.test.find();
    
    cursor.forEach(doc => {
        const { _id, values } = doc;
        let temp =  Object.keys(values).map(k => {
            let obj = {};
            obj.key = k;
            obj.value = parseFloat(doc.values[k]) || 0;
            return obj;
        });
    
        bulkUpdateOps.push({
            "updateOne": {
               "filter": { _id },
               "update": { "$set": { values: temp } },
               "upsert": true
            }
        });
    
        if (bulkUpdateOps.length === 1000) {
            db.test.bulkWrite(bulkUpdateOps);  
            bulkUpdateOps = [];                 
        }
    }); 
    
    if (bulkUpdateOps.length > 0) {
        db.test.bulkWrite(bulkUpdateOps);
    }
    

    db.test.find().forEach(function (doc){
         var keys = Object.keys(doc.values),
            values = keys.map(function(k){
                var obj = {};
                obj.key = k;
                obj.value = parseFloat(doc.values[k]) || 0;
                return obj;
            });
        doc.values = values;
        db.test.save(doc);    
    });
    

    该集合现在将具有上述模式,因此遵循聚合管道,该管道将在一分钟内为您提供平均时间:

    db.test.aggregate([
        {
            "$fields": {
                "average": { "$avg": "$values.value" }
            }
        }    
    ])
    

    或者对于MongoDB 3.0及更低版本

    db.test.aggregate([
        { "$unwind": "$values" },
        {
            "$group": {
                "_id": "$timestamp",
                "average": {
                    "$avg": "$values.value"
                }
            }
        }    
    ])
    

    对于上述文件,输出将为:

    /* 0 */
    {
        "result" : [ 
            {
                "_id" : "2015-05-16T18:12:00.000Z",
                "average" : 25.684
            }
        ],
        "ok" : 1
    }
    

    对于其他 forEach() 选项,操作背后的直觉是您将使用JavaScript进行必要的转换并计算最终平均值。您需要定义三个函数:

    <强>地图

    当你告诉Mongo MapReduce时,你提供的作为map函数的函数将接收每个文档作为this参数。地图的目的是在JavaScript中运用您需要的任何逻辑,然后调用emit 0次或更多次以产生可简化的值。

    var map = function(){
        var obj = this.values;
        var keys = Object.keys(obj);
        var values = [];
        keys.forEach(function(key){  
            var val = parseFloat(obj[key]);
            var value = { count: 1, qty: val };  
            emit(this.timestamp, value);
        }); 
    };
    

    对于每个文档,您需要发出一个键和一个值。键是emit函数的第一个参数,表示您希望如何对值进行分组(在这种情况下,您将按时间戳进行分组)。要发出的第二个参数是值,在这种情况下,它是一个小对象,包含文档计数(始终为1)和每个单值对象键的总值,即每分钟内的每秒。

    <强>减少

    接下来,您需要定义reduce函数,其中Mongo将您发出的项目分组并将它们作为数组传递给此reduce函数它在reduce函数内部,您要进行聚合计算并减少所有对象到单个对象。

    var reduce = function(key, values) {
        var result = {count: 0, total: 0 };
        values.forEach(function(value){               
            result.count += value.count;
            result.total += value.qty;
        });
    
        return result;
    };
    

    此reduce函数返回单个结果。返回值与发射值具有相同的形状非常重要。对于给定的键,MongoDB也可以多次调用reduce函数并要求您处理一组部分值,因此如果需要执行一些最终计算,还可以为MapReduce提供一个finalize函数。

    <强>的Finalize

    finalize函数是可选的,但是如果你需要根据完全缩减的数据集计算某些东西,你将要使用finalize函数。在完成对集合的所有reduce调用之后,Mongo将调用finalize函数。这将是计算文档/时间戳中所有第二个值的平均值的地方:

    var finalize = function (key, value) {
        value.average = value.total / value.count;
        return value;
    };
    

    将它放在一起

    有了JavaScript,剩下的就是告诉MongoDB执行MapReduce:

    var map = function(){
        var obj = this.values;
        var keys = Object.keys(obj);
        var values = [];
        keys.forEach(function(key){  
            var val = parseFloat(obj[key]);
            var value = { count: 1, qty: val };  
            emit(this.timestamp, value);
        }); 
    };
    
    var reduce = function(key, values) {
        var result = {count: 0, total: 0 };
        values.forEach(function(value){               
            result.count += value.count;
            result.total += value.qty;
        });
    
        return result;
    };
    
    var finalize = function (key, value) {
        value.average = value.total / value.count;
        return value;
    };
    
    db.collection.mapReduce(
        map,
        reduce,
        {
            out: { merge: "map_reduce_example" },        
            finalize: finalize
        }
    )
    

    当您查询输出集合map_reduce_example,db.map_reduce_example.find()时,您会得到结果:

    /* 0 */
    {
        "_id" : null,
        "value" : {
            "count" : 5,
            "total" : 128.42,
            "average" : 25.684
        }
    }
    

    <强>参考

    1. Map-Reduce
    2. A Simple MapReduce with MongoDB and C#

答案 1 :(得分:-1)

这种数据结构会产生大量冲突并且难以处理mongo操作。在这种情况下,您要么更改了架构设计。但是,如果您无法更改此架构,请按照以下步骤操作:

在您的架构中有两个主要问题1> keys dynamic and 2> values of given keys in string,因此您应该使用一些编程代码来计算avg检查以下脚本

ref this首次计算的values

大小
Object.size = function(obj) {
    var size = 0,
        key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};

db.collectionName.find().forEach(function(myDoc) {
    var objects = myDoc.values;
    var value = 0;
    // Get the size of an object
    var size = Object.size(objects);
    for (var key in objects) {
        value = value + parseFloat(objects[key]); // parse string values to float

    }
    var avg = value / size
    print(value);
    print(size);
    print(avg);
});