返回顶部

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"
      • 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 可以利用索引查找每个分组中的第一个文档
        1. 在 $sort 阶段之后,且 $sort 阶段对分组字段进行排序
        2. 在分组字段上有一个索引,与 $sort 排序一致
        3. 在 $group 阶段中只使用 $first 累加器
    • $geoNear
  • 预先过滤

    • 当聚合操作只需针对集合中数据的一个子集,则在管道开头使用 \(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:查询条件表达式

限制

  1. \(match 查询语法与读取操作查询语法相同;例如:\)match不接受原始聚合表达式。要在\(match中包含聚合表达式,请使用\)expr查询表达式

    { $match: { $expr: { <aggregation expression> } } }
    
  2. 要在 $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 子句中定义的变量
  • 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报错
    1. branches 字段缺失或不是一个数组
    2. 条件语句不包含 case 字段
    3. 条件语句不包含 then 字段
    4. 条件语句包含 case、then 之外的字段
    5. 未指定 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函数,并用结果覆盖现有文档
      • 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 参数
posted @ 2020-08-29 18:51  usmile  阅读(285)  评论(0编辑  收藏  举报