mongoDB中聚合(aggregate)的具体使用

最近在学习mongoDB的使用,本文来介绍一下其中aggregate的具体使用

先来看一个分组的例子,本例中$group是一个管道操作符,获得的结果可以接着输出到下一个管道,而内部的$sum是一个表达式操作符。

用$group 举个例子

将document分组,用作统计结果
```
    db.Ubisoft.aggregate([ // aggregate方法接收的是一个数组
        {
            $group: {
                _id: '$time', 
                num: {$sum: 1}
            }
        }
    ])
    // 这里的_id字段表示你要基于哪个字段来进行分组(即制定字段值相同的为一组),这里的$time就表示要基于time字段来进行分组

    // 下面的num字段的值$sum: 1表示的是获取满足time字段相同的这一组的数量乘以后面给定的值(本例为1,那么就是同组的数量)。
```

那么看完这个例子之后,mongoDB中还有其他的一些管道操作符和表达式操作符:

管道操作符

常用管道 含义
$group 将collection中的document分组,可用于统计结果
$match 过滤数据,只输出符合结果的文档
$project 修改输入文档的结构(例如重命名,增加、删除字段,创建结算结果等)
$sort 将结果进行排序后输出
$limit 限制管道输出的结果个数
$skip 跳过制定数量的结果,并且返回剩下的结果
$unwind 将数组类型的字段进行拆分

表达式操作符

常用表达式 含义
$sum 计算总和,{$sum: 1}表示返回总和×1的值(即总和的数量),使用{$sum: '$制定字段'}也能直接获取制定字段的值的总和
$avg 平均值
$min min
$max max
$push 将结果文档中插入值到一个数组中
$first 根据文档的排序获取第一个文档数据
$last 同理,获取最后一个数据

了解完这些操作符之后,继续拿$group来试试看:
我们现在有一个名为Ubisoft的一个collection,内部的文档为:

/* 1 */
{
    "_id" : ObjectId("5b0cf67270e4fa02d31de42e"),
    "name" : "rainbowSix Siege",
    "time" : 400.0
}

/* 2 */
{
    "_id" : ObjectId("5b0cf69270e4fa02d31de42f"),
    "name" : "Assassin's creed",
    "time" : 20.0
}

/* 3 */
{
    "_id" : ObjectId("5b0cf6ad70e4fa02d31de430"),
    "name" : "ghost Recon",
    "time" : 0.0
}

/* 4 */
{
    "_id" : ObjectId("5b0d14c870e4fa02d31de436"),
    "name" : "farCry",
    "time" : 0.0
}

我们现在来试试其他的表达式操作符:

   db.Ubisoft.aggregate([
       {
           $group: {
               _id: '$time',
               gameName: {$push: '$name'}
           }
       }
   ]) 

返回结果为:

/* 1 */
{
    "_id" : 20.0,
    "gameName" : [ 
        "Assassin's creed"
    ]
}

/* 2 */
{
    "_id" : 0.0,
    "gameName" : [ 
        "ghost Recon", 
        "farCry"
    ]
}

/* 3 */
{
    "_id" : 400.0,
    "gameName" : [ 
        "rainbowSix Siege"
    ]
}

可以看到time字段相同的document被分为了一组,而且使用$push表达式,将我们制定的document的name字段的值也放到了一个数组中作为我们在mongoDB语句中制定的gameName的值。

另外$group中可以制定_id:null, 即可以把所有的document分为一组,可以用于计算平均值之类的操作

