当前位置:Gxlcms > 数据库问题 > MongoDB—聚合

MongoDB—聚合

时间:2021-07-01 10:21:17 帮助过:3人阅读

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

人气教程排行