如何提升MongoDB+Node.js中传感器数据的查询速度?
我有一个存储传感器数据的集合,对应的MongoDB Schema如下:
const MainSchema: Schema = new Schema( { deviceId: { type: mongoose.Types.ObjectId, required: true, ref: 'Device', }, sensorId: { type: mongoose.Types.ObjectId, default: null, ref: 'Sensor', }, value: { type: Number, }, date: { type: Date, }, }, { versionKey: false, } );
该集合预计将存储超过30万条文档,我需要通过接口获取关联Sensor集合(包含名称、描述等信息)的数据。Sensor集合的Schema如下:
const Sensor: Schema = new Schema( { name: { type: String, required: true, min: 3, }, description: { type: String, default: null, }, type: { type: String, }, }, { timestamps: true, versionKey: false, } );
我采用两种方式查询MainSchema数据:
第一种是使用aggregate聚合查询(startDate、endDate和_sensorId为传入参数):
const data= await MainSchema.aggregate([ { $lookup: { from: 'Sensor', localField: 'sensorId', foreignField: '_id', as: 'sensorDetail', }, }, { $unwind: '$sensorDetail', }, { $match: { $and: [ { sensorId: new Types.ObjectId(_sensorId) }, { date: { $gte: new Date(startDate), $lt: new Date(endDate), }, }, ], }, }, { $project: { sensorDetail: { name: 1, description: 1, }, value: 1, date: 1, }, }, { $sort: { _id: 1, }, }, ]);
第二种是使用find+populate查询:
const data= await MainSchema.find({ sensorId: _sensorId, date: { $gte: new Date(startDate), $lte: new Date(endDate), }, }) .lean() .sort({ date: 1 }) .populate('sensorId', { name: 1, description: 1});
相同数据集下的执行耗时:
- 第一种方式:25-30秒
- 第二种方式:11-15秒
请问如何进一步提升查询速度?哪种方式是最佳实践?还有哪些额外的优化手段?
一、最佳实践选择
从测试结果来看,find+populate是当前更优的方案,原因如下:
- 你当前的aggregate查询顺序完全错误:先执行
$lookup和$unwind关联所有文档的Sensor数据,再过滤符合条件的记录,相当于做了全表关联后再筛选,完全浪费算力。而find+populate是先过滤出符合sensorId和date范围的主集合数据,再批量关联Sensor集合,操作顺序更高效。 - Mongoose的populate内部做了优化:它会收集查询到的所有
sensorId,执行一次$in查询去Sensor集合取数据,而非逐条查询,大幅减少IO次数。
如果一定要用aggregate,必须调整阶段顺序,把$match放在最前面,先过滤数据再关联,性能会大幅提升,调整后的聚合查询如下:
const data= await MainSchema.aggregate([ // 先过滤,缩小数据集 { $match: { sensorId: new Types.ObjectId(_sensorId), date: { $gte: new Date(startDate), $lt: new Date(endDate), }, }, }, // 再关联Sensor { $lookup: { from: 'Sensor', localField: 'sensorId', foreignField: '_id', as: 'sensorDetail', }, }, { $unwind: '$sensorDetail', }, { $project: { sensorDetail: { name: 1, description: 1, }, value: 1, date: 1, }, }, { $sort: { date: 1, }, }, ]);
调整后的aggregate性能会和find+populate接近,但find+populate写法更简洁,维护成本更低。
二、核心查询优化手段
1. 创建复合索引
这是提升查询速度最关键的一步。你的查询条件是sensorId+date,排序字段是date,所以创建复合索引:
// 在MainSchema上创建 MainSchema.index({ sensorId: 1, date: 1 });
这个索引可以直接覆盖查询过滤和排序需求,MongoDB无需扫描全表,直接通过索引定位符合条件的文档,能把查询耗时降到毫秒级。
Sensor集合的_id默认已有索引,关联时无需额外创建。
2. 统一查询范围逻辑
两种查询中date条件一个用$lt,一个用$lte,建议统一逻辑,比如统一用$lt: new Date(endDate),避免边界值重复或遗漏。
3. 精简返回数据
- 无论是aggregate的
$project还是populate的字段选择,只返回需要的字段(如name、description、value、date),减少数据传输量。 - 保留
lean():find().lean()返回普通JS对象,跳过Mongoose的文档初始化逻辑,提升性能。
三、额外优化手段
1. 数据分片(超大规模场景)
如果未来数据量超过千万级别,可对Main集合按sensorId或date分片,把数据分散到多个节点,提升查询和写入的并行处理能力。
2. 预聚合与冷数据归档
- 按时间维度(天/小时)对传感器数据做预聚合,生成统计值(平均值、最大值等),前端查询历史数据时优先返回预聚合结果,减少原始数据查询量。
- 将超过一定时间的冷数据归档到单独集合或低成本存储,只保留近期热数据在主集合,提升查询效率。
3. 连接池优化
调整Mongoose连接池参数,根据服务器资源设置合理连接数,避免因连接不足导致查询排队:
mongoose.connect(uri, { poolSize: 10, // 建议值为服务器CPU核心数*2 });
4. 查询结果分页
如果接口返回数据量较大,必须做分页处理,优先使用基于date的游标分页(比skip更高效):
const lastDate = req.query.lastDate; const pageSize = 100; const query = { sensorId: _sensorId, date: { $gte: new Date(startDate), $lt: new Date(endDate), ...(lastDate && { $gt: new Date(lastDate) }) } }; const data = await MainSchema.find(query) .lean() .sort({ date: 1 }) .limit(pageSize) .populate('sensorId', { name: 1, description: 1 });
内容的提问来源于stack exchange,提问作者hdevtr

