MongoDB-聚合查询操作介绍

目录

前言

MongoDB聚合操作相当于关系型数据库SQL语句的"group by"、"order by"这种语句,而我们MongoDB的聚合操作也是使用到group这个聚合函数,但是在介绍聚合函数之前,需要先介绍一个,查询命令"db.collection.aggregate()",之前我们使用的查询方法都是"db.collection.find()";而当MongoDB需要进行聚合操作时,例如取平均值、统计数据,就需要用到"db.collection.aggregate()"方法了。

db.collection.aggregate()

  • 使用 db.collection.aggregate() 的方法运行聚合管道的操作,并不会真的修改文档,除非管道中包含了merge和out函数,这两个函数都是将结果写入到指定的集合,out是可以指定输出的数据库是哪个。
  • 理论上来说db.collection.aggregate()的效率其实是不如find的,因为db.collection.aggregate()一定是要做聚合操作才使用,否则就用find了。当然db.collection.aggregate()不加任何条件,也可以像find一样查询集合中的所有数据。
  • aggregate的语法格式:db.collection.aggregate(pipeline, options),其中pipeline‌ 为一个数组,主要用于使用一个聚合函数,做到查询过滤、分组排序、投影设置输出字段等。options‌ 呢是一个可选的文档参数,主要是用于传递聚合命令的其他选项、比如上方提到的 $out 或 $merge 。甚至还有explain执行计划等。
  • db.collection.aggregate()的限制,MongoDB 5.0 版本后将单个管道中允许的聚合管道阶段限制为 1000 个,每个单独的管道阶段的 RAM 限制为 100 MB。 默认情况下,如果某个阶段超过此限制,MongoDB 会产生错误。 对于某些管道阶段,您可以使用allowDiskUse选项启用聚合管道阶段以将数据写入临时文件,从而允许管道处理占用更多空间,$search 聚合阶段不限于 100 MB 的 RAM,因为它在单独的进程中运行。

db.collection.aggregate()示例一

