MongoDB—聚合
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
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
-
对同一数据库中未分片的集合进行左外连接,用于查找当前集合中与另一集合条件匹配的文档
-
相当于关系数据库中的左外联查询
左外联:返回包括左表中的所有记录和右表中符合查询条件的记录
右外联:返回包括右表中的所有记录和左表中符合查询条件的记录
相等查询
{
$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> }
Numeric
Sum of Values
{ $sum : <field> }
Numeric and Non-Numeric
Sum of Numeric Values
{ $sum : <field> }
Non-Numeric or Non-Existent
0
- 在包含数字和非数字的字段上使用,则忽略非数字字段,返回数字值之和
- 如果字段不存在,则返回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 字符编码
格式
{ $strcasecmp: [ <expression1>, <expression2> ] }
- expression
- 可以解析为字符串的任何有效的表达式
- 返回结果
- 1 :第一个字符串 大于 第二个字符串
- 0 :两个字符串相等
- -1:第一个字符串 小于 第二个字符串
$toLower
- 将字符串转换为小写字母并返回
- 仅适用于 ASCII 字符编码
格式
{ $toLower: <expression> }
- expression
- 可以解析为字符串的任何有效的表达式
- 如果表达式解析为null,则返回空串
示例
db.inventory.aggregate(
[
{
$project:
{
item: { $toLower: "$item" },
description: { $toLower: "$description" }
}
}
]
)
$toUpper
-
将字符串转换为大写字母并返回
-
仅适用于 ASCII 字符编码
格式
{ $toUpper: <expression> }
- expression
- 可以解析为字符串的任何有效的表达式
- 如果表达式解析为null,则返回空串
$concat
- 合并字符串并返回
格式
{ $concat: [ <expression1>, <expression2>, ... ] }
- expression
- 可以解析为字符串的任何有效的表达式
- 如果表达式解析为 null 或引用丢失的字段,则返回 null
$split
- 根据指定的分隔符将字符串划分为子字符串数组
- 如果在字符串中没有找到分隔符,则将原字符串作为数组的唯一元素返回
- 返回的字符串数组中会忽略分隔符
格式
{ $split: [ <string expression>, <delimiter> ] }
- expression
- 类型:字符串
- 描述:被分隔的字符串
- 可以解析为字符串的任何有效的表达式
- delimiter
- 类型:字符串
- 描述:指定分隔符
- 可以解析为字符串的任何有效的表达式
Example | Results |
---|---|
{ $split: [ "June-15-2013", "-" ] } |
[ "June", "15", "2013" ] |
{ $split: [ "banana split", "a" ] } |
[ "b", "n", "n", " split" ] |
{ $split: [ "Hello World", " " ] } |
[ "Hello", "World" ] |
{ $split: [ "astronomical", "astro" ] } |
[ "", "nomical" ] |
{ $split: [ "pea green boat", "owl" ] } |
[ "pea green boat" ] |
{ $split: [ "headphone jack", 7 ] } |
Errors with message: ... |
{ $split: [ "headphone jack", /jack/ ] } |
Errors with message: ... |
"$split requires an expression that evaluates to a string as a second argument, found: regex"
## 比较运算 ##
$gt
- 比较两个值并返回一个布尔值
格式
{ $gt: [ <expression1>, <expression2> ] }
- 返回值
- true:第一个表达式大于第二个表达式
- false:第一个表达式小于等于第二个表达式
- 对于不同类型的值,使用指定的 BSON 比较顺序来比较值和类型
$gte
- 比较两个值并返回一个布尔值
格式
{ $gte: [ <expression1>, <expression2> ] }
- 返回值
- true:第一个表达式大于等于第二个表达式
- false:第一个表达式小于第二个表达式
- 对于不同类型的值,使用指定的 BSON 比较顺序来比较值和类型
$lt
- 比较两个值并返回一个布尔值
格式
{ $lt: [ <expression1>, <expression2> ] }
- 返回值
- true:第一个表达式小于第二个表达式
- false:第一个表达式大于等于第二个表达式
- 对于不同类型的值,使用指定的 BSON 比较顺序来比较值和类型
$lte
- 比较两个值并返回一个布尔值
格式
{ $lte: [ <expression1>, <expression2> ] }
- 返回值
- true:第一个表达式小于等于第二个表达式
- false:第一个表达式大于第二个表达式
- 对于不同类型的值,使用指定的 BSON 比较顺序来比较值和类型
## 算术运算 ##
$add
- 对数值类型或日期类型的字段进行加法运算
- 如果其中一个参数是日期类型,则将其他参数当作毫秒数相加并返回日期
格式
{ $add: [ <expression1>, <expression2>, ... ] }
- expression
- 可以解析为数值或日期的任何有效的表达式
$subtract
- 对数值类型或日期类型的字段进行减法运算
- 两个参数都是日期,则转换为毫秒数,返回差值
- 一个参数是日期,则将其他参数当作毫秒数相减并返回日期
格式
{ $subtract: [ <expression1>, <expression2> ] }
- expression
- 可以解析为数值或日期的任何有效的表达式
- 第一个参数减去第二个参数
- 要从日期中减去一个数值,则日期必须是第一个参数
示例
文档
db.sales.insertMany([
{ "_id" : 1, "item" : "abc", "price" : 10, "fee" : 2, "discount" : 5, "date" : ISODate("2014-03-01T08:00:00Z") },
{ "_id" : 2, "item" : "jkl", "price" : 20, "fee" : 1, "discount" : 2, "date" : ISODate("2014-03-01T09:00:00Z") }
])
聚合
db.sales.aggregate( [ { $project: { item: 1, dateDifference: { $subtract: [ "$date", 5 * 60 * 1000 ] } } } ] )
-
结果
{ "_id" : 1, "item" : "abc", "dateDifference" : ISODate("2014-03-01T07:55:00Z") } { "_id" : 2, "item" : "jkl", "dateDifference" : ISODate("2014-03-01T08:55:00Z") }
$multiply
- 将数字相乘并返回结果
- 对数值类型的字段进行乘法运算
格式
{ $multiply: [ <expression1>, <expression2>, ... ] }
- expression
- 可以解析为数值的任何有效的表达式
$divide
- 对数值类型的字段进行除法运算
格式
{ $divide: [ <expression1>, <expression2> ] }
-
expression
- 可以解析为数值的任何有效的表达式
- 第一个参数除以第二个参数
-
结果可以保留很多位小数
{ "_id" : 1, "item" : "abc", "dateDiff" : NumberDecimal("3.333333333333333333333333333333333") }
$mod
- 对数值类型的字段进行取模运算(取余数)
格式
{ $mod: [ <expression1>, <expression2> ] }
- expression
- 可以解析为数值的任何有效的表达式
- 第一个参数除以第二个参数
## 时间日期 ##
$year
- 返回日期的年份部分
- 如果值为字符串,则报错
格式
{ $year: <dateExpression> }
-
dateExpression【该部分操作符的格式具有相同的 dateExpression 规范】
-
可以解析为 日期、时间戳、ObjectID 的有效表达式
-
或具有如下格式的文档
{ date: <dateExpression>, timezone: <tzExpression> }
-
date
- $year 操作符获取的日期
- dateExpression
- 可以解析为 日期、时间戳、ObjectID 的有效表达式
-
timezone【可选】
- 指定时区,默认以 UTC 格式显示
-
-
示例
Example | Result |
---|---|
{ $year: new Date("2016-01-01") } |
2016 |
{ $year: { date: new Date("Jan 7, 2003") } } |
2003 |
{ $year: { date: new Date("August 14, 2011"), timezone: "America/Chicago" } } |
2011 |
{ $year: ISODate("1998-11-07T00:00:00Z") } |
1998 |
{ $year: { date: ISODate("1998-11-07T00:00:00Z"), timezone: "-0400" } } |
1998 |
{ $year: "March 28, 1976" } |
error |
{ $year: Date("2016-01-01") } |
error |
{ $year: "2009-04-09" } |
error |
示例
{
"_id" : 1,
"date" : ISODate("2014-01-01T08:15:39.736Z")
}
聚合
db.sales.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" : 1, "year" : 2014, "month" : 1, "day" : 1, "hour" : 8, "minutes" : 15, "seconds" : 39, "milliseconds" : 736, "dayOfYear" : 1, "dayOfWeek" : 4, "week" : 0 }
$month
- 返回日期的月份部分(值为1~12之间)
格式
{ $month: <dateExpression> }
示例
Example | Result |
---|---|
{ $month: new Date("2016-01-01") } |
1 |
{ $month: { date: new Date("Nov 7, 2003") } } |
11 |
{ $month: ISODate("2000-01-01T00:00:00Z") } |
1 |
{ $month: { date: new Date("August 14, 2011"), timezone: "America/Chicago" } } |
8 |
{ $month: { date: ISODate("2000-01-01T00:00:00Z"), timezone: "-0500" } } |
12 |
{ $month: "March 28, 1976" } |
error |
{ $month: { date: Date("2016-01-01"), timezone: "-0500" } } |
error |
{ $month: "2009-04-09" } |
error |
$week
- 返回日期处于一年中的第几周(值为 0~53之间)
- 星期从星期日开始,第一周从一年中的第一个星期日开始,之前的日子在第0周
格式
{ $week: <dateExpression> }
示例
Example | Result |
---|---|
{ $week: new Date("Jan 1, 2016") } |
0 |
{ $week: { date: new Date("2016-01-04") } } |
1 |
{ $week: { date: new Date("August 14, 2011"), timezone: "America/Chicago" } } |
33 |
{ $week: ISODate("1998-11-01T00:00:00Z") } |
44 |
{ $week: { date: ISODate("1998-11-01T00:00:00Z"), timezone: "-0500" } } |
43 |
{ $week: "March 28, 1976" } |
error |
{ $week: Date("2016-01-01") } |
error |
{ $week: "2009-04-09" } |
error |
NOTE
$hour
cannot take a string as an argument.
$hour
- 返回日期的小时部分(值为 0~23 之间)
格式
{ $hour: <dateExpression> }
$minute
- 返回日期的分钟部分(值为0~59之间)
格式
{ $minute: <dateExpression> }
$second
- 返回日期的秒数部分(值为0~59之间,闰秒60)
格式
{ $second: <dateExpression> }
$millisecond
- 返回日期的毫秒部分(值为0~999之间)
格式
{ $millisecond: <dateExpression> }
$dayOfYear
- 返回日期处于一年中的第几天(值为1~366之间)
格式
{ $dayOfYear: <dateExpression> }
$dayOfMonth
- 返回日期处于这月中的第几天(值为1~31之间)
格式
{ $dayOfMonth: <dateExpression> }
$dayOfWeek
- 返回日期处于这周中的第几天(值为1~7)
格式
{ $dayOfWeek: <dateExpression> }
map-reduce
-
是一种数据处理范式(典范模式),将大量数据处理压缩为有用的聚合结果
-
处理过程
-
第一阶段——映射(map)
将输入文档中 指定字段 的值保存到一个数组中(value),映射到 分组字段的值(key),以键值对(key-value)的形式输出至 reduce 阶段
-
第二阶段——归约(reduce)
根据需求对 相同键 的值 做运算,将运算结果作为 文档返回 或 写入集合
-
-
map-reduce 使用定制的 JavaScript函数——map 函数,将值(指定字段)映射到一个键(分组字段),如果一个键有多个值,reduce 函数会将其处理压缩为一个对象
-
支持在分片集合上执行 map-reduce 操作,从 mongodb 4.2 开始
- 若要输出到分片集合,需提前创建
- 不建议替换已存在的分片集合
-
视图不支持 map-reduce 操作
-
vs 聚合管道
-
对大多数聚合操作,聚合管道提供更好的性能和更一致的接口,但是 map-reduce 操作提供了一些目前在聚合管道中无法提供的灵活性
-
map-reduce 的结果文档中只包含新增字段,聚合管道可以通过 $project 控制结果文档中包含原有字段
reduce 函数返回结果文档
{ "_id": "<key>", "value": <执行结果> }
可以在 finalize 函数中添加新字段
-
格式
db.collection.mapReduce(
<map>, // 类似 $group
<reduce>,
{
out: <collection>, // 类似 $out
query: <document>, // 类似 $match
sort: <document>, // 类似 $sort
limit: <number>, // 类似 $limit
finalize: <function>, // 类似 $project
scope: <document>,
jsMode: <boolean>,
verbose: <boolean>,
bypassDocumentValidation: <boolean>
}
)
-
map
-
类型:function
-
描述:
-
JavaScript函数,将输入文档中 指定字段的值 保存到一个数组中(value),映射到 分组字段的值(key),输出至 reduce 函数中
-
作用于每个输入文档,以分组字段的值为键(唯一),以指定字段的值为value,将值作为数组元素映射到键。执行完所有输入文档后,将组成的键值对输出至reduce函数中
相同的键只产生一个键值对
-
-
格式
function() { ... emit(key, value); }
-
在 map 函数中,通过
this
引用当前文档 -
map 函数不能访问数据库
-
map 函数不能对函数外产生影响
-
可以访问在 scope 参数中定义的全局变量
-
一个 emit 只能容纳 mongodb 最大 BSON 文档大小的一半(8MB)
-
map 函数可以多次调用
emit( key, value)
函数,创建键值对-
如果 key 已经存在,则将 value 加入key对应的 value数组中
-
如果 key 不存在,则创建新的 key-value 键值对
根据输入文档字段的值,调用 0 次或1次 emit 函数
function() { if (this.status == 'A') emit(this.cust_id, 1); }
根据输入文档数组字段的元素个数,多次调用 emit 函数
function() { this.items.forEach(function(item){ emit(item.sku, 1); }); }
-
-
-
-
reduce
-
类型:function
-
描述:JavaScript函数,将与键关联的值数组处理压缩为一个对象
-
格式
function(key, values) { ... return result; }
-
reduce 函数不能访问数据库
-
reduce 函数不能对函数外产生影响
-
values 类型为数组,元素为映射到键的值对象
-
可以访问在 scope 参数中定义的全局变量
-
输入 reduce 函数的文档,其大小不能大于mongodb最大BSON文档大小的一半(8MB)
但是reduce函数处理过程中可以超过
-
可以多次调用 reduce 函数,对于指定的键,前一个reduce函数的输出将作为下一个reduce函数的输入
-
对同一个键多次调用reduce函数,或需要执行 finalize 函数,此时
-
reduce 返回值的类型必须与map函数输出值的类型一致,即键值对形式
-
reduce 函数必须是可结合的,以下声明必须为true
reduce(key, [ C, reduce(key, [ A, B ]) ] ) == reduce( key, [ C, A, B ] )
-
reduce 函数必须是冪等的,以下声明必须为true
reduce( key, [ reduce(key, valuesArray) ] ) == reduce( key, valuesArray )
-
reduce 函数应该是可交换的,values 数组中的元素顺序不应该影响输出结果,以下声明为true
reduce( key, [ A, B ] ) == reduce( key, [ B, A ] )
-
-
-
-
options
-
out
- 类型:字符串或Document
-
描述:指定 map-reduce 操作结果的输出形式,可以输出到指定集合中,也可以内联输出
-
格式
输出到指定集合
out: <collectionName>
- 如果集合已经存在,则会 替换 现有文档
指定输出模式
out: { <action>: <collectionName> [, db: <dbName>] [, sharded: <boolean> ] [, nonAtomic: <boolean> ] }
-
action
- replace
- 如果输出集合已经存在,则用结果文档 替换 现有文档
- merge
- 如果输出集合已经存在,则将结果文档与现有文档合并,如果现有文档与结果文档具有相同的键,则覆盖现有文档
- reduce
- 如果输出集合已经存在,则将结果文档与现有文档合并。如果现有文档与结果文档具有相同的键,则对结果文档和现有文档应用reduce函数,并用结果覆盖现有文档
- replace
-
db
-
指定 map-reduce 输出集合所在的数据库
-
默认为与输入集合相同的数据库
-
-
sharded
- 4.2 被废弃
-
nonAtomic
- 将输出操作指定为非原子操作,只适用于 merge 和 reduce 输出模式,需要几分钟的时间来执行
- 默认情况下,nonAtomic 为 false, map-reduce操作在后处理期间锁定数据库
- 如果 nonAtomic 为 true,后处理步骤将防止 MongoDB 锁定数据库,在此期间,其他客户端将能够读取输出集合的中间状态
内联输出
out: { inline: 1 }
-
-
query
- 类型:Document
- 描述
- 使用查询操作符指定查询条件,对输入文档进行筛选
- 在 map 函数之前执行
-
sort
-
类型:Document
-
描述
-
对输入文档进行排序,有利于优化 map-reduce 操作
例如将排序键指定为与 emit 相同的键
-
指定排序的键必须是集合中已经建立索引的键
-
在 map 函数之前执行
-
-
-
limit
- 类型:number
- 描述
- 限制输入文档的数量
- 在map函数之前执行
-
finalize
-
类型:function
-
描述
- 对 reduce 输出结果进行修改
- 在 reduce 函数之后执行
-
格式
function(key, reducedValue) { ... return modifiedObject; }
- finalize 函数不能访问数据库
- finalize 函数不能对函数外产生影响
- 可以访问在 scope 参数中定义的全局变量
-
-
scope
- 类型:Document
- 描述
- 定义可在 map、reduce、finalize 函数中访问的全局变量
-
jsMode
- 类型:布尔
- 描述:是否在 map-reduce 过程中将数据转换成 BSON 格式
- false【默认】
-
verbose
- 类型:布尔
- 描述:是否在结果信息中包含时间信息
- false【默认】
-
collation
- 类型:Document
- 描述:指定排序规则
-
bypassDocumentValidation
- 类型:布尔
- 描述:在 map-reduce 过程中是否绕过数据验证,允许插入不满足数据验证的文档
JS Function
map-reduce 操作和 $where 操作符表达式不能访问某些全局函数或属性,例如db
以下函数和属性可以访问
Available Properties | Available Functions | ||
---|---|---|---|
args | assert() | isNumber() | print() |
MaxKey | BinData() | isObject() | printjson() |
MinKey | DBPointer() | ISODate() | printjsononeline() |
DBRef() | isString() | sleep() | |
doassert() | Map() | Timestamp() | |
emit() | MD5() | tojson() | |
gc() | NumberInt() | tojsononeline() | |
HexData() | NumberLong() | tojsonObject() | |
hex_md5() | ObjectId() | UUID() | |
version() |
示例
普通文档
计算每个消费者的总价
以 cust_id字段 分组,计算每组中 price字段的总和
文档
db.orders.insertMany([
{ _id: 1, cust_id: "Ant O. Knee", ord_date: new Date("2020-03-01"), price: 25, items: [ { sku: "oranges", qty: 5, price: 2.5 }, { sku: "apples", qty: 5, price: 2.5 } ], status: "A" },
{ _id: 2, cust_id: "Ant O. Knee", ord_date: new Date("2020-03-08"), price: 70, items: [ { sku: "oranges", qty: 8, price: 2.5 }, { sku: "chocolates", qty: 5, price: 10 } ], status: "A" },
{ _id: 3, cust_id: "Busby Bee", ord_date: new Date("2020-03-08"), price: 50, items: [ { sku: "oranges", qty: 10, price: 2.5 }, { sku: "pears", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 4, cust_id: "Busby Bee", ord_date: new Date("2020-03-18"), price: 25, items: [ { sku: "oranges", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 5, cust_id: "Busby Bee", ord_date: new Date("2020-03-19"), price: 50, items: [ { sku: "chocolates", qty: 5, price: 10 } ], status: "A"},
{ _id: 6, cust_id: "Cam Elot", ord_date: new Date("2020-03-19"), price: 35, items: [ { sku: "carrots", qty: 10, price: 1.0 }, { sku: "apples", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 7, cust_id: "Cam Elot", ord_date: new Date("2020-03-20"), price: 25, items: [ { sku: "oranges", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 8, cust_id: "Don Quis", ord_date: new Date("2020-03-20"), price: 75, items: [ { sku: "chocolates", qty: 5, price: 10 }, { sku: "apples", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 9, cust_id: "Don Quis", ord_date: new Date("2020-03-20"), price: 55, items: [ { sku: "carrots", qty: 5, price: 1.0 }, { sku: "apples", qty: 10, price: 2.5 }, { sku: "oranges", qty: 10, price: 2.5 } ], status: "A" },
{ _id: 10, cust_id: "Don Quis", ord_date: new Date("2020-03-23"), price: 25, items: [ { sku: "oranges", qty: 10, price: 2.5 } ], status: "A" }
])
-
map-reduce 操作
-
定义 map 函数
- 将 price字段 映射到 cust_id 字段,输出 cust_id-price 键值对
var mapFunction1 = function() { emit(this.cust_id, this.price); };
-
定义 reduce 函数
-
接收 map 函数返回的键值对列表,对每个键值对执行 reduce 函数
-
将键作为
_id
字段的值,执行结果作为value
字段的值,以文档{ "_id": "<key>", "value": <执行结果> }
的形式返回
var reduceFunction1 = function(keyCustId, valuesPrices) { // 具有一些 JS 目前没有的接口 return Array.sum(valuesPrices); };
-
-
对集合中的所有文档执行 map-reduce 操作
- 将结果输出到 map_reduce_example 集合中
db.orders.mapReduce( mapFunction1, reduceFunction1, { out: "map_reduce_example" } )
-
-
聚合操作
db.orders.aggregate([ { $group: { _id: "$cust_id", value: { $sum: "$price" } } }, { $out: "agg_alternative_1" } ])
嵌套文档
计算2020-03-01之后,每个sku(单品)的订单数,总销量,以及每个订单平均销量
以 sku 字段分组,计算每组中 qty 字段的总和,以及每组中
_id
字段不同的文档的数量,最后计算平均销量
-
map-reduce 操作
-
定义map函数
- 将包含 count 和 qty 字段的文档,映射到 sku 字段,输出键值对 key-value
var mapFunction2 = function() { // 遍历数组,对嵌套子文档执行 emit 函数 // 并不是每个元素都创建一个键值对,key已经存在时,只会将 value 放入对应的 value数组 for (var idx = 0; idx < this.items.length; idx++) { var key = this.items[idx].sku; var value = { count: 1, qty: this.items[idx].qty }; emit(key, value); } };
-
定义 reduce 函数
- 接收 map 函数返回的键值对列表,对每个键值对执行 reduce 函数
- 统计每个"值"中 count 字段和 qty 字段的值,输出键值对 key-reduceVal
var reduceFunction2 = function(keySKU, countObjVals) { reducedVal = { count: 0, qty: 0 }; for (var idx = 0; idx < countObjVals.length; idx++) { reducedVal.count += countObjVals[idx].count; reducedVal.qty += countObjVals[idx].qty; } return reducedVal; };
-
定义finalize 函数
- 接收 reduce 函数返回的键值对列表,对每个键值对执行 finalize 函数
- 在返回结果文档中添加新字段 avg
var finalizeFunction2 = function (key, reducedVal) { reducedVal.avg = reducedVal.qty/reducedVal.count; return reducedVal; };
-
对集合中的所有文档执行 map-reduce 操作
- 将结果文档合并到已存在的数据库中
db.orders.mapReduce( mapFunction2, reduceFunction2, { out: { merge: "map_reduce_example2" }, query: { ord_date: { $gte: new Date("2020-03-01") } }, finalize: finalizeFunction2 } );
-
-
聚合管道
db.orders.aggregate( [ { $match: { ord_date: { $gte: new Date("2020-03-01") } } }, { $unwind: "$items" }, { $group: { _id: "$items.sku", qty: { $sum: "$items.qty" }, orders_ids: { $addToSet: "$_id" } } }, { $project: { value: { count: { $size: "$orders_ids" }, qty: "$qty", avg: { $divide: [ "$qty", { $size: "$orders_ids" } ] } } } }, { $merge: { into: "agg_alternative_3", on: "_id", whenMatched: "replace", whenNotMatched: "insert" } } ] )
- $match 阶段,相当于 map-reduce 中的 query 参数
- $unwind 阶段,将嵌套数组拆分成单独的文档
- $group 阶段,相当于 map-reduce 中的 map 和 reduce 函数
- $project 阶段,相当于 map-reduce 中的 finalize 函数
- $merge 阶段,相等于 map-reduce 中的 out 参数