Aggregation
- 聚合操作处理数据记录并返回计算结果
- 聚合操作将来自多个文档的值进行分组,对分组的数据进行各种操作并返回单个结果
- mongodb 提供了三种进行聚合操作的方法:聚合管道、map-reduce函数、single purpose 聚合
Aggregation Pipeline
-
mongodb 的聚合框架是基于数据处理管道的概念建模的,文档通过一个多阶段管道处理,转换为聚合结果
-
聚合管道使用本地操作实现了高效的数据聚合操作,是mongodb首选的数据聚合方法
-
聚合管道可以对分片集合进行操作
-
在聚合管道的某些阶段,可以使用索引来提高性能。此外,聚合管道有一个内部优化阶段
-
聚合管道由多个阶段(stage)组成,每个阶段都会对输入文档进行处理转换。管道阶段不需要为每个输入文档生成一个输出文档,因为有些阶段会生成新的文档或过滤掉文档
-
管道阶段可以在管道中出现多次,但 $out、$merge、$geoNear 阶段只能出现一次
db.collection.aggregate
-
计算集合或视图中数据的聚合结果
-
游标
-
聚合返回的游标只支持对已计算的游标进行操作的方法
https://docs.mongodb.com/v4.2/reference/method/db.collection.aggregate/#cursor-behavior
Cursors returned from aggregation only supports cursor methods that operate on evaluated cursors (i.e. cursors whose first batch has been retrieved)
cursor.hasNext()
cursor.next()
cursor.toArray()
cursor.forEach()
cursor.map()
cursor.objsLeftInBatch()
cursor.itcount()
cursor.pretty() -
在 mongo shell 中,如果
aggregate()方法返回的游标没有使用 var 关键字分配给一个变量,那么 mongo shell 将自动迭代游标20次
-
-
会话
- 从 mongodb 3.6 开始,mongodb驱动和mongo shell 将所有操作与一个服务器会话关联,处理未确认的写操作
- 如果一个会话空闲时间超过30分钟,则mongodb服务器会将其标记为过期,并可能在任何时候关闭。mongodb服务器关闭会话的时候会终止任何正在进行的操作,并打开与会话关联的游标
- 在会话内创建的游标,不能在会话外调用
getMore - 在会话外创建的游标,不能在会话内调用
getMore
格式
db.collection.aggregate(pipeline, options)
-
pipeline
- 类型:数组
- 描述:
- 聚合操作列表
- 可以接收单个阶段而非数组,但是非数组类型无法指定 options 参数
-
options
-
类型:Document
-
描述:aggregate() 方法传递给 aggregate 命令的额外选项,仅当pipeline为数组时可用
-
explain
- 类型:布尔
- 描述
- 指定管道处理的返回信息
-
allowDiskUse
-
类型:布尔
-
描述:是否允许使用临时文件
-
true
聚合操作可以将数据写入临时文件,位于
dbPath目录中的子目录_tmp但 $graphLookup、$addToSet、$push 阶段除外
-
-
-
cursor
- 类型:Document
- 描述:指定游标的初始批处理大小
-
maxTimeMS
- 类型:非负整数(单位 毫秒)
- 描述
- 指定处理游标操作的时间限制
- 如果没有指定,则操作不会超时
- 值0,显式指定默认的无限制行为
- mongodb 使用与
db.killOp()方法相同的机制终止超时的操作。mongodb 只在一个指定的中断点终止一个操作
-
bypassDocumentValidation
- 类型:布尔
- 描述
- 仅当指定 $out 或 $merge 阶段时适用
- 使
aggregate()方法绕过文档数据校验,允许管道处理阶段插入不满足数据校验的文档
-
readConcern
-
类型:Document
-
描述
-
指定读取策略
-
格式
readConcern: { level : <value> }- value 取值
- "local"
- "available"
- "majority"
- "linearizable"
- value 取值
-
-
-
collation
- 类型:Document
- 描述
- 指定排序规则
-
hint
- 类型:字符串或文档
- 描述
- 指定聚合操作使用的索引
- 可以通过索引名称或索引规范文档指定
-
comment
- 类型:字符串
- 描述
- 指定字符串来帮助追踪操作
- 可以在 comment 中编码任意信息,以便更容易地通过系统跟踪或识别特定的操作,例如包含进程ID、线程ID、客户端主机名、发出命令的用户等
-
可通过 database profiler、currentOp、logs 追踪
-
writeConcern
- 类型:Document
- 描述:指定 $out、$merge 阶段的写入策略
-
-
Returns
- 游标,指向聚合管道最后阶段生成的文档
- 游标,如果包含 explain 选项,则指向关于聚合操作处理的详细信息的文档
- 空游标,如果管道中包含 $out 操作
示例
文档
db.orders.insertMany([
{ _id: 1, cust_id: "abc1", ord_date: ISODate("2012-11-02T17:04:11.102Z"), status: "A", amount: 50 },
{ _id: 2, cust_id: "xyz1", ord_date: ISODate("2013-10-01T17:04:11.102Z"), status: "A", amount: 100 },
{ _id: 3, cust_id: "xyz1", ord_date: ISODate("2013-10-12T17:04:11.102Z"), status: "D", amount: 25 },
{ _id: 4, cust_id: "xyz1", ord_date: ISODate("2013-10-11T17:04:11.102Z"), status: "D", amount: 125 },
{ _id: 5, cust_id: "abc1", ord_date: ISODate("2013-11-12T17:04:11.102Z"), status: "A", amount: 25 }
])
-
group and sum
> var res = db.orders.aggregate([ { $match: { status: "A" } }, { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }, { $sort: { total: -1 } } ]) > res { "_id" : "xyz1", "total" : 100 } { "_id" : "abc1", "total" : 75 }- 选择状态为A的文档 -->
- 按 cust_id 字段对匹配的文档进行分组,并计算每组中 amount 字段的总和 -->
- 按 total 字段对结果进行降序排列
-
显示聚合管道执行计划的详细信息
db.orders.explain().aggregate([ { $match: { status: "A" } }, { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }, { $sort: { total: -1 } } ]) -
使用外部存储处理大数据集
var results = db.stocks.aggregate( [ { $project : { cusip: 1, date: 1, price: 1, _id: 0 } }, { $sort : { cusip : 1, date: 1 } } ], { allowDiskUse: true } ) -
指定聚合操作使用的索引
db.foodColl.createIndex( { qty: 1, type: 1 } ); db.foodColl.createIndex( { qty: 1, category: 1 } );db.foodColl.aggregate( [ { $sort: { qty: 1 }}, { $match: { category: "cake", qty: 10 } }, { $sort: { type: -1 } } ], { hint: { qty: 1, category: 1 } } )
Pipeline Expressions
管道表达式
https://docs.mongodb.com/v4.2/core/aggregation-pipeline/#pipeline-expressions
for update
https://docs.mongodb.com/v4.2/tutorial/update-documents-with-aggregation-pipeline/
for Sharded Collections
https://docs.mongodb.com/v4.2/core/aggregation-pipeline-sharded-collections/#aggregation-pipeline-sharded-collection
vs Map-Reduce
- 聚合管道是 map-reduce 的一种替代方案,对于复杂的聚合任务是首选解决方案
限制
-
聚合管道对值类型和结果大小有一些限制
-
结果大小限制
-
aggregate 命令
-
聚合命令可以返回游标,也可以将结果存储在集合中,结果集中的每个文档都受BSON文档大小限制,当前为 16MB,如果某个文档大小超过BSON大小限制,则聚合命令将产生错误
- 仅适用于结果集中返回的文档,管道中的文档不受此限制
- MongoDB 3.6删除了聚合命令以单个文档的形式返回结果的选项
-
-
aggregate() 方法
- 返回游标
-
-
内存大小限制
- 聚合管道每个阶段可以利用的RAM大小为 100MB,超过内存限制则会产生错误
- 对于大型数据集,可以在 aggregate() 方法中设置 allowDiskUse 选项,允许聚合管道操作键该数据写入临时文件中
- 以下聚合操作只能使用内存
- $graphLookup
- $addToSet
- push
- 如果聚合管道的某个阶段设置了 allowDiskUse:true 则对其他阶段也生效
优化
聚合命令对单个集合进行操作,逻辑上将整个集合传递给聚合管道,为了优化操作,应尽可能比表面扫描整个集合
-
利用索引
mongodb的查询规划器(query planner)分析聚合管道,以确定是否可以使用索引来提高某些阶段的性能
- $match
- 如果 $match 处于管道开头,则可利用索引筛选文档,减少扫描文档数量
- $sort
- 只要 $sort 前面没有 $project、$unwind、$group 阶段,则可利用索引排序
- $group
- 满足以下条件,则 $group 可以利用索引查找每个分组中的第一个文档
- 在 $sort 阶段之后,且 $sort 阶段对分组字段进行排序
- 在分组字段上有一个索引,与 $sort 排序一致
- 在 $group 阶段中只使用 $first 累加器
- 满足以下条件,则 $group 可以利用索引查找每个分组中的第一个文档
- $geoNear
- $match
-
预先过滤
-
当聚合操作只需针对集合中数据的一个子集,则在管道开头使用 $match、$limit、$skip 等阶段,限制输入文档的数量
-
在管道开头使用 $match 和 $sort 阶段,逻辑上相当于一个带有排序的查询,并且可以使用索引,如果可能,尽量在管道开头使用 $match 阶段
-
Stages
$group
-
按指定字段对集合中的文档进行分组,每组输出一个文档,输出文档的
_id字段包含唯一值 -
类似 sql 中的
GROUP BY -
输出文档中还可以添加自定义字段,用于显示累加器表达式值
-
如果文档不包含分组字段,则忽略该文档
区别于包含分组字段,但是值为null
格式
db.collection.aggregate([
{
$group:
{
_id: <expression>,
<field>: { <accumulator> : <expression> },
...
}
}
])
-
_id- 类型:表达式(字符串
"$<分组字段>"或 操作符表达式) - 描述:指定分组字段
- 如果指定为 null 或 其他常量值(数字),则将整个集合视为一组进行计算
- 类型:表达式(字符串
-
field- 类型:字符串
- 描述:自定义字段,用于显示累加器表达式的值
-
<accumulator>-
描述:累加器(聚合)操作符
-
常用聚合操作符
操作符 描述 $sum 利用 $group 分组后,对同组内的文档,对指定字段的数值进行求和 $avg 利用 $group 分组后,对同组内的文档,对指定字段的数值求平均值 $first 利用 $group 分组后,对同组内的文档,显示指定字段的第一个值 $last 利用 $group 分组后,对同组内的文档,显示指定字段的最后一个值 $max 利用 $group 分组后,对同组内的文档,显示指定字段的最大值 $min 利用 $group 分组后,对同组内的文档,显示指定字段的最小值 $push 利用 $group 分组后,对同组内的文档,以数组的方式显示指定字段 $addToSet 利用 $group 分组后,对同组内的文档,以数组的方式显示字段不重复的值
-
-
<expression>- 类型:表达式(字符串
"$<计算的字段>"或 操作符表达式) - 描述:累加器操作符计算的字段
- 类型:表达式(字符串
示例
文档
db.sales.insertMany([
{ "_id" : 1, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("2"), "date" : ISODate("2014-03-01T08:00:00Z") },
{ "_id" : 2, "item" : "jkl", "price" : NumberDecimal("20"), "quantity" : NumberInt("1"), "date" : ISODate("2014-03-01T09:00:00Z") },
{ "_id" : 3, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" : NumberInt( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },
{ "_id" : 4, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" : NumberInt("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },
{ "_id" : 5, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },
{ "_id" : 6, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },
{ "_id" : 7, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("10") , "date" : ISODate("2015-09-10T08:43:00Z") },
{ "_id" : 8, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") },
])
-
计算集合中文档的数量
db.sales.aggregate( [ { $group: { _id: null, count: { $sum: 1 } } } ] )-
返回
{ "_id" : null, "count" : 8 }
-
-
_id设为 null 或 常量db.sales.aggregate( [ { $group: { _id: 404, count: { $sum: 1 } } } ] )-
返回
{ "_id" : 404, "count" : 8 }
-
-
检索某个字段的不同值
db.sales.aggregate( [ { $group : { _id : "$item" } } ] ) -
having
db.sales.aggregate( [ // First Stage { $group : { _id : "$item", totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } } } }, // Second Stage { $match: { "totalSaleAmount": { $gte: 100 } } } ] )- 按 item 字段进行分组
- 计算每组的总销售额
- 返回总销售额大于等于100的文档
-
多聚合操作符
db.sales.aggregate([ // First Stage { $match : { "date": { $gte: new ISODate("2014-01-01"), $lt: new ISODate("2015-01-01") } } }, // Second Stage { $group : { // 多个 key-value 键值对 _id : { $dateToString: { format: "%Y-%m-%d", date: "$date" } }, totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } }, averageQuantity: { $avg: "$quantity" }, count: { $sum: 1 } } }, // Third Stage { $sort : { totalSaleAmount: -1 } } ])- 选择 日期范围是 2014 年的文档
- 按照日期进行分组,每组计算销售总额、平均值以及文档数量
- 按照销售总额倒序排列
$project
-
显示并传递指定字段,包括文档中的现有字段或新计算的字段(新增)
-
对于嵌入式文档中的字段,可以通过 点表示法 或 嵌套字段表示法
"contact.address.country": <1 or 0 or expression> contact: { address: { country: <1 or 0 or expression> } } -
如果 $project 指定一个空文档则报错
格式
{ $project: { <specification(s)> } }
-
specifications
-
_id : <0 or false>- 排除
_id字段
- 排除
-
<field> : <0 or 1>-
1:包含指定字段
_id字段默认显示和传递,其他字段默认不显示并排除- 如果指定的字段不存在, $project 将忽略该字段(不会创建)
- 不能同时指定包含和排除字段
-
0:排除指定字段
-
如果有条件的排除,需使用 REMOVE 变量
-
如果排除
_id以外的所有字段,则不能使用任何其他的规范表单if you exclude fields, you cannot also specify the inclusion of fields, reset the value of existing fields, or add new fields.
-
除
_id字段外,其他字段默认不显示,但如果显式排除了某个字段,则其他所有字段将显示并传递
-
-
-
<field> : <expression>- 增加一个新字段或重置已存在的字段
-
-
常用操作符
- 字符串操作符
- 算数运算操作符
- 时间日期操作符
示例
-
排除嵌入式文档中的指定字段
文档
db.books.insert({ "_id" : 1, title: "abc123", isbn: "0001122223334", author: { last: "zzz", first: "aaa" }, copies: 5, lastModified: "2016-07-28" })排除
db.books.aggregate( [ { $project : { "author.first" : 0, "lastModified" : 0 } } ] ) db.bookmarks.aggregate( [ { $project: { "author": { "first": 0}, "lastModified" : 0 } } ] ) -
包含嵌入式文档中的指定字段
文档
db.bookmarks.insertMany([ { _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.bookmarks.aggregate( [ { $project: { "stop.title": 1 } } ] ) db.bookmarks.aggregate( [ { $project: { stop: { title: 1 } } } ] )-
结果
{ "_id" : 1, "stop" : { "title" : "book1" } } { "_id" : 2, "stop" : [ { "title" : "book2" }, { "title" : "book3" } ] } -
多个值将自动以数组的形式返回
-
-
新增字段
db.books.aggregate( [ { $project: { title: 1, isbn: { prefix: { $substr: [ "$isbn", 0, 3 ] }, group: { $substr: [ "$isbn", 3, 2 ] } }, lastName: "$author.last", copiesSold: "$copies" } } ] )-
结果
{ "_id" : 1, "title" : "abc123", "isbn" : { "prefix" : "000", "group" : "11" }, "lastName" : "zzz", "copiesSold" : 5 }
-
-
新增数组
文档
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "x" : 1, "y" : 1 }新增
db.collection.aggregate( [ { $project: { myArray: [ "$x", "$y", "$someField" ] } } ] )-
结果
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "myArray" : [ 1, 1, null ] } -
如果数组中引用了不存在的字段,则用 null 代替
-
$sort
- 对所有的输入文档进行排序,返回排序后的文档
格式
{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }
- <sort order>
- 1 :升序排序
- -1:降序排列
- 多字段排序,从左到右计算排序顺序,先按 field1 排序,具有相同 field1 值的文档再按 field2 排序
优化
-
当 $sort 在 $limit 之前且中间没有改变文档数量的操作时,优化器可以将 $limit 合并到 $sort 中,即 $sort 操作在进行过程中只维护顶部的 n 个结果(n 为 $limit 限定的值),mongodb 只在内存中存储 n 个文档
-
$sort 阶段只能占用 100 MB 的内存,默认超过则报错。为了处理大型数据集,设置 allowDiskUse 为 true,允许 $sort 操作将数据写入临时文件
-
如果管道前面没有 $project、$unwind、$group 阶段,则$sort 可利用索引进行排序
$skip
- 跳过指定数量的文档,返回剩余的文档
- 对文档内容无影响
格式
{ $skip: <positive integer> }
- 值为一个正整数
$limit
- 限制返回的文档数量,返回指定数量的文档(按输入顺序)
- 对文档内容无影响
格式
{ $limit: <positive integer> }
- 值为一个正整数
$match
- 筛选文档,返回符合条件的文档
- 尽可能在管道开头放置 $match 阶段,限制输入文档数量
- 管道开头的 $match 可以像
find()、findOne()那样利用索引
格式
{ $match: { <query> } }
- query:查询条件表达式
限制
-
$match 查询语法与读取操作查询语法相同;例如:$match不接受原始聚合表达式。要在$match中包含聚合表达式,请使用$expr查询表达式
{ $match: { $expr: { <aggregation expression> } } } -
要在 $match 中使用 $text,则必须作为管道的第一阶段
示例
-
相等匹配
db.articles.aggregate( [ { $match : { author : "dave" } } ] ); -
条件查询
db.articles.aggregate( [ { $match: { $or: [ { score: { $gt: 70, $lt: 90 } }, { views: { $gte: 1000 } } ] } }, { $group: { _id: null, count: { $sum: 1 } } } ] );-
$match 选择分数大于70小于90的文档,或者视图大于等于1000的文档,这些文档通过管道传输给 $group 阶段
-
$group 统计文档数量
-
结果
{ "_id" : null, "count" : 5 }
-
$lookup
-
对同一数据库中未分片的集合进行左外连接,用于查找当前集合中与另一集合条件匹配的文档
-
相当于关系数据库中的左外联查询
左外联:返回包括左表中的所有记录和右表中符合查询条件的记录
右外联:返回包括右表中的所有记录和左表中符合查询条件的记录
https://blog.csdn.net/plg17/article/details/78758593
相等查询
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
-
from
- 类型:字符串
- 指定要与输入集合进行左外联的同一数据库中的其他集合
-
localField
- 类型:字符串
- 管道输入集合中需关联的键
- $lookup 将 localField 和 foreignField 进行相等匹配
- 如果输入集合中不包含 localField 指定的字段,则视 localField 值为 null 与 foreignField 进行相等匹配
- 如果localField 字段的类型为数组,则数组元素依次匹配 foreignField
-
foreignField
- 类型:字符串
- from 集合中需关联的键
- $lookup 将 foreignField 和 localField 进行相等匹配
- 如果 from 集合中不包含 foreignField 指定的字段,则视 foreignField 值为 null 与 localField 进行相等匹配
-
as
- 类型:字符串
- 指定要添加到输入文档中的新数组字段的名称
- 该字段包含 from 集合中符合条件的文档
- 如果该字段名称已经存在,则会覆盖现有字段
示例
-
localField为单一值
文档
db.orders.insert([ { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 }, { "_id" : 3 } ]) db.inventory.insert([ { "_id" : 1, "sku" : "almonds", description: "product 1", "instock" : 120 }, { "_id" : 2, "sku" : "bread", description: "product 2", "instock" : 80 }, { "_id" : 3, "sku" : "cashews", description: "product 3", "instock" : 60 }, { "_id" : 4, "sku" : "pecans", description: "product 4", "instock" : 70 }, { "_id" : 5, "sku": null, description: "Incomplete" }, { "_id" : 6 } ])通过 orders 集合中的 item 字段和 inventory 集合汇总的 sku 字段,将两个集合连接起来
db.orders.aggregate([ { $lookup: { from: "inventory", localField: "item", foreignField: "sku", as: "inventory_docs" } } ])-
查询 orders 集合中 item 字段的值与 inventory 集合中 sku 字段的值 相等的 文档
-
输出文档中的 inventory_docs 字段包含 inventory 集合中符合条件的文档
-
结果
{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2, "inventory_docs" : [ { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 } ] } { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1, "inventory_docs" : [ { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 } ] } { "_id" : 3, "inventory_docs" : [ { "_id" : 5, "sku" : null, "description" : "Incomplete" }, { "_id" : 6 } ] }orders 集合中的
{ "_id" : 3 }文档不包含 item 字段,则$lookup将其视为 null 匹配 inventory 集合中的{ "_id" : 5, "sku" : null, "description" : "Incomplete" }和{ "_id" : 6 }两个文档
-
-
localFIeld 为数组
文档
db.classes.insert( [ { _id: 1, title: "Reading is ...", enrollmentlist: [ "giraffe2", "pandabear", "artie" ], days: ["M", "W", "F"] }, { _id: 2, title: "But Writing ...", enrollmentlist: [ "giraffe1", "artie" ], days: ["T", "F"] } ]) db.members.insert( [ { _id: 1, name: "artie", joined: new Date("2016-05-01"), status: "A" }, { _id: 2, name: "giraffe", joined: new Date("2017-05-01"), status: "D" }, { _id: 3, name: "giraffe1", joined: new Date("2017-10-01"), status: "A" }, { _id: 4, name: "panda", joined: new Date("2018-10-11"), status: "A" }, { _id: 5, name: "pandabear", joined: new Date("2018-12-01"), status: "A" }, { _id: 6, name: "giraffe2", joined: new Date("2018-12-01"), status: "D" } ])通过 classes 集合中的 enrollmentlist 字段 和 members 集合中的 name 字段,将两个集合连接起来
db.classes.aggregate([ { $lookup: { from: "members", localField: "enrollmentlist", foreignField: "name", as: "enrollee_info" } } ])-
查询 classes 集合中 enrollmentlist 数组中的元素跟 members 集合中 name 字段值相等的文档
-
输出文档中的 enrollee_info 字段包含 members 集合中符合条件的文档
-
结果
{ "_id" : 1, "title" : "Reading is ...", "enrollmentlist" : [ "giraffe2", "pandabear", "artie" ], "days" : [ "M", "W", "F" ], "enrollee_info" : [ { "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" }, { "_id" : 5, "name" : "pandabear", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "A" }, { "_id" : 6, "name" : "giraffe2", "joined" : ISODate("2018-12-01T00:00:00Z"), "status" : "D" } ] } { "_id" : 2, "title" : "But Writing ...", "enrollmentlist" : [ "giraffe1", "artie" ], "days" : [ "T", "F" ], "enrollee_info" : [ { "_id" : 1, "name" : "artie", "joined" : ISODate("2016-05-01T00:00:00Z"), "status" : "A" }, { "_id" : 3, "name" : "giraffe1", "joined" : ISODate("2017-10-01T00:00:00Z"), "status" : "A" } ] }
-
不相关子查询与多条件查询
{
$lookup:
{
from: <collection to join>,
let: { <var_1>: <expression>, …, <var_n>: <expression> },
pipeline: [ <pipeline to execute on the collection to join> ],
as: <output array field>
}
}
-
from
- 类型:字符串
- 指定要与输入集合进行左外联的同一数据库中的其他集合
-
left
- 类型:文档
let : { <引用变量名> : "$<输入文档中的字段>" } - 使用变量表达式访问输入文档中的字段
- 定义要在 pipeline 管道阶段中使用的变量,用以访问输入 $lookup 阶段的文档中的字段
- 类型:文档
-
pipeline
-
类型:数组
-
指定要在 from 集合上运行的管道,用以筛选符合条件的文档
如要返回所有的文档,则指定一个不含任何阶段的空管道
[ ] -
pipeline 管道中不能包含 $out 和 $merge 阶段
-
pipeline 管道阶段 能 直接访问 from 集合中的文档字段,通过
"$<from集合中的文档字段>" -
pipeline 管道阶段 不能 直接访问输入文档中的字段,必须首先在 let 子句中定义中间变量,然后才能在 pipeline 管道的各个阶段中引用
- 在 pipeline 管道阶段中通过
$$<variable>的形式引用变量 - $match 阶段需要通过 $expr 操作符来使用聚合表达式,访问 let 子句中定义的变量
- 在 pipeline 管道阶段中通过
-
-
as
- 类型:字符串
- 指定要添加到输入文档中的新数组字段的名称
- 该字段包含 from 集合中符合条件的文档
- 如果该字段名称已经存在,则会覆盖现有字段
示例
-
多条件查询
文档
db.orders.insert([ { "_id" : 1, "item" : "almonds", "price" : 12, "ordered" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "ordered" : 1 }, { "_id" : 3, "item" : "cookies", "price" : 10, "ordered" : 60 } ]) db.warehouses.insert([ { "_id" : 1, "stock_item" : "almonds", warehouse: "A", "instock" : 120 }, { "_id" : 2, "stock_item" : "pecans", warehouse: "A", "instock" : 80 }, { "_id" : 3, "stock_item" : "almonds", warehouse: "B", "instock" : 60 }, { "_id" : 4, "stock_item" : "cookies", warehouse: "B", "instock" : 40 }, { "_id" : 5, "stock_item" : "cookies", warehouse: "A", "instock" : 80 } ])通过 item字段 以及 条件(库存数量是否满足订单数量),将orders集合和warehouses集合连接起来
db.orders.aggregate([ { $lookup: { from: "warehouses", let: { order_item: "$item", order_qty: "$ordered" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: [ "$stock_item", "$$order_item" ] }, { $gte: [ "$instock", "$$order_qty" ] } ] } } }, { $project: { stock_item: 0, _id: 0 } } ], as: "stockdata" } } ])-
根据 orders 集合中的 name 字段,查询 warehouses 集合中库存数量满足订单数量的文档
-
输出文档中的 stockdata 字段包含符合条件的 warehouses 集合中的文档
-
结果
{ "_id" : 1, "item" : "almonds", "price" : 12, "ordered" : 2, "stockdata" : [ { "warehouse" : "A", "instock" : 120 }, { "warehouse" : "B", "instock" : 60 } ] } { "_id" : 2, "item" : "pecans", "price" : 20, "ordered" : 1, "stockdata" : [ { "warehouse" : "A", "instock" : 80 } ] } { "_id" : 3, "item" : "cookies", "price" : 10, "ordered" : 60, "stockdata" : [ { "warehouse" : "A", "instock" : 80 } ] }
-
-
不相关子查询
子查询或内部查询
- 嵌套在其它查询中的查询
主查询或外部查询
- 包含子查询的查询
不相关子查询
- 内部查询的执行独立于外部查询,内部查询只执行一次,然后将结果作为外部查询的条件
相关子查询
- 内部查询的执行依赖于外部查询的数据,外部查询每执行一次,内部查询也会执行一次。
- 每次都是外部查询先执行,将当前查询数据传递给内部查询,然后执行内部查询,根据内部查询的执行结果判断当前数据是否满足外部查询的where条件,若满足则当前数据是符合要求的记录
- 外部查询依次扫描每条记录,重复执行上述过程
https://blog.csdn.net/qiushisoftware/article/details/80874463
$count
- 返回输入文档的数量,并传递给下一阶段
- 不改变文档内容
格式
{ $count: <string> }
- string
- 输出字段的名称,值为输入文档的数量
- 非空字符串,不能以
$开头,不能包含点.字符
示例
-
$count 行为等价于 $group + $project
db.collection.aggregate( [ { $group: { _id: null, myCount: { $sum: 1 } } }, { $project: { _id: 0 } } ] ) db.collection.aggregate([ { $count:"myCount" } ])
$unwind
- 解析输入文档中的数组字段,将每个元素作为字段值输出单独的文档
格式
{ $unwind: <field path> }
{
$unwind:
{
path: <field path>,
includeArrayIndex: <string>,
preserveNullAndEmptyArrays: <boolean>
}
}
-
path
- 类型:字符串
- 描述:指定要解析的数组字段
-
includeArrayIndex
- 类型:字符串
- 描述:新字段的名称,用于保存元素在原数组中的索引,不能以
$开头
-
preserveNullAndEmptyArrays
preserve 保留
-
类型:布尔
-
描述:如果文档不包含 path 指定的字段,或字段值为null,或字段值为空数组
[ ]-
true
$unwind 原样输出该文档
-
false【默认】
$unwind 不输出该文档
-
-
示例
-
preserveNullAndEmptyArrays 默认false
db.inventory2.insertMany([ { "_id" : 1, "item" : "ABC", price: NumberDecimal("80"), "sizes": [ "S", "M", "L"] }, { "_id" : 2, "item" : "EFG", price: NumberDecimal("120"), "sizes" : [ ] }, { "_id" : 3, "item" : "IJK", price: NumberDecimal("160"), "sizes": "M" }, { "_id" : 4, "item" : "LMN" , price: NumberDecimal("10") }, { "_id" : 5, "item" : "XYZ", price: NumberDecimal("5.75"), "sizes" : null } ])展开
db.inventory2.aggregate( [ { $unwind: "$sizes" } ] ) db.inventory2.aggregate( [ { $unwind: { path: "$sizes" } } ] )两种语法效果一样
结果
{ "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "S" } { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "M" } { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "L" } { "_id" : 3, "item" : "IJK", "price" : NumberDecimal("160"), "sizes" : "M" }- size 字段丢失、为null、为空数组的文档,默认不输出
-
记录索引,输出文档
db.inventory2.aggregate( [ { $unwind: { path: "$sizes", includeArrayIndex: "arrayIndex", preserveNullAndEmptyArrays: true } }])结果
{ "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "S" } { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "M" } { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "L" } { "_id" : 2, "item" : "EFG", "price" : NumberDecimal("120") } { "_id" : 3, "item" : "IJK", "price" : NumberDecimal("160"), "sizes" : "M" } { "_id" : 4, "item" : "LMN", "price" : NumberDecimal("10") } { "_id" : 5, "item" : "XYZ", "price" : NumberDecimal("5.75"), "sizes" : null } -
解析嵌套数组
- 先解析外层数组,再解析内层数组,path路径需要通过点表示法引用内层数组
{ _id: "1", "items" : [ { "name" : "pens", "tags" : [ "writing", "office", "school", "stationary" ], "price" : NumberDecimal("12.00"), "quantity" : NumberInt("5") }, { "name" : "envelopes", "tags" : [ "stationary", "office" ], "price" : NumberDecimal("1.95"), "quantity" : NumberInt("8") } ] }db.sales.aggregate([ // First Stage { $unwind: "$items" }, // Second Stage { $unwind: "$items.tags" } ])
?
$out
-
将聚合操作的结果写入指定的集合
-
$out 必须是管道的最后一个阶段
-
不能写入固定集合中
-
如果指定的集合不存在,则在完成聚合操作后,会创建该集合
在聚合操作完成之前,该集合是不可见的。如果聚合操作失败,则不会创建
-
如果指定的集合已经存在,则在完成聚合操作后,会用聚合操作结果覆盖原有数据
-
$out 操作符不会改变原集合上建立的索引,如果聚合操作的结果文档违反任一唯一索引(包括原集合
_id字段上建立的索引),则写入失败
格式
{ $out: "<output-collection>" }
- output-collection
- 输出集合的名称
示例
db.books.insert([
{ "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 },
{ "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 },
{ "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 },
{ "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 },
{ "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 }
])
聚合
db.books.aggregate( [
{ $group : { _id : "$author", books: { $push: "$title" } } },
{ $out : "authors" }
] )
结果在当前数据库中新增 authors 集合
{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] }
{ "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] }
$merge
$addFields
- 向输入文档中添加新字段,返回包含所有原有字段和新添加字段的文档
- 相当于 $project 显式输出所有原有字段并添加新字段
- mongodb 4.2 新增 $set 阶段是 $addFields 的别名
格式
{ $addFields: { <newField>: <expression>, ... } }
- newField
- 新增字段名
- 如果已存在该字段,则覆盖原有字段
Operators
## 仅 Group ##
$push
- 利用 $group 分组后,对同组内的文档,以数组的形式,返回指定字段的值
- 只能应用于 $group 阶段
格式
{ $push: <expression> }
$addToSet
- 利用 $group 分组后,对同组内的文档,以数组的形式,返回指定字段 不重复的值
- 只能应用于 $group 阶段
格式
{ $addToSet: <expression> }
- 如果 expression 解析为某个字段的值
"$<field>",则以数组的形式返回该字段不重复的值
$sum
-
利用 $group 分组后,对同组内的文档,对指定字段的数值进行求和
{$sum:1}
-
表示计算文档数量总和,管道中的每个文档代表数值1
-
在 $group 阶段,分组过程中,计算每组文档的数量
-
-
3.2 版本及之前,仅用于 $group 阶段
格式
$group 阶段
{ $sum: <expression> }
其他阶段
{ $sum: <expression> }
{ $sum: [ <expression1>, <expression2> ... ] }
-
非数字或不存在字段
Example Field Values Results { $sum : <field> }NumericSum of Values{ $sum : <field> }Numeric and Non-NumericSum of Numeric Values{ $sum : <field> }Non-Numeric or Non-Existent0- 在包含数字和非数字的字段上使用,则忽略非数字字段,返回数字值之和
- 如果字段不存在,则返回0
- 如果所有操作数都是非数字,则返回0
-
数组字段
- $group 阶段,如果表达式解析为数组,则视为非数字值
- 其他阶段
- 单个表达式作为操作数,解析为数组时,$sum 遍历数组,对数字值求和并返回
- 多个表达式作为操作数,如果某个表达式解析为数组,则 $sum 不会遍历数组,视为非数字值
$avg
-
利用 $group 分组后,对同组内的文档,对指定字段的数值求平均值
-
3.2 版本及之前,仅用于 $group 阶段
格式
$group 阶段
{ $avg: <expression> }
其他阶段
{ $avg: <expression> }
{ $avg: [ <expression1>, <expression2> ... ] }
- 忽略非数字值和丢失的值
- 如果操作数都是非数字值,则返回 null
- 数组字段
- $group 阶段,如果表达式解析为数组,则视为非数字值
- 其他阶段
- 单个表达式作为操作数,解析为数组时,$sum 遍历数组,对数字值求和并返回
- 多个表达式作为操作数,如果某个表达式解析为数组,则 $sum 不会遍历数组,视为非数字值
$first
-
利用 $group 分组后,对同组内的文档,返回 指定字段 的第一个值
-
一般在文档排序后使用才有意义
-
当在 $group 阶段中使用$first时,$group 阶段应该在 $sort 阶段之后,以使输入文档按照已定义的顺序
-
尽管 $sort 阶段将有序的文档作为输入传递到 $group 阶段,但 $group 不能保证在其自己的输出中维护这种排序顺序。
-
-
只能应用于 $group 阶段
格式
{ $first: <expression> }
$last
-
利用 $group 分组后,对同组内的文档,返回 指定字段 的最后一个值
-
一般在文档排序后使用才有意义
当在 $group 阶段中使用$first时,$group 阶段应该在 $sort 阶段之后,以使输入文档按照已定义的顺序
-
只能应用于 $group 阶段
格式
{ $last: <expression> }
$max
- 利用 $group 分组后,对同组内的文档,返回 指定字段 的最大值
- 3.2 版本及之前,仅用于 $group 阶段
格式
$group 阶段
{ $max: <expression> }
其他阶段
{ $max: <expression> }
{ $max: [ <expression1>, <expression2> ... ] }
- 忽略 null 值和丢失字段,仅考虑字段的非空值和非缺失值
- 如果所有字段的值都为 null 或缺失,则返回 null
- 数组字段
- 在$group阶段,如果表达式解析为一个数组,则$max不会遍历该数组并将该数组作为一个整体进行比较。
- 其他阶段
- 单个表达式作为操作数,解析为数组时,$max 遍历数组,对数字值进行操作并返回元素最大值
- 多个表达式作为操作数,如果某个表达式解析为数组,则 $max 不会遍历数组,视为非数字值
$min
- 利用 $group 分组后,对同组内的文档,返回 指定字段 的最小值
- 3.2 版本及之前,仅用于 $group 阶段
格式
$group 阶段
{ $min: <expression> }
其他阶段
{ $min: <expression> }
{ $min: [ <expression1>, <expression2> ... ] }
- 忽略 null 值和丢失字段,仅考虑字段的非空值和非缺失值
- 如果所有字段的值都为 null 或缺失,则返回 null
- 数组字段
- 在$group阶段,如果表达式解析为一个数组,则 $min 不会遍历该数组并将该数组作为一个整体进行比较。
- 其他阶段
- 单个表达式作为操作数,解析为数组时,$min 遍历数组,对数字值进行操作并返回元素最小值
- 多个表达式作为操作数,如果某个表达式解析为数组,则 $min 不会遍历数组,视为非数字值
## 条件判断 ##
$switch
- 对指定字段进行一系列的条件判断,当条件为true时,执行对应的表达式并跳出控制流
- 各个case语句不需要互相排斥,$switch 执行第一个计算结果为true的分支
- 以下情况,$switch报错
- branches 字段缺失或不是一个数组
- 条件语句不包含 case 字段
- 条件语句不包含 then 字段
- 条件语句包含 case、then 之外的字段
- 未指定 default 表达式且没有条件语句计算结果为true
格式
$switch: {
branches: [
{ case: <expression>, then: <expression> },
{ case: <expression>, then: <expression> },
...
],
default: <expression>
}
-
branches
-
类型:文档数组
-
描述:
-
控制分支,每个分支都必须具有 case 和 then 字段
-
case
值为可以解析为布尔值的任何有效的 表达式,如果不是则强制转换为布尔值
-
then
值为任何有效的表达式
-
-
必须至少包含一个条件分支
-
-
-
default【可选】
- 没有条件分支结果为true时,执行默认表达式
- 如果没有指定,且没有条件分支为true,则 $switch 报错
示例
文档
db.grades.insert([
{ "_id" : 1, "name" : "Susan Wilkes", "scores" : [ 87, 86, 78 ] },
{ "_id" : 2, "name" : "Bob Hanna", "scores" : [ 71, 64, 81 ] },
{ "_id" : 3, "name" : "James Torrelio", "scores" : [ 91, 84, 97 ] }
])
聚合
db.grades.aggregate( [
{
$project:
{
"name" : 1,
"summary" :
{
$switch:
{
branches: [
{
case: { $gte : [ { $avg : "$scores" }, 90 ] },
then: "Doing great!"
},
{
case: { $and : [ { $gte : [ { $avg : "$scores" }, 80 ] },
{ $lt : [ { $avg : "$scores" }, 90 ] } ] },
then: "Doing pretty well."
},
{
case: { $lt : [ { $avg : "$scores" }, 80 ] },
then: "Needs improvement."
}
],
default: "No scores found."
}
}
}
}
] )
-
结果
{ "_id" : 1, "name" : "Susan Wilkes", "summary" : "Doing pretty well." } { "_id" : 2, "name" : "Bob Hanna", "summary" : "Needs improvement." } { "_id" : 3, "name" : "James Torrelio", "summary" : "Doing great!" }
## 数组 ##
$size
- 计算并返回数组的长度
格式
{ $size: <expression> }
-
expression
可以解析为数组的任何有效的表达式
## 字符串 ##
$substr
3.4 版本中被废弃,现在是 substrBytes 的别名
- 返回字符串中的子字符串,从指定索引位置开始,包含指定数目的字符。索引从0开始
格式
{ $substr: [ <string>, <start>, <length> ] }
- 值为 数组
- string 一般是字段路径
$<字段名> - 如果 start 是负数,则返回空字符串
- 如果 length 是负数,则返回从指定索引位置开始到末尾的子字符串
$indexOfBytes
-
查询并返回子字符串在字段中第一次出现位置的索引,如果没有找到则返回 -1
UTF-8 字节索引
格式
{ $indexOfBytes: [ <string expression>, <substring expression>, <start>, <end> ] }
- string expression
- 可以解析为字符串的任何有效的表达式
- 如果表达式解析为 null 或引用丢失的字段,则 $indexOfBytes 返回 null
- 非以上情况返回一个错误
- substring expression
- 可以解析为字符串的任何有效的表达式
- start
- 指定搜索的起始索引位置
- 非负整数,从0开始
- end
- 指定搜索的结束索引位置
- 非负整数
- 指定 end,则必须同时指定 start
示例
db.inventory.insert([
{ "_id" : 1, "item" : "foo" },
{ "_id" : 2, "item" : "fóofoo" },
{ "_id" : 3, "item" : "the foo bar" },
{ "_id" : 4, "item" : "hello world fóo" },
{ "_id" : 5, "item" : null },
{ "_id" : 6, "amount" : 3 }
])
聚合
db.inventory.aggregate(
[
{
$project:
{
byteLocation: { $indexOfBytes: [ "$item", "foo" ] },
}
}
]
)
-
结果
{ "_id" : 1, "byteLocation" : "0" } { "_id" : 2, "byteLocation" : "4" } { "_id" : 3, "byteLocation" : "4" } { "_id" : 4, "byteLocation" : "-1" } { "_id" : 5, "byteLocation" : null } { "_id" : 6, "byteLocation" : null }-
注意 fóofoo 索引是4
éis encoded using two bytes. -
每个文档都有一个返回结果
-
$strLenBytes
- 返回指定字符串 UTF-8 编码的字节数(字符串长度)
- 英文字母,使用 1个字节编码(空串 0字节)
- 中文、日文、韩文,使用 3字节编码
- 带有变音符号的字符以及英语字母表之外的拉丁字符,使用2个字节编码
格式
{ $strLenBytes: <string expression> }
- string expression
- 可以解析为字符串的任何有效的表达式
- 如果表达式解析为 null 或引用丢失的字段,则 $strLenBytes 返回一个错误
示例
文档
{ "_id" : 1, "name" : "apple" }
{ "_id" : 2, "name" : "banana" }
{ "_id" : 3, "name" : "éclair" }
{ "_id" : 4, "name" : "hamburger" }
{ "_id" : 5, "name" : "jalape?o" }
{ "_id" : 6, "name" : "pizza" }
{ "_id" : 7, "name" : "tacos" }
{ "_id" : 8, "name" : "寿司" }
聚合
db.food.aggregate(
[
{
$project: {
"name": 1,
"length": { $strLenBytes: "$name" }
}
}
]
)
-
结果
{ "_id" : 1, "name" : "apple", "length" : 5 } { "_id" : 2, "name" : "banana", "length" : 6 } { "_id" : 3, "name" : "éclair", "length" : 7 } { "_id" : 4, "name" : "hamburger", "length" : 9 } { "_id" : 5, "name" : "jalape?o", "length" : 9 } { "_id" : 6, "name" : "pizza", "length" : 5 } { "_id" : 7, "name" : "tacos", "length" : 5 } { "_id" : 8, "name" : "寿司", "length" : 6 }
$strcasecmp
-
对两个字符串执行不区分大小写的比较
-
仅适用于 ASCII