MongoDB Aggregation 聚合管道(Aggregation Pipeline)

来源:互联网 发布:主播都用什么唱歌软件 编辑:程序博客网 时间:2024/05/16 12:23
MongoDB Aggregation 例子文档下载:
http://download.csdn.net/detail/huangchao1010/6820115
一 管道的概念
      
     管道是MongoDB2.2版本引入新的功能 ,它是数据聚合的一个新框架,其概念类似于数据处理的管道。管道的工作方式类似于UNIX-like的shell ps aux | grep mong*  (下面的补充会详细介绍管道原理)。

     每个文档通过一个由多个节点组成的管道,每个节点有自己特殊的功能(分组、过滤等),文档经过管道处理后,最后输出相应的结果。管道基本的功能有两个:一是对文档进行“过滤”,也就是筛选出符合条件的文档;二是对文档进行“变换”,也就是改变文档的输出形式。其他的一些功能还包括按照某个指定的字段分组和排序等。而且在每个阶段还可以使用表达式操作符计算平均值和拼接字符串等相关操作。管道提供了一个MapReduce 的替代方案,MapReduce使用相对来说比较复杂,而管道的拥有固定的接口(操作符表达),使用比较简单,对于大多数的聚合任务管道一般来说是首选方法。
     

二 管道操作符

     管道是由一个个功能节点组成的,这些节点用管道操作符来进行表示。聚合管道以一个集合中的所有文档作为开始,然后这些文档从一个操作节点  流向下一个节点 ,每个操作节点对文档做相应的操作。这些操作可能会创建新的文档或者过滤掉一些不符合条件的文档,在管道中可以对文档进行重复操作。
  
     先看一个管道聚合的例子:


    
 管道操作符的种类:
NameDescription$projectReshapes a document stream. $project can rename, add, or remove fields as well as create computed values and sub-documents.$matchFilters the document stream, and only allows matching documents to pass into the next pipeline stage.$match uses standard MongoDB queries.$limitRestricts the number of documents in an aggregation pipeline.$skipSkips over a specified number of documents from the pipeline and returns the rest.$unwindTakes an array of documents and returns them as a stream of documents.$groupGroups documents together for the purpose of calculating aggregate values based on a collection of documents.$sortTakes all input documents and returns them in a stream of sorted documents.$geoNearReturns an ordered stream of documents based on proximity to a geospatial point.
管道操作符详细使用说明
  1.  $project: 数据投影,主要用于重命名、增加和删除字段
       例如:
db.article.aggregate(    { $project : {        title : 1 ,        author : 1 ,    }} );
       这样的话结果中就只还有_id,tilte和author三个字段了,默认情况下_id字段是被包含的,如果要想不包含_id话可以这样:
db.article.aggregate(    { $project : {        _id : 0 ,        title : 1 ,        author : 1    }});
     也可以在$project内使用算术类型表达式操作符 ,例如:
db.article.aggregate(    { $project : {        title : 1,        doctoredPageViews : { $add:["$pageViews", 10] }    }});
    通过使用$add给pageViews字段的值加10,然后将结果赋值给一个新的字段:doctoredPageViews 
    注:必须将$add计算表达式放到中括号里面
    除此之外使用$project还可以重命名字段名和子文档的字段名:
db.article.aggregate(    { $project : {        title : 1 ,        page_views : "$pageViews" ,        bar : "$other.foo"    }});
 也可以添加子文档:
db.article.aggregate(    { $project : {        title : 1 ,        stats : {            pv : "$pageViews",            foo : "$other.foo",            dpv : { $add:["$pageViews", 10] }        }    }});
产生了一个子文档stats,里面包含pv,foo,dpv三个字段。

2.$match: 滤波操作,筛选符合条件文档,作为下一阶段的输入
   $match的语法和查询表达式(db.collection.find())的语法相同
db.articles.aggregate( [                        { $match : { score : { $gt : 70, $lte : 90 } } },                        { $group: { _id: null, count: { $sum: 1 } } }                       ] );
   $match用于获取分数大于70小于或等于90记录,然后将符合条件的记录送到下一阶段$group管道操作符进行处理。
