MongoDB 基础系列十七:聚合查询之 Aggregation Pipeline - Aggregation Expressions

前言

此篇博文是 Mongdb 基础系列之一;

本文将会介绍 Aggregation 过程中所使用到的 Expressions (表达式);

本文为作者的原创作品,转载需注明出处;

简介

Expressions can include field paths and system variables, literals, expression objects, and expression operators. Expressions can be nested.

可见 Aggregation Expressions 可以由上述四种方式来进行表示,下面笔者将就这四种方式分别进行描述;

field paths and system variables

field paths

比如 user 是某个 document 中的一个字段,那么在表达式中,可以使用 “\$user” 的方式来引用该字段,这种使用 \$ 作为前缀来引用某个字段的方式被称作 field path;

system variables

而实际上,“$<field>>” 的方式等价于 “\$\$CURRENT.<field>”,这里的 使用到的 \$\$CURRENT 就是一个系统变量( system variables ) CURRENT,更多有关系统变量的介绍参考 https://docs.mongodb.com/manual/reference/aggregation-variables/#variable.CURRENT

Literals

定义


\$literal,它的功能实际上是充当的是一个转移符的功能;可以将 \$ 转移为一个普通字符;由 field paths and system variables 中可以知道,在表达式中,我们通过使用 “$<field>>” 的方式来引用某一个字段的值,但是有时候,当使用 \$ 的时候,并不是想引用该字段的值,而是因为我们想使用一个包含 $ 字符的字符串来进行匹配而已;所以,这里可以看做是,通过 \$literals 达到对 \$ 进行转移的效果;具体参考例子将 $ 进行转义

调用表达式格式,

1
{ $literal: <value> }

回过头来,发现,自己之前对 literals 有关的上述的理解是不对的,\$literal 的本来意思并不是说去转义,而是直接将 { \$literal: <value> } 中的 value 不管它是否有任何的特殊表达方式都直接当做一个字符串看待,而不再将其进行任何其它的特殊操作;这一点可以从如下的特性直观的理解到,

如果上述表达式的 value 本身是其它的表达式,那么该表达式并不会进行任何的计算而是按照原样进行输出,

1
{ $literal: { $add: [ 2, 3 ] } }

表达式的输出结果是,

1
{ $add: [ 2, 3 ] }

又比如,

1
{ $literal:  { $literal: 1 } }

表达式的输出结果,

1
{ "$literal" : 1 }

例子

将 $ 进行转义

假设我们有如下的 records 信息,

1
2
3
4
5
db.records.insertMany([
{ "_id" : 1, "item" : "abc123", price: "$2.50" },
{ "_id" : 2, "item" : "xyz123", price: "1" },
{ "_id" : 3, "item" : "ijk123", price: "$1" }
])

注意,第三条数据本身包含一条 price 值为 \$1 的值;下面假设我们需要插叙一条 price = “\$1” 的记录,这里的 “\$1” 表示纯的字符串;

1
2
3
db.records.aggregate( [
{ $project: { costsOneDollar: { $eq: [ "$price", { $literal: "$1" } ] } } }
] )

注意表达式 \$eq: [ “\$price”, { \$literal: “\$1” } ] 这里使用 “\$literal: “\$1” 的方式对其进行转义,表示 \$eq 操作符使用的是字符串 “\$1” 与 \$price 字段值进行的比较;而不是一个为 1 的字段与 \$price 字段进行比较;通过 \$project 我们添加了一个新的字段 costsOneDollar 作为输出,相等则输出为 true 否则输出 false;于是我们得到如下的输出,

1
2
3
{ "_id" : 1, "costsOneDollar" : false }
{ "_id" : 2, "costsOneDollar" : false }
{ "_id" : 3, "costsOneDollar" : true }

假设,我们不使用 \$literal 而是直接使用 \$1,如下所述,

1
2
3
db.records.aggregate( [
{ $project: { costsOneDollar: { $eq: [ "$price", "$1" ] } } }
] )

我们将得到如下的输出,

1
2
3
{ "_id" : 1, "costsOneDollar" : false }
{ "_id" : 2, "costsOneDollar" : false }
{ "_id" : 3, "costsOneDollar" : false }

可以看到,它并不是把 “\$1” 当做字符来比较的,因为默认情况下,表达式中,将 \$ 作为前缀的字符串认为是对某个 field 的引用,因此,这里通过 $eq 操作符来进行的比较,会被认为是对 price 字段和一个 1 的字段进行比较;而 1 的字段并不存在,所以在聚合查询的时候,将其当做 null 处理,因此所有的比对结果都是 false;

