1、聚合框架

使用聚会框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering)、 投射(projecting)、分组(grouping)、排序(sorting)、限制(limiting)和跳过(skipping)。
例如,有下面这样一个集合,里面有100W条数据,现在要找到重名人数最多的那个名字。

1
2
3
4
5
6
7
8
> db.user_list.findOne();
{
    "_id" : ObjectId("5e70f773172c18edcc026c97"),
    "i" : 0,
    "username" : "name43",
    "age" : 81,
    "created" : ISODate("2020-03-17T16:14:43.671Z")
}

实现方式如下:

1
2
3
4
5
> db.user_list.aggregate({"$project": {"username": 1}},
... {"$group": {"_id": "$username", "count": {"$sum": 1}}},
... {"$sort": {"count": -1}},
... {"$limit": 1})
{ "_id" : "name23", "count" : 10204 }

上面用到的操作符解释如下
①. {"$project": {"username": 1}}:将username从每个文档中投射出来。类似 MySQL 的 select field,可以通过指定"fieldname":1选择 需要投射的字段,或者通过指定"fieldname":0排除不需要的字段。执行完以后的结果只会在内存中存在,不会写入磁盘。
②. {"$group": {"_id": "$username", "count": {"$sum": 1}}}:这样就会按照 username 排序,某个 username 每出现一次,就会对这个username的 count 加1(联想一下 Linux 的 uniq -c,是不是有点像)。
③. {"$sort": {"count": -1}}:这个操作就是根据 count 字段进行降序排列。
④. {"$limit": 1}:这个操作就是将最终返回的结果限制为处理完以后结果中的第1个文档。

2、管道操作符

2.1 $match

$match 用户对文档进行筛选,之后就可以在筛选得到的文档子集上做聚合。
注意:不能在$match 中使用地理空间操作符。
建议:尽量将$match 放在管道的前面位置。这样可以快速将不需要的文档过滤掉,以减少管道的工作量,并且在投射和分组之前执行$match,查询可以使用索引。
例如查看名叫name43的人中,那个年龄的最多?

1
2
3
4
5
6
> db.user_list.aggregate({"$match": {"username": "name43"}},
... {"$project": {"age": 1}},
... {"$group": {"_id": "$age", "count": {"$sum": 1}}},
... {"$sort": {"count": -1}},
... {"$limit": 1})
{ "_id" : 81, "count" : 105 }

2.2 $project

使用$project可以从子文档中提取字段,可以重命名字段,还可以有一些其他的操作。

2.2.1 管道表达式

最简单的$project 表达式就是包含和排除,以及字段名称($fieldname)。

2.2.2 数学表达式

①. $add: [expr1[, expr2, ..., exprN]]:将接收到的参数相加

1
2
3
4
5
6
7
> db.user_list.aggregate({"$project": {"ageAddI": {"$add": ["$age", "$i"]},
... "i": 1, "age": 1, "_id": 0}},{"$limit": 5})
{ "i" : 0, "age" : 81, "ageAddI" : 81 }
{ "i" : 1, "age" : 5, "ageAddI" : 6 }
{ "i" : 2, "age" : 9, "ageAddI" : 11 }
{ "i" : 3, "age" : 40, "ageAddI" : 43 }
{ "i" : 4, "age" : 57, "ageAddI" : 61 }

②. $subtract: [expr1, expr2]:将接收到的两个表达式相减

1
2
3
4
5
6
7
> db.user_list.aggregate({"$project": {"ageSubI": {"$subtract": ["$age", "$i"]},
... "age": 1, "i": 1, "_id": 0}},{"$limit": 5})
{ "i" : 0, "age" : 81, "ageSubI" : 81 }
{ "i" : 1, "age" : 5, "ageSubI" : 4 }
{ "i" : 2, "age" : 9, "ageSubI" : 7 }
{ "i" : 3, "age" : 40, "ageSubI" : 37 }
{ "i" : 4, "age" : 57, "ageSubI" : 53 }

③. $multiply: [expr1[, expr2, ..., exprN]]:将接收到的表达式相乘