注意:1.不能在$match操作符中使用$where表达式操作符。
          2.$match尽量出现在管道的前面,这样可以提早过滤文档,加快聚合速度。
          3.如果$match出现在最前面的话,可以使用索引来加快查询。

3.  $limit:  限制经过管道的文档数量
     $limit的参数只能是一个正整数
db.article.aggregate(    { $limit : 5 });
这样的话经过$limit管道操作符处理后,管道内就只剩下前5个文档了

4. $skip: 从待操作集合开始的位置跳过文档的数目
    $skip参数也只能为一个正整数
db.article.aggregate(    { $skip : 5 });
经过$skip管道操作符处理后,前五个文档被“过滤”掉

5.$unwind:将数组元素拆分为独立字段
例如:article文档中有一个名字为tags数组字段:
> db.article.find()
  { "_id" : ObjectId("528751b0e7f3eea3d1412ce2"),
 "author" : "Jone", "title" : "Abook", 
 "tags" : [  "good",  "fun",  "good" ] }
  
使用$unwind操作符后:
> db.article.aggregate({$project:{author:1,title:1,tags:1}},{$unwind:"$tags"})
{
        "result" : [
                {
                        "_id" : ObjectId("528751b0e7f3eea3d1412ce2"),
                        "author" : "Jone",
                        "title" : "A book",
                        "tags" : "good"
                },
                {
                        "_id" : ObjectId("528751b0e7f3eea3d1412ce2"),
                        "author" : "Jone",
                        "title" : "A book",
                        "tags" : "fun"
                },
                {
                        "_id" : ObjectId("528751b0e7f3eea3d1412ce2"),
                        "author" : "Jone",
                        "title" : "A book",
                        "tags" : "good"
                }
        ],
        "ok" : 1
}
注意:a.{$unwind:"$tags"})不要忘了$符号
          b.如果$unwind目标字段不存在的话,那么该文档将被忽略过滤掉,例如:
     > db.article.aggregate({$project:{author:1,title:1,tags:1}},{$unwind:"$tag"})
    { "result" : [ ], "ok" : 1 }
    将$tags改为$tag因不存在该字段,该文档被忽略,输出的结果为空
        c.如果$unwind目标字段不是一个数组的话,将会产生错误,例如:
  > db.article.aggregate({$project:{author:1,title:1,tags:1}},{$unwind:"$title"})
    Error: Printing Stack Trace
    at printStackTrace (src/mongo/shell/utils.js:37:15)
    at DBCollection.aggregate (src/mongo/shell/collection.js:897:9)
    at (shell):1:12
    Sat Nov 16 19:16:54.488 JavaScript execution failed: aggregate failed: {
        "errmsg" : "exception: $unwind:  value at end of field path must be an array",
        "code" : 15978,
        "ok" : 0
} at src/mongo/shell/collection.js:L898
      d.如果$unwind目标字段数组为空的话,该文档也将会被忽略。

  6.$group 对数据进行分组
    $group的时候必须要指定一个_id域,同时也可以包含一些算术类型的表达式操作符:
db.article.aggregate(    { $group : {        _id : "$author",        docsPerAuthor : { $sum : 1 },        viewsPerAuthor : { $sum : "$pageViews" }    }});
注意:  1.$group的输出是无序的。
             2.$group操作目前是在内存中进行的,所以不能用它来对大量个数的文档进行分组。

7.$sort : 对文档按照指定字段排序
  使用方式如下:
    db.users.aggregate( { $sort : { age : -1, posts: 1 } }); 
  按照年龄进行降序操作,按照posts进行升序操作
注意:1.如果将$sort放到管道前面的话可以利用索引,提高效率
          2.MongoDB 24.对内存做了优化,在管道中如果$sort出现在$limit之前的话,$sort只会对前$limit个文档进行操作,这样在内存中也只会保留前$limit个文档,从而可以极大的节省内存
          3.$sort操作是在内存中进行的,如果其占有的内存超过物理内存的10%,程序会产生错误

8.$goNear
        $goNear会返回一些坐标值,这些值以按照距离指定点距离由近到远进行排序