新增一个值为 1 的输出字段

假设我们有如下的 bids 数据,

1
2
3
4
db.bids.insertMany([
{ "_id" : 1, "item" : "abc123", condition: "new" },
{ "_id" : 2, "item" : "xyz123", condition: "new" }
])

加入我们只需要输出文档中包含 _id,item 字段以及一个值永远为 1 的新增字段 startAt,

1
2
3
db.bids.aggregate( [
{ $project: { item: 1, startAt: { $literal: 1 } } }
] )

得到输出结果如下,

1
2
{ "_id" : 1, "item" : "abc123", "startAt" : 1 }
{ "_id" : 2, "item" : "xyz123", "startAt" : 1 }

否则,如果直接使用如下的方式的话,会直接解释为 \$project 的语法,表示输出源文档中的字段 startAt,而因为 startAt 字段并不存在,因此不会输出任何相关 startAt 的信息,

1
2
3
db.bids.aggregate( [
{ $project: { item: 1, startAt: 1 } }
] )

得到相关的输出结果如下,

1
2
{ "_id" : 1, "item" : "abc123" }
{ "_id" : 2, "item" : "xyz123" }

最后,这里的使用到的 \$literal 的目的并不是在于转义,而是将其作为 \$project 表达式中的第三种调用方式 <field>: <expression> 而已,这样 \$project 将使用表达式的计算值来作为字段的输出; 查看 project 的相关用例

Expression Objects

表达式格式,

1
{ <field1>: <expression1>, ... }

如果上述的 expression1 值是数字或者是 boolean 值 (比如 1 或者 true),那么该表达式只能用在 \$project stage 过程中,表示在输出文档中新增该 field1 字段;

那么我的疑问是,除了 \$project stage 以外,是否还有其它的地方可以使用 Expression Objects?

Operator Expressions

定义

Operator Expressions 非常类似于一个 function,可以同时接受多个参数;通常情况下,该表达式接受一个数组作为输入参数,

1
{ <operator>: [ <argument1>, <argument2> ... ] }

如果该 operator 只接受一个参数的情况下,可以将上述的表达式简写为,

1
{ <operator>: <argument> }

Boolean Expressions

显然由该操作符表达式计算出来的结果将会返回一个 boolean 值,Boolean Expressions 包含 \$and,\$or 以及 \$not

$and

Returns true only when all its expressions evaluate to true. Accepts any number of argument expressions.

1
{ $and: [ <expression1>, <expression2>, ... ] }

比如我们有如下的 inventory 的数据,

1
2
3
4
5
{ "_id" : 1, "item" : "abc1", description: "product 1", qty: 300 }
{ "_id" : 2, "item" : "abc2", description: "product 2", qty: 200 }
{ "_id" : 3, "item" : "xyz1", description: "product 3", qty: 250 }
{ "_id" : 4, "item" : "VWZ1", description: "product 4", qty: 300 }
{ "_id" : 5, "item" : "VWZ2", description: "product 5", qty: 180 }

我们想要知道哪些 qty 的值是大于 100 并且小于 250 的记录,

1
2
3
4
5
6
7
8
9
10
11
12
db.inventory.aggregate(
[
{
$project:
{
item: 1,
qty: 1,
result: { $and: [ { $gt: [ "$qty", 100 ] }, { $lt: [ "$qty", 250 ] } ] }
}
}
]
)

输出结果如下,

1
2
3
4
5
{ "_id" : 1, "item" : "abc1", "result" : false }
{ "_id" : 2, "item" : "abc2", "result" : true }
{ "_id" : 3, "item" : "xyz1", "result" : false }
{ "_id" : 4, "item" : "VWZ1", "result" : false }
{ "_id" : 5, "item" : "VWZ2", "result" : true }

$or

1
{ $or: [ <expression1>, <expression2>, ... ] }

$and 不同,这里只需要有任意的一个表达式为 true 便返回 true;

$not

1
{ $not: [ <expression> ] }

Set Expressions

Set expressions performs set operation on arrays, treating arrays as sets. Set expressions ignores the duplicate entries in each input array and the order of the elements.

Set 表达式在 arrays 上执行相关的操作是将 arrays 当做 sets 处理;Set 表达式在处理过程中将会过滤掉输入 array 中重复的 entries 并且并不会考虑队列元素中的顺序;

If the set operation returns a set, the operation filters out duplicates in the result to output an array that contains only unique entries. The order of the elements in the output array is unspecified.

