一、MongoDB优化整体思路
MongoDB的查询语句优化与关系型数据库类似,简单来说就是通过慢查询日志找出慢查询语句,然后通过执行计划进行分析,最后根据实际情况进行优化。
二、慢查询日志分析
2.1 简介
在MongoDB中,慢查询日志被叫做Profiler,我们可以通过设置Profiler来记录慢查询语句;然后就可以根据慢查询日志中的内容进行优化分析了。
MongoDB的慢查询日志记录在system.profile这个集合中,默认情况下慢查询日志是关闭的,可以在数据库级别上或者是节点级别上进行配置。
2.2 慢查询日志的使用
MongoDB有两种方式可以对慢查询日志进行配置:
1、直接在启动参数或者配置文件里进行设置:
#传统配置文件格式
profile = 1
slowms = 100 #默认为100ms
#YAML配置文件格式
operationProfiling:
mode: <string> # 默认为 off,可选值 off、slowOp、all
slowOpThresholdMs: <int> # 阈值,默认值为100ms
slowOpSampleRate: <double> #随机采集慢查询的百分比值,sampleRate值默认为1,表示都采集,0.5 表示采集50%的内容
2、通过db.setProfilingLevel(level,slowms) 语句设置慢查询级别和阈值;
级别 | 含义 | |
---|---|---|
0 | 关闭慢查询 | |
1 | 只记录超过阈值的查询 | |
2 | 记录所有查询 |
MongoDB可以通过db.getProfilingStatus()获取慢查询日志的相关配置信息
# 指定数据库,并指定阈值慢查询 ,超过10毫秒的查询被记录
use test
db.setProfilingLevel(1, { slowms: 10 })
# 随机采集慢查询的百分比值,sampleRate 值默认为1,表示都采集,0.5 表示采集50%的内容。
db.setProfilingLevel(1, { sampleRate: 0.5 })
#查询大于等于5ms的时间
db.system.profile.find( { millis : { $gt : 5 } } )
# 查询最近的5个慢查询日志
db.system.profile.find().limit(5).sort( { ts : -1 } )
# 查询除命令类型为'command'的日志
db.system.profile.find( { op: { $ne : 'command' } } )
# 查询数据库为test集合为t1的日志
db.system.profile.find( { ns : 'test.t1' } )
# 查询时间从 2019-09-01 0点到 2012-10-01 0点之间的日志
db.system.profile.find({
ts : {
$gt: new ISODate("2019-09-01T00:00:00Z"),
$lt: new ISODate("2019-10-01T00:00:00Z")
}
})
2.3 慢查询日志内容详解
{
"op" : "query",
"ns" : "test.data",
"command" : { "find" : "data", "filter" : { "id" : { "$gt" : 2, "$lt" : 10 }, "name" : "ZJ" }, "lsid" : { "id" : UUID("2fc9dd75-6731-4426-b0ba-38e1a5d7317e") }, "$db" : "test" },
"keysExamined" : 0,
"docsExamined" : 1000000,
"cursorExhausted" : true,
"numYield" : 7812,
"nreturned" : 7,
"queryHash" : "17295960",
"planCacheKey" : "17295960",
"locks" : { "ParallelBatchWriterMode" : { "acquireCount" : { "r" : NumberLong(1) } }, "ReplicationStateTransition" : { "acquireCount" : { "w" : NumberLong(7814) } }, "Global" : { "acquireCount" : { "r" : NumberLong(7814) } }, "Database" : { "acquireCount" : { "r" : NumberLong(7813) } }, "Collection" : { "acquireCount" : { "r" : NumberLong(7813) } }, "Mutex" : { "acquireCount" : { "r" : NumberLong(1) } } },
"flowControl" : { },
"storage" : { },
"responseLength" : 546,
"protocol" : "op_msg",
"millis" : 623,
"planSummary" : "COLLSCAN",
"execStats" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ { "name" : { "$eq" : "ZJ" } }, { "id" : { "$lt" : 10 } }, { "id" : { "$gt" : 2 } } ] }, "nReturned" : 7, "executionTimeMillisEstimate" : 2, "works" : 1000002, "advanced" : 7, "needTime" : 999994, "needYield" : 0, "saveState" : 7812, "restoreState" : 7812, "isEOF" : 1, "direction" : "forward", "docsExamined" : 1000000 },
"ts" : ISODate("2019-10-24T04:00:54.221Z"),
"client" : "127.0.0.1",
"appName" : "MongoDB Shell",
"allUsers" : [ ], "user" : ""
}
上面是我截取慢查询日志中的一个查询语句,其中可以把它拆解为几个部分来理解:
- "op"
该项表明该慢日志的种类,主要包含如下几类:
insert
query
update
remove
getmore
command
- "ns"
该项表明该慢日志是哪个库下的哪个集合所对应的慢日志
- "command"
该项详细输出了慢日志的具体语句和行为
- "keysExamined"
该项表明为了找出最终结果MongoDB搜索了多少个key
- "docsExamined"
该项表明为了找出最终结果MongoDB搜索了多少个文档
- "numYield"
为了让别的操作完成而屈服的次数,一般发生在需要访问的数据尚未被完全读取到内存中,MongoDB会优先完成在内存中的操作
- "nreturned"
该项表明返回的记录数
- "locks"
该项表明在操作中产生的锁的相关信息
- 锁的种类:
级别 | 含义 | |
---|---|---|
Global | 全局锁 | |
MMAPV1Journal | MMAPV1同步日志时加的一种锁 | |
Database | 数据库锁 | |
Collection | 集合锁 | |
Metadata | 元数据锁 | |
oplog | oplog锁 |
- 锁的模式:
级别 | 含义 | |
---|---|---|
R | 共享S锁 | |
W | 排他X锁 | |
r | 意向共享IS锁 | |
w | 意向排他IX |
- "responseLength"
结果返回的大小,单位为bytes,该值如果过大,则需考虑limit()等方式减少输出结果
- "millis"
该操作从开始到结束耗时多少,单位为ms
- "execStats"
包含了该操作执行的详细信息
- "ts"
该操作执行时的时间
- "client"
哪个客户端发起的该操作,并显示出该客户端的ip或hostname
- "allUsers"
哪个认证用户执行的该操作
- "user"
是否认证用户执行该操作,如认证后使用其他用户操作,该项为空
三、执行计划分析
3.1 简介
MongoDB查看执行计划的语法:
db.collection.find().explain()
MongoDB执行计划三种模式:
- queryPlanner Mode:只会显示 winning plan 的 queryPlanner,自建MongoDB默认模式
- executionStats Mode:只会显示 winning plan 的 queryPlanner + executionStats
- allPlansExecution Mode:会显示所有执行计划的 queryPlanner + executionStats,阿里云MongoDB默认模式
3.2 执行计划内容详解
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.data",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"name" : {
"$eq" : "ZJ"
}
},
{
"id" : {
"$lt" : 10
}
},
{
"id" : {
"$gt" : 2
}
}
]
},
"queryHash" : "17295960",
"planCacheKey" : "17295960",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"name" : {
"$eq" : "ZJ"
}
},
{
"id" : {
"$lt" : 10
}
},
{
"id" : {
"$gt" : 2
}
}
]
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 7,
"executionTimeMillis" : 650,
"totalKeysExamined" : 0,
"totalDocsExamined" : 1000000,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"name" : {
"$eq" : "ZJ"
}
},
{
"id" : {
"$lt" : 10
}
},
{
"id" : {
"$gt" : 2
}
}
]
},
"nReturned" : 7,
"executionTimeMillisEstimate" : 4,
"works" : 1000002,
"advanced" : 7,
"needTime" : 999994,
"needYield" : 0,
"saveState" : 7812,
"restoreState" : 7812,
"isEOF" : 1,
"direction" : "forward",
"docsExamined" : 1000000
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : "xsj",
"port" : 27017,
"version" : "4.2.0",
"gitVersion" : "a4b751dcf51dd249c5865812b390cfd1c0129c30"
},
"ok" : 1
}
上面这条语句的执行计划可以拆解为几个部分来理解:
- queryPlanner
parsedQuery:该部分解析了SQL的所有过滤条件
这个部分很好理解,不在过多说明;
winningPlan:该部分是SQL最终选择的执行计划
该部分主要关注stage,stage是执行计划的类型,有以下分类:
COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
- executionStats
这个部分最好的情况是:nReturned = totalKeysExamined = totalDocsExamined,此部分主要是文档扫描数以及消耗信息。
四、索引分析
4.1 基本命令
- 查看索引
MongoDB通过getIndexes()查看集合的所有索引
db.getCollection('data').getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.data"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "test.data"
}
]
- 查看索引大小
MongoDB通过totalIndexSize()查看集合索引的总大小
db.getCollection('data').totalIndexSize()
14389248 //单位字节
- 创建索引
db.collection.createIndex(keys,options)
keys,要建立索引的参数列表。如:{KEY:1/-1},其中key表示字段名,1/-1表示升降序。
options:
background,在后台建立索引,以便建立索引时不阻止其他数据库活动。默认值 false。
unique,创建唯一索引。默认值 false。
name,指定索引的名称。如果未指定,MongoDB会生成一个索引字段的名称和排序顺序串联。
dropDups,创建唯一索引时,如果出现重复删除后续出现的相同索引,只保留第一个。
sparse,对文档中不存在的字段数据不启用索引。默认值是 false。
v,索引的版本号。
weights,索引权重值,数值在1到99999 之间,表示该索引相对于其他索引字段的得分权重。
- 删除索引
db.data.dropIndex("user_1")方法用于删除指定的索引
db.data.dropIndexes()方法用于删除全部的索引
4.2 索引分类
- 单字段索引
单字段索引是针对单个字段进行设置索引的操作
#创建索引语法
db.data.createIndex({name:1})
name:1代表按照升序进行排序,降序排序的索引为-1
db.data.createIndex({name:-1})
- 联合索引
联合索引在单字段索引上进行了多个字段操作,将多个字段合并为一个索引的联合索引,联合索引需要遵守最左前缀原则。
#创建索引语法
db.data.createIndex({name:1,time:1})
- 多key索引
当内容是数组或者list集合创建的一种索引。该索引会为数组中的每个字段创建索引。
- 子文档索引
该索引用来嵌入子文档中的字段进行创建索引。操作也可以有复合索引,单字段索引。
db.data.createIndex({"user.name":1})
4.2 索引的属性
Mongodb支持多类型的索引,还能对索引增加一些额外的属性。
唯一索引:在Mongodb中_id就是利用单字段索引加唯一索引的属性构成的
部分索引(3.2版本新增):仅索引符合指定过滤器表达式集合中的文档。部分索引有较低的存储要求,降低索引的创建与维护
稀疏索引:确保索引仅包含具有索引字段的文档的条目,会跳过没有索引字段的文档
TTL索引:在一定时间后自动从集合中删除文档的一种索引
五、总结
本文介绍了MongoDB优化的基本思路以及如何查看慢日志、执行计划和索引的基本使用,如果有兴趣可以针对性的深入研究。