MongoDB聚合操作详解

文章目录

  • 聚合操作
    • 聚合管道
      • 管道(Pipeline)和阶段(Stage)
      • 常用的聚合阶段运算符
      • 准备数据集,执行脚本
      • $project
      • $match
      • $count
      • $group
      • $unwind
      • $limit
      • $skip
      • $sort
      • $lookup
      • 聚合操作案例1
      • 聚合操作案例2
    • 聚合优化
      • 执行顺序
      • 内存排序
    • 整合Springboot进行聚合操作
      • 案例一
      • 案例二
      • 案例三

聚合操作

聚合操作允许用户处理多个文档并返回计算结果。

聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。我们主要掌握其中的聚合管道方式即可。

  • 单一作用聚合

    提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档。

    MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.countDocument(), db.collection.distinct() 这类单一作用的聚合函数。

  • 聚合管道

    一个数据聚合的框架,模型基于数据处理流水线的概念。文档进入多级管道,将文档转换为聚合结果。

  • MapReduce

操作具有两个阶段:处理每个文档并向每个输入文档发射一个或多个对象的map阶段,以及reduce组合map操作的输出阶段。

从MongoDB 5.0开始,map-reduce操作已被弃用。聚合管道比映射-reduce操作提供更好的性能和可用性。



MongoDB 6.0在原有聚合功能的基础上,推出了如下新特性以及优化项:

  • 分片集群实例支持 l o o k u p 和 lookup和 lookupgraphLookup。
  • 改进$lookup对JOINS的支持。
  • 改进$graphLookup对图遍历的支持。
  • 提升$lookup性能,部分场景中性能提升可达百倍。



聚合管道

管道(Pipeline)和阶段(Stage)

整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的, 每个管道:

  • 接受一系列文档(原始数据);
  • 每个阶段对这些文档进行一系列运算;
  • 结果文档输出给下一个阶段;

在这里插入图片描述



聚合管道操作语法:

pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
  • pipelines 一组数据聚合阶段。除$out、$Merge、$geonear阶段之外,每个阶段都可以在管道中出现多次。
  • options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时间、读写策略、强制索引等等



案例:先匹配出status字段为A的文档,再通过_id进行分组,并对amount字段进行求和,将求和的结果取一个别名total

在下面分组的阶段中,我们的字段名都是这种格式"$字段名"

在这里插入图片描述



常用的聚合阶段运算符

SQL 聚合映射图表

文档:Aggregation Pipeline Stages — MongoDB Manual

阶段运算符描述SQL等价运算符
$match筛选条件WHERE
$project投影AS
$lookup左外连接LEFT OUTER JOIN
$sort排序ORDER BY
$group分组GROUP BY
$skip + $limit分页
$unwind展开数组把多个值展开为多个文档
$graphLookup图搜索
f a c e t / facet/ facet/bucket分面搜索



聚合表达式

获取字段信息

$<field>  : 用 $ 指示字段路径
$<field>.<sub field>  : 使用 $  和 .  来指示内嵌文档的路径

常量表达式

$literal :<value> : 指示常量 <value>

系统变量表达式

$$<variable>  使用 $$ 指示系统变量
$$CURRENT  指示管道中当前操作的文档



准备数据集,执行脚本

var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<50;i++){var typeIdx = Math.floor(Math.random()*types.length);var tagIdx = Math.floor(Math.random()*tags.length);var tagIdx2 = Math.floor(Math.random()*tags.length);var favCount = Math.floor(Math.random()*100);var username = "xx00"+Math.floor(Math.random()*10);var age = 20 + Math.floor(Math.random()*15);var book = {title: "book-"+i,type: types[typeIdx],tag: [tags[tagIdx],tags[tagIdx2]],favCount: favCount,author: {name:username,age:age}};books.push(book)
}
db.books.insertMany(books);

为接下来的测试案例,可以直接在mondosh命令行执行,当然也可以创建js文件,通过db.load()方式执行



在这里插入图片描述



插入之后的数据
在这里插入图片描述



$project

投影操作, 将原始字段投影成指定名称,也就是mysql中的字段别名功能。 如将集合中的 title 投影成 name

db.books.aggregate([{ $project: { name: "$title" } }])

在这里插入图片描述



$project 可以灵活控制输出文档的格式,也可以剔除不需要的字段

db.books.aggregate([{$project: {_id: 0,author: 1,type: 1}}
])

在这里插入图片描述



从嵌套文档中排除字段

db.books.aggregate([{$project: {_id: 0,"author.name": 1,type: 1}}
])# 或者使用下面这种写法
db.books.aggregate([{$project: {_id: 0,type: 1,author: { name: 1 }}}
])



$match

$match用于对文档进行筛选,之后可以在得到的文档子集上做聚合。也就是类似于Mysql的where功能