如果 set 操作返回的是一个 set 集合,该操作将会把重复的元素给过滤掉并返回一个 array,该 array 中元素顺序是不定的;(备注,set 操作是将 arrays 当做 set 来进行处理的,所以这里返回的仍然是一个 array )

Set 表达式所包含的操作符如下,

下面笔者就几个有关的操作符进行描述,

$setEquals

定义

表达式,

1
{ $setEquals: [ <expression1>, <expression2>, ... ] }

Compares two or more arrays and returns true if they have the same distinct elements and false otherwise.

比较两个 arrays,如果两个 arrays 拥有相同的 distinct 元素,则返回 true;

\$setEquals performs set operation on arrays, treating arrays as sets. If an array contains duplicate entries, \$setEquals ignores the duplicate entries. \$setEquals ignores the order of the elements.

  • \$setEquals 将 arrays 当做 set 进行处理;
  • \$setEquals 将会忽略掉重复的元素;
  • \$setEquals 将会忽略掉元素的顺序;

最后,如果 \$setEquals 并不会去比对一个嵌入文档中的内容,比如,下面这个表达式将会返回 false;

1
{ $setEquals: [ [ "a", "b" ], [ [ "a", "b" ] ] ] }

该表示返回 false,因为 [ [ “a”, “b” ] ] 表示一个嵌入式文档;所以 \$setEquals 不会匹配;如果将 [ [ “a”, “b” ] ] 改成 [ “a”, “b” ] 来进行匹配,则返回 true,

1
{ $setEquals: [ [ "a", "b" ], [ "a", "b" ] ] }

例子

假设我们有如下的 experiments 的数据,

1
2
3
4
5
6
7
8
9
{ "_id" : 1, "A" : [ "red", "blue" ], "B" : [ "red", "blue" ] }
{ "_id" : 2, "A" : [ "red", "blue" ], "B" : [ "blue", "red", "blue" ] }
{ "_id" : 3, "A" : [ "red", "blue" ], "B" : [ "red", "blue", "green" ] }
{ "_id" : 4, "A" : [ "red", "blue" ], "B" : [ "green", "red" ] }
{ "_id" : 5, "A" : [ "red", "blue" ], "B" : [ ] }
{ "_id" : 6, "A" : [ "red", "blue" ], "B" : [ [ "red" ], [ "blue" ] ] }
{ "_id" : 7, "A" : [ "red", "blue" ], "B" : [ [ "red", "blue" ] ] }
{ "_id" : 8, "A" : [ ], "B" : [ ] }
{ "_id" : 9, "A" : [ ], "B" : [ "red" ] }

需求是,判断 array A 与 B 之间是否有相同的 (distinct) 元素,

1
2
3
4
5
db.experiments.aggregate(
[
{ $project: { A: 1, B: 1, sameElements: { $setEquals: [ "$A", "$B" ] }, _id: 0 } }
]
)

$setUnion

https://docs.mongodb.com/manual/reference/operator/aggregation/setUnion/#exp._S_setUnion

定义

1
{ $setUnion: [ <expression1>, <expression2>, ... ] }

将多个 arrays 作为参数传入,以 set 的方式输出合并的结果 arrays;

从第一个例子中可以看到,合并以后,将会过滤掉重复的元素;第二个例子可以看到,同样 \$setUnion 并不会深入内嵌文档的 arrays 进行处理;

例子

假设我们有如下的 experiments 的相关数据,

1
2
3
4
5
6
7
8
9
{ "_id" : 1, "A" : [ "red", "blue" ], "B" : [ "red", "blue" ] }
{ "_id" : 2, "A" : [ "red", "blue" ], "B" : [ "blue", "red", "blue" ] }
{ "_id" : 3, "A" : [ "red", "blue" ], "B" : [ "red", "blue", "green" ] }
{ "_id" : 4, "A" : [ "red", "blue" ], "B" : [ "green", "red" ] }
{ "_id" : 5, "A" : [ "red", "blue" ], "B" : [ ] }
{ "_id" : 6, "A" : [ "red", "blue" ], "B" : [ [ "red" ], [ "blue" ] ] }
{ "_id" : 7, "A" : [ "red", "blue" ], "B" : [ [ "red", "blue" ] ] }
{ "_id" : 8, "A" : [ ], "B" : [ ] }
{ "_id" : 9, "A" : [ ], "B" : [ "red" ] }

需求是,我们需要合并字段 A 和字段 B 中的队列元素,并且以一个新的字段 allValues 进行输出

