使用mongoDB pipeline进行聚合操作

mongoDB中的聚合操作将多个文档中的值组合在一起,并可对分组数据执行各种操作,以返回单个结果。 在SQL中的 count(*)与group by组合相当于mongodb 中的聚合功能。
mongoDB为我们提供了三种方法来实现聚合操作。分别是aggregation pipeline,Map-Reduce和Single Purpose Aggregation Operations。今天我们主要来讨论一下关于aggregation pipeline的内容。关于Map-Reduce和Single Purpose Aggregation Operations后面有时间了再讨论。


aggregation pipeline主要是用aggregate()方法来实现聚合操作。
pipeline通俗的理解就是一个管道,其中每一个操作就是管道的一个阶段,每次当前操作接受上一个阶段的输出作为输入,并把输出结果作为输入结果给下一个阶段。如果你还不是特别明白,不用担心,慢慢往下看,我们会用更多的例子来说明。语法如下:
db.collection.aggregate( [ { }, … ] )。其中stage可以是如下的内容。

1$project

$project用来重构返回值。下面举个例子来说明。

db.books.insert(
{
  "_id" : 1,
  title: "abc123",
  isbn: "0001122223334",
  author: { last: "zzz", first: "aaa" },
  copies: 5
}
)

此时我们使用如下命令来查询数据:

 db.books.aggregate([{$project:{title:1,author:1}}])

输出如下结果:

{ "_id" : 1, "title" : "abc123", "author" : { "last" : "zzz", "first" : "aaa" } }

可以看出,输出的结果中包含了_id,title,author三个字段。默认情况下_id字段是会输出的,然后在$project中可以通过指定字段的值为0或者1,来决定输出结果中包含还是不包含这个字段。为1时代表返回值包含这个字段,为0时代表返回值不包含这个字段。不想返回_id字段可以这样:

db.books.aggregate( [ { $project : { _id: 0, title : 1 , author : 1 } } ] )

$project除了可以控制是否返回某个字段,还可以给返回结果增加字段。

db.books.aggregate(
   [
      {
         $project: {
            title: 1,
            isbn: {
               prefix: { $substr: [ "$isbn", 0, 3 ] },
               group: { $substr: [ "$isbn", 3, 2 ] },
               publisher: { $substr: [ "$isbn", 5, 4 ] },
               title: { $substr: [ "$isbn", 9, 3 ] },
               checkDigit: { $substr: [ "$isbn", 12, 1] }
            },
            lastName: "$author.last",
            copiesSold: "$copies"
         }
      }
   ]
)

返回结果如下:

{ "_id" : 1, "title" : "abc123", "isbn" : { "prefix" : "000", "group" : "11", "publisher" : "2222", "title" : "333", "checkDigit" : "4" }, "lastName" : "zzz", "copiesSold" : 5 }

可以看到,我们给返回结果的isbsn字段中添加了prefix,group,publisher,title和checkDigit字段,这些字段是通过对原来isbn的字符串进行拆分得到的,另一个增加的字段copiesSold是引用了原来的copies字段。

2$match

$match通过跟查询语句相比对,来过滤集合,只返回跟查询语句相匹配的文档。

db.articles.insert([
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 }
{ "_id" : ObjectId("55f5a192d4bede9ac365b257"), "author" : "ahn", "score" : 60, "views" : 1000 }
{ "_id" : ObjectId("55f5a192d4bede9ac365b258"), "author" : "li", "score" : 55, "views" : 5000 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b259"), "author" : "annT", "score" : 60, "views" : 50 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "author" : "li", "score" : 94, "views" : 999 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "author" : "ty", "score" : 95, "views" : 1000 }
])

执行命令:

db.articles.aggregate([{$match:{author:"dave"}}])

返回结果如下:

{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 }

3$redact

感觉不好说清楚,直接上例子:

db.forecasts.insert(
{
  _id: 1,
  title: "123 Department Report",
  tags: [ "G", "STLW" ],
  year: 2014,
  subsections: [
    {
      subtitle: "Section 1: Overview",
      tags: [ "SI", "G" ],
      content:  "Section 1: This is the content of section 1."
    },
    {
      subtitle: "Section 2: Analysis",
      tags: [ "STLW" ],
      content: "Section 2: This is the content of section 2."
    },
    {
      subtitle: "Section 3: Budgeting",
      tags: [ "TK" ],
      content: {
        text: "Section 3: This is the content of section3.",
        tags: [ "HCS" ]
      }
    }
  ]
}
)

