MongoDB聚合管道所有日期操作均基于UTC,$year等操作符提取的是UTC时间值而非本地时区;需按本地时区分组时须手动偏移毫秒再计算;$dateDiff默认四舍五入,$mod不能直接用于Date类型,必须先提取分钟再运算。

MongoDB 聚合管道本身不处理时区,所有日期运算都基于 UTC 值进行——所谓“时区偏差”,其实是你在聚合中误把 UTC 时间当本地时间用了,或在分组/计算逻辑里没对齐时区语义。
聚合中用 $year/$month 等提取字段时为什么显示错乱?
这些操作符直接从 BSON Date(即 UTC 毫秒值)中提取对应 UTC 时间的年月日,不是你本地时区的。比如北京时间 2026-07-21T23:00:00+08:00 存入后是 UTC 的 2026-07-21T15:00:00Z,$year 提取出来仍是 2026,但 $hour 是 15,不是你想看到的 23。
- 别指望
$year自动按本地时区算;它永远返回 UTC 时间对应的值 - 若需按东八区分组,必须先将 UTC 时间偏移 +8 小时,再提取:用
$add加8 * 60 * 60 * 1000毫秒,再套$dateFromParts或继续用$year/$month - 注意:加毫秒后可能跨天,
$dateFromParts更安全,但要求 MongoDB ≥ 4.0
$dateDiff 计算结果和预期不符,是不是时区问题?
不是时区问题,是 $dateDiff 默认四舍五入——它只认两个 UTC 时间点的差值,然后按 unit 四舍五入。比如 UTC 时间差 3 小时 55 分,unit: "hour" 就返回 4。
- 要向下取整(即“完整小时数”),放弃
$dateDiff,改用:$floor+$divide+$subtract - 示例:计算距今完整小时数:
{$floor: {$divide: [{$subtract: [new Date(), "$updatedAt"]}, 1000 * 60 * 60]}} - 该表达式结果是纯数值,不带时区,但语义明确:多少个完整的 60 分钟
按 15 分钟窗口分组时,$minute 为空或分组错位
根本原因是直接对 $timestamp(Date 对象)用 $mod,而 $mod 只接受数字,MongoDB 会静默返回 null——这不是时区问题,是类型错误。
- 正确做法:先用
$minute提取分钟数(UTC 分钟),再做{$subtract: ["$minute", {$mod: ["$minute", 15]}]} - 这个结果是 0/15/30/45,代表该 UTC 分钟所属的 15 分钟区间起点
- 务必搭配
$year和$dayOfYear一起作为分组键,否则跨天数据会被合并 - 如果你需要的是本地时区的 15 分钟窗口(如北京时间 9:00–9:15),得先偏移再算,不能只调
$minute
真正容易被忽略的点是:MongoDB 所有日期聚合操作都严格运行在 UTC 上,没有“服务端时区”概念。所谓“修复时区”,本质是应用层把 UTC 时间按需偏移后再解析——聚合管道里不做任何自动转换,也不会因配置文件里的 timeZoneInfo 改变行为。

