具体使用参数见下表:
FieldTypeDescriptionnearGeoJSON point orlegacy coordinate pairsThe point for which to find the closest documents.distanceFieldstringThe output field that contains the calculated distance. To specify a field within a subdocument, use dot notation.limitnumberOptional. The maximum number of documents to return. The default value is 100. See also the num option.numnumberOptional. The num option provides the same function as the limitoption. Both define the maximum number of documents to return. If both options are included, the num value overrides the limit value.maxDistancenumberOptional. A distance from the center point. Specify the distance in radians. MongoDB limits the results to those documents that fall within the specified distance from the center point.querydocumentOptional. Limits the results to the documents that match the query. The query syntax is the usual MongoDB read operation query syntax.sphericalBooleanOptional. If true, MongoDB references points using a spherical surface. The default value is false.distanceMultipliernumberOptional. The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth.includeLocsstringOptional. This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within a subdocument, usedot notation.uniqueDocsBooleanOptional. If this value is true, the query returns a matching document once, even if more than one of the document’s location fields match the query. If this value is false, the query returns a document multiple times if the document has multiple matching location fields. See $uniqueDocsfor more information.
  
例如:
db.places.aggregate([                      {                        $geoNear: {                                    near: [40.724, -73.997],                                    distanceField: "dist.calculated",                                    maxDistance: 0.008,                                    query: { type: "public" },                                    includeLocs: "dist.location",                                    uniqueDocs: true,                                    num: 5                                  }                      }                   ])
其结果为:
{  "result" : [               { "_id" : 7,                 "name" : "Washington Square",                 "type" : "public",                 "location" : [                                [ 40.731, -73.999 ],                                [ 40.732, -73.998 ],                                [ 40.730, -73.995 ],                                [ 40.729, -73.996 ]                              ],                 "dist" : {                            "calculated" : 0.0050990195135962296,                            "location" : [ 40.729, -73.996 ]                          }               },               { "_id" : 8,                 "name" : "Sara D. Roosevelt Park",                 "type" : "public",                 "location" : [                                [ 40.723, -73.991 ],                                [ 40.723, -73.990 ],                                [ 40.715, -73.994 ],                                [ 40.715, -73.994 ]                              ],                 "dist" : {                            "calculated" : 0.006082762530298062,                            "location" : [ 40.723, -73.991 ]                          }               }             ],  "ok" : 1}
其中,dist.calculated中包含了计算的结果,而dist.location中包含了计算距离时实际用到的坐标

注意: 1.使用$goNear只能在管道处理的开始第一个阶段进行
           2.必须指定distanceField,该字段用来决定是否包含距离字段
      3.$gonNear和geoNear命令比较相似,但是也有一些不同:distanceField在$geoNear中是必选的,而在geoNear中是可选的;includeLocs在$geoNear中是string类型,而在geoNear中是boolen类型。
   

三 管道表达式
     
     
  管道操作符作为“键”,所对应的“值”叫做管道表达式。例如上面例子中{$match:{status:"A"}},$match称为管道操作符,而{status:"A"}称为管道表达式,它可以看做是管道操作符的操作数(Operand)每个管道表达式是一个文档结构,它是由字段名、字段值、和一些表达式操作符组成的,例如上面例子中管道表达式就包含了一个表达式操作符$sum进行累加求和。

      每个管道表达式只能作用于处理当前正在处理的文档,而不能进行跨文档的操作。管道表达式对文档的处理都是在内存中进行的。除了能够进行累加计算的管道表达式外,其他的表达式都是无状态的,也就是不会保留上下文的信息。累加性质的表达式操作符通常和$group操作符一起使用,来统计该组内最大值、最小值等,例如上面的例子中我们在$group管道操作符中使用了具有累加的$sum来计算总和。

     除了$sum以为,还有以下性质的表达式操作符:

组聚合操作符
NameDescription$addToSetReturns an array of all the unique values for the selected field among for each document in that group.$firstReturns the first value in a group.$lastReturns the last value in a group.$maxReturns the highest value in a group.$minReturns the lowest value in a group.$avgReturns an average of all the values in a group.$pushReturns an array of all values for the selected field among for each document in that group.$sumReturns the sum of all the values in a group.
Bool类型聚合操作符
NameDescription$andReturns true only when all values in its input array are true.$orReturns true when any value in its input array are true.$notReturns the boolean value that is the opposite of the input value.