执行如下操作:

var userAccess = [ "STLW", "G" ];
db.forecasts.aggregate(
   [
     { $match: { year: 2014 } },
     { $redact: {
        $cond: {
           if: { $gt: [ { $size: { $setIntersection: [ "$tags", userAccess ] } }, 0 ] },
           then: "$$DESCEND",
           else: "$$PRUNE"
         }
       }
     }
   ]
);

返回结果如下:

{
  "_id" : 1,
  "title" : "123 Department Report",
  "tags" : [ "G", "STLW" ],
  "year" : 2014,
  "subsections" : [
    {
      "subtitle" : "Section 1: Overview",
      "tags" : [ "SI", "G" ],
      "content" : "Section 1: This is the content of section 1."
    },
    {
      "subtitle" : "Section 2: Analysis",
      "tags" : [ "STLW" ],
      "content" : "Section 2: This is the content of section 2."
    }
  ]
}

解释一下上面的例子,$redact主要有三个关键字:
1)$$DESCEND:包含当前document级别的所有fields。当前级别字段的内嵌文档将会被继续检测。
2)$$PRUNE:不包含当前文档或者内嵌文档级别的所有字段,不会继续检测此级别的其他字段,即使这些字段的内嵌文档持有相同的访问级别。
3)$$KEEP:包含当前文档或内嵌文档级别的所有字段,不再继续检测此级别的其他字段,即使这些字段的内嵌文档中持有不同的访问级别。
在上面的aggregate操作中,首先使用$match过滤掉了一部分数据,只拿到year是2014的文档。此时$matche就是第一个阶段,在这一个阶段中,我们只拿出跟查询条件相匹配的结果,并将这个结果作为输入传递个下一个阶段。在这里下一个阶段就是$redact。在$redact阶段中,我们拿到了上一个阶段的输出结果。$redact常常和$cond配合使用。$cond是英文condition的所写,代表条件。在$cond中通常会使用if–else–then的表达式。在if中传入一个判断语句,如果符合判断语句,执行then的部分,否则执行else的部分。在我们上面的例子中,$setIntersection表示对两个集合取交集。$size表示对集合取大小。所以if判断语句中的意思就是:对于当前阶段输入进来的每一个文档,都判断tags和userAccess的交集的长度是否大于0,如果大于0,就执行then,否则执行else。
在then这一部分使用了$$DESCEND关键字。这个关键字的意思我们在上面已经做了说明。在这个例子中的意思就是说,如果我满足了if条件,那么会继续递归的向下判断我的子文档。在我们的例子中,将继续去判断subsections字段里面的内容,因为它里面也含有tags字段。
在else这一部分使用了$$PRUNE关键字。这个关键字的意思在上面也已经说清楚了。在这个例子中的意思就是说,如果我不满足if条件,那么我直接就舍弃当前文档。

4$limit

用来控制当前阶段输出数据的数据。如下:

db.article.aggregate(
    { $limit : 5 }
);

返回结果只有5条,如果还有下一个阶段的操作,那么也只会向下一阶段传递5条结果。
mongoDB对limit做了优化,如果$sort(排序)刚好在limit前面,那么$sort只会在内存中维护n条记录,这个n就是limit中的数字。

5$skip

跳过一些数据。
如下:

db.article.aggregate(
    { $skip : 5 }
);

在这个例子中将会跳过5条数据,然后将结果返回。如果还有下一步操作,将结果传递给下一阶段。

6$unwind

还是直接上例子。
inventory集合中有这样一个文档。

{ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] }

使用:

db.inventory.aggregate( [ { $unwind : "$sizes" } ] )

返回结果:

{ "_id" : 1, "item" : "ABC1", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "L" }

可以看出,在这个例子中,$unwind命令将sizes字段的数组拆分成了一个个的元素。这就是$unwind的主要功能。
$unwind的完整写法如下:

{
  $unwind:
    {
      path: <field path>,
      includeArrayIndex: <string>,
      preserveNullAndEmptyArrays: <boolean>
    }
}

