学习MongoDB--(6-1):聚合(初级聚合函数使用)
2012-08-12 13:40
405 查看
前面主要介绍了MongoDB的基础操作及索引相关内容,同关系型数据库一样,MongoDB同样也提供了对聚合函数的支持!并且因为其集合没有模式这种特殊性,MongoDB对聚合还有了更强大的支持!
这次我们先来看看一些基本的聚合函数的使用。
【count】
集合的count函数是最简单的聚合函数,返回集合中文档的数量,也可以接受一个查询文档,统计符合这个查询的文档数量:
如上例,不使用任何参数的count,不论集合有多大,都会很快返回结果!使用参数后,会让count函数执行变慢。
【distinct】
用于找出一个集合中,给定键的所有不同的值!
我们往集合的distinct函数中传递键的名称,返回一个数组,包含这个键在集合中的所有值!我们还可通过在数据库上运行命令,来执行distinct聚合函数,此时我们必须指定集合和键:
命令接受的文档参数中,键"distinct"指定统计的集合名称,键"key"指定统计的键的名称!返回一个文档,键“value”指定统计的键在该集合中的所有值!我们还可以看出,在统计时还使用了索引!
【group】
group聚合可以做稍微复杂一些的操作,其执行过程为:先按照指定的键对集合中的文档进行分组,然后通过聚合每一组中的所有文档,来产生最终的结果文档。我们现在有这样一个集合,其中记录了所有蔬菜的价格并会定期刷新,我们要统计一下各个蔬菜的最新价格,即我们首先需要按照蔬菜名称进行分组,然后对每一组文档进行处理,找出最新的价格,我们先通过运行数据库命令的方式来使用group:
从返回的结果文档中,我们可以看到,键“retval”指向的文档数组就是我们需要的所有蔬菜最新价格。我们来看看,运行“group”命令的参数文档的所有键的意义:
1》 “ns” : 字符串,指定要进行操作的集合名称
2》 “key”: 文档,指定要进行分组所使用的键,此处可以指定多个键,分组将按照这些键的值进行,此处会区分大小写!对于某些应用,我们可能要分组不去区分值的大小写,这个后面会提到如何进行!
3》 “initial” : 累加器文档!分组后,会对每一组执行reduce函数,reduce函数会接受两个文档参数,第一个是分组当前遍历的文档,第二个就是initial键指定的累加器文档!每一组所有文档执行reduce都会使用这个文档,所以改变会一直保留在累加器文档中!
4》 “$reduce”: 函数。分组后,会通过这个函数对每个分组进行聚合!注意聚合时,每一组会有一个独立的累加器文档,结束后,累加器文档中即记录聚合结果!
5》 “condition”: 条件。在对集合进行分组时,我们可以过滤部分文档,这里的过滤条件就是我们前面提到的查询条件!可以使用前面提到的各种查询条件操作符!这个键也可以缩写为“cond” 或 “q” 。如上例,我们只想查看“tomato” 和“cucumber” 的最新价格,我们可以这样写:(分别演示了这个键的3种写法)
【使用完成器】
上述我们可以看到group操作的返回值,其中键“retval”指向所有聚合后的文档,这些就是客户端需要的文档。group操作的返回值文档最大为4MB,因为有这个大小的限制和考虑到效率,我们有时需要对这些聚合后得到的文档进行进一步地修剪后再返回!比如,我们有这样一个blog集合,其中每一个文档是一个blog,我们这次要统计的是,每个作者在写博客时最常使用的tag(标签),我们先看看这个集合结构:
按照上述的统计方式,我们会这样做:
从上述结果我们可以看出,返回值中,包含所有作者使用的所有的tag的次数统计,我们可以将这个文档返回给客户端,然后由客户端去比较得到每个作者用得最多的tag。但这样做会产生很多开销影响效率,对于大的集合的统计尤为明显!我们此时可以通过在统计时使用“finalize”键来进一步处理reduce后的结果:
通过在group操作中使用键"finalize",我们看到最后返回的结果简洁清晰了很多,并且就是客户想要的结果。这个finalize函数写得有点问题就是,如果使用最多的tag有多个,他只会得到第一个!这个可以通过改进这个finalize函数来进行!
【将函数作为键使用】
上面我们都是通过直接指定某一个键,让其值作为分组的基准!我们也提到了,按照值分组会区分值得大小写!我们同样拿查询蔬菜最新价格来距离,我们先看不做任何处理的结果:
我们看到这不是我们想要的结果,group将“tomato” 和“Tomato”作为两个组来对待!通过在group函数中使用键“$keyf” 代替键“key” 来指定分组依赖的键,会解决这个问题,使用方位为:
注意键“$keyf”后面是一个函数,function(doc){....},参数doc就表示当前需要提取分组键的文档!这个函数会对这个文档的特定分组键的值进行一些处理!此处进行的是,将键“name”的值全部转化为小写!注意这个函数最后也必须返回一个文档!
我们上述演示的所有group操作,全是通过运行数据库命令实现的!实际上,MongoDB为集合也提供了group函数!使用方法同上,这里就不做演示了。
这次我们先来看看一些基本的聚合函数的使用。
【count】
集合的count函数是最简单的聚合函数,返回集合中文档的数量,也可以接受一个查询文档,统计符合这个查询的文档数量:
> db.user.find(); { "_id" : ObjectId("5020faf5d6acd1b2a3fb316f"), "name" : "tim", "age" : 40, "registered" : ISODate("2007-03-02T16:00:00Z") } { "_id" : ObjectId("5020fb08d6acd1b2a3fb3170"), "name" : "tom", "age" : 29, "registered" : ISODate("2009-07-02T16:00:00Z") } { "_id" : ObjectId("5020fb27d6acd1b2a3fb3171"), "name" : "jimmy", "age" : 18, "registered" : ISODate("2009-09-02T16:00:00Z") } > db.user.count(); 3 > db.user.count({"name":"tim"}); 1 >
如上例,不使用任何参数的count,不论集合有多大,都会很快返回结果!使用参数后,会让count函数执行变慢。
【distinct】
用于找出一个集合中,给定键的所有不同的值!
> db.user.find(); { "_id" : ObjectId("5020faf5d6acd1b2a3fb316f"), "name" : "tim", "age" : 40, "registered" : ISODate("2007-03-02T16:00:00Z") } { "_id" : ObjectId("5020fb08d6acd1b2a3fb3170"), "name" : "tom", "age" : 29, "registered" : ISODate("2009-07-02T16:00:00Z") } { "_id" : ObjectId("5020fb27d6acd1b2a3fb3171"), "name" : "jimmy", "age" : 18, "registered" : ISODate("2009-09-02T16:00:00Z") } > db.user.distinct("name"); [ "jimmy", "tim", "tom" ] >
我们往集合的distinct函数中传递键的名称,返回一个数组,包含这个键在集合中的所有值!我们还可通过在数据库上运行命令,来执行distinct聚合函数,此时我们必须指定集合和键:
> db.user.find(); { "_id" : ObjectId("5020faf5d6acd1b2a3fb316f"), "name" : "tim", "age" : 40, "registered" : ISODate("2007-03-02T16:00:00Z") } { "_id" : ObjectId("5020fb08d6acd1b2a3fb3170"), "name" : "tom", "age" : 29, "registered" : ISODate("2009-07-02T16:00:00Z") } { "_id" : ObjectId("5020fb27d6acd1b2a3fb3171"), "name" : "jimmy", "age" : 18, "registered" : ISODate("2009-09-02T16:00:00Z") } > db.user.distinct("name"); [ "jimmy", "tim", "tom" ] > db.runCommand({"distinct":"user", "key":"name"});
{
"values" : [
"jimmy",
"tim",
"tom"
],
"stats" : {
"n" : 3,
"nscanned" : 3,
"nscannedObjects" : 0,
"timems" : 78,
"cursor" : "BtreeCursor name_1_age_1"
},
"ok" : 1
}
命令接受的文档参数中,键"distinct"指定统计的集合名称,键"key"指定统计的键的名称!返回一个文档,键“value”指定统计的键在该集合中的所有值!我们还可以看出,在统计时还使用了索引!
【group】
group聚合可以做稍微复杂一些的操作,其执行过程为:先按照指定的键对集合中的文档进行分组,然后通过聚合每一组中的所有文档,来产生最终的结果文档。我们现在有这样一个集合,其中记录了所有蔬菜的价格并会定期刷新,我们要统计一下各个蔬菜的最新价格,即我们首先需要按照蔬菜名称进行分组,然后对每一组文档进行处理,找出最新的价格,我们先通过运行数据库命令的方式来使用group:
> db.vegetableprice.find(); { "_id" : ObjectId("50271b4ae02ab93d5c5be795"), "name" : "tomato", "price" : 3.3, "time" : ISODate("2012-08-12T02:56:10.303Z") } { "_id" : ObjectId("50271b58e02ab93d5c5be796"), "name" : "tomato", "price" : 3.5, "time" : ISODate("2012-08-12T02:56:24.843Z") } { "_id" : ObjectId("50271b95e02ab93d5c5be797"), "name" : "eggplant", "price" : 5.6, "time" : ISODate("2012-08-12T02:57:25.605Z") } { "_id" : ObjectId("50271ba6e02ab93d5c5be798"), "name" : "cucumber", "price" : 4.7, "time" : ISODate("2012-08-12T02:57:42.031Z") } { "_id" : ObjectId("50271bafe02ab93d5c5be799"), "name" : "eggplant", "price" : 5.9, "time" : ISODate("2012-08-12T02:57:51.001Z") } { "_id" : ObjectId("50271bb7e02ab93d5c5be79a"), "name" : "cucumber", "price" : 4.3, "time" : ISODate("2012-08-12T02:57:59.363Z") } { "_id" : ObjectId("50271bdae02ab93d5c5be79b"), "name" : "bean", "price" : 8.9, "time" : ISODate("2012-08-12T02:58:34.931Z") } > db.runCommand({"group" : { ... "ns" : "vegetableprice", ... "key" : {"name" : true}, ... "initial" : {"time" : 0}, ... "$reduce" : function(doc, prev) { ... if(doc.time > prev.time) { ... prev.time = doc.time; ... prev.price = doc.price; ... } ... } ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T02:56:24.843Z"), "price" : 3.5 }, { "name" : "eggplant", "time" : ISODate("2012-08-12T02:57:51.001Z"), "price" : 5.9 }, { "name" : "cucumber", "time" : ISODate("2012-08-12T02:57:59.363Z"), "price" : 4.3 }, { "name" : "bean", "time" : ISODate("2012-08-12T02:58:34.931Z"), "price" : 8.9 } ], "count" : 7, "keys" : 4, "ok" : 1 } >
从返回的结果文档中,我们可以看到,键“retval”指向的文档数组就是我们需要的所有蔬菜最新价格。我们来看看,运行“group”命令的参数文档的所有键的意义:
1》 “ns” : 字符串,指定要进行操作的集合名称
2》 “key”: 文档,指定要进行分组所使用的键,此处可以指定多个键,分组将按照这些键的值进行,此处会区分大小写!对于某些应用,我们可能要分组不去区分值的大小写,这个后面会提到如何进行!
3》 “initial” : 累加器文档!分组后,会对每一组执行reduce函数,reduce函数会接受两个文档参数,第一个是分组当前遍历的文档,第二个就是initial键指定的累加器文档!每一组所有文档执行reduce都会使用这个文档,所以改变会一直保留在累加器文档中!
4》 “$reduce”: 函数。分组后,会通过这个函数对每个分组进行聚合!注意聚合时,每一组会有一个独立的累加器文档,结束后,累加器文档中即记录聚合结果!
5》 “condition”: 条件。在对集合进行分组时,我们可以过滤部分文档,这里的过滤条件就是我们前面提到的查询条件!可以使用前面提到的各种查询条件操作符!这个键也可以缩写为“cond” 或 “q” 。如上例,我们只想查看“tomato” 和“cucumber” 的最新价格,我们可以这样写:(分别演示了这个键的3种写法)
> db.runCommand({"group":{ ... "ns":"vegetableprice", ... "key":{"name":true}, ... "initial":{"time":0}, ... "$reduce":function(doc, prev){ ... if(doc.time > prev.time){ ... prev.time = doc.time; ... prev.price = doc.price; ... }}, ... "condition":{"name":{"$in":["tomato", "cucumber"]}} ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T02:56:24.843Z"), "price" : 3.5 }, { "name" : "cucumber", "time" : ISODate("2012-08-12T02:57:59.363Z"), "price" : 4.3 } ], "count" : 4, "keys" : 2, "ok" : 1 } > db.runCommand({"group" : { ... "ns" : "vegetableprice", ... "key" : {"name" : true}, ... "initial" : {"time" : 0}, ... "$reduce" : function(doc, prev){ ... if(doc.time > prev.time){ ... prev.time = doc.time; ... prev.price = doc.price; ... }}, ... "cond":{"name" : {"$in" : ["tomato", "cucumber"]}} ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T02:56:24.843Z"), "price" : 3.5 }, { "name" : "cucumber", "time" : ISODate("2012-08-12T02:57:59.363Z"), "price" : 4.3 } ], "count" : 4, "keys" : 2, "ok" : 1 } > db.runCommand({"group" : { ... "ns" : "vegetableprice", ... "key" : {"name" : true}, ... "initial" : {"time" : 0}, ... "$reduce" : function(doc, prev){ ... if(doc.time > prev.time){ ... prev.time = doc.time; ... prev.price = doc.price; ... }}, ... "q":{"name":{"$in":["tomato", "cucumber"]}} ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T02:56:24.843Z"), "price" : 3.5 }, { "name" : "cucumber", "time" : ISODate("2012-08-12T02:57:59.363Z"), "price" : 4.3 } ], "count" : 4, "keys" : 2, "ok" : 1 } >
【使用完成器】
上述我们可以看到group操作的返回值,其中键“retval”指向所有聚合后的文档,这些就是客户端需要的文档。group操作的返回值文档最大为4MB,因为有这个大小的限制和考虑到效率,我们有时需要对这些聚合后得到的文档进行进一步地修剪后再返回!比如,我们有这样一个blog集合,其中每一个文档是一个blog,我们这次要统计的是,每个作者在写博客时最常使用的tag(标签),我们先看看这个集合结构:
> db.blog.find(); { "_id" : ObjectId("50272f94e02ab93d5c5be79c"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "dog" ] } { "_id" : ObjectId("50272f9ee02ab93d5c5be79d"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "pig" ] } { "_id" : ObjectId("50272fcee02ab93d5c5be79e"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "hamster" ] } { "_id" : ObjectId("50272fe4e02ab93d5c5be79f"), "author" : "tom", "content" : "...", "tag" : [ "db", "oracle", "mysql" ] } { "_id" : ObjectId("50272fede02ab93d5c5be7a0"), "author" : "tom", "content" : "...", "tag" : [ "db", "mongodb", "mysql" ] } >
按照上述的统计方式,我们会这样做:
> db.blog.find(); { "_id" : ObjectId("50272f94e02ab93d5c5be79c"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "dog" ] } { "_id" : ObjectId("50272f9ee02ab93d5c5be79d"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "pig" ] } { "_id" : ObjectId("50272fcee02ab93d5c5be79e"), "author" : "jim", "content" : "...", "tag" : [ "cat", "pet", "hamster" ] } { "_id" : ObjectId("50272fe4e02ab93d5c5be79f"), "author" : "tom", "content" : "...", "tag" : [ "db", "oracle", "mysql" ] } { "_id" : ObjectId("50272fede02ab93d5c5be7a0"), "author" : "tom", "content" : "...", "tag" : [ "db", "mongodb", "mysql" ] } > db.runCommand({"group" : {
... "ns" : "blog",
... "key" : {"author" : true},
... "initial" : {"tag":{}},
... "$reduce" : function(doc, prev){
... for(var i in doc.tag){
... if(doc.tag[i] in prev.tag){
... prev.tag[doc.tag[i]]++;
... } else {
... prev.tag[doc.tag[i]] = 1;
... }
... }
... }
... }});
{
"retval" : [
{
"author" : "jim",
"tag" : {
"cat" : 3,
"pet" : 3,
"dog" : 1,
"pig" : 1,
"hamster" : 1
}
},
{
"author" : "tom",
"tag" : {
"db" : 2,
"oracle" : 1,
"mysql" : 2,
"mongodb" : 1
}
}
],
"count" : 5,
"keys" : 2,
"ok" : 1
}
>
从上述结果我们可以看出,返回值中,包含所有作者使用的所有的tag的次数统计,我们可以将这个文档返回给客户端,然后由客户端去比较得到每个作者用得最多的tag。但这样做会产生很多开销影响效率,对于大的集合的统计尤为明显!我们此时可以通过在统计时使用“finalize”键来进一步处理reduce后的结果:
> db.runCommand({"group" : { ... "ns" : "blog", ... "key" : {"author" : true}, ... "initial" : {"tag" : {}}, ... "$reduce" : function(doc, prev) { ... for(var i in doc.tag){ ... if(doc.tag[i] in prev.tag){ ... prev.tag[doc.tag[i]]++; ... } else { ... prev.tag[doc.tag[i]] = 1; ... } ... } ... }, ... "finalize" : function(reducedoc) { ... var mostpopular = 0; ... for(var i in reducedoc.tag) { ... if(reducedoc.tag[i] > mostpopular) { ... mostpopular = reducedoc.tag[i]; ... reducedoc.usemosttag = i; ... } ... } ... delete reducedoc.tag; ... } ... }}); { "retval" : [ { "author" : "jim", "usemosttag" : "cat" }, { "author" : "tom", "usemosttag" : "db" } ], "count" : 5, "keys" : 2, "ok" : 1 } >
通过在group操作中使用键"finalize",我们看到最后返回的结果简洁清晰了很多,并且就是客户想要的结果。这个finalize函数写得有点问题就是,如果使用最多的tag有多个,他只会得到第一个!这个可以通过改进这个finalize函数来进行!
【将函数作为键使用】
上面我们都是通过直接指定某一个键,让其值作为分组的基准!我们也提到了,按照值分组会区分值得大小写!我们同样拿查询蔬菜最新价格来距离,我们先看不做任何处理的结果:
> db.vegetableprice.find(); { "_id" : ObjectId("50273c44e02ab93d5c5be7a1"), "name" : "tomato", "price" : 3.5, "time" : ISODate("2012-08-12T05:16:52.106Z") } { "_id" : ObjectId("50273c4ce02ab93d5c5be7a2"), "name" : "Tomato", "price" : 3.5, "time" : ISODate("2012-08-12T05:17:00.686Z") } > db.runCommand({"group" : { ... "ns" : "vegetableprice", ... "key" : {"name" : true}, ... "initial" : {"time" : 0}, ... "$reduce" : function(doc, prev) { ... if(doc.time > prev.time) { ... prev.time = doc.time; ... prev.price = doc.price; ... } ... } ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T05:16:52.106Z"), "price" : 3.5 }, { "name" : "Tomato", "time" : ISODate("2012-08-12T05:17:00.686Z"), "price" : 3.5 } ], "count" : 2, "keys" : 2, "ok" : 1 } >
我们看到这不是我们想要的结果,group将“tomato” 和“Tomato”作为两个组来对待!通过在group函数中使用键“$keyf” 代替键“key” 来指定分组依赖的键,会解决这个问题,使用方位为:
> db.vegetableprice.find(); { "_id" : ObjectId("50273c44e02ab93d5c5be7a1"), "name" : "tomato", "price" : 3.5, "time" : ISODate("2012-08-12T05:16:52.106Z") } { "_id" : ObjectId("50273c4ce02ab93d5c5be7a2"), "name" : "Tomato", "price" : 3.5, "time" : ISODate("2012-08-12T05:17:00.686Z") } > db.runCommand({"group" : { ... "ns" : "vegetableprice", ... "$keyf" : function(doc){return {"name" : doc.name.toLowerCase()};}, ... "initial" : {"time" : 0}, ... "$reduce" : function(doc, prev){ ... if(doc.time > prev.time){ ... prev.time = doc.time; ... prev.price = doc.price; ... } ... } ... }}); { "retval" : [ { "name" : "tomato", "time" : ISODate("2012-08-12T05:17:00.686Z"), "price" : 3.5 } ], "count" : 2, "keys" : 1, "ok" : 1 } >
注意键“$keyf”后面是一个函数,function(doc){....},参数doc就表示当前需要提取分组键的文档!这个函数会对这个文档的特定分组键的值进行一些处理!此处进行的是,将键“name”的值全部转化为小写!注意这个函数最后也必须返回一个文档!
我们上述演示的所有group操作,全是通过运行数据库命令实现的!实际上,MongoDB为集合也提供了group函数!使用方法同上,这里就不做演示了。
相关文章推荐
- 学习MongoDB--(6-2):聚合(MapReduce使用)
- 学习MongoDB--(6-2):聚合(MapReduce使用)
- mysql5.6初级使用方法学习第一天
- 使用聚合数据的接口进行的RxAndroid学习
- Git学习笔记03--Git初级使用
- mysql5.6初级使用方法学习第三天
- MongoDB学习笔记~复杂条件拼接和正则的使用
- MongoDB学习之树结构例子(使用NORM驱动)
- MongoDB学习系列(2)--使用PHP访问MongoDB
- Mongodb学习(安装篇):在Window下安装与使用
- Mongodb学习使用手册
- 使用 Java 8 学习 MongoDB(Part 1)
- 09-【MongoDB入门教程】使用mongo命令行进行数据聚合
- mongodb学习记录之四:聚合
- mongodb初级学习之逻辑结构和物理结构
- Mongodb学习笔记 --- Mongodb使用步骤
- MongoDB学习之树结构例子(使用NORM驱动)
- node.js学习笔记(4)_极客学院_MongoDB的使用
- Linq初级学习 [标准查询操作符的使用和调用]
- 学习MongoDB在java中使用的官方资料