MongoDB学习笔记五:聚合

『count』
count是最简单的聚合工具,返回集合中的文档数量:
> db.foo.count()
0
> db.foo.insert({"x" : 1})
> db.foo.count()
1
也可以传递查询,Mongo则会计算查询结果的数量:
> db.foo.insert({"x" : 2})
> db.foo.count()
2
> db.foo.count({"x" : 1})
1
『distinct』
distinct用来找出给定键的所有不同的值。使用时必须制定集合和键。
> db.runCommand({"distinct" : "people", "key" : "age"})
例如,假设有如下文档:
{"name" : "Ada", "age" : 20}
{"name" : "Fred", "age" : 35}
{"name" : "Susan", "age" : 60}
{"name" : "Andy", "age" : 35}
如果对"age"使用distinct,会获得所有不同的年龄:
> db.runCommand({"distinct" : "people", "key" : "age"})
{"values" : [20, 35, 60], "ok" : 1}
『group』
group先选定分组所依据的键,而后MongoDB就会将集合依据选定键值的不同分成若干组。然后通过聚合每一组内的文档,产生一个结果文档。
(这个group和SQL中的GROUP BY差不多。)
假设现在有个站点要跟踪股票价格。从上午10点到下午4点每隔几分钟就更新一下某只股票的价格,并保存在MongoDB中。现在报表程序要获得近30天的收盘价。用group就可以很容易地办到。
股价的集合中包含数以千计的如下形式的文档:
{"day" : "2010/10/03", "time" : "10/3/2010 03:57:01 GMT-400", "price" : 4.23}
{"day" : "2010/10/04", "time" : "10/4/2010 11:28:39 GMT-400", "price" : 4.27}
{"day" : "2010/10/03", "time" : "10/3/2010 05:00:23 GMT-400", "price" : 4.10}
{"day" : "2010/10/06", "time" : "10/6/2010 05:27:58 GMT-400", "price" : 4.30}
{"day" : "2010/10/04", "time" : "10/4/2010 08:34:50 GMT-400", "price" : 4.01}
想获得的结果就是每天最后的价格列表,就像这样:
[
{"day" : "2010/10/03", "time" : "10/3/2010 05:00:23 GMT-400", "price" : 4.10}
{"day" : "2010/10/04", "time" : "10/4/2010 11:28:39 GMT-400", "price" : 4.27}
{"day" : "2010/10/06", "time" : "10/6/2010 05:27:58 GMT-400", "price" : 4.30}
]
先把集合按照天分组,然后在每一组里取包含最新时间戳的文档,将其放置到结果中就完成了。整个过程:

> db.runCommnad({"group" : {
        "ns" : "stocks",
        "key" : "day",
        "initial" : {"time" : 0},
        "$reduce" : function(doc, prev) {
            if (doc.time > prev.time) {
                prev.price = doc.price;
                prev.time = doc.time;
            }
        }}})

分解步骤如下:
"ns" : "stocks"
指定要进行分组的集合。
"key" : "day"
指定文档分组依据的键。这里就是"day"键,所有"day"值相同的文档被划分到一组。
"initial" : {"time" : 0}
每一组reduce函数调用的初试时间,会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器,所以改变会保留住。
"reduce" : function(doc, prev) { ... }
每个文档都对应一次这个调用。系统会传递两个参数:当前文档和累加器文档(本组当前的结果)。本例中,想让reduce函数比较当前文档的时间和累加器文档的时间。如果当前文档的时间更近,则将累加器的日期和价格替换成当前文档的值。每一组都有一个独立的累加器,所以不用担心不同的日期使用同一个累加器。
在问题一开始的描述中,就提到只要最近30天的股价。然而,这里迭代了整个集合,这就是为什么要添加"condition",因为这样就可以值处理满足条件的文档了。

> db.runCommnad({"group" : {
        "ns" : "stocks",
        "key" : "day",
        "initial" : {"time" : 0},
        "$reduce" : function(doc, prev) {
            if (doc.time > prev.time) {
                prev.price = doc.price;
                prev.time = doc.time;
            }},
        "condition" : {"day" : {"$gt" : "2010/09/30"}}
        }})    

最后就会返回由30个文档组成的数组,每个组一个文档。魅族还有分组依据的键(这里就是"day" : string)以及这组最终的prev值。如果有的文档没有依据的键,就都会被分到一组,相应的部分就会使用"day : null"这样的形式。在"condition"中加入"day" : {"$exists" : true}就可以去掉这组。
使用完成器
完成器(finalizer)用以精简从数据库传到用户的数据。
例:博客,其中每篇文章都有多个标签(tag)。现在要找出每天最热点的标签。可以(再一次)按天分组,为每一个标签计数:

> db.posts.group({
    "key" : {"tags" : true},
    "initial" : {"tags" : {}},
    "$reduce" : function(doc, prev) {
        for (i in doc.tags) {
            if (doc.tags[i] in prev.tags) {
                prev.tags[doc.tags[i]]++;
            } else {
                prev.tags[doc.tags[i]] = 1;
            }
        }
    }})

结果会是这样:
[
{"day" : "2010/01/12", "tags" : {"nosql" : 4, "winter" : 10, "sledding" : 2}}
{"day" : "2010/01/13", "tags" : {"soda" : 5, "php" : 2}}
{"day" : "2010/01/14", "tags" : {"python" : 6, "winter" : 4, "nosql" : 15}}
]
然后,使用finalizer过滤服务器传递给客户端过程中不需要的数据:

> db.posts.group({
    "key" : {"tags" : true},
    "initial" : {"tags" : {}},
    "$reduce" : function(doc, prev) {
        for (i in doc.tags) {
            if (doc.tags[i] in prev.tags) {
                prev.tags[doc.tags[i]]++;
            } else {
                prev.tags[doc.tags[i]] = 1;
            }
        }, 
    "finalize" : function(prev) {
        var mostPopular = 0;
        for(i in prev.tags) {
            if(prev.tags[i] > mostPopular) {
                prev.tag = i;
                mostPopular = prev.tags[i]
            }
        }
        delete prev.tags
    }}})

然后,服务器会返回希望的结果:
[
{"day" : "2010/01/12", "tag" : "winter"},
{"day" : "2010/01/13", "tag" : "soda"},
{"day" : "2010/01/14", "tag" : "nosql"}
]
finalize嫩刚修改传递的参数也能返回新值。
将函数作为键使用
定义分组函数要用到"$keyf"键。
例如,由于有很多作者,给文章分类时可能不规律的用了大小写,为了让"MongoDB"和"mongodb"分在同一个组,需要使用"$keyf"定义一个函数来确定文档分组所依据的键:
> db.posts.group({"ns" : "posts", "$keyf" : function(x) { return x.category.toLowerCase(); }, "initializer" : ... })
有了"$keyf"就能依据各种复杂的条件进行分组了。
『MapReduce』
MapReduce:
①映射(map):将操作映射到集合中的每个文档。
②洗牌(shuffle):按照键分组,并将产生的键值组成列表放到对应的键中。
③化简(reduce):把列表中的值简化成一个单值。这个值被返回,然后接着进行洗牌,直到每个键的列表只有一个值为止,这个值就是最后结果。
使用MapReduce的代价就是速度:group不是很快,MapReduce更慢,绝对不要用在“实时”环境中。
【MapReduce例1:找出集合中的所有键】
MongoDB没有模式,所以并不知晓每个文档有多少个键。通常找到集合的所有键的最好方式就是用MapReduce。在本例中,还会记录每个键出现了多少次。
在映射(map)环节,想得到文档中的每个键。map函数使用emit“返回”要处理的值。emit会给MapReduce一个键(类似于前面group多使用的键)和一个值。这里用emit将文档中某个键的计数(count)返回({count : 1})。我们想为每个键单独计数,所以为文档中的每一个键调用一次emit。this就是当前映射文档的引用:

> map = function() {
    for (var key in this) {
        emit(key, {count : 1})
    }};

这样就有了许许多多{count : 1}文档,每一个都与集合中的一个键相关。这种由一个或多个{count : 1}文档组成的数组,会传递给reduce函数。reduce函数有两个参数,一个是key,也就是emit返回的第一个值,还有另外一个数组,由一个或者多个对应于键的{count : 1}文档组成。

> reduce = function(key, emit) {
    total = 0;
    for (var i in emits) {
        total += emits[i].count;
    }
    return {"count" : total};
    }

reduce一定要能被反复调用,不论是映射(map)环节还是前一个简化(reduce)环节。所以reduce返回的文档必须能作为reduce的第二个参数的一个元素。
reduce能处理emit文档和其他reduce结果的各种组合。
MapReduce函数类似这样:

> mr = db.runCommand({"mapreduce" : "foo", "map" : map, "reduce" : reduce})
{
    "result" : "tmp.mapreduce_1266787811_1",
    "timeMillis" : 12,
    "counts" : {
        "input" : 6,
        "emit" : 14,
        "output" : 5
    },
    "ok" : true
}

