ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【详细教程】一文参透MongoDB聚合查询 [打印本页]

作者: tsx81429    时间: 2022-8-27 13:35
标题: 【详细教程】一文参透MongoDB聚合查询

MongoDB聚合查询

什么是聚合查询

聚合操作主要用于处理数据并返回计算结果。聚合操作将来自多个文档的值组合在一起,按条件分组后,再进行一系列操作(如求和、平均值、最大值、最小值)以返回单个结果。
MongoDB的聚合查询

​                聚合是MongoDB的高级查询语言,它允许我们通过转化合并由多个文档的数据来生成新的在单个文档里不存在的文档信息。MongoDB中聚合(aggregate)主要用于处理数据(例如分组统计平均值、求和、最大值等),并返回计算后的数据结果,有点类似sql语句中的 count(*)、group by。
​                在MongoDB中,有两种方式计算聚合:Pipeline 和 MapReduce。Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存。
聚合管道方法

MongoDB 的聚合框架就是将文档输入处理管道,在管道内完成对文档的操作,最终将文档转换为聚合结果,MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理,管道操作是可以重复的。
​                最基本的管道阶段提供过滤器,其操作类似查询和文档转换,可以修改输出文档的形式。其他管道操作提供了按特定字段对文档进行分组和排序的工具,以及用于聚合数组内容(包括文档数组)的工具。
​                此外,在管道阶段还可以使用运算符来执行诸如计算平均值或连接字符串之类的任务。聚合管道可以在分片集合上运行。
聚合流程

db.collection.aggregate()是基于数据处理的聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果。
聚合管道方法的流程参见下图

上图的聚合操作相当于 MySQL 中的以下语句:
  1. select cust_id as _id, sum(amount) as total from orders where status like "%A%" group by cust_id;
复制代码
详细流程

聚合语法
  1. db.collection.aggregate(pipeline, options)
复制代码
参数说明

参数类型描述pipelinearray一系列数据聚合操作或阶段。详见聚合管道操作符 在版本2.6中更改:该方法仍然可以将流水线阶段作为单独的参数接受,而不是作为数组中的元素;但是,如果不将管道指定为数组,则不能指定options参数optionsdocument可选。 aggregate()传递给聚合命令的其他选项。 2.6版中的新增功能:仅当将管道指定为数组时才可用。注意事项

​                使用db.collection.aggregate()直接查询会提示错误,但是传一个空数组如db.collection.aggregate([])则不会报错,且会和find一样返回所有文档。
常用聚合管道

与mysql聚合类比

为了便于理解,先将常见的mongo的聚合操作和mysql的查询做下类比
SQL 操作/函数mongodb聚合操作where$matchgroup by$grouphaving$matchselect$projectorder by$sortlimit$limitsum()$sumcount()$sumjoin$lookup$count

返回包含输入到stage的文档的计数,理解为返回与表或视图的find()查询匹配的文档的计数。db.collection.count()方法不执行find()操作,而是计数并返回与查询匹配的结果数。
语法
  1. { $count: <string> }
复制代码
$count阶段相当于下面$group+$project的序列:
  1. db.collection.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": null,
  5.             "count": {// 这里count自定义,相当于mysql的select count(*) as tables
  6.                 "$sum": 1
  7.             }
  8.         }
  9.     },
  10.     {
  11.         "$project": {// 返回不显示_id字段
  12.             "_id": 0
  13.         }
  14.     }
  15. ])
复制代码
示例

查询人数是100000以上的城市的数量

  1. db.zips.aggregate([
  2.     {
  3.         "$match": {
  4.             "pop": {
  5.                 "$gt": 100000
  6.             }
  7.         }
  8.     },
  9.     {
  10.         "$count": "count"
  11.     }
  12. ])
复制代码

$group

按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id字段,该字段按键包含不同的组。
​                输出文档还可以包含计算字段,该字段保存由$group的_id字段分组的一些accumulator表达式的值。 $group不会输出具体的文档而只是统计信息。
语法
  1. { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
复制代码
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超过此限制,$group将产生错误,但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group操作以写入临时文件。
注意:
示例

按照state分组,并计算每一个state分组的总人数,平均人数以及每个分组的数量
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$state",
  5.             "totalPop": {
  6.                 "$sum": "$pop"
  7.             },
  8.             "avglPop": {
  9.                 "$avg": "$pop"
  10.             },
  11.             "count": {
  12.                 "$sum": 1
  13.             }
  14.         }
  15.     }
  16. ])
复制代码

查找不重复的所有的state的值
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$state"
  5.         }
  6.     }
  7. ])
复制代码

按照city分组,并且分组内的state字段列表加入到stateItem并显示
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$city",
  5.             "stateItem": {
  6.                 "$push": "$state"
  7.             }
  8.         }
  9.     }
  10. ])
复制代码

