资讯专栏INFORMATION COLUMN

MongoDB指南---18、聚合命令

why_rookie / 2512人阅读

摘要:上一篇文章指南下一篇文章为在集合上执行基本的聚合任务提供了一些命令。也可以给传递一个查询文档,会计算查询结果的数量对分页显示来说总数非常必要共个,目前显示个。使用时必须指定集合和键。指定要进行分组的集合。

上一篇文章:MongoDB指南---17、MapReduce
下一篇文章:

MongoDB为在集合上执行基本的聚合任务提供了一些命令。这些命令在聚合框架出现之前就已经存在了,现在(大多数情况下)已经被聚合框架取代。然而,复杂的group操作可能仍然需要使用JavaScript,count和distinct操作可以被简化为普通命令,不需要使用聚合框架。

 count

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

> db.foo.count()
0
> db.foo.insert({"x" : 1})
> db.foo.count()
1

不论集合有多大,count都会很快返回总的文档数量。
也可以给count传递一个查询文档,Mongo会计算查询结果的数量:

> db.foo.insert({"x" : 2})
> db.foo.count()
2
> db.foo.count({"x" : 1})
1

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

 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}

这里还有一个常见问题:有没有办法获得集合里面所有不同的键呢?MongoDB并没有直接提供这样的功能,但是可以用MapReduce(详见7.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;
...     }
... }}})

把这个命令分解开看看。

"ns" : "stocks"

指定要进行分组的集合。

"key" : "day"

指定文档分组依据的键。这里就是"day"键。所有"day"值相同的文档被分到一组。

"initial" : {"time" : 0}

每一组reduce函数调用中的初始"time"值,会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器,所以它的任何变化都可以保存下来。

"$reduce" : function(doc, prev) { ... }

这个函数会在集合内的每个文档上执行。系统会传递两个参数:当前文档和累加器文档(本组当前的结果)。本例中,想让reduce函数比较当前文档的时间和累加器的时间。如果当前文档的时间更晚一些,则将累加器的日期和价格替换为当前文档的值。别忘了,每一组都有一个独立的累加器,所以不必担心不同日期的命令会使用同一个累加器。

在问题一开始的描述中,就提到只要最近30天的股价。然而,我们在这里迭代了整个集合。这就是要添加"condition"的原因,因为这样就可以只对必要的文档进行处理。

> 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;
...     }},
... "condition" : {"day" : {"$gt" : "2010/09/30"}}
... }})

有些参考资料提及"cond"键或者"q"键,其实和"condition"键是完全一样的(就是表达力不如"condition"好)。
最后就会返回一个包含30个文档的数组,其实每个文档都是一个分组。每组都包含分组依据的键(这里就是"day" : string)以及这组最终的prev值。如果有的文档不存在指定用于分组的键,这些文档会被多带带分为一组,缺失的键会使用"day : null"这样的形式。在"condition"中加入"day" : {"$exists" : true}就可以排除不包含指定用于分组的键的文档。group命令同时返回了用到的文档总数和"key"的不同值数量:

> db.runCommand({"group" : {...}})
{
    "retval" :
        [
            {
                "day" : "2010/10/04",
                "time" : "Mon Oct 04 2010 11:28:39 GMT-0400 (EST)"
                "price" : 4.27
            },
            ...
        ],
    "count" : 734,
    "keys" : 30,
    "ok" : 1
}

这里每组的"price"都是显式设置的,"time"先由初始化器设置,然后在迭代中进行更新。"day"是默认被加进去的,因为用于分组的键会默认加入到每个"retval"内嵌文档中。要是不想在结果集中看到这个键,可以用完成器将累加器文档变为任何想要的形态,甚至变换成非文档(例如数字或字符串)。

1. 使用完成器

完成器(finalizer)用于精简从数据库传到用户的数据,这个步骤非常重要,因为group命令的输出结果需要能够通过单次数据库响应返回给用户。为进一步说明,这里举个博客的例子,其中每篇文章都有多个标签(tag)。现在要找出每天最热门的标签。可以(再一次)按天分组,得到每一个标签的计数。就像下面这样:

> db.posts.group({
... "key" : {"day" : true},
... "initial" : {"tags" : {}},
... "$reduce" : function(doc, prev) {
...     for (i in doc.tags) {
...         if (doc.tags[i] in prev.tags) {
...             prev.tags[doc.tags[i]]++;
...         } else {
...             prev.tags[doc.tags[i]] = 1;
...         }
...     }
... }})

得到的结果如下所示:

[
    {"day" : "2010/01/12", "tags" : {"nosql" : 4, "winter" : 10, "sledding" : 2}},
    {"day" : "2010/01/13", "tags" : {"soda" : 5, "php" : 2}},
    {"day" : "2010/01/14", "tags" : {"python" : 6, "winter" : 4, "nosql": 15}}
]

