聚合框架
先看一个示例:有一个保存着杂志文章的集合,希望找出发表文章最多的前3名作者。那么按照以下步骤创建管道:
1:将每篇文章中的作者投射出来;
2:将作者按照名称排序,统计出每个名字出现的次数;
3:将作者按照名字出现的次数降序排列;
4:将返回的结果限制为前3个。
1:{"$project": {"author": 1}} --> {"_id": id, "author": authorName}
通过"fieldName": 1选择需要投射的字段,默认会投射"_id"字段。
2:{"$group": {"_id": "$author", "count": {"$sum": 1}}} --> {"_id": authorName, "count": articleCount}
指明分组字段"author",该操作执行完后,每个作者只对应一个结果文档,所以"author"就成了文档的唯一标识符("_id")。
3:{"$sort": {"$count": 1}}
4:{"$limit": 3}
> db.article.aggregate(
... {"$project": {"author": 1}},
... {"$group": {"_id": "$author", "count": {"$sum": 1}}},
... {"$sort": {"$count": 1}},
... {"$limit": 3}
)
{
"result": [
{
"_id": "Bob",
"count": 490
},
{
"_id": "Ellice",
"count": 420
},
{
"_id": "Nora",
"count": 400
}
],
"ok": 1
}
如果管道没有给出预期的结果,可以按照需要进行调试,逐步添加管道操作符调试。
管道操作符
$match
$match:对于文档集合进行筛选,之后可以在筛选得到的文档子集上做聚合。注意:不能在"$match"上使用地理空间操作符。尽可能将$match放在管道的前面位置,这样做优点:过滤不需要的文档,以减少管道的工作量,在投射和分组之前执行$match,查询可以使用索引。
$project
$project:从文档中提取字段,还可以重命名字段,等做一些有意思的操作。 // 将每个用户文档的"_id"在返回结果中重命名成userId:
> db.article.aggregate(
... {"$project": {"userId": "$_id", "_id": 0}}
)
{
"result": [
{
"userId": ObjectId("54fd0571e4b055a0030461fb")
},
{
"userId": ObjectId("54fd0571e4b055a0030461fc")
},
...
],
"ok": 1
}
注意:对字段进行重命名时,MongoDB并不会记录字段的历史名称。因此,如果在原字段名上有一个索引,被重命名之后,聚合框架之后就无法使用这个索引。因此应该尽量在修改字段名称之前使用索引。
// 聚合框架无法在sort操作中使用在originFieldName上创建的索引:
> db.article.aggregate(
... {"$project": {"newFieldName": "$originFieldName"}},
... {"$sort": {"newFieldName": 1}}
)
1.管道表达式
在集合框架中,有几个表达式可以用来组合或进行任意深度的嵌套,以便创建复杂的表达式。2.数学表达式
// 求总薪资:
> db.employees.aggregate(
... {
... "$project": {
... "totalPay": {
... "$add": ["$salary", $bonus]
... }
... }
... }
)
操作符 | 语法 | 描述 |
$add | [exp1[, exp2, ..., expN]] | 求和 |
$subtract | [exp1, exp2] | 求差 |
$multiply | [exp1[, exp2, ..., expN]] | 求积 |
$divide | [exp1, exp2] | 求商 |
$mod | [exp1, exp2] | 求余 |
3.日期表达式
// 计算每个雇员在公司的工作时间:
> db.employees.aggregate(
... {
... "$project": {
... "tenure": {
... "$subtract": [{"$year": new Date()}}, {"$year": "$hireDate"}]
... }
... }
... }
)
提取日期的表达式:"$year", "$month", "$week", "$dayOfMonth", "$dayOfWeek", "$dayOfYear", "$hour", "$minute", "$second"。
4.字符串表达式
// 生成j.doe@example.com格式的email地址,提取firstName第一个字符,并拼接lastName生成指定格式:
> db.employees.aggregate(
... {
... "$project": {
... "email": {
... "$concat": [
... {
... "$substr": ["$firstName", 0, 1],
... ".",
... "$lastName",
... "@example.com"
... }
... ]
... }
... }
... }
)
操作符 | 语法 | 描述 |
$substr | [expr, startOffset, numToReturn] | 截取字符串,从startOffset字节开始的numToReturn个字节 |
$concat | [exp1[, exp2, ... , expN]] | 连接字符串 |
$toLower | expr | 转换称小写 |
$toUpper | expr | 转换称大写 |
5.逻辑表达式
// 为学生打分,出勤率占10%,日常测验占30%,期末考试占60%,(如果是老师最宠爱的学生,那么分数是100):
> db.students.aggregate(
... {
... "$project": {
... "grade": {
... "$cond": [
... "$teacherPet",
... 100, // if
... { // else
... "$add": [
... "$multiply": [.1, "$attendanceAvg"],
... "$multiply": [.3, "$quizzAvg"],
... "$multiply": [.6, "$testAvg"]
... ]
... }
... ]
... }
... }
... }
)
$cmp | [exp1, exp2] | 相等返回0,exp1<exp2返回负数,否则返回正数 |
$strcasecmp | [string1, string2] | 比较string1和string2,区分大小写,只对罗马字符有效 |
$eq/$ne/$gt/$gte/$le/$lte | [exp1, exp2] | 进行比较操作,返回比较的结果(true/false) |
$and | [exp1[, exp2, ... , expN]] | 与运算 |
$or | [exp1[, exp2, ... , expN]] | 或运算 |
$not | expr | 取反 |
$cond | [booleanExpr, trueExpr, falseExpr] | 如果booleanExpr值为true,返回trueExpr,否则返回falseExpr |
$ifNull | [expr, replacementExpr] | 如果expr是null,返回replacementExpr,否则返回expr |
$group
$group:将文档依据特定字段的不同值进行分组。
1.分组操作符
对每一个分组进行计算,得到相应的结果。例:我们最先介绍的示例2.算术操作符
$sum:value, 对于分组中的每一个文档,将value与计算结果相加。$avg:value, 返回每个分组的平均值。
3.极值操作符
// 查找学生考试成绩的最高分和最低分:
> db.scores.aggregate(
... {
... "$group": {
... "_id": "$grade",
... "lowestScore": {"$min": $score},
... "highestScore": {"$max": $score}
... }
... }
)
如果数据是排过序的,那么$first和$last效率比$min和$max高,否则反之。
操作符 | 语法 | 描述 |
$max | expr | 返回分组内的最大值 |
$min | expr | 返回分组内的最小值 |
$first | expr | 返回分组的第一个值,忽略后面的所有值,只有排序之后(数据有序),才有意义 |
$last | expr | 返回分组的最后一个值,忽略前面所有值,只有排序之后(数据有序),才有意义 |
4.数组操作符
$addToSet:expr, 如果当前数组中不包含expr,那么将其添加到数组中。返回结果集每个元素最多只出现一次,而且元素的顺序时不确定的。$push:expr, 不管expr是什么值,都将其添加到数组中,返回包含所有值的数组。
$unwind
$unwind:可以将数组中的每一个值拆分为单独的文档。 // 拆分博客评论为单个文档
> db.bolgs.aggregate(
... {"$unwind": "$comments"}
)
{
"result": [
{
// 博客公共部分 start
"_id": ObjectId("54fd0571e4b055a0030461fb"),
"author": "r",
"post": "hello world!",
// 博客公共部分 end
"comments": {
"author": "bob",
"content": "good post"
}
},
{
"_id": ObjectId("54fd0571e4b055a0030461fb"),
"author": "r",
"post": "hello world!",
"comments": {
"author": "bill",
"content": "nice post"
}
},
...
],
"ok": 1
}
$sort
$sort:可以根据任何字段(或者多个字段)进行排序。
$limit
$limit:接受一个数字n,返回结果集中的前n个文档。
$skip
$skip:接受一个数字n,丢弃结果集中的前n个文档,将剩余文档作为结果返回。MongoDB 聚合管道