比较类型聚合操作符
NameDescription$cmpCompares two values and returns the result of the comparison as an integer.$eqTakes two values and returns true if the values are equivalent.$gtTakes two values and returns true if the first is larger than the second.$gteTakes two values and returns true if the first is larger than or equal to the second.$ltTakes two values and returns true if the second value is larger than the first.$lteTakes two values and returns true if the second value is larger than or equal to the first.$neTakes two values and returns true if the values are not equivalent.


算术类型聚合操作符
NameDescription$addComputes the sum of an array of numbers.$divideTakes two numbers and divides the first number by the second.$modTakes two numbers and calcualtes the modulo of the first number divided by the second.$multiplyComputes the product of an array of numbers.$subtractTakes two numbers and subtracts the second number from the first.


字符串类型聚合操作符
NameDescription$concatConcatenates two strings.$strcasecmpCompares two strings and returns an integer that reflects the comparison.$substrTakes a string and returns portion of that string.$toLowerConverts a string to lowercase.$toUpperConverts a string to uppercase.

日期类型聚合操作符
NameDescription$dayOfYearConverts a date to a number between 1 and 366.$dayOfMonthConverts a date to a number between 1 and 31.$dayOfWeekConverts a date to a number between 1 and 7.$yearConverts a date to the full year.$monthConverts a date into a number between 1 and 12.$weekConverts a date into a number between 0 and 53$hourConverts a date into a number between 0 and 23.$minuteConverts a date into a number between 0 and 59.$secondConverts a date into a number between 0 and 59. May be 60 to account for leap seconds.$millisecondReturns the millisecond portion of a date as an integer between 0 and 999.

条件类型聚合操作符
NameDescription$condA ternary operator that evaluates one expression, and depending on the result returns the value of one following expressions.$ifNullEvaluates an expression and returns a value.
注:以上操作符都必须在管道操作符的表达式内来使用。


各个表达式操作符的具体使用方式参见:
http://docs.mongodb.org/manual/reference/operator/aggregation-group/


四 总结
    
       管道是MongoDB新引入的聚合框架,它使用了大量的管道操作符来固定聚合的接口,每种管道操作符中又可以包含丰富的表达式操作符,每种操作符都有其特定的功能,只有在理解这些操作符使用方法的基础上,才可以写出正确的管道聚合程序。

      下一节,将详细介绍聚合管道的代码编写。





补充  Linux shell中管道概念
      
      POSIX多线程的使用方式中, 有一种很重要的方式-----流水线(亦称为“管道”)方式,“数据元素”流串行地被一组线程按顺序执行。它的使用架构可参考下图:

       

       以面向对象的思想去理解,整个流水线,可以理解为一个数据传输的管道;该管道中的每一个工作线程,可以理解为一个整个流水线的一个工作阶段stage,这些工作线程之间的合作是一环扣一环的。靠输入口越近的工作线程,是时序较早的工作阶段stage,它的工作成果会影响下一个工作线程阶段(stage)的工作结果,即下个阶段依赖于上一个阶段的输出,上一个阶段的输出成为本阶段的输入。这也是pipeline的一个共有特点!

一 聚合管道的优化
  
   1.$sort   $skip   $limit顺序优化
   如果在执行管道聚合时,如果$sort、$skip、$limit依次出现的话,例如:
 { $sort: { age : -1 } },
 { $skip: 10 },
 { $limit: 5 }

那么实际执行的顺序为:
  { $sort: { age : -1 } },
 { $limit: 15 },
 { $skip: 10 }
$limit会提前到$skip前面去执行。
 此时$limit = 优化前$skip+优化前$limit
  这样做的好处有两个:1.在经过$limit管道后,管道内的文档数量个数会“提前”减小,这样会节省内存,提高内存利用效率。2.$limit提前后,$sort紧邻$limit这样的话,当进行$sort的时候当得到前“$limit”个文档的时候就会停止。

