mongoDB-数据聚合的三种方式

00x1 group

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

1.1 group结构

1
2
3
4
5
6
7
8
db.test.group({
key:{field:true} //key为分组依据,相当于aggregate中的$group
initial:{count:0} //在分组前对变量初始化,这里声明的变量在下面回调函数中作为result的属性使用。
condition://过滤条件,相当于aggregate中的$match。
reduce:function ( curr, result ) {} //第一个参数为当前分组中此时迭代到的文档对象,第二个参数为当前分组
"$keyf":function() {return } //定义分组函数
finalize:function(result){} //这里的result为reduce的result,代表当前分组。此函数对完成当前分组后回调。
})

  Group有传入的命令中共有六个参数,其中三个是JavaScript函数,因此每次查询到匹配的数据,都会被转换为对象传入函数。从运行效率上来说,Group比Aggregate差一大截。

1.2 使用场景

  对返回数据最多只包含20000个元素,最多支持20000独立分组。

00x2 aggregate

  aggregate是mongoDB中经常提起的“管道”。主要用于处理数据(如求和,统计平均值等),并返回计算后的数据结构。
  aggreagte是一个数组,其中包含多个对象(命令),通过遍历Pipleline数组对collection中的数据进行操作。
  下面介绍一下aggregate的聚合管道比较常用的几种操作:

2.1 $project

  修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。

1
2
3
4
5
6
db.testtest.aggregate({
$project:{
"_id":1,
"name":1
}
})

2.2 $match

  用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。

1
2
3
4
5
db.testtest.aggregate({
$match:{
"count":"3",
}
})

2.3 $limit

  用来限制MongoDB聚合管道返回的文档数。

1
2
3
db.testtest.aggregate({
$limit:5
})

2.4 $skip

  在聚合管道中跳过指定数量的文档,并返回余下的文档。

1
2
3
db.testtest.aggregate({
$skip:8
})

2.5 $unwind

  将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。

1
2
3
db.testtest.aggregate({
$unwind:"$identlist"
})

2.6 $group

  将集合中的文档分组,可用于统计结果。

1
2
3
db.testtest.aggregate({
$group:{"_id":"$count"}
})

2.7 $sort

  将输入文档排序后输出

1
2
3
db.testtest.aggregate({
$sort:{"count":1}
})

2.8 使用场景

  应用于常用的聚合操作;对聚合响应性能有一定要求时(索引及组合优化);管道操作在中完成,由于内存有大小限制,处理的数据集大小有限。

00x3 MapReduce

3.1 MapReduce结构

  mapreduce是mongoDB中提供的用于数据聚合的一种方式。通过对集合中的各个满足条件的文档进行预处理,整理出想要的数据然后统计得到最终的统计结果。
  mapreduce的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
db.runCommand({
mapreduce:<collection>, //需要进行处理的集合名
map:<mapfunction>, //映射函数(分组)
reduce:<reducefunction>, //统计函数
[,query:<query filter object>] //,在发往map函数之前,对文档进行过滤
[,sort:<sorts the input objects using this key.Useful for optimization,like sorting by the emit key for fewer reduces>] //在发往map函数之前,对文档进行排序
[,limit:<number of objects to return from collection>] //限制发往map函数的文档数量
[,out:<see output options below>] //新建集合,用于存放统计结果
[,keeptemp:<true|false>] //是否保存统计结果为临时集合
[,finalize:<finalizefunction>] //最终处理函数,对reduce返回结果(存入out之前)进行最终处理
[,scope:<object where fields go into javascript global scope>] //向map、reduce、finalize导入外部变量
[,verbose:true] //详细的统计信息,用于调试
});

  使用MapReduce主要需要实现两个函数:Map函数和Reduce函数。接下来详细介绍这两个函数。

3.2 Map函数

  可以将Map函数理解为分组,调用emit(key,values),遍历collection中所有的记录。其中,emit中的key为分组依据;values为分组后需要保留的数据,为1时则统计该分组的值的个数。
  key对应最后结果集中的_id。经过Map函数处理的集合,每条数据中只有”key”和”values”两个字段。

3.3 Reduce函数

  Reduce为统计函数,接受Map函数处理后返回的key和values作为参数,将key-values变成key-value,也就是把values数组变成一个个单一的value。当key-values中的values数组过大时,会被再切分成很多个小的key-values,再对这些小的key-values分别执行Reduce,再将多个块的结果组合成一个新的数组,作为Reduce函数的第二个参数,继续Reduce操作。这个类似于多阶的归并排序。

