MongoDB 聚合管道

MongoDB的聚合操作主要是对数据的批量处理。一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简单操作,也可以对记录进行数据统计,数据挖掘的复杂操作。聚合操作的输入是集中的文档,输出可以是一个文档也可以是多个文档。

MongoDB 提供了三种强大的聚合操作:

Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息。

本篇主要讲解 单目的聚合操作(Single Purpose Aggregation Operation)聚合管道(Aggregation Pipeline)

一、单目的聚合操作

单目的聚合命令常用的有:count() 和 distinct()。以 distinct() 为例,其工作流程如下:


去重操作流程

1.1 count

count用于返回集合中的文档数量。
示例
求出集合中 job_base-accdate 值大于 2018-11-06 的文档个数

db.getCollection('job_create').find({'job_base-accdate': {$gt: new Date('06/11/2018')}}).count()

等价于

db.getCollection('job_create').count({'job_base-accdate': {$gt: new Date('06/11/2018')}})

1.2 distinct

distinct用于去重
示例
对 job_base-jid 值进行去重操作

db.getCollection('job_create').distinct('job_base-jid')

二、聚合管道

MongoDB 中使用 db.COLLECTION_NAME.aggregate([{<stage>},...]) 方法来构建和使用聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果。聚合管道的工作流程如下:

聚合管道工作流程

  • $match 用于获取 status = "A" 的记录,然后将符合条件的记录送到下一阶段
  • $group 中进行分组求和计算,最后返回 Results。

其中,$match$group 都是阶段操作符,而阶段 $group 中用到的 $sum 是表达式操作符。

2.1 阶段操作符

在下面的示例中我们会使用如下集合进行讲解:

>db.article.find().pretty()
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "title" : "MongoDB Aggregate",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ],
    "pages" : 5.0,
    "time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d454"),
    "title" : "MongoDB Index",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Index",
        "Query"
    ],
    "pages" : 3.0,
    "time" : ISODate("2018-11-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "title" : "MongoDB Query",
    "author" : "Aaron",
    "tags" : [
        "Mongodb",
        "Query"
    ],
    "pages" : 8.0,
    "time" : ISODate("2019-06-11T16:00:00.000Z")
}

2.1.1 $project

$project 用于修改输入文档的结构。可以用来重命名、增加或删除字段(域),也可以用于创建计算结果以及嵌套文档。
示例
返回的文档中只包含_idtages

>db.article.aggregate([{$project:{_id:1,tags:1}}])
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ]
},
{
    "_id" : ObjectId("5c088fec651e67152257d454"),
    "tags" : [
        "Mongodb",
        "Index",
        "Query"
    ]
},
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "tags" : [
        "Mongodb",
        "Query"
    ]
}

新增字段

>db.article.aggregate([{$project:{_id:1,tags:1,editAuthor:'$author'}}])
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ],
    "editAuthor" : "simon"
},
{
    "_id" : ObjectId("5c088fec651e67152257d454"),
    "tags" : [
        "Mongodb",
        "Index",
        "Query"
    ],
    "editAuthor" : "simon"
},
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "tags" : [
        "Mongodb",
        "Query"
    ],
    "editAuthor" : "Aaron"
}

2.1.2 $match

$match用于过滤数据,只输出符合条件的文档。
示例
查询出文档中 author 为 simon的数据

>db.article.aggregate([{$match:{author:'simon'}}])
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "title" : "MongoDB Aggregate",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ],
    "pages" : 5.0,
    "time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d454"),
    "title" : "MongoDB Index",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Index",
        "Query"
    ],
    "pages" : 3.0,
    "time" : ISODate("2018-11-11T16:00:00.000Z")
}

2.1.3 $group

$group用于将集合中的文档分组,可用于统计结果
示例
统计每个作者写的文章篇数

>db.article.aggregate([{$group:{_id:'$author',total:{$sum:1}}}])
{
    "_id" : "Aaron",
    "total" : 1.0
},
{
    "_id" : "simon",
    "total" : 2.0
}

2.1.4 $sort

对集合中的文档进行排序
示例
让集合按照页数进行升序排序

