首页 » MongoDB实战 » MongoDB实战全文在线阅读

《MongoDB实战》5.4 详解聚合

关灯直达底部

本节我将对MongoDB的聚合函数做详细说明。

5.4.1 maxmin

通常总是需要找到给定集合里的最大和最小值。使用SQL的数据库提供了minmax函数,但MongoDB没有这样的函数,我们必须自己实现。要找到某个字段中的最大值,可以按照该字段降序排序,并限制结果集为一个文档;按照相反顺序排序就能取到对应的最小值。例如,如果希望找到投票数最多的评论,查询需要对投票的字段进行排序,限制返回一个文档:

db.reviews.find({}).sort({helpful_votes: -1}).limit(1)  

返回文档中的helpful_votes字段包含了该字段中的最大值。要获取最小值,只要逆序排列就行了:

db.reviews.find({}).sort({helpful_votes: 1}).limit(1)  

如果要在生产环境中发起查询,helpful_votes字段最好能有一个索引。如果想获得特定产品里投票数最多的评论,则需要一个product_idhelpful_votes的复合索引。如果不清楚这么做的原因,可以阅读第7章。

5.4.2 distinct

MongoDB的distinct命令是获取特定字段中不同值列表的最简单工具。该命令既适用于单键,也适用于数组键。distinct默认覆盖整个集合,但也可以通过查询选择器进行约束。

可以像下面这样使用distinct获取产品集合里所有唯一标签的列表:

db.products.distinct(/"tags/")  

这很简单。如果希望操作products集合的一个子集,可以传入一个查询选择器作为第二个参数。这里的查询将不同的标签值限定到Gardening Tools分类里的产品:

db.products.distinct(/"tags/",          {category_id: ObjectId(/"6a5b1476238d3b4dd5000048/")})  

聚合命令限制

在实用性方面,distinctgroup有一个很大的限制:它们返回的结果集不能超过16 MB。16 MB的限制并不是这些命令本身所强加的阈值,这是所有的初始查询结果集大小。distinctgroup是以命令的方式实现的,也就是对特殊的$cmd集合的查询,它们赖以生存的查询则受制于该限制。如果distinctgroup处理不了你的聚合结果集,那么就只能使用map-reduce代替了,它的结果可以保存在集合中而非内联(inline)返回。

5.4.3 group

groupdistinct一样,也是数据库命令,因此它的结果集也受制于同样的16 MB响应限制。而且,为了减少内存消耗,group不会处理多于10 000个唯一键。如果聚合操作在此范围内,group是个不错的选择,因为通常情况下它会比map-reduce快。

我们已经看过根据用户对评论分组的例子了,那个示例只能算“半个”。让我们快速回顾一下传递给group的选项。

  • key,描述分组字段的文档。举例来说,要根据category_id分组,可以将{category_id: true}作为键。此处还可以使用复合键,比如,若想根据user_idrating对一系列帖子做分组,键看起来是这样的:{user_id: true, rating: true}。除非使用keyf,否则key选项是必需的。

  • keyf,这是一个JavaScript函数,应用于文档之上,为该文档生成一个键,当用于分组的键需要计算时,这个函数非常有用。举例来说,如果想根据每个文档创建时是周几来对结果集进行分组,但又不实际存储该值,就可以用键函数来生成这个键:

function(doc) {  return {day: doc.created_at.getDay;}  

这个函数会生成类似{day: 1}这样的键。请注意,如果没有指定标准的key,那么keyf是必需的。

  • initial,作为聚合结果初始值的文档。reduce函数第一次运行时,该初始文档会作为聚合器的第一个值,通常会包含所有要聚合的键。举例来说,如果正在为每个分组项计算总投票数和总文档数,那么初始文档看起来是这样的:{vote_sum: 0.0, doc_count: 0}

请注意,该参数是必需的。

  • reduce,用于执行聚合的JavaScript函数。该函数接受两个参数:正被迭代的当前文档和用于存储聚合结果的聚合器文档。聚合器的初始值就是初始文档。下面是一个聚合投票和文档总数的reduce函数示例:
function(doc, aggregator) {  aggregator.doc_count += 1;  aggregator.vote_sum += doc.vote_count;} 

请注意,reduce函数并不返回任何内容,它只不过是修改聚合器对象。reduce函数也是必需的。

  • cond,过滤要聚合文档的查询选择器。如果不希望分组操作处理整个集合,就必须提供一个查询选择器。例如,假设只想聚合那些拥有五个以上投票的文档,可以提供以下查询选择器:{vote_count: {$gt: 5}}

  • finalize,在返回结果集之前应用于每个结果文档的JavaScript函数。该函数支持对分组操作的结果进行后置处理。我们通常会用它计算平均值,在分组结果的现有值之外,再加另一个值来保存平均值:

function(doc) {  doc.average = doc.vote_count / doc.doc_count;}  

诚然,group有这么多选项,上手比较麻烦。但是,稍加实践之后,你会很快习惯的。

5.4.4 map-reduce

既然groupmap-reduce提供了类似的功能,你可能会想MongoDB为什么要同时对它们提供支持呢?其实,在添加map-reduce之前,group是MongoDB唯一的聚合器,map-reduce是后来出于一些原因加入的。首先,MapReduce风格的操作正在成为主流,而且将这种思考方式融入产品之中看起来是很明智的。1其次,也是更实际的原因:对大数据集进行迭代,尤其是在分片配置中,需要有分布式的聚合器,而MapReduce(范式)恰恰提供所需的内容。

1. 很多开发者是在谷歌那篇著名的关于分布式计算的论文(http://labs.google.com/papers/mapreduce.html)里初次看到MapReduce的。其中的思想后来成了Hadoop的基础,而Hadoop是一个使用分布式MapReduce处理大数据集的开源框架。之后MapReduce的思想得到了广泛传播,例如CouchDB就用MapReduce的范式来声明索引。

map-reduce包含很多选项。此处详细对这些选项做了说明。

  • map,应用于每个文档之上的JavaScript函数。该函数必须调用emit来选择要聚合的键和值。在函数上下文中,this的值指向当前文档。例如,假设想根据用户ID对结果分组,计算出总投票数和总文档数,映射函数应该是这样的:
function {  emit(this.user_id, {vote_sum: this.vote_count, doc_count: 1});}  
  • reduce,一个JavaScript函数,接受一个键和一个值列表。该函数对返回值的结构有严格要求,必须总是与values数组所提供的结构一致。reduce函数通常会迭代一个值的列表,在此过程中对其进行聚合。回到我们的示例,以下展示如何处理映射函数输出的内容:
function(key, values) {  var vote_sum = 0;  var doc_sum = 0;  values.forEach(function(value) {    vote_sum += value.vote_sum;    doc_sum += value.doc_sum;  });  return {vote_sum: vote_sum, doc_sum: doc_sum};}  

请注意,通常在聚合过程中不会用到key参数的值。

  • query,用于过滤映射处理的集合的查询选择器。该参数的作用与groupcond参数相同。

  • sort,对于查询的排序。与limit选项搭配使用时非常有用,这样就可以对1000个最近创建的文档运行map-reduce

  • limit,一个整数,指定了查询和排序的条数。

  • out,该参数决定了如何返回输出内容。要将所有输出作为命令本身的结果,传入{inline: 1}。请注意,这仅适用于结果集符合16 MB返回限制的情况。

另一个选择是将结果放到一个输出集合里。此时,out的值必须是一个字符串,标明用于保存结果的集合的名称。

将结果保存到输出集合时有一个问题:如果最近运行过类似的map-reduce,那么可能会覆盖现有数据。因此,还有两个集合输出选项:一个用于合并结果和老数据,另一个对数据进行reduce处理。在合并的场景中,使用{merge: /"collectionName/"},新结果会覆盖拥有相同键的现有项。如果使用{reduce: /"collectionName/"},会调用reduce函数根据新值来处理现有键的值。尤其是在执行要反复运行的MapReduce任务时,希望把新数据整合到已有的聚合之中,reduce格外有用。在对集合执行新的MapReduce任务时,只需简单添加一个查询选择器来限制聚合所需的数据集。

  • finalize,一个JavaScript函数,在reduce阶段完成后会应用于每个返回的文档上。

  • scope,该文档指定了mapreducefinalize函数可全局访问的变量的值。

  • verbose,一个布尔值,为true时,在命令返回文档中会包含对map-reduce任务执行时间的统计信息。

在考虑使用MongoDB的map-reducegroup时,还有一个重要的限制需要引起注意:速度。对于大的数据集,这些聚合函数通常执行起来满足不了用户对速度的需求。这几乎都要归咎于MongoDB的JavaScript引擎。一个单线程解释(非编译)运行的JavaScript引擎是很难实现高性能的。

但也不要沮丧,map-reduce和group被广泛使用于很多场景之中,并能充分胜任这些任务。对于那些还不适用的场景,则有其他方案,并有望在未来提供支持。其他方案是指在别处执行聚合,拥有大数据集的用户已经在Hadoop集群上成功处理过数据。未来有望加入新的聚合函数,它们使用编译的多线程代码。这些功能计划于MongoDB v2.0之后的某个时间发布,你可以关注https://jira.mongodb.org/browse/SERVER-447。