summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Korshunov <anton.korshunov@mongodb.com>2021-01-18 22:06:40 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-01-30 03:46:12 +0000
commitc088ca3d8fef6d806f92ea2d5869d8910e5cd30c (patch)
treee5d69b3273488d9d0c9aa2894184cc8a7545aac9
parentc08203fe14a89e789b7dc5353c427e34c4c2b31e (diff)
downloadmongo-c088ca3d8fef6d806f92ea2d5869d8910e5cd30c.tar.gz
SERVER-53270 Include SBE PlanStage tree string into explain output
-rw-r--r--jstests/core/bittest.js2
-rw-r--r--jstests/core/coveredIndex1.js2
-rw-r--r--jstests/core/coveredIndex2.js29
-rw-r--r--jstests/core/covered_index_negative_1.js12
-rw-r--r--jstests/core/covered_multikey.js53
-rw-r--r--jstests/core/distinct_compound_index.js11
-rw-r--r--jstests/core/distinct_multikey.js50
-rw-r--r--jstests/core/distinct_multikey_dotted_path.js27
-rw-r--r--jstests/core/distinct_with_hashed_index.js53
-rw-r--r--jstests/core/explain_distinct.js43
-rw-r--r--jstests/core/explain_multikey.js13
-rw-r--r--jstests/core/index_filter_catalog_independent.js8
-rw-r--r--jstests/core/index_filter_collation.js4
-rw-r--r--jstests/core/index_filter_on_hidden_index.js2
-rw-r--r--jstests/core/index_multikey.js2
-rw-r--r--jstests/core/index_partial_read_ops.js26
-rw-r--r--jstests/core/or2.js73
-rw-r--r--jstests/core/or3.js63
-rw-r--r--jstests/core/plan_cache_sbe.js46
-rw-r--r--jstests/core/wildcard_index_hint.js2
-rw-r--r--jstests/libs/analyze_plan.js21
-rw-r--r--jstests/noPassthrough/and_hash.js2
-rw-r--r--jstests/noPassthrough/hybrid_multikey.js3
-rw-r--r--jstests/noPassthrough/validate_adjust_multikey.js5
-rw-r--r--src/mongo/db/exec/plan_cache_util.h3
-rw-r--r--src/mongo/db/exec/sbe/parser/sbe_parser_test.cpp6
-rw-r--r--src/mongo/db/exec/sbe/stages/hash_agg.cpp8
-rw-r--r--src/mongo/db/exec/sbe/stages/project.cpp14
-rw-r--r--src/mongo/db/exec/sbe/util/debug_print.cpp4
-rw-r--r--src/mongo/db/exec/sbe/util/debug_print.h2
-rw-r--r--src/mongo/db/exec/sbe/values/slot.h16
-rw-r--r--src/mongo/db/pipeline/document_source_cursor.h4
-rw-r--r--src/mongo/db/pipeline/plan_explainer_pipeline.cpp10
-rw-r--r--src/mongo/db/pipeline/plan_explainer_pipeline.h1
-rw-r--r--src/mongo/db/query/explain.cpp31
-rw-r--r--src/mongo/db/query/get_executor.cpp3
-rw-r--r--src/mongo/db/query/plan_executor_factory.cpp4
-rw-r--r--src/mongo/db/query/plan_executor_sbe.cpp2
-rw-r--r--src/mongo/db/query/plan_explainer.h10
-rw-r--r--src/mongo/db/query/plan_explainer_factory.cpp9
-rw-r--r--src/mongo/db/query/plan_explainer_factory.h5
-rw-r--r--src/mongo/db/query/plan_explainer_impl.cpp5
-rw-r--r--src/mongo/db/query/plan_explainer_impl.h1
-rw-r--r--src/mongo/db/query/plan_explainer_sbe.cpp30
-rw-r--r--src/mongo/db/query/plan_explainer_sbe.h21
-rw-r--r--src/mongo/db/query/plan_ranker_util.h8
-rw-r--r--src/mongo/db/query/query_planner.cpp3
-rw-r--r--src/mongo/db/query/query_planner.h3
-rw-r--r--src/mongo/db/query/sbe_cached_solution_planner.cpp6
-rw-r--r--src/mongo/db/query/sbe_multi_planner.cpp3
50 files changed, 483 insertions, 281 deletions
diff --git a/jstests/core/bittest.js b/jstests/core/bittest.js
index e5885be06de..9b3fa8639b6 100644
--- a/jstests/core/bittest.js
+++ b/jstests/core/bittest.js
@@ -10,7 +10,7 @@ var coll = db.jstests_bitwise;
function assertQueryCorrect(query, count) {
var explain = coll.find(query).explain("executionStats");
- assert(isCollscan(db, explain.queryPlanner.winningPlan),
+ assert(isCollscan(db, getWinningPlan(explain.queryPlanner)),
"expected bit test query plan to be COLLSCAN");
assert.eq(
count, explain.executionStats.nReturned, "bit test query not returning correct documents");
diff --git a/jstests/core/coveredIndex1.js b/jstests/core/coveredIndex1.js
index a61b6381db3..34e98bd76a0 100644
--- a/jstests/core/coveredIndex1.js
+++ b/jstests/core/coveredIndex1.js
@@ -40,7 +40,7 @@ function assertIfQueryIsCovered(query, projection, isCovered, hint) {
assert(explain.hasOwnProperty("queryPlanner"), tojson(explain));
assert(explain.queryPlanner.hasOwnProperty("winningPlan"), tojson(explain));
- const winningPlan = explain.queryPlanner.winningPlan;
+ const winningPlan = getWinningPlan(explain.queryPlanner);
if (isCovered) {
assert(isIndexOnly(db, winningPlan),
"Query " + tojson(query) + " with projection " + tojson(projection) +
diff --git a/jstests/core/coveredIndex2.js b/jstests/core/coveredIndex2.js
index dc1fb1cf585..3b7891eaa06 100644
--- a/jstests/core/coveredIndex2.js
+++ b/jstests/core/coveredIndex2.js
@@ -5,31 +5,34 @@
// assumes_unsharded_collection,
// requires_fastcount,
// ]
+(function() {
+"use strict";
-t = db["jstests_coveredIndex2"];
+const t = db["jstests_coveredIndex2"];
t.drop();
// Include helpers for analyzing explain output.
load("jstests/libs/analyze_plan.js");
-t.save({a: 1});
-t.save({a: 2});
+assert.commandWorked(t.insert({a: 1}));
+assert.commandWorked(t.insert({a: 2}));
assert.eq(t.findOne({a: 1}).a, 1, "Cannot find right record");
assert.eq(t.count(), 2, "Not right length");
// use simple index
-t.createIndex({a: 1});
-var plan = t.find({a: 1}).explain();
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert.commandWorked(t.createIndex({a: 1}));
+let plan = t.find({a: 1}).explain();
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"Find using covered index but all fields are returned");
-var plan = t.find({a: 1}, {a: 1}).explain();
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+plan = t.find({a: 1}, {a: 1}).explain();
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"Find using covered index but _id is returned");
-var plan = t.find({a: 1}, {a: 1, _id: 0}).explain();
-assert(isIndexOnly(db, plan.queryPlanner.winningPlan), "Find is not using covered index");
+plan = t.find({a: 1}, {a: 1, _id: 0}).explain();
+assert(isIndexOnly(db, getWinningPlan(plan.queryPlanner)), "Find is not using covered index");
// add multikey
-t.save({a: [3, 4]});
-var plan = t.find({a: 1}, {a: 1, _id: 0}).explain();
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert.commandWorked(t.insert({a: [3, 4]}));
+plan = t.find({a: 1}, {a: 1, _id: 0}).explain();
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"Find is using covered index even after multikey insert");
+}());
diff --git a/jstests/core/covered_index_negative_1.js b/jstests/core/covered_index_negative_1.js
index 5b6f68a56a2..f148a32afc7 100644
--- a/jstests/core/covered_index_negative_1.js
+++ b/jstests/core/covered_index_negative_1.js
@@ -23,7 +23,7 @@ coll.createIndex({f: "hashed"});
// Test no projection
var plan =
coll.find({a: 10, b: "strvar_10", c: 0}).hint({a: 1, b: -1, c: 1}).explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.1 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
@@ -33,7 +33,7 @@ assert.neq(0,
var plan = coll.find({a: 10, b: "strvar_10", c: 0}, {a: 1, b: 1, c: 1})
.hint({a: 1, b: -1, c: 1})
.explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.2 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
@@ -41,7 +41,7 @@ assert.neq(0,
// Test projection of non-indexed field
var plan = coll.find({d: 100}, {d: 1, c: 1, _id: 0}).hint({d: 1}).explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.3 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
@@ -49,7 +49,7 @@ assert.neq(0,
// Test query and projection on a multi-key index
var plan = coll.find({e: 99}, {e: 1, _id: 0}).hint({e: 1}).explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.4 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
@@ -77,7 +77,7 @@ assert.neq(0,
var plan = coll.find({d: {$lt: 1000}}, {a: 1, b: 1, c: 1, _id: 0})
.hint({a: 1, b: -1, c: 1})
.explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.7 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
@@ -85,7 +85,7 @@ assert.neq(0,
// Test query on hashed indexed field
var plan = coll.find({f: 10}, {f: 1, _id: 0}).hint({f: "hashed"}).explain("executionStats");
-assert(!isIndexOnly(db, plan.queryPlanner.winningPlan),
+assert(!isIndexOnly(db, getWinningPlan(plan.queryPlanner)),
"negative.1.8 - indexOnly should be false on a non covered query");
assert.neq(0,
plan.executionStats.totalDocsExamined,
diff --git a/jstests/core/covered_multikey.js b/jstests/core/covered_multikey.js
index c1bea4d99fc..9270a2b2c20 100644
--- a/jstests/core/covered_multikey.js
+++ b/jstests/core/covered_multikey.js
@@ -23,8 +23,9 @@ assert.commandWorked(coll.createIndex({a: 1, b: 1}));
assert.eq(1, coll.find({a: 1, b: 2}, {_id: 0, a: 1}).itcount());
assert.eq({a: 1}, coll.findOne({a: 1, b: 2}, {_id: 0, a: 1}));
let explainRes = coll.explain("queryPlanner").find({a: 1, b: 2}, {_id: 0, a: 1}).finish();
-assert(isIxscan(db, explainRes.queryPlanner.winningPlan));
-assert(!planHasStage(db, explainRes.queryPlanner.winningPlan, "FETCH"));
+let winningPlan = getWinningPlan(explainRes.queryPlanner);
+assert(isIxscan(db, winningPlan));
+assert(!planHasStage(db, winningPlan, "FETCH"));
coll.drop();
assert.commandWorked(coll.insert({a: 1, b: [1, 2, 3], c: 3, d: 5}));
@@ -39,73 +40,81 @@ explainRes = coll.explain("queryPlanner")
.find({a: 1, b: 1}, {_id: 0, c: 1, d: 1})
.sort({c: -1, d: -1})
.finish();
-assert(!planHasStage(db, explainRes.queryPlanner.winningPlan, "FETCH"));
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+assert(!planHasStage(db, winningPlan, "FETCH"));
// Verify that a query cannot be covered over a path which is multikey due to an empty array.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.insert({a: []}));
assert.commandWorked(coll.createIndex({a: 1}));
assert.eq({a: []}, coll.findOne({a: []}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: []}, {_id: 0, a: 1}).finish();
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "IXSCAN"));
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "FETCH"));
-let ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+assert(planHasStage(db, winningPlan, "IXSCAN"));
+assert(planHasStage(db, winningPlan, "FETCH"));
+let ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
// Verify that a query cannot be covered over a path which is multikey due to a single-element
// array.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.insert({a: [2]}));
assert.commandWorked(coll.createIndex({a: 1}));
assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish();
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "IXSCAN"));
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "FETCH"));
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+assert(planHasStage(db, winningPlan, "IXSCAN"));
+assert(planHasStage(db, winningPlan, "FETCH"));
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
// Verify that a query cannot be covered over a path which is multikey due to a single-element
// array, where the path is made multikey by an update rather than an insert.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.insert({a: 2}));
assert.commandWorked(coll.createIndex({a: 1}));
assert.commandWorked(coll.update({}, {$set: {a: [2]}}));
assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish();
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "IXSCAN"));
-assert(planHasStage(db, explainRes.queryPlanner.winningPlan, "FETCH"));
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+assert(planHasStage(db, winningPlan, "IXSCAN"));
+assert(planHasStage(db, winningPlan, "FETCH"));
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
// Verify that a trailing empty array makes a 2dsphere index multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: {b: 1}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(false, ixscanStage.isMultiKey);
assert.commandWorked(coll.insert({a: {b: []}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
// Verify that a mid-path empty array makes a 2dsphere index multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: [], c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
// Verify that a single-element array makes a 2dsphere index multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: {b: [3]}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
-ixscanStage = getPlanStage(explainRes.queryPlanner.winningPlan, "IXSCAN");
+winningPlan = getWinningPlan(explainRes.queryPlanner);
+ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
}());
diff --git a/jstests/core/distinct_compound_index.js b/jstests/core/distinct_compound_index.js
index 4e1dfcc5cad..ba2c3ec280f 100644
--- a/jstests/core/distinct_compound_index.js
+++ b/jstests/core/distinct_compound_index.js
@@ -21,9 +21,9 @@ assert.commandWorked(coll.createIndex({a: 1, b: 1}));
const explain_distinct_with_query = coll.explain("executionStats").distinct('b', {a: 1});
assert.commandWorked(explain_distinct_with_query);
-assert(planHasStage(db, explain_distinct_with_query.queryPlanner.winningPlan, "DISTINCT_SCAN"));
-assert(
- planHasStage(db, explain_distinct_with_query.queryPlanner.winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, getWinningPlan(explain_distinct_with_query.queryPlanner), "DISTINCT_SCAN"));
+assert(planHasStage(
+ db, getWinningPlan(explain_distinct_with_query.queryPlanner), "PROJECTION_COVERED"));
// If the collection is sharded, we expect at most 2 distinct values per shard. If the
// collection is not sharded, we expect 2 returned.
assert.lte(explain_distinct_with_query.executionStats.nReturned,
@@ -31,8 +31,9 @@ assert.lte(explain_distinct_with_query.executionStats.nReturned,
const explain_distinct_without_query = coll.explain("executionStats").distinct('b');
assert.commandWorked(explain_distinct_without_query);
-assert(planHasStage(db, explain_distinct_without_query.queryPlanner.winningPlan, "COLLSCAN"));
-assert(!planHasStage(db, explain_distinct_without_query.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+assert(planHasStage(db, getWinningPlan(explain_distinct_without_query.queryPlanner), "COLLSCAN"));
+assert(!planHasStage(
+ db, getWinningPlan(explain_distinct_without_query.queryPlanner), "DISTINCT_SCAN"));
assert.eq(40, explain_distinct_without_query.executionStats.nReturned);
// Verify that compound special indexes such as '2dsphere' and 'text' can never use index to answer
diff --git a/jstests/core/distinct_multikey.js b/jstests/core/distinct_multikey.js
index 7208b67fb64..6d5ccd21252 100644
--- a/jstests/core/distinct_multikey.js
+++ b/jstests/core/distinct_multikey.js
@@ -17,19 +17,21 @@ assert.commandWorked(coll.insert({a: [5, 6, 7]}));
let result = coll.distinct("a");
assert.eq([1, 2, 3, 4, 5, 6, 7], result.sort());
let explain = coll.explain("queryPlanner").distinct("a");
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+let winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
// Test that distinct can correctly use a multikey index when there is a predicate. This query
// should not be eligible for the distinct scan and cannot be covered.
result = coll.distinct("a", {a: 3});
assert.eq([1, 2, 3, 4], result.sort());
explain = coll.explain("queryPlanner").distinct("a", {a: 3});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "FETCH"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "IXSCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "FETCH"));
+assert(planHasStage(db, winningPlan, "IXSCAN"));
// Test distinct over a dotted multikey field, with a predicate.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({"a.b": 1}));
assert.commandWorked(coll.insert({a: {b: [1, 2, 3]}}));
assert.commandWorked(coll.insert({a: {b: [2, 3, 4]}}));
@@ -37,12 +39,13 @@ assert.commandWorked(coll.insert({a: {b: [2, 3, 4]}}));
result = coll.distinct("a.b", {"a.b": 3});
assert.eq([1, 2, 3, 4], result.sort());
explain = coll.explain("queryPlanner").distinct("a.b", {"a.b": 3});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "FETCH"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "IXSCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "FETCH"));
+assert(planHasStage(db, winningPlan, "IXSCAN"));
// Test that the distinct scan can be used when there is a predicate and the index is not
// multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({a: 1}));
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorked(coll.insert({a: 2}));
@@ -51,12 +54,13 @@ assert.commandWorked(coll.insert({a: 3}));
result = coll.distinct("a", {a: {$gte: 2}});
assert.eq([2, 3], result.sort());
explain = coll.explain("queryPlanner").distinct("a", {a: {$gte: 2}});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
// Test a distinct which can use a multikey index, where the field being distinct'ed is not
// multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({a: 1, b: 1}));
assert.commandWorked(coll.insert({a: 1, b: [2, 3]}));
assert.commandWorked(coll.insert({a: 8, b: [3, 4]}));
@@ -65,18 +69,20 @@ assert.commandWorked(coll.insert({a: 7, b: [4, 5]}));
result = coll.distinct("a", {a: {$gte: 2}});
assert.eq([7, 8], result.sort());
explain = coll.explain("queryPlanner").distinct("a", {a: {$gte: 2}});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
// Test distinct over a trailing multikey field.
result = coll.distinct("b", {a: {$gte: 2}});
assert.eq([3, 4, 5], result.sort());
explain = coll.explain("queryPlanner").distinct("b", {a: {$gte: 2}});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "FETCH"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "IXSCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "FETCH"));
+assert(planHasStage(db, winningPlan, "IXSCAN"));
// Test distinct over a trailing non-multikey field, where the leading field is multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({a: 1, b: 1}));
assert.commandWorked(coll.insert({a: [2, 3], b: 1}));
assert.commandWorked(coll.insert({a: [3, 4], b: 8}));
@@ -85,11 +91,12 @@ assert.commandWorked(coll.insert({a: [3, 5], b: 7}));
result = coll.distinct("b", {a: 3});
assert.eq([1, 7, 8], result.sort());
explain = coll.explain("queryPlanner").distinct("b", {a: 3});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
// Test distinct over a trailing non-multikey dotted path where the leading field is multikey.
-coll.drop();
+assert(coll.drop());
assert.commandWorked(coll.createIndex({a: 1, "b.c": 1}));
assert.commandWorked(coll.insert({a: [2, 3], b: {c: 1}}));
assert.commandWorked(coll.insert({a: [3, 4], b: {c: 8}}));
@@ -98,6 +105,7 @@ assert.commandWorked(coll.insert({a: [3, 5], b: {c: 7}}));
result = coll.distinct("b.c", {a: 3});
assert.eq([1, 7, 8], result.sort());
explain = coll.explain("queryPlanner").distinct("b.c", {a: 3});
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_DEFAULT"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "PROJECTION_DEFAULT"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
}());
diff --git a/jstests/core/distinct_multikey_dotted_path.js b/jstests/core/distinct_multikey_dotted_path.js
index 158c4d61f4b..c8530fe6799 100644
--- a/jstests/core/distinct_multikey_dotted_path.js
+++ b/jstests/core/distinct_multikey_dotted_path.js
@@ -51,7 +51,7 @@ function distinctResultsFromPipeline(pipeline) {
assert.sameMembers([1, 2, 3, null], results);
const expl = coll.explain().distinct("a.b.c");
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"), expl);
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "DISTINCT_SCAN"), expl);
// Do an equivalent query using $group.
const pipeline = getAggPipelineForDistinct("a.b.c");
@@ -68,7 +68,7 @@ function distinctResultsFromPipeline(pipeline) {
const expl = coll.explain().distinct("a.b.c", numPredicate);
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"), expl);
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "DISTINCT_SCAN"), expl);
const pipeline = [{$match: numPredicate}].concat(getAggPipelineForDistinct("a.b.c"));
const aggResults = distinctResultsFromPipeline(pipeline);
@@ -92,7 +92,7 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
assert.sameMembers([1, 2, 3, 4, 5, 6, null, undefined], multiKeyResults);
const expl = coll.explain().distinct("a.b.c");
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "DISTINCT_SCAN"));
// Not running same query with $group now that the field is multikey. See comment above.
})();
@@ -108,8 +108,9 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
assert.sameMembers([4, 5, 6], results);
const expl = coll.explain().distinct("a.b.c", pred);
- assert.eq(false, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"), expl);
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "IXSCAN"), expl);
+ const winningPlan = getWinningPlan(expl.queryPlanner);
+ assert.eq(false, planHasStage(db, winningPlan, "DISTINCT_SCAN"), expl);
+ assert.eq(true, planHasStage(db, winningPlan, "IXSCAN"), expl);
// Not running same query with $group now that the field is multikey. See comment above.
})();
@@ -134,7 +135,7 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
multiKeyResults);
const expl = coll.explain().distinct("a.b");
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "DISTINCT_SCAN"));
// Not running same query with $group now that the field is multikey. See comment above.
})();
@@ -152,8 +153,9 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
multiKeyResults);
const expl = coll.explain().distinct("a.b", pred);
- assert.eq(false, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"));
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "IXSCAN"));
+ const winningPlan = getWinningPlan(expl.queryPlanner);
+ assert.eq(false, planHasStage(db, winningPlan, "DISTINCT_SCAN"));
+ assert.eq(true, planHasStage(db, winningPlan, "IXSCAN"));
// Not running same query with $group now that the field is multikey. See comment above.
})();
@@ -165,7 +167,7 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
assert.eq(res, [{c: 4}]);
const expl = coll.explain().distinct("a.b.0");
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "COLLSCAN"), expl);
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "COLLSCAN"), expl);
// Will not attempt the equivalent query with aggregation, since $group by "a.b.0" will
// only treat '0' as a field name (not array index).
@@ -179,7 +181,7 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
assert.sameMembers(res, [{c: 4}, "hello world"]);
const expl = coll.explain().distinct("a.b.0");
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"), expl);
+ assert.eq(true, planHasStage(db, getWinningPlan(expl.queryPlanner), "DISTINCT_SCAN"), expl);
// Will not attempt the equivalent query with aggregation, since $group by "a.b.0" will
// only treat '0' as a field name (not array index).
@@ -197,8 +199,9 @@ assert.commandWorked(coll.insert({a: {b: {c: []}}}));
assert.commandWorked(coll.insert({a: [1, 2, 3]}));
const expl = coll.explain().distinct("a.b.0", pred);
- assert.eq(false, planHasStage(db, expl.queryPlanner.winningPlan, "DISTINCT_SCAN"), expl);
- assert.eq(true, planHasStage(db, expl.queryPlanner.winningPlan, "IXSCAN"), expl);
+ const winningPlan = getWinningPlan(expl.queryPlanner);
+ assert.eq(false, planHasStage(db, winningPlan, "DISTINCT_SCAN"), expl);
+ assert.eq(true, planHasStage(db, winningPlan, "IXSCAN"), expl);
// Will not attempt the equivalent query with aggregation, since $group by "a.b.0" will
// only treat '0' as a field name (not array index).
diff --git a/jstests/core/distinct_with_hashed_index.js b/jstests/core/distinct_with_hashed_index.js
index 4cfd6a39a22..8476c5c5f14 100644
--- a/jstests/core/distinct_with_hashed_index.js
+++ b/jstests/core/distinct_with_hashed_index.js
@@ -30,32 +30,34 @@ assert.commandWorked(coll.createIndex({a: -1, b: "hashed", c: 1}));
// 'distinct' on non-hashed prefix fields can use DISTINCT_SCAN.
assert.eq(100, coll.distinct("a").length);
let plan = coll.explain("executionStats").distinct("a");
-assert(isIndexOnly(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "DISTINCT_SCAN"),
- plan.queryPlanner.winningPlan);
+let winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIndexOnly(db, winningPlan), winningPlan);
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"), winningPlan);
// 'distinct' on non-prefix fields cannot use index.
assert.eq(26, coll.distinct("b").length);
plan = coll.explain("executionStats").distinct("b");
-assert(isCollscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isCollscan(db, winningPlan), winningPlan);
assert.eq(10, coll.distinct("c").length);
plan = coll.explain("executionStats").distinct("c");
-assert(isCollscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isCollscan(db, winningPlan), winningPlan);
// A 'distinct' command that cannot use 'DISTINCT_SCAN', can use index scan for the query part.
assert.eq([2], coll.distinct("c", {a: 12, b: {subObj: "str_12"}}));
plan = coll.explain("executionStats").distinct("c", {a: 12, b: {subObj: "str_12"}});
-assert(isIxscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(!planHasStage(db, plan.queryPlanner.winningPlan, "DISTINCT_SCAN"),
- plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "FETCH"), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIxscan(db, winningPlan), winningPlan);
+assert(!planHasStage(db, winningPlan, "DISTINCT_SCAN"), winningPlan);
+assert(planHasStage(db, winningPlan, "FETCH"), winningPlan);
// 'distinct' with query predicate on index field can get converted to DISTINCT_SCAN.
assert.eq([2], coll.distinct("c", {a: 12}));
plan = coll.explain("executionStats").distinct("c", {a: 12});
-assert(isIndexOnly(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "DISTINCT_SCAN"),
- plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIndexOnly(db, winningPlan), winningPlan);
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"), winningPlan);
// Can use index scan to answer the query even when the key of distinct is not eligible from
// DISTINCT_SCAN. Since the query has a point predicate on "b", we need a filter, since the bounds
@@ -63,10 +65,10 @@ assert(planHasStage(db, plan.queryPlanner.winningPlan, "DISTINCT_SCAN"),
// filter.
assert.eq([], coll.distinct("c", {a: 12, b: 4}));
plan = coll.explain("executionStats").distinct("c", {a: 12, b: 4});
-assert(isIxscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "FETCH"), plan.queryPlanner.winningPlan);
-assert(!planHasStage(db, plan.queryPlanner.winningPlan, "DISTINCT_SCAN"),
- plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIxscan(db, winningPlan), winningPlan);
+assert(planHasStage(db, winningPlan, "FETCH"), winningPlan);
+assert(!planHasStage(db, winningPlan, "DISTINCT_SCAN"), winningPlan);
// Verify that simple $group on non-hashed field can use DISTINCT_SCAN.
let pipeline = [{$group: {_id: "$a"}}];
@@ -114,29 +116,34 @@ assert.commandWorked(coll.createIndex({b: "hashed", c: 1}));
// different values as the same and return only the first one.
assert.eq(26, coll.distinct("b").length);
plan = coll.explain("executionStats").distinct("b");
-assert(isCollscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isCollscan(db, winningPlan), winningPlan);
// 'distinct' with query predicate can use index for the query part.
assert.eq([1], coll.distinct("b", {b: 1}));
plan = coll.explain("executionStats").distinct("b", {b: 1});
-assert(isIxscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "FETCH"), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIxscan(db, winningPlan), winningPlan);
+assert(planHasStage(db, winningPlan, "FETCH"), winningPlan);
// 'distinct' with query predicate cannot use index when query cannot use index.
assert.eq([5], coll.distinct("b", {b: {$lt: 6, $gt: 4}}));
plan = coll.explain("executionStats").distinct("b", {b: {$lt: 6, $gt: 4}});
-assert(isCollscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isCollscan(db, winningPlan), winningPlan);
// 'distinct' with query predicate can use index for the query part.
assert.eq([2], coll.distinct("c", {a: 12, b: 12}));
plan = coll.explain("executionStats").distinct("c", {a: 12, b: 12});
-assert(isIxscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
-assert(planHasStage(db, plan.queryPlanner.winningPlan, "FETCH"), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isIxscan(db, winningPlan), winningPlan);
+assert(planHasStage(db, winningPlan, "FETCH"), winningPlan);
// 'distinct' on non-prefix fields cannot use index.
assert.sameMembers([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], coll.distinct("c"));
plan = coll.explain("executionStats").distinct("c");
-assert(isCollscan(db, plan.queryPlanner.winningPlan), plan.queryPlanner.winningPlan);
+winningPlan = getWinningPlan(plan.queryPlanner);
+assert(isCollscan(db, winningPlan), winningPlan);
// Verify that simple $group on hashed field cannot use DISTINCT_SCAN.
pipeline = [{$group: {_id: "$b"}}];
diff --git a/jstests/core/explain_distinct.js b/jstests/core/explain_distinct.js
index 48840c52171..1282397c4df 100644
--- a/jstests/core/explain_distinct.js
+++ b/jstests/core/explain_distinct.js
@@ -12,11 +12,11 @@
load("jstests/libs/analyze_plan.js");
-var collName = "jstests_explain_distinct";
-var coll = db[collName];
+const collName = "jstests_explain_distinct";
+const coll = db[collName];
function runDistinctExplain(collection, keyString, query) {
- var distinctCmd = {distinct: collection.getName(), key: keyString};
+ const distinctCmd = {distinct: collection.getName(), key: keyString};
if (typeof query !== 'undefined') {
distinctCmd.query = query;
@@ -28,12 +28,13 @@ function runDistinctExplain(collection, keyString, query) {
coll.drop();
// Collection doesn't exist.
-var explain = runDistinctExplain(coll, 'a', {});
+let explain = runDistinctExplain(coll, 'a', {});
assert.commandWorked(explain);
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "EOF"));
+let winningPlan = getWinningPlan(explain.queryPlanner);
+assert(planHasStage(db, winningPlan, "EOF"));
// Insert the data to perform distinct() on.
-for (var i = 0; i < 10; i++) {
+for (let i = 0; i < 10; i++) {
assert.commandWorked(coll.insert({a: 1, b: 1}));
assert.commandWorked(coll.insert({a: 2, c: 1}));
}
@@ -49,22 +50,24 @@ assert.commandWorked(runDistinctExplain(coll, 'a', null));
assert.commandWorked(runDistinctExplain(coll, 'a'));
assert.eq([1], coll.distinct('b'));
-var explain = runDistinctExplain(coll, 'b', {});
+explain = runDistinctExplain(coll, 'b', {});
assert.commandWorked(explain);
+winningPlan = getWinningPlan(explain.queryPlanner);
assert.eq(20, explain.executionStats.nReturned);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, winningPlan));
assert.commandWorked(coll.createIndex({a: 1}));
assert.eq([1, 2], coll.distinct('a'));
-var explain = runDistinctExplain(coll, 'a', {});
+explain = runDistinctExplain(coll, 'a', {});
assert.commandWorked(explain);
+winningPlan = getWinningPlan(explain.queryPlanner);
assert.eq(2, explain.executionStats.nReturned);
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
// Check that the DISTINCT_SCAN stage has the correct stats.
-var stage = getPlanStage(explain.queryPlanner.winningPlan, "DISTINCT_SCAN");
+let stage = getPlanStage(explain.queryPlanner.winningPlan, "DISTINCT_SCAN");
assert.eq({a: 1}, stage.keyPattern);
assert.eq("a_1", stage.indexName);
assert.eq(false, stage.isMultiKey);
@@ -77,17 +80,19 @@ assert("indexBounds" in stage);
assert.commandWorked(coll.createIndex({a: 1, b: 1}));
assert.eq([1], coll.distinct('a', {a: 1}));
-var explain = runDistinctExplain(coll, 'a', {a: 1});
+explain = runDistinctExplain(coll, 'a', {a: 1});
assert.commandWorked(explain);
+winningPlan = getWinningPlan(explain.queryPlanner);
assert.eq(1, explain.executionStats.nReturned);
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
assert.eq([1], coll.distinct('b', {a: 1}));
-var explain = runDistinctExplain(coll, 'b', {a: 1});
+explain = runDistinctExplain(coll, 'b', {a: 1});
assert.commandWorked(explain);
+winningPlan = getWinningPlan(explain.queryPlanner);
assert.eq(1, explain.executionStats.nReturned);
-assert(!planHasStage(db, explain.queryPlanner.winningPlan, "FETCH"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "PROJECTION_COVERED"));
-assert(planHasStage(db, explain.queryPlanner.winningPlan, "DISTINCT_SCAN"));
+assert(!planHasStage(db, winningPlan, "FETCH"));
+assert(planHasStage(db, winningPlan, "PROJECTION_COVERED"));
+assert(planHasStage(db, winningPlan, "DISTINCT_SCAN"));
})();
diff --git a/jstests/core/explain_multikey.js b/jstests/core/explain_multikey.js
index b0d42db9d2d..9bea359edb4 100644
--- a/jstests/core/explain_multikey.js
+++ b/jstests/core/explain_multikey.js
@@ -10,8 +10,8 @@
load("jstests/libs/analyze_plan.js");
-var coll = db.explain_multikey;
-var keyPattern = {
+const coll = db.explain_multikey;
+const keyPattern = {
a: 1,
"b.c": 1,
"b.d": 1,
@@ -35,12 +35,13 @@ function createIndexAndRunExplain(testOptions) {
assert.commandWorked(coll.createIndex(keyPattern));
assert.commandWorked(coll.insert(testOptions.docToInsert));
- var explain = db.runCommand({explain: testOptions.commandObj});
+ const explain = db.runCommand({explain: testOptions.commandObj});
assert.commandWorked(explain);
+ const winningPlan = getWinningPlan(explain.queryPlanner);
- assert(planHasStage(db, explain.queryPlanner.winningPlan, testOptions.stage),
+ assert(planHasStage(db, winningPlan, testOptions.stage),
"expected stage to be present: " + tojson(explain));
- return getPlanStage(explain.queryPlanner.winningPlan, testOptions.stage);
+ return getPlanStage(winningPlan, testOptions.stage);
}
// Calls createIndexAndRunExplain() twice: once with a document that causes the created index to
@@ -51,7 +52,7 @@ function verifyMultikeyInfoInExplainOutput(testOptions) {
a: 1,
b: [{c: ["w", "x"], d: 3}, {c: ["y", "z"], d: 4}],
};
- var stage = createIndexAndRunExplain(testOptions);
+ let stage = createIndexAndRunExplain(testOptions);
assert.eq(true, stage.isMultiKey, "expected index to be multikey: " + tojson(stage));
assert.eq({a: [], "b.c": ["b", "b.c"], "b.d": ["b"]}, stage.multiKeyPaths, tojson(stage));
diff --git a/jstests/core/index_filter_catalog_independent.js b/jstests/core/index_filter_catalog_independent.js
index 71adead3863..35b2bf4134a 100644
--- a/jstests/core/index_filter_catalog_independent.js
+++ b/jstests/core/index_filter_catalog_independent.js
@@ -54,7 +54,7 @@ assertOneIndexFilter({x: 3}, [{x: 1, y: 1}]);
let explain = assert.commandWorked(coll.find({x: 3}).explain());
checkIndexFilterSet(explain, true);
-assertIsIxScanOnIndex(explain.queryPlanner.winningPlan, {x: 1, y: 1});
+assertIsIxScanOnIndex(getWinningPlan(explain.queryPlanner), {x: 1, y: 1});
// Drop an index. The filter should not change.
assert.commandWorked(coll.dropIndex({x: 1, y: 1}));
@@ -64,13 +64,13 @@ assertOneIndexFilter({x: 3}, [{x: 1, y: 1}]);
// Since we dropped the {x: 1, y: 1} index, a COLLSCAN must be used.
explain = coll.find({x: 3}).explain();
checkIndexFilterSet(explain, true);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
// Create another index. This should not change whether the index filter is applied.
assert.commandWorked(coll.createIndex({x: 1, z: 1}));
explain = assert.commandWorked(coll.find({x: 3}).explain());
checkIndexFilterSet(explain, true);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
// Changing the catalog and then setting an index filter should not result in duplicate entries.
assert.commandWorked(coll.createIndex({x: 1, a: 1}));
@@ -84,5 +84,5 @@ assertOneIndexFilter({x: 3}, [{x: 1, y: 1}]);
explain = assert.commandWorked(coll.find({x: 3}).explain());
checkIndexFilterSet(explain, true);
-assertIsIxScanOnIndex(explain.queryPlanner.winningPlan, {x: 1, y: 1});
+assertIsIxScanOnIndex(getWinningPlan(explain.queryPlanner), {x: 1, y: 1});
})();
diff --git a/jstests/core/index_filter_collation.js b/jstests/core/index_filter_collation.js
index 7734acdfa38..74ec64f3b3c 100644
--- a/jstests/core/index_filter_collation.js
+++ b/jstests/core/index_filter_collation.js
@@ -77,10 +77,10 @@ function assertIsIxScanOnIndex(winningPlan, keyPattern) {
// Run the queries and be sure the correct indexes are used.
let explain = coll.find({x: 3}).explain();
checkIndexFilterSet(explain, true);
-assertIsIxScanOnIndex(explain.queryPlanner.winningPlan, {x: 1, y: 1});
+assertIsIxScanOnIndex(getWinningPlan(explain.queryPlanner), {x: 1, y: 1});
// Run the queries and be sure the correct indexes are used.
explain = coll.find({x: 3}).collation(caseInsensitive).explain();
checkIndexFilterSet(explain, true);
-assertIsIxScanOnIndex(explain.queryPlanner.winningPlan, {x: 1});
+assertIsIxScanOnIndex(getWinningPlan(explain.queryPlanner), {x: 1});
})();
diff --git a/jstests/core/index_filter_on_hidden_index.js b/jstests/core/index_filter_on_hidden_index.js
index 0315bd7fad7..3fe468a39a3 100644
--- a/jstests/core/index_filter_on_hidden_index.js
+++ b/jstests/core/index_filter_on_hidden_index.js
@@ -56,7 +56,7 @@ function validateIxscanOrCollscanUsed(queryShape, idxName) {
if (idxName) {
// Expect the given index was used.
- const ixScanStage = getPlanStages(explain.queryPlanner.winningPlan, "IXSCAN")[0];
+ const ixScanStage = getPlanStages(getWinningPlan(explain.queryPlanner), "IXSCAN")[0];
assert(ixScanStage, `Index '${idxName}' was not used.`);
assert.eq(ixScanStage.indexName, idxName, `Index '${idxName}' was not used.`);
} else {
diff --git a/jstests/core/index_multikey.js b/jstests/core/index_multikey.js
index 32ff3bd58c4..58c80bea66d 100644
--- a/jstests/core/index_multikey.js
+++ b/jstests/core/index_multikey.js
@@ -14,7 +14,7 @@ coll.drop();
function getIndexScanExplainOutput() {
const explain = coll.find().hint({a: 1, b: 1}).explain();
assert.commandWorked(explain);
- return getPlanStage(explain.queryPlanner.winningPlan, "IXSCAN");
+ return getPlanStage(getWinningPlan(explain.queryPlanner), "IXSCAN");
}
assert.commandWorked(coll.createIndex({a: 1, b: 1}));
diff --git a/jstests/core/index_partial_read_ops.js b/jstests/core/index_partial_read_ops.js
index 6e13b5e3ba4..c13e8832340 100644
--- a/jstests/core/index_partial_read_ops.js
+++ b/jstests/core/index_partial_read_ops.js
@@ -13,8 +13,8 @@ load("jstests/libs/analyze_plan.js");
(function() {
"use strict";
-var explain;
-var coll = db.index_partial_read_ops;
+let explain;
+const coll = db.index_partial_read_ops;
coll.drop();
assert.commandWorked(coll.createIndex({x: 1}, {partialFilterExpression: {a: {$lte: 1.5}}}));
@@ -28,21 +28,21 @@ assert.commandWorked(coll.insert({x: 6, a: 1})); // In index.
// find() operations that should use index.
explain = coll.explain('executionStats').find({x: 6, a: 1}).finish();
assert.eq(1, explain.executionStats.nReturned);
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
explain = coll.explain('executionStats').find({x: {$gt: 1}, a: 1}).finish();
assert.eq(1, explain.executionStats.nReturned);
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
explain = coll.explain('executionStats').find({x: 6, a: {$lte: 1}}).finish();
assert.eq(1, explain.executionStats.nReturned);
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// find() operations that should not use index.
explain = coll.explain('executionStats').find({x: 6, a: {$lt: 1.6}}).finish();
assert.eq(1, explain.executionStats.nReturned);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
explain = coll.explain('executionStats').find({x: 6}).finish();
assert.eq(1, explain.executionStats.nReturned);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
//
// Verify basic functionality with the count command.
@@ -50,11 +50,11 @@ assert(isCollscan(db, explain.queryPlanner.winningPlan));
// Count operation that should use index.
explain = coll.explain('executionStats').count({x: {$gt: 1}, a: 1});
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// Count operation that should not use index.
explain = coll.explain('executionStats').count({x: {$gt: 1}, a: 2});
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
//
// Verify basic functionality with the aggregate command.
@@ -62,11 +62,11 @@ assert(isCollscan(db, explain.queryPlanner.winningPlan));
// Aggregate operation that should use index.
explain = coll.aggregate([{$match: {x: {$gt: 1}, a: 1}}], {explain: true});
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// Aggregate operation that should not use index.
explain = coll.aggregate([{$match: {x: {$gt: 1}, a: 2}}], {explain: true});
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
//
// Verify basic functionality with the findAndModify command.
@@ -76,13 +76,13 @@ assert(isCollscan(db, explain.queryPlanner.winningPlan));
explain = coll.explain('executionStats')
.findAndModify({query: {x: {$gt: 1}, a: 1}, update: {$inc: {x: 1}}});
assert.eq(1, explain.executionStats.nReturned);
-assert(isIxscan(db, explain.queryPlanner.winningPlan));
+assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// findAndModify operation that should not use index.
explain = coll.explain('executionStats')
.findAndModify({query: {x: {$gt: 1}, a: 2}, update: {$inc: {x: 1}}});
assert.eq(1, explain.executionStats.nReturned);
-assert(isCollscan(db, explain.queryPlanner.winningPlan));
+assert(isCollscan(db, getWinningPlan(explain.queryPlanner)));
//
// Verify functionality with multiple overlapping partial indexes on the same key pattern.
diff --git a/jstests/core/or2.js b/jstests/core/or2.js
index 5ce72cf2cd7..be5653ca6a1 100644
--- a/jstests/core/or2.js
+++ b/jstests/core/or2.js
@@ -1,37 +1,40 @@
-t = db.jstests_or2;
-t.drop();
+(function() {
+"use strict";
// Include helpers for analyzing explain output.
load("jstests/libs/analyze_plan.js");
-checkArrs = function(a, b) {
+const t = db.jstests_or2;
+t.drop();
+
+function checkArrs(a, b) {
assert.eq(a.length, b.length);
- aStr = [];
- bStr = [];
+ const aStr = [];
+ const bStr = [];
a.forEach(function(x) {
aStr.push(tojson(x));
});
b.forEach(function(x) {
bStr.push(tojson(x));
});
- for (i = 0; i < aStr.length; ++i) {
+ for (let i = 0; i < aStr.length; ++i) {
assert.neq(-1, bStr.indexOf(aStr[i]));
}
-};
+}
-doTest = function(index) {
+function doTest(index) {
if (index == null) {
index = true;
}
- t.save({_id: 0, x: 0, a: 1});
- t.save({_id: 1, x: 0, a: 2});
- t.save({_id: 2, x: 0, b: 1});
- t.save({_id: 3, x: 0, b: 2});
- t.save({_id: 4, x: 1, a: 1, b: 1});
- t.save({_id: 5, x: 1, a: 1, b: 2});
- t.save({_id: 6, x: 1, a: 2, b: 1});
- t.save({_id: 7, x: 1, a: 2, b: 2});
+ assert.commandWorked(t.insert({_id: 0, x: 0, a: 1}));
+ assert.commandWorked(t.insert({_id: 1, x: 0, a: 2}));
+ assert.commandWorked(t.insert({_id: 2, x: 0, b: 1}));
+ assert.commandWorked(t.insert({_id: 3, x: 0, b: 2}));
+ assert.commandWorked(t.insert({_id: 4, x: 1, a: 1, b: 1}));
+ assert.commandWorked(t.insert({_id: 5, x: 1, a: 1, b: 2}));
+ assert.commandWorked(t.insert({_id: 6, x: 1, a: 2, b: 1}));
+ assert.commandWorked(t.insert({_id: 7, x: 1, a: 2, b: 2}));
assert.throws(function() {
t.find({x: 0, $or: "a"}).toArray();
@@ -43,43 +46,37 @@ doTest = function(index) {
t.find({x: 0, $or: ["a"]}).toArray();
});
- a1 = t.find({x: 0, $or: [{a: 1}]}).toArray();
+ const a1 = t.find({x: 0, $or: [{a: 1}]}).toArray();
checkArrs([{_id: 0, x: 0, a: 1}], a1);
if (index) {
- var explain = t.find({x: 0, $or: [{a: 1}]}).explain();
- assert(isIxscan(db, explain.queryPlanner.winningPlan));
+ const explain = t.find({x: 0, $or: [{a: 1}]}).explain();
+ assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
}
- a1b2 = t.find({x: 1, $or: [{a: 1}, {b: 2}]}).toArray();
+ const a1b2 = t.find({x: 1, $or: [{a: 1}, {b: 2}]}).toArray();
checkArrs([{_id: 4, x: 1, a: 1, b: 1}, {_id: 5, x: 1, a: 1, b: 2}, {_id: 7, x: 1, a: 2, b: 2}],
a1b2);
if (index) {
- var explain = t.find({x: 0, $or: [{a: 1}]}).explain();
- assert(isIxscan(db, explain.queryPlanner.winningPlan));
+ const explain = t.find({x: 0, $or: [{a: 1}]}).explain();
+ assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
}
-
- /*
- t.drop();
- obj = {_id:0,x:10,a:[1,2,3]};
- t.save( obj );
- t.update( {x:10,$or:[ {a:2} ]}, {$set:{'a.$':100}} );
- assert.eq( obj, t.findOne() ); // no change
- */
-};
+}
doTest(false);
-t.createIndex({x: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, a: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, a: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, b: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, b: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, a: 1, b: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, a: 1, b: 1}));
doTest();
+})();
diff --git a/jstests/core/or3.js b/jstests/core/or3.js
index cb980b9ef12..ed7ab8ecd64 100644
--- a/jstests/core/or3.js
+++ b/jstests/core/or3.js
@@ -1,37 +1,40 @@
-t = db.jstests_or3;
-t.drop();
+(function() {
+"use strict";
// Include helpers for analyzing explain output.
load("jstests/libs/analyze_plan.js");
-checkArrs = function(a, b) {
+const t = db.jstests_or3;
+t.drop();
+
+function checkArrs(a, b) {
assert.eq(a.length, b.length);
- aStr = [];
- bStr = [];
+ const aStr = [];
+ const bStr = [];
a.forEach(function(x) {
aStr.push(tojson(x));
});
b.forEach(function(x) {
bStr.push(tojson(x));
});
- for (i = 0; i < aStr.length; ++i) {
+ for (let i = 0; i < aStr.length; ++i) {
assert.neq(-1, bStr.indexOf(aStr[i]));
}
-};
+}
-doTest = function(index) {
+function doTest(index) {
if (index == null) {
index = true;
}
- t.save({_id: 0, x: 0, a: 1});
- t.save({_id: 1, x: 0, a: 2});
- t.save({_id: 2, x: 0, b: 1});
- t.save({_id: 3, x: 0, b: 2});
- t.save({_id: 4, x: 1, a: 1, b: 1});
- t.save({_id: 5, x: 1, a: 1, b: 2});
- t.save({_id: 6, x: 1, a: 2, b: 1});
- t.save({_id: 7, x: 1, a: 2, b: 2});
+ assert.commandWorked(t.insert({_id: 0, x: 0, a: 1}));
+ assert.commandWorked(t.insert({_id: 1, x: 0, a: 2}));
+ assert.commandWorked(t.insert({_id: 2, x: 0, b: 1}));
+ assert.commandWorked(t.insert({_id: 3, x: 0, b: 2}));
+ assert.commandWorked(t.insert({_id: 4, x: 1, a: 1, b: 1}));
+ assert.commandWorked(t.insert({_id: 5, x: 1, a: 1, b: 2}));
+ assert.commandWorked(t.insert({_id: 6, x: 1, a: 2, b: 1}));
+ assert.commandWorked(t.insert({_id: 7, x: 1, a: 2, b: 2}));
assert.throws(function() {
t.find({x: 0, $nor: "a"}).toArray();
@@ -43,34 +46,36 @@ doTest = function(index) {
t.find({x: 0, $nor: ["a"]}).toArray();
});
- an1 = t.find({$nor: [{a: 1}]}).toArray();
+ const an1 = t.find({$nor: [{a: 1}]}).toArray();
checkArrs(t.find({a: {$ne: 1}}).toArray(), an1);
- an1bn2 = t.find({x: 1, $nor: [{a: 1}, {b: 2}]}).toArray();
+ const an1bn2 = t.find({x: 1, $nor: [{a: 1}, {b: 2}]}).toArray();
checkArrs([{_id: 6, x: 1, a: 2, b: 1}], an1bn2);
checkArrs(t.find({x: 1, a: {$ne: 1}, b: {$ne: 2}}).toArray(), an1bn2);
if (index) {
- var explain = t.find({x: 1, $nor: [{a: 1}, {b: 2}]}).explain();
- assert(isIxscan(db, explain.queryPlanner.winningPlan));
+ const explain = t.find({x: 1, $nor: [{a: 1}, {b: 2}]}).explain();
+ assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
}
- an1b2 = t.find({$nor: [{a: 1}], $or: [{b: 2}]}).toArray();
+ const an1b2 = t.find({$nor: [{a: 1}], $or: [{b: 2}]}).toArray();
checkArrs(t.find({a: {$ne: 1}, b: 2}).toArray(), an1b2);
-};
+}
doTest(false);
-t.createIndex({x: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, a: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, a: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, b: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, b: 1}));
doTest();
-t.drop();
-t.createIndex({x: 1, a: 1, b: 1});
+assert(t.drop());
+assert.commandWorked(t.createIndex({x: 1, a: 1, b: 1}));
doTest();
+})();
diff --git a/jstests/core/plan_cache_sbe.js b/jstests/core/plan_cache_sbe.js
new file mode 100644
index 00000000000..89cc680a64f
--- /dev/null
+++ b/jstests/core/plan_cache_sbe.js
@@ -0,0 +1,46 @@
+/**
+ * Test that for SBE plans a plan cache entry includes a serialized SBE plan tree, and does not for
+ * classic plans.
+ *
+ * @tags: [
+ * # If all chunks are moved off of a shard, it can cause the plan cache to miss commands.
+ * assumes_balancer_off,
+ * does_not_support_stepdowns,
+ * # This test attempts to perform queries with plan cache filters set up. The former operation
+ * # may be routed to a secondary in the replica set, whereas the latter must be routed to the
+ * # primary.
+ * assumes_read_concern_unchanged,
+ * assumes_read_preference_unchanged,
+ * assumes_unsharded_collection,]
+ */
+(function() {
+"use strict";
+
+const coll = db.plan_cache_sbe;
+coll.drop();
+
+// Note that the "getParameter" command is expected to fail in versions of mongod that do not yet
+// include the slot-based execution engine. When that happens, however, 'isSBEEnabled' still
+// correctly evaluates to false.
+const isSBEEnabled = (() => {
+ const getParam = db.adminCommand({getParameter: 1, featureFlagSBE: 1});
+ return getParam.hasOwnProperty("featureFlagSBE") && getParam.featureFlagSBE.value;
+})();
+
+assert.commandWorked(coll.insert({a: 1, b: 1}));
+
+// We need two indexes so that the multi-planner is executed.
+assert.commandWorked(coll.createIndex({a: 1}));
+assert.commandWorked(coll.createIndex({a: 1, b: 1}));
+
+// Run query.
+assert.eq(1, coll.find({a: 1}).itcount());
+
+// Validate plan cache stats entry.
+const allStats = coll.aggregate([{$planCacheStats: {}}]).toArray();
+assert.eq(allStats.length, 1, allStats);
+const stats = allStats[0];
+assert(stats.hasOwnProperty("cachedPlan"), stats);
+assert.eq(stats.cachedPlan.hasOwnProperty("queryPlan"), isSBEEnabled, stats);
+assert.eq(stats.cachedPlan.hasOwnProperty("slotBasedPlan"), isSBEEnabled, stats);
+})();
diff --git a/jstests/core/wildcard_index_hint.js b/jstests/core/wildcard_index_hint.js
index ff481a62712..a5f0f9a4c92 100644
--- a/jstests/core/wildcard_index_hint.js
+++ b/jstests/core/wildcard_index_hint.js
@@ -14,7 +14,7 @@ const assertArrayEq = (l, r) => assert(arrayEq(l, r), tojson(l) + " != " + tojso
// Extracts the winning plan for the given query and hint from the explain output.
const winningPlan = (query, hint) =>
- assert.commandWorked(coll.find(query).hint(hint).explain()).queryPlanner.winningPlan;
+ getWinningPlan(assert.commandWorked(coll.find(query).hint(hint).explain()).queryPlanner);
// Runs the given query and confirms that:
// (1) the expected index was used to answer the query, and
diff --git a/jstests/libs/analyze_plan.js b/jstests/libs/analyze_plan.js
index 21f54ff1e21..0fff85a7f80 100644
--- a/jstests/libs/analyze_plan.js
+++ b/jstests/libs/analyze_plan.js
@@ -5,6 +5,17 @@
load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers.
/**
+ * Returns a sub-element of the 'queryPlanner' explain output which represents a winning plan.
+ */
+function getWinningPlan(queryPlanner) {
+ // The 'queryPlan' format is used when the SBE engine is turned on. If this field is present,
+ // it will hold a serialized winning plan, otherwise it will be stored in the 'winningPlan'
+ // field itself.
+ return queryPlanner.winningPlan.hasOwnProperty("queryPlan") ? queryPlanner.winningPlan.queryPlan
+ : queryPlanner.winningPlan;
+}
+
+/**
* Given the root stage of explain's JSON representation of a query plan ('root'), returns all
* subdocuments whose stage is 'stage'. Returns an empty array if the plan does not have the
* requested stage.
@@ -27,7 +38,7 @@ function getPlanStages(root, stage) {
}
if ("queryPlanner" in root) {
- results = results.concat(getPlanStages(root.queryPlanner.winningPlan, stage));
+ results = results.concat(getPlanStages(getWinningPlan(root.queryPlanner), stage));
}
if ("thenStage" in root) {
@@ -193,7 +204,7 @@ function getAggPlanStages(root, stage) {
getPlanStages(queryLayerOutput.executionStats.executionStages, stage));
} else {
results =
- results.concat(getPlanStages(queryLayerOutput.queryPlanner.winningPlan, stage));
+ results.concat(getPlanStages(getWinningPlan(queryLayerOutput.queryPlanner), stage));
}
return results;
@@ -407,12 +418,12 @@ function assertExplainCount({explainResults, expectedCount}) {
*/
function assertCoveredQueryAndCount({collection, query, project, count}) {
let explain = collection.find(query, project).explain();
- assert(isIndexOnly(db, explain.queryPlanner.winningPlan),
+ assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)),
"Winning plan was not covered: " + tojson(explain.queryPlanner.winningPlan));
// Same query as a count command should also be covered.
explain = collection.explain("executionStats").find(query).count();
- assert(isIndexOnly(db, explain.queryPlanner.winningPlan),
+ assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)),
"Winning plan for count was not covered: " + tojson(explain.queryPlanner.winningPlan));
assertExplainCount({explainResults: explain, expectedCount: count});
}
@@ -424,7 +435,7 @@ function assertCoveredQueryAndCount({collection, query, project, count}) {
*/
function assertStagesForExplainOfCommand({coll, cmdObj, expectedStages, stagesNotExpected}) {
const plan = assert.commandWorked(coll.runCommand({explain: cmdObj}));
- const winningPlan = plan.queryPlanner.winningPlan;
+ const winningPlan = getWinningPlan(plan.queryPlanner);
for (let expectedStage of expectedStages) {
assert(planHasStage(coll.getDB(), winningPlan, expectedStage),
"Could not find stage " + expectedStage + ". Plan: " + tojson(plan));
diff --git a/jstests/noPassthrough/and_hash.js b/jstests/noPassthrough/and_hash.js
index f3e7dd1949e..ba96d11ff16 100644
--- a/jstests/noPassthrough/and_hash.js
+++ b/jstests/noPassthrough/and_hash.js
@@ -49,7 +49,7 @@ function assertAndHashUsed({query, expectedResult, shouldUseAndHash} = {}) {
const expl = queryResult.explain();
assertArrayEq({actual: queryResult.toArray(), expected: expectedResult});
- assert.eq(shouldUseAndHash, planHasStage(db, expl.queryPlanner.winningPlan, "AND_HASH"));
+ assert.eq(shouldUseAndHash, planHasStage(db, getWinningPlan(expl.queryPlanner), "AND_HASH"));
}
// Test basic index intersection where we expect AND_HASH to be used.
diff --git a/jstests/noPassthrough/hybrid_multikey.js b/jstests/noPassthrough/hybrid_multikey.js
index 6fa1845834d..ba0154708f4 100644
--- a/jstests/noPassthrough/hybrid_multikey.js
+++ b/jstests/noPassthrough/hybrid_multikey.js
@@ -3,6 +3,7 @@
* various index types.
*/
load("jstests/noPassthrough/libs/index_build.js");
+load("jstests/libs/analyze_plan.js"); // For getWinningPlan to analyze explain() output.
(function() {
"use strict";
@@ -50,7 +51,7 @@ const runTest = (config) => {
// Ensure the index is usable and has the expected multikey state.
let explain = testColl.find({x: 1}).hint(indexName).explain();
- let plan = explain.queryPlanner.winningPlan;
+ let plan = getWinningPlan(explain.queryPlanner);
assert.eq("FETCH", plan.stage, explain);
assert.eq("IXSCAN", plan.inputStage.stage, explain);
assert.eq(
diff --git a/jstests/noPassthrough/validate_adjust_multikey.js b/jstests/noPassthrough/validate_adjust_multikey.js
index 5b5ec49267e..ad36ac48f35 100644
--- a/jstests/noPassthrough/validate_adjust_multikey.js
+++ b/jstests/noPassthrough/validate_adjust_multikey.js
@@ -2,6 +2,7 @@
* Tests foreground validation's ability to fix up allowable multikey metadata problems.
*/
(function() {
+load("jstests/libs/analyze_plan.js"); // For getWinningPlan to analyze explain() output.
const conn = MongoRunner.runMongod();
const dbName = jsTestName();
@@ -15,7 +16,7 @@ const assertValidate = (coll, assertFn) => {
const assertIndexMultikey = (coll, hint, expectMultikey) => {
const explain = coll.find().hint(hint).explain();
- const plan = explain.queryPlanner.winningPlan;
+ const plan = getWinningPlan(explain.queryPlanner);
assert.eq("FETCH", plan.stage, explain);
assert.eq("IXSCAN", plan.inputStage.stage, explain);
assert.eq(expectMultikey,
@@ -109,4 +110,4 @@ runTest((coll) => {
});
MongoRunner.stopMongod(conn);
-})(); \ No newline at end of file
+})();
diff --git a/src/mongo/db/exec/plan_cache_util.h b/src/mongo/db/exec/plan_cache_util.h
index b2a18634016..717f3c879ef 100644
--- a/src/mongo/db/exec/plan_cache_util.h
+++ b/src/mongo/db/exec/plan_cache_util.h
@@ -114,8 +114,10 @@ void updatePlanCache(
if constexpr (std::is_same_v<PlanStageType, std::unique_ptr<sbe::PlanStage>>) {
return std::make_pair(
plan_explainer_factory::make(candidates[winnerIdx].root.get(),
+ &candidates[winnerIdx].data,
candidates[winnerIdx].solution.get()),
plan_explainer_factory::make(candidates[runnerUpIdx].root.get(),
+ &candidates[runnerUpIdx].data,
candidates[runnerUpIdx].solution.get()));
} else {
static_assert(std::is_same_v<PlanStageType, PlanStage*>);
@@ -139,6 +141,7 @@ void updatePlanCache(
auto winnerExplainer = [&]() {
if constexpr (std::is_same_v<PlanStageType, std::unique_ptr<sbe::PlanStage>>) {
return plan_explainer_factory::make(candidates[winnerIdx].root.get(),
+ &candidates[winnerIdx].data,
candidates[winnerIdx].solution.get());
} else {
static_assert(std::is_same_v<PlanStageType, PlanStage*>);
diff --git a/src/mongo/db/exec/sbe/parser/sbe_parser_test.cpp b/src/mongo/db/exec/sbe/parser/sbe_parser_test.cpp
index 914352adce0..8fcf32cb165 100644
--- a/src/mongo/db/exec/sbe/parser/sbe_parser_test.cpp
+++ b/src/mongo/db/exec/sbe/parser/sbe_parser_test.cpp
@@ -231,9 +231,9 @@ TEST_F(SBEParserTest, TestIdenticalDebugOutputAfterParse) {
sbe::Parser parser;
for (const auto& stage : stages) {
- const auto stageText = printer.print(stage.get());
+ const auto stageText = printer.print(*stage);
const auto parsedStage = parser.parse(nullptr, "testDb", stageText);
- const auto stageTextAfterParse = printer.print(parsedStage.get());
+ const auto stageTextAfterParse = printer.print(*parsedStage);
ASSERT_EQ(stageText, stageTextAfterParse);
}
}
@@ -243,7 +243,7 @@ TEST_F(SBEParserTest, TestPlanNodeIdIsParsed) {
sbe::Parser parser;
for (const auto& stage : stages) {
- const auto stageText = printer.print(stage.get());
+ const auto stageText = printer.print(*stage);
const auto parsedStage = parser.parse(nullptr, "testDb", stageText);
ASSERT_EQ(parsedStage->getCommonStats()->nodeId, planNodeId);
}
diff --git a/src/mongo/db/exec/sbe/stages/hash_agg.cpp b/src/mongo/db/exec/sbe/stages/hash_agg.cpp
index 18e7ee6559a..4c4a5b4f132 100644
--- a/src/mongo/db/exec/sbe/stages/hash_agg.cpp
+++ b/src/mongo/db/exec/sbe/stages/hash_agg.cpp
@@ -197,16 +197,16 @@ std::vector<DebugPrinter::Block> HashAggStage::debugPrint() const {
ret.emplace_back(DebugPrinter::Block("[`"));
bool first = true;
- for (auto& p : _aggs) {
+ value::orderedSlotMapTraverse(_aggs, [&](auto slot, auto&& expr) {
if (!first) {
ret.emplace_back(DebugPrinter::Block("`,"));
}
- DebugPrinter::addIdentifier(ret, p.first);
+ DebugPrinter::addIdentifier(ret, slot);
ret.emplace_back("=");
- DebugPrinter::addBlocks(ret, p.second->debugPrint());
+ DebugPrinter::addBlocks(ret, expr->debugPrint());
first = false;
- }
+ });
ret.emplace_back("`]");
DebugPrinter::addNewLine(ret);
diff --git a/src/mongo/db/exec/sbe/stages/project.cpp b/src/mongo/db/exec/sbe/stages/project.cpp
index a5ab48b1369..10cf54e2b2e 100644
--- a/src/mongo/db/exec/sbe/stages/project.cpp
+++ b/src/mongo/db/exec/sbe/stages/project.cpp
@@ -100,9 +100,9 @@ std::unique_ptr<PlanStageStats> ProjectStage::getStats(bool includeDebugInfo) co
if (includeDebugInfo) {
DebugPrinter printer;
BSONObjBuilder bob;
- for (auto&& [slot, expr] : _projects) {
+ value::orderedSlotMapTraverse(_projects, [&](auto slot, auto&& expr) {
bob.append(str::stream() << slot, printer.print(expr->debugPrint()));
- }
+ });
ret->debugInfo = BSON("projections" << bob.obj());
}
@@ -116,19 +116,21 @@ const SpecificStats* ProjectStage::getSpecificStats() const {
std::vector<DebugPrinter::Block> ProjectStage::debugPrint() const {
auto ret = PlanStage::debugPrint();
+
ret.emplace_back("[`");
bool first = true;
- for (auto& p : _projects) {
+ value::orderedSlotMapTraverse(_projects, [&](auto slot, auto&& expr) {
if (!first) {
ret.emplace_back(DebugPrinter::Block("`,"));
}
- DebugPrinter::addIdentifier(ret, p.first);
+ DebugPrinter::addIdentifier(ret, slot);
ret.emplace_back("=");
- DebugPrinter::addBlocks(ret, p.second->debugPrint());
+ DebugPrinter::addBlocks(ret, expr->debugPrint());
first = false;
- }
+ });
ret.emplace_back("`]");
+
DebugPrinter::addNewLine(ret);
DebugPrinter::addBlocks(ret, _children[0]->debugPrint());
return ret;
diff --git a/src/mongo/db/exec/sbe/util/debug_print.cpp b/src/mongo/db/exec/sbe/util/debug_print.cpp
index b659f9a7ba6..939779be8de 100644
--- a/src/mongo/db/exec/sbe/util/debug_print.cpp
+++ b/src/mongo/db/exec/sbe/util/debug_print.cpp
@@ -116,8 +116,8 @@ std::string DebugPrinter::print(const std::vector<Block>& blocks) {
return ret;
}
-std::string DebugPrinter::print(PlanStage* s) {
- return print(s->debugPrint());
+std::string DebugPrinter::print(const PlanStage& s) {
+ return print(s.debugPrint());
}
} // namespace sbe
} // namespace mongo
diff --git a/src/mongo/db/exec/sbe/util/debug_print.h b/src/mongo/db/exec/sbe/util/debug_print.h
index 7b58391e207..19e29d7bdf4 100644
--- a/src/mongo/db/exec/sbe/util/debug_print.h
+++ b/src/mongo/db/exec/sbe/util/debug_print.h
@@ -116,7 +116,7 @@ public:
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
}
- std::string print(PlanStage* s);
+ std::string print(const PlanStage& s);
std::string print(const std::vector<Block>& blocks);
private:
diff --git a/src/mongo/db/exec/sbe/values/slot.h b/src/mongo/db/exec/sbe/values/slot.h
index 95131fb7fdc..04449a49305 100644
--- a/src/mongo/db/exec/sbe/values/slot.h
+++ b/src/mongo/db/exec/sbe/values/slot.h
@@ -591,4 +591,20 @@ using SlotVector = std::vector<SlotId>;
using SlotIdGenerator = IdGenerator<value::SlotId>;
using FrameIdGenerator = IdGenerator<FrameId>;
using SpoolIdGenerator = IdGenerator<SpoolId>;
+
+/**
+ * Given an unordered slot 'map', calls 'callback' for each slot/value pair in order of ascending
+ * slot id.
+ */
+template <typename T, typename C>
+void orderedSlotMapTraverse(const SlotMap<T>& map, C callback) {
+ std::set<SlotId> slots;
+ for (auto&& elem : map) {
+ slots.insert(elem.first);
+ }
+
+ for (auto slot : slots) {
+ callback(slot, map.at(slot));
+ }
+}
} // namespace mongo::sbe::value
diff --git a/src/mongo/db/pipeline/document_source_cursor.h b/src/mongo/db/pipeline/document_source_cursor.h
index 6df87f7c706..2d3a5332b10 100644
--- a/src/mongo/db/pipeline/document_source_cursor.h
+++ b/src/mongo/db/pipeline/document_source_cursor.h
@@ -115,6 +115,10 @@ public:
return &_stats;
}
+ const PlanExplainer::ExplainVersion& getExplainVersion() const {
+ return _exec->getPlanExplainer().getVersion();
+ }
+
protected:
DocumentSourceCursor(const CollectionPtr& collection,
std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec,
diff --git a/src/mongo/db/pipeline/plan_explainer_pipeline.cpp b/src/mongo/db/pipeline/plan_explainer_pipeline.cpp
index 49e05ead917..4b3bcfac0a6 100644
--- a/src/mongo/db/pipeline/plan_explainer_pipeline.cpp
+++ b/src/mongo/db/pipeline/plan_explainer_pipeline.cpp
@@ -50,6 +50,16 @@ void collectPlanSummaryStats(const DocSourceType& source, PlanSummaryStats* stat
statsOut->accumulate(docSpecificStats.planSummaryStats);
}
+const PlanExplainer::ExplainVersion& PlanExplainerPipeline::getVersion() const {
+ static const ExplainVersion kExplainVersion = "1";
+
+ if (auto docSourceCursor =
+ dynamic_cast<DocumentSourceCursor*>(_pipeline->getSources().front().get())) {
+ return docSourceCursor->getExplainVersion();
+ }
+ return kExplainVersion;
+}
+
std::string PlanExplainerPipeline::getPlanSummary() const {
if (auto docSourceCursor =
dynamic_cast<DocumentSourceCursor*>(_pipeline->getSources().front().get())) {
diff --git a/src/mongo/db/pipeline/plan_explainer_pipeline.h b/src/mongo/db/pipeline/plan_explainer_pipeline.h
index e2021b1c20e..f28adc3c7ea 100644
--- a/src/mongo/db/pipeline/plan_explainer_pipeline.h
+++ b/src/mongo/db/pipeline/plan_explainer_pipeline.h
@@ -44,6 +44,7 @@ public:
return false;
}
+ const ExplainVersion& getVersion() const final;
std::string getPlanSummary() const final;
void getSummaryStats(PlanSummaryStats* statsOut) const final;
PlanStatsDetails getWinningPlanStats(ExplainOptions::Verbosity verbosity) const final;
diff --git a/src/mongo/db/query/explain.cpp b/src/mongo/db/query/explain.cpp
index c271cfe1c81..330e2709a6d 100644
--- a/src/mongo/db/query/explain.cpp
+++ b/src/mongo/db/query/explain.cpp
@@ -84,7 +84,6 @@ void generatePlannerInfo(PlanExecutor* exec,
BSONObjBuilder* out) {
BSONObjBuilder plannerBob(out->subobjStart("queryPlanner"));
- plannerBob.append("plannerVersion", QueryPlanner::kPlannerVersion);
plannerBob.append("namespace", exec->nss().ns());
// Find whether there is an index filter set for the query shape. The 'indexFilterSet' field
@@ -266,6 +265,14 @@ void executePlan(PlanExecutor* exec) {
// Discard the resulting documents.
}
}
+
+/**
+ * Returns a BSON document in the form of {explainVersion: <version>} with the 'version' parameter
+ * serialized into the <version> element.
+ */
+BSONObj explainVersionToBson(const PlanExplainer::ExplainVersion& version) {
+ return BSON("explainVersion" << version);
+}
} // namespace
void Explain::explainStages(PlanExecutor* exec,
@@ -280,6 +287,9 @@ void Explain::explainStages(PlanExecutor* exec,
// Use the stats trees to produce explain BSON.
//
+ auto&& explainer = exec->getPlanExplainer();
+ out->appendElements(explainVersionToBson(explainer.getVersion()));
+
if (verbosity >= ExplainOptions::Verbosity::kQueryPlanner) {
generatePlannerInfo(exec, collection, extraInfo, out);
}
@@ -309,6 +319,8 @@ void Explain::explainPipeline(PlanExecutor* exec,
executePlan(pipelineExec);
}
+ auto&& explainer = pipelineExec->getPlanExplainer();
+ out->appendElements(explainVersionToBson(explainer.getVersion()));
*out << "stages" << Value(pipelineExec->writeExplainOps(verbosity));
explain_common::generateServerInfo(out);
@@ -381,14 +393,15 @@ void Explain::planCacheEntryToBSON(const PlanCacheEntry& entry, BSONObjBuilder*
}
}
- auto explainer = stdx::visit(
- visit_helper::Overloaded{[](const plan_ranker::StatsDetails&) {
- return plan_explainer_factory::make(nullptr);
- },
- [](const plan_ranker::SBEStatsDetails&) {
- return plan_explainer_factory::make(nullptr, nullptr);
- }},
- debugInfo.decision->stats);
+ auto explainer =
+ stdx::visit(visit_helper::Overloaded{[](const plan_ranker::StatsDetails&) {
+ return plan_explainer_factory::make(nullptr);
+ },
+ [](const plan_ranker::SBEStatsDetails&) {
+ return plan_explainer_factory::make(
+ nullptr, nullptr, nullptr);
+ }},
+ debugInfo.decision->stats);
auto plannerStats =
explainer->getCachedPlanStats(debugInfo, ExplainOptions::Verbosity::kQueryPlanner);
auto execStats =
diff --git a/src/mongo/db/query/get_executor.cpp b/src/mongo/db/query/get_executor.cpp
index 8089c0b5b8d..e8e6abfb9d5 100644
--- a/src/mongo/db/query/get_executor.cpp
+++ b/src/mongo/db/query/get_executor.cpp
@@ -483,7 +483,8 @@ public:
invariant(_roots.size() == 1);
invariant(_solutions.size() == 1);
invariant(_roots[0].first);
- auto explainer = plan_explainer_factory::make(_roots[0].first.get(), _solutions[0].get());
+ auto explainer = plan_explainer_factory::make(
+ _roots[0].first.get(), &_roots[0].second, _solutions[0].get());
return explainer->getPlanSummary();
}
diff --git a/src/mongo/db/query/plan_executor_factory.cpp b/src/mongo/db/query/plan_executor_factory.cpp
index f7e02c57529..d43cdfd9e32 100644
--- a/src/mongo/db/query/plan_executor_factory.cpp
+++ b/src/mongo/db/query/plan_executor_factory.cpp
@@ -132,7 +132,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> make(
5,
"SBE plan",
"slots"_attr = data.debugString(),
- "stages"_attr = sbe::DebugPrinter{}.print(rootStage.get()));
+ "stages"_attr = sbe::DebugPrinter{}.print(*rootStage));
rootStage->prepare(data.ctx);
@@ -164,7 +164,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> make(
5,
"SBE plan",
"slots"_attr = candidates.winner().data.debugString(),
- "stages"_attr = sbe::DebugPrinter{}.print(candidates.winner().root.get()));
+ "stages"_attr = sbe::DebugPrinter{}.print(*candidates.winner().root));
return {{new PlanExecutorSBE(opCtx,
std::move(cq),
diff --git a/src/mongo/db/query/plan_executor_sbe.cpp b/src/mongo/db/query/plan_executor_sbe.cpp
index 11b856b10b2..7c32db8ad00 100644
--- a/src/mongo/db/query/plan_executor_sbe.cpp
+++ b/src/mongo/db/query/plan_executor_sbe.cpp
@@ -108,7 +108,7 @@ PlanExecutorSBE::PlanExecutorSBE(OperationContext* opCtx,
candidates.plans.erase(candidates.plans.begin() + candidates.winnerIdx);
}
_planExplainer = plan_explainer_factory::make(
- _root.get(), _solution.get(), std::move(candidates.plans), isMultiPlan);
+ _root.get(), &winner.data, _solution.get(), std::move(candidates.plans), isMultiPlan);
}
void PlanExecutorSBE::saveState() {
diff --git a/src/mongo/db/query/plan_explainer.h b/src/mongo/db/query/plan_explainer.h
index 96f18f8d9ed..e6d19e32470 100644
--- a/src/mongo/db/query/plan_explainer.h
+++ b/src/mongo/db/query/plan_explainer.h
@@ -50,6 +50,11 @@ static constexpr int kMaxExplainStatsBSONSizeMB = 10 * 1024 * 1024;
class PlanExplainer {
public:
/**
+ * A version of the explain format. "1" is used for the classic engine and "2" for SBE.
+ */
+ using ExplainVersion = std::string;
+
+ /**
* This pair holds a serialized BSON document that details the plan selected by the query
* planner, and optional summary stats for an execution tree if the verbosity level for the
* generated stats is 'executionStats' or higher. The format of these stats are opaque to the
@@ -66,6 +71,11 @@ public:
virtual ~PlanExplainer() = default;
/**
+ * Returns a version of the explain format supported by this explainer.
+ */
+ virtual const ExplainVersion& getVersion() const = 0;
+
+ /**
* Returns 'true' if this PlanExplainer can provide information on the winning plan and rejected
* candidate plans, meaning that the QueryPlanner generated multiple candidate plans and the
* winning plan was chosen by the multi-planner.
diff --git a/src/mongo/db/query/plan_explainer_factory.cpp b/src/mongo/db/query/plan_explainer_factory.cpp
index da64fec81f5..74d0c1dfd3d 100644
--- a/src/mongo/db/query/plan_explainer_factory.cpp
+++ b/src/mongo/db/query/plan_explainer_factory.cpp
@@ -43,15 +43,18 @@ std::unique_ptr<PlanExplainer> make(PlanStage* root, const PlanEnumeratorExplain
return std::make_unique<PlanExplainerImpl>(root, explainInfo);
}
-std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root, const QuerySolution* solution) {
- return make(root, solution, {}, false);
+std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root,
+ const stage_builder::PlanStageData* data,
+ const QuerySolution* solution) {
+ return make(root, data, solution, {}, false);
}
std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root,
+ const stage_builder::PlanStageData* data,
const QuerySolution* solution,
std::vector<sbe::plan_ranker::CandidatePlan> rejectedCandidates,
bool isMultiPlan) {
return std::make_unique<PlanExplainerSBE>(
- root, solution, std::move(rejectedCandidates), isMultiPlan);
+ root, data, solution, std::move(rejectedCandidates), isMultiPlan);
}
} // namespace mongo::plan_explainer_factory
diff --git a/src/mongo/db/query/plan_explainer_factory.h b/src/mongo/db/query/plan_explainer_factory.h
index 55e42d90791..f6736067bda 100644
--- a/src/mongo/db/query/plan_explainer_factory.h
+++ b/src/mongo/db/query/plan_explainer_factory.h
@@ -40,8 +40,11 @@ namespace mongo::plan_explainer_factory {
std::unique_ptr<PlanExplainer> make(PlanStage* root);
std::unique_ptr<PlanExplainer> make(PlanStage* root,
const PlanEnumeratorExplainInfo& enumeratorInfo);
-std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root, const QuerySolution* solution);
std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root,
+ const stage_builder::PlanStageData* data,
+ const QuerySolution* solution);
+std::unique_ptr<PlanExplainer> make(sbe::PlanStage* root,
+ const stage_builder::PlanStageData* data,
const QuerySolution* solution,
std::vector<sbe::plan_ranker::CandidatePlan> rejectedCandidates,
bool isMultiPlan);
diff --git a/src/mongo/db/query/plan_explainer_impl.cpp b/src/mongo/db/query/plan_explainer_impl.cpp
index 78614450060..a04882ce57c 100644
--- a/src/mongo/db/query/plan_explainer_impl.cpp
+++ b/src/mongo/db/query/plan_explainer_impl.cpp
@@ -536,6 +536,11 @@ void appendMultikeyPaths(const BSONObj& keyPattern,
subMultikeyPaths.doneFast();
}
+const PlanExplainer::ExplainVersion& PlanExplainerImpl::getVersion() const {
+ static const ExplainVersion kExplainVersion = "1";
+ return kExplainVersion;
+}
+
bool PlanExplainerImpl::isMultiPlan() const {
return getStageByType(_root, StageType::STAGE_MULTI_PLAN) != nullptr;
}
diff --git a/src/mongo/db/query/plan_explainer_impl.h b/src/mongo/db/query/plan_explainer_impl.h
index 82d97e5a83b..859b633e0e1 100644
--- a/src/mongo/db/query/plan_explainer_impl.h
+++ b/src/mongo/db/query/plan_explainer_impl.h
@@ -48,6 +48,7 @@ public:
: PlanExplainer{explainInfo}, _root{root} {}
PlanExplainerImpl(PlanStage* root) : _root{root} {}
+ const ExplainVersion& getVersion() const final;
bool isMultiPlan() const final;
std::string getPlanSummary() const final;
void getSummaryStats(PlanSummaryStats* statsOut) const final;
diff --git a/src/mongo/db/query/plan_explainer_sbe.cpp b/src/mongo/db/query/plan_explainer_sbe.cpp
index 677e0ed393a..e993b2cf6c0 100644
--- a/src/mongo/db/query/plan_explainer_sbe.cpp
+++ b/src/mongo/db/query/plan_explainer_sbe.cpp
@@ -298,22 +298,36 @@ PlanSummaryStats collectExecutionStatsSummary(const sbe::PlanStageStats* stats)
return summary;
}
-PlanExplainer::PlanStatsDetails buildPlanStatsDetails(const QuerySolutionNode* node,
- const sbe::PlanStageStats* stats,
- ExplainOptions::Verbosity verbosity) {
+PlanExplainer::PlanStatsDetails buildPlanStatsDetails(
+ const QuerySolutionNode* node,
+ const sbe::PlanStageStats* stats,
+ const boost::optional<BSONObj>& execPlanDebugInfo,
+ ExplainOptions::Verbosity verbosity) {
BSONObjBuilder bob;
if (verbosity >= ExplainOptions::Verbosity::kExecStats) {
auto summary = collectExecutionStatsSummary(stats);
statsToBSON(stats, &bob, &bob);
+ // At the 'kQueryPlanner' verbosity level we use the QSN-derived format for the given plan,
+ // and thus the winning plan and rejected plans at this verbosity should display the
+ // stringified SBE plan, which is added below. However, at the 'kExecStats' the execution
+ // stats use the PlanStage-derived format for the SBE tree, so there is no need to repeat
+ // the stringified SBE plan and we only included what's been generated from the
+ // PlanStageStats.
return {bob.obj(), std::move(summary)};
}
statsToBSON(node, &bob, &bob);
- return {bob.obj(), boost::none};
+ invariant(execPlanDebugInfo);
+ return {BSON("queryPlan" << bob.obj() << "slotBasedPlan" << *execPlanDebugInfo), boost::none};
}
} // namespace
+const PlanExplainer::ExplainVersion& PlanExplainerSBE::getVersion() const {
+ static const ExplainVersion kExplainVersion = "2";
+ return kExplainVersion;
+}
+
std::string PlanExplainerSBE::getPlanSummary() const {
if (!_solution) {
return {};
@@ -462,7 +476,7 @@ PlanExplainer::PlanStatsDetails PlanExplainerSBE::getWinningPlanStats(
invariant(_root);
invariant(_solution);
auto stats = _root->getStats(true /* includeDebugInfo */);
- return buildPlanStatsDetails(_solution->root(), stats.get(), verbosity);
+ return buildPlanStatsDetails(_solution->root(), stats.get(), _execPlanDebugInfo, verbosity);
}
std::vector<PlanExplainer::PlanStatsDetails> PlanExplainerSBE::getRejectedPlansStats(
@@ -478,7 +492,9 @@ std::vector<PlanExplainer::PlanStatsDetails> PlanExplainerSBE::getRejectedPlansS
invariant(candidate.solution);
auto stats = candidate.root->getStats(true /* includeDebugInfo */);
- res.push_back(buildPlanStatsDetails(candidate.solution->root(), stats.get(), verbosity));
+ auto execPlanDebugInfo = buildExecPlanDebugInfo(candidate.root.get(), &candidate.data);
+ res.push_back(buildPlanStatsDetails(
+ candidate.solution->root(), stats.get(), execPlanDebugInfo, verbosity));
}
return res;
}
@@ -491,7 +507,7 @@ std::vector<PlanExplainer::PlanStatsDetails> PlanExplainerSBE::getCachedPlanStat
auto&& stats = decision.getStats<mongo::sbe::PlanStageStats>();
if (verbosity >= ExplainOptions::Verbosity::kExecStats) {
for (auto&& planStats : stats.candidatePlanStats) {
- res.push_back(buildPlanStatsDetails(nullptr, planStats.get(), verbosity));
+ res.push_back(buildPlanStatsDetails(nullptr, planStats.get(), boost::none, verbosity));
}
} else {
// At the "queryPlanner" verbosity we only need to provide details about the winning plan
diff --git a/src/mongo/db/query/plan_explainer_sbe.h b/src/mongo/db/query/plan_explainer_sbe.h
index d8056ade4ed..dba5316e82b 100644
--- a/src/mongo/db/query/plan_explainer_sbe.h
+++ b/src/mongo/db/query/plan_explainer_sbe.h
@@ -41,6 +41,7 @@ namespace mongo {
class PlanExplainerSBE final : public PlanExplainer {
public:
PlanExplainerSBE(const sbe::PlanStage* root,
+ const stage_builder::PlanStageData* data,
const QuerySolution* solution,
std::vector<sbe::plan_ranker::CandidatePlan> rejectedCandidates,
bool isMultiPlan)
@@ -48,12 +49,14 @@ public:
_root{root},
_solution{solution},
_rejectedCandidates{std::move(rejectedCandidates)},
- _isMultiPlan{isMultiPlan} {}
+ _isMultiPlan{isMultiPlan},
+ _execPlanDebugInfo{buildExecPlanDebugInfo(_root, data)} {}
bool isMultiPlan() const final {
return _isMultiPlan;
}
+ const ExplainVersion& getVersion() const final;
std::string getPlanSummary() const final;
void getSummaryStats(PlanSummaryStats* statsOut) const final;
PlanStatsDetails getWinningPlanStats(ExplainOptions::Verbosity verbosity) const final;
@@ -63,9 +66,25 @@ public:
ExplainOptions::Verbosity) const final;
private:
+ boost::optional<BSONObj> buildExecPlanDebugInfo(
+ const sbe::PlanStage* root, const stage_builder::PlanStageData* data) const {
+ if (root && data) {
+ return BSON("slots" << data->debugString() << "stages"
+ << sbe::DebugPrinter().print(*_root));
+ }
+ return boost::none;
+ }
+
const sbe::PlanStage* _root{nullptr};
const QuerySolution* _solution{nullptr};
const std::vector<sbe::plan_ranker::CandidatePlan> _rejectedCandidates;
const bool _isMultiPlan{false};
+ // Contains information about the slots returned by the PlanStage tree, along with the tree
+ // itself, serialized into a string. This optional object is then included into explain output
+ // and is only initialized when the _root of the PlanStage tree is available. The only case when
+ // it's not available is when PlanStatsDetails are generated from the plan cache (by
+ // calling getCachedPlanStats()), in which case this debug info is already included into the
+ // plan cache entry as part of a serialized winning plan.
+ boost::optional<BSONObj> _execPlanDebugInfo;
};
} // namespace mongo
diff --git a/src/mongo/db/query/plan_ranker_util.h b/src/mongo/db/query/plan_ranker_util.h
index 06557f2b574..58bcc16d3df 100644
--- a/src/mongo/db/query/plan_ranker_util.h
+++ b/src/mongo/db/query/plan_ranker_util.h
@@ -84,8 +84,8 @@ StatusWith<std::unique_ptr<PlanRankingDecision>> pickBestPlan(
candidates[i].solution->_enumeratorExplainInfo);
} else {
static_assert(std::is_same_v<PlanStageStatsType, mongo::sbe::PlanStageStats>);
- return plan_explainer_factory::make(candidates[i].root.get(),
- candidates[i].solution.get());
+ return plan_explainer_factory::make(
+ candidates[i].root.get(), &candidates[i].data, candidates[i].solution.get());
}
}();
@@ -143,8 +143,8 @@ StatusWith<std::unique_ptr<PlanRankingDecision>> pickBestPlan(
// For SBE, we need to store a serialized winning plan within the ranking decision to be
// able to included it into the explain output for a cached plan stats, since we cannot
// reconstruct it from a PlanStageStats tree.
- auto explainer =
- plan_explainer_factory::make(candidates[0].root.get(), candidates[0].solution.get());
+ auto explainer = plan_explainer_factory::make(
+ candidates[0].root.get(), &candidates[0].data, candidates[0].solution.get());
auto&& [stats, _] =
explainer->getWinningPlanStats(ExplainOptions::Verbosity::kQueryPlanner);
SBEStatsDetails details;
diff --git a/src/mongo/db/query/query_planner.cpp b/src/mongo/db/query/query_planner.cpp
index a910d7504d3..70f62ecc418 100644
--- a/src/mongo/db/query/query_planner.cpp
+++ b/src/mongo/db/query/query_planner.cpp
@@ -344,9 +344,6 @@ bool providesSort(const CanonicalQuery& query, const BSONObj& kp) {
return query.getQueryRequest().getSort().isPrefixOf(kp, SimpleBSONElementComparator::kInstance);
}
-// static
-const int QueryPlanner::kPlannerVersion = 1;
-
StatusWith<std::unique_ptr<PlanCacheIndexTree>> QueryPlanner::cacheDataFromTaggedTree(
const MatchExpression* const taggedTree, const vector<IndexEntry>& relevantIndices) {
if (!taggedTree) {
diff --git a/src/mongo/db/query/query_planner.h b/src/mongo/db/query/query_planner.h
index 236ec4f0269..dcd6ca6e542 100644
--- a/src/mongo/db/query/query_planner.h
+++ b/src/mongo/db/query/query_planner.h
@@ -80,9 +80,6 @@ public:
std::map<IndexEntry::Identifier, size_t> indexMap;
};
- // Identifies the version of the query planner module. Reported in explain.
- static const int kPlannerVersion;
-
/**
* Returns the list of possible query solutions for the provided 'query'. Uses the indices and
* other data in 'params' to determine the set of available plans.
diff --git a/src/mongo/db/query/sbe_cached_solution_planner.cpp b/src/mongo/db/query/sbe_cached_solution_planner.cpp
index aece175a58f..321410b9a1a 100644
--- a/src/mongo/db/query/sbe_cached_solution_planner.cpp
+++ b/src/mongo/db/query/sbe_cached_solution_planner.cpp
@@ -52,7 +52,8 @@ CandidatePlans CachedSolutionPlanner::plan(
invariant(candidates.size() == 1);
return std::move(candidates[0]);
}();
- auto explainer = plan_explainer_factory::make(candidate.root.get(), candidate.solution.get());
+ auto explainer = plan_explainer_factory::make(
+ candidate.root.get(), &candidate.data, candidate.solution.get());
if (candidate.failed) {
// On failure, fall back to replanning the whole query. We neither evict the existing cache
@@ -123,7 +124,7 @@ CandidatePlans CachedSolutionPlanner::replan(bool shouldCache) const {
_opCtx, _collection, _cq, *solutions[0], _yieldPolicy);
prepareExecutionPlan(root.get(), &data);
- auto explainer = plan_explainer_factory::make(root.get(), solutions[0].get());
+ auto explainer = plan_explainer_factory::make(root.get(), &data, solutions[0].get());
LOGV2_DEBUG(
2058101,
1,
@@ -153,6 +154,7 @@ CandidatePlans CachedSolutionPlanner::replan(bool shouldCache) const {
MultiPlanner multiPlanner{_opCtx, _collection, _cq, cachingMode, _yieldPolicy};
auto&& [candidates, winnerIdx] = multiPlanner.plan(std::move(solutions), std::move(roots));
auto explainer = plan_explainer_factory::make(candidates[winnerIdx].root.get(),
+ &candidates[winnerIdx].data,
candidates[winnerIdx].solution.get());
LOGV2_DEBUG(2058201,
1,
diff --git a/src/mongo/db/query/sbe_multi_planner.cpp b/src/mongo/db/query/sbe_multi_planner.cpp
index b44d786ed0d..b51c642a447 100644
--- a/src/mongo/db/query/sbe_multi_planner.cpp
+++ b/src/mongo/db/query/sbe_multi_planner.cpp
@@ -65,7 +65,8 @@ CandidatePlans MultiPlanner::finalizeExecutionPlans(
LOGV2_DEBUG(
4822875, 5, "Winning solution", "bestSolution"_attr = redact(winner.solution->toString()));
- auto explainer = plan_explainer_factory::make(winner.root.get(), winner.solution.get());
+ auto explainer =
+ plan_explainer_factory::make(winner.root.get(), &winner.data, winner.solution.get());
LOGV2_DEBUG(4822876, 2, "Winning plan", "planSummary"_attr = explainer->getPlanSummary());
// Close all candidate plans but the winner.