1
2
3
4
5
6
7
> db.user_list.aggregate({"$project": {"ageMultiI": {"$multiply": ["$age", "$i"]},
... "age": 1, "i": 1, "_id": 0}},{"$limit": 5});
{ "i" : 0, "age" : 81, "ageMultiI" : 0 }
{ "i" : 1, "age" : 5, "ageMultiI" : 5 }
{ "i" : 2, "age" : 9, "ageMultiI" : 18 }
{ "i" : 3, "age" : 40, "ageMultiI" : 120 }
{ "i" : 4, "age" : 57, "ageMultiI" : 228 }

④. $divide: [expr1, expr2]:将接收到的表达式相除

1
2
3
4
5
6
7
> db.user_list.aggregate({"$project": {"iDivAge": {"$divide": ["$i", "$age"]},
... "age": 1, "i": 1, "_id": 0}}, {"$limit": 5})
{ "i" : 0, "age" : 81, "iDivAge" : 0 }
{ "i" : 1, "age" : 5, "iDivAge" : 0.2 }
{ "i" : 2, "age" : 9, "iDivAge" : 0.2222222222222222 }
{ "i" : 3, "age" : 40, "iDivAge" : 0.075 }
{ "i" : 4, "age" : 57, "iDivAge" : 0.07017543859649122 }

当0做除数时是会报错的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
> db.user_list.aggregate({"$project": {"ageDivI": {"$divide": ["$age", "$i"]}, "age": 1, "i": 1, "_id": 0}},
... {"$limit": 5})
assert: command failed: { "errmsg" : "exception: can't $divide by zero", "code" : 16608, "ok" : 0 } : aggregate failed
Error: command failed: { "errmsg" : "exception: can't $divide by zero", "code" : 16608, "ok" : 0 } : aggregate failed
    at Error (<anonymous>)
    at doassert (src/mongo/shell/assert.js:11:14)
    at Function.assert.commandWorked (src/mongo/shell/assert.js:254:5)
    at DBCollection.aggregate (src/mongo/shell/collection.js:1278:12)
    at (shell):1:14
2020-03-18T10:48:43.118+0800 E QUERY    Error: command failed: { "errmsg" : "exception: can't $divide by zero", "code" : 16608, "ok" : 0 } : aggregate failed
    at Error (<anonymous>)
    at doassert (src/mongo/shell/assert.js:11:14)
    at Function.assert.commandWorked (src/mongo/shell/assert.js:254:5)
    at DBCollection.aggregate (src/mongo/shell/collection.js:1278:12)
    at (shell):1:14 at src/mongo/shell/assert.js:13

⑤. $mod: [expr1, expr2]:取模

1
2
3
4
5
6
7
> db.user_list.aggregate({"$project": {"iModAge": {"$mod": ["$i", "$age"]}, 
... "age": 1, "i": 1, "_id" : 0}},{"$limit": 5});
{ "i" : 0, "age" : 81, "iModAge" : 0 }
{ "i" : 1, "age" : 5, "iModAge" : 1 }
{ "i" : 2, "age" : 9, "iModAge" : 2 }
{ "i" : 3, "age" : 40, "iModAge" : 3 }
{ "i" : 4, "age" : 57, "iModAge" : 4 }

2.2.3 日期表达式

日期表达式只能对日期类型的字段进行日期操作,不能对数值类型的字段做日期操作,相关表达式主要有这些: $year,$month,$week,$dayOfMonth,$dayOfWeek,$dayOfYear,$hour,$minute,$second。

2.2.4 字符串表达式

①. $substr: [expr, startOffset, numToReturn]:字符串截取
②. $concat: [expr1[, expr2, ..., exprN]]:字符串拼接
③. $toLower: expr:字符串转小写
④. $toUpper: expr:字符串转大写

2.2.5 逻辑表达式

①. $cmp: [expr1, expr2]:比较,expr1等于expr2返回0,大于返回一个正数,小于返回一个负数
②. $strcasecmp: [string1, string2]:比较,区分大小写,只对罗马字符组成的字符串有效
③. $eq/$ne/$gt/$gte/$lt/$lte: [expr1, expr2]:比较,返回 true 或者 false
④. $and: [expr1[, expr2, ..., exprN]]:所有表达式都是 true,返回 true,否则返回 false
⑤. $or: [expr1[, expr2, ..., exprN]]:只要有任意表达式是 true 就返回 true,否则返回 false
⑥. $not: expr:取反
⑦. $cond: [booleanExpr, trueExpr, falseExpr]:相当于 PHP 的三元运算符 $a ? $a : 0
⑧. $ifFull: [expr, replacementExpr]:如果expr是 null,就返回replacementExpr,否则返回expr

