在MongoDB的业务统计场景中,计算两个日期的小时差并按向下取整规则处理是常见需求,比如统计用户在线时长、订单处理耗时等场景都需要这类计算。如果直接使用简单的除法取整,很容易因为毫秒数的精度问题得到错误结果。

基础实现思路
MongoDB中日期类型存储的是从Unix纪元(1970-01-01 00:00:00 UTC)开始的毫秒数,两个日期相减得到的是毫秒差。1小时等于3600000毫秒,因此小时差的计算核心是先将毫秒差转换为小时数,再进行向下取整操作。
聚合管道中可以使用$subtract表达式计算两个日期的差值,再用$divide除以3600000得到小时数,最后用$floor表达式完成向下取整。
简单场景示例
假设集合中有startTime和endTime两个日期字段,计算两者小时差并向下取整的聚合管道如下:
db.order.aggregate([
{
$project: {
// 计算两个日期的毫秒差
millisDiff: { $subtract: ["$endTime", "$startTime"] },
// 毫秒差转小时数
hourDiffRaw: { $divide: ["$millisDiff", 3600000] },
// 向下取整得到最终小时差
hourDiffFloor: { $floor: "$hourDiffRaw" }
}
}
])
边界场景处理
实际业务中可能会遇到日期顺序颠倒、日期为null等边界情况,需要提前做校验避免计算出错。
日期顺序校验
如果endTime早于startTime,毫秒差会是负数,向下取整的结果也会是负数。如果业务要求小时差最小为0,可以结合$max表达式处理:
db.order.aggregate([
{
$project: {
millisDiff: { $subtract: ["$endTime", "$startTime"] },
hourDiffRaw: { $divide: ["$millisDiff", 3600000] },
// 小时差最小取0
hourDiffFloor: { $max: [{ $floor: "$hourDiffRaw" }, 0] }
}
}
])
空值处理
如果日期字段可能存在null值,可以用$ifNull设置默认值,避免计算报错:
db.order.aggregate([
{
$project: {
// 日期为null时默认使用当前时间
validStart: { $ifNull: ["$startTime", new Date()] },
validEnd: { $ifNull: ["$endTime", new Date()] },
millisDiff: { $subtract: ["$validEnd", "$validStart"] },
hourDiffFloor: { $floor: { $divide: ["$millisDiff", 3600000] } }
}
}
])
多阶段聚合中的复用
如果需要在多个聚合阶段使用小时差结果,可以用$addFields阶段先计算出字段,后续阶段直接引用即可,避免重复计算:
db.order.aggregate([
{
$addFields: {
hourDiff: {
$floor: {
$divide: [{ $subtract: ["$endTime", "$startTime"] }, 3600000]
}
}
}
},
{
// 后续按小时差分组统计
$group: {
_id: "$hourDiff",
orderCount: { $sum: 1 }
}
}
])
常见误区说明
很多开发者会尝试用$trunc代替$floor,但$trunc对于负数的处理是向0取整,而$floor是向负无穷取整,两者在负数场景下结果不同,需要根据业务需求选择。如果业务要求小时差负数也向下取整,必须用$floor。
另外不要忽略时区问题,MongoDB存储的日期是UTC时间,如果业务使用的是本地时区,计算前需要先将日期转换为对应时区的时间,否则会出现小时差偏差。可以用$dateAdd或者$dateSubtract调整时区偏移后再计算。