Mongodb(2)—聚合操作(1)原生命令

Aggregation简单来说,就是将文档统计。分析、分类的方法。

Aggregation接收指定collection的数据集,通过计算后返回result数据的过程。它中间会经历多个stages。整体而言就是一个pipeline(管道)。

image.png

图片来源

由上图可知,文档(数据)经过筛选、分组、统计等过程最终输出数据。

语法:

db.collection.aggregate(pipeline,options);

参数说明:

参数 类型 描述
pipeline array 一系列数据聚合操作。详见聚合管道操作符,在版本2.6后,该方法可以接收pipeline作为参数而非数组,但若不指定数组,则不能指定options参数
options document 可选,aggregate()传递给聚合命令的其他选项。2.6版本中新增功能:仅当将管道指定为数组时才可使用。
  • db.collection.aggregate()可以用多个构件创建一个管道,对文档进行一连串的处理。这些构件包括:筛选(match)、映射(project)、分组(group)、排序(sort)、限制(limit)、跳跃(skip)。

  • 每个阶段的管道限制为100M内存,如果一个节点管道超过这个极限,MongoDB会产生一个错误,为了处理大型数据集,可以设置options参数(allowDiskUse=true)将聚合管道节点的数据写入临时文件。这样就可以解决100M的内存限制。

  • db.collection.aggregate()输出结果只能保存在一个文档中,BSON Document大小限制为16M。可以通过返回指针解决。版本2.6后,返回一个指针,可以返回任何数据集大小。

官网描述

1. 聚合操作

原始数据.png

$count:统计数量

   db.stus.aggregate([
     {
         $match:{"scope":{$gte:60}}
     },{
         $count:"count"   //统计函数,计算数量
     }
     ],{
        allowDiskUse:true  //options(可选)即允许磁盘上缓存
     })
显示结果.png

注意$count统计数量,上述有两个操作。

  1. $match阶段,相当于where操作,将大于等于60的scope传递给下一个=阶段。
  2. $count阶段是返回聚合管道中剩余的文档计数,并将值分配给名为count的字段。

相当于sql:select count(*) as count from stus where scope>=60;

$group分组

按指定的表达式对文档进行分组,并将每个不同分组的文档输出到一个阶段。输出文档包含_id字段,文档以_id的值进行分组。

输出文档还可以包含计算字段,该字段保存由$group_id字段分组的一些accumulator表达式的值。$group不会输出具体的文档而只是统计信息。

语法:

{$group:{_id:<expression>,<field>:{<accumulator1> : <expression1>},...}}
  • _id字段是必填的:但是,_id:null可以用来为整个输出文档计算累计值。
  • 剩余字段是可选的,并使用<accumulator>运算符进行计算。
  • _id和<accumulator>表达式可以接受任何有效的表达式
    db.stus.aggregate([
     {
        $group:{
            "_id":"$age",
            "sum":{"$sum":"$scope"},  //计算总和。
            "avg":{"$avg":"$scope"},  //计算平均值。
            "min":{"$min":"$scope"},  //获取集合中所有文档对应值得最小值。
            "max":{$max:"$scope"},    //获取集合中所有文档对应值得最大值。
            "first":{$first:"$scope"}, //返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。
            "last":{$last:"$scope"},   //返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。
            "push":{$push:"$name"},  //将指定的表达式的值添加到一个数组中。
            "addToSet":{$addToSet:"$scope"} //将表达式的值添加到一个集合中(无重复值,无序)。
        }
     }
    ]);
输出结果.png

输出参数类型:

{
  "_id" : "14",
  "sum" : 126,
  "avg" : 63,
  "min" : 59,
  "max" : 67,
  "first" : 59,
  "last" : 67,
  "push" : ["数组4", "数组10"],
  "addToSet" : [59, 67]
}

注意:

  1. $group阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true。
  2. mongodb类型值不会进行隐式转换,即使值相同,但是不是相同的类型,也是不同的组。
  3. accumulator表达式只能处理number类型。

练习2:

插入数据:

    db.items.insert([
    { "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-03-01T08:00:00Z") },
    { "_id" : 2, "item" : "jkl", "price" : 20, "quantity" : 1, "date" : ISODate("2014-03-01T09:00:00Z") },
    { "_id" : 3, "item" : "xyz", "price" : 5, "quantity" : 10, "date" : ISODate("2014-03-15T09:00:00Z") },
    { "_id" : 4, "item" : "xyz", "price" : 5, "quantity" : 20, "date" : ISODate("2014-04-04T11:21:39.736Z") },
    { "_id" : 5, "item" : "abc", "price" : 10, "quantity" : 10, "date" : ISODate("2014-04-04T21:23:13.331Z") }
    ]);

