MongoDB 的慢日志(Slow Query Log)对于运维和程序员来说都非常重要,因为它直接关系到数据库的性能和应用程序的稳定性。以下分享介绍下MongoDB慢日志查询及索引创建相关的一些笔记。
一,准备
1. 使用 db.currentOp()
实时监控
db.currentOp()
可以查看当前正在执行的操作,适合捕捉瞬时的高 CPU 操作。
db.currentOp()
示例:过滤长时间运行的操作
db.currentOp({ "secs_running": { "$gt": 1 } }) // 过滤运行时间超过 1 秒的操作
关键字段:
-
opid
:操作 ID。 -
op
:操作类型(如query
、update
、command
等)。 -
ns
:操作的命名空间(数据库和集合)。 -
secs_running
:操作已经运行的时间(秒)。 -
planSummary
:操作的执行计划摘要。 -
locks
:操作持有的锁信息。
2. 查询当前 profiling 状态
运行以下命令:
db.getProfilingStatus()
输出示例:
{"was": 1,"slowms": 100,"sampleRate": 1.0
}
-
was
:当前的 profiling 级别。-
0
:关闭 profiling。 -
1
:记录慢查询。 -
2
:记录所有查询。
-
-
slowms
:当前的慢查询阈值(单位:毫秒)。 -
sampleRate
:采样率(MongoDB 4.4+ 引入),表示记录慢查询的比例(默认为 1.0,即 100%)。
解释输出
slowms: 100
:表示当前慢查询阈值为 100 毫秒。任何执行时间超过 100 毫秒的查询都会被记录到 system.profile
集合中。
was: 1
:表示当前 profiling 级别为 1
,即只记录慢查询。
3. 修改慢查询阈值
如果你想修改慢查询阈值,可以使用 db.setProfilingLevel()
方法。例如,将阈值改为 200 毫秒:
db.setProfilingLevel(1, { slowms: 200 })
这代表会记录执行时间超过200毫秒的语句。
然后让 MongoDB 运行一段时间,以便收集足够的慢查询数据。这段时间可以根据你的系统负载和查询频率来决定。
4. 查询 profiling 级别
如果你只想查询当前的 profiling 级别(而不关心阈值),可以使用以下命令:
db.getProfilingLevel()
输出示例:
1
-
0
:关闭 profiling。 -
1
:记录慢查询。 -
2
:记录所有查询。
二、查询慢SQL
1.查询语句
db.system.profile.find().sort({ ts: -1 }).limit(10)
2.过滤 millis
大于 3000 的查询
运行以下命令
db.system.profile.find({ millis: { $gt: 3000 } }).sort({ ts: -1 })
-
{ millis: { $gt: 3000 } }
:筛选millis
大于 3000 的文档。 -
.sort({ ts: -1 })
:按时间戳倒序排列,确保最新的查询排在最前面。
3. 进一步筛选
如果你只想查看特定类型的操作(如 query
或 aggregate
),可以在查询中添加额外的过滤条件。例如:
-
只查看
query
操作:db.system.profile.find({ op: "query", millis: { $gt: 3000 } }).sort({ ts: -1 })
- 只查看
aggregate
操作:
db.system.profile.find({ op: "command", "command.aggregate": { $exists: true }, millis: { $gt: 3000 } }).sort({ ts: -1 })
4. 限制返回结果数量
如果结果较多,可以使用 limit
限制返回的文档数量。例如,只返回前 10 条:
db.system.profile.find({ millis: { $gt: 3000 } }).sort({ ts: -1 }).limit(10)
5.查询指定日期时间的记录
假设你想查询 2023-10-01T12:00:00Z
之后的记录,可以使用以下命令:
db.system.profile.find({ts: { $gt: ISODate("2023-10-01T12:00:00Z") }
}).sort({ ts: -1 }).limit(10)
如果你希望查询某个特定日期时间范围内的记录,可以结合 $gt
和 $lt
操作符。
示例:查询 2023-10-01T12:00:00Z
到 2023-10-02T12:00:00Z
之间的记录
db.system.profile.find({ts: {$gt: ISODate("2023-10-01T12:00:00Z"),$lt: ISODate("2023-10-02T12:00:00Z")}
}).sort({ ts: -1 }).limit(10);
6.慢查询日志的字段说明
db.system.profile.find()
返回的文档中,以下字段最为关键:
字段名 | 说明 |
---|---|
op | 操作类型(如 query 、update 、insert 等)。 |
ns | 操作的命名空间(格式为 数据库名.集合名 )。 |
command | 具体的查询或命令内容(如 find 、aggregate 等)。 |
millis | 查询执行时间(单位:毫秒)。 |
planSummary | 查询的执行计划摘要(如使用的索引)。 |
ts | 查询执行的时间戳。 |
keysExamined | 扫描的索引键数量。 |
docsExamined | 扫描的文档数量。 |
nreturned | 返回的文档数量。 |
locks | 锁信息(如锁类型和持有时间)。 |
7. 注意事项
-
system.profile
集合的大小:system.profile
是一个 capped collection(固定大小集合),默认大小较小。如果需要更长时间的日志,可以调整其大小:db.setProfilingLevel(0); // 先关闭 profiling db.system.profile.drop(); db.createCollection("system.profile", { capped: true, size: 1048576 }); // 设置新的大小(单位:字节) db.setProfilingLevel(1, { slowms: 100 }); // 重新启用 profiling
-
性能影响:启用 profiling 会对性能有一定影响,建议在生产环境中谨慎使用。
三、创建索引
1.创建
针对以上捞出来的慢SQL语句,可以添加相应的索引
db.mytable.createIndex({ field1: 1, field2: 1 })
值1是对field1字段创建升序索引。
2.验证
创建索引后,可以使用 explain
方法验证查询是否使用了新创建的索引。
db.mytable.find({ field1: 'abc', field2: '2025-03-19' }
).limit(1).explain("executionStats")
检查输出中的以下字段:
-
winningPlan.inputStage.indexName
:确认是否使用了新创建的索引。 -
executionStats.totalDocsExamined
:检查扫描的文档数量是否显著减少。 -
executionStats.executionTimeMillis
:检查查询执行时间是否减少。
3.索引优化建议
-
索引顺序:复合索引的顺序非常重要。如果查询条件中 field1是等值查询,而 field2是范围查询,那么将 field11放在前面会更高效。
-
索引选择性:选择性高的字段(即唯一值多的字段)放在索引前面。例如,如果 field1的唯一值比 field2多,则将 field1放在前面。
-
覆盖索引:如果查询只需要返回索引字段,可以创建一个覆盖索引,避免回表查询。例如:
db.mytable.createIndex({ field1: 1, field2: 1, other_field: 1 })
4.其他优化建议
-
数据分布:检查
field1
和field2
的数据分布。如果某个字段的值非常集中(如field2
总是相同),索引的效果可能不明显。 -
查询模式:如果查询模式固定(如总是查询
field1
和field2
),复合索引是最佳选择。如果查询模式多变,可能需要创建多个索引。 -
索引大小:索引会占用存储空间,并影响写入性能。确保索引的收益大于其成本。