接着可以在客户端找出"tags"文档中出现次数最多的标签。然而,向客户端发送每天所有的标签文档需要许多额外的开销——每天所有的键/值对都被传送给用户,而我们需要的仅仅是一个字符串。这也就是group有一个可选的"finalize"键的原因。"finalize"可以包含一个函数,在每组结果传递到客户端之前调用一次。可以使用"finalize"函数将不需要的内容从结果集中移除:

> db.runCommand({"group" : {
... "ns" : "posts",
... "key" : {"day" : true},
... "initial" : {"tags" : {}},
... "$reduce" : function(doc, prev) {
...     for (i in doc.tags) {
...         if (doc.tags[i] in prev.tags) {
...             prev.tags[doc.tags[i]]++;
...         } else {
...             prev.tags[doc.tags[i]] = 1;
...         }
...     },
... "finalize" : function(prev) {
...     var mostPopular = 0;
...     for (i in prev.tags) {
...         if (prev.tags[i] > mostPopular) {
...             prev.tag = i;
...             mostPopular = prev.tags[i];
...         }
...     }
...     delete prev.tags
... }}})

现在,我们就得到了想要的信息,服务器返回的内容可能如下:

[
    {"day" : "2010/01/12", "tag" : "winter"},
    {"day" : "2010/01/13", "tag" : "soda"},
    {"day" : "2010/01/14", "tag" : "nosql"}
]

finalize可以对传递进来的参数进行修改,也可以返回一个新值。

2. 将函数作为键使用

有时分组所依据的条件可能会非常复杂,而不是单个键。比如要使用group计算每个类别有多少篇博客文章(每篇文章只属于一个类别)。由于不同作者的风格不同,填写分类名称时可能有人使用大写也有人使用小写。所以,如果要是按类别名来分组,最后“MongoDB”和“mongodb”就是两个完全不同的组。为了消除这种大小写的影响,就要定义一个函数来决定文档分组所依据的键。
定义分组函数就要用到$keyf键(注意不是"key"),使用"$keyf"的group命令如下所示:

> db.posts.group({"ns" : "posts",
... "$keyf" : function(x) { return x.category.toLowerCase(); },
... "initializer" : ... })

有了"$keyf",就能依据各种复杂的条件进行分组了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19572.html

相关文章

  • MongoDB指南---18聚合命令

    摘要:上一篇文章指南下一篇文章为在集合上执行基本的聚合任务提供了一些命令。也可以给传递一个查询文档,会计算查询结果的数量对分页显示来说总数非常必要共个,目前显示个。使用时必须指定集合和键。指定要进行分组的集合。 上一篇文章:MongoDB指南---17、MapReduce下一篇文章: MongoDB为在集合上执行基本的聚合任务提供了一些命令。这些命令在聚合框架出现之前就已经存在了,现在(大多...

    raoyi 评论0 收藏0
  • MongoDB指南---17、MapReduce

    摘要:操作花费的时间,单位是毫秒。处理完成后,会自动将临时集合的名字更改为你指定的集合名,这个重命名的过程是原子性的。作用域在这些函数内部是不变的。上一篇文章指南聚合下一篇文章指南聚合命令 上一篇文章:MongoDB指南---16、聚合下一篇文章:MongoDB指南---18、聚合命令 MapReduce是聚合工具中的明星,它非常强大、非常灵活。有些问题过于复杂,无法使用聚合框架的查询语言...

    jonh_felix 评论0 收藏0
  • MongoDB指南---17、MapReduce

    摘要:操作花费的时间,单位是毫秒。处理完成后,会自动将临时集合的名字更改为你指定的集合名,这个重命名的过程是原子性的。作用域在这些函数内部是不变的。上一篇文章指南聚合下一篇文章指南聚合命令 上一篇文章:MongoDB指南---16、聚合下一篇文章:MongoDB指南---18、聚合命令 MapReduce是聚合工具中的明星,它非常强大、非常灵活。有些问题过于复杂,无法使用聚合框架的查询语言...

    pubdreamcc 评论0 收藏0
  • MongoDB指南---16、聚合

    摘要:将返回结果限制为前个。所以,聚合的结果必须要限制在以内支持的最大响应消息大小。包含字段和排除字段的规则与常规查询中的语法一致。改变字符大小写的操作,只保证对罗马字符有效。只对罗马字符组成的字符串有效。 上一篇文章:MongoDB指南---15、特殊的索引和集合:地理空间索引、使用GridFS存储文件下一篇文章:MongoDB指南---17、MapReduce 如果你有数据存储在Mon...

    Keagan 评论0 收藏0
  • MongoDB指南---16、聚合

    摘要:将返回结果限制为前个。所以,聚合的结果必须要限制在以内支持的最大响应消息大小。包含字段和排除字段的规则与常规查询中的语法一致。改变字符大小写的操作,只保证对罗马字符有效。只对罗马字符组成的字符串有效。 上一篇文章:MongoDB指南---15、特殊的索引和集合:地理空间索引、使用GridFS存储文件下一篇文章:MongoDB指南---17、MapReduce 如果你有数据存储在Mon...

    _Zhao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<