查询优化器
MongoDB的查询计划会将多个索引并行去执行,最先返回101的结果就是胜者,其他查询计划就会被终止,执行优胜的查询计划
这个查询计划将会被缓存,接下来相同的语句查询条件都会使用它
- 何时查询计划才会变
- 建立索引时
- 每执行1000次查询之后,查询优化器就会重新评估查询计划
- 较大的数据波动
explain 使用
db.getCollection('db_name').explain('executionStats').aggregate([....])
// 得到的结果
{
"stages" : [
{
"$cursor" : {
"query" : {
.....
},
"fields" : {
......
},
"queryPlanner" : {
......
},
"winningPlan" : { // 优胜的方案
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"is_deleted" : {
"$eq" : 0.0
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"acc_opening_date" : 1
},
"indexName" : "acc_opening_date_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"acc_opening_date" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"acc_opening_date" : [
"[new Date(1493625600000), new Date(1609350406000)]"
]
}
}
},
"rejectedPlans" : [ // 拒绝的方案
{
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"is_deleted" : {
"$eq" : 0.0
}
},
{
"acc_opening_date" : {
"$lte" : ISODate("2020-12-30T17:46:46.000Z")
}
},
{
"acc_opening_date" : {
"$gte" : ISODate("2017-05-01T08:00:00.000Z")
}
}
]
},
},
{
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"acc_opening_date" : {
"$lte" : ISODate("2020-12-30T17:46:46.000Z")
}
},
{
"acc_opening_date" : {
"$gte" : ISODate("2017-05-01T08:00:00.000Z")
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"is_deleted" : 1 // is_deteted颗粒度这么大的索引不应该建立,难怪要被拒绝
},
"indexName" : "is_deleted_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"is_deleted" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"is_deleted" : [
"[0.0, 0.0]"
]
}
}
}
]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1, // 返回的结果数量
"executionTimeMillis" : 1, // 运行的时间
"totalKeysExamined" : 1, // 扫描的索引数量
"totalDocsExamined" : 1, // 扫描的文档数量
"executionStages" : {
"stage" : "FETCH", // step2: 用is_deleted字段从上一阶段的结果中过滤出相应结果
"filter" : {
"$and" : [
{
"is_deleted" : {
"$eq" : 0.0
}
},
]
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 3,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 1,
"restoreState" : 1,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 1,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN", // step1: 用acc_opening_date字段索引搜索出相应结果
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 1,
"restoreState" : 1,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"acc_opening_date" : 1
},
"indexName" : "acc_opening_date_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"acc_opening_date" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"acc_opening_date" : [
"[new Date(1493625600000), new Date(1609350406000)]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
}
},
{
"$lookup" : {
"from" : "clients",
"as" : "clients",
"localField" : "idp_user_id",
"foreignField" : "idp_user_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
}
}
},
{
"$project" : {
......
}
],
"ok" : 1.0,
"operationTime" : Timestamp(1604133360, 3),
"$clusterTime" : {
"clusterTime" : Timestamp(1604133360, 3),
"signature" : {
"hash" : { "$binary" : "RbWJfLtWiuIthJ5C3oiKbGIt1iY=", "$type" : "00" },
"keyId" : NumberLong(6854528299859705857)
}
}
}
- 查看方式:嵌套最内层往外的顺序看,不是从上到下。
原因:
explain 结果将查询计划以阶段树的形式呈现。
每个阶段将其结果传递给父节点,中间节点操作由子节点产生的文档或索引
- 索引使用情况解读
stage 主要分为以下几种:
COLLSACN: 全盘扫描
IXSACN: 索引扫描
FETCH: 根据前面节点扫描出的文档,进一步过滤抓取
SORT: 内存进行排序
SORT_KEY_GENERATOR: 获取每一个文档排序所用的键值
LIMIT: 使用limit限制返回数
SKIP: 使用skip进行跳过
IDHACK: 针对_id进行查询
COUNTSCAN: count不使用index进行count
COUNT_SCACN: count使用index进行count
TEXT: 使用全文索引进行查询
SUBPLA:未使用到索引的$or查询
PROJECTION:限定返回字段
- 所以不希望看到explain分析出现如下的stage:
COLLSCAN
SORT
COUNTSCAN
SUBPLA
- 最好是如下的组合:
FETCH + IXSCAN
FETCH + IDHACK
LIMIT + ( FETCH + IXSCAN)
PROJECTION + IXSCAN
COUNT_SCAN
效率极低的操作符
- exists:这两个操作符,完全不能使用索引。
- not: 通常来说取反和不等于,可以使用索引,但是效率极低,不是很有效,往往也会退化成扫描全表。
- $nin: 不包含,这个操作符也总是会全表扫描
- 对于管道中的索引,也很容易出现意外,只有在管道最开始时的match sort可以使用到索引,一旦发生过project投射,group分组,lookup表关联,unwind打散等操作后,就完全无法使用索引。
aggregate优化
- 我认为的准则是尽可能先缩小文档大小(例如:sort, skip),最后进行其他复杂操作(project, unwind),因为这些操作打散后,完全无法使用索引.
最佳顺序: sort + $limit + ...- 千万别忘了$lookup连表的字段,两张表一定要建立索引
- 优化案例1:limit提前缩小文档大小,减少内存计算
// 使用时间: 0.044 S
db.getCollection('clients').explain("executionStats").aggregate([
{ '$match': { is_deleted: 0 } },
{ '$sort': { gmt_create: -1 } },
{ '$lookup':
{ from: 'client_infos',
localField: 'client_info_ids',
foreignField: '_id', as: 'client_infos' }
},
{ '$lookup':
{ from: 'accounts',
localField: 'account_id',
foreignField: '_id', as: 'account' }
},
{ '$unwind': '$account' },
{ '$skip': 0 },
{ '$limit': 10 }], {})
// 使用时间: 0.021s
db.getCollection('clients').explain("executionStats").aggregate([
{ '$match': { is_deleted: 0 } },
{ '$sort': { gmt_create: -1 } },
{ '$skip': 0 },
{ '$limit': 10 },
{ '$lookup':
{ from: 'client_infos',
localField: 'client_info_ids',
foreignField: '_id', as: 'client_infos' }
},
{ '$lookup':
{ from: 'accounts',
localField: 'account_id',
foreignField: '_id', as: 'account' }
},
{ '$unwind': '$account' },
], {})
- 优化案例2: 转换搜索的主表,使索引生效
// 使用时间: 3.64s
db.getCollection('clients').explain("executionStats").aggregate([
{ '$lookup':
{ from: 'client_infos',
localField: 'client_info_ids',
foreignField: '_id', as: 'client_infos' }
},
{ '$match': { 'client_infos.phone': 110 } },
{ '$lookup':
{
from: 'accounts',
localField: 'account_id',
foreignField: '_id',
as: 'account',
}
},
{ '$unwind': '$account' },
{ '$skip': 0 },
{ '$limit': 10 },
], {})
// 使用时间:0.065s
db.getCollection('client_infos').aggregate([
{ '$match': { phone: 110, is_deleted: 0} },
{ '$skip': 0 },
{ '$limit': 10 },
{ '$lookup':
{ from: 'clients',
localField: '_id',
foreignField: 'client_info_ids', as: 'clients' }
},
{ '$unwind': '$clients' },
{ '$lookup':
{
from: 'accounts',
localField: 'clients.account_id',
foreignField: '_id',
as: 'account',
}
},
{ '$unwind': '$account' },
], {})
索引设计原则
-
索引字段颗粒度越小越好
颗粒度为结果集在原集合中所占的比例
颗粒度小的,例如身份证号等唯一性质的,索引扫描能够很快定位出位置
相反字段颗粒度大的,例如枚举,例如布尔值,索引定位出的位置不够精准,到头来还得大部分扫描,因为多了索引扫描,最后速度可能还不如全盘扫描。
-
字段更新频率小
索引的缺点之一就是修改时还需要维护索引,所以最好选择字段更新比较小的字段
-
适当冗余设计
aggregate连表查询。如果查询字段在副表中,就无法使用到索引,如果这种连表查询频率较高,可考虑冗余设计。如上诉案例2,可通过冗余phone的字段提高查询效率,以及增加代码通用性
-
索引数量控制
一个索引的字段超过7,8,需考虑合理性
查询优化原则
-
减少带宽
按需取字段,避免返回大字段
-
减少内存计算
减少中间存储,内存计算
-
减少磁盘IO
增加索引,避免全盘扫描,优化sql
参考文档:
https://zhuanlan.zhihu.com/p/77971681
https://docs.mongodb.com/manual/core/aggregation-pipeline/#aggregation-pipeline-operators-and-performance
https://docs.mongodb.com/manual/core/aggregation-pipeline-optimization/
https://jira.mongodb.org/browse/SERVER-28140
https://stackoverflow.com/questions/59811200/lookup-wont-use-indexes-in-second-match-how-can-we-scale
作者:IT女神_
链接:https://www.jianshu.com/p/f92919ae6c90