MongoDB聚合操作详解

news2024/12/22 15:04:57

文章目录

  • 聚合操作
    • 聚合管道
      • 管道(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 inventory
   WHERE 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() {

    //$group
    GroupOperation groupOperation = Aggregation.group("state")
        .sum("pop")
        .as("popSum");

    //$match
    Criteria 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(){

    //$group
    GroupOperation cityGroup = Aggregation.group("state", "city").sum("pop").as("cityPop");
    GroupOperation avgCityGroup = Aggregation.group("_id.state").avg("cityPop").as("avgCityPop");
    // $sort
    SortOperation 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(){
    // $group
    GroupOperation cityPopGroup = Aggregation.group("state", "city").sum("pop").as("pop");

    // $sort
    SortOperation popSort = Aggregation.sort(Sort.Direction.ASC, "pop");
    
 	// $group
    GroupOperation lastFirstGroup = Aggregation.group("_id.state")
        .last("_id.city").as("biggestCity")
        .last("pop").as("biggestPop")
        .first("_id.city").as("smallestCity")
        .first("pop").as("smallestPop");

    // $project
    ProjectionOperation projectionOperation = Aggregation.project("state", "biggestCity", "smallestCity")
        .and("_id").as("state")
        .andExpression("{ name: \"$biggestCity\",  pop: \"$biggestPop\" }")
        .as("biggestCity")
        .andExpression("{ name: \"$smallestCity\", pop: \"$smallestPop\" }")
        .as("smallestCity");

	// $sort
    SortOperation 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.coloradmin.cn/o/1975161.html

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

相关文章

2024有哪些开放式耳机值得入手!开放式耳机推荐!

耳机基本上已经成为我们日常生活中的必需品&#xff0c;开放式蓝牙耳机以其独特的设计&#xff0c;提供了更为自然和安全的听音体验&#xff0c;为了帮助大家更好的选择开放式耳机&#xff0c;我挑选了几个小技巧以及我自用过后觉得还不错的五款耳机推荐给大家。 一、耳机佩戴…

Day6

模板的继承 在一个网页中&#xff0c;可能会出现几个页面样式几乎不变的情况。 确实是可以在每个html文件中写重复代码&#xff0c;但是&#xff0c;假如说后期运维的话&#xff0c;如果需要在某一处修改数据&#xff0c;那用过这个代码的都得逐一修改&#xff0c;这样的运维…

端到端自动驾驶:挑战与前沿

End-to-end Autonomous Driving: Challenges and Frontiers 端到端自动驾驶&#xff1a;挑战与前沿 Abstract The autonomous driving community has witnessed a rapid growth in approaches that embrace an end-to-end algorithm framework, utilizing raw sensor input …

Wiley 旗下一区 Top 期刊编委集体辞职!难道OA真的才是大势所趋?

【SciencePub学术】Wiley作为和Springer Nature、Elsevier并驾齐驱的三大出版商之一&#xff0c;在不久前又被爆出&#xff0c;其旗下一本稳定检索了53年的老牌期刊《Philosophy & Public Affairs》&#xff0c;编委集体辞职&#xff01; 其实&#xff0c;这已经不是Wiley出…

CmsEasy逻辑漏洞--零元购

CmsEasy逻辑漏洞--零元购 选择购买MackBook 购买成功后会员中心发现多出8100快钱 然后就可以正常购买了

@antv/x6 利用interacting方法来设置禁止结点移动的方法实现。

1、官网有个设置说明。 https://x6.antv.antgroup.com/api/graph/graph 2、然后有可行的方法一&#xff1a; let graph new Graph({container: document.getElementById(container),interacting: function (cellView) {if (cellView.cell.getData() ! undefined && …

行为型设计模式2:观察者/职责链/中介者/访问者

设计模式&#xff1a;观察者/职责链/中介者/访问者 (qq.com)

【Android Studiio】default activity 原生安卓和uniapp默认启动分析(uniapp添加MainActivity)

文章目录 思路&#xff1a; 一、原生安卓二、uniapp三、在uni-app项目中&#xff0c;如果你想要加入自定义的MainActivity&#xff08;例如Android平台&#xff09; 探究方向&#xff1a;找到Default Activity 思路&#xff1a; 在Android开发中&#xff0c;"default ac…

谷粒商城实战记录-虚拟机开启密码认证登录

文章目录 一&#xff0c;虚拟机无法用用户名密码登录二&#xff0c;解决方案1&#xff0c;修改配置2&#xff0c;重启sshd服务3&#xff0c;测试SSH登录注意事项结论 参考文献 一&#xff0c;虚拟机无法用用户名密码登录 当使用Vagrant创建和管理虚拟机时&#xff0c;通常会通…

以太网ICMP测试实验

目录 一.概述 1.1IP协议 1.1.1IP协议简介 1.1.2IP协议首部报文格式 1.2ICMP协议 1.2.1ICMP协议简介 1.2.2ICMP报文格式 二.实验任务 三.模块设计 3.1总体模块设计 3.2ICMP模块设计 3.2.1icmp_rx 模块 3.2.2icmp_tx模块 四.modelsim仿真 4.1icmp_rx模块仿真 4.2i…

完美平替navicat的数据库软件DBeaver,免费开源,功能强大,使用简单

DBeaver Community 是一个免费的跨平台数据库工具&#xff0c;适用于开发人员、数据库管理员、分析师和所有与数据打交道的人。它支持所有流行的 SQL 数据库&#xff0c;如 MySQL、MariaDB、PostgreSQL、SQLite、Apache Family 等 1.到官网下载win版本直接安装 https://dbeav…

从一到无穷大 #32 TimeCloth,云上的快速 Point-in-Time Recovery

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言解决方案FAST FINE-GRAINED PITRLog FilterInter-Record Dependency ResolutionL…

全球氢燃料电池汽车市场规划预测:未来六年CAGR为44.4%

一、引言 随着全球能源转型和环保意识的提升&#xff0c;氢燃料电池汽车作为清洁能源汽车的重要组成部分&#xff0c;其市场潜力日益凸显。本文旨在探索氢燃料电池汽车行业的发展趋势、潜在商机及其未来展望。 二、市场趋势 全球氢燃料电池汽车市场的增长主要受全球环保政策推…

springboot辽B代驾管理系统论文源码调试讲解

2 相关技术 2.1 MySQL数据库 本研究与开发的应用程序在数据操作中难以预料&#xff0c;有时候甚至产生变化。没有办法直接在Word中写数据信息&#xff0c;这不但不安全&#xff0c;并且难以达到应用程序的功能。想要实现所需要的文档存储功能&#xff0c;务必选择专业数据库存…

奥运会Ⅲ--AI风险矩阵

问满屋子的人工智能专家&#xff1a;“我们如何知道我们的人工智能是否安全可靠&#xff1f;”你可能会得到十几个不同的答案&#xff0c;每个答案都比上一个更复杂。似乎不知何故&#xff0c;在 OpenAI 成为地球上增长最快的公司一年零几个月后&#xff0c;每个人都是 LLM&…

CANOpen EMCY紧急报文介绍

什么是CANOpen紧急报文 CANOpen中的Emcy紧急报文用于当设备出现故障或警告时&#xff0c;向其它节点报告故障或警告使用的。如设备某个设备出现过压或过流时&#xff0c;就可以发送紧急报文。 紧急报文的格式 错误代码&#xff1a;是0x1003索引预定义错误字段的内容&#xff…

修改VSCode插件默认安装路径

文章目录 前言VSCode插件默认安装路径添加环境变量先修改路径注意事项 前言 今天发现C盘快满了&#xff0c;只能清理点东西&#xff0c;给C盘腾出的空间出来&#xff0c;结果一看&#xff0c;VSCode的插件目录就占了不少呢。 VSCode插件默认安装路径 C:\Users\{个人用户名}\.…

比较支持向量机、AdaBoost、逻辑斯谛回归模型的学习策略与算法

(1)支持向量机&#xff1a;SVM 是一种监督学习算法&#xff0c;用于分类和回归任务。 学习策略&#xff1a;间隔最大化 算法&#xff1a;最大间隔法、软间隔算法。 参数学习&#xff1a;参数学习过程是要最小化目标函数&#xff0c;通常通过优化算法&#xff08;如SMO&#x…

为什么选择 Baklib 而不是 Salesforce 进行知识库管理

对于希望管理其产品和服务的在线文档或知识库以支持其客户和员工的组织来说&#xff0c;市场上有太多的平台和工具。知识库通过向客户和员工提供重要信息来帮助组织提高生产力。这大致分为客户关系管理或客户服务。 很少有平台能够为销售、客户服务等提供一套服务。Salesforce…

Adobe ColdFusion反序列化漏洞(cve-2017-3066)

目录 概述复现过程修复建议 概述 Adobe ColdFusion&#xff08;直译&#xff1a;冷聚变&#xff09;&#xff0c;是一个动态Web服务器&#xff0c;其CFML&#xff08;ColdFusion Markup Language&#xff09;是一种程序设计语言&#xff0c;类似JSP里的JSTL&#xff08;JSP St…