使用聚合框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering)、投射(projecting)、 分组(grouping)、 排序(sorting)、 限制(limiting)和跳过(skipping)。
例如,有一个保存着杂志文章的集合,你可能希望找出发表文章最多的那个作者。假设每篇文章被保存为MongoDB中的一个文档,可以按照如下步骤创建管道。
- 将每个文章文档中的作者投射出来。
- 将作者按照名字排序,统计每个名字出现的次数。
- 将作者按照名字出现次数降序排列。
- 将返回结果限制为前5个。
这里面的每一步都对应聚合框架中的一个操作符:
{"$project": {"autho":1}}
这样可以将author从每个文档中投射出来。
这个语法与查询中的字段选择器比较像:可以通过这顶"fieldname":1选择需要投射的字段,或者通过指定"fieldname":0排除不需要的字段。
执行完这个"$project"操作之后,结果集中的每个文档都会以{"_id":id, “autho”:“authorName”}这样的形式表示。这些结果只会在内存中存在,不会被写入磁盘。{"$group":{"_id": "$author", "count":{"$sum":1}}}
这样就会将作者按照名字排序,某个作者的名字每出现一次,就会对这个作者的’ count"加1。
这里首先指定了需要进行分组的字段"author"。这是由" id" : “$author"指定的。可以将这个操作想象为:这个操作执行完后,每个作者只对应一个结果文档,所以"author"就成了文档的唯一标识符(” id")。
第二个字段的意思是为分组内每个文档的"count"字段加1。注意,新加入的文档中并不会有"count" 字段;这"$group"创建的一个新字段。
执行完这一步之后,结果集中的每个文档会是这样的结构: {"_ id" : “authorName” , “count” : articleCount}。{"$sort":{"count":-1}}
这个操作会对结果集中的文档根据count字段进行降序排列{"$limit": 5}
这个操作将最终的返回结果限制为当前结果的前5个文档。
如果管道没有给出预期的结果,就需要进行调试,调试时,可以先只指定;第一个管道操作符。如果这时得到了预期结果,那就再指定第二个管道操作符。以前面的例子来说,首先要试着只使用"$project"操作符进行聚合;如果这个操作符的结果是有效的,就再添加"$group"操作符;如果结果还是有效的,就再添加"$sort";最后再添加"$limit"操作符。这样就可以逐步定位到造成问题的操作符。
管道操作符
每个操作符都会接受一连串的文档,对这些文档做一些类型转换,最后将转换后的文档作为结果传递给下一个操作符(对于最后一个管道操作符,是将结果返回给客户端)。
不同的管道操作符可以按任意顺序组合在一起使用,而且可以被重复任意多次使用。
$match
$match用于对文档集合进行筛选,之后就可以在筛选得到的文档子集上做聚合。例如,如果想对Oregon (俄勒冈州,简写为OR)的用户做统计,就可以使用{$match : {“state” : “OR”}}。 " $match"可以使用所有常规的查询操作符("$gt"、"$lt"、 “$in"等)。有一个例外需要注意:不能在” $match"中使用地理空间操作符。
$project
使用$project可以从子文档中提取字段,可以重命名字段。
默认情况下,如果文档中存在_id
字段,这个字段就会被返回(_id
字段可以被一些管道操作符移除,也可能已经被之前的投射操作给移除了)。
也可以将投射过的字段进行重命名。例如将_id
字段重命名为userId
:
注意,必须明确指定将"_id"排除,否则这个字段的值会被返回两次:一次被标为"userId",一次被标为 " _id"。可以使用这种技术生成字段的多个副本,以便在之后的"$group" 中使用。
在对字段进行重命名时,MongoDB并不会记录字段的历史名称。因此,如果原字段名上有一个索引,聚合框架无法在后续的排序操作中使用这个索引。
== 1. 管道表达式:==
最简单的"$project"表达式是包含和排除字段,以及字段名称("$fieldname")。但是,还有一些更强大的选项。也可以使用表达式(expression) 将多个字面量和变量组合在一个值中使用。
2. 数学表达式:
算术表达式可用于操作数值。指定一组数值,就可以使用这个表达式进行操作了。
-
$add
:[expr1[, expr2, ..., exprN]]
该操作符接受一个或多个表达式作为参数,将这些表达式相加 -
$subtract
:[expr1, expr2]
接受两个表达式作为参数,用第一个表达式减去第二个表达式作为结果 -
$multiply
:[expr1[, expr2, ..., exprN]]
接受一个或者多个表达式,并且将它们相乘 -
$mod
:[expr1, expr2]
接受两个表达式,将第一个表达式除以第二个表达式得到的余数作为结果
复杂嵌套:((_id + 2) * 5 - 10) / 2
3. 日期表达式:
用于提取日期信息,只能对日期类型的字段进行日期操作,不能对数值类型字段做日期操作。
- $yaer
- $month
- $week
- $dayOfMonth
- $dayOfWeek
- $dayOfYear
- $hour
- $minute
- $second
4. 字符串表达式:
-
substr
:[expr, startOffset, numToReturn]
其中第一个参数expr必须是个字符串,这个操作会截取这个字符串的子串(从startOffset字节开始的numToReturn字节)。
一个中文3个字节:
-
$concat
:[expr1[, expr2, ..., exprN]]
将给定的表达式(或字符串)连接在一起作为返回结果。
-
$toLower
:[expr1, [expr2, ..., exprN]]
参数expr必须是一个字符串值,这个操作返回expr的小写形式。
-
$toUpper
:expr
参数expr必须是个字符串值,这个操作返回expr的大写形式:
5. 逻辑表达式:
$cmp
:[expr1, expr2]
比较expr1和expr2。如果expr1等于expr2,返回0;如果expr1<expr2,返回一个负数;如果expr1>expr2,返回一个正数。
$strcasecmp
:[string1, string2]
比较stirng1和string2,区分大小写。只对罗马字符组成的字符串有效。
-
$eq、$ne、$gt、$gte、$lt、$lte
:[expr1, expr2]
对expr1和expr2执行相应的比较操作,返回比较结果(true或false) -
$and
:[expr1[,expr2, ..., exprN]]
只要有任意表达式的值为true,就返回true,否则返回false -
$not
:expr
对expr取反 -
$cond
:[boolanExpr, trueExpr, falseExpr]
如果booleanExpr的值是true,那就返回trueExpr,否则返回falseExpr,如果是老师最欣赏的学生,那么分数就是100
-
$ifNull
:[expr, replacementExpr]
如果expr是null,返回replacementExpr,否则返回expr
6. 示例:
加入有个教授想通过某种比较复杂的计算为学生打分:出勤率占10%,日常测验成绩占30%,期末考试占60%:
db.students.aggregate(
{
"$project": {
"grade": {
"$cond": [
"$teachersPet", 100, {
"$add": [
{"$multiply":[.1, "$attendanceAvg"]},
{"multiply":[.3, "quizzAvg"]},
{"multiply":[.6, "$testAvg"]}
]
}
]
}
}
}
)
$group
$group操作可以将文档依据特定字段的不同值进行分组。如果选定了需要进行分组的字段,就可以将选定的字段传递给$group函数的_id字段。
1. 分组操作符:
分组操作符允许对每个分组进行极端,得到相应的结果。
2. 算术操作符:
-
$sum:value
对于分组中的每一个文档,将value与计算结果相加。
-
$avg:value
返回每个分组的平均值
3. 极值操作符:
-
$max:expr
返回分组内最大值 -
$min:expr
返回分组内的最小值 -
$first:expr
返回分组的第一个值,忽略后面所有值。只有排序之后,明确知道数据顺序时这个操作才有意义 -
$last:expr
与$fisrt相反,返回分组的最后一个值
4. 数组操作符:
$addToSet:expr
如果当前数组中不包含expr,那就将它添加到数组中。在返回结果集中,每个元素最多只出现一次,而且元素的顺序是不确定的。
$push:expr
不管expr是什么值,都将它添加到数组中。返回包含所有值的数组。
5. 分组行为:
有两个操作符不能用前面介绍的流式工作方式对文档进行处理,"$group"是其中之一。大部分操作符的工作方式都是流式的,只要有新文档进入,就可以对新文档进行处理,但是"$g roup"必须要等收到所有的文档之后,才能对文档进行分组,然后才能将各个分组发送给管道中的下一个操作符。
这意味着,在分片的情况下,"$group"会先在每个分片上执行,然后各个分片上的分组结果会被发送到 mongos再进行最后的统一分组,剩余的管道工作也都是在 mongos(而不是在分片)上运行的。
$unwind
拆分(unwind)可以将数组中的每一个值拆分为单独的文档。
如果希望在询中得到特定的子文档,这个操作符就会非常有用:先使用$unwind"得到所有子文档,再使用" match"得到想要的文档。
$sort
可以根据任何字段(或者多个字段)进行排序,与在普通查询中的语法相同。如果要对大量的文档进行排序,强烈建议在管道的第一阶段进行排序,这时的排序操作可以使用索引。否则,排序过程就会比较慢,而且会占用大量内存
$limit
$limit会接受一个数字n,返回结果集中的前n个文档。
$skip
$skip也是接受一个数字n,丢弃结果集中的前n个文档,将剩余文档作为结果返回。在“普通”查询中,如果需要跳过大量的数,那么这个操作符的效率会很低。
在聚合中也是如此,因为它必须要先匹配到所有需要跳过的文档,然后再将这些文档丢弃。
使用管道
应该尽量在管道的开始阶段(执行$project
、$group
或者$unwind
操作之前)就尽可能多的文档和字段过滤掉。管道如果不是直接从原先的结合中使用数据,那就无法在筛选和排序中使用索引。
MongoDB不允许单一的聚合操作占用过多的系统内存:如果 MongoDB发现某个聚合操作占用了20%以上的内存,这个操作就会直接输出错误。
允许将输出结果利用管道放入一个集合中是为了方便以后使用(这样可以将所需的内存减至最小)。如果能够通过"$ match"操作迅速减小结果集的大小,就可以使用管道进行实时聚合。由于管道会不断包含更多的文档,会越来越复杂,所以几乎不可能实时得到管道的操作结果。