在MongoDB的聚合框架里,经常会遇到需要凭空产生一组连续整数的场景,比如生成近三十天的日期偏移、构造分页用的序号、或是为后续$unwind提供展开基础。聚合管道中的$range表达式就是专门用来解决这类问题的内置算子,它不需要依赖任何已有集合的字段,仅通过参数就能输出一个整型数组。

$range操作符的基础语法与参数含义
$range属于聚合表达式里的数组表达式,其标准写法为{ $range: [ start, end, step ] }。其中start是生成数组的第一个元素值,end用于界定上限,但遵循左闭右开原则,即数组不会包含等于end的那个值。step为步长,可省略,省略时默认为1。当step为正数时,要求start小于end;若step为负数,则要求start大于end,此时数组会递减。
需要注意的是,$range的参数不仅可以是常量,也能是管道中前面阶段计算出的字段或表达式结果。例如在$project阶段引用文档里的min和max字段动态生成区间。下面的示例展示了一个最简单的用法,直接在$project里生成0到4的数组:
db.numbers.aggregate([
{
$project: {
seq: { $range: [ 0, 5, 1 ] }
}
}
]);
// 输出: { "seq" : [ 0, 1, 2, 3, 4 ] }
从输出可见,虽然结束值是5,但数组最后一个元素是4。这种左闭右开的设计和多数编程语言中的range函数保持一致,老练的开发者能很快适应,但初学者常误以为会包含结束值,从而在实际拼接日期或索引时少算一位。明确这一边界,是正确使用$range的前提。
结合$unwind与日期运算构造连续序列
单纯得到一个数字数组往往还不够,真实业务里我们常要把数字变成日期、标签或者关联键。这时$range通常配合$unwind将数组拆成多行,再借助$add和$dateToString等表达式转换。例如要列出从今天起未来七天的日期,可以先用$range生成0到6,拆行后给当前时间加上对应天数。
下面代码先在$project里用$range造出偏移量,再通过$unwind展开,最后用$add把偏移量转为毫秒加到ISODate上。这种方式比在应用层循环查询数据库要高效得多,因为所有计算都在MongoDB服务端一次完成。
db.dates.aggregate([
{
$project: {
_id: 0,
offset: { $range: [ 0, 7 ] }
}
},
{ $unwind: "$offset" },
{
$project: {
day: {
$dateToString: {
format: "%Y-%m-%d",
date: { $add: [ new Date(), { $multiply: [ "$offset", 86400000 ] } ] }
}
}
}
}
]);
上述管道输出的每一行都是一个具体日期字符串。如果步长设为2,则隔天取数;若start设为1、end设为0、step为-1,则生成降序的[1,0],适合做倒序编号。由此可见,$range的灵活性不仅体现在正负步长,也体现在它和日期、算术表达式的无缝嵌套。
与$expr及变量配合实现动态数字范围
有时范围并非固定,而是依赖文档自身字段。比如集合里存了每场活动的抽奖次数count,需要给每个活动生成对应长度的序号数组以供后续展开。此时可在$project中把$range的结束值写成$count字段引用,如果还要加条件限制,可用$cond包裹。
在find阶段虽不能直接用$range,但可借助$expr在查询条件里调用部分聚合表达式做过滤;而完整的$range生成依然建议在aggregate中完成。以下示例根据每个文档的total字段生成从1开始的编号数组:
db.activities.aggregate([
{
$project: {
tickets: { $range: [ 1, { $add: [ "$total", 1 ] } ] }
}
}
]);
这里$add让结束值变成total+1,从而让数组包含total本身。如果total为0,则$range返回空数组,不会报错,这种健壮性使得它很适合处理用户可变输入。当与$size配合时,还能反推文档需要的占位数量,在补零报表或固定列展示时非常实用。
总体来看,$range用极简的声明式语法替代了过往依赖辅助表或脚本的方案。掌握它的边界规则、步长符号以及和$unwind等算子的组合,能显著提升聚合脚本的表达力与执行效率。