熟悉 Linux 操作系统的小伙伴们应该知道 Linux 中有管道的说法,可以用来方便的处理数据。

MongoDB2.2 版本也引入了新的数据聚合框架,一个文档可以经过多个节点组成的管道,每个节点都有自己特殊的功能,比如文档分组、文档过滤等,每一个节点都会接受一连串的文档,对这些文档做一些类型转换,然后将转换后的文档传递给下一个节点,最后一个节点则会将结果返回给客户端。本文我们就先来看几个基本的管道操作符。

# $match

$match 可以用来对文档进行筛选,筛选完成之后我们就可以在筛选获得到的文档子集上来做数据聚合操作了,我们之前介绍的查询的操作符在 $match 中都可以使用,比如获取集合中所有 author 为"杜甫"的文档,如下:

db.sang_collect.aggregate({$match:{author:"杜甫"}})

我们在实际使用时最好将 $match 放在管道的前面,这样可以减少后面管道的工作量,同时,我们在投射和分组之前执行 $match 还可以用索引。

# $project

# 基本用法

$project 可以用来 ** 提取想要的字段 **,如下:

db.sang_collect.aggregate({$project:{title:1,_id:0}})

1 表示要该字段,0 表示不要该字段,也可以对返回的字段进行 ** 重命名 **,比如将 title 改为 articleTitle,如下:

db.sang_collect.aggregate({$project:{"articleTitle":"$title"}})

不过这里有一个问题需要注意,如果原字段上有索引,重命名之后的字段上就没有索引了,因此最好在重命名之前使用索引。

# 数学表达式

数学表达式可以用来对一组数值进行加减乘除取模,比如我的数据结构如下:

{
    "_id" : ObjectId("59f841f5b998d8acc7d08863"),
    "orderAddressL" : "ShenZhen",
    "prodMoney" : 45.0,
    "freight" : 13.0,
    "discounts" : 3.0,
    "orderDate" : ISODate("2017-10-31T09:27:17.342Z"),
    "prods" : [ 
        "可乐", 
        "奶茶"
    ]
}

订单的总费用为商品费用加上运费,查询如下:

db.sang_collect.aggregate({$project:{totalMoney:{$add:["$prodMoney","$freight"]}}})

实际付款的费用是总费用减去折扣,如下:

db.sang_collect.aggregate({$project:{totalPay:{$subtract:[{$add:["$prodMoney","$freight"]},"$discounts"]}}})

再来三个无厘头运算,比如计算 prodMoney 和 freight 和 discounts 的乘积:

db.sang_collect.aggregate({$project:{test1:{$multiply:["$prodMoney","$freight","$discounts"]}}})

再比如求 $prodMoney 和 $freight 的商,如下:

db.sang_collect.aggregate({$project:{test1:{$divide:["$prodMoney","$freight"]}}})

再比如用 $freight 对 $prodMoney 取模,如下:

db.sang_collect.aggregate({$project:{test1:{$mod:["$prodMoney","$freight"]}}})

加法和乘法都可以接收多个参数,其余的都接收两个参数。

# 日期表达式

日期表达式可以从一个日期类型中提取出年、月、日、星期、时、分、秒等信息,如下:

db.sang_collect.aggregate({$project:{"年份":{$year:"$orderDate"},"月份":{$month:"$orderDate"},"一年中第几周":{$week:"$orderDate"},"日期":{$dayOfMonth:"$orderDate"},"星期":{$dayOfWeek:"$orderDate"},"一年中第几天":{$dayOfYear:"$orderDate"},"时":{$hour:"$orderDate"},"分":{$minute:"$orderDate"},"秒":{$second:"$orderDate"},"毫秒":{$millisecond:"$orderDate"},"自定义格式化时间":{$dateToString:{format:"%Y年%m月%d %H:%M:%S",date:"$orderDate"}}}})

执行结果如下:

{
    "_id" : ObjectId("59f841f5b998d8acc7d08861"),
    "年份" : 2017,
    "月份" : 10,
    "一年中第几周" : 44,
    "日期" : 31,
    "星期" : 3,
    "一年中第几天" : 304,
    "时" : 9,
    "分" : 27,
    "秒" : 17,
    "毫秒" : 342,
    "自定义格式化时间" : "2017年10月31 09:27:17"
}

$dayOfWeek 返回的是星期,1 表示星期天,7 表示星期六,$week 表示本周是本年的第几周,从 0 开始计。$dateToString 是 MongoDB3.0+ 中的功能。格式化的字符还有以下几种:

字符 含义 取值范围
%Y Year (4 digits, zero padded) 0000-9999
%m Month (2 digits, zero padded) 01-12
%d Day of Month (2 digits, zero padded) 01-31
%H Hour (2 digits, zero padded, 24-hour clock) 00-23
%M Minute (2 digits, zero padded) 00-59
%S Second (2 digits, zero padded) 00-60
%L Millisecond (3 digits, zero padded) 000-999
%j Day of year (3 digits, zero padded) 001-366
%w Day of week (1-Sunday, 7-Saturday) 1-7
%U Week of year (2 digits, zero padded) 00-53

# 字符串表达式

字符串表达式中有字符串的截取、拼接、转大写、转小写等操作,比如我截取 orderAddressL 前两个字符返回,如下:

db.sang_collect.aggregate({$project:{addr:{$substr:["$orderAddressL",0,2]}}})

再比如我将 orderAddressL 和 orderDate 拼接后返回:

db.sang_collect.aggregate({$project:{addr:{$concat:["$orderAddressL",{$dateToString:{format:"--%Y年%m月%d",date:"$orderDate"}}]}}})

结果如下:

{
    "_id" : ObjectId("59f841f5b998d8acc7d08861"),
    "addr" : "NanJing--2017年10月31"
}

再比如我将 orderAddressL 全部转为小写返回:

db.sang_collect.aggregate({$project:{addr:{$toLower:"$orderAddressL"}}})

再比如我将 orderAddressL 全部转为大写返回:

db.sang_collect.aggregate({$project:{addr:{$toUpper:"$orderAddressL"}}})

# 逻辑表达式

想要比较两个数字的大小,可以使用 $cmp 操作符,如下:

db.sang_collect.aggregate({$project:{test:{$cmp:["$freight","$discounts"]}}})

如果第一个参数大于第二个参数返回正数,第一个参数小于第二个则返回负数,也可以利用 $strcasecmp 来比较字符串(中文无效):

db.sang_collect.aggregate({$project:{test:{$strcasecmp:[{$dateToString:{format:"..%Y年%m月%d",date:"$orderDate"}},"$orderAddressL"]}}})

至于我们之前介绍的 $eq/$ne/$gt/$gte/$lt/$lte 等操作符在这里一样是适用的。另外还有 and、$or、$not 等表达式可用,以 $and 为例,如下:

db.sang_collect.aggregate({$project:{test:{$and:[{"$eq":["$freight","$prodMoney"]},{"$eq":["$freight","$discounts"]}]}}})

$and 中的每个参数都为 true 时返回 true,$or 则表示参数中有一个为 true 就返回 true,$not 则会对它的参数的值取反,如下:

db.sang_collect.aggregate({$project:{test:{$not:{"$eq":["$freight","$prodMoney"]}}}})

另外还有两个流程控制语句,如下:

db.sang_collect.aggregate({$project:{test:{$cond:[false,"trueExpr","falseExpr"]}}})

$cond 第一个参数如果为 true,则返回 trueExpr,否则返回 falseExpr.

db.sang_collect.aggregate({$project:{test:{$ifNull:[null,"replacementExpr"]}}})

$ifNull 第一个参数如果为 null,则返回 replacementExpr,否则就返回第一个参数。

好了,MongoDB 中的管道操作符我们就先说到这里,下篇文章继续,小伙伴们有问题欢迎留言讨论。 参考资料:

  1. 《MongoDB权威指南第2版》
  2. mongodb聚合利用日期分组