好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

MongoDB的聚合框架Aggregation Framework入门学习教程

1. 聚合框架
使用聚合框架对集合中的文档进行变换和组合,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering),投射(projecting),分组(grouping),排序(sorting),限制(limiting),跳过(skipping)。
例如一个保存着动物类型的集合,希望找出最多的那种动物,假设每种动物被保存为一个mongodb文档,可以按照以下步骤创建管道。
1)将每个文档的动物名称映射出来。
2)安装名称排序,统计每个名称出现的次数。
3)将文档按照名称出现的次数降序排列。
4)将返回结果限制为前五个。
具体操作符:
1){"$porject", {"name" : 1}}
类似于查询阶段的字段选择器,指定"fieldname" : 1选定需要的字段,"fieldname" : 0排除不需要的字段,"_id"字段自动显示。结果保存在内存中,不会写入磁盘。

?

1

2

db.test_collection.aggregate({ "$project" : { "name" : 1}});    =>

{ "_id" : ObjectId( "535a2d3c169097010b92fdf6" ), "name" : "snake" }

2){"$group", {"_id" : "$name", "count" : {"$sum" : 1}}}
首先指定了分组的字段"name",该操作执行完后,每个name只对应一个结果,所有可以将name指定为唯一标识符"_id"。
第二个字段表明分组内的每个文档"count"字段加1。新加入的文档中不会有count字段。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

db.test_collection.aggregate({ "$project" : { "name" : 1}}, { "$group" : { "_id" : "$name" , "count" : { "$sum" : 1}}});    =>

{ "_id" : "bird" , "count" : 8344 }

{ "_id" : "snake" , "count" : 8443 }

{ "_id" : "cat" , "count" : 8183 }

{ "_id" : "rabbit" , "count" : 8206 }

{ "_id" : "tiger" , "count" : 8329 }

{ "_id" : "cow" , "count" : 8309 }

{ "_id" : "horse" , "count" : 8379 }

{ "_id" : "dog" , "count" : 8406 }

{ "_id" : "dragon" , "count" : 8372 }

{ "_id" : "elephant" , "count" : 8264 }

{ "_id" : "pig" , "count" : 8403 }

{ "_id" : "lion" , "count" : 8362 }

3){"$sort" : {"count" : -1}}
对结果集中的文档根据count字段做降序排列。
4){"$limit" : 5}
将返回结果限制为5个文档。
将上述结果综合起来:

?

1

2

3

4

5

6

7

db.test_collection.aggregate(

{

   "$project" : { "name" : 1}},

   { "$group" : { "_id" : "$name" , "count" : { "$sum" : 1}}},

   { "$sort" : { "count" : -1}},

   { "$limit" : 5}

);

aggregate会返回一个文档数组,内容为出现次数最多的5个动物:

?

1

2

3

4

5

{ "_id" : "snake", "count" : 8443 }

{ "_id" : "dog", "count" : 8406 }

{ "_id" : "pig", "count" : 8403 }

{ "_id" : "horse", "count" : 8379 }

{ "_id" : "dragon", "count" : 8372 }

调试过程中。可以逐一对管道符进行排查。
聚合框架不能对集合进行写入操作,所有结果返回给客户端,聚合结果必须限制在16M以内。

2. 管道操作符
每个操作符都会接受一连串的文档,对这些文档进行类型转换,最后得到的文档作为结果传递给下一操作符。
不同的管道操作符可以将任意顺序组合在一起使用,而且可以被重复任意多次。

2.1 $match
$match用于对文档集合进行筛选,之后得到的文档子集做聚合。
"$match"支持所有的常规查询操作符("$gt","$lt","$ne")等,不能使用地理空间操作符。
实际操作中尽量将"$match"放在管道的前面部分,一方面可以提快速将不需要的文档过滤掉,另外在映射和分组前筛选,查询可以使用索引。

2.2 $project
使用"$project"可以提取字段,可以重命名字段,

?

1

2

db.foo.aggregate({ "$project" : { "city" : 1, "_id" : 0}})    =>

{ "city" : "NEW WORK" }

可以将投射过的字段重命名:

?

1

2

db.foo.aggregate({ "$project" : { "newcity" : "$city" , "_id" : 0}})    =>

{ "newcity" : "NEW WORK" }