>db.article.aggregate([{$sort:{pages:1}}])
{
    "_id" : ObjectId("5c088fec651e67152257d454"),
    "title" : "MongoDB Index",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Index",
        "Query"
    ],
    "pages" : 3.0,
    "time" : ISODate("2018-11-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "title" : "MongoDB Aggregate",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ],
    "pages" : 5.0,
    "time" : ISODate("2017-06-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "title" : "MongoDB Query",
    "author" : "Aaron",
    "tags" : [
        "Mongodb",
        "Query"
    ],
    "pages" : 8.0,
    "time" : ISODate("2019-06-11T16:00:00.000Z")
}

注意
如果以降序排列,则设置成 pages: -1

2.1.5 $unwind

将文档中数组类型的字段拆分成多条,每条文档包含数组中的一个值
示例
将集合中 tags字段进行拆分

>db.article.aggregate([{$match:{author:'Aaron'}},{$unwind:'$tags'}])
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "title" : "MongoDB Query",
    "author" : "Aaron",
    "tags" : "Mongodb",
    "pages" : 8.0,
    "time" : ISODate("2019-06-11T16:00:00.000Z")
},
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "title" : "MongoDB Query",
    "author" : "Aaron",
    "tags" : "Query",
    "pages" : 8.0,
    "time" : ISODate("2019-06-11T16:00:00.000Z")
}

注意

  • $unwind参数不是一个数组类型时,将会抛出异常
  • $unwind所作的修改,只用于输出,不能改变原文档

2.1.6 $limit

限制返回文档的数量
示例
返回集合的前一条文档

>db.article.aggregate([{$limit: 1}])
{
    "_id" : ObjectId("5c088fec651e67152257d453"),
    "title" : "MongoDB Aggregate",
    "author" : "simon",
    "tags" : [
        "Mongodb",
        "Database",
        "Query"
    ],
    "pages" : 5.0,
    "time" : ISODate("2017-06-11T16:00:00.000Z")
}

2.1.7 $skip

跳过指定数量的文档,并返回余下的文档
示例
跳过集合的前两个文档

>db.article.aggregate([{$skip: 2}])
{
    "_id" : ObjectId("5c088fec651e67152257d455"),
    "title" : "MongoDB Query",
    "author" : "Aaron",
    "tags" : [
        "Mongodb",
        "Query"
    ],
    "pages" : 8.0,
    "time" : ISODate("2019-06-11T16:00:00.000Z")
}

2.2表达式操作符

表达式操作符有很多操作类型,其中最常用的有布尔聚合操作、集合操作、比较聚合操作、算术聚合操作、字符串聚合操作、数组聚合操作、日期聚合操作、条件聚合操作、数据类型聚合操作等

2.2.1 布尔聚合操作

  • $and
  • $or
  • $not

示例

>db.getCollection('col').find()
{
    "_id" : ObjectId("5c08c5b5651e67152257d45b"),
    "name" : "a",
    "classes" : "classe 1",
    "score" : 90.0
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45c"),
    "name" : "b",
    "classes" : "classe 2",
    "score" : 50.0
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45d"),
    "name" : "c",
    "classes" : "classe 3",
    "score" : 60.0
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45e"),
    "name" : "d",
    "classes" : "classe 4",
    "score" : 70.0
}

判断成绩是否大于80或者小于50

>db.col.aggregate(
   [
     {
       $project:
          {
            name: 1,
            score:1,  
            result: { $or: [ { $gt: [ "$score", 80 ] }, { $lt: [ "$score", 50 ] } ] }
          }
     }
   ]
)
{
    "_id" : ObjectId("5c08c5b5651e67152257d45b"),
    "name" : "a",
    "score" : 90.0,
    "result" : true
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45c"),
    "name" : "b",
    "score" : 50.0,
    "result" : false
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45d"),
    "name" : "c",
    "score" : 60.0,
    "result" : false
},
{
    "_id" : ObjectId("5c08c5b5651e67152257d45e"),
    "name" : "d",
    "score" : 70.0,
    "result" : false
}

