-
Notifications
You must be signed in to change notification settings - Fork 5.7k
SERVER-32721 Explain output should indicate when a backup plan is used #1231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
ed35209
a7c1914
b9c7aed
6316125
8731c80
3ec2451
9c66dab
fa97a62
d0f1174
e2f2483
22982fd
ca5e444
ffd904b
d86caa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// Test that the explain will use backup plan if the original winning plan ran out of memory in the | ||
// "executionStats" mode | ||
// This test was designed to reproduce SERVER-32721" | ||
(function() { | ||
"use strict"; | ||
|
||
db.foo.drop() let bulk = db.foo.initializeUnorderedBulkOp(); | ||
for (let i = 0; i < 100000; ++i) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is great, but we tend to favor tests that don't need as much data to be inserted, since they will execute faster. In this case you configure the maxBlockingSortBytes, so do you need this many documents? |
||
bulk.insert({_id: i, x: i, y: i}); | ||
} | ||
|
||
bulk.execute(); | ||
db.foo | ||
.ensureIndex({x: 1}) | ||
|
||
|
||
// Configure log level and lower the sort bytes limit. | ||
db.setLogLevel(5, "query"); | ||
db.adminCommand({setParameter: 1, internalQueryExecMaxBlockingSortBytes: 100}); | ||
|
||
// This query will not use the backup plan, hence it generates only two stages: winningPlan and | ||
// rejectedPlans | ||
assert | ||
.commandWorked(db.foo.find({x: {$gte: 90}}).sort({_id: 1}).explain(true)) | ||
|
||
|
||
// This query will use backup plan, the exaplin output for this query will generate three | ||
// stages: winningPlan, rejectedPlans and originalWinningPlan | ||
assert.commandWorked(db.foo.find({x: {$gte: 90000}}).sort({_id: 1}).explain(true)) | ||
}()); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,6 +71,7 @@ MultiPlanStage::MultiPlanStage(OperationContext* opCtx, | |
_query(cq), | ||
_bestPlanIdx(kNoSuchPlan), | ||
_backupPlanIdx(kNoSuchPlan), | ||
_originalWinningPlanIdx(kNoSuchPlan), | ||
_failure(false), | ||
_failureCount(0), | ||
_statusMemberId(WorkingSet::INVALID_ID) { | ||
|
@@ -128,6 +129,7 @@ PlanStage::StageState MultiPlanStage::doWork(WorkingSetID* out) { | |
// cached plan runner to fall back on a different solution | ||
// if the best solution fails. Alternatively we could try to | ||
// defer cache insertion to be after the first produced result. | ||
_originalWinningPlanIdx = _bestPlanIdx; | ||
|
||
_collection->infoCache()->getPlanCache()->remove(*_query).transitional_ignore(); | ||
|
||
|
@@ -245,6 +247,7 @@ Status MultiPlanStage::pickBestPlan(PlanYieldPolicy* yieldPolicy) { | |
_backupPlanIdx = kNoSuchPlan; | ||
if (bestSolution->hasBlockingStage && (0 == alreadyProduced.size())) { | ||
LOG(5) << "Winner has blocking stage, looking for backup plan..."; | ||
|
||
|
||
for (size_t ix = 0; ix < _candidates.size(); ++ix) { | ||
if (!_candidates[ix].solution->hasBlockingStage) { | ||
LOG(5) << "Candidate " << ix << " is backup child"; | ||
|
@@ -452,6 +455,12 @@ void MultiPlanStage::doInvalidate(OperationContext* opCtx, | |
bool MultiPlanStage::hasBackupPlan() const { | ||
return kNoSuchPlan != _backupPlanIdx; | ||
} | ||
int MultiPlanStage::backupPlanIdx() const { | ||
return _backupPlanIdx; | ||
} | ||
int MultiPlanStage::originalWinningPlanIdx() const { | ||
return _originalWinningPlanIdx; | ||
} | ||
|
||
bool MultiPlanStage::bestPlanChosen() const { | ||
return kNoSuchPlan != _bestPlanIdx; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,6 +132,11 @@ class MultiPlanStage final : public PlanStage { | |
/** Return the index of the best plan chosen, for testing */ | ||
int bestPlanIdx() const; | ||
|
||
/** Return the index of the backup plan chosen, for testing */ | ||
|
||
int backupPlanIdx() const; | ||
|
||
/** Return the index of the backup plan chosen, for testing */ | ||
int originalWinningPlanIdx() const; | ||
/** | ||
* Returns the QuerySolution for the best plan, or NULL if no best plan | ||
* | ||
|
@@ -198,10 +203,14 @@ class MultiPlanStage final : public PlanStage { | |
// uses -1 / kNoSuchPlan when best plan is not (yet) known | ||
int _bestPlanIdx; | ||
|
||
// index into _candidates, of the backup plan for sort | ||
// index into _candidates, of the backup of the plan competition | ||
|
||
// uses -1 / kNoSuchPlan when best plan is not (yet) known | ||
int _backupPlanIdx; | ||
|
||
// index into _candidates, of the original winner of the plan competition | ||
// uses -1 / kNoSuchPlan when best plan is not (yet) known | ||
int _originalWinningPlanIdx; | ||
|
||
// Set if this MultiPlanStage cannot continue, and the query must fail. This can happen in | ||
// two ways. The first is that all candidate plans fail. Note that one plan can fail | ||
// during normal execution of the plan competition. Here is an example: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -289,6 +289,15 @@ unique_ptr<PlanStageStats> getWinningPlanStatsTree(const PlanExecutor* exec) { | |
: std::move(exec->getRootStage()->getStats()); | ||
} | ||
|
||
|
||
/** | ||
* Get PlanExecutor's original winning plan stats tree. | ||
|
||
*/ | ||
unique_ptr<PlanStageStats> getOriginalWinningPlanStatsTree(const PlanExecutor* exec) { | ||
MultiPlanStage* mps = getMultiPlanStage(exec->getRootStage()); | ||
return mps ? std::move(mps->getStats()->children[mps->originalWinningPlanIdx()]) | ||
: std::move(exec->getRootStage()->getStats()); | ||
} | ||
} // namespace | ||
|
||
namespace mongo { | ||
|
@@ -644,6 +653,16 @@ void Explain::generatePlannerInfo(PlanExecutor* exec, | |
BSONObjBuilder plannerBob(out->subobjStart("queryPlanner")); | ||
|
||
plannerBob.append("plannerVersion", QueryPlanner::kPlannerVersion); | ||
|
||
const auto mps = getMultiPlanStage(exec->getRootStage()); | ||
int originaWinningPlanIdx = static_cast<size_t>(mps->originalWinningPlanIdx()); | ||
|
||
if (originaWinningPlanIdx > -1) { | ||
|
||
plannerBob.append("backupPlanUsed", true); | ||
} else { | ||
plannerBob.append("backupPlanUsed", false); | ||
} | ||
|
||
plannerBob.append("namespace", exec->nss().ns()); | ||
|
||
// Find whether there is an index filter set for the query shape. The 'indexFilterSet' | ||
|
@@ -688,6 +707,16 @@ void Explain::generatePlannerInfo(PlanExecutor* exec, | |
} | ||
allPlansBob.doneFast(); | ||
|
||
if (originaWinningPlanIdx > -1) { | ||
// Generate array of original winning plan | ||
BSONObjBuilder originalWinningPlanBob(plannerBob.subobjStart("originalWinningPlan")); | ||
const auto originalWinnerStats = getOriginalWinningPlanStatsTree(exec); | ||
statsToBSON(*originalWinnerStats.get(), | ||
&originalWinningPlanBob, | ||
ExplainOptions::Verbosity::kQueryPlanner); | ||
originalWinningPlanBob.doneFast(); | ||
} | ||
|
||
plannerBob.doneFast(); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you're missing a semi-colon after
.drop()
here.