mongodb进阶二之mongodb聚合

来源:互联网 发布:2017优化六上数学答案 编辑:程序博客网 时间:2024/04/28 08:43
上篇我们说了mongodb的高级查询:http://blog.csdn.net/stronglyh/article/details/46817789

这篇说说mongodb的聚合

一:mongodb中有很多聚合框架,从而实现文档的变换和组合,主要有一下构件

构件类别                操作符
筛选(filtering)         $match
投射(projecting)     $project
分组(grouping        $group
排序(sorting)          $sort
限制(limiting)         $limit
跳过(skipping)       $skip


如果需要聚合数据,那么要使用aggregate方法

db.collection.aggregate(聚合条件);

单个操作,传入一个json对象作为集合条件,如

db.users.aggregate({
    $project:{
        _id:0,
        name:1,
    }
})

如果需要多个操作符,传入一个数组作为条件,如

db.users.aggregate([
    { $skip : 5 },          
    { $project:{ _id:0, name:1, } }    
])

1.1:$match匹配
$match用于对文档集合进行筛选,之后就可以在筛选得到的文档子集上做聚合。

例如,如果想对北京(简写BJ)的用户做统计,就可以使用{$match:{"area":"BJ"}}。"$match"可以使用所有常规的查询操作符("$gt"、"$lt"、"$in"等)。有一个里外需要注意:不能在"$match"中使用地理空间操作符。

通常,在实际使用中应该尽可能将"$match"放在管道的前面位置。这样做有两个好处:

一是可以快速将不需要的文档过滤掉,以减少管道的工作量;

二是如果在投射和分组之前执行"$match",查询可以使用索引。


1.2:$project投射
相对于“普通”的查询而言,管道中的投射操作更加强大。使用"$project"可以从子文档中提取字段,可以重命名字段,还可以在这些字段上进行一些有意思的操作。

最简单的一个"$project"操作是从文档中选择想要的字段。可以指定包含或者不包含一个字段,它的语法和查询中的第二个参数类似。如果在原来的集合上执行下面的代码,返回的结果文档中只包含一个"author"字段。

db.articles.aggregate({"$project":{"author":1,"_id":0})
默认情况下,如果文档中存在"_id"字段,这个字段就会被返回。

赶快亲自动手敲下,看看运行结果。

也可以将投射过的字段进行重命名。例如,可以将每个用户文档的"_id"在返回结果中重命名为"userId":

db.articles.aggregate({"$project":{"userId":"$_id","_id":0}});
这里的"$fieldname"语法是为了在聚合框架中引用fieldname字段(上面的例子中是"_id")的值。例如,"$age"会被替换为"age"字段的内容(可能是数值,可能是字符串),"$tag.3"会被替换为tags数组中的第4个元素。所以,上面例子中的"$_id"会被替换为进入管道的每个文档的"_id"字段的值。

注意,必须明确指定将"_id"排除,否则这个字段的值会被返回两次:一次标记为"userId",一次被标记为"_id"。可以使用这种技术生成字段的多个副本,以便在之后"$group"中使用。

继续学习


1.3:$group分组
$group操作可以将文档依据特定字段的不同值进行分组。举例:

如果有一个学生集合,希望按照分数等级将学生分为多个组,可以根据"grade"字段进行分组。

如果选定了需要进行分组的字段,就可以将选定的字段传递给"$group"函数的"_id"字段。对于上面的例子,相应代码如下:

{"$group":{"_id":"$grade"}}
例如,学生分数等级进行分组的结果可能是:

{"result":[{"_id":"A+"},{"_id":"A"},{"_id":"A-"},...,{"_id":"F"}],"ok":1}
分组操作符

这些分组操作符允许对每个分组进行计算,得到相应的结果。


1.4:$unwind拆分
拆分(unwind)可以将数组中的每一个值拆分为单独的文档。

例如,如果有一篇拥有多条评论的博客文章,可以使用$unwind将每条评论拆分为一个独立的文档:

db.blog.findOne()
{
   "_id":ObjectId("5359f6f6ec7452081a7873d7"),
   "author":"Tom",
   "conments":[
      {
          "author":"Mark",
          "date":ISODate("2014-01-01T17:52:04.148Z"),
          "text":"Nice post"
      },
      {
          "author":"Bill",
          "date":ISODate("2014-01-01T17:52:04.148Z"),
          "text":"I agree"
      }
   ]
}
db.blog.aggregate({"$unwind":"$comments"})

{
   "results":
       {
          "_id":ObjectId("5359f6f6ec7452081a7873d7"),
          "author":"Tom",
          "comments":{
               "author":"Mark",
               "date":ISODate("2014-01-01T17:52:04.148Z"),
               "text":"Nice post"
          }
       },
       {
          "_id":ObjectId("5359f6f6ec7452081a7873d7"),
          "author":"Tom",
          "comments":{
               "author":"Bill",
               "date":ISODate("2014-01-01T17:52:04.148Z"),
               "text":"I agree"
          }
       }
}
如果希望在查询中得到特定的子文档,这个操作符就会非常有用:先使用"$unwind"得到所有子文档,再使用"$match"得到想要的文档。例如,如果要得到特定用户的所有评论(只需要得到评论,不需要返回评论所属的文章),使用普通的查询是不可能做到的。但是,通过提取、拆分、匹配、就很容易了:

db.blog.aggregate({"$project":{"coomments":"$comments"}},
   {"$unwind":"$comments"},
   {"$match":{"comments.author":"Mark"}})
由于最后得到的结果仍然是一个"comments"子文档,所以你可能希望再做一次投射,以便让输出结果更优雅。


1.5:sort排序
可以根据任何字段(或者多个字段)进行排序,与普通查询中的语法相同。如果要对大量的文档进行排序,强烈建议在管道的第一阶段进行排序,这时的排序操作可以使用索引。否则,排序过程就会比较慢,而且会占用大量内存。

可以在排序中使用文档中实际存在的字段,也可以使用在投射时重命名的字段:

db.employees.aggregate(
   {
       "$project":{
           "compensation":{
               "$add":["$salary","$bonus"]
           },
           name:1
       }
   },
   {
       "$sort":{"compensation":-1,"name":1}
   }
)
这个例子会对员工排序,最终的结果是按照报酬从高到低,姓名从A到Z的顺序排序。

排序的方向可以是1(升序)和-1(降序)。

与前面讲过的"$group"一样,"$sort"也是一个无法使用流式工作方式的操作符。"$sort"也必须要接收到所有文档之后才能进行排序。在分片环境下,先在各个分片上进行排序,然后将各个分片的排序结果发送到mongos做进一步处理。


1.6:$limit会接受一个数字n,返回结果集中的前n个文档。

$skip也是接受一个数字n,丢弃结果集中的前n个文档,将剩余文档作为结果返回。在"普通"查询中,如果需要跳过大量的数据,那么这个操作符的效率会很低。在聚合中也是如此,因为它必须要先匹配到所有需要跳过的文档,然后再将这些文档丢弃。


1.7:使用管道
应该尽量在管道的开始阶段(执行"$project"、"$group"或者"$unwind"操作之前)就将尽可能多的文档和字段过滤掉。管道如果不是直接从原先的集合中使用数据,那就无法在筛选和排序中使用索引。如果可能,聚合管道会尝试对操作进行排序,以便能够有效使用索引。


二:聚合命令

2.1:count

count是最简单的聚合工具,用于返回集合中的文档数量:

db.users.count()
0
db.users.insert({"x":1})
db.users.count()
1
不论集合有多大,count都会很快返回总的文档数量。

也可以给count传递一个查询文档,Mongo会计算查询结果的数量:

db.users.insert({"x":2})
db.users.count()
2
db.users.count({"x":1})
1
对于分页显示来说总数非常必要:“共439个,目前显示0~10个”。但是,增加查询条件会使count变慢。count可以使用索引,但是索引并没有足够的元数据提供count使用,所以不如直接使用查询来得快。


2.2: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}


2.3:group

使用group可以执行更复杂的聚合。先选定分组所依据的键,而后MongoDB就会将集合依据选定键的不同值分成若干组。然后可以对每一个分组内的文档进行聚合,得到一个结果文档。

如果你熟悉SQL,那么这个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}
我们需要的结果列表中应该包含每天的最后交易时间和价格,就像下面这样:

