Mongodb聚合(三)——mapreduce

Mongodb聚合(三)

3. MapReduce
Mapreduce很是强大与灵活,Mongodb使用javascript做为查询语言,能够表示任意复杂的逻辑。
Mapreduce很是慢,不该该用在实际的数据分析中。
Mapreduce能够在多台服务器之间并行执行,能够将一个问题拆分为多个小问题,以后将各个小问题发送到不一样的机器上,每台机器只负责完成一部分工做,全部的机器完成时,将这些零碎的解决方案合并为一个完整的解决方案。
最开始的是映射(map),将操做映射到集合中的各个文档,而后是中间环节,成为洗牌(shuffle),按照键分组,将产生的键值组成列表放在对应的键中。化简(reduce)则是把列表中的值化简为一个单值。

3.1 找出集合中的全部键
MongoDB假设你的模式是动态的,因此并不会跟踪记录每一个文档的键。一般找到集合中全部文档的全部键的最好方式就是MapReduce。
在映射环节,map函数使用特别的emit函数返回要处理的值。emit会给MapReduce一个键和一个值。
这里用emit将文档某个键的计数返回。this就是当前映射文档的引用:
map = function() {
    emit(this.country, {count : 1});
}
reduce接受两个参数,一个是key,就是emit返回的第一个值,还有一个数组,由一个或多个键对应的{count : 1}文档组成。
reduce = function(key, value) {
    var result = {count : 0};
    for (var i = 0; i < value.length; i++) {
        result.count += value[i].count;
    }
    return result;
}
示例表数据:
{ "_id" : 38, "country" : "japan", "money" : 724 }
{ "_id" : 39, "country" : "germany", "money" : 520 }
{ "_id" : 40, "country" : "india", "money" : 934 }
{ "_id" : 41, "country" : "china", "money" : 721 }
{ "_id" : 42, "country" : "germany", "money" : 156 }
{ "_id" : 43, "country" : "canada", "money" : 950 }
{ "_id" : 44, "country" : "india", "money" : 406 }
{ "_id" : 45, "country" : "japan", "money" : 776 }
{ "_id" : 46, "country" : "canada", "money" : 468 }
{ "_id" : 47, "country" : "germany", "money" : 262 }
{ "_id" : 48, "country" : "germany", "money" : 126 }
{ "_id" : 49, "country" : "japan", "money" : 86 }
{ "_id" : 50, "country" : "canada", "money" : 870 }
{ "_id" : 51, "country" : "india", "money" : 98 }
{ "_id" : 52, "country" : "india", "money" : 673 }
{ "_id" : 53, "country" : "japan", "money" : 487 }
{ "_id" : 54, "country" : "india", "money" : 681 }
{ "_id" : 55, "country" : "canada", "money" : 491 }
{ "_id" : 56, "country" : "japan", "money" : 98 }
{ "_id" : 57, "country" : "china", "money" : 172 }
Type "it" for more
运行结果:
db.foo.mapReduce(map, reduce, {out : "collection"})
{
        "result" : "collcetion",
        "timeMillis" : 83,
        "counts" : {
                "input" : 99,
                "emit" : 99,
                "reduce" : 5,
                "output" : 5
        },
        "ok" : 1,
        "$gleStats" : {
                "lastOpTime" : Timestamp(1399168165, 15),
                "electionId" : ObjectId("535a2ce15918f42de9ab1427")
        },
}
result:存放的集合名
timeMillis:操做花费的时间,单位是毫秒
input:传入文档数目
emit:此函数被调用的次数
reduce:此函数被调用的次数
output:最后返回文档的个数
查看下collection结果内容:
 db.collection.find();
{ "_id" : "canada", "value" : { "count" : 19 } }
{ "_id" : "china", "value" : { "count" : 15 } }
{ "_id" : "germany", "value" : { "count" : 25 } }
{ "_id" : "india", "value" : { "count" : 20 } }
{ "_id" : "japan", "value" : { "count" : 20 } }

3.2 MapRecude其余的键
"finalize" : function
能够将reduce的结果发送给这个键,这是整个处理过程的最后一步。
"keeptemp自动为true。" : boolean
若是为true,则在链接关闭后结果保存,不然不保存。
"out" : string
输出集合的名称,若是设置,keeptemp自动为true。
"query" : document
在发往map前,先用指定条件过滤文档。
"sort" : document
在发往map前,先进行排序。
"limit" : integer
发往map函数的文档数量上限。
"scope" : document
能够在javascripts代码中使用的变量。
"verbose" : boolean
是否记录详细的服务器日志。
3.2.1 finalize函数
可使用finalize函数做为参数,会在最后一个reduce输出结果后执行,而后将结果保存在临时集合里。
3.2.2 保存结果集合
默认状况下,执行mapreduce时建立一个临时集合,集合名称为mr.stuff.ts.id,即mapreduce.集合名.时间戳.数据库做业ID。MongoDB会在调用的链接关闭时自动销毁这个集合。
3.2.3 对子文档执行mapreduce
每一个传递给map的文档都须要先反序列化,从BSON对象转换为js对象,这个过程很是耗时,能够先对文档过滤来提升map速度,能够经过"query","limit"和"sort"等对文档进行过滤。
"query"的值是一个查询文档。
"limit","sort"配合能够发挥很大的做用。
"query","limit"和"sort"能够随意组合使用。
3.2.4 做用域
做用域键"scope",能够用变量名:值这样普通的文档来设置该选项,
3.2.5 获取更多的输出
设置verbose为true,能够将mapreduce过程更多的信息输出到服务器日志上。

