Mongodb 聚合

参考

MongoDB权威指南(第2版)

Mongodb Docs

前言

Mongodb提供了一个强大的处理框架,可以对集合中的文档进行各种组合、过滤、输出,如果你通过Mongodb其他的查询方法无法处理你的查询或者查询难度很高,那么你可以试试聚合。

这里我们只讲解一下常用的聚合管道操作符,其他的可以自行到官方文档进行查询。

语法

官方文档的聚合语法是这样的:

db.collection.aggregate(pipeline,options);

聚合:计算集合或视图中数据的聚合值,并且聚合接受一个数组,数组的每一个成员都是对象,对象里面子键都代表一个管道操作,按照顺序的从第一个管道操作到最后一个,每次操作完的文档返回给下一个管道操作。

  • pipeline: 一系列数据聚合操作或阶段。有关详细信息,请参阅 聚合管道运算,在版本2.6中更改:该方法仍然可以将流水线阶段接受为单独的参数,而不是数组中的元素; 但是,如果不指定pipeline为数组,则不能指定 options参数。

  • options:可选的。aggregate()传递给aggregate命令的附加选项。版本2.6中的新功能,仅当您指定pipeline为数组时可用。

大概我们可以把聚合理解为对文档进行一系列的操作从而达到我们需要的查询文档,聚合提供了很多管道操作符,这些管道操作符就像一根一根的管道一些,而我们通过聚合操作的文档就像管道里面的水,这根管道流下来的水继续流到下一根管道,直到没有了管道,水就流出来了。

我将在下面讲解几个常用的管道操作符,你也可以单独使用其中任意的管道操作符,分别为:

  • match(过滤): 过滤文档流,只允许匹配的文档未修改地传递到下一个流水线阶段。$match 使用标准的MongoDB查询。对于每个输入文档,输出一个文档(匹配)或零个文档(不匹配)。

  • $project(映射):重新整理流中的每个文档,例如添加新字段或删除现有字段。对于每个输入文档,输出一个文档。

  • $group(分组):通过一些指定的累加器将组文档输出到下一阶段,为每个不同的分组输出文档。

  • $unwind(拆分):从输入文档中解构一个数组字段,以输出每个元素的文档。每个输出文档用元素值替换数组。对于每个输入文档,输出n个文档,其中n是数组元素的数量,对于空数组可以为零。

  • $sort(排序):按指定的排序键重新排序文档流。

  • $limit(限制):将前n个文档传递到管道,n是一个数字。

  • $skip(跳过):将前n个文档丢弃,后面的部分传递到管道。

上面的顺序一般是我们常用的顺序,首先我们筛选一部分文档,然后从筛选中整理一份更加便于我们操作的文档,然后在对整理好的文档通过一些表达式来分组整理,然后通过排序、限制、跳过,得到我们最终的结果。

在管道操作符中如果需要指定文档中的某个键的时候,需要在键名前面加上$符号,比如文档中的name键,在管道操作中要指定它就得这样写$name

并且我们所有的操作的文档都不是操作储存在硬盘中的数据,我们操作的文档都是储存在内存当中,所以不影响原始数据。

管道操作符

$match

$match应尽量放在聚合的最前面,因为$match使用的是Mongodb的标准查询,所以可以使用索引来提高我们的效率,其次是可以过滤掉不需要的文档,让后续的操作更加效率。

既然是标准的查询我们就可以使用标准查询里面所有操作符

下面来大概演示一下$match的操作方法:

实例的文档结构如下:

{ "_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 }

查询author键的值为dave的文档:

db.blog.aggregate([
  {
    $match: {
      author: 'dave'
    }
  }
]);

返回的文档为:

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

$project

映射管道操作就如最开始介绍的,整理文档便于我们后面更好的操作,它可以重新命名键,还可以去除不需要的键。

实例的文档结构如下:

{ "_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 }

重新命名authorname,这里需要注意的是重新命名键需要给文档中原始的键名前面加上一个$符号,并只输出namescore

代码:

db.blog.aggregate([
    {
        $project: {
            name: '$author',
            score: 1,

        }
    }
]);

返回的文档内容(Mongodb中所有的文档是默认返回_id,除非显示的声明它不需要返回):

{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "score" : 80, "name" : "dave" }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "score" : 85, "name" : "dave" }
{ "_id" : ObjectId("55f5a192d4bede9ac365b257"), "score" : 60, "name" : "ahn" }
{ "_id" : ObjectId("55f5a192d4bede9ac365b258"), "score" : 55, "name" : "li" }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b259"), "score" : 60, "name" : "annT" }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "score" : 94, "name" : "li" }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "score" : 95, "name" : "ty" }

$project管道操作符同样支持Mongodb规定的表达式,大家有兴趣可以去看看。

通过表达式我们能更好的操作文档,比如下面,我想给下面的文档中author键为dave的文档中的score加上10分,那么需要用到表达式中的$add操作符,可以像下面这样来操作:

实例的文档结构如下:

{ "_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.blog.aggregate([
    {
        $match: {
            author: 'dave'
        }
    },
    {
        $project: {
            score: {
                $add: ['$score', 10]
            },
            author: 1,
            _id: 0
        }
    }

]);

返回的文档内容:

{ "author" : "dave", "score" : 90 }
{ "author" : "dave", "score" : 95 }

$group

分组的作用是将文档中键的值,分成不同的组,然后通过累加器操作,然后输出文档,在进行$group管道操作中,_id键是必须存在的。

比如下面我需要将文档中author键的值相同的文档作一个统计:

实例的文档结构如下:

{ "_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.blog.aggregate([
  {
    $group: {
      _id: '$author',
      total: {
        $sum: 1
      }
    }
  }
]);

返回的文档内容:

{ "_id" : "ty", "total" : 1 }
{ "_id" : "annT", "total" : 1 }
{ "_id" : "li", "total" : 2 }
{ "_id" : "ahn", "total" : 1 }
{ "_id" : "dave", "total" : 2 }

$unwind

$unwind管道操作符可以将数组中的每个成员拆分为一个单独的文档,并输出给下一个管道操作。

比如下面的文档中a键包含了3个数组,使用$unwind管道操作符将成员拆分为单独的文档并输出。

实例的文档结构如下:

{
    a: [
        {
            b: 1
        },
        {
            b: 2
        },
        {
            b: 3
        }
    ]
}

执行的代码:

db.blog.aggregate([
    {
        $unwind: '$a'
    }
]);

返回的文档内容:

{ "_id" : ObjectId("598c5807841dccce335a0e98"), "a" : { "b" : 1 } }
{ "_id" : ObjectId("598c5807841dccce335a0e98"), "a" : { "b" : 2 } }
{ "_id" : ObjectId("598c5807841dccce335a0e98"), "a" : { "b" : 3 } }

$sort

排序是将上一个管道操作符输入进来的文档进行排序,它指定排序的键然后值为升序1和降序-1

比如需要对下面的文档中的score键的值进行降序排序:

实例的文档结构如下:

{ "_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.blog.aggregate([
    {
        $sort: {
            score: -1
        }
    }
]);

返回的文档:

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

$limit

$limint管道操作符接受一个数字,将前n个文档输出。

比如下面输出下面文档的前3个文档:

{ "_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.blog.aggregate([
    {
        $limit: 3
    }
]);

返回的文档内容:

{ "_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 }

$skip

$limint管道操作符接受一个数字,将前n个文档后的文档输出到下一个管道。

比如下面输出下面文档的后2个文档:

{ "_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.blog.aggregate([
    {
        $skip: 5
    }
]);

返回的文档内容:

{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "author" : "li", "score" : 94, "views" : 999 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "author" : "ty", "score" : 95, "views" : 1000 }

其他

Mongodb不允许单一聚合操作符占用超过20%的内存,如果超过了就会直接抛出错误,并且尽可能的让$match管道操作符在前面,因为它可以使用索引,然后通过$project$group尽可能的过滤掉不需要的数据。

文档信息

发表评论

电子邮件地址不会被公开。 必填项已用*标注