###插入一批测试数据:name为匹萨的种类、size是披萨的尺寸、price是披萨的单价、quantity是售卖的订单数量、date为时间。
db.orders.insertMany( [
   { _id: 0, name: "Pepperoni", size: "small", price: 19,
     quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) },
   { _id: 1, name: "Pepperoni", size: "medium", price: 20,
     quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) },
   { _id: 2, name: "Pepperoni", size: "large", price: 21,
     quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) },
   { _id: 3, name: "Cheese", size: "small", price: 12,
     quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) },
   { _id: 4, name: "Cheese", size: "medium", price: 13,
     quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) },
   { _id: 5, name: "Cheese", size: "large", price: 14,
     quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) },
   { _id: 6, name: "Vegan", size: "small", price: 17,
     quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) },
   { _id: 7, name: "Vegan", size: "medium", price: 18,
     quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) }
] )
###查询数据,使用db.orders.aggregate()也可以像find命令一样直接查询数据。
Enterprise test [direct: primary] mongodb_test> db.orders.aggregate()
[
  {
    _id: 0,
    name: 'Pepperoni',
    size: 'small',
    price: 19,
    quantity: 10,
    date: ISODate('2021-03-13T08:14:30.000Z')
  },
  {
    _id: 1,
    name: 'Pepperoni',
    size: 'medium',
    price: 20,
    quantity: 20,
    date: ISODate('2021-03-13T09:13:24.000Z')
  },
  {
    _id: 2,
    name: 'Pepperoni',
    size: 'large',
    price: 21,
    quantity: 30,
    date: ISODate('2021-03-17T09:22:12.000Z')
  },
...
###使用聚合操作过滤size字段等于medium的数据,按照name进行分组排序,计算quantity列数据的总和,其中过滤使用到match函数,分组排序使用到group函数、计算总和用到sum函数。
mongodb_test> db.orders.aggregate( [
{
$match: { size: "medium" }
},
{
   $group: { _id: "$name", totalQuantity: { $sum: "$quantity" } }
}
] )
...
[
  { _id: 'Cheese', totalQuantity: 50 },
  { _id: 'Vegan', totalQuantity: 10 },
  { _id: 'Pepperoni', totalQuantity: 20 }
]
###此时通过查询可以发现,我们的数据并没有被修改或变更。所有的操作都在内存中实现
Enterprise test [direct: primary] mongodb_test> db.orders.aggregate()
[
  {
    _id: 0,
    name: 'Pepperoni',
    size: 'small',
    price: 19,
    quantity: 10,
    date: ISODate('2021-03-13T08:14:30.000Z')
  },
  ...
  • 上方的聚合操作执行流程为:先通过match函数过滤了size为medium的数据,然后把数据传递下一阶段的管道,给了group函数,group函数拿到数据后,根据name列进行分组排序此处的_id为表达式指定组键简单说_id后面跟哪个列就是按照哪个列排序,并通过sum函数对名字相同的quantity列的数据进行相加,输出结果为totalQuantity。

db.collection.aggregate()示例二

###过滤一个指定日期范围内,每天的披萨的订单总额是多少,并且所有种类披萨加一起,平均出了多少单。根据订单总额从大到小排序
Enterprise test [direct: primary] mongodb_test> db.orders.aggregate( [
... 
...    // Stage 1: Filter pizza order documents by date range
...    {
...       $match:
...       {
...          "date": { $gte: new ISODate( "2020-01-30" ), $lt: new ISODate( "2022-01-30" ) }
...       }
...    },
... 
...    // Stage 2: Group remaining documents by date and calculate results
...    {
...       $group:
...       {
...          _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
...          totalOrderValue: { $sum: { $multiply: [ "$price", "$quantity" ] } },
...          averageOrderQuantity: { $avg: "$quantity" }
...       }
...    },
... 
...    // Stage 3: Sort documents by totalOrderValue in descending order
...    {
...       $sort: { totalOrderValue: -1 }
...    }
... 
...  ] )
[
  { _id: '2022-01-12', totalOrderValue: 790, averageOrderQuantity: 30 },
  { _id: '2021-03-13', totalOrderValue: 770, averageOrderQuantity: 15 },
  { _id: '2021-03-17', totalOrderValue: 630, averageOrderQuantity: 30 },
  { _id: '2021-01-13', totalOrderValue: 350, averageOrderQuantity: 10 }
  • match阶段根据时间范围为大于等于2020年1月30日,小于2022年1月30日过滤数据。
  • 根据match过滤的数据传递给group,group通过dateToString函数按照年月日进行分区排序,然后通过multiply函数让price乘以quantity,再根据分组进行sum,让年月日相同的分组进行相加。最后输出为totalOrderValue字段。再通过avg函数,对相同年月日的数据,取quantity字段的平均值(将quantity列数据相加除以相同分组有多少条数据),最后输出为averageOrderQuantity字段。
  • 最后的sort函数数根据totalOrderValue字段,按照-1排序,-1表示降序。从大到小排列。

复杂的聚合操作

  • 根据上方的介绍,基本上对聚合操作有了一定的认识,下面将难度提升,看一下复杂的聚合操作又是怎么实现的。

通过mongoimport导入测试数据

  • 为了后面的操作,我们先导入一批数据,这个测试数据来源自MongoDB官网。

测试数据地址:https://media.mongodb.org/zips.json

### 安装mongodb tools工具,里面包含了mongoimport
root # wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-rhel70-x86_64-100.10.0.rpm
root # yum localinstall -y mongodb-database-tools-rhel70-x86_64-100.10.0.rpm

### 下载测试文件到服务器上
root # wget https://media.mongodb.org/zips.json

### 使用mongodb的mongoimport 导入数据到MongoDB里
root # mongoimport --username admin --password admin --host 127.0.0.1 --port 27017 --authenticationDatabase=admin -d mongodb_test --type=json --file=./zips.json
  • authenticationDatabase指定认证库
  • -d指定导入到的数据库
  • type=json表示这是个json的文档
  • file表示指定需要导入的文件

复杂的聚合操作示例

1.根据zips集合中的state字段(州)和pop(城市区域人口)列的相加进行分组排序,再计算小于100万人口的州有哪些。

db.zips.aggregate( [
   { $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
   { $match: { totalPop: { $lt: 1000000 } } }
] )
[
  { _id: 'ND', totalPop: 638272 },
  { _id: 'AK', totalPop: 544698 },
  { _id: 'MT', totalPop: 798948 },
  { _id: 'VT', totalPop: 562758 },
  { _id: 'SD', totalPop: 695397 },
  { _id: 'WY', totalPop: 453528 },
  { _id: 'DC', totalPop: 606900 },
  { _id: 'DE', totalPop: 666168 }
]
  • group阶段,按照两个列进行分组,一个州信息缩写state列,和totalPop列,totalPop列为所有州人口的总和。
  • match阶段得时候,被group传递了两列数据,一个是_id列为state得州信息,一个是totalPop总人口信息,随后通过对totalPop列进行过滤条件匹配,返回数据。

这个测试数据中,不同的州可能拥有相同的城市、相同的城市区域不同,人口不同。

2.计算每个州state、每个城市city,每个区域的,平均人口有多少。需要先按照州、城市、和区域进行分组排序,然后将人口进行相加。随后通过分组后的数据,二次根据州城市分组排序,取总人口的平均值。

###先计算每个州每个城市的总人口,数据量很大。
db.zips.aggregate( [
   { $group: { _id: { state: "$state", city:"$city",loc:"$loc" },pop: { $sum: "$pop" } } },
] )
###通过group聚合操作,再次对state州和城市进行分组排序。
db.zips.aggregate( [
   { $group: { _id: { state: "$state", city:"$city" ,loc:"$loc"}, pop: { $sum: "$pop" } } },
   { $group: { _id:{state:"$_id.state",city:"$_id.city"}}}
] )
###现在可以取平均值了。
db.zips.aggregate( [
   { $group: { _id: { state: "$state", city:"$city",loc:"$loc" }, pop: { $sum: "$pop" } } },
   { $group: { _id:{state:"$_id.state",city:"$_id.city"}, avgcityloc:{$avg:"$pop"}}}
] )
  • 先通过聚合函数group,对state、city、loc三列进行分组排序,随后使用sum函数进行相加。这时候传递给下个管道时就是有四列信息,分别是state、city、loc、以及相加后的pop。
  • 二次group时再对排序过再次做聚合操作,通过_id.state和_id.city字段的方式对,传递过来的数据进行二次聚合操作,聚合完成就可以直接通过avg函数对pop字段取平均值了。

小知识:在mongoshell中,当输出结果集行数过多,默认是显示不完的,需要设置参数指定需要显示的行数,DBQuery.shellBatchSize = 100000,这里表示设置输出结果集显示1万行。

3.按照state、city、区域,返回最大人口的城市和最小的城市是哪两个。

###先根据state和city进行排序,然后对人口进行相加得到每个state州的每个city城市有多少人口。
db.zips.aggregate( [
   { $group:
      {
        _id: { state: "$state", city: "$city" },
        pop: { $sum: "$pop" }
      }
   }
] )
###使用前面提到的sort函数,对人口进行从小到达的一个排序,这样我们就得到了一个有序的数据。
db.zips.aggregate( [
   { $group:
      {
        _id: { state: "$state", city: "$city" },
        pop: { $sum: "$pop" }
      }
   },
   { $sort: { pop: 1 } }
] )
###此时就要开始二次聚合操作,处理数据了。根据州state,进行分组排序,但是不同的是我们在排序的同时需要获取最大人口的城市和最小人口的城市。因为我们是按照州排序的,就可以通过两个参数,捞到每个州最大人口的城市和最小的城市了,一个是函数$last一个是first。分别是获取文档中最后一条数据和第一条数据。而刚才我们通过sort排序后最大的数据会在最后一行,最小的会在第一行。
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函数,它可以修改我们的输出结果。重定向字段。
db.zipcodes.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" }
    }
  }
] )
  • 通过第一个group对state和city进行分组,然后通过sum函数获取到了city的总人口
  • 再通过sort进行升序排序。将最大的值放在最下面,将最小的值放在最后面。
  • 第二个group通过对_id.state再次分组排序,然后根据州的分组,通过last和first函数,取出city和pop列取出每个州第一行数据,和最后一行数据。表示最大值和最小值。
  • 通过project函数输出重定向为一个自定义文档,_id:0为抑制这个字段的输出,再通过新建state,输出_id列的值。创建文档biggestCity和smallestCity,里面的name和pop,分别调用biggestCity和biggestPop,以及smallestCity和smallestPop。返回输出结果。

