diff --git a/docs/uniCloud/cf-database.md b/docs/uniCloud/cf-database.md index b7ae4556a35c477d695897fd470e21659895884a..20ed1645403792a1f7ee5a9d3bb6dfc9d187209b 100644 --- a/docs/uniCloud/cf-database.md +++ b/docs/uniCloud/cf-database.md @@ -1169,4 +1169,2470 @@ db.collection('user').where({ |:-: |:-: | |add |使用阿里云时在集合不存在的时候调用会自动创建集合 | |数据库实时推送 |阿里云暂不支持 | -|GEO地理位置 |阿里云暂不支持 | --> \ No newline at end of file +|GEO地理位置 |阿里云暂不支持 | --> + +## 聚合操作 + +获取数据库集合的聚合操作实例 +``` +db.collection('scores').aggregate() +``` + +### addFields + +聚合阶段。添加新字段到输出的记录。经过 `addFields` 聚合阶段,输出的所有记录中除了输入时带有的字段外,还将带有 `addFields` 指定的字段。 + + +**API 说明** + +`addFields` 等同于同时指定了所有已有字段和新增字段的 `project` 阶段。 + +**`addFields` 的形式如下:** +``` +addFields({ + <新字段>: <表达式> +}) +``` +`addFields` 可指定多个新字段,每个新字段的值由使用的表达式决定。 + +如果指定的新字段与原有字段重名,则新字段的值会覆盖原有字段的值。注意 `addFields` 不能用来给数组字段添加元素。 + +**示例 1:连续两次 addFields** + +假设集合 scores 有如下记录: +``` +{ + _id: 1, + student: "Maya", + homework: [ 10, 5, 10 ], + quiz: [ 10, 8 ], + extraCredit: 0 +} +{ + _id: 2, + student: "Ryan", + homework: [ 5, 6, 5 ], + quiz: [ 8, 8 ], + extraCredit: 8 +} +``` +应用两次 `addFields`,第一次增加两个字段分别为 `homework` 和 `quiz` 的和值,第二次增加一个字段再基于上两个和值求一次和值。 +``` +const $ = db.command.aggregate +db.collection('scores').aggregate() + .addFields({ + totalHomework: $.sum('$homework'), + totalQuiz: $.sum('$quiz') + }) + .addFields({ + totalScore: $.add(['$totalHomework', '$totalQuiz', '$extraCredit']) + }) + .end() +``` + +返回结果如下: +``` +{ + "_id" : 1, + "student" : "Maya", + "homework" : [ 10, 5, 10 ], + "quiz" : [ 10, 8 ], + "extraCredit" : 0, + "totalHomework" : 25, + "totalQuiz" : 18, + "totalScore" : 43 +} +{ + "_id" : 2, + "student" : "Ryan", + "homework" : [ 5, 6, 5 ], + "quiz" : [ 8, 8 ], + "extraCredit" : 8, + "totalHomework" : 16, + "totalQuiz" : 16, + "totalScore" : 40 +} +``` + +**示例 2:在嵌套记录里增加字段** + +可以用点表示法在嵌套记录里增加字段。假设 vehicles 集合含有如下记录: +``` +{ _id: 1, type: "car", specs: { doors: 4, wheels: 4 } } +{ _id: 2, type: "motorcycle", specs: { doors: 0, wheels: 2 } } +{ _id: 3, type: "jet ski" } +``` +可以用如下操作在 `specs` 字段下增加一个新的字段 `fuel_type`,值都设为固定字符串 `unleaded`: +``` +db.collection('vehicles').aggregate() + .addFields({ + 'spec.fuel_type': 'unleaded' + }) + .end() +``` + +返回结果如下: +``` +{ _id: 1, type: "car", + specs: { doors: 4, wheels: 4, fuel_type: "unleaded" } } +{ _id: 2, type: "motorcycle", + specs: { doors: 0, wheels: 2, fuel_type: "unleaded" } } +{ _id: 3, type: "jet ski", + specs: { fuel_type: "unleaded" } } +``` + +**示例 3:设置字段值为另一个字段** + +可以通过 `$` 加字段名组成的字符串作为值的表达式来设置字段的值为另一个字段的值。 + +同样用上一个集合示例,可以用如下操作添加一个字段 `vehicle_type`,将其值设置为 `type` 字段的值: +``` +db.collection('vehicles').aggregate() + .addFields({ + vehicle_type: '$type' + }) + .end() +``` +返回结果如下: +``` +{ _id: 1, type: "car", vehicle_type: "car", + specs: { doors: 4, wheels: 4, fuel_type: "unleaded" } } +{ _id: 2, type: "motorcycle", vehicle_type: "motorcycle", + specs: { doors: 0, wheels: 2, fuel_type: "unleaded" } } +{ _id: 3, type: "jet ski", vehicle_type: "jet ski", + specs: { fuel_type: "unleaded" } } +``` + +### bucket + +聚合阶段。将输入记录根据给定的条件和边界划分成不同的组,每组即一个 `bucket`。 + +**API 说明** + +每组分别作为一个记录输出,包含一个以下界为值的 `_id` 字段和一个以组中记录数为值的 `count` 字段。`count` 在没有指定 `output` 的时候是默认输出的。 + +`bucket` 只会在组内有至少一个记录的时候输出。 + +**bucket 的形式如下:** +``` +bucket({ + groupBy: , + boundaries: [, , ...], + default: , + output: { + : , + ... + : + } +}) +``` +`groupBy` 是一个用以决定分组的表达式,会应用在各个输入记录上。可以用 `$` 前缀加上要用以分组的字段路径来作为表达式。除非用 `default` 指定了默认值,否则每个记录都需要包含指定的字段,且字段值必须在 `boundaries` 指定的范围之内。 + +`boundaries` 是一个数组,每个元素分别是每组的下界。必须至少指定两个边界值。数组值必须是同类型递增的值。 + +`default` 可选,指定之后,没有进入任何分组的记录将都进入一个默认分组,这个分组记录的 `_id` 即由 `default` 决定。`default` 的值必须小于 `boundaries` 中的最小值或大于等于其中的最大值。`default` 的值可以与 `boundaries` 元素值类型不同。 + +`output` 可选,用以决定输出记录除了 `_id` 外还要包含哪些字段,各个字段的值必须用累加器表达式指定。当 `output` 指定时,默认的 `count` 是不会被默认输出的,必须手动指定: +``` +output: { + count: $.sum(1), + ... + : +} +``` +使用 bucket 需要满足以下至少一个条件,否则会抛出错误: + +每一个输入记录应用 groupBy 表达式获取的值都必须是一个在 boundaries 内的值 + +指定一个 default 值,该值在 boundaries 以外,或与 boundaries 元素的值不同的类型。 + +**示例** + +假设集合 items 有如下记录: +``` +{ + _id: "1", + price: 10 +} +{ + _id: "2", + price: 50 +} +{ + _id: "3", + price: 20 +} +{ + _id: "4", + price: 80 +} +{ + _id: "5", + price: 200 +} +``` + +对上述记录进行分组,将 [0, 50) 分为一组,[50, 100) 分为一组,其他分为一组: + +``` +const $ = db.command.aggregate +db.collection('items').aggregate() + .bucket({ + groupBy: '$price', + boundaries: [0, 50, 100], + default: 'other', + output: { + count: $.sum(), + ids: $.push('$_id') + } + }) + .end() +``` + +返回结果如下: + +``` +[ + { + "_id": 0, + "count": 2, + "ids": [ + "1", + "3" + ] + }, + { + "_id": 50, + "count": 2, + "ids": [ + "2", + "4" + ] + }, + { + "_id": "other", + "count": 22, + "ids": [ + "5" + ] + } +] +``` + +### bucketAuto + +聚合阶段。将输入记录根据给定的条件划分成不同的组,每组即一个 `bucket`。与 `bucket` 的其中一个不同之处在于无需指定 `boundaries`,`bucketAuto` 会自动尝试将记录尽可能平均的分散到每组中。 + +**API 说明** +每组分别作为一个记录输出,包含一个以包含组中最大值和最小值两个字段的对象为值的 _id 字段和一个以组中记录数为值的 count 字段。count 在没有指定 output 的时候是默认输出的。 + +**bucketAuto 的形式如下:** +``` +bucketAuto({ + groupBy: , + buckets: , + granularity: , + output: { + : , + ... + : + } +}) +``` +`groupBy` 是一个用以决定分组的表达式,会应用在各个输入记录上。可以用 $ 前缀加上要用以分组的字段路径来作为表达式。除非用 `default` 指定了默认值,否则每个记录都需要包含指定的字段,且字段值必须在 `boundaries` 指定的范围之内。 + +`buckets` 是一个用于指定划分组数的正整数。 + +`granularity` 是可选枚举值字符串,用于保证自动计算出的边界符合给定的规则。这个字段仅可在所有 `groupBy` 值都是数字并且没有 `NaN` 的情况下使用。枚举值包括:`R5、R10、R20、R40、R80、1-2-5、E6、E12、E24、E48、E96、E192、POWERSOF2`。 + +`output` 可选,用以决定输出记录除了 `_id` 外还要包含哪些字段,各个字段的值必须用累加器表达式指定。当 `output` 指定时,默认的 `count` 是不会被默认输出的,必须手动指定: +``` +output: { + count: $.sum(1), + ... + : +} +``` +在以下情况中,输出的分组可能会小于给定的组数: + +输入记录数少于分组数 +- `groupBy` 计算得到的唯一值少于分组数 +- `granularity` 的间距少于分组数 +- `granularity` 不够精细以至于不能平均分配到各组 + +**granularity 详细说明** + +`granularity` 用于保证边界值属于一个给定的数字序列。 + +**Renard 序列** + +Renard 序列是以 10 的 5 / 10 / 20 / 40 / 80 次方根来推导的、在 1.0 到 10.0 (如果是 R80 则是 10.3) 之间的数字序列。 + +设置 granularity 为 R5 / R10 / R20 / R40 / R80 就把边界值限定在序列内。如果 groupBy 的值不在 1.0 到 10.0 (如果是 R80 则是 10.3) 内,则序列数字会自动乘以 10。 + +**E 序列** + +E 序列是以 10 的 6 / 12 / 24 / 48 / 96 / 192 次方跟来推导的、带有一个特定误差的、在 1.0 到 10.0 之间的数字序列。 + +**1-2-5 序列** + +1-2-5 序列 表现与三值 Renard 序列一样。 + +**2的次方序列** + +由 2 的各次方组成的序列数字。 + +**示例** + +假设集合 items 有如下记录: +``` +{ + _id: "1", + price: 10.5 +} +{ + _id: "2", + price: 50.3 +} +{ + _id: "3", + price: 20.8 +} +{ + _id: "4", + price: 80.2 +} +{ + _id: "5", + price: 200.3 +} +``` +对上述记录进行自动分组,分成三组: +``` +const $ = db.command.aggregate +db.collection('items').aggregate() + .bucket({ + groupBy: '$price', + buckets: 3, + }) + .end() +``` +返回结果如下: +``` +{ + "_id": { + "min": 10.5, + "max": 50.3 + }, + "count": 2 +} +{ + "_id": { + "min": 50.3, + "max": 200.3 + }, + "count": 2 +} +{ + "_id": { + "min": 200.3, + "max": 200.3 + }, + "count": 1 +} +``` + +### count + +聚合阶段。计算上一聚合阶段输入到本阶段的记录数,输出一个记录,其中指定字段的值为记录数。 + +**API 说明** + +**count 的形式如下:** +``` +count() +``` + 是输出记录数的字段的名字,不能是空字符串,不能以 $ 开头,不能包含 . 字符。 + +count 阶段等同于 group + project 的操作: +``` +const $ = db.command.aggregate +db.collection('items').aggregate() + .group({ + _id: null, + count: $.sum(1), + }) + .project({ + _id: 0, + }) + .end() +``` +上述操作会输出一个包含 count 字段的记录。 + +**示例** + +假设集合 items 有如下记录: +``` +{ + _id: "1", + price: 10.5 +} +{ + _id: "2", + price: 50.3 +} +{ + _id: "3", + price: 20.8 +} +{ + _id: "4", + price: 80.2 +} +{ + _id: "5", + price: 200.3 +} +``` +找出价格大于 50 的记录数: +``` +const $ = db.command.aggregate +db.collection('items').aggregate() + .match({ + price: $.gt(50) + }) + .count('expensiveCount') + .end() +``` +返回结果如下: +``` +{ + "expensiveCount": 3 +} +``` + + + +### group + +聚合阶段。将输入记录按给定表达式分组,输出时每个记录代表一个分组,每个记录的 _id 是区分不同组的 key。输出记录中也可以包括累计值,将输出字段设为累计值即会从该分组中计算累计值。 + + +**API 说明** + +**group 的形式如下:** +``` +group({ + _id: , + : , + ... + : +}) +``` + +`_id` 参数是必填的,如果填常量则只有一组。其他字段是可选的,都是累计值,用 `$.sum` 等累计器(`const $ = db.command.aggregate`),但也可以使用其他表达式。 + +累计器必须是以下操作符之一: + +详细使用方法见[累计器操作符](#累计器操作符) + +|操作符 |说明 | +|---- |---- | +|addToSet |向数组中添加值,如果数组中已存在该值,不执行任何操作 | +|avg |返回一组集合中,指定字段对应数据的平均值 | +|sum |计算并且返回一组字段所有数值的总和 | +|first |返回指定字段在一组集合的第一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 | +|last |返回指定字段在一组集合的最后一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 | +|max |返回一组数值的最大值 | +|min |返回一组数值的最小值 | +|push |在 group 阶段,返回一组中表达式指定列与对应的值,一起组成的数组 | +|stdDevPop |返回一组字段对应值的标准差 | +|stdDevSamp |计算输入值的样本标准偏差。如果输入值代表数据总体,或者不概括更多的数据,请改用 db.command.aggregate.stdDevPop| +|mergeObjects |将多个文档合并为单个文档 | + +**内存限制** + +该阶段有 100M 内存使用限制。 + +**示例 1:按字段值分组** + +假设集合 avatar 有如下记录: +``` +{ + _id: "1", + alias: "john", + region: "asia", + scores: [40, 20, 80], + coins: 100 +} +{ + _id: "2", + alias: "arthur", + region: "europe", + scores: [60, 90], + coins: 20 +} +{ + _id: "3", + alias: "george", + region: "europe", + scores: [50, 70, 90], + coins: 50 +} +{ + _id: "4", + alias: "john", + region: "asia", + scores: [30, 60, 100, 90], + coins: 40 +} +{ + _id: "5", + alias: "george", + region: "europe", + scores: [20], + coins: 60 +} +{ + _id: "6", + alias: "john", + region: "asia", + scores: [40, 80, 70], + coins: 120 +} +``` +``` +const $ = db.command.aggregate +db.collection('avatar').aggregate() + .group({ + _id: '$alias', + num: $.sum(1) + }) + .end() +``` +返回结果如下: +``` +{ + "_id": "john", + "num": 3 +} +{ + "_id": "authur", + "num": 1 +} +{ + "_id": "george", + "num": 2 +} +``` + +**示例 2:按多个值分组** + +可以给 _id 传入记录的方式按多个值分组。还是沿用上面的示例数据,按各个区域(region)获得相同最高分(score)的来分组,并求出各组虚拟币(coins)的总量: +``` +const $ = db.command.aggregate +db.collection('avatar').aggregate() + .group({ + _id: { + region: '$region', + maxScore: $.max('$scores') + }, + totalCoins: $.sum('$coins') + }) + .end() +``` +返回结果如下: +``` +{ + "_id": { + "region": "asia", + "maxScore": 80 + }, + "totalCoins": 220 +} +{ + "_id": { + "region": "asia", + "maxScore": 100 + }, + "totalCoins": 100 +} +{ + "_id": { + "region": "europe", + "maxScore": 90 + }, + "totalCoins": 70 +} +{ + "_id": { + "region": "europe", + "maxScore": 20 + }, + "totalCoins": 60 +} +``` + +### limit + +聚合阶段。限制输出到下一阶段的记录数。 + +**示例** + +假设集合 items 有如下记录: +``` +{ + _id: "1", + price: 10 +} +{ + _id: "2", + price: 50 +} +{ + _id: "3", + price: 20 +} +{ + _id: "4", + price: 80 +} +{ + _id: "5", + price: 200 +} +``` +返回价格大于 20 的记录的最小的两个记录: +``` +const $ = db.command.aggregate +db.collection('items').aggregate() + .match({ + price: $.gt(20) + }) + .sort({ + price: 1, + }) + .limit(2) + .end() +``` +返回结果如下: +``` +{ + "_id": "3", + "price": 20 +} +{ + "_id": "4", + "price": 80 +} +``` + +### lookup + +聚合阶段。聚合阶段。联表查询。与同个数据库下的一个指定的集合做 left outer join(左外连接)。对该阶段的每一个输入记录,lookup 会在该记录中增加一个数组字段,该数组是被联表中满足匹配条件的记录列表。lookup 会将连接后的结果输出给下个阶段。 + +**API 说明** + +`lookup` 有两种使用方式 + +1. 相等匹配 +将输入记录的一个字段和被连接集合的一个字段进行相等匹配时,采用以下定义: +``` +lookup({ + from: <要连接的集合名>, + localField: <输入记录的要进行相等匹配的字段>, + foreignField: <被连接集合的要进行相等匹配的字段>, + as: <输出的数组字段名> +}) +``` + +**参数详细说明** + +|参数字段 |说明 | +|---- |---- | +|from |要进行连接的另外一个集合的名字 | +|localField |当前流水线的输入记录的字段名,该字段将被用于与 from 指定的集合的 foreignField 进行相等匹配。如果输入记录中没有该字段,则该字段的值在匹配时会被视作 null| +|foreignField |被连接集合的字段名,该字段会被用于与 localField 进行相等匹配。如果被连接集合的记录中没有该字段,该字段的值将在匹配时被视作 null | +|as |指定连接匹配出的记录列表要存放的字段名,这个数组包含的是匹配出的来自 from 集合的记录。如果输入记录中本来就已有该字段,则该字段会被覆写 | + +这个操作等价于以下伪 SQL 操作: + +``` +SELECT *, +FROM collection +WHERE IN (SELECT * + FROM + WHERE = ); +``` + +**例子:** + +- 指定一个相等匹配条件 +- 对数组字段应用相等匹配 +- 组合 mergeObjects 应用相等匹配 + +2. 自定义连接条件、拼接子查询 + +如果需要指定除相等匹配之外的连接条件,或指定多个相等匹配条件,或需要拼接被连接集合的子查询结果,那可以使用如下定义: +``` +lookup({ + from: <要连接的集合名>, + let: { <变量1>: <表达式1>, ..., <变量n>: <表达式n> }, + pipeline: [ <在要连接的集合上进行的流水线操作> ], + as: <输出的数组字段名> +}) +``` + +**参数详细说明** + +参数字段 说明 +from 要进行连接的另外一个集合的名字 +let 可选。指定在 pipeline 中可以使用的变量,变量的值可以引用输入记录的字段,比如 let: { userName: '$name' } 就代表将输入记录的 name 字段作为变量 userName 的值。在 pipeline 中无法直接访问输入记录的字段,必须通过 let 定义之后才能访问,访问的方式是在 expr 操作符中用 $$变量名 的方式访问,比如 $$userName。 +pipeline 指定要在被连接集合中运行的聚合操作。如果要返回整个集合,则该字段取值空数组 []。在 pipeline 中无法直接访问输入记录的字段,必须通过 let 定义之后才能访问,访问的方式是在 expr 操作符中用 $$变量名 的方式访问,比如 $$userName。 +as 指定连接匹配出的记录列表要存放的字段名,这个数组包含的是匹配出的来自 from 集合的记录。如果输入记录中本来就已有该字段,则该字段会被覆写 + +该操作等价于以下伪 SQL 语句: +``` +SELECT *, +FROM collection +WHERE IN (SELECT + FROM + WHERE ); +``` + +**例子** + +- 指定多个连接条件 +- 拼接被连接集合的子查询 + +**示例** + +**指定一个相等匹配条件** + +假设 orders 集合有以下记录: +``` +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1}, + {"_id":6} +] +``` +books 集合有以下记录: +``` +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"}, + {"_id":"book2","author":"author 2","category":"novel","stock":20,"title":"novel 2"}, + {"_id":"book5","author":"author 4","category":"science","stock":50,"title":null}, + {"_id":"book6","author":"author 5","category":"novel","stock":"60"} +] +``` +以下聚合操作可以通过一个相等匹配条件连接 `orders` 和 `books` 集合,匹配的字段是 `orders` 集合的 `book` 字段和 `books` 集合的 title 字段: +``` +const db = cloud.database() +db.collection('orders').aggregate() + .lookup({ + from: 'books', + localField: 'book', + foreignField: 'title', + as: 'bookList', + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` +结果: +``` +[ + { + "_id": 4, + "book": "novel 1", + "price": 30, + "quantity": 2, + "bookList": [ + { + "_id": "book1", + "title": "novel 1", + "author": "author 1", + "category": "novel", + "stock": 10 + } + ] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "author": "author 3", + "stock": 30 + } + ] + }, + { + "_id": 6, + "bookList": [ + { + "_id": "book5", + "category": "science", + "author": "author 4", + "stock": 50, + "title": null + }, + { + "_id": "book6", + "author": "author 5", + "stock": "60", + "category": "novel" + } + ] + } +] +``` +对数组字段应用相等匹配 +假设 authors 集合有以下记录: +``` +[ + {"_id": 1, "name": "author 1", "intro": "Two-time best-selling sci-fiction novelist"}, + {"_id": 3, "name": "author 3", "intro": "UCB assistant professor"}, + {"_id": 4, "name": "author 4", "intro": "major in CS"} +] +``` +books 集合有以下记录: +``` +[ + {"_id":"book1","authors":["author 1"],"category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","authors":["author 3", "author 4"],"category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","authors":["author 3"],"category":"science","stock":40,"title":"science 2"} +] +``` +以下操作获取作者信息及他们分别发表的书籍,使用了 lookup 操作匹配 authors 集合的 name 字段和 books 集合的 authors 数组字段: +``` +const db = cloud.database() +db.collection('authors').aggregate() + .lookup({ + from: 'books', + localField: 'name', + foreignField: 'authors', + as: 'publishedBooks', + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` +结果 +``` +[ + { + "_id": 1, + "intro": "Two-time best-selling sci-fiction novelist", + "name": "author 1", + "publishedBooks": [ + { + "_id": "book1", + "title": "novel 1", + "category": "novel", + "stock": 10, + "authors": [ + "author 1" + ] + } + ] + }, + { + "_id": 3, + "name": "author 3", + "intro": "UCB assistant professor", + "publishedBooks": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "stock": 30, + "authors": [ + "author 3", + "author 4" + ] + }, + { + "_id": "book4", + "title": "science 2", + "category": "science", + "stock": 40, + "authors": [ + "author 3" + ] + } + ] + }, + { + "_id": 4, + "intro": "major in CS", + "name": "author 4", + "publishedBooks": [ + { + "_id": "book3", + "category": "science", + "title": "science 1", + "stock": 30, + "authors": [ + "author 3", + "author 4" + ] + } + ] + } +] +``` + +**组合 mergeObjects 应用相等匹配** + +假设 `orders` 集合有以下记录: +``` +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1}, + {"_id":6} +] +``` +`books` 集合有以下记录: +``` +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"}, + {"_id":"book2","author":"author 2","category":"novel","stock":20,"title":"novel 2"}, + {"_id":"book5","author":"author 4","category":"science","stock":50,"title":null}, + {"_id":"book6","author":"author 5","category":"novel","stock":"60"} +] +``` +以下操作匹配 orders 的 book 字段和 books 的 title 字段,并将 books 匹配结果直接 merge 到 orders 记录中。 +``` +var db = cloud.database() +var $ = db.command.aggregate +db.collection('orders').aggregate() + .lookup({ + from: "books", + localField: "book", + foreignField: "title", + as: "bookList" + }) + .replaceRoot({ + newRoot: $.mergeObjects([ $.arrayElemAt(['$bookList', 0]), '$$ROOT' ]) + }) + .project({ + bookList: 0 + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` +结果 +``` +[ + { + "_id": 4, + "title": "novel 1", + "author": "author 1", + "category": "novel", + "stock": 10, + "book": "novel 1", + "price": 30, + "quantity": 2 + }, + { + "_id": 5, + "category": "science", + "title": "science 1", + "author": "author 3", + "stock": 30, + "book": "science 1", + "price": 20, + "quantity": 1 + }, + { + "_id": 6, + "category": "science", + "author": "author 4", + "stock": 50, + "title": null + } +] +``` + +**指定多个连接条件** + +假设 `orders` 集合有以下记录: +``` +[ + {"_id":4,"book":"novel 1","price":300,"quantity":20}, + {"_id":5,"book":"science 1","price":20,"quantity":1} +] +``` +`books` 集合有以下记录: +``` +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"} +] +``` +以下操作连接 `orders` 和 `books` 集合,要求两个条件: + +- orders 的 book 字段与 books 的 title 字段相等 +- orders 的 quantity 字段大于或等于 books 的 stock 字段 +``` +const db = cloud.database() +const $ = db.command.aggregate +db.collection('orders').aggregate() + .lookup({ + from: 'books', + let: { + order_book: '$book', + order_quantity: '$quantity' + }, + pipeline: $.pipeline() + .match(_.expr($.and([ + $.eq(['$title', '$$order_book']), + $.gte(['$stock', '$$order_quantity']) + ]))) + .project({ + _id: 0, + title: 1, + author: 1, + stock: 1 + }) + .done(), + as: 'bookList', + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` +结果: +``` +[ + { + "_id": 4, + "book": "novel 1", + "price": 300, + "quantity": 20, + "bookList": [] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + } + ] + } +] +``` + +**拼接被连接集合的子查询** + +假设 `orders` 集合有以下记录: +``` +[ + {"_id":4,"book":"novel 1","price":30,"quantity":2}, + {"_id":5,"book":"science 1","price":20,"quantity":1} +] +``` +`books` 集合有以下记录: +``` +[ + {"_id":"book1","author":"author 1","category":"novel","stock":10,"time":1564456048486,"title":"novel 1"}, + {"_id":"book3","author":"author 3","category":"science","stock":30,"title":"science 1"}, + {"_id":"book4","author":"author 3","category":"science","stock":40,"title":"science 2"} +] +``` +在每条输出记录上加上一个数组字段,该数组字段的值是对 books 集合的一个查询语句的结果: +``` +const db = cloud.database() +const $ = db.command.aggregate +db.collection('orders').aggregate() + .lookup({ + from: 'books', + let: { + order_book: '$book', + order_quantity: '$quantity' + }, + pipeline: $.pipeline() + .match({ + author: 'author 3' + }) + .project({ + _id: 0, + title: 1, + author: 1, + stock: 1 + }) + .done(), + as: 'bookList', + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` +结果 +``` +[ + { + "_id": 4, + "book": "novel 1", + "price": 30, + "quantity": 20, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + }, + { + "title": "science 2", + "author": "author 3", + "stock": 40 + } + ] + }, + { + "_id": 5, + "book": "science 1", + "price": 20, + "quantity": 1, + "bookList": [ + { + "title": "science 1", + "author": "author 3", + "stock": 30 + }, + { + "title": "science 2", + "author": "author 3", + "stock": 40 + } + ] + } +] +``` + +### match + +聚合阶段。根据条件过滤文档,并且把符合条件的文档传递给下一个流水线阶段。 + +**API 说明** + +**match 的形式如下:** +``` +match(<查询条件>) +``` + +查询条件与普通查询一致,可以用普通查询操作符,注意 match 阶段和其他聚合阶段不同,不可使用聚合操作符,只能使用查询操作符。 +``` +// 直接使用字符串 +match({ + name: 'Tony Stark' +}) +``` +``` +// 使用操作符 +const _ = db.command +match({ + age: _.gt(18) +}) +``` + +**示例** + +假设集合 articles 有如下记录: +``` +{ "_id" : "1", "author" : "stark", "score" : 80 } +{ "_id" : "2", "author" : "stark", "score" : 85 } +{ "_id" : "3", "author" : "bob", "score" : 60 } +{ "_id" : "4", "author" : "li", "score" : 55 } +{ "_id" : "5", "author" : "jimmy", "score" : 60 } +{ "_id" : "6", "author" : "li", "score" : 94 } +{ "_id" : "7", "author" : "justan", "score" : 95 } +``` + +**匹配** + +下面是一个直接匹配的例子: +``` +db.collection('articles') + .aggregate() + .match({ + author: 'stark' + }) + .end() +``` +这里的代码尝试找到所有 author 字段是 stark 的文章,那么匹配如下: +``` +{ "_id" : "1", "author" : "stark", "score" : 80 } +{ "_id" : "2", "author" : "stark", "score" : 85 } +``` + +**计数** + +match 过滤出文档后,还可以与其他流水线阶段配合使用。 + +比如下面这个例子,我们使用 group 进行搭配,计算 score 字段大于 80 的文档数量: +``` +const _ = db.command +const $ = _.aggregate +db.collection('articles') + .aggregate() + .match({ + score: _.gt(80) + }) + .group({ + _id: null, + count: $.sum(1) + }) + .end() +``` +返回值如下: +``` +{ "_id" : null, "count" : 3 } +``` + +### project + +聚合阶段。把指定的字段传递给下一个流水线,指定的字段可以是某个已经存在的字段,也可以是计算出来的新字段。 + +**API 说明** + +**project 的形式如下:** +``` +project({ + <表达式> +}) +``` + +表达式可以有以下格式: + +|格式 |说明 | +|---- |---- | +|<字段>: <1 或 true> |指定包含某个已有字段 | +|_id: <0 或 false> |舍弃 _id 字段 | +|<字段>: <表达式> |加入一个新字段,或者重置某个已有字段 | +|<字段>: <0 或 false> |舍弃某个字段(如果你指定舍弃了某个非 _id 字段,那么在此次 project 中,你不能再使用其它表达式) | + +**指定包含字段** + +_id 字段是默认包含在输出中的,除此之外其他任何字段,如果想要在输出中体现的话,必须在 project 中指定; 如果指定包含一个尚不存在的字段,那么 project 会忽略这个字段,不会加入到输出的文档中; + +**指定排除字段** + +如果你在 project 中指定排除某个字段,那么其它字段都会体现在输出中; 如果指定排除的是非 _id 字段,那么在本次 project 中,不能再使用其它表达式; + +**加入新的字段或重置某个已有字段** + +你可以使用一些特殊的表达式加入新的字段,或重置某个已有字段。 + +**多层嵌套的字段** + +有时有些字段处于多层嵌套的底层,我们可以使用点记法: +``` +"contact.phone.number": <1 or 0 or 表达式> +``` +也可以直接使用嵌套的格式: +``` +contact: { phone: { number: <1 or 0 or 表达式> } } +``` + +**示例** + +假设我们有一个 articles 集合,其中含有以下文档: +``` +{ + "_id": 666, + "title": "This is title", + "author": "Nobody", + "isbn": "123456789", + "introduction": "......" +} +``` +**指定包含某些字段** + +下面的代码使用 project,让输出只包含 _id、title 和 author 字段: +``` +db.collection('articles') + .aggregate() + .project({ + title: 1, + author: 1 + }) + .end() +``` + +输出如下: +``` +{ "_id" : 666, "title" : "This is title", "author" : "Nobody" } +``` + +**去除输出中的 _id 字段** + +_id 是默认包含在输出中的,如果不想要它,可以指定去除它: +``` +db.collection('articles') + .aggregate() + .project({ + _id: 0, // 指定去除 _id 字段 + title: 1, + author: 1 + }) + .end() +``` +输出如下: +``` +{ "title" : "This is title", "author" : "Nobody" } +``` + +**去除某个非 _id 字段** + +我们还可以指定在输出中去掉某个非 _id 字段,这样其它字段都会被输出: +``` +db.collection('articles') + .aggregate() + .project({ + isbn: 0, // 指定去除 isbn 字段 + }) + .end() +``` +输出如下,相比输入,没有了 isbn 字段: +``` +{ + "_id" : 666, + "title" : "This is title", + "author" : "Nobody", + "introduction": "......" +} +``` + +**加入计算出的新字段** + +假设我们有一个 students 集合,其中包含以下文档: +``` +{ + "_id": 1, + "name": "小明", + "scores": { + "chinese": 80, + "math": 90, + "english": 70 + } +} +``` +下面的代码,我们使用 project,在输出中加入了一个新的字段 totalScore: +``` +const { sum } = db.command.aggregate +db.collection('students') + .aggregate() + .project({ + _id: 0, + name: 1, + totalScore: sum([ + "$scores.chinese", + "$scores.math", + "$scores.english" + ]) + }) + .end() +``` +输出为: +``` +{ "name": "小明", "totalScore": 240 } +``` + +**加入新的数组字段** + +假设我们有一个 points 集合,包含以下文档: +``` +{ "_id": 1, "x": 1, "y": 1 } +{ "_id": 2, "x": 2, "y": 2 } +{ "_id": 3, "x": 3, "y": 3 } +``` + +下面的代码,我们使用 project,把 x 和 y 字段,放入到一个新的数组字段 coordinate 中: +``` +db.collection('points') + .aggregate() + .project({ + coordinate: ["$x", "$y"] + }) + .end() +``` +输出如下: +``` +{ "_id": 1, "coordinate": [1, 1] } +{ "_id": 2, "coordinate": [2, 2] } +{ "_id": 3, "coordinate": [3, 3] } +``` + +### replaceRoot + +聚合阶段。指定一个已有字段作为输出的根节点,也可以指定一个计算出的新字段作为根节点。 + +**API 说明** + +**replaceRoot 使用形式如下:** +``` +replaceRoot({ + newRoot: <表达式> +}) +``` +表达式格式如下: + +|格式 |说明 | +|---- |---- | +|<字段名> |指定一个已有字段作为输出的根节点(如果字段不存在则报错) | +|<对象> |计算一个新字段,并且把这个新字段作为根节点 | + +**示例** + +**使用已有字段作为根节点** + +假设我们有一个 schools 集合,内容如下: +``` +{ + "_id": 1, + "name": "SFLS", + "teachers": { + "chinese": 22, + "math": 18, + "english": 21, + "other": 123 + } +} +``` +下面的代码使用 replaceRoot,把 teachers 字段作为根节点输出: +``` +db.collection('schools') + .aggregate() + .replaceRoot({ + newRoot: '$teachers' + }) + .end() +``` +输出如下: +``` +{ + "chinese": 22, + "math": 18, + "english": 21, + "other": 123 +} +``` +**使用计算出的新字段作为根节点** + +假设我们有一个 roles 集合,内容如下: +``` +{ "_id": 1, "first_name": "四郎", "last_name": "黄" } +{ "_id": 2, "first_name": "邦德", "last_name": "马" } +{ "_id": 3, "first_name": "牧之", "last_name": "张" } +``` +下面的代码使用 replaceRoot,把 first_name 和 last_name 拼在一起: +``` +const { concat } = db.command.aggregate +db.collection('roles') + .aggregate() + .replaceRoot({ + newRoot: { + full_name: concat(['$last_name', '$first_name']) + } + }) + .end() +``` +输出如下: +``` +{ "full_name": "黄四郎" } +{ "full_name": "马邦德" } +{ "full_name": "张牧之" } +``` + +### sample + +聚合阶段。随机从文档中选取指定数量的记录。 + +**API 说明** + +**sample 的形式如下:** +``` +sample({ + size: <正整数> +}) +``` +请注意:size 是正整数,否则会出错。 + +**示例** + +假设文档 users 有以下记录: +``` +{ "name": "a" } +{ "name": "b" } +``` + +**随机选取** + +如果现在进行抽奖活动,需要选出一名幸运用户。那么 sample 的调用方式如下: +``` +db.collection('users') + .aggregate() + .sample({ + size: 1 + }) + .end() +``` + +返回了随机选中的一个用户对应的记录,结果如下: + +``` +{ "_id": "696529e4-7e82-4e7f-812e-5144714edff6", "name": "b" } +``` + +### skip + +聚合阶段。指定一个正整数,跳过对应数量的文档,输出剩下的文档。 + +**示例** +``` +db.collection('users') + .aggregate() + .skip(5) + .end() +``` + +这段代码会跳过查找到的前 5 个文档,并且把剩余的文档输出。 + +### sort + +聚合阶段。根据指定的字段,对输入的文档进行排序。 + +**API 说明** + +**形式如下:** +``` +sort({ + <字段名1>: <排序规则>, + <字段名2>: <排序规则>, +}) +``` + +<排序规则>可以是以下取值: + +- 1 代表升序排列(从小到大); +- -1 代表降序排列(从大到小); + +**示例** + +升序/降序排列 + +假设我们有集合 articles,其中包含数据如下: +``` +{ "_id": "1", "author": "stark", "score": 80, "age": 18 } +{ "_id": "2", "author": "bob", "score": 60, "age": 18 } +{ "_id": "3", "author": "li", "score": 55, "age": 19 } +{ "_id": "4", "author": "jimmy", "score": 60, "age": 22 } +{ "_id": "5", "author": "justan", "score": 95, "age": 33 } +``` +``` +db.collection('articles') + .aggregate() + .sort({ + age: -1, + score: -1 + }) + .end() +``` +上面的代码在 students 集合中进行聚合搜索,并且将结果排序,首先根据 age 字段降序排列,然后再根据 score 字段进行降序排列。 + +输出结果如下: +``` +{ "_id": "5", "author": "justan", "score": 95, "age": 33 } +{ "_id": "4", "author": "jimmy", "score": 60, "age": 22 } +{ "_id": "3", "author": "li", "score": 55, "age": 19 } +{ "_id": "1", "author": "stark", "score": 80, "age": 18 } +{ "_id": "2", "author": "bob", "score": 60, "age": 18 } +``` + +### sortByCount + +聚合阶段。根据传入的表达式,将传入的集合进行分组(group)。然后计算不同组的数量,并且将这些组按照它们的数量进行排序,返回排序后的结果。 + +**API 说明** + +**sortByCount 的调用方式如下:** +``` +sortByCount(<表达式>) +``` + +表达式的形式是:$ + 指定字段。请注意:不要漏写 $ 符号。 + +**示例** + +**统计基础类型** + +假设集合 passages 的记录如下: +``` +{ "category": "Web" } +{ "category": "Web" } +{ "category": "Life" } +``` +下面的代码就可以统计文章的分类信息,并且计算每个分类的数量。即对 category 字段执行 sortByCount 聚合操作。 +``` +db.collection('passages') + .aggregate() + .sortByCount('$category') + .end() +``` + +返回的结果如下所示:Web 分类下有2篇文章,Life 分类下有1篇文章。 +``` +{ "_id": "Web", "count": 2 } +{ "_id": "Life", "count": 1 } +``` +**解构数组类型** + +假设集合 passages 的记录如下:tags 字段对应的值是数组类型。 +``` +{ "tags": [ "JavaScript", "C#" ] } +{ "tags": [ "Go", "C#" ] } +{ "tags": [ "Go", "Python", "JavaScript" ] } +``` +如何统计文章的标签信息,并且计算每个标签的数量?因为 tags 字段对应的数组,所以需要借助 unwind 操作解构 tags 字段,然后再调用 sortByCount。 + +下面的代码实现了这个功能: +``` +db.collection('passages') + .aggregate() + .unwind(`$tags`) + .sortByCount(`$tags`) + .end() +``` +返回的结果如下所示: +``` +{ "_id": "Go", "count": 2 } +{ "_id": "C#", "count": 2 } +{ "_id": "JavaScript", "count": 2 } +{ "_id": "Python", "count": 1 } +``` + +### unwind + +聚合阶段。使用指定的数组字段中的每个元素,对文档进行拆分。拆分后,文档会从一个变为一个或多个,分别对应数组的每个元素。 + +**API 说明** + +使用指定的数组字段中的每个元素,对文档进行拆分。拆分后,文档会从一个变为一个或多个,分别对应数组的每个元素。 + +**unwind 有两种使用形式:** + +**参数是一个字段名** +``` +unwind(<字段名>) +``` +**参数是一个对象** +``` +unwind({ + path: <字段名>, + includeArrayIndex: , + preserveNullAndEmptyArrays: +}) +``` + +|字段 |类型 |说明 | +|---- |---- |---- | +|path |string |想要拆分的数组的字段名,需要以 $ 开头。 | +|includeArrayIndex |string |可选项,传入一个新的字段名,数组索引会保存在这个新的字段上。新的字段名不能以 $ 开头。 | +|preserveNullAndEmptyArrays |boolean|如果为 true,那么在 path 对应的字段为 null、空数组或者这个字段不存在时,依然会输出这个文档;如果为 false,unwind 将不会输出这些文档。默认为 false。| + +**示例** + +**拆分数组** + +假设我们有一个 products 集合,包含数据如下: +``` +{ "_id": "1", "product": "tshirt", "size": ["S", "M", "L"] } +{ "_id": "2", "product": "pants", "size": [] } +{ "_id": "3", "product": "socks", "size": null } +{ "_id": "4", "product": "trousers", "size": ["S"] } +{ "_id": "5", "product": "sweater", "size": ["M", "L"] } +``` + +我们根据 size 字段对这些文档进行拆分 +``` +db.collection('products') + .aggregate() + .unwind('$size') + .end() +``` + +输出如下: +``` +{ "_id": "1", "product": "tshirt", "size": "S" } +{ "_id": "1", "product": "tshirt", "size": "M" } +{ "_id": "1", "product": "tshirt", "size": "L" } +{ "_id": "4", "product": "trousers", "size": "S" } +{ "_id": "5", "product": "sweater", "size": "M" } +{ "_id": "5", "product": "sweater", "size": "L" } +``` + +**拆分后,保留原数组的索引** + +我们根据 size 字段对文档进行拆分后,想要保留原数组索引在新的 index 字段中。 +``` +db.collection('products') + .aggregate() + .unwind({ + path: '$size', + includeArrayIndex: 'index' + }) + .end() +``` +输出如下: +``` +{ "_id": "1", "product": "tshirt", "size": "S", "index": 0 } +{ "_id": "1", "product": "tshirt", "size": "M", "index": 1 } +{ "_id": "1", "product": "tshirt", "size": "L", "index": 2 } +{ "_id": "4", "product": "trousers", "size": "S", "index": 0 } +{ "_id": "5", "product": "sweater", "size": "M", "index": 0 } +{ "_id": "5", "product": "sweater", "size": "L", "index": 1 } +``` + +**保留字段为空的文档** + +注意到我们的集合中有两行特殊的空值数据: +``` +... +{ "_id": "2", "product": "pants", "size": [] } +{ "_id": "3", "product": "socks", "size": null } +... +``` +如果想要在输出中保留 size 为空数组、null,或者 size 字段不存在的文档,可以使用 preserveNullAndEmptyArrays 参数 +``` +db.collection('products') + .aggregate() + .unwind({ + path: '$size', + preserveNullAndEmptyArrays: true + }) + .end() +``` +输出如下: +``` +{ "_id": "1", "product": "tshirt", "size": "S" } +{ "_id": "1", "product": "tshirt", "size": "M" } +{ "_id": "1", "product": "tshirt", "size": "L" } +{ "_id": "2", "product": "pants", "size": null } +{ "_id": "3", "product": "socks", "size": null } +{ "_id": "4", "product": "trousers", "size": "S" } +{ "_id": "5", "product": "sweater", "size": "M" } +{ "_id": "5", "product": "sweater", "size": "L" } +``` + +### end + +标志聚合操作定义完成,发起实际聚合操作 + +**返回值** + +Promise.<Object> + +|属性 |类型 |说明 | +|--- |--- |--- | +|list |Array.<any>|聚合结果列表 | + +**示例代码** +``` +const $ = db.command.aggregate +db.collection('books').aggregate() + .group({ + // 按 category 字段分组 + _id: '$category', + // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值 + avgSales: $.avg('$sales') + }) + .end() + .then(res => console.log(res)) + .catch(err => console.error(err)) +``` + + +### 累计器操作符 + +#### addToSet + +聚合操作符。聚合运算符。向数组中添加值,如果数组中已存在该值,不执行任何操作。它只能在 group stage 中使用。 + +**API 说明** + +**addToSet 语法如下:** +``` +db.command.aggregate.addToSet(<表达式>) +``` +表达式是形如 $ + 指定字段 的字符串。如果指定字段的值是数组,那么整个数组会被当作一个元素。 + +**示例代码** + +假设集合 passages 的记录如下: +``` +{ "category": "web", "tags": [ "JavaScript", "CSS" ], "title": "title1" } +{ "category": "System", "tags": [ "C++", "C" ], "title": "title2" } +``` + +**非数组字段** + +每条记录的 category 对应值的类型是非数组,利用 addToSet 统计所有分类: +``` +const $ = db.command.aggregate +db + .collection('passages') + .aggregate() + .group({ + _id: null, + categories: $.addToSet('$category') + }) + .end() +``` +返回的结果如下: +``` +{ "_id": null, "categories": [ "System", "web" ] } +``` + +**数组字段** + +每条记录的 tags 对应值的类型是数组,数组不会被自动展开: +``` +const $ = db.command.aggregate +db + .collection('passages') + .aggregate() + .group({ + _id: null, + tagsList: $.addToSet('$tags') + }) + .end() +``` +返回的结果如下: +``` +{ "_id": null, "tagsList": [ [ "C++", "C" ], [ "JavaScript", "CSS" ] ] } +``` + +#### avg + +聚合操作符。返回一组集合中,指定字段对应数据的平均值。 + +**API 说明** + +**avg 的语法如下:** +``` +db.command.aggregate.avg() +``` + +avg 传入的值除了数字常量外,也可以是任何最终解析成一个数字的表达式。它会忽略非数字值。 + +**示例代码** +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 avg 可以计算所有记录的 score 的平均值: +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .group({ + _id: null, + average: $.avg('$score') + }) + .end() +``` +返回的结果如下: +``` +{ "_id": null, "average": 90 } +``` + +#### first + +聚合操作符。返回指定字段在一组集合的第一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 + +**API 说明** + +**first 的语法如下:** +``` +db.command.aggregate.first(<表达式>) +``` + +表达式是形如 $ + 指定字段 的字符串。 + +first 只能在 group 阶段被使用,并且需要配合 sort 才有意义。 + +**示例代码** + +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +如果需要得到所有记录中 score 的最小值,可以先将所有记录按照 score 排序,然后取出第一条记录的 first。 +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .sort({ + score: 1 + }) + .group({ + _id: null, + min: $.first('$score') + }) + .end() +``` +返回的数据结果如下: +``` +{ "_id": null, "min": 80 } +``` + +#### last + +聚合操作符。返回指定字段在一组集合的最后一条记录对应的值。仅当这组集合是按照某种定义排序( sort )后,此操作才有意义。 + +**API 说明** + +**last 的语法如下:** +``` +db.command.aggregate.last(<表达式>) +``` +表达式是形如 $ + 指定字段 的字符串。 + +last 只能在 group 阶段被使用,并且需要配合 sort 才有意义。 + +**示例代码** +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +如果需要得到所有记录中 score 的最大值,可以先将所有记录按照 score 排序,然后取出最后一条记录的 last。 +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .sort({ + score: 1 + }) + .group({ + _id: null, + max: $.last('$score') + }) + .end() +``` +返回的数据结果如下: +``` +{ "_id": null, "max": 100 } +``` + +#### max + +聚合操作符。返回一组数值的最大值。 + +**API 说明** + +**max 的语法如下:** +``` +db.command.aggregate.max(<表达式>) +``` +表达式是形如 $ + 指定字段 的字符串。 + +**示例代码** +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 max 可以统计不同组( group )中成绩的最高值,代码如下: +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .group({ + _id: '$group', + maxScore: $.max('$score') + }) + .end() +``` +返回的数据结果如下: +``` +{ "_id": "b", "maxScore": 100 } +{ "_id": "a", "maxScore": 96 } +``` + +#### mergeObjects + +聚合操作符。将多个文档合并为单个文档。 + +**API 说明** + +**使用形式如下: 在 group() 中使用时:** +``` +mergeObjects() +``` +**在其它表达式中使用时:** +``` +mergeObjects([, , ...]) +``` +**示例代码** + +**搭配 group() 使用** + +假设集合 sales 存在以下文档: +``` +{ "_id": 1, "year": 2018, "name": "A", "volume": { "2018Q1": 500, "2018Q2": 500 } } +{ "_id": 2, "year": 2017, "name": "A", "volume": { "2017Q1": 400, "2017Q2": 300, "2017Q3": 0, "2017Q4": 0 } } +{ "_id": 3, "year": 2018, "name": "B", "volume": { "2018Q1": 100 } } +{ "_id": 4, "year": 2017, "name": "B", "volume": { "2017Q3": 100, "2017Q4": 250 } } +``` +下面的代码使用 `mergeObjects()`,将用相同 `name` 的文档合并: +``` +const $ = db.command.aggregate +db.collection('sales').aggregate() + .group({ + _id: '$name', + mergedVolume: $.mergeObjects('$volume') + }) + .end() +``` +输出如下: +``` +{ "_id": "A", "mergedVolume": { "2017Q1": 400, "2017Q2": 300, "2017Q3": 0, "2017Q4": 0, "2018Q1": 500, "2018Q2": 500 } } +{ "_id": "B", "mergedVolume": { "2017Q3": 100, "2017Q4": 250, "2018Q1": 100 } } +``` +**一般用法** + +假设集合 test 存在以下文档: +``` +{ "_id": 1, "foo": { "a": 1 }, "bar": { "b": 2 } } +{ "_id": 2, "foo": { "c": 1 }, "bar": { "d": 2 } } +{ "_id": 3, "foo": { "e": 1 }, "bar": { "f": 2 } } +``` +下面的代码使用 `mergeObjects()`,将文档中的 `foo` 和 `bar` 字段合并为 `foobar`: +``` +const $ = db.command.aggregate +db.collection('sales').aggregate() + .project({ + foobar: $.mergeObjects(['$foo', '$bar']) + }) + .end() +``` +输出结果如下: +``` +{ "_id": 1, "foobar": { "a": 1, "b": 2 } } +{ "_id": 2, "foobar": { "c": 1, "d": 2 } } +{ "_id": 3, "foobar": { "e": 1, "f": 2 } } +``` + +#### min + +聚合操作符。返回一组数值的最小值。 + +**API 说明** + +**min 的语法如下:** +``` +db.command.aggregate.min(<表达式>) +``` +表达式是形如 $ + 指定字段 的字符串。 + +**示例代码** + +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 min 可以统计不同组( group )中成绩的最低值,代码如下: +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .group({ + _id: '$group', + minScore: $.min('$score') + }) + .end() +``` +返回的数据结果如下: +``` +{ "_id": "b", "minScore": 80 } +{ "_id": "a", "minScore": 84 } +``` + +#### push + +聚合操作符。在 group 阶段,返回一组中表达式指定列与对应的值,一起组成的数组。 + +**API 说明** + +**push 语法如下:** +``` +db.command.aggregate.push({ + <字段名1>: <指定字段1>, + <字段名2>: <指定字段2>, + ... +}) +``` +**示例代码** + +假设集合 students 的记录如下: +``` +{ "group": "a", "name": "stu1", "score": 84 } +{ "group": "a", "name": "stu2", "score": 96 } +{ "group": "b", "name": "stu3", "score": 80 } +{ "group": "b", "name": "stu4", "score": 100 } +``` +借助 push 操作,对不同分组( group )的所有记录,聚合所有数据并且将其放入一个新的字段中,进一步结构化和语义化数据。 +``` +const $ = db.command.aggregate +db + .collection('students') + .aggregate() + .group({ + _id: '$group', + students: $.push({ + name: '$name', + score: '$score' + }) + }) + .end() +``` +输出结果如下: +``` +{ "_id": "b", "students": [{ "name": "stu3", "score": 80 }, { "name": "stu4", "score": 100 }] } +{ "_id": "a", "students": [{ "name": "stu1", "score": 84 }, { "name": "stu2", "score": 96 }] } +``` + +#### stdDevPop + +聚合操作符。返回一组字段对应值的标准差。 + +**API 说明** + +**stdDevPop 的使用形式如下:** +``` +db.command.aggregate.stdDevPop(<表达式>) +``` + +表达式传入的是指定字段,指定字段对应的值的数据类型必须是 number ,否则结果会返回 null。 + +**示例代码** + +假设集合 students 的记录如下:a 组同学的成绩分别是84和96,b组同学的成绩分别是80和100。 +``` +{ "group":"a", "score":84 } +{ "group":"a", "score":96 } +{ "group":"b", "score":80 } +{ "group":"b", "score":100 } +``` +可以用 `stdDevPop` 来分别计算 a 和 b 两组同学成绩的标准差,以此来比较哪一组同学的成绩更稳定。代码如下: +``` +const $ = db.command.aggregate +db.collection('students').aggregate() + .group({ + _id: '$group', + stdDev: $.stdDevPop('$score') + }) + .end() +``` +返回的数据结果如下: +``` +{ "_id": "b", "stdDev": 10 } +{ "_id": "a", "stdDev": 6 } +``` + +#### stdDevSamp + +聚合操作符。计算输入值的样本标准偏差。如果输入值代表数据总体,或者不概括更多的数据,请改用 db.command.aggregate.stdDevPop。 + +**API 说明** + +**stdDevSamp 的使用形式如下:** +``` +db.command.aggregate.stdDevSamp(<表达式>) +``` + +表达式传入的是指定字段,stdDevSamp 会自动忽略非数字值。如果指定字段所有的值均是非数字,那么结果返回 null。 + +**示例代码** + +假设集合 students 的记录如下: +``` +{ "score": 80 } +{ "score": 100 } +``` + +可以用 `stdDevSamp` 来计算成绩的标准样本偏差。代码如下: +``` +const $ = db.command.aggregate +db.collection('students').aggregate() + .group({ + _id: null, + ageStdDev: $.stdDevSamp('$score') + }) + .end() +``` + +返回的数据结果如下: +``` +{ "_id": null, "ageStdDev": 14.142135623730951 } +``` +如果向集合 students 添加一条新记录,它的 score 字段类型是 string: + +``` +{ "score": "aa" } +``` +用上面代码计算标准样本偏差时,stdDevSamp 会自动忽略类型不为 number 的记录,返回结果保持不变。 + +#### sum + +聚合操作符。计算并且返回一组字段所有数值的总和。 + +**API 说明** + +**sum 的使用形式如下:** +``` +db.command.aggregate.sum(<表达式>) +``` + +表达式可以传入指定字段,也可以传入指定字段组成的列表。sum 会自动忽略非数字值。如果字段下的所有值均是非数字,那么结果返回 0。若传入数字常量,则当做所有记录该字段的值都给给定常量,在聚合时相加,最终值为输入记录数乘以常量。 + +**示例代码** + +假设代表商品的集合 goods 的记录如下:price 代表商品销售额,cost 代表商品成本 +``` +{ "cost": -10, "price": 100 } +{ "cost": -15, "price": 1 } +{ "cost": -10, "price": 10 } +``` + +**单独字段** + +借助 sum 可以计算所有商品的销售总和,代码如下: +``` +const $ = db.command.aggregate +db + .collection('goods') + .aggregate() + .group({ + _id: null, + totalPrice: $.sum('$price') + }) + .end() +``` +返回的数据结果如下:销售额是 111 +``` +{ "_id": null, "totalPrice": 111 } +``` + +**字段列表** + +如果需要计算所有商品的利润总额,那么需要将每条记录的 cost 和 price 相加得到此记录对应商品的利润。最后再计算所有商品的利润总额。 + +借助 sum,代码如下: +``` +const $ = db.command.aggregate +db + .collection('goods') + .aggregate() + .group({ + _id: null, + totalProfit: $.sum( + $.sum(['$price', '$cost']) + ) + }) + .end() +``` +返回的数据结果如下:利润总额为 76 +``` +{ "_id": null, "totalProfit": 76 } +``` \ No newline at end of file