Aggregation简单来说,就是将文档统计。分析、分类的方法。
Aggregation接收指定collection的数据集,通过计算后返回result数据的过程。它中间会经历多个stages。整体而言就是一个pipeline(管道)。
图片来源
由上图可知,文档(数据)经过筛选、分组、统计等过程最终输出数据。
语法:
db.collection.aggregate(pipeline,options);
参数说明:
参数 | 类型 | 描述 |
---|---|---|
pipeline | array | 一系列数据聚合操作。详见聚合管道操作符,在版本2.6后,该方法可以接收pipeline作为参数而非数组,但若不指定数组,则不能指定options参数 |
options | document | 可选,aggregate()传递给聚合命令的其他选项。2.6版本中新增功能:仅当将管道指定为数组时才可使用。 |
db.collection.aggregate()可以用多个构件创建一个管道,对文档进行一连串的处理。这些构件包括:筛选(match)、映射(project)、分组(group)、排序(sort)、限制(limit)、跳跃(skip)。
每个阶段的管道限制为100M内存,如果一个节点管道超过这个极限,MongoDB会产生一个错误,为了处理大型数据集,可以设置options参数(allowDiskUse=true)将聚合管道节点的数据写入临时文件。这样就可以解决100M的内存限制。
db.collection.aggregate()
输出结果只能保存在一个文档中,BSON Document大小限制为16M。可以通过返回指针解决。版本2.6后,返回一个指针,可以返回任何数据集大小。
官网描述
1. 聚合操作
$count
:统计数量
db.stus.aggregate([
{
$match:{"scope":{$gte:60}}
},{
$count:"count" //统计函数,计算数量
}
],{
allowDiskUse:true //options(可选)即允许磁盘上缓存
})
注意$count
统计数量,上述有两个操作。
-
$match
阶段,相当于where操作,将大于等于60的scope
传递给下一个=阶段。 -
$count
阶段是返回聚合管道中剩余的文档计数,并将值分配给名为count
的字段。
相当于sql:select count(*) as count from stus where scope>=60;
$group
分组
按指定的表达式对文档进行分组,并将每个不同分组的文档输出到一个阶段。输出文档包含_id
字段,文档以_id
的值进行分组。
输出文档还可以包含计算字段,该字段保存由$group
的_id
字段分组的一些accumulator
表达式的值。$group
不会输出具体的文档而只是统计信息。
语法:
{$group:{_id:,:{ : },...}}
- _id字段是必填的:但是,
_id:null
可以用来为整个输出文档计算累计值。 - 剩余字段是可选的,并使用
运算符进行计算。 - _id和
表达式可以接受任何有效的表达式。
db.stus.aggregate([
{
$group:{
"_id":"$age",
"sum":{"$sum":"$scope"}, //计算总和。
"avg":{"$avg":"$scope"}, //计算平均值。
"min":{"$min":"$scope"}, //获取集合中所有文档对应值得最小值。
"max":{$max:"$scope"}, //获取集合中所有文档对应值得最大值。
"first":{$first:"$scope"}, //返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。
"last":{$last:"$scope"}, //返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。
"push":{$push:"$name"}, //将指定的表达式的值添加到一个数组中。
"addToSet":{$addToSet:"$scope"} //将表达式的值添加到一个集合中(无重复值,无序)。
}
}
]);
输出参数类型:
{
"_id" : "14",
"sum" : 126,
"avg" : 63,
"min" : 59,
"max" : 67,
"first" : 59,
"last" : 67,
"push" : ["数组4", "数组10"],
"addToSet" : [59, 67]
}
注意:
-
$group
阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true。 - mongodb类型值不会进行隐式转换,即使值相同,但是不是相同的类型,也是不同的组。
- accumulator表达式只能处理number类型。
练习2:
插入数据:
db.items.insert([
{ "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-03-01T08:00:00Z") },
{ "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-03-01T09:00:00Z") },
{ "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-03-15T09:00:00Z") },
{ "_id" : 4, "item" : "xyz", "price" : 5, "quantity" : 20, "date" : ISODate("2014-04-04T11:21:39.736Z") },
{ "_id" : 5, "item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-04-04T21:23:13.331Z") }
]);
1. 按date
的月份、日期、年份对文档进行分组,并计算total price
和average quantity
,并汇总各文档的数量。
$group
可以对多个字段进行分组。
最终执行结果如下图所示。
/* 1 */
{
"_id" : {
"month" : 3,
"day" : 1,
"year" : 2014
},
"totalPrice" : 40,
"avg" : 1.5,
"count" : 2
}
/* 2 */
{
"_id" : {
"month" : 4,
"day" : 4,
"year" : 2014
},
"totalPrice" : 200,
"avg" : 15,
"count" : 2
}
/* 3 */
{
"_id" : {
"month" : 3,
"day" : 15,
"year" : 2014
},
"totalPrice" : 50,
"avg" : 10,
"count" : 1
}
2. 那么group null是什么效果呢?
可以看出,实际上是对所有文档进行统计。
3. 如何查询distinct values的数据
使用group分组完成去重的功能。
4. 数据转换
1)将集合中的数据按price
分组并转换成item的数组。
price
相同的item
在一个数组中
2)使用系统变量$$ROOT
按price对文档进行分组,生成的文档不得超过BSON文档大小限制。
db.items.aggregate([{
$group:{_id:"$price",items:{$push:"$$ROOT"}}
}]);
执行结果:
/* 1 */
{
"_id" : 5,
"items" : [{
"_id" : 3,
"item" : "xyz",
"price" : 5,
"quantity" : 10,
"date" : ISODate("2014-03-15T09:00:00Z")
}, {
"_id" : 4,
"item" : "xyz",
"price" : 5,
"quantity" : 20,
"date" : ISODate("2014-04-04T11:21:39.736Z")
}]
}
/* 2 */
{
"_id" : 20,
"items" : [{
"_id" : 2,
"item" : "jkl",
"price" : 20,
"quantity" : 1,
"date" : ISODate("2014-03-01T09:00:00Z")
}]
}
/* 3 */
{
"_id" : 10,
"items" : [{
"_id" : 1,
"item" : "abc",
"price" : 10,
"quantity" : 2,
"date" : ISODate("2014-03-01T08:00:00Z")
}, {
"_id" : 5,
"item" : "abc",
"price" : 10,
"quantity" : 10,
"date" : ISODate("2014-04-04T21:23:13.331Z")
}]
}
$match
过滤操作
过滤文档,仅将符合指定条件的文档传递到下一个管道阶段。
$match
接受一个指定查询条件的文档。
语法:
{ $match: { } }
管道优化:
$match
用于对文档进行筛选,之后可以得到文档子集上做聚合,$match
可以使用除了地理空间之外的所有常规查询操作符。在实际应用中尽可能将$match放在管道的前面位置。
- 可以快速将不需要的文档过滤掉,以减少管道的工作量;
- 如果在
$project
和$group
之前执行$match
,查询可以使用索引。:
案例:
- 插入数据:
db.views.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 }
]);
1. 使用$match
做简单的匹配查询
2. 使用$match
管道处理文档,然后将结果分组
$match
使用MongoDB的标准查询操作,放在group前相当于where,放在group后相当having。
unwind
$unwind
:将文档中某一个数组类型字段拆分成多条,每条包含数组中的一个值。
语法:
{$unwind:}
注:要指定字段的路径,在字段名称前加上$符并使用引号
在3.2后增加的新语法
{
$unwind:{
path:,
includeArrayIndex:, //可选。一个新字段的名称用于存放元素的数组索引,该名称不能以$开头。
preserveNullAndEmptyArrays: #可选default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档
}
}
如果为输出文档汇总不存在的字段指定路径,或者该字段为空数组,则$unwind
默认会忽略输入文档,并且不会输出该文档的文档。
案例1:简单的$unwind
命令
db.testunwind.insert(
{ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] }
);
使用$unwind
为sizes数组中的每个元素输出一个文档。
每个文档与输入文档相同,除了sizes字段的值是元素sizes数组的值。
案例二:使用3.2新增的属性字段
db.testunwind2.insert([
{ "_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 }
]);
1)使用includeArrayIndex参数
db.testunwind2.aggregate([
{$unwind:{path:"$sizes",includeArrayIndex:"arrayIndex"}}
]);
最终结果是将含有sizes
数组的文档进行输出,且携带着数组元素下标。
{ "_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 }
2)使用preserveNullAndEmptyArrays参数
$project
映射操作
可以将其看做sql中的select
操作。
$project
可以在文档中选择想要的字段和不想要的字段。也可以通过管道表达式进行一些复杂的操作。如数学操作、日期操作、字符串操作、逻辑操作。
语法:
{ $project: { } }
$project
管道符的作用是选择字段(指定字段、添加字段、不显示字段,排除字段等),重命名字段,派生字段。
specification有以下形式:
-
是否包含该字段,field:1/0,表示选择/不选择field。:<1 or true> -
_id:<0 or false>
是否指定_id字段。 : 添加新字段或重置现有字段的值,在版本3.6中更改:Mongodb3.6添加变量REMOVE。如果表达式的计算结果为 $$REMOVE
,则该字段将排除在输出外。- 投影或添加/重置嵌入文档的字段时,可以使用
.
符号,例如:
"contact.address.country": <1 or 0 or expression>
或
contact: { address: { country: <1 or 0 or expression> } }
案例一:
db.testproject.insert(
{
"_id" : 1,
title: "abc123",
isbn: "0001122223334",
author: { last: "zzz", first: "aaa" },
copies: 5,
lastModified: "2016-07-28"
}
)
1)$project
默认输出_id
字段。
2)_id
字段默认包含在内,要从$project
阶段的输出文档中排除_id字段。
3)在$project
阶段从输出中排除lastModified字段:
4)在嵌套文档中排除字段。
5)3.6版本中的新功能:从3.6版本开始,可以从聚合表达式中使用变量REMOVE
来有条件地禁止一个字段。
继续插入数据
db.testproject.insert([
{
"_id" : 2,
title: "Baked Goods",
isbn: "9999999999999",
author: { last: "xyz", first: "abc", middle: "" },
copies: 2,
lastModified: "2017-07-21"
},
{
"_id" : 3,
title: "Ice Cream Cakes",
isbn: "8888888888888",
author: { last: "xyz", first: "abc", middle: "mmm" },
copies: 5,
lastModified: "2017-07-22"
}
])
$project
阶段使用REMOVE变量来排除author.middle
字段,前提是它等于"":
db.testproject.aggregate([
{
$project:{
title:1,
"author.first":1, //嵌套文档输出特别的内容
"author.middle":{
$cond:{
if:{$eq:["","$author.middle"]},
then:"$$REMOVE",
else:"$author.middle"
}
}
}
}
])
最终效果:
/* 1 */
{
"_id" : 1,
"title" : "abc123",
"author" : {
"first" : "aaa"
}
}
/* 2 */
{
"_id" : 2,
"title" : "Baked Goods",
"author" : {
"first" : "abc"
}
}
/* 3 */
{
"_id" : 3,
"title" : "Ice Cream Cakes",
"author" : {
"first" : "abc",
"middle" : "mmm"
}
}
6)只含有嵌套文档的指定字段
db.testproject1.insert([
{ _id: 1, user: "1234", stop: { title: "book1", author: "xyz", page: 32 } },
{ _id: 2, user: "7890", stop: [ { title: "book2", author: "abc", page: 5 }, { title: "book3", author: "ijk", page: 100 } ] }
])
db.testproject1.aggregate([
{
$project:{"stop.title":1}
}
])
返回结果:
{ "_id" : 1, "stop" : { "title" : "book1" } }
{ "_id" : 2, "stop" : [ { "title" : "book2" }, { "title" : "book3" } ] }
对字段进行计算
插入数据:
db.testproject2.insert([
{
"_id" : 1,
title: "abc123",
isbn: "0001122223334",
author: { last: "zzz", first: "aaa" },
copies: 5
}
])
db.testproject2.aggregate([
{
$project:{
title:1,
isbn:{
prefix:{$substr:["$isbn",0,3]}, //对字段进行处理(截取)
checkDigit: { $substr: [ "$isbn", 12, 1] }
},
lastname:"author.last",
copiesSold: "$copies"
}
}
])
返回结果:
{
"_id" : 1,
"title" : "abc123",
"isbn" : {
"prefix" : "000",
"checkDigit" : "4"
},
"lastname" : "author.last",
"copiesSold" : 5
}
投影出新的数组字段
db.testproject3.insert([
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "x" : 1, "y" : 1 }
]);
db.testproject3.aggregate([
{
$project:{
myArray:["$x","$y"]
}
}
])
如果数组中包含不存在的字段,则会返回null
$sort
:排序,1升,-1降,sort一般在group后,也就是说得到结果在排序,如果先排序在分组没有意义。
db.pts.aggregate([
{
$group:{
"_id":"$school_id",
"count":{"$sum":1}
}
}, //先分组
{
$sort:{"count":-1} //对分组内容进行排序
}
])
$limit
用来限制Mongodb聚合管道返回的文档数,相当于mysql中的limit m
,不能设置偏移量。
db.pts.aggregate([
{
$limit:2
}
]);
$skip
聚合管道中跳过指定数量的文档,并返回余下的文档。
db.pts.aggregate([
{
$skip:2
}
]);
skip(), limilt(), sort()三个放在一起执行的时候,执行的顺序是先 sort(), 然后是 skip(),最后是显示的 limit()。
$unwind
:将文档中某一个数组类型字段拆分成多条,每条包含数组中的一个值。
db.pts.aggregate([
{
$unwind:"$items"
}
]);
$group
将集合中的文档分组,可用于统计结果,其中_id固定写法。
db.pts.aggregate([
{
$group:{
"_id":"$school_id", //以school_id的值分组
"count":{"$sum":1} //聚合函数,输出count(1)
}
}
]);
推荐阅读
https://www.cnblogs.com/duanxz/p/3600052.html
mongodb高级聚合查询