「简明教程」轻松掌握 MongDB 流式聚合操作

「简明教程」轻松掌握 MongDB 流式聚合操作

信息科学中的聚合是指对相关数据进行内容筛选、处理和归类并输出结果的过程。MongoDB 中的聚合是指同时对多个文档中的数据进行处理、筛选和归类并输出结果的过程。数据在聚合操作的过程中,就像是水流过一节一节的管道一样,所以 MongoDB 中的聚合又被人称为流式聚合。

MongoDB 提供了几种聚合方式:

  • Aggregation Pipeline
  • Map-Reduce
  • 简单聚合

接下来,我们将全方位地了解 MongoDB 中的聚合。

Aggregation Pipeline

Aggregation Pipeline 又称聚合管道。开发者可以将多个文档传入一个由多个 Stage 组成的 Pipeline,每一个 Stage 处理的结果将会传入下一个 Stage 中,最后一个 Stage 的处理结果就是整个 Pipeline 的输出。

创建聚合管道的语法如下:

复制代码db.collection.aggregate( [ { <stage> }, ... ] )

MongoDB 提供了 23 种 Stage,它们是:

Stage描述
$addFields向文档添加新字段。
$bucket根据指定的表达式和存储区边界将传入的文档分组。
$bucketAuto根据指定的表达式将传入的文档分类为特定数量的组,自动确定存储区边界。
$collStats返回有关集合或视图的统计信息。
$count返回聚合管道此阶段的文档数量计数。
$facet在同一组输入文档的单个阶段内处理多个聚合操作。
$geoNear基于与地理空间点的接近度返回有序的文档流。
$graphLookup对集合执行递归搜索。
$group按指定的标识符表达式对文档进行分组。
$indexStats返回集合的索引信息。
$limit将未修改的前 n 个文档传递给管道。
$listSessions列出system.sessions集合的所有会话。
$lookup对同一数据库中的另一个集合执行左外连接。
$match过滤文档,仅允许匹配的文档地传递到下一个管道阶段。
$out将聚合管道的结果文档写入指定集合,它必须是管道中的最后一个阶段。
$project为文档添加新字段或删除现有字段。
$redact可用于实现字段级别的编辑。
$replaceRoot用指定的嵌入文档替换文档。该操作将替换输入文档中的所有现有字段,包括_id字段。指定嵌入在输入文档中的文档以将嵌入文档提升到顶层。
$sample从输入中随机选择指定数量的文档。
$skip跳过前 n 个文档,并将未修改的其余文档传递到下一个阶段。
$sort按指定的排序键重新排序文档流。只有订单改变; 文件保持不变。对于每个输入文档,输出一个文档。
$sortByCount对传入文档进行分组,然后计算每个不同组中的文档计数。
$unwind解构文档中的数组字段。

文档、StagePipeline 的关系如下图所示:

在这里插入图片描述

上图描述了文档经过 $match$sample$project 等三个 Stage 并输出的过程。SQL 中常见的聚合术语有 WHERESUMCOUNT 等。下表描述了常见的 SQL 聚合术语、函数和概念以及对应的 MongoDB 操作符或 Stage

SQLMongoDB
WHERE$match
GROUP BY$group
HAVING$match
SELECT$project
ORDER BY$sort
LIMIT$limit
SUM()$sum
COUNT()$sum$sortByCount
join$lookup

下面,我们将通过示例了解 AggregateStagePipeline 之间的关系。

概念浅出

$match 的描述为“过滤文档,仅允许匹配的文档地传递到下一个管道阶段”。其语法格式如下:

复制代码{ $match: { <query> } }

在开始学习之前,我们需要准备以下数据:

