需求
现有如下mongodb数据表demo
,N/A
表示该条数据没有对应字段(建表语句见文末附件)。
要求查询逻辑和步骤为:
- 先按条件筛选表数据
- 按照
point_time
计算后的值和create_time
字段降序 - 取出第2~4条数据
其中,point_time
计算方法为:如果字段存在
且值不为null
且值大于等于指定时间
时,取1
,否则取0
。
_id | type | create_time | point_time |
---|---|---|---|
1 | t1 | 2019-03-25 | 2018-03-25 |
2 | t2 | 2019-01-07 | 2019-01-01 |
3 | t3 | 2019-01-07 | 2019-02-01 |
4 | t1 | 2019-05-03 | N/A |
5 | t1 | 2019-01-01 | 2018-06-01 |
6 | t2 | 2019-03-01 | N/A |
备注:
2
和3
具有相同的create_time
值4
和6
没有字段point_time
- 指定时间为
2018-09-29
point_time
计算后,2
、3
为1,1
、4
、5
、6
为0- 执行排序后结果按
_id
列出应该为:2 3 4 1 6 5
- 最终结果按
_id
列出应该为:3 4 1
换作mysql时的实现语句
如果放在mysql中,这个SQL是没有难度的,筛选后用order by
排序,point_time
计算后再应用到排序中即可。就如下面SQL(where语句省略,下述语句均省略)。
1 | select * from demo where ... |
或者预先构建一个排序字段is_top
,然后再按字段排序:
1 | select *, if(point_time is not null and point_time >= '2018-09-29', 1, 0) as is_top |
mongo实现语句
但是到了mongodb的查询就不同了:
sort
语句语法如下,sort
字段没有提供计算语法。
1 | db.demo.find().sort({ field: value }) |
find
语句没有提供像mysql一样添加自定义字段的语法。
如果需要实现上述需求,需要用到mongo的聚合操作
。
聚合操作简单介绍
聚合操作(aggregation operations)可以理解为一系列计算操作的管道(pipeline,我认为翻译为流水线或执行流更容易理解),操作在管道中按阶段执行。操作有如:分组($group)、修改数据结构($project)、排序($sort)、计算数量($count)等,每个操作在管道中叫做阶段(stage),每个阶段执行的结果会给下一个阶段。当管道执行完毕后可以得到新的数据集合(doucments,mongon将一条数据或一条命令语句叫做document,都是BSON结构)。
聚合操作有的阶段操作和查询命令相似,但并不是同样的东西,前者是阶段操作,后者是命令,不论写法还是调用方式都有些区别的,比如阶段的名称前面有$
符号,阶段是作为key被调用,而命令是类似.find()
这种形式的,不要混淆了。
语法
聚合操作调用语法如下,每一个{}
表示一个阶段。
1 | db.demo.aggregate( [ {}, {}, ... ] ) |
阶段语法如下,各阶段的{语句}
有所不同,具体参考mongo的文档。
1 | { $阶段名: {语句} } |
$addFields
例子
比如要添加字段的阶段名为:$addFields
,{语句}
的语法为:
1 | {$字段名: 指定值} |
- 现要添加字段
is_top
,计算方法为point_time
是否大于等于指定时间2018-09-29
,调用聚合操作的语句如下。注意,调用point_time
时前面必须添加$
符号。
1 | db.demo.aggregate([{ |
$match
和$sort
例子
再比如筛选($match)和排序($sort),{语句}
的语法与查询(find语句
)相同,即{字段名: {$操作名, 指定值}}
:
- 现要筛选
create_time
大于等于2018-09-29
的数据,语句如下:
1 | db.demo.aggregate([{ |
- 现要按照
create_time
降序,语句如下。
1 | db.demo.aggregate([{ |
需求实现
考虑到聚合操作的$sort阶段依然无法对字段进行计算,因此采用类似mysql查询语句的第二种语句实现,即添加一个排序字段。按照需求处理排序字段:
- 排序字段命名为
is_top
- 字段通过
point_tmie
计算得出,point_tmie
字段在数据中可能不存在,因此需要先判空,如果字段不存在则使用"null"
替代(null字符串) - 根据
point_tmie
不为"null"
并且大于等于2018-09-29
来判断排序字段为1
或0
下面实现的语句或命令均为聚合操作的语句。
1. 空字段处理操作:$ifNull
聚合操作中$ifNull
操作用来做空判断,与mysql的ifnull
函数类似,即字段不存在或为undified或为null
即用第二个参数替代。语句如下。
1 | { $ifNull: [ 字段, 替代值 ] } |
现$point_time
可能不存在,所以使用下面语句将值替换为"null"
,注意point_time
前面必须添加$
符号,否则会被认为是一个字符串,结果会总是非空的。
1 | {$ifNull: ["$point_time", "null"]} |
2. 排序字段的计算操作:$cond
、$ne
、$gte
、$and
排序字段计算需要用到if判断,聚合操作中if条件判断使用$cond
操作,与mysql的if
函数类似,语句如下。
1 | { $cond: [ 条件语句, 为真的值, 为假的值 ] } |
现要判断point_time
(替换空后)的值不为”null”并且大于等于2018-09-29
,判断操作的语法如下:
1 | { $操作名: [ "$字段名": 比较值 ] } |
那么判断不为”null”语句如下:
1 | {$ne: [{$ifNull: ["$point_time", "null"]}, "null"]} |
判断大于等于2018-09-29
语句如下:
1 | {$gte: ["$point_time", "2018-09-29"]} |
要执行且
操作,语法如下:
1 | { $and: [ 表达式1, 表达式2, ... ]} |
那么将且
操作($and)和if
操作($cond)写入语句:
1 | $cond: [ |
需要特别注意的是和查询操作的筛选语法进行区别,虽然操作名都是一样的,但语法不同,查询语法的操作名和字段名均为key,比如查询操作语句筛选point_time
大于等于2018-09-29
语句如下。而聚合操作中一些比较操作语句的操作名为key,值的第一个元素才是字段名,需要注意不要混淆了。
1 | db.demo.find({ |
3. 添加字段阶段:$addFields
现需要将计算完成的排序字段添加到数据结果集中(与mysql类似,不会真正的写入数据库,只是新增一个展示字段)。考虑到之前的所有字段都要展示出来,因此采用$addFields
,$addFields
语法如下。
1 | { $addFields: { 自定义字段名: 字段值 } } |
其中,字段值可以是bson(类似json的格式),就是说可以有复杂的判断等操作。
那么语句如下,语句中字段值
为上述2. 排序字段的计算操作
中的if判断后的结果。
1 | {$addFields: { |
4. 筛选阶段:$match
比较简单,只需要注意排序在聚合操作中是$match
阶段,语法和find命令筛选语法类似,语法如下。
1 | { $match: { 字段名: { $操作名: 指定值 } } } |
比如要筛选create_time
大于2019-09-29
,语句如下:
1 | {$match: { |
5. 排序阶段:$sort
也是需要注意其在聚合操作中是$sort
阶段,语法和sort命令类似。实例语句如下:
1 | {$sort: { |
6. 分页阶段:$skip
、$limit
分页与mysql类似,只是语法上使用2组阶段完成,语句如下:
1 | // skip从0开始,1表示从第二条数据开始 |
7. 阶段装配
将$addFields
、$match
、$sort
按照语法组合,最终语句如下(mongo都是啥语法呀,还是mysql语法简洁😂)
1 | db.demo.aggregate( |
8. 运行测试
运行语句后,结果结果按_id
列出为:2 4 1
,嗯?为什么不是3 4 1
?
测试发现当2条数据排序条件相同时,分页时如果只取其中一条数据时每次都会选取顺序更靠前的那条(也不知道是不是mongo的bug…)
解决方法是强制按照_id
排序即可,即$sort
阶段改为:
1 | {$sort: { |
聚合操作拾遗
$project用法
上述语句中添加自定义字段使用的$addFields
,表示保留原先的字段并再添加一个自定义字段。但是如果想修改已有字段,如删除字段,可以使用另一个阶段操作:$project
(我也很是疑惑为什么叫project,可能翻译为设计
、投射
吧)。
- 表示只显示字段名1和字段名2,使用语法如下:
1 | { $project: { |
- 表示不显示字段名1和字段名2,可使用:
1 | { $project: { |
需要注意1和0不能混用,否则会报错(仔细想想混用的话确是不合逻辑的)。但也有个特殊字段_id
,这个字段默认是显示的,如果不要显示可以设置为0,此时就可以混用0和1,语句如下:
1 | { $project: { |
参考
- Aggregation:聚合操作介绍文档
- Aggregation Pipeline Stages:聚合操作阶段文档
- Aggregation Pipeline Operators:聚合操作阶段的操作文档
附件
- 数据创建语句
1 | db.demo.drop(); |