2.$limit + $skip + $limit + $skip Sequence Optimization

    如果聚合管道内反复出现下面的聚合序列:
  { $limit: 100 },
  { $skip: 5 },
  { $limit: 10},
  { $skip: 2 }     
    
     首先进行局部优化为:可以按照上面所讲的先将第二个$limit提前:
     { $limit: 100 },
  { $limit: 15},
  { $skip: 5 },
  { $skip: 2 }
     进一步优化:两个$limit可以直接取最小值 ,两个$skip可以直接相加:
     { $limit: 15 },
  { $skip: 7 }

3.Projection Optimization
    
     过早的使用$project投影,设置需要使用的字段,去掉不用的字段,可以大大减少内存。除此之外也可以过早使用

      我们也应该过早使用$match、$limit、$skip操作符,他们可以提前减少管道内文档数量,减少内存占用,提供聚合效率。
       除此之外,$match尽量放到聚合的第一个阶段,如果这样的话$match相当于一个按条件查询的语句,这样的话可以使用索引,加快查询效率。


二 聚合管道的限制
   
    1.类型限制
   
     在管道内不能操作 Symbol, MinKey, MaxKey, DBRef, Code, CodeWScope类型的数据( 2.4版本解除了对二进制数据的限制).

     2.结果大小限制
     
     管道线的输出结果不能超过BSON 文档的大小(16M),如果超出的话会产生错误.

     3.内存限制 

      如果一个管道操作符在执行的过程中所占有的内存超过系统内存容量的10%的时候,会产生一个错误。
      当$sort和$group操作符执行的时候,整个输入都会被加载到内存中,如果这些占有内存超过系统内存的%5的时候,会将一个warning记录到日志文件。同样,所占有的内存超过系统内存容量的10%的时候,会产生一个错误。


三 分片上使用聚合管道

     聚合管道支持在已分片的集合上进行聚合操作。当分片集合上进行聚合操纵的时候,聚合管道被分为两成两个部分,分别在mongod实例和mongos上进行操作。

四 聚合管道使用   
    
    首先下载测试数据:http://media.mongodb.org/zips.json 并导入到数据库中。

