如何优化MongoDB查询以提升API响应速度?
核心问题点
不区分大小写的正则查询无法利用普通索引
你使用了new RegExp(params.search, 'i')这类无前缀限制的不区分大小写正则,MongoDB的普通单字段索引无法被这类查询命中,会触发全集合扫描,这是慢查询的主要诱因。$or + $and的组合查询导致索引失效
MongoDB对$or的索引支持要求每个分支都有对应索引,但当$or与$and组合、且$or包含大量字段时,查询优化器很难选择最优索引,大概率会放弃索引走全表扫描。skip分页的性能损耗
skip()会让数据库先遍历跳过指定数量的文档,即便只有3000条数据,当offset较大时,这个操作的耗时也会明显增加。重复查询的额外开销
countDocuments()和find()是两次独立查询,相当于扫描了两次集合,额外增加了IO消耗。
具体优化步骤
1. 替换正则查询为文本索引查询
将需要模糊搜索的字段统一创建文本索引,用MongoDB原生文本查询替代正则,既能支持不区分大小写的模糊搜索,又能高效利用索引:
// 先在MongoDB中创建文本索引(仅需执行一次) db.supplierProfiles.createIndex({ refNo: "text", businessName: "text", tradingName: "text", "agentTypes.name": "text", "departments.name": "text", "locality.name": "text", "type.name": "text", "status.name": "text", "factories.name": "text", "externalReferences.source": "text", "externalReferences.id": "text", "classification.name": "text", "incoterms.name": "text", "addresses.country.name": "text" }) // 代码中替换$or正则逻辑为文本查询 if (params.search) { searchQuery.$text = { $search: params.search }; }
文本查询默认支持不区分大小写,且匹配效率远高于无限制正则。
2. 优化过滤条件的索引配置
对于status.id、agentTypes.id这类精确匹配的嵌套字段,确保创建对应单字段索引:
db.supplierProfiles.createIndex({ "status.id": 1 }) db.supplierProfiles.createIndex({ "agentTypes.id": 1 }) // 其他过滤字段同理创建单字段索引
如果经常同时使用多个过滤条件,可创建复合索引提升组合查询效率,比如:
db.supplierProfiles.createIndex({ "status.id": 1, createdAt: -1 })
3. 替换skip分页为基于唯一键的分页
利用你排序用的refNo(需保证唯一)替代skip,避免遍历跳过大量文档:
// 假设前端传递上一页最后一条的refNo:lastRefNo let query = {...searchQuery}; if (params.lastRefNo) { query.refNo = { $gt: params.lastRefNo }; // 对应排序规则refNo:1 } // 查询时移除skip,直接用query过滤 profiles = await SModel.find(query) .limit(parseInt(this.queryLimit, 10)) .lean() .sort({ refNo: 1 })
这种方式完全依赖索引,无论offset多大,性能都保持稳定。
4. 优化计数查询
如果不需要精确的总条数,可用estimatedDocumentCount()替代countDocuments(),它直接读取集合元数据,速度极快:
const count = Object.keys(searchQuery).length ? await SModel.countDocuments(searchQuery).lean() : await SModel.estimatedDocumentCount().lean()
5. 只返回必要字段
在find()中指定需要的字段,减少数据传输和序列化开销:
profiles = await SModel.find(query, { refNo:1, businessName:1, tradingName:1 /* 其他业务需要的字段 */ }) .limit(parseInt(this.queryLimit, 10)) .lean() .sort({ refNo: 1 })
6. 验证索引命中情况
用explain()查看查询执行计划,确认索引是否被正确使用:
await SModel.find(searchQuery).explain("executionStats")
若executionStats.totalDocsExamined等于集合总数(3000),说明仍在走全表扫描,需要调整索引或查询逻辑。
额外建议
- 若
agentTypes是数组类型,确保针对数组元素的索引已正确创建,MongoDB会自动处理数组索引,但需确认查询逻辑匹配索引规则。 - 定期在业务低峰期执行
db.collection.reIndex()重建索引,清理索引碎片。
内容的提问来源于stack exchange,提问作者Vijay Prajapati