2.3 $group

2.3.1 分组操作符

分组操作符允许对每个分组进行计算,得到相应的结果。

2.3.2 算数操作符

①. $sum: value:对于分组中的每一个文档,将 value 与计算结果相加。
②. $avg: value:返回每个分组的平均值。

2.3.3 极值操作符

①. $max:expr:返回分组内的最大值。
②. $mix: expr:返回分组内的最小值。
③. $first: expr:返回分组内的第一个值。
④. $last: expr:返回分组内的最后一个值。

2.3.4 数组操作符

①. $addToSet: expr:去重,添加元素到数组中
②. $push: expr:直接添加元素到数组中

2.3.5 分组行为

$group 必须要等收到所有文档之后,才能对文档进行分组,然后才能将各个分组发送给管道的下一个操作符。

2.4 $unwind

可以将数组中的每一个值拆分成单独的文档。
原始数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
> db.posts.findOne();
{
    "_id" : ObjectId("5e704e860a1ed3c6d7339065"),
    "title" : "post title",
    "content" : "帖子内容",
    "author" : {
        "name" : "demojoe",
        "email" : "joe@example.com"
    },
    "comments" : [
        {
            "name" : "joe",
            "email" : "joe@example",
            "content" : "评论一下",
            "like_count" : 1,
            "comment_id" : 123
        },
        {
            "name" : "jode",
            "email" : "jode@example",
            "content" : "再评论一下啊",
            "comment_id" : 124,
            "like_count" : 2
        }
    ]
}

拆分以后的效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
> db.posts.aggregate({"$unwind": "$comments"}).pretty();
{
    "_id" : ObjectId("5e704e860a1ed3c6d7339065"),
    "title" : "post title",
    "content" : "帖子内容",
    "author" : {
        "name" : "demojoe",
        "email" : "joe@example.com"
    },
    "comments" : {
        "name" : "joe",
        "email" : "joe@example",
        "content" : "评论一下",
        "like_count" : 1,
        "comment_id" : 123
    }
}
{
    "_id" : ObjectId("5e704e860a1ed3c6d7339065"),
    "title" : "post title",
    "content" : "帖子内容",
    "author" : {
        "name" : "demojoe",
        "email" : "joe@example.com"
    },
    "comments" : {
        "name" : "jode",
        "email" : "jode@example",
        "content" : "再评论一下啊",
        "comment_id" : 124,
        "like_count" : 2
    }
}

2.5 $sort

顾名思义,就是排序。可以根据任何字段进行排序,排序方向可以是1(升序)或者-1(降序)。

2.6 $limit

接收数字 n,限制返回结果集中的前 n 个文档。

2.7 $skip

也是接收一个数字 n,表示跳过结果集中的前 n 个文档。

3、聚合命令

3.1 count

顾名思义,获取集合中文档的数量。

3.2 distinct

找出给定键的所有不同值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
> db.runCommand({"distinct": "user_list", "key": "age"});
{
    "values" : [
        81,
        5,
        9,
        40,
        57
    ],
    "stats" : {
        "n" : 1000000,
        "nscanned" : 0,
        "nscannedObjects" : 1000000,
        "timems" : 417,
        "planSummary" : "COLLSCAN"
    },
    "ok" : 1
}

3.3 group

分组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
> db.runCommand({"group": {
... "ns": "user_list",
... "key": "username",
... "initial": {"age": 0},
... "$reduce" : function(doc, prev) {
... if (doc.age > prev.age) {
... prev.age = doc.age;
... prev.username=doc.username; }
... }}})
{
    "retval" : [
        {
            "age" : 119,
            "username" : "name60"
        }
    ],
    "count" : NumberLong(1000000),
    "keys" : NumberLong(1),
    "ok" : 1
}

运行之后才发现,不是我理解的那个结果。