聚合操作的偏好设置-project函数

  • project函数主要用于,将上层管道传递来的所有数据,也可以是将数据传递给下个管道,数据来源可以是输入已有的文档内容,也可以进行自己计算得出新的字段。
  • project的常用功能主要有:抑制字段(包括嵌入式文档)、排除字段(包括嵌入式文档)、添加新字段或重置现有字段,以及上方提到的,自定义文档输出。
  • 在project中不支持数组索引

1.插入一批测试数据

db.members.insertMany( [
   {
      _id: "jane",
      joined: ISODate("2011-03-02"),
      likes: ["golf", "racquetball"]
   },
   {
      _id: "joe",
      joined: ISODate("2012-07-02"),
      likes: ["tennis", "golf", "swimming"]
   },
   {
      _id: "ruth",
      joined: ISODate("2012-01-14"),
      likes: ["golf", "racquetball"]
   },
   {
      _id: "harold",
      joined: ISODate("2012-01-21"),
      likes: ["handball", "golf", "racquetball"]
   },
   {
      _id: "kate",
      joined: ISODate("2012-01-14"),
      likes: ["swimming", "tennis"]
   }
] )

2.通过project函数抑制_id字段的输出或只输出_id字段。

###抑制_id字段
db.members.aggregate(
   [
      { $project: { _id: 0 } }
   ]
)

