$toDate是首选,因它原生支持秒级(10位)和毫秒级(13位)数值时间戳,自动识别无需手动判断;而$dateFromString仅接受ISO字符串格式,对数值直接报错。

为什么 $toDate 是首选,而不是 $dateFromString
Unix 时间戳(秒级或毫秒级)不是 ISO 字符串,$dateFromString 会直接报错 invalid date —— 它只认字符串格式的日期,比如 "2024-01-01T00:00:00Z"。而 $toDate 原生支持数值型时间戳,自动识别秒级(10 位)和毫秒级(13 位),无需手动判断或补零。
实操建议:
- 直接用
$toDate转换字段:{ $toDate: "$ts" },其中ts是数值型字段(int 或 long) - 若字段是字符串(如
"1717027200"),先用$toInt转成整数再进$toDate,否则会静默返回null - 避免用
$concat拼接字符串再走$dateFromString,性能差且易出错(时区、精度丢失)
秒级 vs 毫秒级时间戳:不转换就错
MongoDB 的 $toDate 默认按毫秒处理 —— 如果你传入的是秒级时间戳(10 位),它会被当成“公元 1970 年后的毫秒数”,结果变成一个极小的日期(如 1970-01-01T00:00:01.717Z)。
实操建议:
- 秒级时间戳必须乘以 1000:
{ $toDate: { $multiply: ["$ts", 1000] } } - 不确定来源时,可用
$strLenCP判断长度:{ $cond: [{ $eq: [{ $strLenCP: { $toString: "$ts" } }, 10] }, { $multiply: ["$ts", 1000] }, "$ts"] },再喂给$toDate - 应用层写入时统一存毫秒级,比聚合时做判断更可靠
在 $group 或 $facet 中使用转换结果要注意什么
日期转换必须在引用前完成。不能在 $group 的 _id 或 $sum 表达式里直接嵌套 $toDate 后再取 $year 等字段 —— MongoDB 不允许在分组键中对表达式结果再做字段提取。
实操建议:
- 先用
$addFields提前转好日期字段,例如:{ $addFields: { eventDate: { $toDate: { $multiply: ["$ts", 1000] } } } } - 再在后续阶段用
$year、$month等操作这个新字段:{ $group: { _id: { year: { $year: "$eventDate" }, month: { $month: "$eventDate" } } } } - 如果跳过
$addFields直接写{ $year: { $toDate: ... } },会报错Expression $toDate is not supported(某些旧版本)或返回null
时区处理:$toDate 默认用 UTC,别被本地时间误导
$toDate 总是解析为 UTC 时间,不会读取系统时区或 shell 设置。如果你原始时间戳本就是东八区“北京时间”秒数(比如 1717027200 对应 2024-05-31 00:00:00 CST),直接转完仍是 2024-05-30 16:00:00 UTC —— 这不是 bug,是设计如此。
实操建议:
- 需要按本地时区分组?用
$dateToString配合timezone参数输出字符串,再用于分组:{ $dateToString: { date: "$utcDate", format: "%Y-%m", timezone: "Asia/Shanghai" } } - 不要试图用
$add手动加 8 小时(易错且无法处理夏令时) - 真正要存本地时间语义的数据,建议在写入时就转成对应时区的 UTC 时间戳(即把“北京时间 5 月 31 日 0 点”存为
1717027200,它本身就是 UTC+8 的等价表示)
最常被忽略的一点:时间戳字段类型是否真的是数值。用 db.collection.findOne() 看一眼 ts 的 BSON 类型,如果是 string 或 double,$toDate 行为会不同 —— double 可能带精度误差,string 必须先转整型,否则全为 null。

















