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

