聚合操作处理数据记录并返回计算结果。将来自多个文档的操作组值聚合在一起,并可以对分组的数据执行各种操作以返回单个结果。MongoDB提供了三种执行聚合的方法:聚合管道、map-reduce函数和单一用途的聚合方法。
一、聚合管道
聚合管道是基于数据处理管道概念建模的数据聚合框架。文档进入一个多阶段的管道,该管道将文档转换为聚合的结果。例如:
例子:
db.orders.aggregate([
{ $match: { status: "A" } },
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])
第一阶段:$match阶段根据状态字段筛选文档,并将状态等于“A”的文档传递到下一阶段。
第二阶段:$group阶段根据cust_id字段对文档进行分组,以计算每个惟一的cust_id的金额总和。
1.管道
MongoDB聚合管道由阶段组成。每个阶段都在文档通过管道时对其进行转换。流水线阶段不需要为每个输入文档生成一个输出文档;例如,某些阶段可能生成新文档或过滤掉文档。
除了$out、$merge和$geoNear阶段外,管道阶段可以在管道中出现多次。有关所有可用阶段的列表,请参见聚合管道阶段。
MongoDB在mongo shell中提供了db.collection.aggregate()方法,并提供了运行聚合管道的聚合命令。
例如,使用聚合管道时,请考虑使用用户首选项数据进行聚合和使用邮政编码数据集进行聚合。
从MongoDB 4.2开始,你可以使用聚合管道进行更新:
Command | mongo Shell Methods |
---|---|
findAndModify |
db.collection.findOneAndUpdate() db.collection.findAndModify() |
update |
db.collection.updateOne() db.collection.updateMany() db.collection.update() Bulk.find.update() Bulk.find.updateOne() Bulk.find.upsert() |
2. 管道表达式
一些管道阶段采用管道表达式作为操作数。管道表达式指定要应用到输入文档的转换。表达式具有文档结构,可以包含其他表达式。
管道表达式只能对管道中的当前文档进行操作,不能引用来自其他文档的数据:表达式操作提供文档在内存中的转换。
通常,表达式是无状态的,只有在聚合过程看到一个例外情况时才进行计算:累加器表达式。
在$group阶段中使用的累加器在文档通过管道时维护它们的状态(例如总计、最大值、最小值和相关数据)。
版本3.2的变化:$project阶段提供了一些累加器;但是,在$project阶段使用时,累加器不会跨文档维护它们的状态。
有关表达式的更多信息,请参见表达式。
3.聚合管道的行为
在MongoDB中,聚合命令操作单个集合,逻辑上将整个集合传递到聚合管道。为了优化操作,尽可能使用以下策略来避免扫描整个集合。
3.1 管道运营商及指标
MongoDB的查询规划器分析聚合管道,以确定是否可以使用索引来提高管道性能。例如,下面的管道阶段可以利用索引:
请注意:
下面的管道阶段并不代表可以使用索引的所有阶段的完整列表。
$match
如果索引出现在管道的开头,则$match阶段可以使用索引来筛选文档。
$sort
$sort阶段可以使用索引,只要索引之前没有$project、$unwind或$group阶段。
$group
$group阶段有时可以使用索引来查找每个组中的第一个文档,前提是满足以下所有条件:
$geoNear
$geoNear管道操作符利用地理空间索引。在使用$geoNear时,$geoNear管道操作必须作为聚合管道中的第一阶段出现。
3.2版本中的变化:从MongoDB 3.2开始,索引可以覆盖聚合管道。在MongoDB 2.6和3.0中,索引不能覆盖聚合管道,因为即使管道使用索引,聚合仍然需要访问实际的文档。
3.2 早期的过滤
如果聚合操作只需要集合中的一部分数据,那么可以使用$match、$limit和$skip阶段来限制在管道开始时输入的文档。当放置在管道的开头时,$match操作使用合适的索引只扫描集合中匹配的文档。
在管道的开始处放置一个$match管道阶段,然后放置一个$sort阶段,这在逻辑上等同于一个带有sort的查询,并且可以使用索引。如果可能,在管道的开头放置$match操作符。
4. 注意事项
4.1 分片集合
聚合管道支持对切分集合的操作。参见聚合管道和切分集合。
4.2 聚合vs使用映射-规约模式
聚合管道提供了map-reduce之外的另一种选择,并且可能是不需要map-reduce复杂性的聚合任务的首选解决方案。
4.3 局限性
聚合管道对值类型和结果大小有一些限制。有关聚合管道的限制和限制的详细信息,请参阅聚合管道限制。
4.4 流水线优化
聚合管道有一个内部优化阶段,为某些操作符序列提供改进的性能。有关详细信息,请参见聚合管道优化。
二、聚合管道优化
聚合管道操作有一个优化阶段,该阶段尝试重新塑造管道以提高性能。
要查看优化器如何转换特定的聚合管道,请在db. collections .aggregate()方法中包含explain选项。
1. 投影优化
聚合管道可以确定是否只需要文档中的一个字段子集就可以获得结果。如果是这样,管道将只使用那些必需的字段,从而减少通过管道的数据量。
2.管道序列优化
($project或$unset或$addFields或$set) + $match序列优化
对于包含一个投影阶段($project或$unset或$addFields或$set)和一个$match阶段的聚合管道,MongoDB将$match阶段中不需要在投影阶段计算值的任何过滤器移动到一个新的$match阶段,然后再进行投影。
如果聚合管道包含多个投影和/或$match阶段,MongoDB将对每个$match阶段执行此优化,在筛选器不依赖的所有投影阶段之前移动每个$match筛选器。
考虑以下几个阶段的管道:
{ $addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
} },
{ $project: {
_id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: {
name: "Joe Schmoe",
maxTime: { $lt: 20 },
minTime: { $gt: 5 },
avgTime: { $gt: 7 }
} }
优化器将$match阶段分解为四个单独的过滤器,每个过滤器对应$match查询文档中的每个键。然后优化器在尽可能多的投影阶段之前移动每个筛选器,根据需要创建新的$match阶段。在这个例子中,优化器产生以下优化管道:
{ $match: { name: "Joe Schmoe" } },
{ $addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
} },
{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } },
{ $project: {
_id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: { avgTime: { $gt: 7 } } }
$match过滤器{avgTime: {$gt: 7}}依赖于$project阶段来计算avgTime字段。$project阶段是此管道中的最后一个投影阶段,因此无法移动avgTime上的$match过滤器。
maxTime和minTime字段在$addFields阶段计算,但不依赖于$project阶段。优化器为这些字段上的过滤器创建了一个新的$match阶段,并将其置于$project阶段之前。
$match筛选器{name: "Joe Schmoe"}不使用在$project或$addFields阶段中计算的任何值,因此它在两个投影阶段之前被移动到一个新的$match阶段。
请注意:
优化之后,过滤器{name: "Joe Schmoe"}在管道开始处处于$match阶段。这还有一个好处,即允许聚合在最初查询集合时使用name字段上的索引。有关更多信息,请参见管道操作符和索引。
$sort + $match序列优化
当您有一个$sort后面跟着$match的序列时,$match在$sort之前移动,以最小化要排序的对象的数量。例如,如果管道由以下几个阶段组成:
{ $sort: { age : -1 } },
{ $match: { status: 'A' } }
在优化阶段,优化器将序列转换为以下内容:
{ $match: { status: 'A' } },
{ $sort: { age : -1 } }
$redact + $match序列优化
如果可能,当管道中的$redact阶段紧跟在$match阶段之后时,聚合有时可以在$redact阶段之前添加$match阶段的一部分。如果添加的$match阶段位于管道的开头,则聚合可以使用索引和查询集合来限制进入管道的文档数量。有关更多信息,请参见管道操作符和索引。
例如,如果管道由以下几个阶段组成:
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }
优化器可以在$redact阶段之前添加相同的$match阶段:
{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }
$project/$unset + $skip序列优化
新版本3.2。
当您有一个$project或$unset后面跟着$skip的序列时,$skip在$project之前移动。例如,如果管道由以下几个阶段组成:
{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }
在优化阶段,优化器将序列转换为以下内容:
{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }
3. 管道联合优化
在可能的情况下,优化阶段将管道阶段合并到其前身。通常,在任何序列重新排序优化之后都会发生合并。
$sort + $limit合并
在4.0版本中进行了更改。
当$sort先于$limit时,如果没有中间阶段修改文档的数量(例如$unwind、$group),那么优化器可以将$limit合并到$sort中。如果有管道阶段在$sort和$limit阶段之间更改文档数量,MongoDB不会将$limit合并到$sort中。
例如,如果管道由以下几个阶段组成:
{ $sort : { age : -1 } },
{ $project : { age : 1, status : 1, name : 1 } },
{ $limit: 5 }
在优化阶段,优化器合并序列如下:
{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(5)
}
},
{ "$project" : {
"age" : 1,
"status" : 1,
"name" : 1
}
}
这允许排序操作在进行过程中只维护前n个结果,其中n是指定的限制,MongoDB只需要在内存[1]中存储n个条目。有关更多信息,请参见$sort操作符和内存。
使用$SKIP进行序列优化
如果在$sort和$limit阶段之间存在$skip阶段,MongoDB将把$limit合并到$sort阶段,并将$limit值增加$skip金额。有关示例,请参见$sort + $skip + $limit Sequence。
$limit + $limit合并
当一个$限额紧随另一个$限额之后时,这两个阶段可以合并为一个$限额,其中限额金额是两个初始限额中较小的那个。例如,管道包含以下序列:
{ $limit: 100 },
{ $limit: 10 }
然后,第二个$limit阶段可以合并为第一个$limit阶段,并产生一个$limit阶段,其中限额金额10是两个初始限额100和10的最小值。
{ $limit: 10 }
$skip + $skip合并
当一个$skip紧跟着另一个$skip时,这两个阶段可以合并成一个$skip,其中skip金额是两个初始skip金额的总和。例如,管道包含以下序列:
{ $skip: 5 },
{ $skip: 2 }
然后,第二个$skip阶段可以合并为第一个$skip阶段,并产生一个$skip阶段,其中skip amount 7是两个初始限制5和2的总和。
{ $skip: 7 }
$match + $match合并
当$match紧跟在另一个$match之后时,这两个阶段可以合并为单个$match,将条件与$and组合在一起。例如,管道包含以下序列:
{ $match: { year: 2014 } },
{ $match: { status: "A" } }
然后,第二个$match阶段可以合并为第一个$match阶段,并产生单个$match阶段
{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
$lookup + $unwind合并
新版本3.2。
当$unwind紧跟在另一个$lookup之后,并且$unwind操作在$lookup的as字段上时,优化器可以将$unwind合并到$lookup阶段。这避免了创建大型中间文档。
例如,管道包含以下序列:
{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y"
}
},
{ $unwind: "$resultingArray"}
优化器可以将$unwind阶段合并为$lookup阶段。如果使用explain选项运行聚合,则explain输出将显示合并阶段:
{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y",
unwinding: { preserveNullAndEmptyArrays: false }
}
}
4. 例子
$sort + $skip + $limit序列
管道包含一个$sort序列,后面跟着一个$skip,后面跟着一个$limit:
{ $sort: { age : -1 } },
{ $skip: 10 },
{ $limit: 5 }
优化器执行$sort + $limit合并,将序列转换为以下内容:
{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(15)
}
},
{
"$skip" : NumberLong(10)
}
MongoDB通过重新排序增加$limit金额。
三、聚合管道极限
使用聚合命令的聚合操作具有以下限制。
1.结果大小限制
版本3.6中的变化:MongoDB 3.6删除了聚合命令以单个文档的形式返回结果的选项。
聚合命令可以返回游标,也可以将结果存储在集合中。当返回游标或将结果存储在集合中时,结果集中的每个文档都受BSON文档大小限制,目前为16兆字节;如果任何单个文档超过了BSON文档大小限制,该命令将产生一个错误。该限制只适用于已退回的文件;在管道处理期间,文档可能超过这个大小。aggregate()方法返回一个游标。
2. 内存限制
流水线阶段的RAM限制为100 MiB(100 * 1024 * 1024字节)。如果一个阶段超过这个限制,MongoDB将产生一个错误。要允许处理大型数据集,可以在aggregate()方法中设置allowDiskUse选项。allowDiskUse选项允许大多数聚合管道操作将数据写入临时文件。allowDiskUse选项的例外是以下聚合操作;这些操作必须保持在内存限制限制:
如果管道包含其他在aggregate()操作中观察allowDiskUse: true的阶段,则allowDiskUse: true选项对这些其他阶段有效。
从MongoDB 4.2开始,分析器日志消息和诊断日志消息包括一个usedDisk指示器,如果任何聚合阶段由于内存限制将数据写入临时文件。
四、聚合管道和切分集合
聚合管道支持对切分集合的操作。本节描述特定于聚合管道和切分集合的行为。
1. 性能
在3.2版本中进行了更改。
如果管道从切分键上的精确$match开始,则整个管道仅在匹配的切分上运行。在以前,管道会被分割,而合并它的工作必须在主碎片上完成。
对于必须在多个切分上运行的聚合操作,如果这些操作不需要在数据库的主切分上运行,则这些操作将把结果路由到一个随机切分以合并结果,以避免对该数据库的主切分超载。$out阶段和$lookup阶段需要在数据库的主碎片上运行。
2. 优化
当将聚合管道分成两部分时,将管道进行分割,以确保shard执行尽可能多的阶段,同时考虑优化。
要查看如何分割管道,请在db.collection.aggregate()方法中包含explain选项。
优化可能会在不同版本之间发生变化。
五、Aggregation with the Zip Code Data Set
本文档中的示例使用zipcodes集合。这个集合可以在:media.mongodb.org/zips.json中找到。使用mongoimport将这个数据集加载到mongod实例中。
1. 数据模型
邮政编码收集内的每一份文件均有下列表格:
{
"_id": "10280",
"city": "NEW YORK",
"state": "NY",
"pop": 5574,
"loc": [
-74.016323,
40.710537
]
}
1.1 aggregate()方法
下面的所有示例都使用了mongo shell中的aggregate()帮助器。
aggregate()方法使用聚合管道将文档处理为聚合结果。聚合管道由多个阶段组成,每个阶段在文档通过管道时处理文档。文档依次通过各个阶段。
mongo shell中的aggregate()方法为aggregate数据库命令提供了一个包装器。有关数据聚合操作的更惯用的接口,请参阅驱动程序的文档。
1.2 返回人口超过1000万的州
下面的聚合操作返回总人口大于1000万的所有状态:
db.zipcodes.aggregate( [
{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
{ $match: { totalPop: { $gte: 10*1000*1000 } } }
] )
在本例中,聚合管道由$group阶段和$match阶段组成:
在$group阶段之后,准备中的文档如下:
{
"_id" : "AK",
"totalPop" : 550043
}
这个聚合操作的等效SQL是:
SELECT state, SUM(pop) AS totalPop
FROM zipcodes
GROUP BY state
HAVING totalPop >= (10*1000*1000)
2. 按州返回平均城市人口
下面的汇总操作返回每个州城市的平均人口:
db.zipcodes.aggregate( [
{ $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
{ $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }
] )
在本例中,聚合管道由$group阶段和另一个$group阶段组成:
在这个准备阶段之后,文件类似如下:
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
这种聚合操作产生的文档如下:
{
"_id" : "MN",
"avgCityPop" : 5335
}
3. 按州返回最大和最小的城市
下面的聚合操作按人口大小返回每个州的城市:
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" }
}
},
// the following $project is optional, and
// modifies the output format.
{ $project:
{ _id: 0,
state: "$_id",
biggestCity: { name: "$biggestCity", pop: "$biggestPop" },
smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
}
}
] )
在本例中,聚合管道包括$group阶段、$sort阶段、另一个$group阶段和$project阶段:
在筹备阶段,这些文件大致如下:
{
"_id" : {
"state" : "CO",
"city" : "EDGEWATER"
},
"pop" : 13154
}
目前正在拟订的文件如下:
{
"_id" : "WA",
"biggestCity" : "SEATTLE",
"biggestPop" : 520096,
"smallestCity" : "BENGE",
"smallestPop" : 2
}
该聚合操作的输出文档如下:
{
"state" : "RI",
"biggestCity" : {
"name" : "CRANSTON",
"pop" : 176404
},
"smallestCity" : {
"name" : "CLAYVILLE",
"pop" : 45
}
}
六、与用户首选项数据聚合
1.数据模型
假设一个体育俱乐部拥有一个数据库,其中包含一个用户集合,该集合跟踪用户的加入日期、运动偏好,并将这些数据存储在类似于以下文档的文档中:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : ["golf", "racquetball"]
}
{
_id : "joe",
joined : ISODate("2012-07-02"),
likes : ["tennis", "golf", "swimming"]
}
2. 对文档进行规范化和排序
下面的操作以大写字母和字母顺序返回用户名。聚合包括用户集合中所有文档的用户名。您可以这样做来规范化处理的用户名。
db.users.aggregate(
[
{ $project : { name:{$toUpper:"$_id"} , _id:0 } },
{ $sort : { name : 1 } }
]
)
用户集合中的所有文档都要经过流水线,流水线由以下操作组成:
(1)$project操作:
(2)$sort操作符根据name字段对结果排序。
汇总的结果如下:
{
"name" : "JANE"
},
{
"name" : "JILL"
},
{
"name" : "JOE"
}
3. 返回按加入月排序的用户名
下面的聚合操作将返回按加入月份排序的用户名。这种聚合可以帮助生成会员续订通知。
db.users.aggregate(
[
{ $project :
{
month_joined : { $month : "$joined" },
name : "$_id",
_id : 0
}
},
{ $sort : { month_joined : 1 } }
]
)
管道通过以下操作传递用户集合中的所有文档:
(1)$project
操作符:
(2)$month操作符将联接字段的值转换为月份的整数表示形式。然后,$project操作符将这些值分配给month_joining字段。
(3)$sort操作符根据month_joining字段对结果进行排序。
操作返回类似以下的结果:
{
"month_joined" : 1,
"name" : "ruth"
},
{
"month_joined" : 1,
"name" : "harold"
},
{
"month_joined" : 1,
"name" : "kate"
}
{
"month_joined" : 2,
"name" : "jill"
}
4. 返回每个月的连接总数
下面的操作显示每年每个月有多少人加入。您可以将这些聚合数据用于招聘和营销策略。
db.users.aggregate(
[
{ $project : { month_joined : { $month : "$joined" } } } ,
{ $group : { _id : {month_joined:"$month_joined"} , number : { $sum : 1 } } },
{ $sort : { "_id.month_joined" : 1 } }
]
)
管道通过以下操作传递用户集合中的所有文档:
(1)$project操作符创建一个名为month_join的新字段。
(2)$month操作符将联接字段的值转换为月份的整数表示形式。然后,$project操作符将这些值分配给month_joining字段。
(3)$group操作符收集具有给定month_join值的所有文档,并计算该值有多少个文档。具体来说,对于每个惟一的值,$group创建一个新的“每月”文档,其中包含两个字段:
(4)$sort操作符根据month_joining字段的内容对$group创建的文档进行排序。
此聚合操作的结果将类似于以下内容:
{
"_id" : {
"month_joined" : 1
},
"number" : 3
},
{
"_id" : {
"month_joined" : 2
},
"number" : 9
},
{
"_id" : {
"month_joined" : 3
},
"number" : 5
}
5. 返回五个最常见的“赞”
下面的汇总收集了数据集中最受欢迎的五个活动。这种类型的分析可以帮助规划和未来的发展。
db.users.aggregate(
[
{ $unwind : "$likes" },
{ $group : { _id : "$likes" , number : { $sum : 1 } } },
{ $sort : { number : -1 } },
{ $limit : 5 }
]
)
管道从用户集合中的所有文档开始,通过以下操作传递这些文档:
例子:
给定用户集合中的以下文档:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : ["golf", "racquetball"]
}
$unwind操作符将创建以下文档:
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : "golf"
}
{
_id : "jane",
joined : ISODate("2011-03-02"),
likes : "racquetball"
}
汇总的结果如下:
{
"_id" : "golf",
"number" : 33
},
{
"_id" : "racquetball",
"number" : 31
},
{
"_id" : "swimming",
"number" : 24
},
{
"_id" : "handball",
"number" : 19
},
{
"_id" : "tennis",
"number" : 18
}
七、Map-Reduce和Sharded集合
Map-reduce支持对切分集合进行操作,既可以作为输入,也可以作为输出。本节描述特定于切分集合的mapReduce行为。
但是,从4.2版开始,MongoDB就不支持使用map-reduce选项来创建新的切分集合,也不支持使用切分选项来进行map-reduce。要输出到sharded集合,首先创建sharded集合。MongoDB 4.2还反对替换现有的切分集合。
1. 作为输入的切分集合
当使用sharded collection作为map-reduce操作的输入时,mongos将自动并行地将map-reduce作业分派给每个shard。不需要特殊选项。mongos将等待所有碎片上的工作完成。
2. 作为输出的切分集合
如果mapReduce的out字段有分片值,MongoDB使用_id字段作为分片键对输出集合进行分片。
请注意:
从4.2版开始,MongoDB就不支持在mapReduce/db.collection.mapReduce中使用切分选项。
输出到分片集合:
请注意:
在后来的map-reduce作业中,MongoDB根据需要分割块。
为了避免并发性问题,在后处理期间会自动阻止输出集合的块的平衡。
八、使用映射-规约模式并发
map-reduce操作由许多任务组成,包括对输入集合的读取、map函数的执行、reduce函数的执行、在处理期间对临时集合的写入以及对输出集合的写入。
在操作过程中,map-reduce采取以下锁:
请注意:
后处理期间的最终写锁使结果自动显示。但是,合并和减少输出操作可能需要几分钟的时间。对于merge和reduce,非原子标记是可用的,它在写入每个输出文档之间释放锁。从MongoDB 4.2开始,显式地设置nonAtomic: false是不赞成的。有关更多信息,请参见db.collection.mapReduce()引用。
九、使用映射-规约模式的例子
在mongo shell中,db.collection.mapReduce()方法是mapReduce命令的包装器。下面的例子使用了db.collection.mapReduce()方法:
考虑以下对包含以下原型文档的集合订单的映射-简化操作:
{
_id: ObjectId("50a8240b927d5d8b5891743c"),
cust_id: "abc123",
ord_date: new Date("Oct 04, 2012"),
status: 'A',
price: 25,
items: [ { sku: "mmm", qty: 5, price: 2.5 },
{ sku: "nnn", qty: 5, price: 2.5 } ]
}
1. 返回每个客户的总价
根据cust_id对订单集合进行map-reduce操作,并计算每个cust_id的价格总和:
(1)定义map函数来处理每个输入文档:
var mapFunction1 = function() {
emit(this.cust_id, this.price);
};
(2)定义相应的减少函数有两个参数keyCustId和valuesPrices:
var reduceFunction1 = function(keyCustId, valuesPrices) {
return Array.sum(valuesPrices);
};
(3)使用mapFunction1映射函数和reduceFunction1 reduce函数对orders集合中的所有文档执行map-reduce。
db.orders.mapReduce(
mapFunction1,
reduceFunction1,
{ out: "map_reduce_example" }
)
该操作将结果输出到一个名为map_reduce_example的集合。如果map_reduce_example集合已经存在,这个操作将用这个map-reduce操作的结果来替换内容:
2. 计算订单和总数量,每项平均数量
在本例中,您将对所有ord_date值大于01/01/2012的文档的orders集合执行映射缩减操作。按项目分组。sku字段,并计算每个sku的订单数量和订单总量。通过计算每个sku值的订单平均数量,得出以下结论:
(1)定义map函数来处理每个输入文档:
var mapFunction2 = function() {
for (var idx = 0; idx < this.items.length; idx++) {
var key = this.items[idx].sku;
var value = {
count: 1,
qty: this.items[idx].qty
};
emit(key, value);
}
};
(2)定义相应的reduce函数有两个参数keySKU和countObjVals:
var reduceFunction2 = function(keySKU, countObjVals) {
reducedVal = { count: 0, qty: 0 };
for (var idx = 0; idx < countObjVals.length; idx++) {
reducedVal.count += countObjVals[idx].count;
reducedVal.qty += countObjVals[idx].qty;
}
return reducedVal;
};
(3)定义一个带有两个参数key和reducedVal的finalize函数。该函数修改reducedVal对象,添加一个名为avg的计算字段,并返回修改后的对象:
var finalizeFunction2 = function (key, reducedVal) {
reducedVal.avg = reducedVal.qty/reducedVal.count;
return reducedVal;
};
(4)使用mapFunction2、reduceFunction2和finalizeFunction2函数对orders集合执行map-reduce操作。
db.orders.mapReduce( mapFunction2,
reduceFunction2,
{
out: { merge: "map_reduce_example" },
query: { ord_date:
{ $gt: new Date('01/01/2012') }
},
finalize: finalizeFunction2
}
)
该操作使用query字段只选择那些ord_date大于new Date(01/01/2012)的文档。然后将结果输出到集合map_reduce_example。如果map_reduce_example集合已经存在,那么该操作将把现有的内容与这个map-reduce操作的结果合并起来。
八、执行增量使用映射-规约模式
Map-reduce操作可以处理复杂的聚合任务。要执行map-reduce操作,MongoDB提供mapReduce命令,在mongo shell中,提供db.collection.mapReduce()包装器方法。
如果map-reduce数据集不断增长,您可能希望每次对整个数据集执行递增的map-reduce操作,而不是执行map-reduce操作。
要执行增量映射-减少:
考虑下面的示例,在这个示例中,您计划在每天结束时对一个sessions集合执行一个map-reduce操作。
1. 数据设置
sessions集合包含每天记录用户会话的文档,例如:
db.sessions.save( { userid: "a", ts: ISODate('2011-11-03 14:17:00'), length: 95 } );
db.sessions.save( { userid: "b", ts: ISODate('2011-11-03 14:23:00'), length: 110 } );
db.sessions.save( { userid: "c", ts: ISODate('2011-11-03 15:02:00'), length: 120 } );
db.sessions.save( { userid: "d", ts: ISODate('2011-11-03 16:45:00'), length: 45 } );
db.sessions.save( { userid: "a", ts: ISODate('2011-11-04 11:05:00'), length: 105 } );
db.sessions.save( { userid: "b", ts: ISODate('2011-11-04 13:14:00'), length: 120 } );
db.sessions.save( { userid: "c", ts: ISODate('2011-11-04 17:00:00'), length: 130 } );
db.sessions.save( { userid: "d", ts: ISODate('2011-11-04 15:37:00'), length: 65 } );
2. 当前集合的初始映射缩减
按如下步骤运行第一个map-reduce操作:
(1)定义将userid映射到包含userid、total_time、count和avg_time字段的对象的映射函数:
var mapFunction = function() {
var key = this.userid;
var value = {
userid: this.userid,
total_time: this.length,
count: 1,
avg_time: 0
};
emit( key, value );
};
(2)定义相应的reduce函数,使用两个参数键和值来计算总时间和计数。键对应于userid,值是一个数组,其元素对应于映射到mapFunction中的userid的单个对象。
var reduceFunction = function(key, values) {
var reducedObject = {
userid: key,
total_time: 0,
count:0,
avg_time:0
};
values.forEach( function(value) {
reducedObject.total_time += value.total_time;
reducedObject.count += value.count;
}
);
return reducedObject;
};
(3)使用两个参数key和reducedValue定义finalize函数。该函数修改reducedValue文档以添加另一个字段平均值并返回修改后的文档。
var finalizeFunction = function (key, reducedValue) {
if (reducedValue.count > 0)
reducedValue.avg_time = reducedValue.total_time / reducedValue.count;
return reducedValue;
};
(4) 使用mapFunction、reduceFunction和finalizeFunction函数对会话集合执行map-reduce。将结果输出到集合session_stat。如果session_stat集合已经存在,则操作将替换内容:
db.sessions.mapReduce( mapFunction,
reduceFunction,
{
out: "session_stat",
finalize: finalizeFunction
}
)
3. 后续的增量使用映射-规约模式
稍后,随着sessions集合的增长,您可以运行额外的map-reduce操作。例如,向会话集合添加新文档:
db.sessions.save( { userid: "a", ts: ISODate('2011-11-05 14:17:00'), length: 100 } );
db.sessions.save( { userid: "b", ts: ISODate('2011-11-05 14:23:00'), length: 115 } );
db.sessions.save( { userid: "c", ts: ISODate('2011-11-05 15:02:00'), length: 125 } );
db.sessions.save( { userid: "d", ts: ISODate('2011-11-05 16:45:00'), length: 55 } );
在一天结束时,对sessions集合执行增量的map-reduce,但是使用query字段只选择新文档。将结果输出到集合session_stat,但是使用增量映射-reduce的结果减少内容:
db.sessions.mapReduce( mapFunction,
reduceFunction,
{
query: { ts: { $gt: ISODate('2011-11-05 00:00:00') } },
out: { reduce: "session_stat" },
finalize: finalizeFunction
}
);
九、排除映射函数的故障
map函数是一个JavaScript函数,它将值与键关联或“映射”,并在map-reduce操作期间发出键和值对。
请注意:
从4.2.1版开始,MongoDB就不支持在map、reduce和finalize函数中使用带有作用域(即BSON类型15)的JavaScript。要确定变量的作用域,请使用作用域参数。
要验证map函数发出的键和值对,请编写自己的emit函数。
考虑一个包含以下原型文档的集合订单:
{
_id: ObjectId("50a8240b927d5d8b5891743c"),
cust_id: "abc123",
ord_date: new Date("Oct 04, 2012"),
status: 'A',
price: 250,
items: [ { sku: "mmm", qty: 5, price: 2.5 },
{ sku: "nnn", qty: 5, price: 2.5 } ]
}
(1)定义映射函数,将价格映射到每个文档的cust_id,并发出cust_id和价格对:
var map = function() {
emit(this.cust_id, this.price);
};
(2)定义emit函数来打印key和value:
var emit = function(key, value) {
print("emit");
print("key: " + key + " value: " + tojson(value));
}
(3)使用orders集合中的单个文档调用map函数:
var myDoc = db.orders.findOne( { _id: ObjectId("50a8240b927d5d8b5891743c") } );
map.apply(myDoc);
(4)验证键和值对是否如您所期望的那样。
emit
key: abc123 value:250
(5)使用来自orders集合的多个文档调用map函数:
var myCursor = db.orders.find( { cust_id: "abc123" } );
while (myCursor.hasNext()) {
var doc = myCursor.next();
print ("document _id= " + tojson(doc._id));
map.apply(doc);
print();
}
(6)验证键和值对是否如预期的那样。
十、对Reduce函数进行故障排除
reduce函数是一个JavaScript函数,它将在map-reduce操作期间与特定键关联的所有值“简化”为一个单一对象。reduce功能必须满足各种需求。本教程帮助验证reduce函数是否满足以下条件:
有关reduce函数的所有要求的列表,请参见mapReduce或mongo shell帮助器方法db.collection.mapReduce()。
请注意:
从4.2.1版开始,MongoDB就不支持在map、reduce和finalize函数中使用带有作用域(即BSON类型15)的JavaScript。要确定变量的作用域,请使用作用域参数。
1. 确认输出类型
您可以测试reduce函数是否返回与map函数发出的值类型相同的值。
(1)定义一个reduceFunction1函数,它接受参数keyCustId和valuesPrices。valuesPrices是一个整数数组:
var reduceFunction1 = function(keyCustId, valuesPrices) {
return Array.sum(valuesPrices);
};
(2)定义一个整数数组样本:
var myTestValues = [ 5, 5, 10 ];
(3)使用myTestValues调用reduceFunction1:
reduceFunction1('myKey', myTestValues);
(4)验证reduceFunction1返回的是一个整数:
20
(5)定义一个reduceFunction2函数,它接受参数keySKU和valuesCountObjects。valuesCountObjects是一个包含两个字段count和qty的文档数组:
var reduceFunction2 = function(keySKU, valuesCountObjects) {
reducedValue = { count: 0, qty: 0 };
for (var idx = 0; idx < valuesCountObjects.length; idx++) {
reducedValue.count += valuesCountObjects[idx].count;
reducedValue.qty += valuesCountObjects[idx].qty;
}
return reducedValue;
};
(6)定义一个文档样本数组:
var myTestObjects = [
{ count: 1, qty: 5 },
{ count: 2, qty: 10 },
{ count: 3, qty: 15 }
];
(7)使用myTestObjects调用reduceFunction2:
reduceFunction2('myKey', myTestObjects);
(8)验证reduceFunction2返回的文档是否包含count和qty字段:
{ "count" : 6, "qty" : 30 }
2. 确保对映射值的顺序不敏感
reduce函数的参数是一个键和一个值数组。您可以测试reduce函数的结果是否依赖于值数组中元素的顺序。
(1)定义一个示例values1数组和一个示例values2数组,它们只在数组元素的顺序上不同:
var values1 = [
{ count: 1, qty: 5 },
{ count: 2, qty: 10 },
{ count: 3, qty: 15 }
];
var values2 = [
{ count: 3, qty: 15 },
{ count: 1, qty: 5 },
{ count: 2, qty: 10 }
];
(2)定义一个reduceFunction2函数,它接受参数keySKU和valuesCountObjects。valuesCountObjects是一个包含两个字段count和qty的文档数组:
var reduceFunction2 = function(keySKU, valuesCountObjects) {
reducedValue = { count: 0, qty: 0 };
for (var idx = 0; idx < valuesCountObjects.length; idx++) {
reducedValue.count += valuesCountObjects[idx].count;
reducedValue.qty += valuesCountObjects[idx].qty;
}
return reducedValue;
};
(3)首先使用values1调用reduceFunction2,然后使用values2:
reduceFunction2('myKey', values1);
reduceFunction2('myKey', values2);
(4)验证reduceFunction2返回相同的结果:
{ "count" : 6, "qty" : 30 }
3. 确保降低函数幂等性
因为map-reduce操作可以对同一个键多次调用reduce,而不会对工作集中键的单个实例调用reduce,所以reduce函数必须返回与map函数发出的值相同类型的值。您可以测试reduce函数处理“简化”的值,而不影响最终的值。
(1)定义一个reduceFunction2函数,它接受参数keySKU和valuesCountObjects。valuesCountObjects是一个包含两个字段count和qty的文档数组:
var reduceFunction2 = function(keySKU, valuesCountObjects) {
reducedValue = { count: 0, qty: 0 };
for (var idx = 0; idx < valuesCountObjects.length; idx++) {
reducedValue.count += valuesCountObjects[idx].count;
reducedValue.qty += valuesCountObjects[idx].qty;
}
return reducedValue;
};
(2)定义一个样本密钥:
var myKey = 'myKey';
(3)定义一个示例valuesIdempotent数组,该数组包含一个调用reduceFunction2函数的元素:
var valuesIdempotent = [
{ count: 1, qty: 5 },
{ count: 2, qty: 10 },
reduceFunction2(myKey, [ { count:3, qty: 15 } ] )
];
(4)定义一个组合传递给reduceFunction2的值的示例values1数组:
var values1 = [
{ count: 1, qty: 5 },
{ count: 2, qty: 10 },
{ count: 3, qty: 15 }
];
(5)首先使用myKey和valuesIdempotent调用reduceFunction2,然后使用myKey和values1:
reduceFunction2(myKey, valuesIdempotent);
reduceFunction2(myKey, values1);
(6)验证reduceFunction2返回相同的结果:
{ "count" : 6, "qty" : 30 }
十一、聚合管道快速引用
请注意:
有关特定运算符的详细信息,包括语法和示例,请单击特定运算符进入其参考页面。
1. Stages
1.1 Stages (db.collection.aggregate
)
db.collection.aggregate
方法,流水线阶段出现在一个数组中。文档依次通过各个阶段。除了$out、$merge和$geoNear阶段外,其他阶段都可以在管道中多次出现。
db.collection.aggregate( [ { }, ... ] )
Stage | Description |
---|---|
$addFields |
向文档添加新字段。与$project类似,$addFields会对流中的每个文档进行整形;具体来说,通过向输出文档添加新字段,这些输出文档包含来自输入文档的现有字段和新添加的字段。 $set是$addFields的别名。 |
$bucket |
根据指定的表达式和桶边界,将传入的文档分类到称为桶的组中。 |
$bucketAuto |
根据指定的表达式将传入的文档分类到特定数量的组(称为bucket)中。Bucket边界将自动确定,以便将文档平均分配到指定数量的Bucket中。 |
$collStats |
返回关于集合或视图的统计信息。 |
$count |
返回聚合管道此阶段的文档数量的计数。 |
$facet |
在同一输入文档集的单个阶段内处理多个聚合管道。支持创建能够在单个阶段跨多个维度或方面描述数据的多面聚合。 |
$geoNear |
根据与地理空间点的接近程度返回有序的文档流。为地理空间数据合并了$match、$sort和$limit功能。输出文档包含一个额外的距离字段,并且可以包含一个位置标识符字段。 |
$graphLookup |
对集合执行递归搜索。在每个输出文档中添加一个新的数组字段,该字段包含对该文档的递归搜索的遍历结果。 |
$group |
按指定的标识符表达式对输入文档进行分组,并将累加器表达式(如果指定的话)应用于每个组。使用所有输入文档,并为每个不同的组输出一个文档。输出文档只包含标识符字段,如果指定,还包含累计字段。 |
$indexStats |
返回关于集合中每个索引的使用情况的统计信息。 |
$limit |
将未修改的前n个文档传递到指定限制为n的管道。对于每个输入文档,输出一个文档(前n个文档)或零文档(前n个文档之后)。 |
$listSessions |
列出已激活足够长的时间以传播到系统的所有会话。会话集合。 |
$lookup |
对同一数据库中的另一个集合执行左外连接,以便从“已连接”集合中筛选文档进行处理。 |
$match |
筛选文档流,只允许将匹配的文档未经修改地传递到下一个管道阶段。$match使用标准的MongoDB查询。对于每个输入文档,输出一个文档(匹配)或零文档(不匹配)。 |
$merge |
将聚合管道的结果文档写入集合。该阶段可以将结果合并到输出集合中(插入新文档、合并文档、替换文档、保留现有文档、操作失败、使用自定义更新管道处理文档)。要使用$merge阶段,它必须是管道中的最后一个阶段。 |
$out |
将聚合管道的结果文档写入集合。要使用$out阶段,它必须是管道中的最后一个阶段。 |
$planCacheStats |
返回集合的计划缓存信息。 |
$project |
对流中的每个文档进行整形,例如添加新字段或删除现有字段。对于每个输入文档,输出一个文档。 请参阅$unset以删除现有字段。 |
$redact |
根据存储在文档本身的信息限制每个文档的内容,从而重新构造流中的每个文档。合并了$project和$match的功能。可用于实现字段级编校。对于每个输入文档,输出一个或零一个文档。 |
$replaceRoot |
用指定的嵌入文档替换文档。该操作替换输入文档中的所有现有字段,包括_id字段。指定嵌入在输入文档中的文档,将嵌入的文档提升到顶层。 |
$replaceWith |
用指定的嵌入文档替换文档。该操作替换输入文档中的所有现有字段,包括_id字段。指定嵌入在输入文档中的文档,将嵌入的文档提升到顶层。 $replaceWith是$replaceRoot阶段的别名。 |
$sample |
从其输入中随机选择指定数量的文档。 |
$set |
向文档添加新字段。与$project类似,$set对流中的每个文档进行整形;具体来说,通过向输出文档添加新字段,这些输出文档包含来自输入文档的现有字段和新添加的字段。 $set是$addFields阶段的别名。 |
$skip |
跳过前n个文档,其中n是指定的跳过号,并将未修改的其余文档传递给管道。对于每个输入文档,输出零个文档(对于前n个文档)或一个文档(如果在前n个文档之后)。 |
$sort |
通过指定的排序键对文档流重新排序。只有顺序改变了;这些文件没有修改。对于每个输入文档,输出一个文档。 |
$sortByCount |
根据指定表达式的值对传入文档进行分组,然后计算每个不同组中的文档数。 |
$unset |
从文档中删除/排除字段。 $unset是$project stage的别名,用于删除字段。 |
$unwind |
从输入文档解构一个数组字段,为每个元素输出一个文档。每个输出文档用一个元素值替换数组。对于每个输入文档,输出n个文档,其中n是数组元素的数量,对于空数组可以是0。 |
1.2 Stages (db.aggregate
)
从3.6版开始,MongoDB也提供了db.aggregate
方法:
db.aggregate( [ { }, ... ] )
以下阶段使用的是db.aggregate()方法,而不是db.collection.aggregate()方法。
Stage | Description |
---|---|
$currentOp |
返回关于MongoDB部署的活动和/或休眠操作的信息。 |
$listLocalSessions |
列出当前连接的mongos或mongod实例上最近使用的所有活动会话。这些会话可能还没有传播到系统。会话集合。 |
1.3 可用于更新的阶段
从MongoDB 4.2开始,你可以使用聚合管道进行更新:
findAndModify
db.collection.findOneAndUpdate()
db.collection.findAndModify()
update
db.collection.updateOne()
db.collection.updateMany()
db.collection.update()
Bulk.find.update()
Bulk.find.updateOne()
Bulk.find.upsert()
对于更新,管道可以包括以下几个阶段:
2. 表达式
表达式可以包括字段路径、文字、系统变量、表达式对象和表达式操作符。表达式可以嵌套。
2.1 Field Paths
聚合表达式使用字段路径访问输入文档中的字段。要指定字段路径,请在字段名或点字段名(如果字段在嵌入的文档中)前面加上美元符号$。例如,“$user”指定用户字段的字段路径,或者“$user.name”指定“user.name”字段的字段路径。
“$<字段>”等于“$$CURRENT”。
2.2 聚合变量
MongoDB为表达式提供了各种聚合系统变量。要访问变量,在变量名前面加上$$。例如:
Variable | Access via $$ |
Brief Description |
---|---|---|
NOW |
$$NOW |
返回当前日期时间值,该值在部署的所有成员中都是相同的,在整个聚合管道中保持不变。(4.2 +) |
CLUSTER_TIME |
$$CLUSTER_TIME |
返回当前时间戳值,该值在部署的所有成员之间是相同的,在整个聚合管道中保持不变。仅用于复制集和分片集群。(4.2 +) |
ROOT |
$$ROOT |
引用根文档,即顶级文档。 |
CURRENT |
$$CURRENT |
引用字段路径的开始,默认情况下是根路径,但可以更改。 |
REMOVE |
$$REMOVE |
允许字段的条件排除。(3.6 +) |
DESCEND |
$$DESCEND |
一个$redact表达式允许的结果之一。 |
PRUNE |
$$PRUNE |
一个$redact表达式允许的结果之一。 |
KEEP |
$$KEEP |
一个$redact表达式允许的结果之一。 |
2.3 Literals
文字可以是任何类型。但是,MongoDB解析以美元符号$作为字段路径开头的字符串,解析表达式对象中的数字/布尔值作为投影标志。要避免解析文字,可以使用$literal表达式。
2.4 表达式对象
表达式对象有以下形式:
{ : , ... }
如果表达式是数值型或布尔型文字,MongoDB将文字作为投影标志(例如1或true来包含字段),仅在$project阶段有效。为了避免将数字或布尔文字作为投影标志,可以使用$literal表达式来包装数字或布尔文字。
3. 操作符表达式
$group
)操作符表达式类似于接受参数的函数。通常,这些表达式采用参数数组,形式如下:
{ : [ , ... ] }
如果操作符接受单个参数,你可以忽略指定参数列表的外部数组:
{ : }
如果参数是文字数组,为了避免解析歧义,您必须将文字数组包装在$literal表达式中,或者保留指定参数列表的外部数组。
3.1 算术表达式运算符
算术表达式对数字进行数学运算。一些算术表达式也可以支持日期运算。
Name | Description |
---|---|
$abs |
返回一个数字的绝对值。 |
$add |
添加数字以返回总和,或添加数字和日期以返回新日期。如果添加数字和日期,则将这些数字视为毫秒。接受任意数量的参数表达式,但最多只能解析一个表达式到一个日期。 |
$ceil |
返回大于或等于指定数字的最小整数。 |
$divide |
返回第一个数字除以第二个数字的结果。接受两个参数表达式。 |
$exp |
e的指定指数次方。 |
$floor |
返回小于或等于指定数字的最大整数。 |
$ln |
计算一个数的自然对数。 |
$log |
计算指定基数中数字的日志。 |
$log10 |
计算以10为底的对数。 |
$mod |
返回第一个数字除以第二个数字的余数。接受两个参数表达式。 |
$multiply |
将数字相乘以返回产品。接受任意数量的参数表达式。 |
$pow |
将数字提升到指定的指数。 |
$round |
将数字舍入为整数或指定的小数。 |
$sqrt |
计算平方根。 |
$subtract |
返回从第一个值减去第二个值的结果。如果两个值是数字,则返回差值。如果两个值是日期,则以毫秒为单位返回差值。如果这两个值是一个日期和一个以毫秒为单位的数字,则返回结果日期。接受两个参数表达式。如果这两个值是日期和数字,那么首先指定date参数,因为从数字中减去日期没有意义。 |
$trunc |
将数字截断为整数或指定的小数位数。 |
3.2 数组表达式运算符
$arrayElemAt |
返回指定数组索引处的元素。 |
---|---|
$arrayToObject |
将键值对数组转换为文档。 |
$concatArrays |
连接数组以返回连接后的数组。 |
$filter |
选择数组的一个子集来返回一个数组,该数组只包含与筛选条件匹配的元素。 |
$in |
返回一个布尔值,指示指定的值是否在数组中。 |
$indexOfArray |
在数组中搜索指定值的出现,并返回第一次出现的数组索引。如果没有找到子字符串,则返回-1。 |
$isArray |
确定操作数是否为数组。返回一个布尔值。 |
$map |
对数组的每个元素应用子表达式,并按顺序返回结果值的数组。接受命名参数。 |
$objectToArray |
将文档转换为表示键-值对的文档数组。 |
$range |
根据用户定义的输入输出一个包含整数序列的数组。 |
$reduce |
将表达式应用于数组中的每个元素并将它们组合成单个值。 |
$reverseArray |
以相反的顺序返回元素的数组。 |
$size |
返回数组中元素的数目。接受单个表达式作为参数。 |
$slice |
返回数组的一个子集。 |
$zip |
合并两个数组。 |
3.3 布尔表达式运算符
布尔表达式将其参数表达式计算为布尔值,并返回一个布尔值作为结果。
除了假布尔值之外,布尔表达式的以下计算结果为假:null、0和未定义的值。布尔表达式计算所有其他值为真,包括非零数值和数组。
Name | Description |
---|---|
$and |
仅当其所有表达式求值为true时才返回true。接受任意数量的参数表达式。 |
$not |
返回与参数表达式相反的布尔值。接受单个参数表达式。 |
$or |
当其中一个表达式的计算结果为true时,返回true。接受任意数量的参数表达式。 |
3.4 比较表达式操作符
除了$cmp返回一个数字之外,比较表达式返回一个布尔值。
比较表达式采用两个参数表达式,使用指定的BSON比较顺序比较不同类型的值和类型。
$cmp |
如果两个值相等,则返回0;如果第一个值大于第二个值,则返回1;如果第一个值小于第二个值,则返回-1。 |
---|---|
$eq |
如果值相等,则返回true。 |
$gt |
如果第一个值大于第二个值,则返回true。 |
$gte |
如果第一个值大于或等于第二个值,则返回true。 |
$lt |
如果第一个值小于第二个值,则返回true。 |
$lte |
如果第一个值小于或等于第二个值,则返回true。 |
$ne |
如果值不相等,则返回true。 |
3.5 条件表达式运算符
Name | Description |
---|---|
$cond |
计算一个表达式的三元运算符,根据结果返回另外两个表达式之一的值。接受有序列表中的三个表达式或三个命名参数 |
$ifNull |
返回第一个表达式的非空结果,如果第一个表达式导致一个空结果,则返回第二个表达式的结果。空结果包含未定义值或丢失字段的实例。接受两个表达式作为参数。第二个表达式的结果可以是null。 |
$switch |
计算一系列大小写表达式。当它找到一个计算结果为true的表达式时,$switch将执行指定的表达式并跳出控制流。 |
3.6 日期表达运营商
以下操作符返回日期对象或日期对象的组件:
Name | Description |
---|---|
$dateFromParts |
给定日期的组成部分构造BSON日期对象。 |
$dateFromString |
将日期/时间字符串转换为日期对象。 |
$dateToParts |
返回包含日期组成部分的文档。 |
$dateToString |
以格式化字符串的形式返回日期。 |
$dayOfMonth |
将日期的月日作为1到31之间的数字返回。 |
$dayOfWeek |
返回日期在1(星期日)和7(星期六)之间的数字。 |
$dayOfYear |
返回日期在1和366之间的日期(闰年) |
$hour |
将日期的小时作为0到23之间的数字返回。 |
$isoDayOfWeek |
返回ISO 8601格式的工作日编号,范围从1(周一)到7(周日)。 |
$isoWeek |
返回ISO 8601格式的周数,范围从1到53。周数从1开始,包含一年的第一个周四的那一周(周一到周日)。 |
$isoWeekYear |
以ISO 8601格式返回年份号。这一年从第一周的星期一开始(ISO 8601),到最后一周的星期日结束(ISO 8601)。 |
$millisecond |
以0到999之间的数字形式返回日期的毫秒数。 |
$minute |
将日期的分钟作为0到59之间的数字返回。 |
$month |
返回日期的月份作为1(一月)到12(十二月)之间的数字。 |
$second |
以0到60之间的数字(闰秒)返回日期的秒数。 |
$toDate |
将值转换为日期。 新版本4.0。 |
$week |
返回日期的周数,作为0(一年的第一个星期日之前的部分周)和53(闰年)之间的数字。 |
$year |
以数字形式返回日期的年份(如2014年)。 |
以下算术运算符可以接受日期操作数:
Name | Description |
---|---|
$add |
添加数字和日期以返回新日期。如果添加数字和日期,则将这些数字视为毫秒。接受任意数量的参数表达式,但最多只能解析一个表达式到一个日期。 |
$subtract |
返回从第一个值减去第二个值的结果。如果两个值是日期,则以毫秒为单位返回差值。如果这两个值是一个日期和一个以毫秒为单位的数字,则返回结果日期。接受两个参数表达式。如果这两个值是日期和数字,那么首先指定date参数,因为从数字中减去日期没有意义。 |
3.7 文字表达式运算符
Name | Description |
---|---|
$literal |
返回一个不需要解析的值。用于聚合管道可能解释为表达式的值。例如,对以$开头的字符串使用$literal表达式,以避免将其解析为字段路径。 |
3.8 对象表达式操作
Name | Description |
---|---|
$mergeObjects |
将多个文档组合成单个文档。 新版本3.6。 |
$objectToArray |
将文档转换为表示键-值对的文档数组。 新版本3.6。 |
3.9 Set表达式操作
Set表达式对数组执行Set操作,将数组视为集合。Set表达式忽略每个输入数组中的重复项和元素的顺序。
如果set操作返回一个set,则该操作过滤掉结果中的重复项,以输出只包含唯一条目的数组。输出数组中元素的顺序是未指定的。
如果集合包含嵌套的数组元素,则集合表达式不会下降到嵌套的数组中,而是在顶层计算数组。
Name | Description |
---|---|
$allElementsTrue |
如果集合中没有元素的计算结果为false,则返回true,否则返回false。接受单个参数表达式。 |
$anyElementTrue |
如果集合中的任何元素的值为true,则返回true;否则,返回false。接受单个参数表达式。 |
$setDifference |
返回一个集合,其中的元素出现在第一个集合中,但不在第二个集合中;即执行第二个集合相对于第一个集合的相对补码。只接受两个参数表达式。 |
$setEquals |
如果输入集具有相同的不同元素,则返回true。接受两个或多个参数表达式。 |
$setIntersection |
返回一个集合,其中的元素出现在所有输入集中。接受任意数量的参数表达式。 |
$setIsSubset |
如果第一个集合的所有元素都出现在第二个集合中,包括当第一个集合等于第二个集合时,返回true;即不是一个严格的子集。只接受两个参数表达式。 |
$setUnion |
返回包含任何输入集中出现的元素的集合。 |
3.10 字符串表达式运算符
除了$concat之外,字符串表达式只有定义良好的ASCII字符字符串行为。
无论使用什么字符,都可以定义$concat行为。
Name | Description |
---|---|
$concat |
连接任意数量的字符串。 |
$dateFromString |
将日期/时间字符串转换为日期对象。 |
$dateToString |
以格式化字符串的形式返回日期。 |
$indexOfBytes |
在字符串中搜索子字符串的出现,并返回第一次出现的UTF-8字节索引。如果没有找到子字符串,则返回-1。 |
$indexOfCP |
在字符串中搜索子字符串的出现,并返回第一次出现的UTF-8代码点索引。如果没有找到子字符串,则返回-1 |
$ltrim |
删除字符串开头的空白或指定字符。 新版本4.0。 |
$regexFind |
将正则表达式(正则表达式)应用于字符串并返回第一个匹配的子字符串的信息。 新版本4.2。 |
$regexFindAll |
将正则表达式(正则表达式)应用于字符串并返回所有匹配的子字符串的信息。 新版本4.2。 |
$regexMatch |
将正则表达式(regex)应用于字符串并返回一个布尔值,该布尔值指示是否找到匹配项。 新版本4.2。 |
$rtrim |
从字符串末尾删除空白或指定的字符。 新版本4.0。 |
$split |
根据分隔符将字符串分成子字符串。返回子字符串数组。如果在字符串中没有找到分隔符,则返回包含原始字符串的数组。 |
$strLenBytes |
返回字符串中UTF-8编码的字节数。 |
$strLenCP |
返回字符串中UTF-8代码点的数量。 |
$strcasecmp |
执行不区分大小写的字符串比较并返回:如果两个字符串相等,则返回0;如果第一个字符串大于第二个字符串,则返回1;如果第一个字符串小于第二个字符串,则返回-1。 |
$substr |
弃用。使用$substrBytes或$substrCP。 |
$substrBytes |
返回字符串的子字符串。从字符串中指定的UTF-8字节索引(从零开始)处的字符开始,然后继续指定的字节数。 |
$substrCP |
返回字符串的子字符串。从字符串中指定的UTF-8代码点(CP)索引(从零开始)处的字符开始,然后继续指定的代码点数量。 |
$toLower |
将字符串转换为小写形式。接受单个参数表达式。 |
$toString |
将值转换为字符串。新版本4.0。 |
$trim |
删除字符串开头和结尾的空白或指定字符。 新版本4.0。 |
$toUpper |
将字符串转换为大写。接受单个参数表达式。 |
3.11 文本表达式运算符
Name | Description |
---|---|
$meta |
访问文本搜索元数据。 |
3.12 三角函数表达式运算符
三角表达式对数字进行三角运算。表示角度的值总是以弧度表示输入或输出。使用$degreesToRadians和$radiansToDegrees在度和弧度度量之间进行转换。
Name | Description |
---|---|
$sin |
返回以弧度度量的值的正弦值。 |
$cos |
返回以弧度度量的值的余弦值。 |
$tan |
返回以弧度表示的值的正切值。 |
$asin |
返回以弧度为单位的值的反正弦值(arcsin)。 |
$acos |
返回以弧度为单位的值的反余弦(arccos)。 |
$atan |
返回以弧度为单位的值的反正切(arctan)。 |
$atan2 |
返回以弧度表示的y / x的反正切(arctan),其中y和x分别是传递给表达式的第一个和第二个值。 |
$asinh |
返回以弧度为单位的值的反双曲正弦(双曲反正弦)。 |
$acosh |
返回以弧度为单位的值的反双曲余弦(双曲反余弦)。 |
$atanh |
返回以弧度为单位的值的反双曲正切(双曲反正切)。 |
$degreesToRadians |
将值从度转换为弧度。 |
$radiansToDegrees |
将值从弧度转换为角度。 |
3.12 类型表达式运算符
Name | Description |
---|---|
$convert |
将值转换为指定的类型。 新版本4.0。 |
$toBool |
将值转换为布尔值。 新版本4.0。 |
$toDate |
将值转换为日期。 新版本4.0。 |
$toDecimal |
将值转换为小数128。新版本4.0。 |
$toDouble |
将值转换为双精度值。 新版本4.0。 |
$toInt |
将值转换为整数。 新版本4.0。 |
$toLong |
将值转换为long。 新版本4.0。 |
$toObjectId |
将值转换为ObjectId。新版本4.0。 |
$toString |
将值转换为字符串。 新版本4.0。 |
$type |
返回字段的BSON数据类型。 |
3.13 分组($group)
在$group阶段可用,累加器是操作符,当文档在管道中进展时,它们维护自己的状态(例如,总计、最大值、最小值和相关数据)。
在$group阶段中用作累加器时,这些操作符将单个表达式作为输入,对每个输入文档求值一次,并为共享相同组键的文档组维护它们的阶段。
Name | Description |
---|---|
$addToSet |
返回每个组的唯一表达式值数组。数组元素的顺序未定义。 |
$avg |
返回数值的平均值。忽略了非数字值。 |
$first |
为每个组从第一个文档返回一个值。只有当文档处于已定义的顺序时,才定义Order。 |
$last |
为每个组从上一个文档返回一个值。只有当文档处于已定义的顺序时,才定义Order。 |
$max |
返回每个组的最高表达式值。 |
$mergeObjects |
返回通过组合每个组的输入文档创建的文档。 |
$min |
返回每个组的最低表达式值。 |
$push |
返回每个组的表达式值数组。 |
$stdDevPop |
返回输入值的总体标准差。 |
$stdDevSamp |
返回输入值的样本标准差。 |
$sum |
返回数值的和。忽略了非数字值。 |
3.14 分组(其它)
在$group阶段可用作累加器的一些操作符也可用作其他阶段的累加器,但不能用作累加器。当在这些其他阶段使用时,这些操作符不维护它们的状态,可以接受单个参数或多个参数作为输入。有关详细信息,请参阅特定的操作员页面。
在3.2版本中进行了更改。
以下累加器操作符在$project、$addFields和$set stage中也可用。
Name | Description |
---|---|
$avg |
返回每个文档的指定表达式或表达式列表的平均值。忽略了非数字值。 |
$max |
返回每个文档的指定表达式或表达式列表的最大值 |
$min |
返回每个文档的指定表达式或表达式列表的最小值 |
$stdDevPop |
返回输入值的总体标准差。 |
$stdDevSamp |
返回输入值的样本标准差。 |
$sum |
返回数值的和。忽略了非数字值。 |
3.15 变量表达式运算符
Name | Description |
---|---|
$let |
定义在子表达式范围内使用的变量,并返回子表达式的结果。接受命名参数。 接受任意数量的参数表达式。 |
3.16 表达式运算符索引
|
|
|
|
4. 聚合的命令
请注意:
有关特定运算符的详细信息,包括语法和示例,请单击特定运算符进入其参考页面。
4.1 聚合的命令
Name | Description |
---|---|
aggregate |
使用聚合框架执行聚合任务,如分组。 |
count |
计算集合或视图中的文档数量。 |
distinct |
显示在集合或视图中为指定键找到的不同值。 |
mapReduce |
对大数据集执行map-reduce聚合。 |
4.2 聚合的方法
Name | Description |
---|---|
db.collection.aggregate() |
提供对聚合管道的访问。 |
db.collection.mapReduce() |
对大数据集执行map-reduce聚合。 |
5. 聚合命令比较
下表简要概述了MongoDB聚合命令的特性。
aggregate / db.collection.aggregate() |
mapReduce / db.collection.mapReduce() |
Description | 为提高聚合任务的性能和可用性而设计。 使用“管道”方法,对象在通过一系列管道操作符(如$group、$match和$sort)时进行转换。 有关管道操作符的更多信息,请参见聚合管道操作符。 |
实现用于处理大型数据集的Map-Reduce聚合。 |
Key Features | 可以根据需要重复管道操作符。 |
除了分组操作之外,还可以执行复杂的聚合任务,以及对不断增长的数据集执行增量聚合。 |
Flexibility | 仅限于聚合管道支持的操作符和表达式。 |
自定义映射、reduce和finalize JavaScript函数为聚合逻辑提供了灵活性。 |
Output Results | 以游标的形式返回结果。如果管道包含$out阶段或$merge阶段,则游标为空。
|
返回各种选项的结果(内联、新集合、合并、替换、减少)。有关输出选项的详细信息,请参见mapReduce。 |
Sharding | 支持非切分输入集合和切分输入集合。 | 支持非切分输入集合和切分输入集合。 |
More Information |
|
|
6. 聚合表达式中的变量
聚合表达式既可以使用用户定义的变量,也可以使用系统变量。
变量可以保存任何BSON类型的数据。要访问变量的值,在变量名前面加上双美元符号($$);即。“$ $ <变量>”。
如果变量引用对象,要访问对象中的特定字段,请使用点符号;即。变量“$ $ < >。<字段>”。
6.1 用户自定义变量
用户变量名可以包含ascii字符[_a-zA-Z0-9]和任何非ascii字符。
用户变量名必须以小写的ascii字母[a-z]或非ascii字符开头。
6.2 系统变量
MongoDB提供了以下系统变量:
Variable | Description |
---|---|
|
返回当前日期时间值的变量。现在,为部署的所有成员返回相同的值,并在聚合管道的所有阶段保持相同的值。新版本4.2。 |
|
返回当前时间戳值的变量。 CLUSTER_TIME只在复制集和分片集群上可用。 CLUSTER_TIME为部署的所有成员返回相同的值,并在管道的所有阶段保持相同的值。 新版本4.2。 |
|
引用当前正在聚合管道阶段处理的根文档,即顶级文档。 |
|
引用聚合管道阶段中正在处理的字段路径的开始。除非另有说明,否则所有阶段都以与ROOT相同的CURRENT开始。 目前是可修改的。但是,由于$<字段>等于$$CURRENT. |
|
求值为缺失值的变量。允许字段的条件排除。在$投射中,变量REMOVE的字段被排除在输出之外。 有关其用法的示例,请参见有条件排除字段。 新版本3.6。 |
|
一个$redact表达式允许的结果之一。 |
|
一个$redact表达式允许的结果之一。 |
|
一个$redact表达式允许的结果之一。 |
7. SQL到聚合映射图
聚合管道允许MongoDB提供与SQL中的许多常见数据聚合操作相对应的本地聚合功能。
下表概述了常见的SQL聚合术语、函数和概念,以及相应的MongoDB聚合操作符:
SQL术语、函数和概念 | MongoDB聚合运算符 |
WHERE | $match |
GROUP BY | $group |
HAVING | $match |
SELECT | $project |
ORDER BY | $sort |
LIMIT | $limit |
SUM() | $sum |
COUNT() | $sum $sortByCount |
join | $lookup |
SELECT INTO NEW_TABLE | $out |
MERGE INTO TABLE | $merge(可在MongoDB 4.2中启动) |
7.1 举例
下表给出了SQL聚合语句和相应的MongoDB语句的快速引用。表中的例子假设了以下条件:
{
cust_id: "abc123",
ord_date: ISODate("2012-11-02T17:04:11.102Z"),
status: 'A',
price: 50,
items: [ { sku: "xxx", qty: 25, price: 1 },
{ sku: "yyy", qty: 25, price: 1 } ]
}
SQL Example | MongoDB Example | Description |
---|---|---|
SELECT COUNT(*) AS count FROM orders |
db.orders.aggregate( [ { $group: { _id: null, count: { $sum: 1 } } } ] ) |
统计订单中的所有记录 |
SELECT SUM(price) AS total FROM orders |
db.orders.aggregate( [ { $group: { _id: null, total: { $sum: "$price" } } } ] ) |
对订单中的价格字段求和 |
SELECT cust_id, SUM(price) AS total FROM orders GROUP BY cust_id |
db.orders.aggregate( [ { $group: { _id: "$cust_id", total: { $sum: "$price" } } } ] ) |
对于每个惟一的cust_id,对price字段求和。 |
SELECT cust_id, SUM(price) AS total FROM orders GROUP BY cust_id ORDER BY total |
db.orders.aggregate( [ { $group: { _id: "$cust_id", total: { $sum: "$price" } } }, { $sort: { total: 1 } } ] ) |
对于每个惟一的cust_id,对price字段求和,结果按sum排序。 |
SELECT cust_id, ord_date, SUM(price) AS total FROM orders GROUP BY cust_id, ord_date |
db.orders.aggregate( [ { $group: { _id: { cust_id: "$cust_id", ord_date: { $dateToString: { format: "%Y-%m-%d", date: "$ord_date" }} }, total: { $sum: "$price" } } } ] ) |
对于每个惟一的cust_id、ord_date分组,对price字段求和。不包括日期的时间部分。 |
SELECT cust_id, count(*) FROM orders GROUP BY cust_id HAVING count(*) > 1 |
db.orders.aggregate( [ { $group: { _id: "$cust_id", count: { $sum: 1 } } }, { $match: { count: { $gt: 1 } } } ] ) |
对于包含多条记录的cust_id,返回cust_id和相应的记录计数。 |
SELECT cust_id, ord_date, SUM(price) AS total FROM orders GROUP BY cust_id, ord_date HAVING total > 250 |
db.orders.aggregate( [ { $group: { _id: { cust_id: "$cust_id", ord_date: { $dateToString: { format: "%Y-%m-%d", date: "$ord_date" }} }, total: { $sum: "$price" } } }, { $match: { total: { $gt: 250 } } } ] ) |
对于每个惟一的cust_id、ord_date分组,对price字段求和,只有当总和大于250时才返回。不包括日期的时间部分。 |
SELECT cust_id, SUM(price) as total FROM orders WHERE status = 'A' GROUP BY cust_id |
db.orders.aggregate( [ { $match: { status: 'A' } }, { $group: { _id: "$cust_id", total: { $sum: "$price" } } } ] ) |
对于每个惟一的cust_idwith状态A,对price字段求和。 |
SELECT cust_id, SUM(price) as total FROM orders WHERE status = 'A' GROUP BY cust_id HAVING total > 250 |
db.orders.aggregate( [ { $match: { status: 'A' } }, { $group: { _id: "$cust_id", total: { $sum: "$price" } } }, { $match: { total: { $gt: 250 } } } ] ) |
对于每个惟一的cust_idwith状态A,对price字段求和,只在总和大于250时返回。 |
SELECT cust_id, SUM(li.qty) as qty FROM orders o, order_lineitem li WHERE li.order_id = o.id GROUP BY cust_id |
db.orders.aggregate( [ { $unwind: "$items" }, { $group: { _id: "$cust_id", qty: { $sum: "$items.qty" } } } ] ) |
对于每个惟一的cust_id,将与订单相关的行项目qty字段相加。 |
SELECT COUNT(*) FROM (SELECT cust_id, ord_date FROM orders GROUP BY cust_id, ord_date) as DerivedTable |
db.orders.aggregate( [ { $group: { _id: { cust_id: "$cust_id", ord_date: { $dateToString: { format: "%Y-%m-%d", date: "$ord_date" }} } } }, { $group: { _id: null, count: { $sum: 1 } } } ] ) |
计算不同的cust_id、ord_date分组的数量。不包括日期的时间部分。 |