[
   {"time" : "10/3/2010 05:00:23 GMT-400","price" : 4.10}
   {"time" : "10/4/2010 11:28:39 GMT-400","price" : 4.27}
   {"time" : "10/6/2010 05:27:58 GMT-400","price" : 4.30}
]
先把集合按照"day"字段进行分组,然后在每个分组中查找"time"值最大的文档,将其添加到结果集中就完成了。整个过程如下所示:

> db.runCommand({"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;
...        }
... }}})     
 

三:MapReduce

好烦,说到这,好想跟大伙说说hadoop中的mapreduce,可是最好不要说串掉,要不然就误人子弟了,其实原理都是一样的啦

MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算。概念"Map(映射)"和"Reduce(归约)",和它们的主要思想,都是从函数式编程语言里借来的,还有从矢量编程语言里借来的特性。它极大地方便了编程人员在不会分布式并行编程的情况下,将自己的程序运行在分布式系统上。 当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。

3.1:找出集合中的所有键
MongoDB没有模式,所以并不知晓每个文档有多少个键.通常找到集合的所有键的做好方式是用MapReduce。 在映射阶段,想得到文档中的每个键.map函数使用emit 返回要处理的值.emit会给MapReduce一个键和一个值。 这里用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,emits){
... total=0;
... for(var i in emits){
...     total+=emits[i].count; 
... }
... return {count:total};
... }
reduce要能被反复被调用,不论是映射环节还是前一个化简环节。reduce返回的文档必须能作为reduce的第二个参数的一个元素。如x键映射到了3个文档{"count":1,id:1},{"count":1,id:2},{"count":1,id:3} 其中id键用于区别。MongoDB可能这样调用reduce:

>r1=reduce("x",[{"count":1,id:1},{"count":1,id:2}])
{count:2}
>r2=reduce("x",[{"count":1,id:3}])
{count:1}
>reduce("x",[r1,r2])
{count:3}
不能认为第二个参数总是初始文档之一(比如{count:1})或者长度固定。reduce应该能处理emit文档和其他reduce返回结果的各种组合。

总之,MapReduce函数可能会是下面这样:

> mr = db.runCommand({"mapreduce" : "foo", "map" : map,"reduce" : reduce})
{
    "reduce" : "tmp.mr.mapreduce_1266787811_1", // 这是存放MapReduce结果集合名,临时集合连接关闭自动删除
    "timeMillis" : 12,  // 操作花费的时间,单位毫秒
    "count" : {
        "input" :  6  //发往到map函数的文档个数
        "emit"  : 14  //在map函数中emit被调用的次数
        "output" : 5  //结果集合中的文档数量
     },
     "ok" : true
}



3.2:网页分类
我们有这样一个网站,用户可以在其上提交他们喜爱的链接url,比如汇智网(http://www.hubwiz.com),并且提交者可以为这个url添加一些标签,作为主题,其他用户可以为这条信息打分。我们有一个集合,收集了这些信息,然后我们需要看看哪种主题最为热门,热门程度由最新打分日期和所给分数共同决定。

首先建立一个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":this.score}); 
...     } 
... };
现在就化简同一个标签的所有值,以得到这个标签的分数:

> reduce = function(key, emits) { 
...     var 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; 
... };


3.2:MongoDB和MapReduce
前面两个例子只用到了MapReduce、map和reduce键。这3个键是必需的,但是MapReduce命令还有很多可选的键。

"finalize" : 函数

将reduce的结果发送给这个键,这是处理过程的最后一步。

"keeplize" : 布尔

如果值为true,那么在连接关闭时会将临时结果集合保存下来,否则不保存。

"output" : 字符串

输出集合的名称,如果设置了这项,系统会自动设置keeptemp : true。

"query" : 文档

在发往map函数前,先用指定条件过滤文档。

"sort" : 文档

在发往map函数前给文档排序(与limit一同使用非常有用)。

"limit" : 整数

在发往map函数的文档数量的上限。

"scope" : 文档

可以再Javascript代码中使用的变量。

"verbose" : 布尔

是否记录详细的服务器日志。






感谢汇智网:http://hubwiz.com/


1 1
原创粉丝点击