下面聚合操作使用系统变量$$ROOT按item对文档进行分组,生成的文档不得超过BSON文档大小限制
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$city",
  5.             "item": {
  6.                 "$push": "$$ROOT"
  7.             }
  8.         }
  9.     }
  10. ]).pretty();
复制代码

$match

过滤文档,仅将符合指定条件的文档传递到下一个管道阶段。
$match接受一个指定查询条件的文档,查询语法与读操作查询语法相同。
语法
  1. { $match: { <query> } }
复制代码
管道优化

​                $match用于对文档进行筛选,之后可以在得到的文档子集上做聚合,$match可以使用除了地理空间之外的所有常规查询操作符,在实际应用中尽可能将$match放在管道的前面位置。这样有两个好处:
使用限制

示例

使用 $match做简单的匹配查询,查询缩写是NY的城市数据
  1. db.zips.aggregate([
  2.     {
  3.         "$match": {
  4.             "state": "NY"
  5.         }
  6.     }
  7. ]).pretty();
复制代码

使用$match管道选择要处理的文档,然后将结果输出到$group管道以计算文档的计数
  1. db.zips.aggregate([
  2.     {
  3.         "$match": {
  4.             "state": "NY"
  5.         }
  6.     },
  7.     {
  8.         "$group": {
  9.             "_id": null,
  10.             "sum": {
  11.                 "$sum": "$pop"
  12.             },
  13.             "avg": {
  14.                 "$avg": "$pop"
  15.             },
  16.             "count": {
  17.                 "$sum": 1
  18.             }
  19.         }
  20.     }
  21. ]).pretty();
复制代码

$unwind

从输入文档解构数组字段以输出每个元素的文档,简单说就是 可以将数组拆分为单独的文档。
语法

要指定字段路径,在字段名称前加上$符并用引号括起来。
  1. { $unwind: <field path> }
复制代码
v3.2+支持如下语法
  1. {
  2.   $unwind:
  3.     {
  4.       path: <field path>,
  5.       #可选,一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。
  6.       includeArrayIndex: <string>,
  7.       #可选,default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档
  8.       preserveNullAndEmptyArrays: <boolean>
  9.         }
  10. }
复制代码
​                如果为输入文档中不存在的字段指定路径,或者该字段为空数组,则$unwind默认会忽略输入文档,并且不会输出该输入文档的文档。
版本3.2中的新功能:要输出数组字段丢失的文档,null或空数组,请使用选项preserveNullAndEmptyArrays。
示例

以下聚合使用$unwind为loc数组中的每个元素输出一个文档:
  1. db.zips.aggregate([
  2.     {
  3.         "$match": {
  4.             "_id": "01002"
  5.         }
  6.     },
  7.     {
  8.         "$unwind": "$loc"
  9.     }
  10. ]).pretty();
复制代码

$project

$project可以从文档中选择想要的字段,和不想要的字段(指定的字段可以是来自输入文档或新计算字段的现有字段),也可以通过管道表达式进行一些复杂的操作,例如数学操作,日期操作,字符串操作,逻辑操作。
语法

$project 管道符的作用是选择字段(指定字段,添加字段,不显示字段,_id:0,排除字段等),重命名字段,派生字段。
  1. { $project: { <specification(s)> } }
复制代码
specifications有以下形式:
:   是否包含该字段,field:1/0,表示选择/不选择 field
_id:     是否指定_id字段
:  添加新字段或重置现有字段的值。 在版本3.6中更改:MongoDB 3.6添加变量REMOVE。如果表达式的计算结果为$$REMOVE,则该字段将排除在输出中。
:  v3.4新增功能,指定排除字段
示例

以下$project阶段的输出文档中只包含_id,city和state字段
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "_id": 1,
  5.             "city": 1,
  6.             "state": 1
  7.         }
  8.     }
  9. ]).pretty();
复制代码

_id字段默认包含在内。要从$ project阶段的输出文档中排除_id字段,请在project文档中将_id字段设置为0来指定排除_id字段。
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "_id": 0,
  5.             "city": 1,
  6.             "state": 1
  7.         }
  8.     }
  9. ]).pretty();
复制代码

以下$ project阶段从输出中排除loc字段
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "loc": 0
  5.         }
  6.     }
  7. ]).pretty();
复制代码

可以在聚合表达式中使用变量REMOVE来有条件地禁止一个字段,
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "_id": 1,
  5.             "city": 1,
  6.             "state": 1,
  7.             "pop": 1,
  8.             "loc": {
  9.                 "$cond": {
  10.                     "if": {
  11.                         "$gt": [
  12.                             "$pop",
  13.                             1000
  14.                         ]
  15.                     },
  16.                     "then": "$$REMOVE",
  17.                     "else": "$loc"
  18.                 }
  19.             }
  20.         }
  21.     }
  22. ]).pretty();
复制代码

