$datetrunc是mongodb 5.0+的日期截断操作符,需配合$group使用;必须注意时区对齐、输入为date类型、binsize为数字且整除单位秒数、无法走索引等要点。

什么是 $dateTrunc?它和 $group 配合统计时必须注意什么
$dateTrunc 是 MongoDB 5.0+ 引入的聚合操作符,用于将日期截断到指定的时间单位(如小时、天、月),本质是“向下取整”——不是四舍五入,也不是向上对齐。它本身不输出统计结果,必须配合 $group 才能做窗口统计。
常见误用是把它当 $dateToString 用,试图直接格式化输出;但 $dateTrunc 返回的是一个 Date 类型值,不是字符串。如果后续要按“2024-03”这种字符串分组,得再套一层 $dateToString,否则 $group 会按毫秒级时间戳精确分组,看似按天截断,实则因时区或毫秒差异导致同一天被拆成多组。
- 必须确保输入字段是合法
Date类型,String格式(如"2024-03-15T08:30:00Z")需先用$dateFromString转换 - 默认使用 UTC 时区截断,若业务在东八区,需显式传
timezone: "+08:00",否则凌晨 0 点数据会被归入前一天 -
binSize参数仅支持number,不能写"1"(字符串),否则报错Expected number in $dateTrunc
按小时统计订单量:$dateTrunc + $group 的标准写法
假设集合 orders 有字段 createdAt(Date 类型),要统计每小时下单数:
[
{
$group: {
_id: { $dateTrunc: { date: "$createdAt", unit: "hour" } },
count: { $sum: 1 }
}
}
]
这个写法没问题,但要注意:_id 是 Date 类型,返回类似 ISODate("2024-03-15T08:00:00Z")。如果前端需要 “2024-03-15 08:00” 这种可读格式,得加一步投影:
[
{
$group: {
_id: { $dateTrunc: { date: "$createdAt", unit: "hour" } },
count: { $sum: 1 }
}
},
{
$project: {
hour: { $dateToString: { format: "%Y-%m-%d %H:00", date: "$_id" } },
count: 1,
_id: 0
}
}
]
- 别漏掉
$project中的_id: 0,否则原始 Date 值仍会输出,干扰结构 -
format里不能用%I(12 小时制),MongoDB 不支持;必须用%H(24 小时制) - 如果
createdAt可能为null或缺失,$dateTrunc会返回null,导致所有空值被归入同一组,建议前置$match: { createdAt: { $exists: true, $type: "date" } }
按 30 分钟窗口统计活跃用户:用 binSize 实现非标准周期
unit: "minute" 最小只支持分钟级,但实际常需 15 分、30 分、甚至 5 分钟粒度。这时靠 binSize 参数实现:
{
$dateTrunc: {
date: "$lastActiveAt",
unit: "minute",
binSize: 30
}
}
效果是把每 30 分钟切一刀:00:00–00:29 → 00:00,00:30–00:59 → 00:30,以此类推。注意 binSize 是从 Unix epoch(1970-01-01T00:00:00Z)开始算的偏移,不是从当天零点起算——所以如果你希望每天从 00:00 开始 30 分钟一窗,得结合 timezone 使用:
- 错误写法:
{ unit: "minute", binSize: 30 }—— 在 UTC 时区下,00:15 的记录会被截到 00:00,但 00:45 会被截到 00:30,跨天边界可能错乱 - 正确写法:
{ unit: "minute", binSize: 30, timezone: "+08:00" }—— 确保所有时间按本地时区对齐 -
binSize必须整除unit对应的秒数(如minute= 60 秒,binSize只能是 1、2、3、5、6、10、12、15、20、30、60),填 25 会报错
性能陷阱:$dateTrunc 能否走索引?
不能直接走索引。即使你在 createdAt 字段建了索引,$dateTrunc: { date: "$createdAt", unit: "day" } 这种表达式仍无法命中索引,MongoDB 会全表扫描后再计算截断值。
想提速,得提前物化截断字段。例如,在写入时就存一个 createdAtDay 字段(值为 new Date(date.getFullYear(), date.getMonth(), date.getDate())),然后对该字段建索引并直接 $group:
{ $group: { _id: "$createdAtDay", count: { $sum: 1 } } }
- 物化字段方案适合写少读多、时间粒度固定的场景(如只按天统计)
- 如果需要灵活切换粒度(小时/天/周),还是得用
$dateTrunc,但务必加$match尽量缩小数据范围,比如先筛出最近 7 天的数据再截断 - 聚合管道中
$dateTrunc放得越靠后越好,避免在大量文档上重复计算
真正麻烦的不是语法,而是时区对齐和索引失效这两点——线上跑慢了,八成卡在这儿。