1
2
3
4
5
db.experiments.aggregate(
[
{ $project: { A:1, B: 1, allValues: { $setUnion: [ "$A", "$B" ] }, _id: 0 } }
]
)

输出结果如下,

1
2
3
4
5
6
7
8
9
{ "A": [ "red", "blue" ], "B": [ "red", "blue" ], "allValues": [ "blue", "red" ] }
{ "A": [ "red", "blue" ], "B": [ "blue", "red", "blue" ], "allValues": [ "blue", "red" ] }
{ "A": [ "red", "blue" ], "B": [ "red", "blue", "green" ], "allValues": [ "blue", "red", "green" ] }
{ "A": [ "red", "blue" ], "B": [ "green", "red" ], "allValues": [ "blue", "red", "green" ] }
{ "A": [ "red", "blue" ], "B": [ ], "allValues": [ "blue", "red" ] }
{ "A": [ "red", "blue" ], "B": [ [ "red" ], [ "blue" ] ], "allValues": [ "blue", "red", [ "red" ], [ "blue" ] ] }
{ "A": [ "red", "blue" ], "B": [ [ "red", "blue" ] ], "allValues": [ "blue", "red", [ "red", "blue" ] ] }
{ "A": [ ], "B": [ ], "allValues": [ ] }
{ "A": [ ], "B": [ "red" ], "allValues": [ "red" ] }

Comparison Expressions

Comparison expressions return a boolean except for \$cmp which returns a number.

除了 \$cmp 将返回一个数字以外其它的表达式均会返回一个 boolean 值;

The comparison expressions take two argument expressions and compare both value and type, using the specified BSON comparison order for values of different types.

接受两个参数,同时比较两个参数的值与类型;如果两个值的类型不同,使用 BSON 所指定的比较顺序执行比较,如下所述,

  1. MinKey (internal type)
  2. Null
  3. Numbers (ints, longs, doubles, decimals)
  4. Symbol, String
  5. Object
  6. Array
  7. BinData
  8. ObjectId
  9. Boolean
  10. Date
  11. Timestamp
  12. Regular Expression
  13. MaxKey (internal type)

所包含的表达式如下,

下面笔者就部分操作符进行描述;

$cmp

1
{ $cmp: [ <expression1>, <expression2> ] }

Compares two values and returns:

  • -1 if the first value is less than the second.
  • 1 if the first value is greater than the second.
  • 0 if the two values are equivalent.

The $cmp compares both value and type, using the specified BSON comparison order for values of different types.

$cmp has the following syntax:

1
{ $cmp: [ <expression1>, <expression2> ] }

假设我们有 inventory 相关的数据,

1
2
3
4
5
{ "_id" : 1, "item" : "abc1", description: "product 1", qty: 300 }
{ "_id" : 2, "item" : "abc2", description: "product 2", qty: 200 }
{ "_id" : 3, "item" : "xyz1", description: "product 3", qty: 250 }
{ "_id" : 4, "item" : "VWZ1", description: "product 4", qty: 300 }
{ "_id" : 5, "item" : "VWZ2", description: "product 5", qty: 180 }

使用 $cmp 分别对文档的每个 qty 与数字 250 进行比较,

1
2
3
4
5
6
7
8
9
10
11
12
13
db.inventory.aggregate(
[
{
$project:
{
item: 1,
qty: 1,
cmpTo250: { $cmp: [ "$qty", 250 ] },
_id: 0
}
}
]
)

返回如下的结果,

1
2
3
4
5
{ "item" : "abc1", "qty" : 300, "cmpTo250" : 1 }
{ "item" : "abc2", "qty" : 200, "cmpTo250" : -1 }
{ "item" : "xyz1", "qty" : 250, "cmpTo250" : 0 }
{ "item" : "VWZ1", "qty" : 300, "cmpTo250" : 1 }
{ "item" : "VWZ2", "qty" : 180, "cmpTo250" : -1 }

$eq

Compares two values and returns:

  • true when the values are equivalent.
  • false when the values are not equivalent.
1
{ $eq: [ <expression1>, <expression2> ] }

假设我们有如下 inventory 的数据,

1
2
3
4
5
{ "_id" : 1, "item" : "abc1", description: "product 1", qty: 300 }
{ "_id" : 2, "item" : "abc2", description: "product 2", qty: 200 }
{ "_id" : 3, "item" : "xyz1", description: "product 3", qty: 250 }
{ "_id" : 4, "item" : "VWZ1", description: "product 4", qty: 300 }
{ "_id" : 5, "item" : "VWZ2", description: "product 5", qty: 180 }

