简介
MongoDB采用自底向上的方式来构造查询计划,每一个查询计划 (query plan)都会被分解为若千个有层次的阶段 (stage)。有意思的是,整个查询计划最终会呈现出一颗多叉树的形状。
整个计算过程是从下向上投递的,每一个阶段的计算结果都是其上层阶段的输入
var collection = db.getCollection("practise");
var count = 10000;
var base = 10;
var items = [];
for (var i = 0; i <= count; i++) {
var item = {};
item.x = Math.round(Math.random() * base);
item.y = Math.round(Math.random() * base);
item.z = Math.round(Math.random() * base);
item.did = "ITEM" + i;
items.push(item);
if (i % 1000 == 0) {
collection.insertMany(items);
items = [];
}
}
db.getCollection('practise').ensureIndex({x: 1, y: 1, z: 1})
db.getCollection('practise').ensureIndex({did: 1})
全表扫描
db.getCollection('practise').find({aaa: 11}).explain()
{
...
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"aaa" : {
"$eq" : 11.0
}
},
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"aaa" : {
"$eq" : 11.0
}
},
"direction" : "forward"
},
"rejectedPlans" : []
},
...
}
单键索引命中
db.getCollection('practise').find({did: "ITEM24"}).explain()
{
...
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"did" : {
"$eq" : "ITEM24"
}
},
"queryHash" : "B4F08825",
"planCacheKey" : "9F7BDA42",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"did" : 1.0
},
"indexName" : "did_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"did" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"did" : [
"[\"ITEM24\", \"ITEM24\"]"
]
}
}
},
"rejectedPlans" : []
},
...
}
覆盖索引
db.getCollection('practise').find({did: "ITEM24"}, {did: 1, _id: 0}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"did" : {
"$eq" : "ITEM24"
}
},
"queryHash" : "BA4C57C6",
"planCacheKey" : "302BC8FB",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "PROJECTION_COVERED",
"transformBy" : {
"did" : 1.0,
"_id" : 0.0
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"did" : 1.0
},
"indexName" : "did_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"did" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"did" : [
"[\"ITEM24\", \"ITEM24\"]"
]
}
}
},
"rejectedPlans" : []
},
}
列表查询+skip/limit
db.getCollection('practise').find({x: {$gt: 3}}).skip(10).limit(5).explain()
{
...
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$gt" : 3.0
}
},
"queryHash" : "39913629",
"planCacheKey" : "CB9286EB",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 5,
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SKIP",
"skipAmount" : 10,
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"(3.0, inf.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
}
}
},
"rejectedPlans" : []
},
...
}
内存排序
使用了{x1: 1}
, 因此无法利用索引进行排序,只能在内存里排序。当内存排序超过了memLimit时,查询就会出错
db.getCollection('practise').find({x: {$gt: 3}}).sort({x1: 1}).explain("executionStats")
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$gt" : 3.0
}
},
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {
"x1" : 1
},
"memLimit" : 104857600,
"type" : "simple",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"(3.0, inf.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
}
},
"rejectedPlans" : []
},
}
组合索引无法命中
查询无法满足前缀匹配原则,实际上做了全表扫描
db.getCollection('practise').find({y: 1, z: 3}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"y" : {
"$eq" : 1.0
}
},
{
"z" : {
"$eq" : 3.0
}
}
]
},
"queryHash" : "2B5DAA81",
"planCacheKey" : "189A1787",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [
{
"y" : {
"$eq" : 1.0
}
},
{
"z" : {
"$eq" : 3.0
}
}
]
},
"direction" : "forward"
},
"rejectedPlans" : []
},
}
组合索引排序命中
sort({y: -1, z: -1})
y和z都使用了降序,所以可以使用索引排序
db.getCollection('practise').find({x: 1}).sort({y: -1, z: -1}).limit(5).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$eq" : 1.0
}
},
"queryHash" : "D1E516FC",
"planCacheKey" : "36B48F45",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 5,
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"x" : [
"[1.0, 1.0]"
],
"y" : [
"[MaxKey, MinKey]"
],
"z" : [
"[MaxKey, MinKey]"
]
}
}
}
},
"rejectedPlans" : []
},
}
组合索引命中,内存排序
sort({y: 1, z: -1})
y和z排序方向不同,所以只能在内存中排序。需要注意的是,这里的内存排序是基于索引的而不是文档的,但在mongodb4.0及以前的版本中,对于这种查询的排序是基于文档的,也就是先执行FETCH再执行SORT。
db.getCollection('practise').find({x: 1}).sort({y: 1, z: -1}).limit(5).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$eq" : 1.0
}
},
"queryHash" : "048FB511",
"planCacheKey" : "3CE59AC2",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SORT",
"sortPattern" : {
"y" : 1,
"z" : -1
},
"memLimit" : 104857600,
"limitAmount" : 5,
"type" : "default",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[1.0, 1.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
}
},
"rejectedPlans" : []
},
}
组合索引命中,范围+排序
db.getCollection('practise').find({x: {$gt: 3}}).sort({x: 1, y: 1, z: 1}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$gt" : 3.0
}
},
"queryHash" : "D7099141",
"planCacheKey" : "4B559A11",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"(3.0, inf.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : []
},
}
不合适的组合索引,范围+排序
x不是等值匹配,因此{y: 1, z: 1}
的排序无法利用组合索引的顺序,此时产生了内存排序
db.getCollection('practise').find({x: {$gt: 3}}).sort({y: 1, z: 1}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$gt" : 3.0
}
},
"queryHash" : "916DB19C",
"planCacheKey" : "B7596367",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SORT",
"sortPattern" : {
"y" : 1,
"z" : 1
},
"memLimit" : 104857600,
"type" : "default",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"(3.0, inf.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
}
},
"rejectedPlans" : []
},
}
合并排序
这里使用$in将目标值锁定在有限的若干个值上,数据库会使用归并排序的方式来保证结果的有序性
db.getCollection('practise').find({x: {$in: [1, 2, 3, 4]}}).sort({y: 1}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"x" : {
"$in" : [
1.0,
2.0,
3.0,
4.0
]
}
},
"queryHash" : "C28AC753",
"planCacheKey" : "AB92D2F0",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SORT_MERGE",
"sortPattern" : {
"y" : 1.0
},
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[1.0, 1.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[2.0, 2.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[3.0, 3.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"y" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[4.0, 4.0]"
],
"y" : [
"[MinKey, MaxKey]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
]
}
},
"rejectedPlans" : []
},
}
跨索引的合并排序
db.practise.ensureIndex({x: 1, z: 1});
db.practise.ensureIndex({y: 1, z: 1});
db.getCollection('practise').find({$or: [{x: 1}, {y: 1}]}).sort({z: 1}).explain()
{
"queryPlanner" : {
"namespace" : "test.practise",
"indexFilterSet" : false,
"parsedQuery" : {
"$or" : [
{
"x" : {
"$eq" : 1.0
}
},
{
"y" : {
"$eq" : 1.0
}
}
]
},
"queryHash" : "56592F73",
"planCacheKey" : "CE19B698",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "SUBPLAN",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "SORT_MERGE",
"sortPattern" : {
"z" : 1.0
},
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {
"x" : 1.0,
"z" : 1.0
},
"indexName" : "x_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"x" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"x" : [
"[1.0, 1.0]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"y" : 1.0,
"z" : 1.0
},
"indexName" : "y_1_z_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"y" : [],
"z" : []
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"y" : [
"[1.0, 1.0]"
],
"z" : [
"[MinKey, MaxKey]"
]
}
}
]
}
}
},
"rejectedPlans" : []
},
}