path:必须有,指明需要查分的字段,一般为一个数组
includeArrayIndex: 可选,如果需要拆分开的数据包含数据下标,可以指定数组下标的字段名
preserveNullAndEmptyArrays: 可选,在数组为null,缺失 ,或者空的情况下,如果为true, $unwind 同样输出当前文档. 如果为false, \$unwind不输出文档。默认是false.
下面的写法效果是一样的:

db.inventory.aggregate( [ { $unwind: "$sizes" } ] )
db.inventory.aggregate( [ { $unwind: { path: "$sizes" } } ] )

假设现在inventory中有如下文档数据:

{ "_id" : 1, "item" : "ABC", "sizes": [ "S", "M", "L"] }
{ "_id" : 2, "item" : "EFG", "sizes" : [ ] }
{ "_id" : 3, "item" : "IJK", "sizes": "M" }
{ "_id" : 4, "item" : "LMN" }
{ "_id" : 5, "item" : "XYZ", "sizes" : null }

使用如下命令:

db.inventory.aggregate( [ { $unwind: "$sizes" } ] )

返回结果:

{ "_id" : 1, "item" : "ABC", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC", "sizes" : "L" }
{ "_id" : 3, "item" : "IJK", "sizes" : "M" }

可以看到,在sizes是空,null或者缺失的情况下,文档结果不会输出来。(preserveNullAndEmptyArrays默认是false)
我们来将preserveNullAndEmptyArrays指定为true,看一下结果。
执行:

db.inventory.aggregate( [
   { $unwind: { path: "$sizes", preserveNullAndEmptyArrays: true } }
] )

返回结果:

{ "_id" : 1, "item" : "ABC", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC", "sizes" : "L" }
{ "_id" : 2, "item" : "EFG" }
{ "_id" : 3, "item" : "IJK", "sizes" : "M" }
{ "_id" : 4, "item" : "LMN" }
{ "_id" : 5, "item" : "XYZ", "sizes" : null }

区别已经一目了然。
我们还可以通过来显示被查分的数据的数组下标。

db.inventory.aggregate( [ { $unwind: { path: "$sizes", includeArrayIndex: "arrayIndex" } } ] )

在这里,返回结果中会多出一个字段叫做:arrayIndex,这个字段包含了当前文档原来的数据在数组中的下标。上面的返回结果:

{ "_id" : 1, "item" : "ABC", "sizes" : "S", "arrayIndex" : NumberLong(0) }
{ "_id" : 1, "item" : "ABC", "sizes" : "M", "arrayIndex" : NumberLong(1) }
{ "_id" : 1, "item" : "ABC", "sizes" : "L", "arrayIndex" : NumberLong(2) }
{ "_id" : 3, "item" : "IJK", "sizes" : "M", "arrayIndex" : null }

7$sample

$sample随机返回指定大小的数据集合。标准的语法模板如下:

{ $sample: { size: <positive integer> } }

通过下面的命令从users集合中随机选出三条记录。

db.users.aggregate(
   [ { $sample: { size: 3 } } ]
)

结果将返回users中随机的三条记录。
值得注意的是,$sample的返回结果中,某一条记录可能会出现不止一次。

8$group

$group几乎是用的最为频繁的聚合函数。先来举个例子看一下效果:

 db.products.insert([
   {proId:"001",saleNum:10},
   {proId:"002",saleNum:5},
   {proId:"001",saleNum:15}
   ])

现在products集合中有三条数据。现在我们想要统计出proId为001的产品总的saleNum。

 db.products.aggregate([
    {
       $group:{_id:"$proId",
              totalNum:{$sum:"$saleNum"}
              }
    }
    ])

$group就类似与mysql中的group by,指定数据根据某个字段分组。在$group中我们是通过_id来指定分组依据。上面的例子中,我们指定数据根据proId来尽心分组,然后在每个分组中通过$sum函数来计算saleNum的总数,并将计算结果给了totalNum这个字段。
返回结果如下:

{ "_id" : "002", "totalNum" : 5 }
{ "_id" : "001", "totalNum" : 25 }

通过上面这个例子,相信大家对$group的大致使用已经有所了解。现在我们来看看$group的完整的使用模板:

{ $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }

其中_id指定分组依据,后面的accumulator可以是下面的函数:

  • $sum:返回分组中某个字段的和。如果某个字符串无法转换成数字,mongoDB将忽略它。从3.2版本之后,在$group和$project阶段都有效。
  • $avg:返回分组内某个字段的平均值。如果某个字符串无法转换成数字,mongoDB将忽略它。从3.2版本之后,在$group和$project阶段都有效。
  • $first:返回分组内的第一条文档数据。仅仅在$group阶段有效。
  • $last:返回分组内的最后一条文档数据。仅仅在$group阶段有效。
  • $max:返回分组内某个字段最大的数据。从3.2版本之后,在$group和$project阶段都有效。
  • $min:返回分组内某个字段最小的数据。从3.2版本之后,在$group和$project阶段都有效。
  • $push:将分组内某个字段的值加入到一个数组中。仅仅在$group阶段有效。
  • $addToSet:将分组内的某个字段加入一个集合中,无重复值。仅仅在$group阶段有效。
  • $stdDevPop:计算分组内某个字段的标准差。从3.2版本之后,在$group和$project阶段都有效。
  • $stdDevSamp:计算样本中某个字段的标准差。从3.2版本之后,在$group和$project阶段都有效。
    下面我们一一举例来说明。
    $sum
    表达式模板:
{ $sum: <expression> } 

集合sales中有如下数据:

{ "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-01-01T08:00:00Z")}
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-02-03T09:00:00Z")}
{ "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 5, "date" : ISODate("2014-02-03T09:05:00Z")}
{ "_id" : 4, "item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-02-15T08:00:00Z")}
{ "_id" : 5, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-02-15T09:05:00Z")}

执行命令:

db.sales.aggregate(
   [
     {
       $group:
         {
           _id: { day: { $dayOfYear: "$date"}, year: { $year: "$date" } },
           totalAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } },
           count: { $sum: 1 }
         }
     }
   ]
)