找到与 250 相等的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
db.inventory.aggregate(
[
{
$project:
{
item: 1,
qty: 1,
qtyEq250: { $eq: [ "$qty", 250 ] },
_id: 0
}
}
]
)

输出

1
2
3
4
5
{ "item" : "abc1", "qty" : 300, "qtyEq250" : false }
{ "item" : "abc2", "qty" : 200, "qtyEq250" : false }
{ "item" : "xyz1", "qty" : 250, "qtyEq250" : true }
{ "item" : "VWZ1", "qty" : 300, "qtyEq250" : false }
{ "item" : "VWZ2", "qty" : 180, "qtyEq250" : false }

$gte

这个是大于等于;

$lte

这个是小于等于;

$ne

not equals

Arithmetic Expressions

Arithmetic Expressions 对数字进行数学运算;总共包含如下的数学运算符,


笔者就几个典型的算术的操作符简要的介绍如下,

$add

可以将多个数字或者是 Date 进行相加,如果是 Date 相加,将会转换成 millisecondes 进行相加;

假设我们有如下的 sales 数据,

1
2
3
{ "_id" : 1, "item" : "abc", "price" : 10, "fee" : 2, date: ISODate("2014-03-01T08:00:00Z") }
{ "_id" : 2, "item" : "jkl", "price" : 20, "fee" : 1, date: ISODate("2014-03-01T09:00:00Z") }
{ "_id" : 3, "item" : "xyz", "price" : 5, "fee" : 0, date: ISODate("2014-03-15T09:00:00Z") }

数字相加

1
2
3
4
5
db.sales.aggregate(
[
{ $project: { item: 1, total: { $add: [ "$price", "$fee" ] } } }
]
)

输出结果,

1
2
3
{ "_id" : 1, "item" : "abc", "total" : 12 }
{ "_id" : 2, "item" : "jkl", "total" : 21 }
{ "_id" : 3, "item" : "xyz", "total" : 5 }

日期相加

sales 数据中的每个 date 字段累加三天( 32460*60000 milliseconds )

1
2
3
4
5
db.sales.aggregate(
[
{ $project: { item: 1, billing_date: { $add: [ "$date", 3*24*60*60000 ] } } }
]
)

得到如下的结果,

1
2
3
{ "_id" : 1, "item" : "abc", "billing_date" : ISODate("2014-03-04T08:00:00Z") }
{ "_id" : 2, "item" : "jkl", "billing_date" : ISODate("2014-03-04T09:00:00Z") }
{ "_id" : 3, "item" : "xyz", "billing_date" : ISODate("2014-03-18T09:00:00Z") }

$divide

接收两个参数,第一个参数是被除数,第二个参数是除数,并输出计算结果

1
{ $divide: [ <expression1>, <expression2> ] }

假设我们有如下的 planning 相关的数据,

1
2
{ "_id" : 1, "name" : "A", "hours" : 80, "resources" : 7 },
{ "_id" : 2, "name" : "B", "hours" : 40, "resources" : 4 }

假设我们需要输出按照工作天数的方式来输出 planning 数据,

1
2
3
4
5
db.planning.aggregate(
[
{ $project: { name: 1, workdays: { $divide: [ "$hours", 8 ] } } }
]
)

得到相关的输出结果,

1
2
{ "_id" : 1, "name" : "A", "workdays" : 10 }
{ "_id" : 2, "name" : "B", "workdays" : 5 }

$subtract

定义

Subtracts two numbers to return the difference, or two dates to return the difference in milliseconds, or a date and a number in milliseconds to return the resulting date.

  • 若是两个数相减,则返回差值;
  • 如果是两个 dates 相减,返回 milliseconds 的差值;
  • 如果是一个 date 和一个 milliseconds 数字相减,返回相减以后的日期值;这种情况下,date 必须是第一个参数;
1
{ $subtract: [ <expression1>, <expression2> ] }

例子

假设我们有如下 sales 相关的数据,

1
2
{ "_id" : 1, "item" : "abc", "price" : 10, "fee" : 2, "discount" : 5, "date" : ISODate("2014-03-01T08:00:00Z") }
{ "_id" : 2, "item" : "jkl", "price" : 20, "fee" : 1, "discount" : 2, "date" : ISODate("2014-03-01T09:00:00Z") }
数字相减

计算得出实际支付的费用,( price + fee( 小费 ) ) - discount = 实际消费额,

1
db.sales.aggregate( [ { $project: { item: 1, total: { $subtract: [ { $add: [ "$price", "$fee" ] }, "$discount" ] } } } ] )

得到如下的结果输出,