2.2.2 集合操作

  • $setEquals 除了重复元素外,包括的元素相同
  • $setIntersection 交集
  • $setUnion 并集
  • $setDifference 只在前一集合出现,也就是后一个集合的补集
  • $setIsSubset 前一个集合是后一个集合的子集
  • $anyElementTrue 一个集合内,只要一个元素为真,则返回true
  • $allElementsTrue 一个集合内,所有的元素都为真,则返回true

示例

>db.col.find()
{
    "_id" : ObjectId("5c08c98d651e67152257d45f"),
    "A" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ]
},
{
    "_id" : ObjectId("5c08c98d651e67152257d460"),
    "A" : [ 
        "java", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ]
},
{
    "_id" : ObjectId("5c08c98d651e67152257d461"),
    "A" : [ 
        "java", 
        "c++"
    ],
    "B" : []
}

计算A和B集合的

>db.col.aggregate(
   [
     { $project: { A:1, B: 1, union: { $setIntersection: [ "$A", "$B" ] }} }
   ]
)
{
    "_id" : ObjectId("5c08c98d651e67152257d45f"),
    "A" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "union" : [ 
        "c++", 
        "java", 
        "phython"
    ]
},
{
    "_id" : ObjectId("5c08c98d651e67152257d460"),
    "A" : [ 
        "java", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "union" : [ 
        "c++", 
        "java"
    ]
},
{
    "_id" : ObjectId("5c08c98d651e67152257d461"),
    "A" : [ 
        "java", 
        "c++"
    ],
    "B" : [],
    "union" : []
}

2.2.3 比较操作

  • $cmd 两个值相等返回0,前值大于后值返回1,前值小于后值返回-1
  • $eq 是否相等
  • $gt 前值是否大于后值
  • $gte 前值是否大于等于后值
  • $lt 前值是否小于后值
  • $lte 前值是否小于等于后值
  • $ne 是否不相等

示例

>db.col.find()
{
    "_id" : ObjectId("5c08cbb3651e67152257d463"),
    "score" : 80.0
}

score 大于等于 80

>db.col.aggregate(
[
    {$project:{_id:1,score:1,result:{$gte:['$score',80]}}}
]
)
{
    "_id" : ObjectId("5c08cbb3651e67152257d463"),
    "score" : 80.0,
    "result" : true
}

2.2.4 算数聚合操作

  • $abs 绝对值
  • $add
  • $ceil 向上取整
  • $divide
  • $exp 几次方
  • $floor 向下取整
  • $ln 自然对数
  • $log 对数
  • $log10 以10为底的对数
  • $mod 取模
  • $multiply
  • $pow 指数
  • $sqrt 平方根
  • $subtract
  • $trunc 截掉小数取整

示例
score 加 10

db.col.aggregate(
[
    {$project:{_id:1,score:1,result:{$add:['$score',10]}}}
]
)
{
    "_id" : ObjectId("5c08cbb3651e67152257d463"),
    "score" : 80.0,
    "result" : 90.0
}

2.2.5 字符串聚合操作

  • $concat 字符串连接
  • $indexOfBytes 子串位置(字节)
  • $indexOfCP 子串位置(字符)
  • $split 分割字符串
  • $strLenBytes 字节长度
  • $strLenCP 字符长度
  • $strcasecmp 字符串比较
  • $substrBytes 创建子串(按字节)
  • $substrCP 创建子串(按字符)
  • $toLower 小写
  • $toUpper 大写

示例

>db.col.find()
{
    "_id" : ObjectId("5c08cf2d651e67152257d464"),
    "name" : "abcdefgAAADccsD"
}

将 name 值大写

>db.col.aggregate([
   {
     $project: {name: 1,result:{$toUpper:'$name'}}
   }
])
{
    "_id" : ObjectId("5c08cf2d651e67152257d464"),
    "name" : "abcdefgAAADccsD",
    "result" : "ABCDEFGAAADCCSD"
}

2.2.6 数组聚合操作

  • $arrayElemAt 返回指定数组索引中的元素
  • $concatArrays 数组连接
  • $filter 返回筛选后的数组
  • $indexOfArray 索引
  • $isArray 是否是数组
  • $range 创建数值数组
  • $reverseArray 反转数组
  • $reduce 对数组中的每个元素应用表达式,并将它们组合成一个值
  • $size 数组元素个数
  • $slice 子数组
  • $zip 合并数组
  • $in 返回一个布尔值,表示指定的值是否在数组中

示例

>db.col.find()
{
    "A" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ]
}