解释一下这条命令,$dayOfYear是个函数取到某天是一年中的第几天,$yearz这个函数取到年份,所以在上面的例子中,我们是用一年中的第几天和年份来作为分组依据。假设某个分组内有n条数据,对于每一条数据,$multiply函数用来做乘法计算,将每一个分组内每一条数据中的price和quantity相乘。一共计算n次,得到n个乘积,最后把得到的n个乘积求和给totalAmount这个字段。
count: { $sum: 1 }跟totalAmount类似,只是在这里把price和quantity相乘直接换成了1。这样写的意思就是统计每个分组内有多少条数据。
输出的结果如下:

{ "_id" : { "day" : 46, "year" : 2014 }, "totalAmount" : 150, "count" : 2 }
{ "_id" : { "day" : 34, "year" : 2014 }, "totalAmount" : 45, "count" : 2 }
{ "_id" : { "day" : 1, "year" : 2014 }, "totalAmount" : 20, "count" : 1 }

补充:

在 $group阶段,如果表达式中的数据是数组,那么$sum把该数组看做非数字类型。

exampleField ValuesResults
{ $sum : <field> }数字计算values的和
{ $sum : <field> }数字和非数字混合忽略非数字,计算数字的和
{ $sum : <field> }非数字,或者field不存在0

$avg
计算每一个分组内某个指定字段的平均值。
表达式:

{ $avg: <expression> }

这个跟上面的sum使用方法差不多,就不赘述了,举个例子相信大家都看得懂。
在sales集合中有如下数据:

{ "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-01-01T08:00:00Z")}
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-02-03T09:00:00Z")}
{ "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 5, "date" : ISODate("2014-02-03T09:05:00Z")}
{ "_id" : 4, "item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-02-15T08:00:00Z")}
{ "_id" : 5, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-02-15T09:12:00Z")}

执行命令:

db.sales.aggregate(
   [
     {
       $group:
         {
           _id: "$item",
           avgAmount: { $avg: { $multiply: [ "$price", "$quantity" ] } },
           avgQuantity: { $avg: "$quantity" }
         }
     }
   ]
)

返回结果如下:

{ "_id" : "xyz", "avgAmount" : 37.5, "avgQuantity" : 7.5 }
{ "_id" : "jkl", "avgAmount" : 20, "avgQuantity" : 1 }
{ "_id" : "abc", "avgAmount" : 60, "avgQuantity" : 6 }

补充:
在 $group阶段,如果表达式中的数据是数组,那么$avg把该数组看做非数字类型。
$first
返回每个分组中第一条数据,注意只有在组内数据有一定的顺序的时候,这个操作才具有意义。
语法如下:

{ $first: <expression> }

假定sales集合中有如下数据:

{ "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-01-01T08:00:00Z")}
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-02-03T09:00:00Z")}
{ "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 5, "date" : ISODate("2014-02-03T09:05:00Z")}
{ "_id" : 4,"item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-02-15T08:00:00Z")}
{ "_id" : 5,"item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-02-15T09:05:00Z")}
{ "_id" : 6, "item" : "xyz", "price" : 5, "quantity" : 5, "date" : ISODate("2014-02-15T12:05:10Z") }
{ "_id" : 7, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-02-15T14:12:12Z")}

执行如下操作:

db.sales.aggregate(
   [
     { $sort: { item: 1, date: 1 } },
     {
       $group:
         {
           _id: "$item",
           firstSalesDate: { $first: "$date" }
         }
     }
   ]
)

首先根据item和date做正向排序,然后根据item字段分组,并将每一组内第一条数据给firstSalesDate字段,这样就拿出了每种item最早的销售日期。
返回结果:

{ "_id" : "xyz", "firstSalesDate" : ISODate("2014-02-03T09:05:00Z") }
{ "_id" : "jkl", "firstSalesDate" : ISODate("2014-02-03T09:00:00Z") }
{ "_id" : "abc", "firstSalesDate" : ISODate("2014-01-01T08:00:00Z") }

$last
跟$first用法一样,不再赘述。
$max
返回最大值。例子一看大家都明白。

db.sales.aggregate(
   [
     {
       $group:
         {
           _id: "$item",
           maxTotalAmount: { $max: { $multiply: [ "$price", "$quantity" ] } },
           maxQuantity: { $max: "$quantity" }
         }
     }
   ]
)

返回结果:

{ "_id" : "xyz", "maxTotalAmount" : 50, "maxQuantity" : 10 }
{ "_id" : "jkl", "maxTotalAmount" : 20, "maxQuantity" : 1 }
{ "_id" : "abc", "maxTotalAmount" : 100, "maxQuantity" : 10 }

$min
跟$max类似,不在赘述。
$push
将指定的表达式的值添加到一个数组中。
还是以上面的sales集合为例。

db.sales.aggregate(
   [
     {
       $group:
         {
           _id: { day: { $dayOfYear: "$date"}, year: { $year: "$date" } },
           itemsSold: { $push:  { item: "$item", quantity: "$quantity" } }
         }
     }
   ]
)

在这个例子中我们将item: “$item”, quantity: “$quantity”这样形式的数据每次都放入一个数组中,数组名字我们取名为itemSold.
返回结果:

{
   "_id" : { "day" : 46, "year" : 2014 },
   "itemsSold" : [
      { "item" : "abc", "quantity" : 10 },
      { "item" : "xyz", "quantity" : 10 },
      { "item" : "xyz", "quantity" : 5 },
      { "item" : "xyz", "quantity" : 10 }
   ]
}
{
   "_id" : { "day" : 34, "year" : 2014 },
   "itemsSold" : [
      { "item" : "jkl", "quantity" : 1 },
      { "item" : "xyz", "quantity" : 5 }
   ]
}
{
   "_id" : { "day" : 1, "year" : 2014 },
   "itemsSold" : [ { "item" : "abc", "quantity" : 2 } ]
}

$addToSet
用法同$push,只是不能有重复数据。
依然用sales集合做例子:

db.sales.aggregate(
   [
     {
       $group:
         {
           _id: { day: { $dayOfYear: "$date"}, year: { $year: "$date" } },
           itemsSold: { $addToSet: "$item" }
         }
     }
   ]
)

返回结果:

{ "_id" : { "day" : 46, "year" : 2014 }, "itemsSold" : [ "xyz", "abc" ] }
{ "_id" : { "day" : 34, "year" : 2014 }, "itemsSold" : [ "xyz", "jkl" ] }
{ "_id" : { "day" : 1, "year" : 2014 }, "itemsSold" : [ "abc" ] }

$stdDevPop
返回标准差。
user集合中有如下数据:

{ "_id" : 1, "name" : "dave123", "quiz" : 1, "score" : 85 }
{ "_id" : 2, "name" : "dave2", "quiz" : 1, "score" : 90 }
{ "_id" : 3, "name" : "ahn", "quiz" : 1, "score" : 71 }
{ "_id" : 4, "name" : "li", "quiz" : 2, "score" : 96 }
{ "_id" : 5, "name" : "annT", "quiz" : 2, "score" : 77 }
{ "_id" : 6, "name" : "ty", "quiz" : 2, "score" : 82 }

执行命令:

db.users.aggregate([
   { $group: { _id: "$quiz", stdDev: { $stdDevPop: "$score" } } }
])

返回结果:

{ "_id" : 2, "stdDev" : 8.04155872120988 }
{ "_id" : 1, "stdDev" : 8.04155872120988 }

可以看出,$stdDevPop求出了每个分组内score字段的标准差。
$stdDevSamp
用法同$stdDevPop,只是$stdDevPop是在有全部数据的情况下做运算计算标准差,$stdDevSamp是在只有样本的情况下计算标准差。
users中有如下数据。

{_id: 0, username: "user0", age: 20}
{_id: 1, username: "user1", age: 42}
{_id: 2, username: "user2", age: 28}
...

执行计算:

db.users.aggregate(
   [
      { $sample: { size: 100 } },
      { $group: { _id: null, ageStdDev: { $stdDevSamp: "$age" } } }
   ]
)

返回结果:

{ "_id" : null, "ageStdDev" : 7.811258386185771 }

先用$sample选取了100个样本,然后计算出了这一百个样本的标准偏差。
值得注意的是,在这里$group分组的时候,_id指定了null,意思就是不分组,所有的数据都作为一组参与当前运算。

9$sort

对所有的输入数据进行排序,并输出排序好的数据。
模板如下:

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

其中sort order可以取值为1,-1,或者textScore.关于textScore这里先不做说明,后面有时间再讨论这个。
当sort order取值为1时,代表升序。
当sort order取值为-1时,代表降序。
将user表先按照age进行降序再按照posts字段进行升序排序。

db.users.aggregate(
   [
     { $sort : { age : -1, posts: 1 } }
   ]
)

如果$sort后面紧跟着$limit,$sort能够只维持前n条记录。n就是limit限定的条数。这样能够提高效率并且节省内存。
一般来说 $sort阶段有100M的内存限制。如果在运算的过程中超过这个大小,就会报错。为了能够处理更大规模的数据,可以再做聚合操作的时候将allowDiskUse设置为true,默认是false。就想下面这样:

db.stocks.aggregate([
                    { $project : { cusip: 1, date: 1, price: 1, _id: 0 } },
                    { $sort : { cusip : 1, date: 1 } }
                    ],
                    {
                      allowDiskUse: true
                    }
                   )

$sort操作可以利用索引以提高性能。但是必须在aggregate管道开始的地方使用$sort或者在$project,$unwind和$group的前面使用。如果在$project,$unwind和$group的后面使用$sort,那么系统将不能使用任何索引,这一点要引起注意。

10$lookup

先看一下语法模板:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

$lookup将用localField字段中的内容和from集合中的foreignField字段进行比较,如果比较相同,就将from集合中的这条记录加入当前文档中,字段名为as的值。
举个例子。
orders集合内容如下:

{ "_id" : 1, "item" : "abc", "price" : 12, "quantity" : 2 }
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1 }
{ "_id" : 3  }