1
2
{ "_id" : 1, "item" : "abc", "total" : 7 }
{ "_id" : 2, "item" : "jkl", "total" : 19 }
日期相减

获取 date 字段与当前日期的差值

1
db.sales.aggregate( [ { $project: { item: 1, dateDifference: { $subtract: [ new Date(), "$date" ] } } } ] )

得到如下的结果,

1
2
{ "_id" : 1, "item" : "abc", "dateDifference" : NumberLong("11713985194") }
{ "_id" : 2, "item" : "jkl", "dateDifference" : NumberLong("11710385194") }
日期和 milliseconds 数字相减

通过 milliseconds 对 date 字段减少 5 分钟,

1
db.sales.aggregate( [ { $project: { item: 1, dateDifference: { $subtract: [ "$date", 5 * 60 * 1000 ] } } } ] )

得到输出结果

1
2
{ "_id" : 1, "item" : "abc", "dateDifference" : ISODate("2014-03-01T07:55:00Z") }
{ "_id" : 2, "item" : "jkl", "dateDifference" : ISODate("2014-03-01T08:55:00Z") }

String Expressions

除了 \$concat 操作符以外,其它操作符只对 ASCII 的字符进行有效的操作;也就是说,其它的操作只对英文字符、数字以及 ASCII 的相关符号进行有效的操作;

\$contact 可以对任何字符进行操作;

$contact

将两个字符串进行串接,然后返回串接以后的字符串,

1
{ $concat: [ <expression1>, <expression2>, ... ] }

如果串接中的某一个参数值为 null 或者不存在,那么 \$contact 返回 null;

假设我们有如下的 inventory 数据,

1
2
3
{ "_id" : 1, "item" : "ABC1", quarter: "13Q1", "description" : "product 1" }
{ "_id" : 2, "item" : "ABC2", quarter: "13Q4", "description" : "product 2" }
{ "_id" : 3, "item" : "XYZ1", quarter: "14Q2", "description" : null }

我们需要将 item 和 description 字段拼接起来作为一个新的字段 itemDescription 进行输出,

1
2
3
4
5
db.inventory.aggregate(
[
{ $project: { itemDescription: { $concat: [ "$item", " - ", "$description" ] } } }
]
)

将会返回如下的结果,

1
2
3
{ "_id" : 1, "itemDescription" : "ABC1 - product 1" }
{ "_id" : 2, "itemDescription" : "ABC2 - product 2" }
{ "_id" : 3, "itemDescription" : null }

可见,如果两个参数中如果有一个为 null 值,则返回结果为 null;

$split

根据分隔符对 string 进行拆分,将拆分以后的各个部分存放到一个数组中;

Text Search Expressions

\$meta

总共只有一个对应的操作符;

Array Expressions

对应数组的操作符如下,

不打算做进一步分析,这里只做 quick references;

$map

定义

对 array 中的每一个元素进行运算,然后返回经过计算以后的 array;

1
{ $map: { input: <expression>, as: <string>, in: <expression> } }
  • input
    输入的 array;
  • as
    定义一个局部的参数,来表示输入 array 中的每一个元素,并且通过 as 所定义的局部变量值作用到 in;这个和 \$set 的做法非常的类似;
  • in
    in 表达式用来对输入的 array 的每一个元素进行

例子

假设我们有如下的 grades 数据,

1
2
3
{ _id: 1, quizzes: [ 5, 6, 7 ] }
{ _id: 2, quizzes: [ ] }
{ _id: 3, quizzes: [ 3, 8, 9 ] }

假设我们需要对 quizzes 数组中的每一个元素都累加 1,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
db.grades.aggregate(
[
{ $project:
{ adjustedGrades:
{
$map:
{
input: "$quizzes",
as: "grade",
in: { $add: [ "$$grade", 2 ] }
}
}
}
}
]
)

注意,这里我们通过 as 定义了局部变量 grade,由其来表示 quizzes 数组中的元素;这样在 in 的表达式中就可以通过 “\$\$grade” 来引用 array 中的每一个元素了;

输出如下结果,

1
2
3
{ "_id" : 1, "adjustedGrades" : [ 7, 8, 9 ] }
{ "_id" : 2, "adjustedGrades" : [ ] }
{ "_id" : 3, "adjustedGrades" : [ 5, 10, 11 ] }

Variable Expressions

$set

定义

