MongoDB旧文档聚合排序慢查询及跨库性能差异排查
问题背景
- 部署两套测试数据库DB1、DB2,单集合文档量级为数千至数十万,两套库Schema设计、CRUD逻辑完全一致,均通过Mongoose驱动操作
- 下述聚合查询在DB2执行耗时仅数毫秒,最长不超过10秒;在DB1执行耗时始终不低于40秒
- 已核对两库文档结构、索引配置完全一致,删除DB1对应集合并全量重写文档后,查询性能恢复至正常水平
const eventQueryPipeline = [ { $match: { $and: [{ userId: req.body.userId }, { serverId: req.body.serverId }], }, }, { $sort: { sort: -1, }, }, ]; const aggregation = db.collection .aggregate(eventQueryPipeline) .allowDiskUse(true); aggregation.exect((err, result) => { res.json(result); });
问题根因
这类场景是MongoDB非常典型的运维类问题,90%以上的触发原因集中在两点:
- WiredTiger存储层碎片+索引结构劣化
MongoDB默认WiredTiger存储引擎在经历高频增删改操作后,会产生数据块空洞、索引B树页分裂、无效索引节点残留问题。即使你看到的索引配置和DB2完全一致,DB1的索引实际物理结构已经存在大量不连续的碎片页,查询时需要扫描远多于正常状态的索引条目、跨磁盘块随机读取数据,直接拉高查询耗时。删除集合重导数据本质是重新生成了连续无碎片的数据文件和全新的索引B树,所以性能会直接恢复。 - 查询优化器缓存了低效执行计划
MongoDB的查询优化器会自动缓存命中的执行计划,如果某一时刻因为集合统计信息过期、临时写入特殊分布的数据,优化器错误选择了非最优执行路径(比如未命中userId+serverId+sort的最优复合索引,转而走单键索引扫描、甚至全表扫描+磁盘排序),这个低效计划会长期缓存在内存中被重复调用,不会自动失效。删除集合重建的操作会同步清空该集合的所有执行计划缓存,后续查询会重新生成最优执行计划。
另外你代码中开启的allowDiskUse(true)会放大这个问题:一旦优化器某次判断内存不足触发磁盘排序,后续缓存的执行计划会直接走磁盘临时文件排序路径,哪怕实际返回数据量完全可以在内存中完成排序,耗时会比内存排序高1~2个量级。
无需重建集合的修复&验证方案
- 第一步先在DB1的问题集合上执行
db.collection.getPlanCache().clear()清空集合的执行计划缓存,重新执行查询,如果耗时恢复正常,即可确认是坏执行计划缓存导致的问题 - 如果清缓存无效,在两个库上分别执行
db.collection.aggregate(eventQueryPipeline).explain("executionStats"),对比两个执行结果的totalKeysExamined(索引扫描条目数)、totalDocsExamined(文档扫描条目数)、SORT阶段的usedDisk标记:如果DB1的扫描条目数远高于DB2、且出现usedDisk: true标记,即可确认是索引碎片化问题,直接执行db.collection.reIndex()重建集合索引即可恢复性能,不需要全量重导数据 - 长期运维可以针对测试环境高频增删改的集合,定期执行compact操作清理存储碎片,避免同类问题反复出现
这类问题在测试环境极其常见,测试过程中反复造数、删数、改数的集合通常运行1~2周就可能触发,和业务代码、Schema设计、索引配置本身没有关系。
内容的提问来源于stack exchange,提问作者thomscoder
相关产品推荐
相关产品推荐