1. 按date的月份、日期、年份对文档进行分组,并计算total priceaverage quantity,并汇总各文档的数量。

表达式.png

$group可以对多个字段进行分组。

最终执行结果如下图所示。

/* 1 */
{
  "_id" : {
    "month" : 3,
    "day" : 1,
    "year" : 2014
  },
  "totalPrice" : 40,
  "avg" : 1.5,
  "count" : 2
}

/* 2 */
{
  "_id" : {
    "month" : 4,
    "day" : 4,
    "year" : 2014
  },
  "totalPrice" : 200,
  "avg" : 15,
  "count" : 2
}

/* 3 */
{
  "_id" : {
    "month" : 3,
    "day" : 15,
    "year" : 2014
  },
  "totalPrice" : 50,
  "avg" : 10,
  "count" : 1
}

2. 那么group null是什么效果呢?

group null的效果.png

可以看出,实际上是对所有文档进行统计。

3. 如何查询distinct values的数据

使用group分组完成去重的功能。

image.png

4. 数据转换

1)将集合中的数据按price分组并转换成item的数组。

数据转换

price相同的item在一个数组中

2)使用系统变量$$ROOT按price对文档进行分组,生成的文档不得超过BSON文档大小限制。

    db.items.aggregate([{
        $group:{_id:"$price",items:{$push:"$$ROOT"}}
     }]);

执行结果:

/* 1 */
{
  "_id" : 5,
  "items" : [{
      "_id" : 3,
      "item" : "xyz",
      "price" : 5,
      "quantity" : 10,
      "date" : ISODate("2014-03-15T09:00:00Z")
    }, {
      "_id" : 4,
      "item" : "xyz",
      "price" : 5,
      "quantity" : 20,
      "date" : ISODate("2014-04-04T11:21:39.736Z")
    }]
}

/* 2 */
{
  "_id" : 20,
  "items" : [{
      "_id" : 2,
      "item" : "jkl",
      "price" : 20,
      "quantity" : 1,
      "date" : ISODate("2014-03-01T09:00:00Z")
    }]
}

/* 3 */
{
  "_id" : 10,
  "items" : [{
      "_id" : 1,
      "item" : "abc",
      "price" : 10,
      "quantity" : 2,
      "date" : ISODate("2014-03-01T08:00:00Z")
    }, {
      "_id" : 5,
      "item" : "abc",
      "price" : 10,
      "quantity" : 10,
      "date" : ISODate("2014-04-04T21:23:13.331Z")
    }]
}

$match过滤操作

过滤文档,仅将符合指定条件的文档传递到下一个管道阶段。

$match接受一个指定查询条件的文档。

语法:

{ $match: { <query> } }

管道优化:
$match用于对文档进行筛选,之后可以得到文档子集上做聚合,$match可以使用除了地理空间之外的所有常规查询操作符。在实际应用中尽可能将$match放在管道的前面位置

  1. 可以快速将不需要的文档过滤掉,以减少管道的工作量;
  2. 如果在$project$group之前执行$match,查询可以使用索引。:

案例:

  1. 插入数据:
     db.views.insert([
     { "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 },
     { "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 },
     { "_id" : ObjectId("55f5a192d4bede9ac365b257"), "author" : "ahn", "score" : 60, "views" : 1000 },
     { "_id" : ObjectId("55f5a192d4bede9ac365b258"), "author" : "li", "score" : 55, "views" : 5000 },
     { "_id" : ObjectId("55f5a1d3d4bede9ac365b259"), "author" : "annT", "score" : 60, "views" : 50 },
     { "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "author" : "li", "score" : 94, "views" : 999 },
     { "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "author" : "ty", "score" : 95, "views" : 1000 }
     ]);

1. 使用$match做简单的匹配查询

案例.png

2. 使用$match管道处理文档,然后将结果分组

统计数量.png

$match使用MongoDB的标准查询操作,放在group前相当于where,放在group后相当having。

unwind

$unwind:将文档中某一个数组类型字段拆分成多条,每条包含数组中的一个值。

语法:

{$unwind:<field path>}

注:要指定字段的路径,在字段名称前加上$符并使用引号

在3.2后增加的新语法

{
  $unwind:{
       path:<field path>,
       includeArrayIndex:<String>, //可选。一个新字段的名称用于存放元素的数组索引,该名称不能以$开头。
       preserveNullAndEmptyArrays:<boolean> #可选default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档
    }
}

如果为输出文档汇总不存在的字段指定路径,或者该字段为空数组,则$unwind默认会忽略输入文档,并且不会输出该文档的文档。

案例1:简单的$unwind命令

     db.testunwind.insert(
        { "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] }
     );

使用$unwind为sizes数组中的每个元素输出一个文档。

image.png

每个文档与输入文档相同,除了sizes字段的值是元素sizes数组的值。

案例二:使用3.2新增的属性字段

     db.testunwind2.insert([
     { "_id" : 1, "item" : "ABC", "sizes": [ "S", "M", "L"] },
     { "_id" : 2, "item" : "EFG", "sizes" : [ ] },
     { "_id" : 3, "item" : "IJK", "sizes": "M" },
     { "_id" : 4, "item" : "LMN" },
     { "_id" : 5, "item" : "XYZ", "sizes" : null }
     ]);

1)使用includeArrayIndex参数

     db.testunwind2.aggregate([
        {$unwind:{path:"$sizes",includeArrayIndex:"arrayIndex"}}
     ]);

最终结果是将含有sizes数组的文档进行输出,且携带着数组元素下标。

{ "_id" : 1, "item" : "ABC", "sizes" : "S", "arrayIndex" : NumberLong(0) }
{ "_id" : 1, "item" : "ABC", "sizes" : "M", "arrayIndex" : NumberLong(1) }
{ "_id" : 1, "item" : "ABC", "sizes" : "L", "arrayIndex" : NumberLong(2) }
{ "_id" : 3, "item" : "IJK", "sizes" : "M", "arrayIndex" : null }

2)使用preserveNullAndEmptyArrays参数

image.png

$project映射操作

可以将其看做sql中的select操作。

$project可以在文档中选择想要的字段和不想要的字段。也可以通过管道表达式进行一些复杂的操作。如数学操作、日期操作、字符串操作、逻辑操作。

语法:

{ $project: { <specification(s)> } }

$project管道符的作用是选择字段(指定字段、添加字段、不显示字段,排除字段等),重命名字段,派生字段。

specification有以下形式:

  1. <field>:<1 or true>是否包含该字段,field:1/0,表示选择/不选择field。
  2. _id:<0 or false>是否指定_id字段。
  3. <field>:<expression>添加新字段或重置现有字段的值,在版本3.6中更改:Mongodb3.6添加变量REMOVE。如果表达式的计算结果为$$REMOVE,则该字段将排除在输出外。
  4. 投影或添加/重置嵌入文档的字段时,可以使用.符号,例如:
"contact.address.country": <1 or 0 or expression>
或
contact: { address: { country: <1 or 0 or expression> } }

案例一:

     db.testproject.insert(
      {
         "_id" : 1,
         title: "abc123",
         isbn: "0001122223334",
         author: { last: "zzz", first: "aaa" },
         copies: 5,
         lastModified: "2016-07-28"
       }
     )

1)$project默认输出_id字段。

image.png

2)_id字段默认包含在内,要从$project阶段的输出文档中排除_id字段。

排除_id字段.png

3)在$project阶段从输出中排除lastModified字段:

image.png

4)在嵌套文档中排除字段。

image.png
image.png

5)3.6版本中的新功能:从3.6版本开始,可以从聚合表达式中使用变量REMOVE来有条件地禁止一个字段。

继续插入数据

     db.testproject.insert([
         {
         "_id" : 2,
         title: "Baked Goods",
         isbn: "9999999999999",
         author: { last: "xyz", first: "abc", middle: "" },
         copies: 2,
         lastModified: "2017-07-21"
         },
        {
         "_id" : 3,
         title: "Ice Cream Cakes",
         isbn: "8888888888888",
         author: { last: "xyz", first: "abc", middle: "mmm" },
         copies: 5,
         lastModified: "2017-07-22"
         }
      ])

$project阶段使用REMOVE变量来排除author.middle字段,前提是它等于"":

      db.testproject.aggregate([
      {
        $project:{
            title:1,
            "author.first":1, //嵌套文档输出特别的内容
            "author.middle":{
                $cond:{
                    if:{$eq:["","$author.middle"]},
                    then:"$$REMOVE",
                    else:"$author.middle"
                }
            }
        }
      }
      ])

