mongodb之(4)mongodb聚合管道

1、聚合查询

聚合查询主要使用聚合框架对集合中的文档进行变换与组合,以实现对文档的一连串处理。这些处理包含筛选(Filtering)、投射(projecting)、分组(grouping)、排序(sorting)、限制(limiting)和跳过(skipping)。
聚合操作通过管道操作符进行处理,每个操作符都会接受一连串的文档,对这些文档做一些类型转换,最后将转换后的文档作为结果传递给下一个操作符。不同的管道操作符可以按任意顺序组合在一起,而且可以被重复任意多次。

1.1、$match

$match用于对文档集合进行筛选,之后就可以在筛选的带的文档子集上做聚合。$match可以使用常规的查询操作符,如$gt/$lt/$in等,但不能在$match中使用地理位置空间操作符。
通常在使用中应尽可能将$match放在管道的前面位置。这样好处为:一是可以快速将不需要的文档过滤掉,以减少管道的工作量;二是如果在投射和分组之前执行$match,查询可以使用索引。

使用示例:

> db.city.aggregate({"$match":{"code":{$lt:2}}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : [ "朝阳1", "通州", "海淀", "大兴" ], "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
>

1.2、$project

$project可以从文档中提取字段,可以重命名字段,还可以在这些字段上进行一些操作。$project最简单的操作是选择想要的字段,可以指定包含或不包含某个字段。同时,也可将投射过的字段进行重命名。$fieldName在聚合框架中的含义是引用某个字段的值。

1.2.1、管道表达式

最简单的$project表达式是包含和排除字段,以及字段名称。也可以使用表达式将多个字面量和变量组合在一个值中使用。文档的id默认是包含的。
使用示例:

> db.city.aggregate({"$project":{"name":1,"code":1}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "code" : 1 }
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "name" : "广州", "code" : 3 }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "name" : "上海", "code" : 2 }
>

1.2.2、数学表达式

数学表达式可用于操作数值。指定一组数值,就可以使用数学表达式进行操作了。
操作语法如下:

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

使用示例:

> db.city.aggregate({"$project":{"code+2":{"$add":["$code",2]},"code*2*2*2":{"$multiply":["$code",2,2,2]},"code%3":{"$mod":["$code",3]}}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "code+2" : 3, "code*2*2*2" : 8, "code%3" : 1 }
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "code+2" : 5, "code*2*2*2" : 24, "code%3" : 0 }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "code+2" : 4, "code*2*2*2" : 16, "code%3" : 2 }
>

1.2.3、日期表达式

聚合框架中包含了一些用于提取日期信息的表达式:"$year"、"$month"、"$dayOfMonth"、"$dayOfWeek"、"$dayOfYear"、"$hour"、"$minute"、"$second"。只能对日期类型的字段进行日期操作,不能对数值类型的字段做日期操作。

使用示例:

> db.city.aggregate({"$project":{"crete_year":{"$year":"$create_date"}, "create_week":{"$dayOfWeek":"$create_date"}}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "crete_year" : null, "create_week" : null }
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "crete_year" : null, "create_week" : null }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "crete_year" : 2019, "create_week" : 5 }
>

1.2.4、字符串表达式

字符串操作如下:

  • "$substr":[ expr, startOffset, numToReturn ]。这个操作会截取字符串的字串,startOffset为字节偏移量,- - - numToReturn为字节数,expr为输入字符串。
  • "$concat":[ expr1, [ , expr2, … , exprN ] ]。将给定的字符串连接在一起返回结果;
  • "$toLower":expr。返回expr字符串的小写形式;
  • "$toUpper":expr。返回expr字符串的大写形式;

使用示例:

> db.city.aggregate({"$project":{"country_city":{"$concat":["$country","-","$name"]}}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "country_city" : "中国-北京" }
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "country_city" : "中国-广州" }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "country_city" : "中国-上海" }
>

1.2.5、逻辑表达式

比较表达式:

  • "$cmp":[ expr1, expr2 ]。比较expr1和expr2。若相等,返回0;若expr1expr2则返回正数。
  • "$strcasecmp":[ string1, string2]。比较string1和string2,区分大小写。只对罗马字符组成的字符串有效。
  • "$eq"、"$ne"、"$gt"、"$gte"、"$lt"、"$lte":[ expr1, expr2 ]。对expr1和expr2执行相应的比较操作,返回比较的结果(true或false)。

布尔表达式:

  • "$and":[ expr1, [ , expr2, … , exprN ] ]。如果所有表达式的值都是true,那就返回true,否则返回false;
  • "$or":[ expr1, [ , expr2, … , exprN ] ]。只要有任意表达式为true,则返回true,否则返回false。
  • "$not":expr。对expr取反。

控制语句:

  • "$cond":[ booleanExpr, trueExpr, falseExpr]。如果booleanExpr的值时true,那就返回trueExpr,否则返回falseExpr。
  • "$ifNull":[ expr, replacementExpr]。如果expr是null,返回replacementExpr,否则返回expr。

使用示例:

> db.city.aggregate({"$project":{"hugeCity":{"$eq":["$name","北京"]}}})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "hugeCity" : true }
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "hugeCity" : false }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "hugeCity" : false }
>