判断指定元素是否在数组中

db.col.aggregate([
   {
     $project: {A: 1,B:1,result:{$in:['java','$A']}}
   }
])
{
    "_id" : ObjectId("5c08c98d651e67152257d45f"),
    "A" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "B" : [ 
        "java", 
        "phython", 
        "c++"
    ],
    "result" : true
}

2.2.7 日期聚合操作

  • $dayOfYear 日(1-366)
  • $dayOfMonth 月(1-23)
  • $dayOfWeek 星期(1 (Sunday) 到 7 (Saturday))
  • $year
  • $month 月(1-12)
  • $week 周(0-53)
  • $hour 时(0-23)
  • $minute 分(0-59)
  • $second 秒(0-60)
  • $millisecond 毫秒(0-999)
  • $dateToString 返回格式化字符串的日期
  • $isoDayOfWeek 以ISO 8601格式返回星期几
  • $isoWeek 以ISO 8601格式返回周号,范围从1到53
  • $isoWeekYear 以ISO 8601格式返回年份编号

示例

>db.col.find()
{
    "_id" : ObjectId("5c08d61d651e67152257d465"),
    "date" : ISODate("2018-12-06T07:56:13.930Z")
}

日期聚合操作

>db.col.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" : ObjectId("5c08d61d651e67152257d465"),
    "year" : 2018,
    "month" : 12,
    "day" : 6,
    "hour" : 7,
    "minutes" : 56,
    "seconds" : 13,
    "milliseconds" : 930,
    "dayOfYear" : 340,
    "dayOfWeek" : 5,
    "week" : 48
}

2.2.8 数据类型集合操作

  • $type 返回字段类型

示例

>db.col.aggregate(
   [
     {
       $project:
         {
           date:1,  
           type:{$type:'$date'}
         }
     }
   ]
)
{
    "_id" : ObjectId("5c08d61d651e67152257d465"),
    "date" : ISODate("2018-12-06T07:56:13.930Z"),
    "type" : "date"
}

2.3 聚合管道的优化与限制

2.3.1 优化

默认情况下,在整个集合作为聚合管道的输入情况下,为了提高处理数据的效率,可以使用一下策略:

  • $match$sort 放到管道的前面,可以给集合建立索引,来提高处理数据的效率
  • 可以用 $match$limit$skip 对文档进行提前过滤,以减少后续处理文档的数量

当聚合管道执行命令时,MongoDB 也会对各个阶段自动进行优化,主要包括以下几个情况:

  • $sort + $match 顺序优化:如果$match 出现在 $sort 之后,优化器会自动把 $match 放到 $sort 前面
  • $skip + $limit 顺序优化:如果 $skip$limit 之后,优化器会把 $limit 移动到 $skip 的前面,移动后 $limit的值等于原来的值加上 $skip 的值。例如:移动前:{$skip: 10, $limit: 5},移动后:{$limit: 15, $skip: 10}

2.3.2 限制

  • 返回结果大小:聚合结果返回的是一个文档,不能超过 16M,从 MongoDB 2.6版本以后,返回的结果可以是一个游标或者存储到集合中,返回的结果不受 16M 的限制
  • 内存:聚合管道的每个阶段最多只能用 100M 的内存,如果超过100M,会报错,如果需要处理大数据,可以使用 allowDiskUse 选项,存储到磁盘上
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • MongoDB 聚合 MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回...
    AllSun阅读 739评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,893评论 2 89
  • 我们先介绍一下 MongoDB 的聚合功能,聚合操作主要用于对数据的批量处理,往往将记录按条件分组以后,然后再进行...
    一秆子数码阅读 566评论 0 2
  • 某天晚上和一个昆明朋友聊天,发现他对雾霾没有任何概念。我作为一个能忍受寒冷和雾霾的北方人,有必要介绍一下我的经历。...
    壹步尘埃阅读 385评论 11 6