3.4 out和keeptemp

  out:
  在文档输出时,output是可选的,一般结构为{ “out”: option }。
  option可以有以下几个选项。

1
2
3
4
{ replace : "collection name" } – mapReduce的输出结果会替换掉原来的collection,collection不存在则创建。
{ merge : "collection name" } – 将新老数据进行合并,新的替换旧的,没有的添加进去。
{ reduce : "collection name" } – 存在老数据时,在原来基础上加新数据(即 new value = old value + mapReduce value)。
{ inline : 1 } – 不会创建collection,结果保存在内存里,只限于结果小于16MB的情况。

  通常结构为{“out”:”collection name”},如果collection不存在,就新建一个集合。

  keeptemp
  值只能为true或者false,表明输出到的collection是否是临时的,如果想在连接关闭后任然保留这个集合,则需要指定keeptemp的值为”true”。在使用output的情况下,不必指定keeptemp为true。

3.5 使用场景

  聚合要求复杂;大型数据集

00x4 三者比较

group aggregate MapReduce
是否使用JavaScript引擎 是,定制reduce函数 是,不能编写自定义函数 是,MapReduce函数是用JavaScript编写的
返回结果集保存位置 内联,结果必须符合BSON文档的限制(当前是16Mb) 内联,服务器支持的最大文档大小(16Mb),超过时会报错 内联、新集合、合并、替换、减少
处理数据集大小 将不会分组到一个超过10,000个键的结果集 操作在内存中完成,有内存大小限制,处理数据集大小有限 大型数据集,超过20000的独立分组建议采用MapReduce
处理性能 低于aggregate 较高,管道可重复使用 低于aggregate
灵活度 低于MapReduce 低于MapRduce 较高,能使用JavaScript

00x5 从一个小例子具体分析

    为公司的每个用户分配一张卡(有唯一的卡号”_id”);持有该卡的用户可以使用这张卡在不同的超市消费,每个超市都有一个标识码,用”identlist”存放用户消费过的超市标识码;持有该卡的用户名字用”name”表示,默认在该公司中,每个人的姓名都是唯一的,与”_id”一一对应;”eventline”列举用户每次消费购买的物品;”timeline”记录每次消费的时间;”newtimeline”为用户最近一次消费的时间。
   示例数据如下:
mockjs
   知道最近有几家超市做促销(数组A),要求(1)获取在A中任意一家或多家超市消费过的卡的持有者;(2)这些卡的最新消费时间;(3)这些卡的累积消费次数;(4)根据最新消费时间/累积消费次数对获取到的这些卡的数据进行排序;对排序后的数据进行分页。
   分析:要求中最难的是第一步:遍历数组A,将A中的每一条数据,作为分组依据(可能将原来的一条数据拆分成几条);再对分组后的数据以”_id”进行聚合。

5.1 使用MapReduce实现以上要求。

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
28
29
var map = function(){
emit(this.name,this.timeline);
};
var reduce = function(key,values){
return Array.sum(values);
};
db.testtest.mapReduce(
map,
reduce,
{
query:{$where:function(){
var arr = new Array("Jk3Nx5-YUxBJZ-Zklt","LMuHW7-JtnwQC-OBIh");
for(var index in arr){
for(var current in this.identlist){
if(this.identlist[current] == arr[index]){
return true;
}
return false;
}
}
}},
sort:{"count":1},
finalize:function(key,value){return {count:value.length,time:value[0]};},
out:"bbb_result"
}
)
db.bbb_result.find().skip(1).limit(5)

5.2 使用aggregate

1
2
3
4
5
6
7
8
9
10
11
12
var arr = new Array("wdeVyU-YVutsF-CEza","Jk3Nx5-YUxBJZ-Zklt");
db.getCollection('testtest').aggregate([
{'$unwind':"$identlist"},
{"$match":{"identlist":{"$in":arr}}},
{$group:{_id:"$name",timeline:{$addToSet:"$timeline"}}},
{'$unwind':"$timeline"},
{'$unwind':"$timeline"},
{$group:{_id:"$_id",time:{$first:"$timeline"},count:{"$sum":1}}},
{$sort:{"count":-1}},
{$skip:0},
{$limit:5}
])

   以上两个小例子经测试后均能实现要求,这里不做详细解释,需要的请自取~

Miss Me wechat
light