inventory集合内容如下:

{ "_id" : 1, "sku" : "abc", description: "product 1", "instock" : 120 }
{ "_id" : 2, "sku" : "def", description: "product 2", "instock" : 80 }
{ "_id" : 3, "sku" : "ijk", description: "product 3", "instock" : 60 }
{ "_id" : 4, "sku" : "jkl", description: "product 4", "instock" : 70 }
{ "_id" : 5, "sku": null, description: "Incomplete" }
{ "_id" : 6 }

执行如下命令:

db.orders.aggregate([
    {
      $lookup:
        {
          from: "inventory",
          localField: "item",
          foreignField: "sku",
          as: "inventory_docs"
        }
   }
])

输出结果如下:

{
  "_id" : 1,
   "item" : "abc",
  "price" : 12,
  "quantity" : 2,
  "inventory_docs" : [
    { "_id" : 1, "sku" : "abc", description: "product 1", "instock" : 120 }
  ]
}
{
  "_id" : 2,
  "item" : "jkl",
  "price" : 20,
  "quantity" : 1,
  "inventory_docs" : [
    { "_id" : 4, "sku" : "jkl", "description" : "product 4", "instock" : 70 }
  ]
}
{
  "_id" : 3,
  "inventory_docs" : [
    { "_id" : 5, "sku" : null, "description" : "Incomplete" },
    { "_id" : 6 }
  ]
}