4 聚合命名
count和distinct操做能够简化为普通命令,不须要使用聚合框架。
4.1 count
count返回集合中的文档数量:
db.foo.count() =>
99
能够传入一个查询文档:
db.foo.count({country : "china"}) =>
15
增长查询条件会使count变慢。
4.2 distinct
distinct用来找出给定键的全部不一样值。使用时必须指定集合和键。
db.runCommand({ "distinct" : "foo", "key" : "country"}) =>
{
        "values" : [
                "japan",
                "germany",
                "india",
                "china",
                "canada"
        ],
        "stats" : {
                "n" : 99,
                "nscanned" : 99,
                "nscannedObjects" : 99,
                "timems" : 22,
                "cursor" : "BasicCursor"
        },
        "ok" : 1,
        "$gleStats" : {
                "lastOpTime" : Timestamp(1399171995, 15),
                "electionId" : ObjectId("535a2ce15918f42de9ab1427")
        }
}
4.3 group
使用group能够进行更为复杂的聚合。先选定分组所依据的键,而后根据选定键的不一样值分为若干组,而后对每个分组进行聚合,获得结果文档。
插入示例数据:
var name = ["Caoqing", "Spider-man", "Garfield"]
for (var i = 0; i < 10000; i++) {
    iname = name[Math.floor(Math.random() * name.length)];
    date = new Date().getTime();
    number = Math.floor(100 * Math.random());
    db.coll.insert({_id : i, name : iname, time : date, age : number});
}
生成的列表中包含最新的时间和最新的时间对应的年纪。
能够安装name进行分组,而后取出每一个分组中date最新的文档,将其加入结果集。
db.runCommand({"group" : {
    "ns" : "coll",
    "key" : {"name" : true},
    "initial" : {"time" : 0},
    "$reduce" : function(doc, prev) {
        if (doc.time > prev.time) {
            prev.age = doc.age;
            prev.time = doc.time;
        }
    }
}})
"ns" : "coll"
指定进行分组的集合。
"key" : {"name" : true}
指定分组依据的键。
"initial" : {"time" : 0}
初始化time值,做为初始Wednesday传递给后续过程。每组成员都会使用这个累加器。
结果:
"$reduce" : function(doc, prev) {...}
{
        "retval" : [
                {
                        "name" : "Spider-man",
                        "time" : 1399179398567,
                        "age" : 55
                },
                {
                        "name" : "Garfield",
                        "time" : 1399179398565,
                        "age" : 85
                },
                {
                        "name" : "Caoqing",
                        "time" : 1399179398566,
                        "age" : 86
                }
        ],
        "count" : 10000,
        "keys" : 3,
        "ok" : 1,
        "$gleStats" : {
                "lastOpTime" : Timestamp(1399179362, 1),
                "electionId" : ObjectId("535a2ce15918f42de9ab1427")
        }
}
若是有文档不存在指定分组的键,这些文档会单独分为一组,缺失的键会使用name:null这样的形式。以下:
db.coll.insert({age : 5, time : new Date().getTime()})
返回结果:
        ...
        {
            "name" : null,
            "time" : 1399180685288,
            "age" : 5
        }
        "count" : 10001,
        "keys" : 4,
        ...
为了排除不包含指定用于分组的键的文档,能够在"condition"中加入"name":{"$exists" : true}。
db.runCommand({"group" : {
    "ns" : "coll",
    "key" : {"name" : true},
    "initial" : {"time" : 0},
    "$reduce" : function(doc, prev) {
        if (doc.time > prev.time) {
            prev.age = doc.age;
            prev.time = doc.time;
        }
    },
    "condition" : {"name" : {"$exists" : true}}
}})
4.3.1 使用完成器
完成器(finalizer)用于精简从数据库传到用户的数据,由于group命令的输出结果须要可以经过单次数据库响应返回给用户。
4.3.2 将函数做为键使用
分组条件能够很是复杂,不是单个键,例如分组时按照类别分组dog和DOG是两个彻底不一样的组,为了消除大小写差别,能够定义一个函数决定文档分组所依据的键。
定义分组函数须要用到"$keyf"键,
db.foo.group({
    "ns" : "foo",
    "$keyf" : function(x) { return x.category.toLowerCase(); };
    "initial" : ...,
    ......
})
相关文章
相关标签/搜索