我们还可以改变数据,将人数大于1000的城市坐标重置为0
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "_id": 1,
  5.             "city": 1,
  6.             "state": 1,
  7.             "pop": 1,
  8.             "loc": {
  9.                 "$cond": {
  10.                     "if": {
  11.                         "$gt": [
  12.                             "$pop",
  13.                             1000
  14.                         ]
  15.                     },
  16.                     "then": [
  17.                         0,
  18.                         0
  19.                     ],
  20.                     "else": "$loc"
  21.                 }
  22.             }
  23.         }
  24.     }
  25. ]).pretty();
复制代码

新增字段列
  1. db.zips.aggregate([
  2.     {
  3.         "$project": {
  4.             "_id": 1,
  5.             "city": 1,
  6.             "state": 1,
  7.             "pop": 1,
  8.             "desc": {
  9.                 "$cond": {
  10.                     "if": {
  11.                         "$gt": [
  12.                             "$pop",
  13.                             1000
  14.                         ]
  15.                     },
  16.                     "then": "人数过多",
  17.                     "else": "人数过少"
  18.                 }
  19.             },
  20.             "loc": {
  21.                 "$cond": {
  22.                     "if": {
  23.                         "$gt": [
  24.                             "$pop",
  25.                             1000
  26.                         ]
  27.                     },
  28.                     "then": [
  29.                         0,
  30.                         0
  31.                     ],
  32.                     "else": "$loc"
  33.                 }
  34.             }
  35.         }
  36.     }
  37. ]).pretty();
复制代码

$limit

限制传递到管道中下一阶段的文档数
语法
  1. { $limit: <positive integer> }
复制代码
示例,此操作仅返回管道传递给它的前5个文档。 $limit对其传递的文档内容没有影响。
  1. db.zips.aggregate({
  2.     "$limit": 5
  3. });
复制代码
注意

​                当$sort在管道中的$limit之前立即出现时,$sort操作只会在过程中维持前n个结果,其中n是指定的限制,而MongoDB只需要将n个项存储在内存中。当allowDiskUse为true并且n个项目超过聚合内存限制时,此优化仍然适用。
$skip

跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段
语法
  1. { $skip: <positive integer> }
复制代码
示例,此操作将跳过管道传递给它的前5个文档, $skip对沿着管道传递的文档的内容没有影响。
  1. db.zips.aggregate({
  2.     "$skip": 5
  3. });
复制代码
$sort

对所有输入文档进行排序,并按排序顺序将它们返回到管道。
语法
  1. { $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }
复制代码
$sort指定要排序的字段和相应的排序顺序的文档。 可以具有以下值之一:

示例

要对字段进行排序,请将排序顺序设置为1或-1,以分别指定升序或降序排序,如下例所示:
  1. db.zips.aggregate([
  2.     {
  3.         "$sort": {
  4.             "pop": -1,
  5.             "city": 1
  6.         }
  7.     }
  8. ])
复制代码

$sortByCount

根据指定表达式的值对传入文档分组,然后计算每个不同组中文档的数量。每个输出文档都包含两个字段:包含不同分组值的_id字段和包含属于该分组或类别的文档数的计数字段,文件按降序排列。
语法
  1. { $sortByCount:  <expression> }
复制代码
使用示例

下面举了一些常用的mongo聚合例子和mysql对比,假设有一条如下的数据库记录(表名:zips)作为例子:
统计所有数据

SQL的语法格式如下
  1. select count(1) from zips;
复制代码
mongoDB的语法格式
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": null,
  5.             "count": {
  6.                 "$sum": 1
  7.             }
  8.         }
  9.     }
  10. ])
复制代码

对所有城市人数求合

SQL的语法格式如下
  1. select sum(pop) AS tota from zips;
复制代码
mongoDB的语法格式
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": null,
  5.             "total": {
  6.                 "$sum": "$pop"
  7.             }
  8.         }
  9.     }
  10. ])
复制代码

对城市缩写相同的城市人数求合

SQL的语法格式如下
  1. select state,sum(pop) AS tota from zips group by state;
复制代码
mongoDB的语法格式
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$state",
  5.             "total": {
  6.                 "$sum": "$pop"
  7.             }
  8.         }
  9.     }
  10. ])
复制代码

state重复的城市个数

SQL的语法格式如下
  1. select state,count(1) AS total from zips group by state;
复制代码
mongoDB的语法格式
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$state",
  5.             "total": {
  6.                 "$sum": 1
  7.             }
  8.         }
  9.     }
  10. ])
复制代码

state重复个数大于100的城市

SQL的语法格式如下
  1. select state,count(1) AS total from zips group by state having count(1)>100;
复制代码
mongoDB的语法格式
  1. db.zips.aggregate([
  2.     {
  3.         "$group": {
  4.             "_id": "$state",
  5.             "total": {
  6.                 "$sum": 1
  7.             }
  8.         }
  9.     },
  10.     {
  11.         "$match": {
  12.             "total": {
  13.                 "$gt": 100
  14.             }
  15.         }
  16.     }
  17. ])
复制代码

本文由传智教育博学谷 - 狂野架构师教研团队发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4