$nor查询必然触发全表扫描,因其无法使用任何索引;替代方案包括用$and+$ne组合(需为各字段单独建索引)或改写为$in取反(配合覆盖索引更优)。

$nor 查询几乎必然触发全表扫描,根本原因在于它无法使用任何索引 —— 无论字段上有没有索引,MongoDB 都会放弃索引走 COLLSCAN。
为什么 $nor 不能走索引
MongoDB 的 B-tree 索引只支持“正向匹配”(如 $eq、$in、$gt)或“范围跳转”,而 $nor 是对多个条件的否定合取,逻辑上等价于 {$not: {$or: [...]}}。B-tree 无法高效表达“所有不满足 A 且不满足 B”的路径,引擎只能退化为逐文档检查。
- 即使你为
status字段建了索引,db.orders.find({$nor: [{status: "pending"}, {status: "cancelled"}]})仍会显示"stage": "COLLSCAN"在explain("executionStats")中 -
$nor内部每个子条件即使单独能走索引,组合后也全部失效 - 官方文档明确标注:
$nor不支持索引 —— 这不是 bug,是设计限制
用 $and + $ne 替代 $nor 的实操要点
把 $nor[{a:1}, {b:2}] 转成 {$and: [{a: {$ne: 1}}, {b: {$ne: 2}}]},虽然语义不完全等价(前者要求同时不满足两个条件,后者是各自独立否定),但在多数业务场景中更贴近真实意图,且关键是可以走索引。
- 必须确保每个
$ne字段都已建立索引,否则单个$ne本身也会退化为 COLLSCAN - 复合索引无效:不要试图建
{a: 1, b: 1}来加速{$and: [{a: {$ne: 1}}, {b: {$ne: 2}}]}—— MongoDB 不会用这个索引,因为$ne不参与 ESR 规则中的 “Equality” 阶段 - 真正有效的做法是:为
a单独建{a: 1},为b单独建{b: 1},让查询能分别命中两个索引(MongoDB 4.2+ 支持索引合并) - 验证方式:
db.orders.find({a: {$ne: 1}, b: {$ne: 2}}).explain("queryPlanner")查看"indexBounds"是否非空
更推荐的替代方案:用 $in 取反 + 覆盖索引
如果被排除的值集合固定且较小(比如状态枚举只有 5 个值,你只想排除其中 2 个),直接查“属于其余值”比查“不属于这 2 个”更高效:
- 原写法:
{$nor: [{status: "pending"}, {status: "cancelled"}]}→ 全表扫 - 改写为:
{status: {$in: ["completed", "shipped", "refunded"]}}→ 只要status有索引,就能走IXSCAN - 进一步优化:加上投影
{_id: 0, status: 1, orderDate: 1},配合db.orders.createIndex({status: 1, orderDate: 1})实现覆盖索引,避免回表 - 注意:
$in值列表长度建议控制在几百以内;超过时性能会下降,此时应考虑预计算或物化视图
容易被忽略的陷阱
很多人以为加了索引就万事大吉,但实际线上慢查询往往卡在这几个细节上:
-
$nor和$not混用时(如{$not: {$nor: [...]}}),逻辑双重否定 ≠ 简化,执行计划依然 COLLSCAN - 聚合管道里写
{$nor: [...]}在$match阶段,同样无法利用索引 —— 必须前置改写 - 使用
allowDiskUse: true对$nor查询毫无帮助,它只解决内存溢出,不解决扫描方式 - 测试时数据量小(











