MongoDB系列六(聚合).

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介:  一、概念     使用聚合框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering)、投射(projecting)、分组(grouping)、排序(sorting)、限制(limiting)和跳过(skipping)。

 一、概念

    使用聚合框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包括筛选(filtering)、投射(projecting)、分组(grouping)、排序(sorting)、限制(limiting)和跳过(skipping)。

二、聚合函数

db.driverLocation.aggregate(
    {"$match":{"areaCode":"350203"}},
    {"$project":{"driverUuid":1,"uploadTime":1,"positionType":1}},
    {"$group":{"_id":{"driverUuid":"$driverUuid","positionType":"$positionType"},"uploadTime":{"$first":{"$year":"$uploadTime"}},"count":{"$sum":1}}},
    {"$sort":{"count":-1}},
    {"$limit":100},
    {"$skip":50}
)

     管道操作符是按照书写的顺序依次执行的,每个操作符都会接受一连串的文档,对这些文档做一些类型转换,最后将转换后的文档作为结果传递给下一个操作符(对于最后一个管道操作符,是将结果返回给客户端),称为流式工作方式。

    大部分操作符的工作方式都是流式的,只要有新文档进入,就可以对新文档进行处理,但是"$group" 和 "$sort" 必须要等收到所有的文档之后,才能对文档进行分组排序,然后才能将各个分组发送给管道中的下一个操作符。这意味着,在分片的情况下,"$group" 或 "$sort"会先在每个分片上执行,然后各个分片上的分组结果会被发送到mongos再进行最后的统一分组,剩余的管道工作也都是在mongos(而不是在分片)上运行的。

    不同的管道操作符可以按任意顺序组合在一起使用,而且可以被重复任意多次。例如,可以先做"$match",然后做"$group",然后再做"$match"(与之前的"$match"匹配不同的查询条件)。

    $fieldname"语法是为了在聚合框架中引用fieldname字段。

  • 筛选(filtering)—> $match

    用于对文档集合进行筛选,之后就可以在筛选得到的文档子集上做聚合。例如,如果想对Oregon(俄勒冈州,简写为OR)的用户做统计,就可以使用{$match : {"state" :"OR"}}。"$match"可以使用所有常规的查询操作符("$gt"、"$lt"、"$in"等)。有一个例外需要注意:不能在"$match"中使用地理空间操作符。
    通常,在实际使用中应该尽可能将"$match"放在管道的前面位置。这样做有两个好处:一是可以快速将不需要的文档过滤掉,以减少管道的工作量;二是如果在投射和分组之前执行"$match",查询可以使用索引。

  • 投射(projecting)—> $project

    这个语法与查询中的字段选择器比较像:可以通过指定 {"fieldname" : 1} 选择需要投射的字段,或者通过指定 { "fieldname":0 } 排除不需要的字段。执行完这个"$project"操作之后,结果集中的每个文档都会以{"_id" : id, "fieldname" :"xxx"}这样的形式表示。这些结果只会在内存中存在,不会被写入磁盘。

    还可以对字段进行重命名:db.users.aggregate({"$project" : {"userId" : "$_id", "_id" : 0}}),在对字段进行重命名时,MongoDB并不会记录字段的历史名称。

  • 分组(grouping)—> $group

     如果选定了需要进行分组的字段,就可以将选定的字段传递给"$group"函数的"_id"字段。对于上面的例子:我们选择了driverUuid 和 positionType 当作我们分组的条件(当然只选择一个字段也是可以的)。分组过后,文档的 driverUuid 和 positionType 组成的对象就变成了文档的唯一标识(_id)。

  "count":{"$sum":1} 是为分组内每个文档的"count"字段加1。注意,新加入的文档中并不会有"count"字段;这"$group"创建的一个新字段。  

  • 排序(sorting)—> $sort

    排序方向可以是1(升序)和 -1(降序)。  

   可以根据任何字段(或者多个字段)进行排序,与在普通查询中的语法相同。如果要对大量的文档进行排序,强烈建议在管道的第一阶段进行排序,这时的排序操作可以使用索引。否则,排序过程就会比较慢,而且会占用大量内存。

  • 限制(limiting)—> $limit

   $limit会接受一个数字n,返回结果集中的前n个文档。

  • 跳过(skipping)—> $skip

   $skip也是接受一个数字n,丢弃结果集中的前n个文档,将剩余文档作为结果返回。在“普通”查询中,如果需要跳过大量的数据,那么这个操作符的效率会很低。在聚合中也是如此,因为它必须要先匹配到所有需要跳过的文档,然后再将这些文档丢弃。

  • 拆分(unwind)—> $unwind

   可以将数组中的每一个值拆分为单独的文档。

   例如文档:{ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] }

   聚合运算:db.inventory.aggregate( [ { $unwind : "$sizes" } ] )

   结果:

{ "_id" : 1, "item" : "ABC1", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "L" }

Spring Data MongoDB 中使用聚合函数:

    /**
     * db.driverLocation.aggregate(
     *     {"$match":{"areaCode":"350203"}},
     *     {"$project":{"driverUuid":1,"uploadTime":1,"positionType":1}},
     *     {"$group":{"_id":{"driverUuid":"$driverUuid","positionType":"$positionType"},"uploadTime":{"$first":{"$year":"$uploadTime"}},"count":{"$sum":1}}},
     *     {"$sort":{"count":-1}},
     *     {"$limit":100},
     *     {"$skip":50}
     * )
     */
    @Test
    public void test04(){
        //match
        Criteria criteria = Criteria.where("350203").is("350203");
        AggregationOperation matchOperation = Aggregation.match(criteria);
        //project
        AggregationOperation projectionOperation = Aggregation.project("driverUuid", "uploadTime", "positionType");
        //group
        AggregationOperation groupOperation = Aggregation.group("driverUuid", "positionType")
                .first(DateOperators.dateOf("uploadTime").year()).as("uploadTime")
                .count().as("count");
        //sort
        Sort sort = new Sort(Sort.Direction.DESC, "count");
        AggregationOperation sortOperation = Aggregation.sort(sort);
        //limit
        AggregationOperation limitOperation = Aggregation.limit(100L);
        //skip
        AggregationOperation skipOperation = Aggregation.skip(50L);

        Aggregation aggregation = Aggregation.newAggregation(matchOperation, projectionOperation, groupOperation, sortOperation, limitOperation, skipOperation);
        AggregationResults<Object> driverLocation = mongoOperations.aggregate(aggregation, "driverLocation", Object.class);
        List<Object> mappedResults = driverLocation.getMappedResults();

    }

 

三、聚合管道操作符

    MongoDB提供了很多的操作符用来文档聚合后字段间的运算或者分组内的统计,比如上文提到的$sum、$first、$year 等。MongoDB提供了包括分组操作符、数学操作符、日期操作符、字符串表达式 等等 一系列的操作符...

  • 分组操作符

类似 SQL中分组后的操作,只适用于分组后的统计工作,不适用于单个文档。

  1. {"$sum" : value}  对于分组中的每一个文档,将value与计算结果相加。
  2. {"$avg" : value} 返回每个分组的平均值
  3. {"$max" : expr} 返回分组内的最大值。
  4. {"$min" : expr} 返回分组内的最小值。
  5. {"$first" : expr} 返回分组的第一个值,忽略后面所有值。只有排序之后,明确知道数据顺序时这个操作才有意义。
  6. {"$last" : expr} 与"$first"相反,返回分组的最后一个值。
  7. {"$addToSet" : expr} 针对数组字段, 如果当前数组中不包含expr ,那就将它添加到数组中。在返回结果集中,每个元素最多只出现一次,而且元素的顺序是不确定的。
  8. {"$push" : expr} 针对数组字段,不管expr是什么值,都将它添加到数组中。返回包含所有值的数组。
  • 数学操作符

适用于单个文档的运算。

  1. {"$add" : [expr1[, expr2, ..., exprN]]} 这个操作符接受一个或多个表达式作为参数,将这些表达式相加。
  2. {"$subtract" : [expr1, expr2]} 接受两个表达式作为参数,用第一个表达式减去第二个表达式作为结果。
  3. {"$multiply" : [expr1[, expr2, ..., exprN]]} 接受一个或者多个表达式,并且将它们相乘。
  4. {"$divide" : [expr1, expr2]} 接受两个表达式,用第一个表达式除以第二个表达式的商作为结果。
  5. {"$mod" : [expr1, expr2]} 接受两个表达式,将第一个表达式除以第二个表达式得到的余数作为结果。
  • 字符串表达式