MapReduce返回的文档包含很多与操作有关的元信息:
·"result" : "tmp.mapreduce_1266787811_1"
这是存放MapReduce结果的集合名。这是一个临时集合,MapReduce的连接关闭后自动就被删除了。
·"timeMillis" : 12
操作花费的时间,单位是毫秒。
·"count" : { ... }
这个内嵌文档包含3个键。
·"input" : 6
发送到map函数的文档个数。
·"emit" : 14
在map函数中emit被调用的次数。
·"output" : 5
结果集合中创建的文档数量。
"count"对调试非常有帮助。
对结果几核进行查询会发现原有集合的所有键及其计数:
> db[mr.result].find()
{ "_id" : "_id", "value" : {"count" : 6} }
{ "_id" : "a", "value" : { "count" : 4 } }
{ "_id" : "b", "value" : { "count" : 2 } }
{ "_id" : "x", "value" : { "count" : 1 } }
{ "_id" : "y", "value" : { "count" : 1 } }
每个键值变为一个"_id",最终花间步骤的结果变为"value"。
【MapReduce例2:网页分类】
假设有一个网站,人们可以提交其他网页的链接,比如rebbit.com,提交者可以给这个链接做标签,表明主题,比如"politics","geek"或者"icanhascheezburger",可以用MapReduce找出哪个主题最为热门,热门与否由最近的投票决定。
首先,建立一个map函数,发出(emit)标签和一个基于流行度和新近成都的值。

map = function() {
    for (var i in this.tags) {
        var recency = 1/(new Date() - this.Date);
        var score = recency * this.score;
        
        emit(this.tags[i], {"urls" : [this.url], "score" : score});
    }
};

现在就简化同一个标签的所有值,形成这个标签的分数:

reduce = function(key, emits) {
    vat total = {urls : [], score : 0}
    for (var i in emits) {
        emits[i].urls.forEach(function(url)) {
            total.urls.push(url);
        }
        total.score += emits[i].score;
    }
    return total;
}

最终的集合包含每个标签的URL列表和表示该标签流行程度的分数。
-- MapReduce部分没有完全掌握! --
『MongoDB和MapReduce』
前面两个例子只用到了mapreduce、map和reduce键。这三个键是必须的,除此之外MapReduce命令还有很多可选的键。
·"finalize":函数
将reduce的结果发送给这个键,这是处理过程的最后一步。
·"keeptemp":布尔
连接关闭时临时结果集合是否保存。
·"output":字符串
集合结果的名字。设定该项则隐含着keeptemp : true。
·"query":文档
会在发往map函数前,先用指定条件过滤文档。
·"sort":文档
在发往map前先给文档排序(与limit一同使用非常有用)。
·"limit":整数
发往map函数的文档数量的上限。
·"scope":文档
JavaScript代码中要用到的变量。
·"verbose":布尔
是否产生更加详尽的服务器日志。
⒈finalize函数
finalize会在最后reduce得到输出后执行,然后将结果存到临时集合中。
⒉保留结果集合
设置keeptemp为true或者设置out选项给集合取个好点的名字。
⒊对文档子集执行MapReduce
有时候需要对集合的一部分执行MapReduce。只需要在传给map函数前添加一个查询来过滤一下文档就好了。
过滤主要就是用"query"、"limit"和"sort"键指定。
"query"键的值是一个查询文档。通常查询返回的结果就传递给了map函数。例如,有个应用程序做跟踪分析,需要上周的概要,只要使用如下命令对上周的文档执行MapReduce就好了:
> db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce, "query" : {"date" : {"$gt" : week_ago}}})
sort选项一般和limit一铜发挥重要作用。limit也可以单独使用,用来截取一部分文档发送给map函数。
如果在上个例子中想分析最近10000个页面视图(而不是最近一周的),则可以借助limit和sort:
> db.runCommand({"mapreduce" : "analytics", "map" : map, "reduce" : reduce, "limit" : 10000, "sort" : {"date" : -1}})
query、limit、sort可以随意组合,但要是没有limit,sort单独使用的用处不大。
⒋使用作用域
例:在之前的一个例子中,用1/(new Date() - this.date)计算了页面的新近程度。还可以将当前的日期作为作用域的一部分传递进去:
> db.runCommand({"mapreduce" : "webpages", "map" : map, "reduce" : reduce, "scope" : {now : new Date()}})
这样,在map函数中就能计算1/(now-this.date)了。
⒌获得更多的输出
如果想看看MapReduce的运行过程,可以用"verbose" : true。
也可以用print把map、reduce、finalize过程中的信息输出到服务器日志上。

原文地址:https://www.cnblogs.com/answernotfound/p/mongodbnote5.html