如果localField是一个数组而foreignField单独的元素,那么你需要使用$unwind来拆分数组。
orders有如下数据:

{ "_id" : 1, "item" : "MON1003", "price" : 350, "quantity" : 2, "specs" :
[ "27 inch", "Retina display", "1920x1080" ], "type" : "Monitor" }

inventory有如下数据:

{ "_id" : 1, "sku" : "MON1003", "type" : "Monitor", "instock" : 120,
"size" : "27 inch", "resolution" : "1920x1080" }
{ "_id" : 2, "sku" : "MON1012", "type" : "Monitor", "instock" : 85,
"size" : "23 inch", "resolution" : "1280x800" }
{ "_id" : 3, "sku" : "MON1031", "type" : "Monitor", "instock" : 60,
"size" : "23 inch", "display_type" : "LED" }

执行命令:

db.orders.aggregate([
   {
      $unwind: "$specs"
   },
   {
      $lookup:
         {
            from: "inventory",
            localField: "specs",
            foreignField: "size",
            as: "inventory_docs"
        }
   },
   {
      $match: { "inventory_docs": { $ne: [] } }
   }
])

首先将orders中的specs数组拆分成一个个的元素,然后执行$lookup操作,执行完毕后使用$match来匹配到”inventory_docs”非空的字段。
返回结果如下:

{
   "_id" : 1,
   "item" : "MON1003",
   "price" : 350,
   "quantity" : 2,
   "specs" : "27 inch",
   "type" : "Monitor",
   "inventory_docs" : [
      {
         "_id" : 1,
         "sku" : "MON1003",
         "type" : "Monitor",
         "instock" : 120,
         "size" : "27 inch",
         "resolution" : "1920x1080"
      }
   ]
}

11$out

指定将结果输出到哪里。
book包含如下数据:

{ "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 }
{ "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 }
{ "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 }
{ "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 }
{ "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 }

在books集合上执行如下操作:

db.books.aggregate( [
                      { $group : { _id : "$author", books: { $push: "$title" } } },
                      { $out : "authors" }
                  ] )

执行完毕后authors集合中就会有如下数据,可以通过db.authors.find()来进行查询。

{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] }
{ "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] }

12$facet

在一个stage里处理多条pipeline。
语法模板如下:

{ $facet:
   {
      <outputField1>: [ <stage1>, <stage2>, ... ],
      <outputField2>: [ <stage1>, <stage2>, ... ],
      ...
   }
}

每一个outputField对应一个pipeline。
artwork中含有如下数据:

{ "_id" : 1, "title" : "The Pillars of Society", "artist" : "Grosz", "year" : 1926,
  "price" : NumberDecimal("199.99"),
  "tags" : [ "painting", "satire", "Expressionism", "caricature" ] }
{ "_id" : 2, "title" : "Melancholy III", "artist" : "Munch", "year" : 1902,
  "price" : NumberDecimal("280.00"),
  "tags" : [ "woodcut", "Expressionism" ] }
{ "_id" : 3, "title" : "Dancer", "artist" : "Miro", "year" : 1925,
  "price" : NumberDecimal("76.04"),
  "tags" : [ "oil", "Surrealism", "painting" ] }
{ "_id" : 4, "title" : "The Great Wave off Kanagawa", "artist" : "Hokusai",
  "price" : NumberDecimal("167.30"),
  "tags" : [ "woodblock", "ukiyo-e" ] }
{ "_id" : 5, "title" : "The Persistence of Memory", "artist" : "Dali", "year" : 1931,
  "price" : NumberDecimal("483.00"),
  "tags" : [ "Surrealism", "painting", "oil" ] }
{ "_id" : 6, "title" : "Composition VII", "artist" : "Kandinsky", "year" : 1913,
  "price" : NumberDecimal("385.00"),
  "tags" : [ "oil", "painting", "abstract" ] }
{ "_id" : 7, "title" : "The Scream", "artist" : "Munch", "year" : 1893,
  "tags" : [ "Expressionism", "painting", "oil" ] }
{ "_id" : 8, "title" : "Blue Flower", "artist" : "O'Keefe", "year" : 1918,
  "price" : NumberDecimal("118.42"),
  "tags" : [ "abstract", "painting" ] }

执行命令:

db.artwork.aggregate( [
  {
    $facet: {
      "categorizedByTags": [
        { $unwind: "$tags" },
        { $sortByCount: "$tags" }
      ],
      "categorizedByPrice": [
        // Filter out documents without a price e.g., _id: 7
        { $match: { price: { $exists: 1 } } },
        {
          $bucket: {
            groupBy: "$price",
            boundaries: [  0, 150, 200, 300, 400 ],
            default: "Other",
            output: {
              "count": { $sum: 1 },
              "titles": { $push: "$title" }
            }
          }
        }
      ],
      "categorizedByYears(Auto)": [
        {
          $bucketAuto: {
            groupBy: "$year",
            buckets: 4
          }
        }
      ]
    }
  }
])

在这个例子中,我们有三条pipeline,分别为:categorizedByTags,categorizedByPrice,categorizedByYears。
最终返回的结果如下:
关于$bucket, $bucketAuto, $sortByCount的具体用法请参阅链接。
$bucket官方文档$bucketAuto官方文档, $sortByCount官方文档

{
  "categorizedByYears(Auto)" : [
    // First bucket includes the document without a year, e.g., _id: 4
    { "_id" : { "min" : null, "max" : 1902 }, "count" : 2 },
    { "_id" : { "min" : 1902, "max" : 1918 }, "count" : 2 },
    { "_id" : { "min" : 1918, "max" : 1926 }, "count" : 2 },
    { "_id" : { "min" : 1926, "max" : 1931 }, "count" : 2 }
  ],
  "categorizedByPrice" : [
    {
      "_id" : 0,
      "count" : 2,
      "titles" : [
        "Dancer",
        "Blue Flower"
      ]
    },
    {
      "_id" : 150,
      "count" : 2,
      "titles" : [
        "The Pillars of Society",
        "The Great Wave off Kanagawa"
      ]
    },
    {
      "_id" : 200,
      "count" : 1,
      "titles" : [
        "Melancholy III"
      ]
    },
    {
      "_id" : 300,
      "count" : 1,
      "titles" : [
        "Composition VII"
      ]
    },
    {
      // Includes document price outside of bucket boundaries, e.g., _id: 5
      "_id" : "Other",
      "count" : 1,
      "titles" : [
        "The Persistence of Memory"
      ]
    }
  ],
  "categorizedByTags" : [
    { "_id" : "painting", "count" : 6 },
    { "_id" : "oil", "count" : 4 },
    { "_id" : "Expressionism", "count" : 3 },
    { "_id" : "Surrealism", "count" : 2 },
    { "_id" : "abstract", "count" : 2 },
    { "_id" : "woodblock", "count" : 1 },
    { "_id" : "woodcut", "count" : 1 },
    { "_id" : "ukiyo-e", "count" : 1 },
    { "_id" : "satire", "count" : 1 },
    { "_id" : "caricature", "count" : 1 }
  ]
}

13$count

scores集合中有如下数据:

{ "_id" : 1, "subject" : "History", "score" : 88 }
{ "_id" : 2, "subject" : "History", "score" : 92 }
{ "_id" : 3, "subject" : "History", "score" : 97 }
{ "_id" : 4, "subject" : "History", "score" : 71 }
{ "_id" : 5, "subject" : "History", "score" : 79 }
{ "_id" : 6, "subject" : "History", "score" : 83 }

执行命令:

db.scores.aggregate(
  [
    {
      $match: {
        score: {
          $gt: 80
        }
      }
    },
    {
      $count: "passing_scores"
    }
  ]
)

在这个例子中,首先使用match得到score大于80的文档集合,然后计算当前文档集合中文档的数量。并且将这个数量给”passing_scores”这个字段。
返回结果:

{ "passing_scores" : 4 }
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页