MongoDB中用$expr判断“最近”时间需手动计算边界,如{ $expr: { $gt: ["$updatedAt", { $subtract: ["$$NOW", 3600000] }] } },且仅单字段时间比较可走索引,混用普通条件或复杂表达式将失效索引。

用 $expr 在 find() 里比较时间字段是否“最近”
直接说结论:MongoDB 没有内置的“最近 N 分钟/小时”语法,必须靠 $expr + 时间操作符(如 $gt、$subtract)手动算时间边界。它不是语法糖,而是把时间计算逻辑从应用层搬到查询层的关键开关。
常见错误是写成 { updatedAt: { $gt: new Date(Date.now() - 60 * 60 * 1000) } } —— 这根本没用 $expr,只是 JS 构造了时间对象传给驱动,和 $expr 无关。真正要用 $expr,就得让 MongoDB 自己算“现在减一小时”。
-
$expr只接受聚合表达式,不能直接写 JS 或字面量时间 - 必须用
$now获取服务器当前时间戳(毫秒级),再用$subtract减去毫秒数 - 注意字段类型:确保
updatedAt是Date类型,不是字符串或数字,否则比较结果恒为false - 示例:查过去 1 小时内更新的文档:
{ $expr: { $gt: ["$updatedAt", { $subtract: ["$$NOW", 3600000] }] } }
$expr 和普通查询条件混用的坑
很多人以为加了 $expr 就能随便混写,其实不行。它本质是聚合上下文,和普通查询条件(如 { status: "active" })属于不同执行路径,混用时容易漏索引、性能骤降,甚至语义错乱。
典型错误是写成 { status: "active", $expr: { $gt: ["$updatedAt", ...] } } —— 看似合理,但 MongoDB 会先走 status 索引过滤,再对结果逐条跑 $expr 计算,无法利用 updatedAt 索引加速时间判断。
- 想高效查“状态为 active 且最近更新”,应把所有条件都塞进
$expr,用$and包裹:{ $expr: { $and: [ { $eq: ["$status", "active"] }, { $gt: ["$updatedAt", { $subtract: ["$$NOW", 3600000] }] } ] } } - 但这样就完全放弃索引——
$expr内部字段无法走普通 B-tree 索引(除非建带$expr的索引,见下一条) - 更实际的做法:先用普通条件(如
status)缩小范围,再在应用层或后续聚合阶段用$expr做精细时间筛选
给 $expr 时间比较建索引的唯一办法
MongoDB 对 $expr 查询默认不走索引,但有一个例外:用 createIndex() 显式声明一个“支持表达式的索引”,也就是带 $** 或字段路径 + 表达式类型的索引。不过目前(v7.0)只支持一种方式:对时间字段建普通索引,再配合 $expr 中的 $gt/$lt 单边比较,MongoDB 才可能命中索引。
- 必须是对单个时间字段的简单比较(如
$gt、$lt),不能是$and多条件或$subtract后再比 - 索引要建在被比较的字段上:
db.collection.createIndex({ updatedAt: 1 }) - 查询必须是纯
$expr+ 单字段时间比较,例如:{ $expr: { $gt: ["$updatedAt", { $subtract: ["$$NOW", 3600000] }] } },此时可走updatedAt索引 - 一旦加上其他字段条件(哪怕只是
{ status: "active" }),索引就大概率失效
为什么不用 $dateFromParts 或 $isoDayOfWeek 算“最近”
有人想用日期解析类操作符(比如取今天是周几、拼出昨天的日期)来定义“最近”,这在逻辑上可行,但实际几乎没人这么做——因为太重、太慢、还容易出错。
$dateFromParts 要求你手动拆年月日,还得处理时区;$isoDayOfWeek 返回的是数字,没法直接和 Date 字段比大小;而且所有这些操作都在每次查询时实时计算,无法缓存,CPU 开销远高于直接减毫秒数。
- “最近”本质是时间差判断,不是日期结构分析,优先用
$subtract+$now - 如果真要按“最近 7 天”“本周一以来”这种业务语义查,建议在写入时就存一个
weekStartAt字段,查的时候用普通等值或范围查询,更快更稳 -
$expr不是万能胶,别为了炫技把简单问题搞复杂
时间计算在服务端做还是客户端做,得看延迟容忍度和数据一致性要求。MongoDB 的 $$NOW 是服务器时间,如果应用部署在多个时区,而你又依赖“用户本地最近”,那 $expr 就不是解法——得把时间边界算好传进去。

