1.查询人口树大于一千万的州的名字
 > db.zipcode.aggregate({$group:{_id:"$state",totalPop:{$sum:"$pop"}}},
                                        {$match:{totalPop:{$gte:10*1000*1000}}},
                                        {$sort:{_id:1}})
{
     "result" : [
          {
               "_id" : "CA",
               "totalPop" : 29760021
          },
          {
               "_id" : "FL",
               "totalPop" : 12937926
          },
          {
               "_id" : "IL",
               "totalPop" : 11430602
          },
          {
               "_id" : "NY",
               "totalPop" : 17990455
          },
          {
               "_id" : "OH",
               "totalPop" : 10847115
          },
          {
               "_id" : "PA",
               "totalPop" : 11881643
          },
          {
               "_id" : "TX",
               "totalPop" : 16986510
          }
     ],
     "ok" : 1
}     
2.计算每个州平均每个城市打人口数
> db.zipcode.aggregate({$group:{_id:{state:"$state",city:"$city"},pop:{$sum:"$pop"}}},
                                       {$group:{_id:"$_id.state",avCityPop:{$avg:"$pop"}}},
                                       {$sort:{_id:1}})
{
     "result" : [
          {
               "_id" : "AK",
               "avCityPop" : 2989.3641304347825
          },
          {
               "_id" : "AL",
               "avCityPop" : 7907.2152641878665
          },
          {
               "_id" : "AR",
               "avCityPop" : 4175.355239786856
          },
          {
               "_id" : "AZ",
               "avCityPop" : 20591.16853932584
          },
          {
               "_id" : "CA",
               "avCityPop" : 27581.113067655235
          },
          {
               "_id" : "CO",
               "avCityPop" : 9922.873493975903
          },
          {
               "_id" : "CT",
               "avCityPop" : 14674.625
          },
          {
               "_id" : "DC",
               "avCityPop" : 303450
          },
          {
               "_id" : "DE",
               "avCityPop" : 14481.91304347826
          },
          {
               "_id" : "FL",
               "avCityPop" : 26676.136082474226
          },
………………
………………
………………
          {
               "_id" : "WI",
               "avCityPop" : 7323.00748502994
          },
          {
               "_id" : "WV",
               "avCityPop" : 2759.1953846153847
          },
          {
               "_id" : "WY",
               "avCityPop" : 3359.911111111111
          }
     ],
     "ok" : 1
}  
3.计算每个州人口最多和最少的城市名字
>db.zipcode.aggregate({$group:{_id:{state:"$state",city:"$city"},pop:{$sum:"$pop"}}},
                                      {$sort:{pop:1}},
                                      {$group:{_id:"$_id.state",biggestCity:{$last:"$_id.city"},biggestPop:{$last:"$pop"},smallestCity:{$first:"$_id.city"},smallestPop:{$first:"$pop"}}},
                                      {$project:{_id:0,state:"$_id",biggestCity:{name:"$biggestCity",pop:"$biggestPop"},smallestCity:{name:"$smallestCity",pop:"$smallestPop"}}})
{
     "result" : [ 
               "biggestCity" : {
                    "name" : "GRAND FORKS",
                    "pop" : 59527
               },
               "smallestCity" : {
                    "name" : "TROTTERS",
                    "pop" : 12
               },
               "state" : "ND"
          },
…………………
…………………
………………...
          {
               "biggestCity" : {
                    "name" : "WASHINGTON",
                    "pop" : 606879
               },
               "smallestCity" : {
                    "name" : "PENTAGON",
                    "pop" : 21
               },
               "state" : "DC"
          },
          {
               "biggestCity" : {
                    "name" : "DENVER",
                    "pop" : 451182
               },
               "smallestCity" : {
                    "name" : "CHEYENNE MTN AFB",
                    "pop" : 0
               },
               "state" : "CO"
          }
      ],
     "ok" : 1
}  


五 总结

         对于大多数的聚合操作,聚合管道可以提供很好的性能和一致的接口,使用起来比较简单, 和MapReduce一样,它也可以作用于分片集合,但是输出的结果只能保留在一个文档中,要遵守BSON Document大小限制(当前是16M)。
       管道对数据的类型和结果的大小会有一些限制,对于一些简单的固定的聚集操作可以使用管道,但是对于一些复杂的、大量数据集的聚合任务还是使用MapReduce。

MongoDB 2.1 多了新Feature - Aggregation Framework。最近工作需要就稍微看了下,Mark之。

Overview

    Aggregation 提供的功能map-reduce也能做(诸如统计平均值,求和等)。官方那个大胖子说这东西比map-reduce简单, map-reduce 我没用过, 不过从使用Aggregation的情况来看, 进行统计等操作还是蛮方便的。

    总体而言,Aggregation就是类似 Unix-like中的 管道 的概念,可以将很多数据流串起来,不同的数据处理阶段可以再上一个阶段的基础上再次加工。

Pipeline-Operator

    比较常用的有:    

    •$project -  可以重构数据
    •$match - 可以实现类似query的功能
    •$limit - 限制返回个数,你懂的
    •$skip - 同上
    •$unwind - 可以将一个包含数组的文档切分成多个, 比如你的文档有 中有个数组字段 A, A中有10个元素, 那么                                 经过 $unwind处理后会产生10个文档,这些文档只有 字段 A不同
    •$group - 统计操作, 还提供了一系列子命令
         –$avg, $sum …

    •$sort  - 排序

Usage - Java

    我在db中造了些数据(数据时随机生成的, 能用即可),没有建索引,文档结构如下:


01Document结构:
02  {
03   "_id" : ObjectId("509944545"),
04   "province" "海南",
05   "age" 21,
06   "subjects" : [
07    {
08    "name""语文",
09    "score" 53
10    },
11    {
12    "name""数学",
13    "score" 27
14    },
15    {
16    "name""英语",
17    "score" 35
18    }
19     ],
20   "name" "刘雨"
21  }
     接下来要实现两个功能:
  1.     统计上海学生平均年龄
  2.     统计每个省各科平均成绩

    接下来一一道来

    统计上海学生平均年龄

    从这个需求来讲,要实现功能要有几个步骤: 1. 找出上海的学生. 2. 统计平均年龄 (当然也可以先算出所有省份的平均值再找出上海的)。如此思路也就清晰了

    首先上 $match, 取出上海学生

    1{$match:{'province':'上海'}}
    接下来 用 $group 统计平均年龄


    1{$group:{_id:’$province’,$avg:’$age’}}
    $avg 是 $group的子命令,用于求平均值,类似的还有 $sum, $max ....

    上面两个命令等价于

    1select province, avg(age)
    2 from student
    3 where province = '上海'
    4 group by province

    下面是Java代码

    01Mongo m = new Mongo("localhost"27017);
    02 DB db = m.getDB("test");
    03 DBCollection coll = db.getCollection("student");
    04 
    05 /*创建 $match, 作用相当于query*/
    06 DBObject match = new BasicDBObject("$match", new BasicDBObject("province", "上海"));
    07  
    08 /* Group操作*/
    09 DBObject groupFields = new BasicDBObject("_id", "$province");
    10 groupFields.put("AvgAge", new BasicDBObject("$avg", "$age"));
    11 DBObject group = new BasicDBObject("$group", groupFields);
    12  
    13 /* 查看Group结果 */
    14 AggregationOutput output = coll.aggregate(match, group); // 执行 aggregation命令
    15 System.out.println(output.getCommandResult());
    输出结果:
    1"serverUsed" "localhost/127.0.0.1:27017" ,       
    2  "result" : [
    3    "_id" "上海" "AvgAge" 32.09375}
    4    ] ,         
    5   "ok" 1.0
    6 }
    如此工程就结束了,再看另外一个需求


    统计每个省各科平均成绩

    首先更具数据库文档结构,subjects是数组形式,需要先‘劈’开,然后再进行统计

    主要处理步骤如下:

    1. 先用$unwind 拆数组 2. 按照 province, subject 分租并求各科目平均分

    $unwind 拆数组

    1{$unwind:’$subjects’}
    按照 province, subject 分组,并求平均分
    1{$group:{
    2     _id:{
    3         subjname:”$subjects.name”,   // 指定group字段之一 subjects.name, 并重命名为 subjname
    4         province:’$province’         // 指定group字段之一 province, 并重命名为 province(没变)
    5      },
    6     AvgScore:{
    7        $avg:”$subjects.score”        // 对 subjects.score 求平均
    8     }
    9 }
    java代码如下:


    01Mongo m = new Mongo("localhost"27017);
    02 DB db = m.getDB("test");
    03 DBCollection coll = db.getCollection("student");
    04  
    05 /* 创建 $unwind 操作, 用于切分数组*/
    06 DBObject unwind = new BasicDBObject("$unwind", "$subjects");
    07  
    08 /* Group操作*/
    09 DBObject groupFields = new BasicDBObject("_id", new BasicDBObject("subjname", "$subjects.name").append("province", "$province"));
    10 groupFields.put("AvgScore", new BasicDBObject("$avg", "$subjects.scores"));
    11 DBObject group = new BasicDBObject("$group", groupFields);
    12 
    13 /* 查看Group结果 */
    14 AggregationOutput output = coll.aggregate(unwind, group);  // 执行 aggregation命令
    15 System.out.println(output.getCommandResult());
    输出结果



    01"serverUsed" "localhost/127.0.0.1:27017" ,
    02    "result" : [
    03      "_id" : { "subjname" "英语" "province" "海南"} , "AvgScore" 58.1} ,
    04      "_id" : { "subjname" "数学" "province" "海南"} , "AvgScore" 60.485} ,
    05      "_id" : { "subjname" "语文" "province" "江西"} , "AvgScore" 55.538} ,
    06      "_id" : { "subjname" "英语" "province" "上海"} , "AvgScore" 57.65625} ,
    07      "_id" : { "subjname" "数学" "province" "广东"} , "AvgScore" 56.690} ,
    08      "_id" : { "subjname" "数学" "province" "上海"} , "AvgScore" 55.671875,
    09      "_id" : { "subjname" "语文" "province" "上海"} , "AvgScore" 56.734375,
    10      "_id" : { "subjname" "英语" "province" "云南"} , "AvgScore" 55.7301 } ,
    11      .
    12      .
    13      .
    14      .
    15     "ok" 1.0
    16 }
    统计就此结束.... 稍等,似乎有点太粗糙了,虽然统计出来的,但是根本没法看,同一个省份的科目都不在一起。囧


    接下来进行下加强, 

    支线任务: 将同一省份的科目成绩统计到一起( 即,期望 'province':'xxxxx', avgscores:[ {'xxx':xxx}, ....] 这样的形式)

    要做的有一件事,在前面的统计结果的基础上,先用 $project 将平均分和成绩揉到一起,即形如下面的样子

    1"subjinfo" : { "subjname" "英语" ,"AvgScores" 58.1 } ,"province" "海南" }

    再按省份group,将各科目的平均分push到一块,命令如下:

    $project 重构group结果

    1{$project:{province:"$_id.province", subjinfo:{"subjname":"$_id.subjname""avgscore":"$AvgScore"}}
    $使用 group 再次分组
    1{$group:{_id:"$province", avginfo:{$push:"$subjinfo"}}}
    java 代码如下:
    01Mongo m = new Mongo("localhost"27017);
    02DB db = m.getDB("test");
    03DBCollection coll = db.getCollection("student");
    04             
    05/* 创建 $unwind 操作, 用于切分数组*/
    06DBObject unwind = new BasicDBObject("$unwind", "$subjects");
    07             
    08/* Group操作*/
    09DBObject groupFields = new BasicDBObject("_id", new BasicDBObject("subjname", "$subjects.name").append("province", "$province"));
    10groupFields.put("AvgScore", new BasicDBObject("$avg", "$subjects.scores"));
    11DBObject group = new BasicDBObject("$group", groupFields);
    12             
    13/* Reshape Group Result*/
    14DBObject projectFields = new BasicDBObject();
    15projectFields.put("province", "$_id.province");
    16projectFields.put("subjinfo", new BasicDBObject("subjname","$_id.subjname").append("avgscore", "$AvgScore"));
    17DBObject project = new BasicDBObject("$project", projectFields);
    18             
    19/* 将结果push到一起*/
    20DBObject groupAgainFields = new BasicDBObject("_id", "$province");
    21groupAgainFields.put("avginfo", new BasicDBObject("$push", "$subjinfo"));
    22DBObject reshapeGroup = new BasicDBObject("$group", groupAgainFields);
    23  
    24/* 查看Group结果 */
    25AggregationOutput output = coll.aggregate(unwind, group, project, reshapeGroup);
    26System.out.println(output.getCommandResult());

    结果如下:

    01"serverUsed" "localhost/127.0.0.1:27017" ,
    02  "result" : [
    03       "_id" "辽宁" "avginfo" : [ { "subjname" "数学" "avgscore" 56.46666666666667} , { "subjname" "英语" "avgscore" 52.093333333333334} , { "subjname" "语文" "avgscore" 50.53333333333333}]} ,
    04       "_id" "四川" "avginfo" : [ { "subjname" "数学" "avgscore" 52.72727272727273} , { "subjname" "英语" "avgscore" 55.90909090909091} , { "subjname" "语文" "avgscore" 57.59090909090909}]} ,
    05       "_id" "重庆" "avginfo" : [ { "subjname" "语文" "avgscore" 56.077922077922075} , { "subjname" "英语" "avgscore" 54.84415584415584} , { "subjname" "数学" "avgscore" 55.33766233766234}]} ,
    06       "_id" "安徽" "avginfo" : [ { "subjname" "英语" "avgscore" 55.458333333333336} , { "subjname" "数学" "avgscore" 54.47222222222222} , { "subjname" "语文" "avgscore" 52.80555555555556}]}
    07    .
    08    .
    09    .
    10   ] , "ok" 1.0}
    至此,功能也就完成了,呼。


0 0
原创粉丝点击