使用"$fieldname"语法为了在聚合框架中引用fieldname字段,例如上面"$city"会被替换为"NEW WORK"。
对字段重命名后,Mongdb不会记录其记录字段的历史名称,所以应该在修改字段名称前使用索引。
2.2.1 管道表达式
可以使用表达式将多个字面量和变量组合为一个值。
可以使用组合或者任意深度的嵌套,创建复杂的表达式。
2.2.2 数学表达式
数学表示式用来操作数据运算。

?

1

2

3

4

5

6

7

8

9

db.foo.aggregate(

   { "$project" :

     { "total" :

       { "$add" : [ "$age" , "$year" ]},

       "_id" : 0

     }

   }

)

{ "total" : 15}

可以将多个表达式组合为更为复杂的表达式:

?

1

2

3

4

5

6

7

8

9

db.foo.aggregate(

   { "$project" :

     { "sub" :

       { "$subtract" : [{ "$add" : [ "$age" , "$year" ]}, 7]},

       "_id" : 0

     }

   }

)

{ "sub" : 8 }

操作符语法:
1)"$add" : [expr1, [, expr2, ..., exprN]]
将表达式相加
2)"$subtract" : [expr1, expr2]
表达式1减去表达式2
3)"$multiply" : [expr1, [, expr2, ..., exprN]]
将表达式相乘
4)"$divide" : [expr1, expr2]
表达式1除以表达式2得到商
5)"$mod" : [expr1, expr2]
表达式1除以表达式2得到余数

2.2.3 日期表达式
用于提取日期信息的表达式:"$year","$month","$week","$dayOfMonth","$dayOfweek","$hour","$minute","$second"。只能对日期类型的字段进行日期操作,不能对数值类型进行日期操作。

?

1

2

3

4

5

6

7

8

9

10

db.bar.insert({ "name" : "pipi" , "date" : new Date()})

db.bar.aggregate(

   { "$project" :

     { "birth-month" :

       { "$month" : "$date" },

       "_id" : 0

     }

   }

)

{ "birth-month" : 4 }

也可以使用字面量日期。

?

1

2

3

4

5

6

7

8

9

db.bar.aggregate(

   { "$project" :

     { "up-to-now" :

       { "$subtract" : [{ "$minute" : new Date()}, { "$minute" : "$date" }]},

       "_id" : 0

     }

   }

)

{ "up-to-now" : 18 }

2.2.3 字符串表达式
操作符语法:
1)"$substr" : [expr, startOffset, numoReturn]
接受字符串,起始位置以后偏移N个字节,截取字符串。
2)"$concat" : [expr1[, expr2, ..., exprN]]
将给定的表达式连接在一起作为返回结果。
3)"$toLower" : expr
返回参数的小写形式
4)"$toUpper" : expr
返回参数的大写形式
例如:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

db.foo.insert({ "firstname" : "caoqing" , "lastname" : "lucifer" })

db.foo.aggregate(

{

   "$project" : {

     "email" : {

       "$concat" : [

         { "$substr" : [ "$firstname" , 0, 1]},

         "." ,

         "$lastname" ,

         "@gmail.com"

           ]

         },

         "_id" : 0

       }

   }

)

{ "email" : "c.lucifer@gmail.com" }

2.2.3 逻辑表达式
操作符语法:
1)"$cmp" : [expr1, expr2]
比较两个参数,相等返回0,大于返回整数,小于返回负数。
2)"$strcasecmp" : [string1, string2]
比较字符串,区分大小写
3)"$eq"/"$ne"/"$gt"/"$gte"/"lt"/"lte" : [expr1, expr2]
比较字符串,返回结果(true or false)
4)"$and" : [expr1[, expr2, ..., exprN]]
所有值为true返回true,否则返回false。
5)"$or" : [expr1[, expr2, ..., exprN]]
任意表达式为true返回true,否则返回false
6)"$not" : expr
对表示式取反
还有两个控制语句。

?

1

"$crond" : [booleanExpr, trueExpr, falseExpr]

如果为true,返回trueExpr,否则,返回falseExpr。

?

1

"$ifFull" : [expr, replacementExpr]

如果expr为null,返回replacementExpr,否则返回expr。
算术操作符必须接受数值,日期操作符必须接受日期,字符串操作符必须接受字符串。
例如,根据学生出勤率(10%),平时作业(30%)和考试成绩(60%)得出最终成绩,如果是老师宠爱的学生,直接得100分:
插入数据:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

db.bar.insert(

   {

     "name" : "xiaobao" ,

     "teachersPet" : 1,

     "attendance" : 90,

     "quizz" : 80,

     "test" : 85

   }

)