1
2
3
4
5
6
7
{
$let:
{
vars: { <var1>: <expression>, ... },
in: <expression>
}
}
  • vars
    提供一个参数的设置区域 ( Assignment Block ),该区域内所设置的参数将会提供给 in 的 expressions 使用;注意,在 vars 中设置的变量,只在 in 的表达式之内使用,使用在其它地方没有任何意义;

  • in
    定义需要被执行的表达式,该表达式将会使用到 vars 中所定义的变量;

备注,要访问 aggregation 中的所定义的变量,在字符前面使用前缀 \$\$ 即可;

特性

\$let 可以在 vars 赋值区域中使用外部定义的变量,该变量无论是用户变量还是系统变量都可以;如果,你在 vars 中修改了该外部变量,那么该修改后的变量值也只会作用到 in 的表达式中,在 in 表达式之外,外部变量依旧是原来的值;比如,我们有如下的一个例子,

1
2
3
4
5
6
7
{
$let:
{
vars: { low: 1, high: "$$low" },
in: { $gt: [ "$$low", "$$high" ] }
}
}

vars 赋值区域内部,通过 “\$\$low” 使用到了外部变量 low 并赋值给变量 high,我们来分析一下 in 表达式中的取值行为,

1
in: { $gt: [ "$$low", "$$high" ] }

“\$\$low” 引用的是 vars 表达式中定义的变量 low,”\$\$high”同理;

例子

加入我们有如下的 sales 的数据,

1
2
{ _id: 1, price: 10, tax: 0.50, applyDiscount: true }
{ _id: 2, price: 10, tax: 0.25, applyDiscount: false }

下面,通过使用 \$let 设置局部变量的方式来设置 discount,并以此计算出最终金额总数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.sales.aggregate( [
{
$project: {
finalTotal: {
$let: {
vars: {
total: { $add: [ '$price', '$tax' ] },
discounted: { $cond: { if: '$applyDiscount', then: 0.9, else: 1 } }
},
in: { $multiply: [ "$$total", "$$discounted" ] }
}
}
}
}
] )

这里要注意 \$cond 的用法,可以通过条件来进行赋值;如果是使用了 discount,则打 9 折;最后,在表达式 in 中通过引用在 vars 中所定义的变量 total 和 discounted 来计算出总额;得到的相关结果如下,

1
2
{ "_id" : 1, "finalTotal" : 9.450000000000001 }
{ "_id" : 2, "finalTotal" : 10.25 }

Literal Expressions

相关内容参考前面总结的 Literals

Data Type Expressions

https://docs.mongodb.com/manual/reference/operator/aggregation/#date-operators

$dayOfYear

返回一年中的第几天,该值的取值范围是 1 到 366 之间;

1
{ $dayOfYear: <expression> }

参数 expression 可以接受一切可以解析为 date 的参数值;

假设我们有如下的 sales 数据,

1
{ "_id" : 1, "item" : "abc", "price" : 10, "quantity" : 2, "date" : ISODate("2014-01-01T08:15:39.736Z") }

比如我们知道 date 是 year 中的第几天?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
db.sales.aggregate(
[
{
$project:
{
year: { $year: "$date" },
month: { $month: "$date" },
day: { $dayOfMonth: "$date" },
hour: { $hour: "$date" },
minutes: { $minute: "$date" },
seconds: { $second: "$date" },
milliseconds: { $millisecond: "$date" },
dayOfYear: { $dayOfYear: "$date" },
dayOfWeek: { $dayOfWeek: "$date" },
week: { $week: "$date" }
}
}
]
)

上述的 dayOfYear 进行了相应的转换;输出如下的结果,

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_id" : 1,
"year" : 2014,
"month" : 1,
"day" : 1,
"hour" : 8,
"minutes" : 15,
"seconds" : 39,
"milliseconds" : 736,
"dayOfYear" : 1,
"dayOfWeek" : 4,
"week" : 0
}

Conditional Expressions

$cond

定义

该表达式有两种写法,

New in version 2.6.

1
{ $cond: { if: <boolean-expression>, then: <true-case>, else: <false-case-> } }

_OR_

1
{ $cond: [ <boolean-expression>, <true-case>, <false-case> ] }

返回匹配条件的其中一个 expression 的结果;

例子

假设我们有如下的 inventory 的数据,

1
2
3
{ "_id" : 1, "item" : "abc1", qty: 300 }
{ "_id" : 2, "item" : "abc2", qty: 200 }
{ "_id" : 3, "item" : "xyz1", qty: 250 }

假设我们需要对 qty >= 250 的商品进行不同的折扣处理,满足这个条件的折扣幅度为 30%,若不满足这个条件的,折扣幅度为 20%;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.inventory.aggregate(
[
{
$project:
{
item: 1,
discount:
{
$cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
}
}
}
]
)