在实际应用中尽可能将$match放在管道的前面位置

# 查找type字段为novel的文档
db.books.aggregate([{$match: {type: "novel"}}
])

在这里插入图片描述



筛选管道操作和其他管道操作配合时候时,尽量放到开始阶段,这样可以减少后续管道操作符要操作的文档数,提升效率

# 先查询type字段为novel的文档,然后再投影,只显示某些字段
db.books.aggregate([{$match: {type: "novel"}},{$project: {_id:0, title:1, type:1, favCount:1}}
])



$count

计数并返回与查询匹配的结果数

# 先查询type字段为novel的文档,然后再统计个数,并将该值分配给type_count
db.books.aggregate([{$match: {type: "novel"}},{$count: "type_count"}
])# 执行结果
[ { type_count: 9 } ]



$group

官方文档

按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段

使用 $group 管道阶段中的 _id 字段来设置组键。也就是说我想按照什么来进行分组就需要在_id中设置

# 标准格式
{$group:{_id: <expression>, // Group key<field1>: { <accumulator1> : <expression1> },...}}
  • _id字段是必填的;但是,可以指定_id值为null来为整个输入文档计算累计值。
  • <field1>可选。使用累加器操作符
  • <accumulator> 操作符必须是累加器操作符之一



常用的accumulator操作符如下所示

名称描述类比sql
$avg计算均值avg
$first返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。limit 0,1
$last返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。-
$max根据分组,获取集合中所有文档对应值得最大值。max
$min根据分组,获取集合中所有文档对应值得最小值。min
$push将指定的表达式的值添加到一个数组中。-
$addToSet将表达式的值添加到一个集合中(无重复值,无序)。-
$sum计算总和sum
$stdDevPop返回输入值的总体标准偏差(population standard deviation)-
$stdDevSamp返回输入值的样本标准偏差(the sample standard deviation)-



$group阶段的内存限制为100M。

默认情况下,如果stage超过此限制, g r o u p 将产生错误。但是,要允许处理大型数据集,请将 a l l o w D i s k U s e 选项设置为 t r u e 以启用 group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用 group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用group操作以写入临时文件。



# book的数量,收藏总数和平均值  这里就不需要进行分组了
db.books.aggregate([{$group: {_id: null,bookcountSum: { $sum: 1 },favCountSum: { $sum: "$favCount" },favCountAvg: { $avg: "$favCount" }}}
])# 执行结果
[{_id: null,bookcountSum: 50,favCountSum: 2349,favCountAvg: 46.98}
# 统计每个作者的book收藏总数
db.books.aggregate([{$group: {_id: "$author.name",bookFavSum: { $sum: "$favCount" }}}
])# 执行结果
[{ _id: 'xx000', bookFavSum: 373 },{ _id: 'xx004', bookFavSum: 173 },{ _id: 'xx009', bookFavSum: 313 },{ _id: 'xx001', bookFavSum: 143 },{ _id: 'xx002', bookFavSum: 377 },{ _id: 'xx005', bookFavSum: 245 },{ _id: 'xx003', bookFavSum: 137 },{ _id: 'xx008', bookFavSum: 187 },{ _id: 'xx006', bookFavSum: 198 },{ _id: 'xx007', bookFavSum: 203 }
]
# 统计每个作者的每本book的收藏数
db.books.aggregate([{$group: {_id: {name: "$author.name",book: "$title"},bookFavSum: {$sum: "$favCount"}}}
])# 执行结果
[{ _id: { name: 'xx001', book: 'book-0' }, bookFavSum: 63 },{ _id: { name: 'xx009', book: 'book-8' }, bookFavSum: 62 },{ _id: { name: 'xx002', book: 'book-22' }, bookFavSum: 39 },{ _id: { name: 'xx008', book: 'book-12' }, bookFavSum: 37 },{ _id: { name: 'xx006', book: 'book-33' }, bookFavSum: 9 },{ _id: { name: 'xx002', book: 'book-7' }, bookFavSum: 69 },{ _id: { name: 'xx005', book: 'book-38' }, bookFavSum: 27 },{ _id: { name: 'xx004', book: 'book-23' }, bookFavSum: 85 },{ _id: { name: 'xx007', book: 'book-34' }, bookFavSum: 26 },{ _id: { name: 'xx004', book: 'book-21' }, bookFavSum: 78 },{ _id: { name: 'xx000', book: 'book-29' }, bookFavSum: 54 },{ _id: { name: 'xx006', book: 'book-40' }, bookFavSum: 93 },{ _id: { name: 'xx000', book: 'book-27' }, bookFavSum: 93 },{ _id: { name: 'xx002', book: 'book-24' }, bookFavSum: 63 },{ _id: { name: 'xx006', book: 'book-41' }, bookFavSum: 1 },{ _id: { name: 'xx005', book: 'book-45' }, bookFavSum: 56 },{ _id: { name: 'xx009', book: 'book-46' }, bookFavSum: 3 },{ _id: { name: 'xx007', book: 'book-28' }, bookFavSum: 10 },{ _id: { name: 'xx006', book: 'book-49' }, bookFavSum: 6 },{ _id: { name: 'xx006', book: 'book-17' }, bookFavSum: 32 }
]
# 每个作者book  的type合集
db.books.aggregate([{$group: {_id: {name: "$author.name"},typeList: {$addToSet: "$type"}}}
])# 执行结果为
[{_id: { name: 'xx000' },typeList: [ 'technology', 'literature', 'travel', 'novel' ]},{ _id: { name: 'xx004' }, typeList: [ 'technology', 'literature' ] },{_id: { name: 'xx009' },typeList: [ 'novel', 'technology', 'literature', 'sociality' ]},{_id: { name: 'xx001' },typeList: [ 'literature', 'sociality', 'technology' ]},
......
]



$unwind

官方文档

可以将数组拆分为单独的文档

# 您可以将数组字段路径传递给 $unwind。使用该语法时,如果字段值为 null、缺失或空数组,则 $unwind 不会输出文档。
# 如需指定字段路径,在字段名称前加上美元符号 $,并用引号括起来。
{ $unwind: <field path> }



v3.2+支持如下语法:

{$unwind:{#要指定字段路径,在字段名称前加上$符并用引号括起来。path: <field path>,#可选,一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。includeArrayIndex: <string>,  #可选,default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档preserveNullAndEmptyArrays: <boolean> } }



# 姓名为xx006的作者的book的tag数组拆分为多个文档
db.books.aggregate([{$match: {"author.name": "xx006"}},{$unwind: "$tag"}
])# 或者是下面这种写法
db.books.aggregate([{$match: {"author.name": "xx006"}},{$unwind: {path: "$tag",includeArrayIndex: "tagIndex",preserveNullAndEmptyArrays: true}}
])# 输出结果为
[
.....{_id: ObjectId("66a5b0106ae7536b7b29726f"),title: 'book-49',type: 'travel',tag: 'nosql',favCount: 6,author: { name: 'xx006', age: 30 },tagIndex: Long("0")  # tagIndex为我自定义的字段名 原数组索引下标},{_id: ObjectId("66a5b0106ae7536b7b29726f"),title: 'book-49',type: 'travel',tag: 'developer',favCount: 6,author: { name: 'xx006', age: 30 },tagIndex: Long("1")   # 原数组索引下标}
]
# 每个作者的book的tag合集
db.books.aggregate([{$unwind: {path: "$tag"}},{$group: {_id: "$author.name",types: {$addToSet: "$tag"}}}
])# 输出结果
[{_id: 'xx000',types: [ 'document', 'mongodb', 'nosql', 'popular', 'developer' ]},{_id: 'xx004',types: [ 'mongodb', 'nosql', 'developer', 'popular' ]},{_id: 'xx001',types: [ 'document', 'mongodb', 'nosql', 'popular', 'developer' ]},{_id: 'xx003',types: [ 'mongodb', 'developer', 'nosql', 'popular' ]},
......
]



案例

示例数据

# tag为[]空数组、没有tag字段、tag数组有值  三种情况
db.books.insert([
{"title" : "book-51","type" : "technology","favCount" : 11,"tag":[],"author" : {"name" : "hushang","age" : 28}
},{"title" : "book-52","type" : "technology","favCount" : 15,"author" : {"name" : "hushang","age" : 28}
},{"title" : "book-53","type" : "technology","tag" : ["nosql","document"],"favCount" : 20,"author" : {"name" : "hushang","age" : 28}
}])



测试

# 只要作者为hushang的文档  使用includeArrayIndex选项来输出数组元素的数组索引
db.books.aggregate([{$match: {"author.name": "hushang"}},{$unwind: {path: "$tag",includeArrayIndex: 'tagIndex'}}
])# 执行结果,只有两条数据
[{_id: ObjectId("66a5cf9fe78ef1c1a23525b3"),title: 'book-53',type: 'technology',tag: 'nosql',favCount: 20,author: { name: 'hushang', age: 28 },tagIndex: Long("0")     # 源数组下标索引},{_id: ObjectId("66a5cf9fe78ef1c1a23525b3"),title: 'book-53',type: 'technology',tag: 'document',favCount: 20,author: { name: 'hushang', age: 28 },tagIndex: Long("1")       # 源数组下标索引}
]
# 使用preserveNullAndEmptyArrays选项在输出中包含缺少path字段,null或空数组的文档
db.books.aggregate([{$match: {"author.name": "hushang"}},{$unwind: {path: "$tag",includeArrayIndex: 'tagIndex',preserveNullAndEmptyArrays: true}}
])# 此时的执行结果就有四条数据了,将另外两个tag字段为空数组 或者 null 也一起输出了
[{  # 没有tag字段,并且tagIndex为null_id: ObjectId("66a5cf9fe78ef1c1a23525b1"),title: 'book-51',type: 'technology',favCount: 11,author: { name: 'hushang', age: 28 },tagIndex: null},{ # 没有tag字段,并且tagIndex为null_id: ObjectId("66a5cf9fe78ef1c1a23525b2"),title: 'book-52',type: 'technology',favCount: 15,author: { name: 'hushang', age: 28 },tagIndex: null},{_id: ObjectId("66a5cf9fe78ef1c1a23525b3"),title: 'book-53',type: 'technology',tag: 'nosql',favCount: 20,author: { name: 'hushang', age: 28 },tagIndex: Long("0")},{_id: ObjectId("66a5cf9fe78ef1c1a23525b3"),title: 'book-53',type: 'technology',tag: 'document',favCount: 20,author: { name: 'hushang', age: 28 },tagIndex: Long("1")}
]



$limit

官方文档

限制传递到管道中下一阶段的文档数

db.books.aggregate([{$limit: 5}
])

此操作仅返回管道传递给它的前5个文档。 $limit对其传递的文档内容没有影响。

注意:当$sort在管道中的$limit之前出现时,$sort操作只会在过程中维持前n个结果,其中n是指定的限制,而MongoDB只需要将n个项存储在内存中。



$skip

官方文档

跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段

此操作将跳过管道传递给它的前5个文档。 $skip对沿着管道传递的文档的内容没有影响。

db.books.aggregate([{ $skip : 5 }
]);



$sort

官方文档

将所有输入文档进行排序,然后按照排序将其返回至管道。

语法:

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

$sort 接受指定要作为排序依据的字段以及相应排序顺序的文档。

  • field表示要排序依据的字段
  • sort order,如果为1表示升序排序,-1表示降序排序

如果对多个字段进行排序,则按从左到右的顺序进行排序。

db.books.aggregate([{$sort: {favCount: 1,"author.age": -1}}
])



$lookup

$lookup 官方文档

5.1 版本中进行了更改。可以将 $lookup 与分片集合一起使用。

同一 数据库中的一个集合执行左外连接,以过滤“已连接”集合中的文档以便进行处理。

$lookup 阶段向每个输入文档添加一个新的数组字段,(可根据需要命名新key )。数组列存放的数据是来自被Join集合的适配文档,如果没有,集合为空(即 为[ ])

db.collection.aggregate([{$lookup: {from: "<collection to join>",localField: "<field from the input documents>",foreignField: "<field from the documents of the from collection>",as: "<output array field>"}})
关键字说明
from同一个数据库下等待被Join的集合。
localField源集合中的match值,如果输入的集合中,某文档没有 localField这个Key(Field),在处理的过程中,会默认为此文档含有 localField:null的键值对。
foreignField待Join的集合的match值,如果待Join的集合中,文档没有foreignField值,在处理的过程中,会默认为此文档含有 foreignField:null的键值对。
as为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉

注意:null = null 此为真



案例:

数据准备

db.orders.insertMany( [{ "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },{ "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },{ "_id" : 3  }
] )db.inventory.insertMany( [{ "_id" : 1, "sku" : "almonds", "description": "product 1", "instock" : 120 },{ "_id" : 2, "sku" : "bread", "description": "product 2", "instock" : 80 },{ "_id" : 3, "sku" : "cashews", "description": "product 3", "instock" : 60 },{ "_id" : 4, "sku" : "pecans", "description": "product 4", "instock" : 70 },{ "_id" : 5, "sku": null, "description": "Incomplete" },{ "_id" : 6 }
] )



orders 集合的字段 item 和来自 inventory 集合的 sku 字段,将来自 orders 的文档与来自 inventory 集合的文档联接在一起:

# orders集合的item字段     inventory集合的sku字段
db.orders.aggregate( [{$lookup:{from: "inventory",localField: "item",foreignField: "sku",as: "inventory_docs"}}
] )# 该操作对应于如下伪 SQL 语句:
SELECT *, inventory_docs
FROM orders
WHERE inventory_docs IN (SELECT *FROM inventoryWHERE sku = orders.item
)
# 输出结果为下面三行,注意最后一行,null == null 条件成立的结果
[{_id: 1,item: 'almonds',price: 12,quantity: 2,inventory_docs: [{_id: 1,sku: 'almonds',description: 'product 1',instock: 120}]},{_id: 2,item: 'pecans',price: 20,quantity: 1,inventory_docs: [{ _id: 4, sku: 'pecans', description: 'product 4', instock: 70 }]},{_id: 3,inventory_docs: [ { _id: 5, sku: null, description: 'Incomplete' }, { _id: 6 } ]}
]



聚合操作案例1

原始数据

test> db.books.find()
[{_id: ObjectId("66a5b0106ae7536b7b29723e"),title: 'book-0',type: 'literature',tag: [ 'nosql', 'popular' ],favCount: 63,author: { name: 'xx001', age: 26 }},{_id: ObjectId("66a5b0106ae7536b7b29723f"),title: 'book-1',type: 'novel',tag: [ 'popular', 'popular' ],favCount: 54,author: { name: 'xx005', age: 22 }},{_id: ObjectId("66a5b0106ae7536b7b297240"),title: 'book-2',type: 'technology',tag: [ 'popular', 'popular' ],favCount: 24,author: { name: 'xx005', age: 23 }},......
]
# 统计每个分类的book文档数量
db.books.aggregate([{$group:{_id: "$type",bookcount: {$count: {}}}},{$sort:{bookcount: -1}}
])
# 输出结果
[{ _id: 'technology', bookcount: 15 },{ _id: 'sociality', bookcount: 11 },{ _id: 'literature', bookcount: 11 },{ _id: 'novel', bookcount: 9 },{ _id: 'travel', bookcount: 7 }
]



# 标签的热度排行,标签的热度则按其关联book文档的收藏数(favCount)来计算
db.books.aggregate([{$match:{favCount: {$gt: 0}}},{$unwind:{path: "$tag",includeArrayIndex: "arrayIndex",preserveNullAndEmptyArrays: true}},{$group:{_id: "$tag",countSum: {$sum: "$favCount"}}},{$sort:{countSum: -1}}
]
  1. 过滤掉favCount=0的文档
  2. 将tag标签中的数组拆分为一个个的文档,这样一个包含3个标签的文档会被拆解为3个条目。
  3. 再按照tag分组,并求出各个标签订阅的总数
  4. 对总数排序
# 执行结果
[{ _id: 'popular', total: 1159 },{ _id: 'nosql', total: 1095 },{ _id: 'developer', total: 1014 },{ _id: 'mongodb', total: 872 },{ _id: 'document', total: 598 }
]



聚合操作案例2

导入邮政编码数据集:https://media.mongodb.org/zips.json

使用mongoimport工具导入数据

MongoDB Database Tools 工具下载路径

mongoimport -h 192.168.75.100 -d test -u hushang -p 123456 --authenticationDatabase=admin -c zips --file D:\downfile\goodle下载\zips.json  

h,–host :代表远程连接的数据库地址,默认连接本地Mongo数据库;

–port:代表远程连接的数据库的端口,默认连接的远程端口27017;

-u,–username:代表连接远程数据库的账号,如果设置数据库的认证,需要指定用户账号;

-p,–password:代表连接数据库的账号对应的密码;

-d,–db:代表连接的数据库;

-c,–collection:代表连接数据库中的集合;

-f, --fields:代表导入集合中的字段;

–type:代表导入的文件类型,包括csv和json,tsv文件,默认json格式;

–file:导入的文件名称

–headerline:导入csv文件时,指明第一行是列名,不需要导入;




在这里插入图片描述



test> show collections
books
emp
zips
test> db.zips.countDocuments()
29353# 原始数据
test> db.zips.find()
[{_id: '01035',city: 'HADLEY',loc: [ -72.571499, 42.36062 ],pop: 4231,state: 'MA'},{_id: '01030',city: 'FEEDING HILLS',loc: [ -72.675077, 42.07182 ],pop: 11985,state: 'MA'},{_id: '01026',city: 'CUMMINGTON',loc: [ -72.905767, 42.435296 ],pop: 1484,state: 'MA'},
......
]



# 返回人口超过1000万的州
db.zips.aggregate([{$group: {_id: "$state",popSum: { $sum: "$pop" }}},{ $match: { popSum: { $gte: 1000000 } } }
])
# 返回各州中各个城市平均人口
db.zips.aggregate([{$group: {_id: { state: "$state", city: "$city" },cityPop: { $sum: "$pop" }}},{$group: {_id: "$_id.state",avgCityPop: { $avg: "$cityPop" }}}
])
# 按州返回人口最大和最小的城市
db.zips.aggregate( [# 计算出各个州下 各个城市的人数{ $group:{_id: { state: "$state", city: "$city" },pop: { $sum: "$pop" }}},# 对人数进行排序{ $sort: { pop: 1 } },# 取排序后的第一个数据和最后一个数据{ $group:{_id : "$_id.state",biggestCity:  { $last: "$_id.city" },biggestPop:   { $last: "$pop" },smallestCity: { $first: "$_id.city" },smallestPop:  { $first: "$pop" }}},# 对输出的内容包装一下{ $project:{ _id: 0,state: "$_id",biggestCity:  { name: "$biggestCity",  pop: "$biggestPop" },smallestCity: { name: "$smallestCity", pop: "$smallestPop" }}}
] )



聚合优化

https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/

聚合优化的三大目标:

  • 尽可能利用索引完成搜索和排序
  • 尽早尽多减少数据量,比如使用$match
  • 尽可能减少执行步骤

在这里插入图片描述



执行顺序

$match/$sort vs $project/$addFields

为了使查询能够命中索引,$match/$sort步骤需要在最前面,该原则适用于MongoDB <=3.4版本。MongoDB 3.6开始具备一定的自动优化能力。

$project + $skip/$limit

$skip/$limit应该尽可能放在$project之前,减少$project投影的工作量 。3.6开始自动完成这个优化。

在这里插入图片描述



内存排序

在没有索引支持的情况下,MongoDB最多只支持使用100MB内存进行排序。

假设总共可用内存为16GB,一个请求最多可以使用100MB内存排序,总共可以有16000/ 100= 160个请求同时执行。

内存排序消耗的不仅是内存,还有大量CPU



方案一: $sort + $limit

只排Top N ,只要N条记录总和不超过100MB即可



方案二: {allowDiskUse: true}

使用磁盘作为交换空间完成全量,超出100MB部分与磁盘交换排序



方案三: 索引排序

使用索引完成排序,没有内存限制



整合Springboot进行聚合操作

SpringBoot整合MongoDB的详细流程请参考上文《SpringBoot整合MongoDB》



MongoTemplate提供了aggregate方法来实现对数据的聚合操作。

在这里插入图片描述



基于聚合管道mongodb提供的可操作的内容:

在这里插入图片描述



基于聚合操作Aggregation.group,mongodb提供可选的表达式

在这里插入图片描述



案例一

返回人口超过1000万的州

db.zips.aggregate([{$group: {_id: "$state",popSum: { $sum: "$pop" }}},{ $match: { popSum: { $gte: 1000000 } } }
])
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;/*** 返回人口超过1000万的州* 先对州进行分组* 求和,州的总人数* 筛选、匹配、过滤*/
@Test
public void testGtPop() {//$groupGroupOperation groupOperation = Aggregation.group("state").sum("pop").as("popSum");//$matchCriteria criteria = Criteria.where("popSum").gte(1000000);MatchOperation matchOperation = Aggregation.match(criteria);// 按顺序组合每一个聚合步骤TypedAggregation<Zips> zipsTypedAggregation = Aggregation.newAggregation(Zips.class,groupOperation, matchOperation);// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<Map> aggregationResults = mongoTemplate.aggregate(zipsTypedAggregation, Map.class);// 取出最终结果List<Map> mappedResults = aggregationResults.getMappedResults();mappedResults.forEach(System.out::println);}



案例二

返回各州中各个城市平均人口

db.zips.aggregate([{$group: {_id: { state: "$state", city: "$city" },cityPop: { $sum: "$pop" }}},{$group: {_id: "$_id.state",avgCityPop: { $avg: "$cityPop" }}},{$sort: {avgCityPop: -1}}
])
/*** 返回各州中各个城市平均人口*      先安装州、城市进行分组*      得到哥走中各个城市的人口*      再求平均值*/
@Test
public void testPopAvg(){//$groupGroupOperation cityGroup = Aggregation.group("state", "city").sum("pop").as("cityPop");GroupOperation avgCityGroup = Aggregation.group("_id.state").avg("cityPop").as("avgCityPop");// $sortSortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "avgCityPop");// 按顺序组合每一个聚合步骤TypedAggregation<Zips> zipsTypedAggregation = Aggregation.newAggregation(Zips.class,cityGroup, avgCityGroup, sortOperation);// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<Map> aggregationResults = mongoTemplate.aggregate(zipsTypedAggregation, Map.class);// 取出最终结果List<Map> mappedResults = aggregationResults.getMappedResults();mappedResults.forEach(System.out::println);
}



案例三

按州返回人口最大和最小的城市

# 按州返回人口最大和最小的城市
db.zips.aggregate( [{ $group:{_id: { state: "$state", city: "$city" },pop: { $sum: "$pop" }}},{ $sort: { pop: 1 } },{ $group:{_id : "$_id.state",biggestCity:  { $last: "$_id.city" },biggestPop:   { $last: "$pop" },smallestCity: { $first: "$_id.city" },smallestPop:  { $first: "$pop" }}},{ $project:{ _id: 0,state: "$_id",biggestCity:  { name: "$biggestCity",  pop: "$biggestPop" },smallestCity: { name: "$smallestCity", pop: "$smallestPop" }}},{ $sort: { state: 1 } }
] )
@Test
public void testMinMaxPop(){// $groupGroupOperation cityPopGroup = Aggregation.group("state", "city").sum("pop").as("pop");// $sortSortOperation popSort = Aggregation.sort(Sort.Direction.ASC, "pop");// $groupGroupOperation lastFirstGroup = Aggregation.group("_id.state").last("_id.city").as("biggestCity").last("pop").as("biggestPop").first("_id.city").as("smallestCity").first("pop").as("smallestPop");// $projectProjectionOperation projectionOperation = Aggregation.project("state", "biggestCity", "smallestCity").and("_id").as("state").andExpression("{ name: \"$biggestCity\",  pop: \"$biggestPop\" }").as("biggestCity").andExpression("{ name: \"$smallestCity\", pop: \"$smallestPop\" }").as("smallestCity");// $sortSortOperation stateSort = Aggregation.sort(Sort.Direction.ASC, "state");// 按顺序组合每一个聚合步骤TypedAggregation<Zips> zipsTypedAggregation = Aggregation.newAggregation(Zips.class,cityPopGroup, popSort, lastFirstGroup, projectionOperation, stateSort);// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<Map> aggregationResults = mongoTemplate.aggregate(zipsTypedAggregation, Map.class);// 取出最终结果List<Map> mappedResults = aggregationResults.getMappedResults();mappedResults.forEach(System.out::println);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/877169.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hugo部署到Vercel踩大坑——全是XML文件?

问题描述 部署到Vercel全都是XML文件 Vercel是著名PAAS服务&#xff0c;相比于 Github Pages&#xff0c;其中国大陆可直接访问&#xff0c;因此尝试把Hugo站点发布到vercel中&#xff0c;部署后遇到问题&#xff0c;所有页面都为xml文件&#xff0c;如下所示&#xff1a; Ve…

AIGC-VDM -Video Diffusion Models论文解读

homepage&#xff1a;https://video-diffusion.github.io/ paper:https://arxiv.org/pdf/2204.03458 参考:https://zhuanlan.zhihu.com/p/585009571 视频生成方面的扩散模型 论文Video Diffusion Models精读&#xff0c;笔者会多多更新AIGC相关知识&#xff01;点个关注吧&…

SpringCloud Alibaba 微服务(二):Nacos

目录 前言 一、什么是Nacos&#xff1f; 二、Nacos的主要功能 服务发现与注册 配置管理 服务健康监控 集群模式 三、安装Nacos 下载Nacos 初始化Nacos 单机模式 集群模式 访问Nacos 四、服务注册 新建子工程 引入Nacos依赖 配置application.yml 创建启动类 …

内网穿透的应用-中文版个人知识库Trilium笔记本地安装结合内网穿透远程协作

文章目录 前言1. 安装docker与docker-compose2. 启动容器运行镜像3. 本地访问测试4.安装内网穿透5. 创建公网地址6. 创建固定公网地址 前言 今天和大家分享一款在G站获得了26K的强大的开源在线协作笔记软件&#xff0c;Trilium Notes的中文版如何在Linux环境使用docker本地部署…

Linux基础复习(五)

前言 本文介绍了Linux常用命令&#xff0c;接Linux基础复习&#xff08;四&#xff09; 一、常用命令 命令通配符 在Linux中&#xff0c;命令通配符&#xff08;也称为通配符模式或通配符表达式&#xff09;是用来匹配文件名或其他字符串的一种特殊字符。这些通配符可以帮助…

深入解析 GZIP 压缩传输:优化网络性能的利器

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[2435024119@qq.com] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? 专栏导…

访问控制列表(ACL)

文章目录 ACL原理与基本配置ACL分类ACL组成ACL规则的匹配与应用 ACL原理与基本配置 ACL(Access Control List&#xff0c;访问控制列表) 读取二层、三层、四层报文信息根据预先定义好的规则对报文进行过滤和分类实现网络访问控制、防止网络攻击和提高网络带宽利用率等目的提高…

力扣SQL50 上级经理已离职的公司员工 一题双解

Problem: 1978. 上级经理已离职的公司员工 Code -- 方法 1 -- select e1.employee_id -- from employees e1 -- left join employees e2 -- on e1.manager_id e2.employee_id -- where e1.salary < 30000 -- and e1.manager_id is not null -- and e2.employee_id is…

学鸿蒙开发好找工作吗?

学鸿蒙开发是一个非常有潜力的方向&#xff0c;因为鸿蒙系统是华为自主研发的操作系统&#xff0c;随着其在全球范围内的推广和应用&#xff0c;越来越多的企业和组织开始重视并采用这一系统。这使得鸿蒙开发者的市场需求正在持续增长&#xff0c;为鸿蒙开发者提供了更多的就业…

C# 设计倒计时器、串口助手开发

文章目录 1. 实现一个简单的倒计时器开始、暂停2. 串口助手开发 1. 实现一个简单的倒计时器开始、暂停 namespace Timer {public partial class Form1 : Form{int count;//用于定时器计数int time;//存储设定的定时值bool parse false;//控制暂停计时public Form1(){Initiali…

Qt Designer,仿作一个ui界面的练习(一):界面的基本布局

初学不要太复杂&#xff0c;先做一个结构简单的&#xff0c;大致规划一下功能分区&#xff0c;绘制草图&#xff1a; 最终的效果&#xff1a; 界面主要由顶边栏、侧边栏、内容区构成。顶边栏左边是logo&#xff0c;右边是时钟显示。侧边栏最上边是切换按钮&#xff0c;用以动画…

Qt中对象树机制的优点以及潜在的隐患

目录 一、什么是对象树 二、对象树的优点 三、对象树潜在的隐患 一、什么是对象树 Qt中的 QObject 会用对象树来组织管理自己&#xff0c;那什么是对象树? 这个概念非常好理解。因为 QObject 类就有一个私有变量 QList<QObject *>&#xff0c;专门存储这个类的子孙后…

centos7-8/redhat7-8一键安装配置vsftp服务

1.脚本介绍 1.1.介绍&#xff1a; linux下一键安装及配置vsftpd服务 &#xff0c;通过执行install.sh脚本&#xff0c;脚本会根据参数区域的值执行安装和配置vsftp服务&#xff0c;安装后会创建一个默认ftp用户wangxf密码wangxf2023 1、支持自定义安装(更改脚本内参数值) 2、…

一篇文章带你入门爬虫并编写自己的第一个爬虫程序

一、引言 目前我们处在一个信息快速迭代更新的时代&#xff0c;海量的数据以大爆炸的形式出现在网络之中&#xff0c;相比起过去那个通过广播无线电、书籍报刊等传统媒介获取信息的方式&#xff0c;我们现在通过网络使用搜索引擎几乎可以获得任何我们需要的信息资源。 但与此同…

优化mac outlook通过nginx反向代理后使用ews访问Exchange 2016邮件访问速度慢的有效方法

在nginx配置exchange的反向代理后,mac系统上通过exchange邮箱,通过nginx代理连接邮箱,发现速度很慢,通过查看日志,也存在大量的401失败日志。通过不断的优化和尝试,目前来看,基本上正常了,基本上没有出现大量访问失败的问题。以下就是优化过程中尝试过的方法。 1. 身份…

【精通Redis】Redis命令详解

引言 Redis是一个内存数据库&#xff0c;在学习它的内部原理与实现之前&#xff0c;我们首先要做到的就是学会使用&#xff0c;学会其丰富的命令操作。 一、字符串 Redis的字符串类型之前笔者的一篇入门介绍中曾经说过&#xff0c;不是简单的只存人可以阅读的字符串&#xf…

【前端 19】使用Vue-CLI脚手架构建Vue2项目

使用Vue CLI构建Vue 2项目 引言 Vue.js 是一个构建用户界面的渐进式JavaScript框架&#xff0c;以其轻量级和易用性受到前端开发者的广泛喜爱。Vue CLI&#xff08;Vue Command Line Interface&#xff09;是一个基于Vue.js进行快速开发的完整系统&#xff0c;提供了零配置的项…

数据结构之线性表(顺序表的实现)

目录 一、线性表的原理 二、线性表的实现&#xff08;顺序表&#xff09; 1.定义顺序表 2.初始化顺序表 3.判断顺序表是否为空 4.获取顺序表的长度 5.向顺序表中插入元素 6.删除指定位置的元素 7.遍历顺序表 8.得到指定位置的元素 三、打印测试功能 1.测试 2.结果…

全球相机控制面板市场展望与未来增长机遇:预计未来六年年复合增长率CAGR为4.3%

在全球摄影器材和专业影像设备需求增长的背景下&#xff0c;相机控制面板正成为市场的焦点。本文详细分析了全球相机控制面板市场的现状、增长趋势及未来前景&#xff0c;旨在为投资者和业内人士提供深入的市场洞察和指导。 市场概览 据恒州诚思团队研究分析显示&#xff0c;2…

RK3568笔记四十七:PWM 子系统

若该文为原创文章&#xff0c;转载请注明原文出处。 pwm 子系统功能单一&#xff0c;很少单独使用&#xff0c;一般用于控制显示屏的背光、控制无源蜂鸣器、伺服电机、电压调节等等。 一、PWM介绍 PWM(Pulse width modulation)&#xff0c;脉冲宽度调制。在内核中 PWM 驱动较简…