###只输出id字段
db.members.aggregate(
   [
      { $project: { _id: 1 } }
   ]
)

2.对name字段内容进行全部大写处理后再排序输出

db.members.aggregate(
  [
    { $project: { name: { $toUpper: "$_id" }, _id: 0 } },
    { $sort: { name: 1 } }
  ]
)
  • project阶段,将_id列通过toUpper函数处理成全部大写,随后再重定义为name字段,然后将_id字段抑制,因为不抑制,_id字段默认是会输出结果的。随后传递给下一管道,sort。
  • sort拿到数据后,对新建的name列进行升序排列。返回数据。

2.按照月份重定义字段,升序输出。

db.members.aggregate( [
    {
      $project: {
         month_joined: { $month: "$joined" },
         name: "$_id",
         _id: 0
       }
    },
    { $sort: { month_joined: 1 } }
] )
  • project阶段,通过month函数对日期字段joined进行处理,month按照时间返回月份的整数(1、2、3月这样),也就是只显示月份。随后重定义字段为month_joined,再输出新定义name列,值为_id列。随后抑制_id列输出。
  • sort阶段通过新定义的字段month_joined进行升序排列。

3.按照月份排序,并统计相同的月份有多少数据,随后再按照数量进行升序排序

db.members.aggregate( [
   { $project: { month_joined: { $month: "$joined" } } } ,
   { $group: { _id: { month_joined: "$month_joined" } , count: { $sum: 1 } } },
   { $sort: { count: 1 } }
] )
  • project阶段通过month函数对joined列进行只输出月份处理,重定义为month_joined字段。传递给下一个管道group。
  • group阶段拿到month_joined列这行数据,进行分组排序,再通过$sum: 1的方式,统计相同月份的数据行有多少。然后传递给下个管道的函数sort
  • sort拿到数据后,按照count列,进行升序排列。

