MONGODB05 - 通过MongoDB aggreate执行计划查看aggreate指令执行过程以及与find指令的区别
起因:开发过程中使用MongoDB,因为有一些关联会使用到MongoDB的aggregate部分指令,但是在拼装aggregate指令顺序发生变化时,查询的结果出现不一致的情况,导致查非所查的问题出现,故通过分析MongoDB的执行计划来看一下aggregate的执行过程,以及看下它与find的区别
查询语句如下:
db.classifiedOperationLog.aggregate([
{$sort:{createDate:-1}},
{$skip:0},
{$limit:5},
{$project:{_id:1,createDate:1}}
]);
一、find指令
上面的语句我们用find指令编写如下:
db.classifiedOperationLog.find({},{_id:1,createDate:1})
.sort({createDate:-1})
.skip(0)
.limit(5)
查询的结果如下:
{
"_id" : "6CCC129FC8BD4BA1B9F89B053E86112E",
"createDate" : ISODate("2020-12-24T11:25:04.675+0800")
}
{
"_id" : "8B325EC1E7DA4AD390EC301EF5012BE0",
"createDate" : ISODate("2020-12-24T11:25:00.176+0800")
}
{
"_id" : "498781F2606D4001977BDB8FAE038DF9",
"createDate" : ISODate("2020-12-24T11:24:54.748+0800")
}
{
"_id" : "FBE41B432313469588CE5A80BAB7F7BB",
"createDate" : ISODate("2020-12-24T09:52:27.219+0800")
}
{
"_id" : "37D2D451BC53489E945828559AE1EDC0",
"createDate" : ISODate("2020-12-24T08:57:12.702+0800")
}
我们看下find的执行计划,执行命令 db.collection.explain():
db.classifiedOperationLog.find({},{_id:1,createDate:1})
.sort({createDate:-1})
.skip(0)
.limit(5)
.explain()
//缺省情况下,explain包括2个部分,一个是queryPlanner,一个是serverInfo
//如果使用了executionStats或者allPlansExecution,则还会返回executionStats信息
{
"queryPlanner" : {
"plannerVersion" : 1.0, //查询计划版本
"namespace" : "xxx.classifiedOperationLog", //被查询对象
"indexFilterSet" : false, //是否用到索引来过滤
"parsedQuery" : { //解析查询,即过滤条件是什么
},
"winningPlan" : { //最佳的执行计划
"stage" : "LIMIT", //使用limit限制返回数
"limitAmount" : 5.0, //limit 限制数
"inputStage" : {
"stage" : "PROJECTION", //使用 skip 进行跳过
"transformBy" : { //字段过滤
"_id" : 1.0,
"createDate" : 1.0
},
"inputStage" : {
"stage" : "FETCH", //检出文档
"inputStage" : {
"stage" : "IXSCAN", //索引扫描,创建日期加了索引,此处排序走的是索引
"keyPattern" : {
"createDate" : 1.0
},
"indexName" : "createDate_1", //索引名称
"isMultiKey" : false, //是否复合索引
"multiKeyPaths" : {
"createDate" : [
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2.0,
"direction" : "backward",
"indexBounds" : {
"createDate" : [
"[MaxKey, MinKey]"
]
}
}
}
}
},
"rejectedPlans" : [ //拒绝的执行计划,此处没有
]
},
"serverInfo" : { //服务器信息,包括主机名,端口,版本等
"host" : "node-0",
"port" : 28000.0,
"version" : "3.6.8",
"gitVersion" : "6bc9ed599c3fa164703346a22bad17e33fa913e4"
},
"ok" : 1.0,
"operationTime" : Timestamp(1608789303, 1)
}
其中stage常见的操作描述如下:
- COLLSCAN 集合扫描
- IXSCAN 索引扫描
- FETCH 检出文档
- SHARD_MERGE 合并分片中结果
- SHARDING_FILTER 分片中过滤掉孤立文档
- LIMIT 使用limit 限制返回数
- PROJECTION 使用 skip 进行跳过
- IDHACK 针对_id进行查询
- COUNT 利用db.coll.explain().count()之类进行count运算
- COUNTSCAN count不使用Index进行count时的stage返回
- COUNT_SCAN count使用了Index进行count时的stage返回
- SUBPLA 未使用到索引的$or查询的stage返回
- TEXT 使用全文索引进行查询时候的stage返回
- PROJECTION 限定返回字段时候stage的返回
二、aggregate指令
在查看aggregate聚合查询之前我们看个有趣的现象,为了能演示效果,我们把skip的值改为2,把limit改为3
find查询结果
db.classifiedOperationLog.find({},{_id:1,createDate:1})
.sort({createDate:-1})
.skip(2)
.limit(3)
{
"_id" : "498781F2606D4001977BDB8FAE038DF9",
"createDate" : ISODate("2020-12-24T11:24:54.748+0800")
}
{
"_id" : "FBE41B432313469588CE5A80BAB7F7BB",
"createDate" : ISODate("2020-12-24T09:52:27.219+0800")
}
{
"_id" : "37D2D451BC53489E945828559AE1EDC0",
"createDate" : ISODate("2020-12-24T08:57:12.702+0800")
}
aggregate复刻版
db.classifiedOperationLog.aggregate([
{$sort:{createDate:-1}},
{$skip:2},
{$limit:3},
{$project:{_id:1,createDate:1}}
]);
{
"_id" : "498781F2606D4001977BDB8FAE038DF9",
"createDate" : ISODate("2020-12-24T11:24:54.748+0800")
}
{
"_id" : "FBE41B432313469588CE5A80BAB7F7BB",
"createDate" : ISODate("2020-12-24T09:52:27.219+0800")
}
{
"_id" : "37D2D451BC53489E945828559AE1EDC0",
"createDate" : ISODate("2020-12-24T08:57:12.702+0800")
}
可以看到aggregate查询出来的结果和find结果一致,此时我们把 \(sort** 下移到 **\)limit 之后
db.classifiedOperationLog.aggregate([
{$skip:2},
{$limit:3},
{$sort:{createDate:-1}},
{$project:{_id:1,createDate:1}}
]);
{
"_id" : "A3D65CCB5F7144F080B9B972A9595F04",
"createDate" : ISODate("2020-09-22T20:57:41.260+0800")
}
{
"_id" : "7C7F4D28F2794B3CB4E361ACAF797646",
"createDate" : ISODate("2020-09-22T20:50:21.702+0800")
}
{
"_id" : "4344982784CE454B83D73764986F65E1",
"createDate" : ISODate("2020-09-22T20:48:38.409+0800")
}
可以看到还是3条数据,还是倒叙排列,但是时间和ID都不一样,结果产生了变差,貌似不是我们想要的结果,这个时候,我们再把 \(skip**移到 **\)limit下面
db.classifiedOperationLog.aggregate([
{$limit:3},
{$skip:2},
{$sort:{createDate:-1}},
{$project:{_id:1,createDate:1}}
]);
{
"_id" : "4344982784CE454B83D73764986F65E1",
"createDate" : ISODate("2020-09-22T20:48:38.409+0800")
}
数据变成了一条,且不在期望的查询结果里,我们再把 \(skip**移到 **\)sort下面
db.classifiedOperationLog.aggregate([
{$limit:3},
{$sort:{createDate:-1}},
{$skip:2},
{$project:{_id:1,createDate:1}}
]);
{
"_id" : "EA42C914214C4C18A6B788F897C5F29A"
}
数据又又又变了,但是随着我们的尝试,规律越来越清晰了,我们来看下aggregate的执行计划
db.classifiedOperationLog.aggregate([
{$sort:{createDate:-1}},
{$skip:2},
{$limit:3},
{$project:{_id:1,createDate:1}}
],{explain:true}); //注意执行计划的参数写法
{
"stages" : [ //查询步骤
{
"$cursor" : { //1、游标查询(索引排序)
"query" : { //查询参数,这里没有
},
"sort" : {
"createDate" : NumberInt(-1) //排序
},
"limit" : NumberLong(5), //查询文档数,这里比较有意思,是skip+limit数量之和
"fields" : { //这里也比较有意思,$project其实在第三步才执行的,这里应当是MongoDB查询优化,减少网络IO,后续可查一下MongoDB这一块的实现再进行补充
"createDate" : NumberInt(1),
"_id" : NumberInt(1)
},
"queryPlanner" : { //与find类似不赘述
"plannerVersion" : NumberInt(1),
"namespace" : "xx.classifiedOperationLog",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN", //与find一致走的是索引扫描
"keyPattern" : {
"createDate" : NumberInt(1)
},
"indexName" : "createDate_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"createDate" : [
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : NumberInt(2),
"direction" : "backward",
"indexBounds" : {
"createDate" : [
"[MaxKey, MinKey]"
]
}
}
},
"rejectedPlans" : [
]
}
}
},
{
"$skip" : NumberLong(2) //2、skip跳过
},
{
"$project" : { //3、过滤字段
"_id" : true,
"createDate" : true
}
}
],
"ok" : 1.0,
"operationTime" : Timestamp(1608792723, 3),
"$gleStats" : {
"lastOpTime" : Timestamp(0, 0),
"electionId" : ObjectId("7fffffff0000000000000002")
},
"$configServerState" : {
"opTime" : {
"ts" : Timestamp(1608792719, 3),
"t" : NumberLong(1)
}
},
"$clusterTime" : {
"clusterTime" : Timestamp(1608792723, 3),
"signature" : {
"hash" : BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
aggregate其实是MongoDB的聚合管道,\(project** 、**\)limit、$sort都是管道操作符,MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理,管道操作是可以重复的。
ps:之前blog中分组去重计数就用了两次group来实现,其实现原理就是管道重复。参考链接:MONGODB03 - 分组计数/分组去重计数(基于 spring-data-mongodb)
聚合框架中常用的几个操作符:
- $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
- \(match:用于过滤数据,只输出符合条件的文档。\)match使用MongoDB的标准查询操作。
- $limit:用来限制MongoDB聚合管道返回的文档数。
- $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
- $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
- $group:将集合中的文档分组,可用于统计结果。
- $sort:将输入文档排序后输出。
- $geoNear:输出接近某一地理位置的有序文档。
了解了aggregate的执行原理之后我们再变更一下管道符顺序,看一下新的执行计划
db.classifiedOperationLog.aggregate([
{$skip:2},
{$limit:3},
{$sort:{createDate:-1}},
{$project:{_id:1,createDate:1}}
],{explain:true});
{
"stages" : [
{
"$cursor" : { //1、游标取数执行limit,取到5条数据,skip+limit数量之和
"query" : {
},
"limit" : NumberLong(5),
"fields" : {
"createDate" : NumberInt(1),
"_id" : NumberInt(1)
},
"queryPlanner" : {
"plannerVersion" : NumberInt(1),
"namespace" : "xxx.classifiedOperationLog",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "COLLSCAN", //集合扫描,取出limit数量文档
"direction" : "forward"
},
"rejectedPlans" : [
]
}
}
},
{
"$skip" : NumberLong(2) //2、跳过两条
},
{
"$sort" : { //3、对结果进行排序
"sortKey" : {
"createDate" : NumberInt(-1)
}
}
},
{
"$project" : { //4、过滤字段
"_id" : true,
"createDate" : true
}
}
],
"ok" : 1.0,
"operationTime" : Timestamp(1608794303, 1),
"$gleStats" : {
"lastOpTime" : Timestamp(0, 0),
"electionId" : ObjectId("7fffffff0000000000000002")
},
"$configServerState" : {
"opTime" : {
"ts" : Timestamp(1608794306, 3),
"t" : NumberLong(1)
}
},
"$clusterTime" : {
"clusterTime" : Timestamp(1608794306, 4),
"signature" : {
"hash" : BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
通过上述的执行计划分析,了解find和aggregate的工作机制,小伙伴可以根据需要选择对应的指令,aggregate可利用通过参数顺序和二次复用的特性满足一些特定场景的需求。
参考链接:
https://www.runoob.com/mongodb/mongodb-aggregate.html
https://blog.csdn.net/user_longling/article/details/83957085