db.bar.insert(

   {

     "name" : "caoqing" ,

     "teachersPet" : 0,

     "attendance" : 20,

     "quizz" : 50,

     "test" : 90

   }

)

db.bar.insert(

   {

     "name" : "pipi" ,

     "teachersPet" : 0,

     "attendance" : 100,

     "quizz" : 50,

     "test" : 10

   }

)

聚合:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

db.bar.aggregate(

   {

     "$project" : {

       "grade" : {

         "$cond" : [

           "$teachersPet" ,

           100,

         {

           "$add" : [

             { "$multiply" : [0.1, "$attendance" ]},

             { "$multiply" : [0.3, "$quizz" ]},

             { "$multiply" : [0.6, "$test" ]},

           ]

         }

         ]

       },

       "_id" : 0

     }

   }

)

返回结果:

?

1

2

3

{ "grade" : 100 }

{ "grade" : 71 }

{ "grade" : 31 }

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

3.1 找出集合中的所有键
MongoDB假设你的模式是动态的,所以并不会跟踪记录每个文档的键。通常找到集合中所有文档的所有键的最好方式就是MapReduce。
在映射环节,map函数使用特别的emit函数返回要处理的值。emit会给MapReduce一个键和一个值。
这里用emit将文档某个键的计数返回。this就是当前映射文档的引用:

?

1

2

3

map = function () {

   emit( this .country, {count : 1});

}

reduce接受两个参数,一个是key,就是emit返回的第一个值,还有一个数组,由一个或多个键对应的{count : 1}文档组成。

?

1

2

3

4

5

6

7

reduce = function (key, value) {

   var result = {count : 0};

   for ( var i = 0; i < value.length; i++) {

     result.count += value[i].count;

   }

   return result;

}

示例表数据:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

{ "_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 }

运行结果:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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" )

     },

}

(1)result:存放的集合名
(2)timeMillis:操作花费的时间,单位是毫秒
(3)input:传入文档数目
(4)emit:此函数被调用的次数
(5)reduce:此函数被调用的次数
(6)output:最后返回文档的个数
查看下collection结果内容:

?

1

2

3

4

5

6

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其他的键
(1)"finalize" : function
可以将reduce的结果发送给这个键,这是整个处理过程的最后一步。
(2)"keeptemp自动为true。" : boolean
如果为true,则在连接关闭后结果保存,否则不保存。
(3)"out" : string
输出集合的名称,如果设置,keeptemp自动为true。
(4)"query" : document
在发往map前,先用指定条件过滤文档。
(5)"sort" : document
在发往map前,先进行排序。
(6)"limit" : integer
发往map函数的文档数量上限。
(7)"scope" : document
可以在javascripts代码中使用的变量。
(8)"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返回集合中的文档数量:

?

1

2

db.foo.count() =>

99

可以传入一个查询文档:

?

1

2

db.foo.count({country : "china" }) =>

15

增加查询条件会使count变慢。
4.2 distinct
distinct用来找出给定键的所有不同值。使用时必须指定集合和键。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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可以进行更为复杂的聚合。先选定分组所依据的键,然后根据选定键的不同值分为若干组,然后对每一个分组进行聚合,得到结果文档。
插入示例数据:

?

1

2

3

4

5

6

7

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最新的文档,将其加入结果集。

?

1

2

3

4

5

6

7

8

9

10

11

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;

     }

   }

}})

(1)"ns" : "coll"
指定进行分组的集合。
(2)"key" : {"name" : true}
指定分组依据的键。
(3)"initial" : {"time" : 0}
初始化time值,作为初始Wednesday传递给后续过程。每组成员都会使用这个累加器。
结果:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

"$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这样的形式。如下:

?

1

db.coll.insert({age : 5, time : new Date().getTime()})

返回结果:

?

1

2

3

4

5

6

7

8

9

...

{

   "name" : null ,

   "time" : 1399180685288,

   "age" : 5

}

"count" : 10001,

"keys" : 4,

...

为了排除不包含指定用于分组的键的文档,可以在"condition"中加入"name":{"$exists" : true}。

?

1

2

3

4

5

6

7

8

9

10

11

12

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"键,

?

1

2

3

4

5

6

db.foo.group({

   "ns" : "foo" ,

   "$keyf" : function (x) { return x.category.toLowerCase(); };

   "initial" : ...,

   ......

})

 

查看更多关于MongoDB的聚合框架Aggregation Framework入门学习教程的详细内容...

  阅读:26次