聚合操作的偏好设置-unwind函数

  • unwind函数主要是解析输入文档中的数组字段,然后再为每个数组中的元素输出成文档。每个输出的文档都是和输入文档一样的字段,只是在数组字段时被替换了,从下方可以看到,就是将一行数据变成了多行数据。

    Enterprise test [direct: primary] mongodb_test> db.members.aggregate(
    ... [
    ... { unwind: "likes" }
    ... ]
    ... )
    [
    {
    _id: 'jane',
    joined: ISODate('2011-03-02T00:00:00.000Z'),
    likes: 'golf'
    },
    {
    _id: 'jane',
    joined: ISODate('2011-03-02T00:00:00.000Z'),
    likes: 'racquetball'
    },
    {
    _id: 'joe',
    joined: ISODate('2012-07-02T00:00:00.000Z'),
    likes: 'tennis'
    },
    {
    _id: 'joe',
    joined: ISODate('2012-07-02T00:00:00.000Z'),
    likes: 'golf'
    },
    {
    _id: 'joe',
    joined: ISODate('2012-07-02T00:00:00.000Z'),
    likes: 'swimming'
    },
    {
    _id: 'ruth',
    joined: ISODate('2012-01-14T00:00:00.000Z'),
    likes: 'golf'
    },
    {
    _id: 'ruth',
    joined: ISODate('2012-01-14T00:00:00.000Z'),
    likes: 'racquetball'
    },
    {
    _id: 'harold',
    joined: ISODate('2012-01-21T00:00:00.000Z'),
    likes: 'handball'
    },
    {
    _id: 'harold',
    joined: ISODate('2012-01-21T00:00:00.000Z'),
    likes: 'golf'
    },
    {
    _id: 'harold',
    joined: ISODate('2012-01-21T00:00:00.000Z'),
    likes: 'racquetball'
    },
    {
    _id: 'kate',
    joined: ISODate('2012-01-14T00:00:00.000Z'),
    likes: 'swimming'
    },
    {
    _id: 'kate',
    joined: ISODate('2012-01-14T00:00:00.000Z'),
    likes: 'tennis'
    }
    ]

  • 输出likes字段这个数组中,出现最多的元素是哪个,并且进行降序排列,而且只显示前五行。

    db.members.aggregate(
    [
    { unwind: "likes" },
    { group: { _id: "likes" , count: { $sum: 1 } } },
    { $sort: { count: -1 } },
    { $limit: 5 }
    ]
    )
    ...
    [
    { _id: 'golf', count: 4 },
    { _id: 'racquetball', count: 3 },
    { _id: 'swimming', count: 2 },
    { _id: 'tennis', count: 2 },
    { _id: 'handball', count: 1 }
    ]

  • 因为我们需要对数组进行处理,所以第一个管道,先将数据拆解成多个文档。使用unwind函数,然后再传递给下个管道group

  • group阶段,通过对likes字段排序,再通过sum函数,对相同likes字段的数据行进行计数,传递给下个管道sort

  • sort通过sum统计的列,count进行降序排列,传递给下个管道limit

  • limit函数表示显示几行数据,这里显示5行,和MySQL的limit类似。

相关推荐
梁萌2 小时前
Linux安装Docker
linux·运维·docker·helloworld·容器化部署
彩虹糖_haha2 小时前
Linux高并发服务器开发 第五天(压缩解压缩/vim编辑器/查找替换/分屏操作/vim的配置)
linux·运维·服务器
旺仔学IT2 小时前
Centos7中使用yum命令时候报错 “Could not resolve host: mirrorlist.centos.org; 未知的错误“
linux·运维·centos
ROCKY_8172 小时前
Mysql复习(二)
数据库·mysql·oracle
qq_433618443 小时前
shell 编程(五)
linux·运维·服务器
问道飞鱼5 小时前
【知识科普】认识正则表达式
数据库·mysql·正则表达式
HaiFan.5 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
水根LP495 小时前
linux系统上SQLPLUS的重“大”发现
数据库·oracle
途途途途5 小时前
精选9个自动化任务的Python脚本精选
数据库·python·自动化
04Koi.6 小时前
Redis--常用数据结构和编码方式
数据库·redis·缓存