输出结果如下,

1
2
3
{ "_id" : 1, "item" : "abc1", "discount" : 30 }
{ "_id" : 2, "item" : "abc2", "discount" : 20 }
{ "_id" : 3, "item" : "xyz1", "discount" : 30 }

$ifNull

定义

1
{ $ifNull: [ <expression>, <replacement-expression-if-null> ] }

通过 \$ifNull 来检验第一个参数 expression

  • 如果该 expression 的取值不为 null,则返回该 expression 的值
  • 如果该 expression 的取值为 null 或者相关字段不存在,则返回第二个参数所对应的 expression 的值;

例子

1
2
3
{ "_id" : 1, "item" : "abc1", description: "product 1", qty: 300 }
{ "_id" : 2, "item" : "abc2", description: null, qty: 200 }
{ "_id" : 3, "item" : "xyz1", qty: 250 }

假设,我们要对不存在 description 或者 description 的值为 null 这样的 documents,将其 description 的值设置为 “undefined”;

1
2
3
4
5
6
7
8
9
10
db.inventory.aggregate(
[
{
$project: {
item: 1,
description: { $ifNull: [ "$description", "Unspecified" ] }
}
}
]
)

我们将会得到如下的结果,

1
2
3
{ "_id" : 1, "item" : "abc1", "description" : "product 1" }
{ "_id" : 2, "item" : "abc2", "description" : "Unspecified" }
{ "_id" : 3, "item" : "xyz1", "description" : "Unspecified" }

$switch

https://docs.mongodb.com/manual/reference/operator/aggregation/switch/

定义

非常类似于我们经常使用到的语法 switch … case … 的方式,依次遍历每一个 case 的表达式,如果匹配该 case 条件,则执行其相关的逻辑,然后退出,不再执行后续的 case 条件;

1
2
3
4
5
6
7
8
$switch: {
branches: [
{ case: <expression>, then: <expression> },
{ case: <expression>, then: <expression> },
...
],
default: <expression>
}

每一个 case 语句所对应的 document 对应的就是一个分支,default 表示默认分支,若没有任何一个分支匹配的情况下,使用该默认分支;

例子

假设我们有如下的 grades 数据,

1
2
3
{ "_id" : 1, "name" : "Susan Wilkes", "scores" : [ 87, 86, 78 ] }
{ "_id" : 2, "name" : "Bob Hanna", "scores" : [ 71, 64, 81 ] }
{ "_id" : 3, "name" : "James Torrelio", "scores" : [ 91, 84, 97 ] }

然后,我们期望对每一个学生的学习成绩做出一些评价信息,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
db.grades.aggregate( [
{
$project:
{
"name" : 1,
"summary" :
{
$switch:
{
branches: [
{
case: { $gte : [ { $avg : "$scores" }, 90 ] },
then: "Doing great!"
},
{
case: { $and : [ { $gte : [ { $avg : "$scores" }, 80 ] },
{ $lt : [ { $avg : "$scores" }, 90 ] } ] },
then: "Doing pretty well."
},
{
case: { $lt : [ { $avg : "$scores" }, 80 ] },
then: "Needs improvement."
}
],
default: "No scores found."
}
}
}
}
] )

输出如下的结果,

1
2
3
{ "_id" : 1, "name" : "Susan Wilkes", "summary" : "Doing pretty well." }
{ "_id" : 2, "name" : "Bob Hanna", "summary" : "Needs improvement." }
{ "_id" : 3, "name" : "James Torrelio", "summary" : "Doing great!" }

Date Type Expressions

$type

定义

返回当前参数的 BSON Type

例子

假设我们有如下 coll 的数据,

1
2
3
4
5
6
{ _id: 0, a : 8 }
{ _id: 1, a : [ 41.63, 88.19 ] }
{ _id: 2, a : { a : "apple", b : "banana", c: "carrot" } }
{ _id: 3, a : "caribou" }
{ _id: 4, a : NumberLong(71) }
{ _id: 5 }

期望是输出字段 a 的每一个元素的类型,

1
2
3
4
5
db.coll.aggregate([{
$project: {
a : { $type: "$a" }
}
}])

将会输出如下的结果,

1
2
3
4
5
6
{ _id: 0, "a" : "double" }
{ _id: 1, "a" : "array" }
{ _id: 2, "a" : "object" }
{ _id: 3, "a" : "string" }
{ _id: 4, "a" : "long" }
{ _id: 5, "a" : "missing" }

References

https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#expressions