我们可以用$指定字段来表示选定的document的field,另外可以使用$$ROOT来表示选定的document的所有内容(例如:chosenDocument: {$push: '$$ROOT'}

上述例子基本介绍了表达式操作符的用法。

接着来看$match

$match

    db.Ubisoft.aggregate([
        {
            $match: {
                time: {$gte: 20} //选取time字段 >=20的document
            }
        }
    ])

这就拿到了所有time>=20的document,然后可以通过再接个管道来进行其他操作,比如说我们再接一个$group来进行分组,显示筛选出来的所有time>=20的document的个数。

    db.Ubisoft.aggregate([
        {
            $match: {
                time: {$gte: 20}
            }
        },
        {
            $group: {
                _id: null, // _id: null表示全选
                totalNum: {$sum: 1}
            }
        }
    ])

输出结果为:

/* 1 */
{
    "_id" : null,
    "totalNum" : 2.0
}

可以看到time>=20的document的个数为2

$project 投影

修改输入文档的结构(例如重命名,增加、删除字段,创建结算结果等)

$project和直接使用find()的写法一样:

db.Ubisoft.aggregate([
    {
        $project: {
            _id: 0,  //不显示_id字段
        }
    }
])

和我们直接写db.Ubisoft.find({},{'_id': 0})写法一样

输出结果为:

/* 1 */
{
    "name" : "rainbowSix Siege",
    "time" : 400.0
}

/* 2 */
{
    "name" : "Assassin's creed",
    "time" : 20.0
}

/* 3 */
{
    "name" : "ghost Recon",
    "time" : 0.0
}

/* 4 */
{
    "name" : "farCry",
    "time" : 0.0
}

可以看到没有_id字段了。

那么我们现在如果想拿到所有time>=20的document的name字段的话,可以把管道搭配起来用:

db.Ubisoft.aggregate([
    {
        $match: {
            time: {$gte: 20}
        }
    },
    {
        $project: {
            _id: 0, // _id不显示
            name: 1 // name是要显示的
        }
    },
    {
        $group: {
            _id: null,
            name: {$push: '$name'}
        }
    }
])

输出结果为:

/* 1 */
{
    "_id" : null,
    "name" : [ 
        "rainbowSix Siege", 
        "Assassin's creed"
    ]
}

$sort

$sort和我们find()中排序的写法也是一样的。

现在我们想将所有的document按照time降序来排列的话:

db.Ubisoft.find().sort({time: -1})写法是一样的:

db.Ubisoft.aggregate([
    {
        $sort: {
            time: -1
        }
    }
])

同理,$sort也可以和其他管道搭配使用

$limit $skip

和limit()以及skip()的写法也是一样的。

db.Ubisoft.find().skip(1).limit(2)

使用聚合可以写成:

db.Ubisoft.aggregate([
    {
        $skip: 1
    },
    {
        $limit: 2
    }
])

limit和skip搭配使用可以达到分页的效果。

注意先写skip在写limit

$unwind

$unwind管道可以document中的数组类型的字段进行拆分,每条包含数组中的一个值。

  • 基本使用

在Ubisoft这个集合里新增如下一条document:

/* 5 */
{
    "_id" : ObjectId("5b0e242ed85f6f9cc56da7cc"),
    "name" : "gameList",
    "list" : [ 
        "dota2", 
        "csgo", 
        "ow"
    ]
}

我们针对这个document中的list字段来进行$unwind

db.Ubisoft.aggregate([
    {
        $unwind: '$list' // 指定list字段
    }
])

输出结果为:

/* 1 */
{
    "_id" : ObjectId("5b0e242ed85f6f9cc56da7cc"),
    "name" : "gameList",
    "list" : "dota2"
}

/* 2 */
{
    "_id" : ObjectId("5b0e242ed85f6f9cc56da7cc"),
    "name" : "gameList",
    "list" : "csgo"
}

/* 3 */
{
    "_id" : ObjectId("5b0e242ed85f6f9cc56da7cc"),
    "name" : "gameList",
    "list" : "ow"
}

可以看到unwind是将文档中的数组字段进行拆分,如果有其他文档的list字段也是数组,也会一并拆分。

  • 特殊情况下的unwind(空数组,null,非数组,无指定字段)

针对特殊情况,新建一个colletion,内容为:

/* 1 */
{
    "_id" : ObjectId("5b0e27fdd85f6f9cc56da7ce"),
    "list" : null
}

/* 2 */
{
    "_id" : ObjectId("5b0e2827d85f6f9cc56da7cf"),
    "list" : []
}

/* 3 */
{
    "_id" : ObjectId("5b0e2834d85f6f9cc56da7d0"),
    "list" : "notArray"
}

/* 4 */
{
    "_id" : ObjectId("5b0e2844d85f6f9cc56da7d1")
}

来进行$unwind

db.unwind.aggregate([
    {
        $unwind: '$list'
    }
])

输出结果为:

/* 1 */
{
    "_id" : ObjectId("5b0e2834d85f6f9cc56da7d0"),
    "list" : "notArray"
}

可以看到[],null,以及无指定字段的数据都丢失了,

为了不丢失数据,我们可以写成:

db.unwind.aggregate([
    {
        $unwind: {
            path: '$list', // path是指定字段
            preserveNullAndEmptyArrays: true //该属性为true即保留
        }
    }
])

这次输出结果就保留了null以及空数组,值得关注的就是preserveNullAndEmptyArrays这个属性,为true的时候就保留。

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

推荐阅读更多精彩内容