MongoDB慢查询日志是定位数据库性能瓶颈最直接的数据来源。当应用出现偶发性超时、CPU占用异常升高或者磁盘IO排队时,单纯靠业务代码日志很难判断是不是某条聚合语句在全表扫描。MongoDB从设计上提供了多层次的查询监控能力,既包括全局的慢查询日志(对应operationProfiling配置),也包括会话级的profile采集,以及单条语句的explain执行计划。理解这三层机制的关系,是做优化的前提。

慢查询日志的开启与参数配置
在MongoDB中,慢查询的采集并不是默认全量打开的。最基础的方式是通过配置文件或命令行设置operationProfiling.mode与operationProfiling.slowOpThresholdMs。slowOpThresholdMs定义了什么算慢,默认是100毫秒,也就是说执行超过100毫秒的操作才会被记录到日志文件。对于高并发的互联网服务,这个默认值往往偏高,可以下调到20或者50毫秒,以便捕捉潜在隐患。
除了全局日志,MongoDB还提供数据库级别的profiler,通过db.setProfilingLevel控制。级别0表示关闭,1表示只记录慢操作,2表示记录所有操作。生产环境一般设为1,并配合自定义阈值。下面的代码展示了如何在mongo shell中动态开启并设定阈值为30毫秒:
// 开启当前库的慢查询采集,阈值30毫秒
db.setProfilingLevel(1, { slowms: 30 })
// 查看当前profiling配置
db.getProfilingStatus()
// 关闭profiling
db.setProfilingLevel(0)
开启之后,慢操作会写入system.profile集合,而不是普通的文本日志。这个集合是一个固定大小的capped collection,默认几兆字节,循环覆盖。如果你的实例写入量极大,可能需要通过db.setProfilingLevel配合profile集合的size参数调整容量,否则老数据很快被冲掉,导致排查时查不到历史慢语句。
和配置文件方式相比,profiler的优势在于可以用标准查询语句去检索。比如按耗时倒序查看最近十条慢操作,或者只筛某张集合的更新语句。这种结构化能力比grep日志文件高效得多,也方便接入监控平台做聚合统计。
从日志字段解读性能根因
一条典型的慢查询记录包含很多关键字段,其中ns表示命名空间,op是操作类型,millis是耗时,nReturned是返回文档数,keysExamined是索引扫描数,docsExamined是文档扫描数。判断一条语句健不健康,最核心的对比就是keysExamined和docsExamined是否接近nReturned。
如果docsExamined远大于nReturned,说明MongoDB读了很多无用文档,基本可以确定没用上索引或者索引区分度差。例如一次查询返回5条却扫描了五万条,那就是典型的全集合扫描。此时应该结合planSummary字段看执行计划是不是COLLSCAN。下面的示例展示如何从system.profile中筛出扫描文档数异常高的语句:
// 查询扫描文档数超过10000的慢操作
db.system.profile.find({
docsExamined: { $gt: 10000 }
}).sort({ millis: -1 }).limit(20)
// 查看某集合最耗时的前十操作
db.system.profile.find({
ns: "shop.orders"
}).sort({ millis: -1 }).limit(10)
另一个容易忽视的字段是locks和waitingForLock。当写操作频繁时,读查询可能长时间拿不到锁,日志里会体现等待时间。这种慢并非语句本身差,而是并发模型问题,可能需要读写分离或者调整写批量大小。很多团队只看millis就断定加索引,结果加了也没用,就是因为没有综合看锁与扫描字段。
对于聚合管道(aggregation pipeline),慢日志还会记录每一个stage的耗时。如果发现$match之后$group极慢,通常是因为$match没下推到索引,导致先在内存里灌入大量数据再分组。此时优化方向是在管道最前面放能走索引的等值或范围条件,减少进入内存的数据量。
基于explain的索引优化实战
光看慢日志只能发现问题,真正解决要靠explain。对可疑查询执行explain("executionStats"),可以拿到完整执行统计。重点关注executionStages.stage是不是IXSCAN,以及totalKeysExamined与totalDocsExamined的比例。若是COLLSCAN,直接建索引;若是IXSCAN但依旧慢,可能是索引顺序与排序不匹配,触发了内存排序。
复合索引的设计遵循ESR原则:等值(Equality)字段在前,排序(Sort)字段居中,范围(Range)字段在后。例如查询status=1且createdAt>某时间并按amount降序,合理索引应是{status:1, amount:-1, createdAt:1}。下面代码演示如何创建并验证:
// 创建复合索引
db.orders.createIndex({ status: 1, amount: -1, createdAt: 1 })
// 执行explain查看是否走索引
db.orders.explain("executionStats").find({
status: 1,
createdAt: { $gt: ISODate("2023-01-01") }
}).sort({ amount: -1 })
有时候索引建了却没生效,原因可能是查询里对字段用了函数,比如where或者$where,以及类型不一致。MongoDB是大小写敏感且类型敏感的,如果集合里userId存的是字符串,而查询传的是数字,索引直接失效。慢日志里往往看不出类型问题,必须靠explain的inputStage细节和schema核对。
最后要提的是部分索引(partial index)和覆盖查询(covered query)。如果业务只关心特定状态的记录,用partial index能大幅减少索引体积并提升写入速度;如果查询只需要索引字段不需要取文档,MongoDB可以直接从索引返回,连docsExamined都为0。这类优化在日志里表现为nReturned等于keysExamined且docsExamined为0,是性能最优态。
持续监控与治理策略
慢查询优化不是一次性工作。业务迭代会使查询模式变化,原先的高效索引可能变成冗余拖累写入。建议将system.profile的采样定期同步到时序数据库,用看板观察P95耗时趋势。同时设置告警:当某集合的docsExamined/nReturned中位数超过50,自动通知负责人。
在DevOps流程中,可以把explain检查做成上线前卡点。比如通过脚本在预发环境跑核心查询,若发现COLLSCAN直接阻断发布。这样能把大多数性能事故消灭在测试期。对于已经上线的老系统,则可以先用慢日志反向推导缺失索引,用db.advisory类工具或手动分析,分批补建,避免一次性大建索引引发锁表。
此外,MongoDB的hint可以强制走某个索引,在紧急止血时很有用,但长期依赖hint说明统计信息或索引设计有问题,应作为临时手段。真正健康的系统,是查询自然匹配到最优索引,慢日志里几乎看不到百毫秒级操作,整体数据库CPU平稳,业务侧超时率趋近于零。
MongoDBslow_query_logquery_optimization修改时间:2026-08-13 15:21:39