最终效果:

/* 1 */
{
  "_id" : 1,
  "title" : "abc123",
  "author" : {
    "first" : "aaa"
  }
}

/* 2 */
{
  "_id" : 2,
  "title" : "Baked Goods",
  "author" : {
    "first" : "abc"
  }
}

/* 3 */
{
  "_id" : 3,
  "title" : "Ice Cream Cakes",
  "author" : {
    "first" : "abc",
    "middle" : "mmm"
  }
}

6)只含有嵌套文档的指定字段

     db.testproject1.insert([
        { _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.testproject1.aggregate([
     {
        $project:{"stop.title":1}
     }
     ])

返回结果:

{ "_id" : 1, "stop" : { "title" : "book1" } }
{ "_id" : 2, "stop" : [ { "title" : "book2" }, { "title" : "book3" } ] }

对字段进行计算
插入数据:

     db.testproject2.insert([
     {
        "_id" : 1,
         title: "abc123",
         isbn: "0001122223334",
         author: { last: "zzz", first: "aaa" },
         copies: 5
     }
     ])
     db.testproject2.aggregate([
        {
            $project:{
                title:1,
                isbn:{
                    prefix:{$substr:["$isbn",0,3]},  //对字段进行处理(截取)
                    checkDigit: { $substr: [ "$isbn", 12, 1] }
                    },
                lastname:"author.last",
                copiesSold: "$copies"
            }
        }
     ])

返回结果:

{
  "_id" : 1,
  "title" : "abc123",
  "isbn" : {
    "prefix" : "000",
    "checkDigit" : "4"
  },
  "lastname" : "author.last",
  "copiesSold" : 5
}

投影出新的数组字段

db.testproject3.insert([
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "x" : 1, "y" : 1 }
]);
db.testproject3.aggregate([
    {
        $project:{
            myArray:["$x","$y"]
        }
    }
])
最终返回字段.png

如果数组中包含不存在的字段,则会返回null

$sort:排序,1升,-1降,sort一般在group后,也就是说得到结果在排序,如果先排序在分组没有意义。

    db.pts.aggregate([
        {
        $group:{
            "_id":"$school_id",
            "count":{"$sum":1}
            }  
        },  //先分组
        {
         $sort:{"count":-1}  //对分组内容进行排序
        }
     ])

$limit用来限制Mongodb聚合管道返回的文档数,相当于mysql中的limit m,不能设置偏移量。

    db.pts.aggregate([
        {
            $limit:2  
        }
     ]);

$skip聚合管道中跳过指定数量的文档,并返回余下的文档。

    db.pts.aggregate([
        {
        $skip:2
        }
     ]);

skip(), limilt(), sort()三个放在一起执行的时候,执行的顺序是先 sort(), 然后是 skip(),最后是显示的 limit()。

$unwind:将文档中某一个数组类型字段拆分成多条,每条包含数组中的一个值。

    db.pts.aggregate([
        {
        $unwind:"$items"
        }
     ]);

$group将集合中的文档分组,可用于统计结果,其中_id固定写法。

    db.pts.aggregate([
        {
            $group:{
            "_id":"$school_id",  //以school_id的值分组
            "count":{"$sum":1}  //聚合函数,输出count(1)
            }
        }
     ]);

推荐阅读

https://www.cnblogs.com/duanxz/p/3600052.html

mongodb高级聚合查询

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • 一、MongoDB简介 1.概述 ​ MongoDB是一个基于分布式文件存储的数据库,由C++语言编写。旨在为WE...
    郑元吉阅读 976评论 0 2
  • MongoDB的聚合操作主要是对数据的批量处理。一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简...
    AaronSimon阅读 2,466评论 0 6
  • 关注公众号【Ccww笔记】,获取干货资料   MongoDB中聚合(aggregate) 操作将来自多个docu...
    Ccwwl阅读 1,572评论 0 2
  • 在mongodb中有时候我们需要对数据进行分析操作,比如一些统计操作,这个时候简单的查询操作(find)就搞不定...
    huan1993阅读 2,574评论 1 1
  • 简介 MongoDB 是一个基于分布式文件存储的NoSQL数据库 由C++语言编写,运行稳定,性能高 旨在为 WE...
    大熊_7d48阅读 36,778评论 1 9