深入浅出MongoDB(六)
文章目录
分析查询性能
- mongodb compass提供解释计划标签页,其中显示有关查询性能的统计信息。这些统计信息可用于衡量查询是否以及如何使用索引。
cursor.explain("executionStats")
和db.collection.explain("executionStats")
方法提供了有关查询性能的统计信息。- 评估查询性能
shell
# 往集合inventory中添加文档
db.inventory.insertMany([
{_id: 1, item: 'f1', type: 'food', quantity: 500},
{_id: 2, item: 'f2', type: 'food', quantity: 100},
{_id: 3, item: 'p1', type: 'paper', quantity: 200},
{_id: 4, item: 'p2', type: 'paper', quantity: 150},
{_id: 5, item: 'f3', type: 'food', quantity: 300},
{_id: 6, item: 't1', type: 'toys', quantity: 500},
{_id: 7, item: 'a1', type: 'apparel', quantity: 250},
{_id: 8, item: 'a2', type: 'apparel', quantity: 400},
{_id: 9, item: 't2', type: 'toys', quantity: 50},
{_id: 10, item: 'f4', type: 'food', quantity: 75}
])
# 无索引查询quantity字段值在100~200之间的文档
db.inventory.find({quantity: {$gte: 100, $lte: 200}})
# 查看选定的查询计划,将cursor.explain("executionStats")游标方法连接到find命令末尾
db.inventory.find({quantity: {$gte: 100, $lte: 200}}).explain('executionStats')
# explain方法返回结果如下
# queryPlanner.winningPlan.stage是COLLSCAN表示会进行集合扫描,mongod必须对整个集合的文档进行逐份扫描才能识别结果,此项操作通常成本很高,可能会降低查询速度。
# executionStats.nReturned是3,表示获胜查询计划返回三个文档。
# executionStats.totalKeysExamined是0,表示该查询未使用索引。
# executionStats.totalDocsExamined是10,表示mongodb必须扫描10个文档以查找三个匹配的文档。
[
{
"command": {
"find": "inventory",
"filter": {
"quantity": {
"$gte": 100,
"$lte": 200
}
},
"$db": "test"
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 22,
"totalKeysExamined": 0,
"totalDocsExamined": 10,
"executionStages": {
"stage": "COLLSCAN",
"filter": {
"$and": [
{
"quantity": {
"$lte": 200
}
},
{
"quantity": {
"$gte": 100
}
}
]
},
"nReturned": 3,
"executionTimeMillisEstimate": 0,
"works": 12,
"advanced": 3,
"needTime": 8,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"direction": "forward",
"docsExamined": 10
}
},
"explainVersion": "1",
"ok": 1,
"queryPlanner": {
"namespace": "test.inventory",
"indexFilterSet": false,
"parsedQuery": {
"$and": [
{
"quantity": {
"$lte": 200
}
},
{
"quantity": {
"$gte": 100
}
}
]
},
"maxIndexedOrSolutionsReached": false,
"maxIndexedAndSolutionsReached": false,
"maxScansToExplodeReached": false,
"winningPlan": {
"stage": "COLLSCAN",
"filter": {
"$and": [
{
"quantity": {
"$lte": 200
}
},
{
"quantity": {
"$gte": 100
}
}
]
},
"direction": "forward"
},
"rejectedPlans": []
},
"serverInfo": {
"host": "30fb3e0dfa72",
"port": 27017,
"version": "5.0.5",
"gitVersion": "d65fd89df3fc039b5c55933c0f71d647a54510ae"
},
"serverParameters": {
"internalQueryFacetBufferSizeBytes": 104857600,
"internalQueryFacetMaxOutputDocSizeBytes": 104857600,
"internalLookupStageIntermediateDocumentMaxSizeBytes": 104857600,
"internalDocumentSourceGroupMaxMemoryBytes": 104857600,
"internalQueryMaxBlockingSortMemoryUsageBytes": 104857600,
"internalQueryProhibitBlockingMergeOnMongoS": 0,
"internalQueryMaxAddToSetBytes": 104857600,
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes": 104857600
}
}
]
# 带索引的查询,为quantity字段添加索引
db.inventory.createIndex({quantity: 1})
# 查看查询计划的统计信息
db.inventory.find({quantity: {$gte: 100, $lte: 200}}).explain('executionStats')
# explain方法结果如下
# queryPlanner.winningPlan.stage是IXSCAN,表示使用索引。
# executionStats.nReturned是3,表示获胜查询计划返回三个文档。
# executionStats.totalKeysExamined是3,表示扫描到了3个索引条目。检查的密钥数量与返回的文档数量相匹配,只需要检查索引建就可以返回结果。
# executionStats.totalDocsExamined是3,表示mongodb扫描了3个文档。
[
{
"command": {
"find": "inventory",
"filter": {
"quantity": {
"$gte": 100,
"$lte": 200
}
},
"$db": "test"
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 86,
"totalKeysExamined": 3,
"totalDocsExamined": 3,
"executionStages": {
"stage": "FETCH",
"nReturned": 3,
"executionTimeMillisEstimate": 19,
"works": 4,
"advanced": 3,
"needTime": 0,
"needYield": 0,
"saveState": 1,
"restoreState": 1,
"isEOF": 1,
"docsExamined": 3,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 3,
"executionTimeMillisEstimate": 19,
"works": 4,
"advanced": 3,
"needTime": 0,
"needYield": 0,
"saveState": 1,
"restoreState": 1,
"isEOF": 1,
"keyPattern": {
"quantity": 1
},
"indexName": "quantity_1",
"isMultiKey": false,
"multiKeyPaths": {
"quantity": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"quantity": ["[100, 200]"]
},
"keysExamined": 3,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0
}
}
},
"explainVersion": "1",
"ok": 1,
"queryPlanner": {
"namespace": "test.inventory",
"indexFilterSet": false,
"parsedQuery": {
"$and": [
{
"quantity": {
"$lte": 200
}
},
{
"quantity": {
"$gte": 100
}
}
]
},
"maxIndexedOrSolutionsReached": false,
"maxIndexedAndSolutionsReached": false,
"maxScansToExplodeReached": false,
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"quantity": 1
},
"indexName": "quantity_1",
"isMultiKey": false,
"multiKeyPaths": {
"quantity": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"quantity": ["[100, 200]"]
}
}
},
"rejectedPlans": []
},
"serverInfo": {
"host": "30fb3e0dfa72",
"port": 27017,
"version": "5.0.5",
"gitVersion": "d65fd89df3fc039b5c55933c0f71d647a54510ae"
},
"serverParameters": {
"internalQueryFacetBufferSizeBytes": 104857600,
"internalQueryFacetMaxOutputDocSizeBytes": 104857600,
"internalLookupStageIntermediateDocumentMaxSizeBytes": 104857600,
"internalDocumentSourceGroupMaxMemoryBytes": 104857600,
"internalQueryMaxBlockingSortMemoryUsageBytes": 104857600,
"internalQueryProhibitBlockingMergeOnMongoS": 0,
"internalQueryMaxAddToSetBytes": 104857600,
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes": 104857600
}
}
]
原子性和事务
- 在mongodb中写入操作在单个文档级别上是原子性的,即使该操作修改单个文档中的多个嵌入文档也是这样的。
- 当单个写操作比如
db.collection.updateMany()
修改了多份文档,则每份文档的修改都是原子性的,但整个操作不是原子性的。在执行多文档写入操作时,无论是通过单次写入操作还是多次写入操作,其他操作都可能会交错进行。 - 并发控制允许多个应用程序同时运行,而不会造成数据不一致或数据冲突。对文档的
findAndModify
操作是原子性操作,如果查找条件与文档匹配,则对该文档执行更新。在当前更新完成之前,该文档的并发查询和其他更新不会受影响。
shell
db.myCollection.insertMany([
{_id: 0, a: 1, b: 1},
{_id: 1, a: 1, b: 1}
])
# 两个findAndModify操作同时运行后,保证两份文档中的a和b都设置为2
db.myCollection.findAndModify({
query: {a: 1},
update: {$inc: {b: 1}, $set: {a: 2}}
})
- 我们可以对字段创建唯一索引,以使其仅拥有唯一值,可以防止插入和更新操作创建重复数据;还可以对多个字段创建唯一索引,以确保字段值的组合是唯一的。
字段名称带句点和美元符号
- mongodb 5支持以美元为前缀或包含句点的字段名称的支持。在大多数情况下,使用类似字段名称存储的数据不能直接访问,我们需要在访问字段的查询中使用诸如
$getField
、$setField
和$literal
等辅助方法。 - 插入操作
shell
# 允许以$为前缀的字段作为插入操作的顶层和嵌套字段名
db.sales.insertOne({
"$price": 50.00,
"quantity": 30
})
# 附带$前缀的字段可以在使用其他保留字的插入操作中使用
# $inc一类的运算符名称以及类似id、db和ref的词可用作字段名称
db.books.insertOne({
'$id': 'h1999-09',
'location': {
'$db': 'novels',
'$ref': '2024100712',
'$inc': true
}
})
# 在更新或插入(upsert)期间创建新文档的更新被视为insert而非update,可以接受$为前缀的字段
# 如果更新中的match部分选择的是现有文档,类似的更新操作可能会引发错误
# upsert为true,如果集合中没有指定日期的文档,会插入一个新文档,如果与现有文档匹配则更新失败
db.expenses.updateOne(
{'date': '2024-10-07'},
{$set: {
'phone': 25.12,
'$hotel': 320.15
}},
{upsert: true}
)
- 文档替换更新
shell
# 更新操作符需要用新文档替换现有文档或者修改这些字段,通过更新替换时,不得以$前缀字段作为顶级字段名称
# 可以使用替换现有文档的更新操作符来修改address.$street字段,但不能以这种方式更新rooms字段,使用$setField作为聚合管道的一部分来更新以$为前缀的顶级字段
{
"_id": "E123",
"address": {
"$number": 123,
"$street": "Elm Road"
},
"$rooms": {
"br": 2,
"bath": 1
}
}
- 文档修改更新
shell
# 当更新修改而不是替换现有文档字段时,$前缀字段可以使顶级字段名称,可以直接访问子字段,但是需要一个辅助方法来访问顶级字段。
{
_id: ObjectId("610023ad7d58ecda39b8d161"),
"part": "AB305",
"$bin": 200,
"quantity": 100,
"pricing": { sale: true, "$discount": 60 }
}
# 可以直接查询pricing.$discount子字段
db.inventory.findAndModify({
query: {'part': {$eq: 'AB305'}},
update: {$inc: {'pricing.$discount': 10}}
})
# 使用$getField和$literal访问顶级$bin字段的值
db.inventory.findAndModify({
query: {$expr: {
$eq: [{$getField: {$literal: '$bin'}}, 200]
}},
update: {$inc: {'quantity': 10}}
})
- 使用聚合管道进行更新
shell
# 在$replaceWith阶段使用$setField、$getField和$literal来修改聚合管道中以$为前缀的字段
{
"_id": 100001,
"$term": "fall",
"registered": true,
"grade": 4
}
# 使用管道为春季学期创建一个新集合
db.school.aggregate([
{$match: {'registered': true}},
{$replaceWith: {
$setField: {
field: {$literal: '$term'},
input: '$$ROOT',
value: 'spring'
}
}},
{$out: 'spring2024'}
])
- 一般限制
shell
以$为前缀的字段不能编入索引、用作分片密钥的一部分、使用$jsonSchema验证、使用转义序列进行修改、与字段级加密一起使用、用作_id文档中的子字段。
查询计划
- 对于任何给定的查询,在给定可用索引的情况下,mongodb查询规划器会选择并缓存最高效的查询计划。为了评估查询计划的效率,查询规划器会在试用期内运行所有候选计划。一般情况下,获胜计划是在试用期间产生最多结果同时执行最少工作量的查询计划。关联的计划缓存条目用于具有相同查询形状的后续查询。
- 规划缓存条目状态,从mongodb 4.2开始,每个查询结构都与缓存中的三种状态之一相关联。
shell
缺失:缓存中不存在此形状的条目。对于查询,如果查询结构的缓存条目状态为Missing,对候选计划进行评估,并选出获选计划。缓存会为处于非活动状态的查询结构创建一个条目,该条目的值可量化计划所需的工作量。
非活动:缓存中的条目是此形状的占位符条目。规划器已查看形状,计算了一个值来量化计划所需的工作量,并存储了形状占位符条目,但查询结构不用于生成查询计划。对于查询,如果形状的缓存条目状态为Active,对候选计划进行评估,并选出候选计划。将量化计划所需工作量的所选计划的值与非活动条目的值进行比较。如果选定计划值为小于或等于非活动条目的值,所选计划取代了占位符非活动条目,并处于活动状态。大于非活动条目的值,非活动条目保持不变,但其量化计划所需工作量的值会递增。
活跃的:缓存中的条目是面向获胜计划的。规划器可以使用此条目生成查询计划。对于查询,如果查询结构的缓存条目状态为活动,活动条目用于生成查询计划。规划器还会评估条目的性能,如果其用于量化计划所需工作量的值不再符合选择条件,则会转换为非活动状态。
- 查询计划和缓存信息,要查看给定查询的查询计划信息,可以使用
db.collection.explain()
或cursor.explain()
。要查看集合的计划缓存信息,可以使用$planCacheStats
聚合阶段。 - 计划缓存刷新,如果mongod重新启动或关闭,查询计划缓存将不复存在。此外,目录操作比如索引或集合删除会清除计划缓存。最近最少使用缓存机制会清除最近最少访问的缓存条目,而无论条目处于何种状态。此外还可以使用使用以下方法。
shell
# 清除整个计划缓存
PlanCache.clear()
# 清除特定计划缓存条目
PlanCache.clearPlansByQuery()
- 计划缓存调试信息大小限制,从mongodb 5.0开始,当所有集合的计划缓存累计大小低于0.5GB时,计划缓存才会保存完整的计划缓存条目。当所有集合的计划缓存累计大小超过此阈值时,计划缓存会存储额外的计划缓存条目,但不会包含以下调试信息。计划缓存条目的估计大小以字节为单位可以在
$planCacheStats
的输出中找到。
shell
createdFromQuery, cachedPlan, creationExecStats, candidatePlanScores
-
为了帮助识别具有相同查询结构的慢速查询,每个查询结构都与一个queryHash相关联。
queryHash
是一个十六进制字符串,表示查询结构的哈希值,并且仅依赖于查询结构。两个不同的查询结构可能会产生相同的哈希值,但是不同查询结构之间不太可能发生哈希冲突。 -
为了更深入地了解查询计划缓存,mongodb推出了
planCacheKey
,它是与查询关联的计划缓存条目键的哈希值,是查询结构和该结构当前可用索引的函数。如果添加或删除可以支持该查询结构的索引,则planCacheKey
值可能会更改,而queryHash
值不会更改。 -
queryHash
和planCacheKey
用途
shell
explain()输出字段queryPlanner.queryHash和queryPlanner.planCacheKey
记录慢速查询时的日志消息,包括分析器日志消息和诊断日志消息
$planCacheStats聚合阶段
PlanCache.listQueryShapes()方法/planCacheListQueryShapes命令
PlanCache.getPlansByQuery()方法/planCacheListPlans命令
- 索引筛选器
shell
索引筛选器使用planCacheSetFilter命令设置,并确定优化器对查询结构评估哪些索引。查询结构由查询、排序和投影规范的组合组成。如果给定查询结构存在索引筛选器,则优化器仅考虑筛选器中指定的索引。
当查询结构存在索引筛选器时,mongodb会忽略hint()。要查看mongodb是否为查询结构应用了索引筛选器,检查db.collection.explain()方法的indexFilterSet字段。
索引筛选器仅影响优化器评估哪些索引,优化器仍可能选择collection扫描作为给定查询结构的获胜计划。索引筛选器在服务器进程期间存在,而在关闭后会消失。
诊断日志消息
$planCacheStats聚合阶段
PlanCache.listQueryShapes()方法/planCacheListQueryShapes命令
PlanCache.getPlansByQuery()方法/planCacheListPlans命令