MongoDB的聚合操作主要是对数据的批量处理。一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简单操作,也可以对记录进行数据统计,数据挖掘的复杂操作。聚合操作的输入是集中的文档,输出可以是一个文档也可以是多个文档。
MongoDB 提供了三种强大的聚合操作:
Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息。
本篇主要讲解 单目的聚合操作(Single Purpose Aggregation Operation)和聚合管道(Aggregation Pipeline)。
一、单目的聚合操作
单目的聚合命令常用的有:count() 和 distinct()。以 distinct() 为例,其工作流程如下:
1.1 count
count用于返回集合中的文档数量。
示例
求出集合中 job_base-accdate 值大于 2018-11-06 的文档个数
db.getCollection('job_create').find({'job_base-accdate': {$gt: new Date('06/11/2018')}}).count()
等价于
db.getCollection('job_create').count({'job_base-accdate': {$gt: new Date('06/11/2018')}})
1.2 distinct
distinct用于去重
示例
对 job_base-jid 值进行去重操作
db.getCollection('job_create').distinct('job_base-jid')
二、聚合管道
MongoDB 中使用 db.COLLECTION_NAME.aggregate([{<stage>},...])
方法来构建和使用聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果。聚合管道的工作流程如下:
-
$match
用于获取status = "A"
的记录,然后将符合条件的记录送到下一阶段 -
$group
中进行分组求和计算,最后返回 Results。
其中,$match
、$group
都是阶段操作符,而阶段 $group
中用到的 $sum
是表达式操作符。
2.1 阶段操作符
在下面的示例中我们会使用如下集合进行讲解:
>db.article.find().pretty()
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"title" : "MongoDB Aggregate",
"author" : "simon",
"tags" : [
"Mongodb",
"Database",
"Query"
],
"pages" : 5.0,
"time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"title" : "MongoDB Index",
"author" : "simon",
"tags" : [
"Mongodb",
"Index",
"Query"
],
"pages" : 3.0,
"time" : ISODate("2018-11-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : [
"Mongodb",
"Query"
],
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
}
2.1.1 $project
$project
用于修改输入文档的结构。可以用来重命名、增加或删除字段(域),也可以用于创建计算结果以及嵌套文档。
示例
返回的文档中只包含_id
和tages
>db.article.aggregate([{$project:{_id:1,tags:1}}])
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"tags" : [
"Mongodb",
"Database",
"Query"
]
},
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"tags" : [
"Mongodb",
"Index",
"Query"
]
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"tags" : [
"Mongodb",
"Query"
]
}
新增字段
>db.article.aggregate([{$project:{_id:1,tags:1,editAuthor:'$author'}}])
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"tags" : [
"Mongodb",
"Database",
"Query"
],
"editAuthor" : "simon"
},
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"tags" : [
"Mongodb",
"Index",
"Query"
],
"editAuthor" : "simon"
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"tags" : [
"Mongodb",
"Query"
],
"editAuthor" : "Aaron"
}
2.1.2 $match
$match
用于过滤数据,只输出符合条件的文档。
示例
查询出文档中 author 为 simon的数据
>db.article.aggregate([{$match:{author:'simon'}}])
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"title" : "MongoDB Aggregate",
"author" : "simon",
"tags" : [
"Mongodb",
"Database",
"Query"
],
"pages" : 5.0,
"time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"title" : "MongoDB Index",
"author" : "simon",
"tags" : [
"Mongodb",
"Index",
"Query"
],
"pages" : 3.0,
"time" : ISODate("2018-11-11T16:00:00.000Z")
}
2.1.3 $group
$group
用于将集合中的文档分组,可用于统计结果
示例
统计每个作者写的文章篇数
>db.article.aggregate([{$group:{_id:'$author',total:{$sum:1}}}])
{
"_id" : "Aaron",
"total" : 1.0
},
{
"_id" : "simon",
"total" : 2.0
}
2.1.4 $sort
对集合中的文档进行排序
示例
让集合按照页数进行升序排序
>db.article.aggregate([{$sort:{pages:1}}])
{
"_id" : ObjectId("5c088fec651e67152257d454"),
"title" : "MongoDB Index",
"author" : "simon",
"tags" : [
"Mongodb",
"Index",
"Query"
],
"pages" : 3.0,
"time" : ISODate("2018-11-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"title" : "MongoDB Aggregate",
"author" : "simon",
"tags" : [
"Mongodb",
"Database",
"Query"
],
"pages" : 5.0,
"time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : [
"Mongodb",
"Query"
],
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
}
注意
如果以降序排列,则设置成 pages: -1
2.1.5 $unwind
将文档中数组类型的字段拆分成多条,每条文档包含数组中的一个值
示例
将集合中 tags
字段进行拆分
>db.article.aggregate([{$match:{author:'Aaron'}},{$unwind:'$tags'}])
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : "Mongodb",
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
},
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : "Query",
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
}
注意
-
$unwind
参数不是一个数组类型时,将会抛出异常 -
$unwind
所作的修改,只用于输出,不能改变原文档
2.1.6 $limit
限制返回文档的数量
示例
返回集合的前一条文档
>db.article.aggregate([{$limit: 1}])
{
"_id" : ObjectId("5c088fec651e67152257d453"),
"title" : "MongoDB Aggregate",
"author" : "simon",
"tags" : [
"Mongodb",
"Database",
"Query"
],
"pages" : 5.0,
"time" : ISODate("2017-06-11T16:00:00.000Z")
}
2.1.7 $skip
跳过指定数量的文档,并返回余下的文档
示例
跳过集合的前两个文档
>db.article.aggregate([{$skip: 2}])
{
"_id" : ObjectId("5c088fec651e67152257d455"),
"title" : "MongoDB Query",
"author" : "Aaron",
"tags" : [
"Mongodb",
"Query"
],
"pages" : 8.0,
"time" : ISODate("2019-06-11T16:00:00.000Z")
}
2.2表达式操作符
表达式操作符有很多操作类型,其中最常用的有布尔聚合操作、集合操作、比较聚合操作、算术聚合操作、字符串聚合操作、数组聚合操作、日期聚合操作、条件聚合操作、数据类型聚合操作等
2.2.1 布尔聚合操作
-
$and
与 -
$or
或 -
$not
非
示例
>db.getCollection('col').find()
{
"_id" : ObjectId("5c08c5b5651e67152257d45b"),
"name" : "a",
"classes" : "classe 1",
"score" : 90.0
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45c"),
"name" : "b",
"classes" : "classe 2",
"score" : 50.0
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45d"),
"name" : "c",
"classes" : "classe 3",
"score" : 60.0
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45e"),
"name" : "d",
"classes" : "classe 4",
"score" : 70.0
}
判断成绩是否大于80或者小于50
>db.col.aggregate(
[
{
$project:
{
name: 1,
score:1,
result: { $or: [ { $gt: [ "$score", 80 ] }, { $lt: [ "$score", 50 ] } ] }
}
}
]
)
{
"_id" : ObjectId("5c08c5b5651e67152257d45b"),
"name" : "a",
"score" : 90.0,
"result" : true
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45c"),
"name" : "b",
"score" : 50.0,
"result" : false
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45d"),
"name" : "c",
"score" : 60.0,
"result" : false
},
{
"_id" : ObjectId("5c08c5b5651e67152257d45e"),
"name" : "d",
"score" : 70.0,
"result" : false
}
2.2.2 集合操作
-
$setEquals
除了重复元素外,包括的元素相同 -
$setIntersection
交集 -
$setUnion
并集 -
$setDifference
只在前一集合出现,也就是后一个集合的补集 -
$setIsSubset
前一个集合是后一个集合的子集 -
$anyElementTrue
一个集合内,只要一个元素为真,则返回true -
$allElementsTrue
一个集合内,所有的元素都为真,则返回true
示例
>db.col.find()
{
"_id" : ObjectId("5c08c98d651e67152257d45f"),
"A" : [
"java",
"phython",
"c++"
],
"B" : [
"java",
"phython",
"c++"
]
},
{
"_id" : ObjectId("5c08c98d651e67152257d460"),
"A" : [
"java",
"c++"
],
"B" : [
"java",
"phython",
"c++"
]
},
{
"_id" : ObjectId("5c08c98d651e67152257d461"),
"A" : [
"java",
"c++"
],
"B" : []
}
计算A和B集合的
>db.col.aggregate(
[
{ $project: { A:1, B: 1, union: { $setIntersection: [ "$A", "$B" ] }} }
]
)
{
"_id" : ObjectId("5c08c98d651e67152257d45f"),
"A" : [
"java",
"phython",
"c++"
],
"B" : [
"java",
"phython",
"c++"
],
"union" : [
"c++",
"java",
"phython"
]
},
{
"_id" : ObjectId("5c08c98d651e67152257d460"),
"A" : [
"java",
"c++"
],
"B" : [
"java",
"phython",
"c++"
],
"union" : [
"c++",
"java"
]
},
{
"_id" : ObjectId("5c08c98d651e67152257d461"),
"A" : [
"java",
"c++"
],
"B" : [],
"union" : []
}
2.2.3 比较操作
-
$cmd
两个值相等返回0,前值大于后值返回1,前值小于后值返回-1 -
$eq
是否相等 -
$gt
前值是否大于后值 -
$gte
前值是否大于等于后值 -
$lt
前值是否小于后值 -
$lte
前值是否小于等于后值 -
$ne
是否不相等
示例
>db.col.find()
{
"_id" : ObjectId("5c08cbb3651e67152257d463"),
"score" : 80.0
}
score 大于等于 80
>db.col.aggregate(
[
{$project:{_id:1,score:1,result:{$gte:['$score',80]}}}
]
)
{
"_id" : ObjectId("5c08cbb3651e67152257d463"),
"score" : 80.0,
"result" : true
}
2.2.4 算数聚合操作
-
$abs
绝对值 -
$add
和 -
$ceil
向上取整 -
$divide
除 -
$exp
几次方 -
$floor
向下取整 -
$ln
自然对数 -
$log
对数 -
$log10
以10为底的对数 -
$mod
取模 -
$multiply
乘 -
$pow
指数 -
$sqrt
平方根 -
$subtract
减 -
$trunc
截掉小数取整
示例
score 加 10
db.col.aggregate(
[
{$project:{_id:1,score:1,result:{$add:['$score',10]}}}
]
)
{
"_id" : ObjectId("5c08cbb3651e67152257d463"),
"score" : 80.0,
"result" : 90.0
}
2.2.5 字符串聚合操作
-
$concat
字符串连接 -
$indexOfBytes
子串位置(字节) -
$indexOfCP
子串位置(字符) -
$split
分割字符串 -
$strLenBytes
字节长度 -
$strLenCP
字符长度 -
$strcasecmp
字符串比较 -
$substrBytes
创建子串(按字节) -
$substrCP
创建子串(按字符) -
$toLower
小写 -
$toUpper
大写
示例
>db.col.find()
{
"_id" : ObjectId("5c08cf2d651e67152257d464"),
"name" : "abcdefgAAADccsD"
}
将 name 值大写
>db.col.aggregate([
{
$project: {name: 1,result:{$toUpper:'$name'}}
}
])
{
"_id" : ObjectId("5c08cf2d651e67152257d464"),
"name" : "abcdefgAAADccsD",
"result" : "ABCDEFGAAADCCSD"
}
2.2.6 数组聚合操作
-
$arrayElemAt
返回指定数组索引中的元素 -
$concatArrays
数组连接 -
$filter
返回筛选后的数组 -
$indexOfArray
索引 -
$isArray
是否是数组 -
$range
创建数值数组 -
$reverseArray
反转数组 -
$reduce
对数组中的每个元素应用表达式,并将它们组合成一个值 -
$size
数组元素个数 -
$slice
子数组 -
$zip
合并数组 -
$in
返回一个布尔值,表示指定的值是否在数组中
示例
>db.col.find()
{
"A" : [
"java",
"phython",
"c++"
],
"B" : [
"java",
"phython",
"c++"
]
}
判断指定元素是否在数组中
db.col.aggregate([
{
$project: {A: 1,B:1,result:{$in:['java','$A']}}
}
])
{
"_id" : ObjectId("5c08c98d651e67152257d45f"),
"A" : [
"java",
"phython",
"c++"
],
"B" : [
"java",
"phython",
"c++"
],
"result" : true
}
2.2.7 日期聚合操作
-
$dayOfYear
日(1-366) -
$dayOfMonth
月(1-23) -
$dayOfWeek
星期(1 (Sunday) 到 7 (Saturday)) -
$year
年 -
$month
月(1-12) -
$week
周(0-53) -
$hour
时(0-23) -
$minute
分(0-59) -
$second
秒(0-60) -
$millisecond
毫秒(0-999) -
$dateToString
返回格式化字符串的日期 -
$isoDayOfWeek
以ISO 8601格式返回星期几 -
$isoWeek
以ISO 8601格式返回周号,范围从1到53 -
$isoWeekYear
以ISO 8601格式返回年份编号
示例
>db.col.find()
{
"_id" : ObjectId("5c08d61d651e67152257d465"),
"date" : ISODate("2018-12-06T07:56:13.930Z")
}
日期聚合操作
>db.col.aggregate(
[
{
$project:
{
year: { $year: "$date" },
month: { $month: "$date" },
day: { $dayOfMonth: "$date" },
hour: { $hour: "$date" },
minutes: { $minute: "$date" },
seconds: { $second: "$date" },
milliseconds: { $millisecond: "$date" },
dayOfYear: { $dayOfYear: "$date" },
dayOfWeek: { $dayOfWeek: "$date" },
week: { $week: "$date" }
}
}
]
)
{
"_id" : ObjectId("5c08d61d651e67152257d465"),
"year" : 2018,
"month" : 12,
"day" : 6,
"hour" : 7,
"minutes" : 56,
"seconds" : 13,
"milliseconds" : 930,
"dayOfYear" : 340,
"dayOfWeek" : 5,
"week" : 48
}
2.2.8 数据类型集合操作
-
$type
返回字段类型
示例
>db.col.aggregate(
[
{
$project:
{
date:1,
type:{$type:'$date'}
}
}
]
)
{
"_id" : ObjectId("5c08d61d651e67152257d465"),
"date" : ISODate("2018-12-06T07:56:13.930Z"),
"type" : "date"
}
2.3 聚合管道的优化与限制
2.3.1 优化
默认情况下,在整个集合作为聚合管道的输入情况下,为了提高处理数据的效率,可以使用一下策略:
- 将
$match
和$sort
放到管道的前面,可以给集合建立索引,来提高处理数据的效率 - 可以用
$match
、$limit
、$skip
对文档进行提前过滤,以减少后续处理文档的数量
当聚合管道执行命令时,MongoDB 也会对各个阶段自动进行优化,主要包括以下几个情况:
-
$sort
+$match
顺序优化:如果$match
出现在$sort
之后,优化器会自动把$match
放到$sort
前面 -
$skip
+$limit
顺序优化:如果$skip
在$limit
之后,优化器会把$limit
移动到$skip
的前面,移动后$limit
的值等于原来的值加上$skip
的值。例如:移动前:{$skip: 10, $limit: 5}
,移动后:{$limit: 15, $skip: 10}
2.3.2 限制
- 返回结果大小:聚合结果返回的是一个文档,不能超过 16M,从 MongoDB 2.6版本以后,返回的结果可以是一个游标或者存储到集合中,返回的结果不受 16M 的限制
- 内存:聚合管道的每个阶段最多只能用 100M 的内存,如果超过100M,会报错,如果需要处理大数据,可以使用 allowDiskUse 选项,存储到磁盘上