后续会更新出更多关于 aggregate 的内容。基于 mongodb 4.2 版本。
区分我的术语
- 属性值为数组内嵌文档即案例中的 "races" 对应的属性值
- 属性值为内嵌文档即案例中的 "raletion" 对应的属性值
{
"_id" : ObjectId("5f92760d1e17044b8df04955"),
"countryName" : "艾欧尼亚",
"introduction" : "初生之土",
"description" : "初生之土,艾欧尼亚。这片岛屿大陆上充盈着自然之美和原生魔法。居民散布于各个行省,崇尚灵性,追求和谐的存世之道。艾欧尼亚悠久的中立姿态随着诺克萨斯的入侵而被打破——野蛮的侵占迫使艾欧尼亚重新审视自己在这个世界上的位置,未来的道路也变得扑朔迷离。",
"area" : NumberDecimal("200001"),
"createAt" : ISODate("2020-10-23T06:19:57.386+0000"),
"races" : [
{
"_id" : ObjectId("5f92760d1e17044b8df0494c"),
"name" : "均衡寺院",
"description" : "圣所",
"talent" : "均衡教派的大本营。他们立誓守护艾欧尼亚的平衡。",
"createAt" : ISODate("2020-10-23T06:19:57.385+0000"),
"peoples" : [
{
"_id" : ObjectId("5f92760d1e17044b8df04948"),
"status" : "壮年",
"name" : "离群之刺",
"nickname" : "阿卡丽",
"age" : NumberInt(38),
"createAt" : ISODate("2020-10-23T06:19:57.385+0000")
},
{
"_id" : ObjectId("5f92760d1e17044b8df04949"),
"status" : "壮年",
"name" : "狂暴之心",
"nickname" : "凯南",
"age" : NumberInt(38),
"createAt" : ISODate("2020-10-23T06:19:57.385+0000")
}
]
},
{
"_id" : ObjectId("5f92760d1e17044b8df0494e"),
"name" : "帕拉斯神庙",
"description" : "圣所",
"talent" : "暗裔韦鲁斯在此被监禁了数百年,但封印已经破除……",
"createAt" : ISODate("2020-10-23T06:19:57.386+0000"),
"peoples" : [
{
"_id" : ObjectId("5f92760d1e17044b8df0494d"),
"status" : "壮年",
"name" : "惩戒之剑",
"nickname" : "维鲁斯",
"age" : NumberInt(38),
"createAt" : ISODate("2020-10-23T06:19:57.385+0000")
}
]
}
],
"raletion": {
"description": "lol世界观",
"age": NumberInt(11)
}
}
aggregate
聚合函数提供了多个管道(Pipeline)操作,我常用的具体如下:
| 阶段(Stage) | 描述 |
|---|---|
| $match | 将符合查询条件的文档传递到下一个管道 |
| $project | 可以重塑管道中每个文档的每个属性(只要你愿意,所以这个阶段非常的强大) |
| $lookup | 类似与 mysql 中 lefe join(尽量不要这样做,好的文档设计会为你减少很多表关联。同时会被动要用更多的复杂mongo函数去维护) |
| $group | 对所有文档进行分组,类似 mysql 中的 group by ,相同的 groupId 应该怎么取唯一的一个 name |
| $unwind | 因为主张非范式化的设计,所以一个文档会被设计的非常的复杂。看案例。 |
| $count | 返回聚合管道此阶段的文档的总数。即过了此阶段,管道中只剩下了数量 |
| $sort | 按照指定字段的升序降序排序 |
| $limit | 返回前n条文档到下一个管道中 |
| $skip | 跳过前n条文档 |
所有的管道操作能重复使用。仅限一些较实用且生僻的用法。属性名即文档的 key、键名。
$match
- 文档中属性值和值的比较
// 1. $eq, $ne, $in 等于不等于
db.test_collection.aggregate([
{$match: {"countryName" : "艾欧尼亚"}},
{$match: {"countryName" : {$eq: "艾欧尼亚"}}},
{$match: {"countryName" : {$ne: "abc"}}}, // 不等于
{$match: {"countryName" : {$in: ["abc", "def"]}}}
])
// 2. $gt, $gte, $lt, $lte 大于小于
db.test_collection.aggregate([
{$match: {"countryName" : {$gt: "abc", $lt: "def"}}} // 1 < a < 2 形式,也可以只要一个即 1 < a
])
// 3. $and, $or
db.test_collection.aggregate([
{$match: {$and: [{"countryName" : "艾欧尼亚"}, {"introduction" : "初生之土"}]}} // $or 与之相同用法
])
// 4. $expr, $ifNull
db.test_collection.aggregate([
{$match: {$expr: {$eq: ['$countryName', '$introduction']}}}, // 这里也可以用上 $gt, $lt, $ne
{$match: {$expr: {$eq: [{$ifNull: ['$countryName', '我是默认值1']}, {$ifNull: ['$introduction', '我是默认值2']}]}}}
])
$ 符号后接字符 $countryName 表示引用函数或者属性名所对应的值。
$expr:可以用到上面的$eq, $ne, $gt, $lt, $and, $or等操作符,表示用于同一文档的不同属性值进行比较。仅用$eq操作符时可以用上索引。
$ifNull:如果某个值不存在或者为null则输出默认值。 mongodb 是松散结构非常的自由,不用$ifNull限制会导致某些属性值不存在输出null导致和预期结果不一致。
- 内嵌文档作为查询条件
// 内嵌文档的比较,是从属性名,属性名顺序,属性名的值进行比较。
let obj = {
"name": "abc",
"age": NumberInt(1)
}
db.test_collection.aggregate([
{$match: {"races": obj}}
])
// 或者用其属性名.属性名
db.test_collection.aggregate([
{$match: {"races.b.c.d": "被匹配的值"}}
])
- 内嵌数组文档的二级文档作为查询条件
// 直接进行匹配,忽略数组下标。能够匹配到有这个属性值的文档。
// 但是有时候我们只想要子文档 name == '均衡寺院' 的子文档,不需要其他的子文档。
// 就需要在 $project 管道中 $elemMatch。即在 $match 和 $project 都能用 $elemMatch,且效果不一样
db.test_collection.aggregate([
{$match: {"races.name": "均衡寺院"}}
])
// $elemMatch 操作符
db.test_collection.aggregate([
{$match: {"races": {$elemMatch: {"name": "均衡寺院"}}}}
])
// 或者直接使用 数组下标匹配
db.test_collection.aggregate([
{$match: {"races.0.name": "均衡寺院"}}
])
$group
mysql 的 group by
$group, $first, $last, $sum, $max, $min, $avg, $push, $addToSet
- $group
db.test_collection.aggregate([
{
$group: {
// 这里的写法有很多。单个属性分组 "_id": "$rank"
// 多个:"_id": ["$rank", "$age"]
// 或者 "_id": {"rank": "$rank", "age": "$age"}
"_id": ["$rank", "$age"],
// 对于相同的分组去唯一的一个值可以用 $first, $last, $max, $mix,看心情
"name": {$first: '$name'},
"rank": {$first: "$rank"},
"age": {$first: "$age"},
count: {$sum: 1} // 统计每个分组的数量
}
}
])
_id 是必须存在的即:如何多个属性分组都必须在_id后面。单个属性分组:"_id": "$rank", 多个属性分组:"_id": ["$rank", "$age"]即_id属性值变成一个数组,多个:或者 "_id": {"rank": "$rank", "age": "$age"}即_id属性值变成一个内嵌文档。
-
push
指定分组后唯一的属性值可以用
$first,$last,$sum,$max,$min,$avg,$push,$addToSet,现在你也可以选择把分组后重复的值放在一个数组里(既可以是数组也可以是数组内嵌文档对象)
db.test_collection.aggregate([
{
$group: {
"_id": ["$rank"],
"name": {$first: '$name'},
"rank": {
$push: { // 可以用 $addToSet 代替,保证插入元素的不重复
"rank": "$rank",
"age": "$age"
}
},
"age": {$first: "$age"}
}
}
])
$project
个人认为因为 mongodb 在推荐好的非范式数据库设计能带来更优的性能,mongodb 也提供了非常好的性能,但是额外带来的是非范式文档结构维护难度的增加。毕竟mysql不用担心有一个属性值居然是一个数组内嵌文档。
- 那些显示那些不显示
// mongodb 默认显示所有字段,当你显式的声明某个字段应该显示即 "age": 1 抛弃了其他未声明为 'true' 的字段(但是_id)需要显示的声明不显示
// 反之显式的声明某个字段不显示不影响其他字段显示不显示。
db.test_collection.aggregate([
{
$project: {
"_id": 0,
"age": 1,
"newAge": "$age", // 重命名
"newField": "我是手动新增字段" // 手动增加一个字段
}
}
])
// if else 支持
db.inventory.aggregate([
{
$project:
{
item: 1,
discount:
{
$cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
}
}
}
])
// case when 支持
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."
}
}
}
}
])
$project 阶段对数组的操作
mysql 中的 group_concat
-
mysql 中的 group_concat 应该怎么实现,包括 distinct orderBy?
在
$group阶段用$push/$addToSet可以把用一个组某个属性值都存在一个数组中。然后$reduce会应用于数组中每个元素并压缩成一个值。$indexOfArray:判断内嵌文档对象在数组中的下标位置。$reverseArray:反转数组中的顺序。如果只是单纯的想对数组内嵌文档排序,推荐用updateOne()方法,$push一个[]空数组并运用其中的$sort方法即可对数组内嵌文档排序。实现查询出的数组内嵌文档按指定排序输出。即查询前先执行一条push空数组命令对数组内容排序,然后执行find语句$map:和$reduce对应的就是$map,并不会压缩对象,而是会遍历每个数组内嵌文档。对内嵌文档做一些增减操作。
{
$reduce: {
input: <array>, // [1, 2, 3] 或者数组属性值 "$var"
initialValue: <expression>, // 这个初始值只能被应用到一次
in: <expression> // 做一些操作
}
}
// 完成 group_concat 中字符串拼接并且 orderBy
db.test_collection.aggregate([
{$match: {"countryName" : "艾欧尼亚"}},
{
$sort: {
"orderBy": 1 // 实现 group_concat 中 orderBy 的功能
}
},
{
$project: {
"racesList": {
$reduce: {
input: {$reverseArray: "$races"}, // 反转数组顺序,至于为什么这里需要反转顺序,可以实测一番mongodb读取数组内容下标从 0 开始还是 length -1 开始
initialValue: "",
in: {
$cond: { // if else 支持
if: {$eq: [{$indexOfArray: ["$races", "$$this"]}, NumberInt(0)]},
then: {$concat: ["", "$$this.name", "$$value"]}, // 不知道为啥,这个 "$$value" 好像必须要出现。不然达不到效果
else: {$concat: [",", "$$this.name", "$$value"]}
}
}
}
}
}
}
])
// 结果如下
//{
// "_id" : ObjectId("5f92760d1e17044b8df04955"),
// "racesList" : "均衡寺院,帕拉斯神庙,希拉娜修道院"
//}
滤掉部分数组内嵌文档
当你想改这个东西的时候即你显示的声明了想要显示这个字段,你还需要显示的声明其他本不需要显示声明的字段。例如:你必须强制的把所有想要显示的字段全部显示一遍,对字段很多的 vo 简直是噩梦。 这时可以试试这个管道操作:
$addFields,只单纯的增加字段,并不会影响原先的映射字段。
-
我查出来了一条文档,有一个属性是数组内嵌文档,怎么过滤掉部分数组内嵌文档即{"race.name": "均衡寺院"}
- 有且仅返回一条符合匹配条件的数组内嵌文档
$elemMatch。在findOne(query, project)中的project
// findOne(query, project) 中 仅返回被匹配的第一条。其属性还是数组内嵌文档 db.test_collection.findOne( {"countryName" : "艾欧尼亚"}, { "countryName": 1, "introduction": 1, "description": 1, "area": 1, "createAt": 1, "races": {$elemMatch: {"name": "均衡寺院"}} // 就是因为这个,导致上面的都必须显示的声明出来。字段一多就特别麻烦 } )- 按照匹配条件匹配一条至多条
$filter
// 按照匹配条件匹配一条至多条。其属性还是数组内嵌文档 db.test_collection.aggregate([ { $match: {"countryName": "艾欧尼亚"} }, { "countryName": 1, "introduction": 1, "description": 1, "area": 1, "createAt": 1, $project: { "races": { $filter: { input: "$races", as: "item", cond: { $eq: ["$$item.name", "均衡寺院"] // 注意这里不是大括号而是中括号 } } } } } ])- 将属性值为数组内嵌文档变成内嵌文档
$unwind
db.test_collection.aggregate([ { $match: {"countryName": "艾欧尼亚"} }, {$unwind: "$races"}, { $match: {"races.name": "均衡寺院"} } ])结果如下:
- 有且仅返回一条符合匹配条件的数组内嵌文档