MongoDB 日期提取,一篇讲透 $hour 和 $dayOfWeek 的坑与巧用
在 MongoDB 中提取日期字段的小时数或星期几,是常见的需求场景:例如按小时统计订单数量,或者过滤掉周末的日志记录。虽然看起来简单,但在实际编写聚合管道时,不少开发者容易踩坑却浑然不觉。本文快速拆解几个容易出错的点,并给出每种场景下的正确处理方法。

提取日期字段的小时值:用 $hour 处理 ISODate 或时间戳
使用 $hour 提取小时之前,必须先明确一个前提条件:它只支持 Date 类型(即 ISODate 格式),如果传入字符串或纯数字时间戳,会直接返回 null,并且不会报错。这一问题在线上聚合任务中非常隐蔽,常常排查许久才发现是字段类型不匹配。
- 检查字段类型:可以使用
{$type: "$field"}来判断,只有返回"date"才安全。 - 字符串转日期:如果字段是
"2024-05-20T14:30:00Z"这种字符串格式,提前用$dateFromString将其转换为日期对象。 - 毫秒时间戳转换:MongoDB 4.0 及以上版本可以直接用
{$toDate: "$ts_field"}转换,也可以使用$dateFromParts方式。 - 时区处理:
$hour默认按 UTC 解析时间,如果需要本地时区的小时值,通常配合$dateToString的timezone参数来做预处理。
{$project: {hour_of_event: {$hour: "$created_at"}}}
提取星期几:$dayOfWeek 返回 1(周日)到 7(周六)
$dayOfWeek 的返回规则是 周日 = 1,这与 JavaScript 的 getDay() 一致,但与 ISO 8601 标准(周一 = 1)相冲突。如果业务要求“周一显示为 1”,就不能直接使用 $dayOfWeek 的返回值,需要手动进行偏移处理。
- 直接使用:
{$dayOfWeek: "$order_time"}→ 周日=1,周六=7。适合统计非工作日、按星期几分组等场景。 - 转换为 ISO 标准(周一=1):可以先执行
{$add: [{$dayOfWeek: "$order_time"}, -1]},然后对“周日变 0”的情况做特殊处理。更严谨的写法是使用$cond:{$cond: [{eq: [{$dayOfWeek: "$order_time"}, 1]}, 7, {$subtract: [{$dayOfWeek: "$order_time"}, 1]}]} - 空值容错:如果字段本身可能是 null 或无效日期,
$dayOfWeek也会返回 null。建议在前置阶段使用$ifNull或$cond进行兜底处理。
聚合中同时提取多个时间单位:避免重复解析日期
一次 $dateFromString 或 $toDate 的解析开销在百万级数据量下不容忽视。如果需要同时获取小时、星期几、月份等信息,最忌讳的做法是每次提取一个单位就对同一字段调用一次转换操作符。正确的做法是:只解析一次,重复使用结果。
- 错误示例:在
$project中连续写三遍{$toDate: "$ts"} - 推荐方案:先在
$addFields阶段将解析好的日期存入临时字段,比如parsed_date。后续所有对$hour、$dayOfWeek的调用都基于这个临时字段。 - 内存考量:临时字段虽然方便,但在极端大数据量的聚合中会增加文档体积,需要兼顾可读性与资源消耗。
聚合阶段顺序影响结果:$project 中不能依赖未定义字段
$hour 和 $dayOfWeek 都是表达式操作符,只能在允许表达式的阶段中使用,例如 $project、$addFields、$group 的 $sum 表达式内部。它们不能直接出现在 $match 阶段作为查询条件,除非配合 $expr 进行包装。
- 按星期几筛选:若要写“星期几 = 2”,必须这样写:
{$match: {$expr: {$eq: [{$dayOfWeek: "$dt"}, 2]}}} - 按小时分桶:在
$group中写{$group: {_id: {$hour: "$ts"}, count: {$sum: 1}}}是直接支持的用法。 - 调试不易:嵌套表达式中如果某个环节格式错误,例如
{$hour: {$toDate: "$str_ts"}}里$str_ts转换失败,整个字段会返回 null,但 MongoDB 不会抛出异常。线上排查时通常需要先输出中间字段来定位问题。
最后再多提一句:时区的隐含行为以及字符串日期的静默失败,是线上聚合任务中最容易导致统计偏差的根源。处理之前先确认字段类型,处理之后做小样本验证,可以节省大量排查时间。