1.3、$group

$group操作可以将文档依据特定字段的不同值进行分组。

1.3.1、分组操作符

分组操作符允许对每个分组进行计算,得到相应的结果。

1.3.2、算术操作符

$sum和$average两个操作符可以对数值类型的字段的值进行计算。
"$sum":value。对分组中的每个文档,将value与计算结果相加。
"$avg":value。返回每个分组的平均值。‘
使用示例:

> db.city.aggregate({"$group":{"_id":"$captial","avgCode":{"$avg":"$code"},"sumCode":{"$sum":"$code"}}})
{ "_id" : false, "avgCode" : 2.5, "sumCode" : 5 }
{ "_id" : true, "avgCode" : 1, "sumCode" : 1 }

1.3.3、极值操作符

  • "$max":expr。返回分组内的最大值。
  • "$min":expr。返回分组内的最小值。
  • "$first":expr。返回分组的第一个值,忽略后面所有值。只有排序之后,明确知道数据顺序是这个操作才有意义。
  • "$last":expr。与first相反,返回分组的最后一个值。

$max和$min会查看每个文档,以便得到极值。因此,如果数据时无序的,这两个操作符也可以有效工作;如果数据时有序的,这两个操作符就会有些浪费,可以直接用$first和$last获取最小和最大值。
如果数据时排序的,使用last会比较高效;否则使用$min和$max比先排序再使用$first和$last更高效。
使用示例:

> db.city.aggregate({"$group":{"_id":"captial","minCode":{"$min":"$code"},"lastCode":{"$last":"$code"}}})
{ "_id" : "captial", "minCode" : 1, "lastCode" : 2 }

1.3.4、数组操作符

  • "$addToSet":expr。如果当前数组中不包含expr,就将它添加到数组中。在返回的结果集中,每个元素最多只出现一次,而且元素的顺序是不确定的。
  • "$push":expr。不管expr是什么,都将它添加到数组中。返回包含所有值的数组。
    使用示例:
> db.city.aggregate({"$group":{"_id":"captial", "onlyDistrict":{"$addToSet":"$district"}}})
{ "_id" : "captial", "onlyDistrict" : [ [ "朝阳1", "通州", "海淀", "大兴" ] ] }
>

1.3.5、分组行为

有两个操作符不能用在流式工作方式对文档的处理,$group必须等收到所有文档之后,才能对文档进行分组,然后才能将各个分组发送给管道的下一个操作符。故在分片情况下,$group会先在每个分片上执行,然后各个分片上的分组结果会被发送到mongos在进行最后的统一分组,剩余的管道工作也是在mongos上进行、

1.4、$unwind

$unwind可以将数组中的每个值拆分为单独的文档。如果希望在查询中得到特定的子文档,这个操作符就会非常有用,先使用$unwind得到所有子文档,在用$match得到想要的文档。

使用示例:

> db.city.aggregate({"$unwind":"$district"})
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : "朝阳1", "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : "通州", "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : "海淀", "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : "大兴", "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
>

1.5、$sort

可以根据任何字段进行排序,与在普通查询中的语法相同。如果需要对大量的文档进行排序,强烈建议在管道的第一阶段进行排序,这时的排序操作可以使用索引。否则,排序过程会比较慢,而且占用大量内存。$sort也无法使用流式工作方式,必须要接收所有文档之后才能进行排序,在分片环境下,先在各个分片上进行排序,然后将各个分片的排序结果发生到mongos做进一步的处理。

使用示例:

> db.city.aggregate({"$sort":{"code":-1,"captial":1}})
{ "_id" : ObjectId("5de5ac646bba334fecac2f30"), "name" : "广州", "country" : "中国", "code" : 3, "captial" : false, "bigCity" : true }
{ "_id" : ObjectId("5de647f5b4cd9b14f46e0e30"), "name" : "上海", "country" : "中国", "code" : 2, "captial" : false, "bigCity" : true, "north" : null, "create_date" : ISODate("2019-12-05T14:52:07.860Z") }
{ "_id" : ObjectId("5de52e826bba334fecac2f28"), "name" : "北京", "country" : "中国", "code" : 1, "captial" : true, "people" : 20009000, "district" : [ "朝阳1", "通州", "海淀", "大兴" ], "north" : true, "subId" : [ 1, 10, 20, 50, 100 ] }
>

1.6、$limit

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

1.7、$skip

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

2、聚合管道的优化

聚合管道可以检测到是否仅使用文档中的一部分字段就可以完成聚合。如果是的话,管道就可以仅使用这些必要的字段,从而减少进入管道的数据量。

2.1、管道顺序优化

2.1.1、$sort + $match 顺序优化

如果你的管道中, $sort 后面跟着 $match ,把 $match 移到 $sort 前面可以减少需要排序的对象个数。例如,如果管道中有以下两个部分:

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

转换为如下形式可提高性能:

{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

2.1.2、$skip + $limit 顺序优化

如果你的管道中, $skip 后面跟着 $limit ,优化器会把 $limit 移到 $skip 前面,这个时候, $limit 的值会加上 $skip 的个数。
若有如下管道组成:

{ $skip: 10 },
{ $limit: 5 }

转换器在优化时会转换为如下形式:

{ $limit: 15 },
{ $skip: 10 }

2.1.3、$redact + $match 顺序优化

如果可能,当管道中 $redact 阶段后面紧接着有 $match 操作,聚合有时候会添加一个 $match 到 $redact 前面。如果在管道在一开始有 $match ,聚合操作在查询时可以使用索引,以减少进入到管道中的文档个数。更多详情请查看 管道操作符和索引 。
如果有如下形式的管道:

{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

优化器会做如下优化:

{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

2.1.3、$project + $skip or $limit 顺序优化

若聚合查询的$project后面跟随$skip或$limit,则$skip或$limit将会移到$project前面。
如有如下管道示例:

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $limit: 5 }

优化器将会进行如下优化:

{ $sort: { age : -1 } },
{ $limit: 5 }
{ $project: { status: 1, name: 1 } },

2.2、管道合并优化

优化器可以在管道开始之前合并其他的管道。一般情况下,合并发生在所有顺序优化之后。

2.2.1、$sort +$limit 合并

如果 $sort 在 $limit 前面,优化器可以把 $limit 合并在 $sort 内部。此时如果指定了限定返回 n 个结果,那么排序操作仅需要维护最前面的 n 个结果,MongoDB只需要在内存中存储 n 个元素。

2.2.2、$limit + $limit 合并

当 $limit 操作后面还有一个 $limit 操作,这两步可以合并成一个单独的 $limit 操作,此时限制的个数是前面两个限制个数中较小的值。例如,一个管道操作包含有如下操作序列:

{ $limit: 100 },
{ $limit: 10 }

此时,第二个 $limit 操作可以合并到第一个 $limit 操作中,最后生成一个 $limit 操作并且限制个数为初始两个限制个数 100 和 10 中的较小的一个 10 。

{ $limit: 10 }

2.2.3、$skip + $skip 合并

当 $skip 操作后面还有一个 $skip 操作,这两步可以合并成一个单独的 $skip 操作,此时跳过的个数是前面两个跳过个数的和。例如,一个管道操作包含有如下操作序列:

{ $skip: 5 },
{ $skip: 2 }

此时,第二个 $skip 操作可以合并到第一个 $skip 操作中,最后生成一个 $skip 操作并且跳过个数为初始两个跳过个数 5 与 2 的相加值 7 。

{ $skip: 7 }

2.2.4、$match +\ $match 合并

当 $match 操作后面还有一个 $match 操作,可以将这两步中的条件使用 $and 表达式合并成一个单独的 $match 操作。例如,一个管道操作包含有如下操作序列:

{ $match: { year: 2014 } },
{ $match: { status: "A" } }

此时,第二个 $match 操作可以合并到第一个 $match 操作中,最后生成一个 $match 操作。

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }

2.2.5、$lookup + $unwind 合并

当$unwind后面紧跟着另一个$lookuo,并且$unwind操作的字段同$lookup一致,这优化器会将$unwind合并到$lookup中。这样可以减少创建大的临时文档。
如有如下管道示例:

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y"
  }
},
{ $unwind: "$resultingArray"}

优化器会将$unwind聚合到$lookup内。

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y",
    unwinding: { preserveNullAndEmptyArrays: false }
  }
}

3、聚合管道的限制

使用聚合管道会有一定的限制。

3.1、结果集大小的限制

从MongoDB的2.6版本以后,聚合命令可以返回一个游标或将结果集保存在一个集合中。当以此种方式返回聚合结果时,结果集中的每个文档大小受限于mongodb的BSON文档大小(当前未16MB)。当任何单个文档大小超过BSON大小的限制,则聚合命令将报错。管道处理过程中会对结果集进行大小的校验。2.6版本之后默认返回游标。
如果用户为声明使用游标或存储结构到集合中,聚合操作将返回单个BSON文档,文档中包含所有的结果集。这样一来,若聚合结果集大小超过BSON限制大小,则聚合操作将报错。早起版本只能返回单个BSON文档。

3.2、内存限制

管道操作内心限制大小为100MB,如果管道操作的某一处理超过了这个内存限制,则报错。为运行处理大数据集,可设置allowDiskUse选项使能聚合操作将数据写入临时文件。

4、聚合管道与分片集合

聚合管道支持在 分片 集合执行。

4.1、特点

如果管道起始于分片建上的精确$match操作,则整个管道运行在匹配的分片上。显然,管道会被拆分处理,并在主分片上进行结果合并。
应聚合操作必须在多个分片上运行处理,如果操作无需运行在主分片数据库中,则这些处理将会被路由到随机的分片服务器上进行结果的合并,以免主分片负载过重。$out和$lookup阶段需要在主分片服务器中处理。

4.2、优化

将聚合管道分为两个步骤,极大的方便了对集群的性能做多个方面的优化。使用 db.collection.aggregate() 的 explain 选项可以获得管道分裂的详细信息。

你可能感兴趣的:(mongodb之(4)mongodb聚合管道)