复制代码> db.artic.insertMany([
... { "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 },
... { "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 },
... { "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 },
... { "_id" : 4, "author" : "line", "score" : 55, "views" : 300 }
... ])

然后我们建立只有一个 StagePipeline,以实现过滤出 authordave 的文档。对应示例如下:

复制代码> db.artic.aggregate([
... {$match: {author: "dave"}}
... ])
{ "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : 2, "author" : "dave", "score" : 85, "views" : 521 }

如果要建立有两个 StagePipeline,那么就在 aggregate 中添加一个 Stage 即可。现在有这样一个需求:统计集合 articscore 大于 70 且小于 90 的文档数量。这个需求分为两步进行:

  • 过滤出符合要求的文档
  • 统计文档数量

Aggregation 非常适合这种多步骤的操作。在这个场景中,我们需要用到 $match$group 这两个 Stage ,然后再与聚合表达式 $sum 相结合,对应示例如下:

复制代码> db.artic.aggregate([
... {$match: {score: {$gt: 70, $lt: 90}}},
... {$group: {_id: null, number: {$sum: 1}}}
... ])
{ "_id" : null, "number" : 2 }

这个示例的完整过程可以用下图表示:

在这里插入图片描述

通过上面的描述和举例,我相信你对 AggregateStagePipeline 有了一定的了解。接下来,我们将学习常见的 Stage 的语法和用途。

常见的 Stage

sample

$sample 的作用是从输入中随机选择指定数量的文档,其语法格式如下:

复制代码{ $sample: { size: <positive integer> } }

假设要从集合 artic 中随机选择两个文档,对应示例如下:

复制代码> db.artic.aggregate([
... {$sample: {size: 2}}
... ])
{ "_id" : 1, "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : 3, "author" : "anna", "score" : 60, "views" : 706 }

size 对应的值必须是正整数,如果输入负数会得到错误提示:size argument to $sample must not be negative。要注意的是,当值超过集合中的文档数量时,返回结果是集合中的所有文档,但文档顺序是随机的。

project

$project 的作用是过滤文档中的字段,这与投影操作相似,但处理结果将会传入到下一个阶段 。其语法格式如下:

复制代码{ $project: { <specification(s)> } }

准备以下数据:

复制代码> db.projects.save(
	{_id: 1, title: "篮球训练营青春校园活动开始啦", numb: "A829Sck23", author: {last: "quinn", first: "James"}, hot: 35}
)

假设 Pipeline 中的下一个 Stage 只需要文档中的 titleauthor 字段,对应示例如下:

复制代码> db.projects.aggregate([{$project: {title: 1, author: 1}}])
{ "_id" : 1, "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }

01 可以同时存在。对应示例如下:

复制代码> db.projects.aggregate([{$project: {title: 1, author: 1, _id: 0}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }

true 等效于 1false 等效于 0,也可以混用布尔值和数字,对应示例如下:

复制代码> db.projects.aggregate([{$project: {title: 1, author: true, _id: false}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "last" : "quinn", "first" : "James" } }

如果想要排除指定字段,那么在 $project 中将其设置为 0false 即可,对应示例如下:

复制代码> db.projects.aggregate([{$project: {author: false, _id: false}}])
{ "title" : "篮球训练营青春校园活动开始啦", "numb" : "A829Sck23", "hot" : 35 }

$project 也可以作用于嵌入式文档。对于 author 字段,有时候我们只需要 FirstName 或者 Lastname ,对应示例如下:

复制代码> db.projects.aggregate([{$project: {author: {"last": false}, _id: false, numb: 0}}])
{ "title" : "篮球训练营青春校园活动开始啦", "author" : { "first" : "James" }, "hot" : 35 }

这里使用 {author: {"last": false}} 过滤掉 LastName,但保留 first

以上就是 $project 的基本用法和作用介绍,更多与 $project 相关的知识可查阅官方文档 $project。

lookup

$lookup 的作用是对同一数据库中的集合执行左外连接,其语法格式如下:

复制代码{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

左外连接类似与下面的伪 SQL 语句:

复制代码SELECT *, <output array field>
FROM collection WHERE <output array field> IN (
SELECT * FROM <collection to join> WHERE 
<foreignField>= <collection.localField>);

lookup 支持的指令及对应描述如下:

领域描述
from指定集合名称。
localField指定输入 $lookup 中的字段。
foreignField指定from 给定的集合中的文档字段。
as指定要添加到输入文档的新数组字段的名称。 新数组字段包含from集合中的匹配文档。 如果输入文档中已存在指定的名称,则会覆盖现有字段 。

准备以下数据:

复制代码> db.sav.insert([
   { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },
   { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },
   { "_id" : 3  }
])

> db.avi.insert([
   { "_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 }
])

假设要连接集合 sav 中的 item 和集合 avi 中的 sku,并将连接结果命名为 savi。对应示例如下:

复制代码> db.sav.aggregate([
   {
     $lookup:
       {
         from: "avi",
         localField: "item",
         foreignField: "sku",
         as: "savi"
       }
  }
])

命令执行后,输出如下内容:

复制代码{
   "_id" : 1,
   "item" : "almonds",
   "price" : 12,
   "quantity" : 2,
   "savi" : [
      { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 }
   ]
}
{
   "_id" : 2,
   "item" : "pecans",
   "price" : 20,
   "quantity" : 1,
   "savi" : [
      { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 }
   ]
}
{
   "_id" : 3,
   "savi" : [
      { "_id" : 5, "sku" : null, "description" : "Incomplete" },
      { "_id" : 6 }
   ]
}

上面的连接操作等效于下面这样的伪 SQL:

复制代码SELECT *, savi
FROM sav
WHERE savi IN (SELECT *
FROM avi
WHERE sku= sav.item);

以上就是 lookup 的基本用法和作用介绍,更多与 lookup 相关的知识可查阅官方文档 lookup。

unwind

unwind 能将包含数组的文档拆分称多个文档,其语法格式如下:

复制代码{
  $unwind:
    {
      path: <field path>,
      includeArrayIndex: <string>,
      preserveNullAndEmptyArrays: <boolean>
    }
}

unwind 支持的指令及对应描述如下:

指令类型描述
pathstring指定数组字段的字段路径, 必填。
includeArrayIndexstring用于保存元素的数组索引的新字段的名称。
preserveNullAndEmptyArraysboolean默认情况下,如果pathnull、缺少该字段或空数组, 则不输出文档。反之,将其设为 true 则会输出文档。

在开始学习之前,我们需要准备以下数据:

复制代码> db.shoes.save({_id: 1, brand: "Nick", sizes: [37, 38, 39]})

集合 shoes 中的 sizes 是一个数组,里面有多个尺码数据。假设要将这个文档拆分成 3 个 size 为单个值的文档,对应示例如下:

复制代码> db.shoes.aggregate([{$unwind : "$sizes"}])
{ "_id" : 1, "brand" : "Nick", "sizes" : 37 }
{ "_id" : 1, "brand" : "Nick", "sizes" : 38 }
{ "_id" : 1, "brand" : "Nick", "sizes" : 39 }

显然,这样的文档更方便我们做数据处理。preserveNullAndEmptyArrays 指令默认为 false,也就是说文档中指定的 path 为空、null 或缺少该 path 的时候,会忽略掉该文档。假设数据如下:

复制代码> db.shoes2.insertMany([
{"_id": 1, "item": "ABC", "sizes": ["S", "M", "L"]},
{"_id": 2, "item": "EFG", "sizes": [ ]},
{"_id": 3, "item": "IJK", "sizes": "M"},
{"_id": 4, "item": "LMN" },
{"_id": 5, "item": "XYZ", "sizes": null}
])

我们执行以下命令:

复制代码> db.shoes2.aggregate([{$unwind: "$sizes"}])

就会得到如下输出:

复制代码{ "_id" : 1, "item" : "ABC", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC", "sizes" : "L" }
{ "_id" : 3, "item" : "IJK", "sizes" : "M" }

_id245 的文档由于满足 preserveNullAndEmptyArrays 的条件,所以不会被拆分。

以上就是 unwind 的基本用法和作用介绍,更多与 unwind 相关的知识可查阅官方文档 unwind。

out

out 的作用是聚合 Pipeline 返回的结果文档,并将其写入指定的集合。要注意的是,out 操作必须出现在 Pipeline 的最后。out 语法格式如下:

复制代码{ $out: "<output-collection>" }

准备以下数据:

复制代码> db.books.insertMany([
{ "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 },
{ "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 },
{ "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 },
{ "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 },
{ "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 }
])

假设要集合 books 的分组结果保存到名为 books_result 的集合中,对应示例如下:

复制代码> db.books.aggregate([
... { $group : {_id: "$author", books: {$push: "$title"}}},
... { $out : "books_result" }
... ])

命令执行后,MongoDB 将会创建 books_result 集合,并将分组结果保存到该集合中。集合 books_result 中的文档如下:

复制代码{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] }
{ "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] }

以上就是 out 的基本用法和作用介绍,更多与 out 相关的知识可查阅官方文档 out。

Map-Reduce

Map-reduce 用于将大量数据压缩为有用的聚合结果,其语法格式如下:

复制代码db.runCommand(
               {
                 mapReduce: <collection>,
                 map: <function>,
                 reduce: <function>,
                 finalize: <function>,
                 out: <output>,
                 query: <document>,
                 sort: <document>,
                 limit: <number>,
                 scope: <document>,
                 jsMode: <boolean>,
                 verbose: <boolean>,
                 bypassDocumentValidation: <boolean>,
                 collation: <document>,
                 writeConcern: <document>
               }
             )

其中,db.runCommand({mapReduce: <collection>}) 也可以写成 db.collection.mapReduce()。各指令的对应描述如下:

指令类型描述
mapReducecollection集合名称,必填。
mapfunctionJavaScript 函数,必填。
reducefunctionJavaScript 函数,必填。
outstring or document指定输出结果,必填。
querydocument查询条件语句。
sortdocument对文档进行排序。
limitnumber指定输入到 map 中的最大文档数量。
finalizefunction修改 reduce 的输出。
scopedocument指定全局变量。
jsModeboolean是否在执行mapreduce 函数之间将中间数据转换为 BSON 格式,默认 false
verboseboolean结果中是否包含 timing 信息,默认 false
bypassDocumentValidationboolean是否允许 mapReduce在操作期间绕过文档验证,默认 false
collationdocument指定要用于操作的排序规则。
writeConcerndocument指定写入级别,不填写则使用默认级别。
简单的 mapReduce

一个简单的 mapReduce 语法示例如下:

复制代码var mapFunction = function() { ... };
var reduceFunction = function(key, values) { ... };
db.runCommand(
... {
... ... mapReduce: <input-collection>,
... ... map: mapFunction,
... ... reduce: reduceFunction,
... ... out: { merge: <output-collection> },
... ... query: <query>
... })

map 函数负责将每个输入的文档转换为零个或多个文档。map 结构如下:

复制代码function() {
   ...
   emit(key, value);
}

emit 函数的作用是分组,它接收两个参数:

  • key:指定用于分组的字段。
  • value:要聚合的字段。

map 中可以使用 this 关键字引用当前文档。reduce 结构如下:

复制代码function(key, values) {
   ...
   return result;
}

reduce 执行具体的数据处理操作,它接收两个参数:

  • key:与 map 中的 key 相同,即分组字段。
  • values:根据分组字段,将相同 key 的值放到同一个数组,values 就是包含这些分类数组的对象。

out 用于指定结果输出,out: <collectionName> 会将结果输出到新的集合,或者使用以下语法将结果输出到已存在的集合中:

复制代码out: { <action>: <collectionName>
        [, db: <dbName>]
        [, sharded: <boolean> ]
        [, nonAtomic: <boolean> ] }

要注意的是,如果 out 指定的 collection 已存在,那么它就会覆盖该集合。在开始学习之前,我们需要准备以下数据:

复制代码> db.mprds.insertMany([
... {_id: 1, numb: 3, score: 9, team: "B"},
... {_id: 2, numb: 6, score: 9, team: "A"},
... {_id: 3, numb: 24, score: 9, team: "A"},
... {_id: 4, numb: 6, score: 8, team: "A"}
... ])

接着定义 map 函数、reduce 函数,并将其应用到集合 mrexample 上。然后为输出结果指定存放位置,这里将输出结果存放在名为 mrexample_result 的集合中。

复制代码> var func_map = function(){emit(this.numb, this.score);};
> var func_reduce = function(key, values){return Array.sum(values);};
> db.mprds.mapReduce(func_map, func_reduce, {query: {team: "A"}, out: "mprds_result"})

map 函数指定了结果中包含的两个键,并将 this.class 相同的文档输出到同一个文档中。reduce 则对传入的列表进行求和,求和结果作为结果中的 value 。命令执行完毕后,结果会被存放在集合 mprds_result 中。用以下命令查看结果:

复制代码> db.mprds_result.find()
{ "_id" : 6, "value" : 17 }
{ "_id" : 24, "value" : 9 }

结果文档中的 _idmap 中的 this.numbvaluereduce 函数的返回值。

下图描述了此次 mapReduce 操作的完整过程:

在这里插入图片描述

finallize 剪枝

finallize 用于修改 reduce 的输出结果,其语法格式如下:

复制代码function(key, reducedValue) {
   ...
   return modifiedObject;
}

它接收两个参数:

key,与 map 中的 key 相同,即分组字段。

reducedValue,一个 Obecjt,是reduce 的输出。

上面我们介绍了 mapreduce,并通过一个简单的示例了解 mapReduce 的基本组成和用法。实际上我们还可以编写功能更丰富的 reduce 函数,甚至使用 finallize 修改 reduce 的输出结果。以下 reduce 函数将传入的 values 进行计算和重组,返回一个 reduceVal 对象:

复制代码> var func_reduce2 = function(key, values){
	reduceVal = {team: key, score: values, total: Array.sum(values), count: values.length};
	return reduceVal;
};

reduceVal 对象中包含 teamscoretotalcount 四个属性。但我们还想为其添加 avg 属性,那么可以在 finallize 函数中执行 avg 值的计算和 avg 属性的添加工作:

复制代码> var func_finalize = function(key, values){
	values.avg = values.total / values.count;
	return values;
};

map 保持不变,将这几个函数作用于集合 mprds 上,对应示例如下:

复制代码> db.mprds.mapReduce(func_map, func_reduce2, {query: {team: "A"}, out: "mprds_result", finalize: func_finalize})

命令执行后,结果会存入指定的集合中。此时,集合 mprds_result 内容如下:

复制代码{ "_id" : 6, "value" : { "team" : 6, "score" : [ 9, 8 ], "total" : 17, "count" : 2, "avg" : 8.5 } }
{ "_id" : 24, "value" : 9 }

下图描述了此次 mapReduce 操作的完整过程:

在这里插入图片描述

finallizereduce 后面使用,微调 reduce 的处理结果。这着看起来像是一个园丁在修剪花圃的枝丫,所以人们将 finallize 形象地称为“剪枝”。

要注意的是:map 会将 key 值相同的文档中的 value 归纳到同一个对象中,这个对象会经过 reducefinallize。对于 key 值唯一的那些文档,指定的 keyvalue 会被直接输出。

简单的聚合

除了 Aggregation Pipeline 和 Map-Reduce 这些复杂的聚合操作之外,MongoDB 还支持一些简单的聚合操作,例如 countgroupdistinct 等。

count

count 用于计算集合或视图中的文档数,返回一个包含计数结果和状态的文档。其语法格式如下:

复制代码{
  count: <collection or view>,
  query: <document>,
  limit: <integer>,
  skip: <integer>,
  hint: <hint>,
  readConcern: <document>
}

count 支持的指令及对应描述如下:

指令类型描述
countstring要计数的集合或视图的名称,必填。
querydocument查询条件语句。
limitinteger指定要返回的最大匹配文档数。
skipinteger指定返回结果之前要跳过的匹配文档数。
hintstring or document指定要使用的索引,将索引名称指定为字符串或索引规范文档。

假设要统计集合 mprds 中的文档数量,对应示例如下:

复制代码> db.runCommand({count: 'mprds'})
{ "n" : 4, "ok" : 1 }

假设要统计集合 mprdsnumb6 的文档数量,对应示例如下:

复制代码> db.runCommand({count: 'mprds', query: {numb: {$eq: 6}}})
{ "n" : 2, "ok" : 1 }

指定返回结果之前跳过 1 个文档,对应示例如下:

复制代码> db.runCommand({count: 'mprds', query: {numb: {$eq: 6}}, skip: 1})
{ "n" : 1, "ok" : 1 }

更多关于 count 的知识可查阅官方文档 Count。

group

group 的作用是按指定的键对集合中的文档进行分组,并执行简单的聚合函数,它与 SQL 中的 SELECT ... GROUP BY 类似。其语法格式如下:

复制代码{
  group:
   {
     ns: <namespace>,
     key: <key>,
     $reduce: <reduce function>,
     $keyf: <key function>,
     cond: <query>,
     finalize: <finalize function>
   }
}

group 支持的指令及对应描述如下:

指令类型描述
nsstring通过操作执行组的集合,必填。
keyducoment要分组的字段或字段,必填。
$reducefunction在分组操作期间对文档进行聚合操作的函数。 该函数有两个参数:当前文档和该组的聚合结果文档。 必填。
initialdocument初始化聚合结果文档, 必填。
$keyffunction替代 key。指定用于创建“密钥对象”以用作分组密钥的函数。 使用$keyf而不是 key按计算字段而不是现有文档字段进行分组。
conddocument用于确定要处理的集合中的哪些文档的选择标准。 如果省略,group 会处理集合中的所有文档。
finalizefunction在返回结果之前运行,此函数可以修改结果文档。

准备以下数据:

复制代码> db.sales.insertMany([
{_id: 1, orderDate: ISODate("2012-07-01T04:00:00Z"), shipDate: ISODate("2012-07-02T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香槟金"}},
{_id: 2, orderDate: ISODate("2012-07-03T05:20:00Z"), shipDate: ISODate("2012-07-04T09:00:00Z"), attr: {name: "高邦篮球鞋", price: 1999, size: 43, color: "狮王棕"}},
{_id: 3, orderDate: ISODate("2012-07-03T05:20:10Z"), shipDate: ISODate("2012-07-04T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香槟金"}},
{_id: 4, orderDate: ISODate("2012-07-05T15:11:33Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "极速跑鞋", price: 500, size: 43, color: "西湖蓝"}},
{_id: 5, orderDate: ISODate("2012-07-05T20:22:09Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "新款椰子鞋", price: 2999, size: 42, color: "香槟金"}},
{_id: 6, orderDate: ISODate("2012-07-05T22:35:20Z"), shipDate: ISODate("2012-07-06T09:00:00Z"), attr: {name: "透气网跑", price: 399, size: 38, color: "玫瑰红"}}
])

假设要将集合 sales 中的文档按照 attr.name 进行分组,并限定参与分组的文档的 shipDate 大于指定时间。对应示例如下:

复制代码> db.runCommand({
    group:{
    	ns: 'sales',
      key: {"attr.name": 1},
      cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}},
      $reduce: function(curr, result){},
      initial: {}
    }
})

命令执行后,会返回一个结果档。其中, retval 包含指定字段 attr.name 的数据,count 为参与分组的文档数量,keys 代表组的数量,ok 代表文档状态。结果文档如下:

复制代码{
	"retval" : [
		{
			"attr.name" : "高邦篮球鞋"
		},
		{
			"attr.name" : "新款椰子鞋"
		},
		{
			"attr.name" : "极速跑鞋"
		},
		{
			"attr.name" : "透气网跑"
		}
	],
	"count" : NumberLong(5),
	"keys" : NumberLong(4),
	"ok" : 1
}

上方示例指定的 keyattr.name。由于参与分组的 5 个文档中只有 2 个文档的 attr.name 是相同的,所以分组结果中的 keys4,这代表集合 sales 中的文档被分成了 4 组。

attr.name换成 shipDate,看看结果会是什么。对应示例如下:

复制代码> db.runCommand(
{
    group:{
        ns: 'sales',
        key: {shipDate: 1},
        cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}},
        $reduce: function(curr, result){},
        initial: {}
        }
	}
)

命令执行后,返回如下结果:

复制代码{
	"retval" : [
		{
			"shipDate" : ISODate("2012-07-04T09:00:00Z")
		},
		{
			"shipDate" : ISODate("2012-07-06T09:00:00Z")
		}
	],
	"count" : NumberLong(5),
	"keys" : NumberLong(2),
	"ok" : 1
}

由于参与分组的 5 个文档中有几个文档的 shipDate 是重复的,所以分组结果中的 keys2,这代表集合 sales 中的文档被分成了 2 组。

上面的示例并没有用到 reduceinitialfinallize ,接下来我们将演示它们的用法和作用。假设要统计同组的销售总额,那么可以在 reduce 中执行具体的计算逻辑。对应示例如下:

复制代码> db.runCommand(
{
    group:{
        ns: 'sales',
        key: {shipDate: 1},
        cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}},
        $reduce: function(curr, result){
        	result.total += curr.attr.price;
        	},
        initial: {total: 0}
        }
	}
)

命令执行后,返回结果如下:

复制代码{
	"retval" : [
		{
			"shipDate" : ISODate("2012-07-04T09:00:00Z"),
			"total" : 4998
		},
		{
			"shipDate" : ISODate("2012-07-06T09:00:00Z"),
			"total" : 3898
		}
	],
	"count" : NumberLong(5),
	"keys" : NumberLong(2),
	"ok" : 1
}

人工验证一下,发货日期 shipDate 大于 2012-07-04T09:00:00Z 的文档为:

复制代码{ "_id" : 2, "orderDate" : ISODate("2012-07-03T05:20:00Z"), "shipDate" : ISODate("2012-07-04T09:00:00Z"), "attr" : { "name" : "高邦篮球鞋", "price" : 1999, "size" : 43, "color" : "狮王棕" } }
{ "_id" : 3, "orderDate" : ISODate("2012-07-03T05:20:10Z"), "shipDate" : ISODate("2012-07-04T09:00:00Z"), "attr" : { "name" : "新款椰子鞋", "price" : 2999, "size" : 42, "color" : "香槟金" } }

销售总额为 1999 + 2999 = 4998,与返回结果相同。发货日期 shipDate 大于 2012-07-06T09:00:00Z 的文档为:

复制代码{ "_id" : 4, "orderDate" : ISODate("2012-07-05T15:11:33Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "极速跑鞋", "price" : 500, "size" : 43, "color" : "西湖蓝" } }
{ "_id" : 5, "orderDate" : ISODate("2012-07-05T20:22:09Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "新款椰子鞋", "price" : 2999, "size" : 42, "color" : "香槟金" } }
{ "_id" : 6, "orderDate" : ISODate("2012-07-05T22:35:20Z"), "shipDate" : ISODate("2012-07-06T09:00:00Z"), "attr" : { "name" : "透气网跑", "price" : 399, "size" : 38, "color" : "玫瑰红" } }

销售总额为 500 + 2999 + 399 = 3898,与返回结果相同。

有时候可能需要统计每个组的文档数量以及计算平均销售额,对应示例如下:

复制代码> db.runCommand(
{
    group:{
        ns: 'sales',
        key: {shipDate: 1},
        cond: {shipDate: {$gt: ISODate('2012-07-04T00:00:00Z')}},
        $reduce: function(curr, result){
        	result.total += curr.attr.price;
        	result.count ++;
        	},
        initial: {total: 0, count: 0},
        finalize: function(result){
        	result.avg = Math.round(result.total / result.count);
        	}
        }
	}
)

上面的示例中改动了 $reduce 函数,目的是为了统计 count。然后新增了 finalize,目的是计算分组中的平均销售额。命令执行后,返回以下文档:

复制代码{
	"retval" : [
		{
			"shipDate" : ISODate("2012-07-04T09:00:00Z"),
			"total" : 4998,
			"count" : 2,
			"avg" : 2499
		},
		{
			"shipDate" : ISODate("2012-07-06T09:00:00Z"),
			"total" : 3898,
			"count" : 3,
			"avg" : 1299
		}
	],
	"count" : NumberLong(5),
	"keys" : NumberLong(2),
	"ok" : 1
}

以上就是 group 的基本用法和作用介绍,更多与 group 相关的知识可查阅官方文档 group。

distinct

distinct 的作用是查找单个集合中指定字段的不同值,其语法格式如下:

复制代码{
  distinct: "<collection>",
  key: "<field>",
  query: <query>,
  readConcern: <read concern document>,
  collation: <collation document>
}

distinct 支持的指令及对应描述如下:

指令类型描述
distinctstring集合名称, 必填。
keystring指定的字段, 必填。
querydocument查询条件语句。
readConcerndocument
collationdocument

准备以下数据:

复制代码> db.dress.insertMany([
... {_id: 1, "dept": "A", attr: {"款式": "立领", color: "red" }, sizes: ["S", "M" ]},
... {_id: 2, "dept": "A", attr: {"款式": "圆领", color: "blue" }, sizes: ["M", "L" ]},
... {_id: 3, "dept": "B", attr: {"款式": "圆领", color: "blue" }, sizes: "S" },
... {_id: 4, "dept": "A", attr: {"款式": "V领", color: "black" }, sizes: ["S" ] }
])

假设要统计集合 dress 中所有文档的 dept 字段的不同值,对应示例如下:

复制代码> db.runCommand ( { distinct: "dress", key: "dept" } )
{ "values" : [ "A", "B" ], "ok" : 1 }

或者看看有那些款式,对应示例如下

复制代码> db.runCommand ( { distinct: "dress", key: "attr.款式" } )
{ "values" : [ "立领", "圆领", "V领" ], "ok" : 1 }

就算值是数组, distinct 也能作出正确处理,对应示例如下:

复制代码> db.runCommand ( { distinct: "dress", key: "sizes" } )
{ "values" : [ "M", "S", "L" ], "ok" : 1 }

流式聚合操作小结

以上就是本篇对 MongoDB 中流式聚合操作的介绍。聚合与管道的概念并不常见,但是理解起来也不难。只要跟着示例思考,并动手实践,相信你很快就能够熟练掌握聚合操作。

看不够?

除了帮助你掌握流式聚合操作之外,我还写了一整套 MongoDB 快速入门教程,看完教程你将收获:

  • 文档的 CRUD 操作和 Cursor 对象
  • 掌握流式聚合操作,轻松面对任何数据处理需求
  • 了解 MongoDB 的查询效率和优化
  • 学会提高 MongoDB 的可用性
  • 学会应对数据服务故障
  • 理解 MongoDB 的访问控制
  • 学会用数据模型降低数据冗余,提高效率
  • 掌握 mongodump 数据备份与还原方法

MongoDB 系列教程适合人群

  • 对 MongoDB 感兴趣的 0 基础开发者
  • 有一定基础,想要全面了解 MongoDB 的开发者

为什么选择这套 MongoDB 教程?

  • 内容类似的 MongoDB 教程动辄几百元
  • MongoDB 官方文档晦涩难懂
  • 网上其他文章不够全面,难以形成体系化知识
  • Chat 中的示例代码可以直接复制使用,方便练习
  • MongoDB 内容繁多,自学者不确定需要学习哪些知识

这是写给 0 基础同学的 MongoDB 快速入门文章。内容从文档 CRUD 到流式聚合操作;从执行计划、索引、数据模型到复制集;从分片、访问控制到数据备份与还原。全篇近 5 万词的内容覆盖了 MongoDB 的大部分知识点,完全满足日常开发的要求。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ 程序文档生成器(doxygen)使用说明

程序文档&#xff0c;是每个程序员必看文档&#xff0c;在日常业务开发中&#xff0c;难免会封装一些组件。没有很好的组件文档&#xff0c;再好的组件都是废物&#xff0c;。因此大型业务中&#xff0c;文档和思维导图&#xff0c;两个都是必备&#xff01; 一、注释风格 …

【面试合集】说说微信小程序的支付流程?

面试官&#xff1a;说说微信小程序的支付流程&#xff1f; 一、前言 微信小程序为电商类小程序&#xff0c;提供了非常完善、优秀、安全的支付功能 在小程序内可调用微信的API完成支付功能&#xff0c;方便、快捷 场景如下图所示&#xff1a; 用户通过分享或扫描二维码进入商…

[Python进阶] 正则表达式的验证

8.2 正则表达式的验证 正则表达式的语法很令人头疼&#xff0c;即使对经常使用它的人来说也是如此。由于难于读写&#xff0c;容易出错&#xff0c;所以找一种工具对正则表达式进行测试是很有必要的。 8.2.1 本地验证 通过Regex Tester这款软件可以在本地对正则表达式进行验…

机器学习算法实战案例:Informer实现多变量负荷预测

文章目录 机器学习算法实战案例系列答疑&技术交流1 实验数据集2 如何运行自己的数据集3 报错分析 机器学习算法实战案例系列 机器学习算法实战案例&#xff1a;确实可以封神了&#xff0c;时间序列预测算法最全总结&#xff01; 机器学习算法实战案例&#xff1a;时间序列…

Linux shell编程学习笔记39:df命令

0 前言1 df命令的功能、格式和选项说明 1.1 df命令的功能1.2 df命令的格式1.3 df命令选项说明 2 df命令使用实例 2.1 df&#xff1a;显示主要文件系统信息2.2 df -a&#xff1a;显示所有文件系统信息2.3 df -t[]TYPE或--type[]TYPE&#xff1a;显示TYPE指定类型的文件系统信…

AIGC实战——像素卷积神经网络(PixelCNN)

AIGC实战——像素卷积神经网络 0. 前言1. PixelCNN 工作原理1.1 掩码卷积层 1.2 残差块2. 训练 PixelCNN3. PixelCNN 分析4. 使用混合分布改进 PixelCNN小结系列链接 0. 前言 像素卷积神经网络 (Pixel Convolutional Neural Network, PixelCNN) 是于 2016 年提出的一种图像生成…

LINUX基础培训九之网络管理

前言、本章学习目标 了解LINUX网络接口和主机名配置的方法熟悉网络相关的几个配置文件了解网关和路由熟悉网络相关的命令使用 一、网络IP地址配置 在Linux中配置IP地址的方法有以下这么几种&#xff1a; 1、图形界面配置IP地址&#xff08;操作方式如Windows系统配置IP&…

机器学习:线性回归模型的原理、应用及优缺点

一、原理 线性回归是一种统计学和机器学习中常用的方法&#xff0c;用于建立变量之间线性关系的模型。其原理基于假设因变量&#xff08;或响应变量&#xff09;与自变量之间存在线性关系。 下面是线性回归模型的基本原理&#xff1a; 模型拟合&#xff1a; 通过最小二乘法&…

1、机器学习模型的工作方式

第一步,如果你是机器学习新手。 本课程所需数据集夸克网盘下载链接:https://pan.quark.cn/s/9b4e9a1246b2 提取码:uDzP 文章目录 1、简介2、决策树优化3、继续1、简介 我们将从机器学习模型如何工作以及如何使用它们的概述开始。如果你以前做过统计建模或机器学习,这可能感…

【Web】CTFSHOW 文件上传刷题记录(全)

期末考完终于可以好好学ctf了&#xff0c;先把这些该回顾的回顾完&#xff0c;直接rushjava&#xff01; 目录 web151 web152 web153 web154-155 web156-159 web160 web161 web162-163 web164 web165 web166 web167 web168 web169-170 web151 如果直接上传php文…

生物制药厂污水处理需要哪些工艺设备

生物制药厂是一种特殊的工业场所&#xff0c;由于其生产过程中涉及的有机物较多&#xff0c;导致废水中含有高浓度的有机物和微生物等污染物&#xff0c;因此需要采用一些特殊的工艺设备来进行污水处理。本文将介绍生物制药厂污水处理中常用的工艺设备。 首先&#xff0c;对于生…

Java NIO (二)NIO Buffer类的重要方法(备份)

1 allocate()方法 在使用Buffer实例前&#xff0c;我们需要先获取Buffer子类的实例对象&#xff0c;并且分配内存空间。需要获取一个Buffer实例对象时&#xff0c;并不是使用子类的构造器来创建&#xff0c;而是调用子类的allocate()方法。 public class AllocateTest {static…

【FastAPI】路径参数(二)

预设值 如果你有一个接收路径参数的路径操作&#xff0c;但你希望预先设定可能的有效参数值&#xff0c;则可以使用标准的 Python Enum 类型。 导入 Enum 并创建一个继承自 str 和 Enum 的子类。通过从 str 继承&#xff0c;API 文档将能够知道这些值必须为 string 类型并且能…

PromptCast-时间序列预测的好文推荐

前言 这是关于大语言模型和时间序列预测结合的好文推荐&#xff0c;发现这篇文章&#xff0c;不仅idea不错和代码开源维护的不错&#xff0c;论文也比较详细&#xff08;可能是顶刊而不是顶会&#xff0c;篇幅大&#xff0c;容易写清楚&#xff09;&#xff0c;并且关于它的Br…

STM32+HAL库驱动ADXL345传感器(SPI协议)

STM32HAL库驱动ADXL345传感器&#xff08;SPI协议&#xff09; ADXL345传感器简介实物STM32CubeMX配置SPI配置片选引脚配置串口配置 特别注意&#xff08;重点部分&#xff09;核心代码效果展示 ADXL345传感器简介 ADXL345 是 ADI 公司推出的基于 iMEMS 技术的 3 轴、数字输出加…

Spring Security- 基于角色的访问控制

基于角色 或权限 进行访问控制 hasAuthority方法 如果当前的主体具有指定的权限,则返回true,否则返回false 修改配置类 //当前登录用户 只有具备admins权限才可以访问这个路径.antMatchers("/test/index").hasAuthority("admins") 代码如下: package c…

达芬奇调色软件DaVinci Resolve Studio 18 中文激活版

DaVinci Resolve Studio 18是一款功能强大的视频编辑软件&#xff0c;它可以帮助用户轻松完成视频剪辑、调色、音频处理和特效合成等任务。 软件下载&#xff1a;DaVinci Resolve Studio 18 中文激活版下载 这款软件具有友好的用户界面和易于使用的功能&#xff0c;使得用户能够…

云服务器CVM_云主机_云计算服务器_弹性云服务器

腾讯云服务器CVM提供安全可靠的弹性计算服务&#xff0c;腾讯云明星级云服务器&#xff0c;弹性计算实时扩展或缩减计算资源&#xff0c;支持包年包月、按量计费和竞价实例计费模式&#xff0c;CVM提供多种CPU、内存、硬盘和带宽可以灵活调整的实例规格&#xff0c;提供9个9的数…

如何安装“MySQL在虚拟机ubuntu”win10系统?

1、 更新列表 sudo apt-get update 2、 安装MySQL服务器 sudo apt-get install mysql-server 3、 安装MySQL客户端 sudo apt-get install mysql-client 4、 配置MySQL sudo mysql_secure_installation 5、 测试MySQL systemctl status mysql.service MySQL数据库基本…

transbigdata笔记:轨迹停止点和行程提取

1 traj_stay_move——标识停靠点和行程 1.1 方法介绍 如果两个连续轨迹数据点&#xff08;栅格化处理之后&#xff09;之间的持续时间超过设定的阈值&#xff0c;将其视为停靠点。两个停靠点之间的时间段被视为一个行程 1.2 使用方法 transbigdata.traj_stay_move(data, pa…