You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose查询极慢但Mongo Shell查询飞快的问题排查

问题:Mongoose查询耗时远超Mongo Shell的原因及解决

背景信息

Device集合Schema

@Schema({ timestamps: true })
export class Device {
  _id: Types.ObjectId;

  id: string;

  @Prop()
  deviceId: string;

  @Prop()
  name: string;

  @Prop()
  type: DeviceType;

  @Prop({ type: [Types.ObjectId], ref: Group.name, autopopulate: true })
  groups: Group[];

  // 省略其他属性
}

查询需求及语句

查询属于指定Group集合且type为"LAMP"的所有Device,Mongo Shell中的查询语句:

db.devices.find({"$and":[{"groups":{"$in":["615d64780ca1a1003ec38736","615e4e513fd5450033259086","615e4e593fd54500332590bc","615e4e683fd5450033259102","615e4e703fd545003325915a","615e4e833fd54500332591c4","61e917f9eee5c1d205a5f195","64d5f18ed5c1e5922047da7c","64d5f24bd5c1e5922047dc8d","64d5f252d5c1e5922047dcf1","64d5f25ad5c1e5922047dd5c","64d5f27dd5c1e5922047decb","64d5f284d5c1e5922047df5a","64d5f28bd5c1e5922047dfe8","64d5f705d5c1e5922048c453","64d5f71ed5c1e5922048c4e7","64d5f72ed5c1e5922048c58a","64d5f74bd5c1e5922048c640","64d5f75cd5c1e5922048c6e9","64d5f781d5c1e5922048c7b5","64d5f79cd5c1e5922048c919","64d5f7a7d5c1e5922048c9df"]}},{"type":"LAMP"}]} { strictQuery: false }

异常现象

Mongo Shell中使用explain("executionStats")分析显示查询仅耗时1ms,且命中type_1索引,执行高效;但NestJS中使用Mongoose执行相同查询耗时约5-10秒。


可能的原因及解决方法

1. 自动填充(autopopulate)的额外开销

Schema中groups字段设置了autopopulate: true,Mongoose会在查询Device后自动去关联的Group集合查询匹配的分组数据。如果查询返回的Device数量多,这一步会产生大量额外的关联查询,大幅增加整体耗时。而Mongo Shell的查询仅返回Device原始文档,不会执行自动填充。

解决方法:
如果不需要返回Group的详细数据,查询时显式禁用自动填充:

// 仅填充group的_id,或完全不填充
this.deviceModel.find(query).populate({ path: 'groups', select: '_id', autopopulate: false }).exec();

// 完全跳过填充
this.deviceModel.find(query).exec();

2. Mongoose文档序列化的性能开销

Mongoose会将Mongo返回的BSON文档转换为Mongoose Document实例,这个过程包含验证、钩子(hooks)、属性转换等操作。若返回结果集较大,这些操作的累计开销会非常明显。而Mongo Shell直接返回原始BSON,无额外处理步骤。

解决方法:
使用.lean()跳过Document实例的创建,直接返回普通JavaScript对象:

this.deviceModel.find(query).lean().exec();

3. 索引使用不一致

虽然Shell中查询命中了type_1索引,但需确认Mongoose查询是否使用了相同索引。可能存在数据类型转换差异(比如传入的Group ID是字符串而非Types.ObjectId),导致索引未被命中。

解决方法:
在Mongoose中添加.explain('executionStats')检查执行计划:

this.deviceModel.find(query).explain('executionStats').then(plan => console.log(plan));

对比Shell中的执行计划,确认索引命中情况。若未命中,手动将Group ID转换为Types.ObjectId:

const groupIds = groupIdStrings.map(id => new Types.ObjectId(id));
const query = { groups: { $in: groupIds }, type: 'LAMP' };

4. 复合索引缺失

当前查询同时使用type和groups条件,仅靠type_1索引可能不是最优解。创建{ type: 1, groups: 1 }的复合索引,能让Mongo直接通过索引过滤两个条件,进一步提升查询效率。

解决方法:
在Schema中定义复合索引,或手动创建:

// 方式1:在Schema中添加
@Schema({ timestamps: true, indexes: [{ fields: { type: 1, groups: 1 } }] })
export class Device {
  // ... 字段定义
}

// 方式2:手动创建索引
this.deviceModel.createIndex({ type: 1, groups: 1 });

内容的提问来源于stack exchange,提问作者James Fu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 00:48:10