适用于单个文档的运算。

  1. {$substr" : [expr, startOffset, numToReturn]} 其中第一个参数expr必须是个字符串,这个操作会截取这个字符串的子串(从第startOffset字节开始的numToReturn字节,注意,是字节,不是字符。在多字节编码中尤其要注意这一点)expr必须是字符串。
  2. {"$concat" : [expr1[, expr2, ..., exprN]]} 将给定的表达式(或者字符串)连接在一起作为返回结果。
  3. {"$toLower" : expr} 参数expr必须是个字符串值,这个操作返回expr的小写形式。
  4. {"$toUpper" : expr} 参数expr必须是个字符串值,这个操作返回expr的大写形式。
  • 逻辑表达式

适用于单个文档的运算,通过这些操作符,就可以在聚合中使用更复杂的逻辑,可以对不同数据执行不同的代码,得到不同的结果。

  1. {$cmp" : [expr1, expr2]} 比较expr1和expr2。如果expr1等于expr2,返回0;如果expr1 < expr2,返回一个负数;如果expr1 > expr2,返回一个正数。
  2. {"$strcasecmp" : [string1, string2]} 比较string1和string2,区分大小写。只对 ASCII 组成的字符串有效。
  3. {"$eq"/"$ne"/"$gt"/"$gte"/"$lt"/"$lte" : [expr1, expr2]} 对expr1和expr2执行相应的比较操作,返回比较的结果(true或false)。
  4. {"$and" : [expr1[, expr2, ..., exprN]]} 如果所有表达式的值都是true,那就返回true,否则返回false。
  5. {"$or" : [expr1[, expr2, ..., exprN]]} 只要有任意表达式的值为true,就返回true,否则返回false。
  6. {"$not" : expr} 对expr取反。
  7. {"$cond" : [booleanExpr, trueExpr, falseExpr]} 如果booleanExpr的值是true,那就返回trueExpr,否则返回falseExpr。
  8. {"$ifNull" : [expr, replacementExpr]} 如果expr是null,返回replacementExpr,否则返回expr。
  • 日期表达式

适用于单个文档的运算,只能对日期类型的字段进行日期操作,不能对非日期类型字段做日期操作。

  1. {$year: "$date" } 返回日期的年份部分
  2. {$month: "$date" } 返回日期的月份部分
  3. {$dayOfMonth: "$date" } 返回日期的天部分
  4. {$hour: "$date" } 返回日期的小时部分
  5. {$minute: "$date" } 返回日期的分钟部分
  6. {$second: "$date" } 返回日期的秒部分
  7. {$millisecond: "$date" } 返回日期的毫秒部分
  8. {$dayOfYear: "$date" } 一年中的第几天
  9. {$dayOfWeek: "$date" } 一周中的第几天,between 1 (Sunday) and 7 (Saturday).
  10. {$week: "$date" } 以0到53之间的数字返回一年中日期的周数。周从星期日开始,第一周从一年中的第一个星期天开始。一年中第一个星期日之前的日子是在第0周。

可参考:https://docs.mongodb.com/manual/reference/operator/aggregation/

四、结语

    应该尽量在管道的开始阶段(执行"$project"、"$group"或者"$unwind"操作之前)就将尽可能多的文档和字段过滤掉。管道如果不是直接从原先的集合中使用数据,那就无法在筛选和排序中使用索引。如果可能,聚合管道会尝试对操作进行排序,以便能够有效使用索引。

    MongoDB不允许单一的聚合操作占用过多的系统内存:如果MongoDB发现某个聚合操作占用了20%以上的内存,这个操作就会直接输出错误。允许将输出结果利用管道放入一个集合中是为了方便以后使用(这样可以将所需的内存减至最小)。

    这篇文章主要摘录自《MongoDB权威指南第二版》,Mongo系列的最后一篇文章了,最近学MongoDB学得头都有点大了,准备换个方向学学了...共勉!

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
目录
相关文章
|
2月前
|
SQL NoSQL Unix
MongoDB 聚合
10月更文挑战第17天
26 4
|
3月前
|
SQL NoSQL Unix
MongoDB聚合操作总结
这篇文章总结了MongoDB中聚合操作的作用、方法、常见聚合表达式以及聚合管道的概念和常用操作符,以及SQL与MongoDB聚合操作的对应关系。
45 2
MongoDB聚合操作总结
|
2月前
|
SQL NoSQL 数据处理
深入探索MongoDB的聚合操作
【10月更文挑战第13天】
23 0
|
3月前
|
NoSQL MongoDB 数据库
python3操作MongoDB的crud以及聚合案例,代码可直接运行(python经典编程案例)
这篇文章提供了使用Python操作MongoDB数据库进行CRUD(创建、读取、更新、删除)操作的详细代码示例,以及如何执行聚合查询的案例。
35 6
|
3月前
|
SQL NoSQL JavaScript
04 MongoDB各种查询操作 以及聚合操作总结
文章全面总结了MongoDB中的查询操作及聚合操作,包括基本查询、条件筛选、排序以及聚合管道的使用方法和实例。
95 0
|
4月前
|
持续交付 jenkins Devops
WPF与DevOps的完美邂逅:从Jenkins配置到自动化部署,全流程解析持续集成与持续交付的最佳实践
【8月更文挑战第31天】WPF与DevOps的结合开启了软件生命周期管理的新篇章。通过Jenkins等CI/CD工具,实现从代码提交到自动构建、测试及部署的全流程自动化。本文详细介绍了如何配置Jenkins来管理WPF项目的构建任务,确保每次代码提交都能触发自动化流程,提升开发效率和代码质量。这一方法不仅简化了开发流程,还加强了团队协作,是WPF开发者拥抱DevOps文化的理想指南。
92 1
|
4月前
|
NoSQL BI 数据处理
【超实用攻略】MongoDB 聚合框架:从入门到精通,带你解锁数据处理新姿势!
【8月更文挑战第24天】MongoDB是一款以其灵活性和高性能闻名的NoSQL数据库。其强大的聚合框架采用管道式处理,允许用户定义多个数据处理阶段如过滤、分组等。本文通过示例数据库`orders`和`products`,演示如何利用聚合框架计算各产品的总销售额。示例代码展示了使用`$lookup`连接两集合、`$unwind`打平数组及`$group`按产品ID分组并计算总销售额的过程。这突显了聚合框架处理复杂查询的强大能力,是进行数据分析和报表生成的理想选择。
58 3
|
4月前
|
存储 NoSQL JavaScript
MongoDB存储过程实战:聚合框架、脚本、最佳实践,一文全掌握!
【8月更文挑战第24天】MongoDB是一款备受欢迎的文档型NoSQL数据库,以灵活的数据模型和强大功能著称。尽管其存储过程支持不如传统关系型数据库,本文深入探讨了MongoDB在此方面的最佳实践。包括利用聚合框架处理复杂业务逻辑、封装业务逻辑提高复用性、运用JavaScript脚本实现类似存储过程的功能以及考虑集成其他工具提升数据处理能力。通过示例代码展示如何创建订单处理集合并定义验证规则,虽未直接实现存储过程,但有效地演示了如何借助JavaScript脚本处理业务逻辑,为开发者提供更多实用指导。
77 2
|
4月前
|
存储 NoSQL 数据处理
【MongoDB大神级操作】揭秘聚合框架,让你的数据处理能力瞬间飙升,秒变数据界的超级英雄!
【8月更文挑战第24天】MongoDB是一款备受欢迎的非关系型数据库,以其灵活的文档模型和出色的可扩展性著称。其聚合框架尤其亮眼,能高效地对数据库中的数据执行复杂的转换与聚合操作,无需将数据导出到应用端处理,极大提升了数据处理的效率与灵活性。例如,在一个大型电商数据库中,聚合框架能轻松分析出最热卖的商品或特定时段内某类别商品的销售总额。通过一系列管道操作,如$unwind、$group等,可以对数据进行逐步处理并得到最终结果,同时还支持过滤、排序、分页等多种操作,极大地丰富了数据处理的能力,成为进行数据分析、报表生成及复杂业务逻辑实现的强大工具。
78 2
|
4月前
|
持续交付 jenkins C#
“WPF与DevOps深度融合:从Jenkins配置到自动化部署全流程解析,助你实现持续集成与持续交付的无缝衔接”
【8月更文挑战第31天】本文详细介绍如何在Windows Presentation Foundation(WPF)项目中应用DevOps实践,实现自动化部署与持续集成。通过具体代码示例和步骤指导,介绍选择Jenkins作为CI/CD工具,结合Git进行源码管理,配置构建任务、触发器、环境、构建步骤、测试及部署等环节,显著提升开发效率和代码质量。
80 0