/**
* Copyright (C) 2013-2014 MongoDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery
#include "mongo/platform/basic.h"
#include "mongo/db/exec/subplan.h"
#include "mongo/client/dbclientinterface.h"
#include "mongo/db/exec/multi_plan.h"
#include "mongo/db/exec/scoped_timer.h"
#include "mongo/db/matcher/extensions_callback_real.h"
#include "mongo/db/query/get_executor.h"
#include "mongo/db/query/plan_executor.h"
#include "mongo/db/query/planner_access.h"
#include "mongo/db/query/planner_analysis.h"
#include "mongo/db/query/query_planner.h"
#include "mongo/db/query/query_planner_common.h"
#include "mongo/db/query/stage_builder.h"
#include "mongo/stdx/memory.h"
#include "mongo/util/log.h"
#include "mongo/util/scopeguard.h"
namespace mongo {
using std::endl;
using std::unique_ptr;
using std::vector;
using stdx::make_unique;
const char* SubplanStage::kStageType = "SUBPLAN";
SubplanStage::SubplanStage(OperationContext* txn,
Collection* collection,
WorkingSet* ws,
const QueryPlannerParams& params,
CanonicalQuery* cq)
: PlanStage(kStageType, txn),
_collection(collection),
_ws(ws),
_plannerParams(params),
_query(cq) {
invariant(_collection);
}
namespace {
/**
* Returns true if 'expr' is an AND that contains a single OR child.
*/
bool isContainedOr(const MatchExpression* expr) {
if (MatchExpression::AND != expr->matchType()) {
return false;
}
size_t numOrs = 0;
for (size_t i = 0; i < expr->numChildren(); ++i) {
if (MatchExpression::OR == expr->getChild(i)->matchType()) {
++numOrs;
}
}
return (numOrs == 1U);
}
} // namespace
bool SubplanStage::canUseSubplanning(const CanonicalQuery& query) {
const LiteParsedQuery& lpq = query.getParsed();
const MatchExpression* expr = query.root();
// Hint provided
if (!lpq.getHint().isEmpty()) {
return false;
}
// Min provided
// Min queries are a special case of hinted queries.
if (!lpq.getMin().isEmpty()) {
return false;
}
// Max provided
// Similar to min, max queries are a special case of hinted queries.
if (!lpq.getMax().isEmpty()) {
return false;
}
// Tailable cursors won't get cached, just turn into collscans.
if (query.getParsed().isTailable()) {
return false;
}
// Snapshot is really a hint.
if (query.getParsed().isSnapshot()) {
return false;
}
// TODO: For now we only allow rooted OR. We should consider also allowing contained OR that
// does not have a TEXT or GEO_NEAR node.
return MatchExpression::OR == expr->matchType();
}
std::unique_ptr SubplanStage::rewriteToRootedOr(
std::unique_ptr root) {
dassert(isContainedOr(root.get()));
// Detach the OR from the root.
std::vector& rootChildren = *root->getChildVector();
std::unique_ptr orChild;
for (size_t i = 0; i < rootChildren.size(); ++i) {
if (MatchExpression::OR == rootChildren[i]->matchType()) {
orChild.reset(rootChildren[i]);
rootChildren.erase(rootChildren.begin() + i);
break;
}
}
// We should have found an OR, and the OR should have at least 2 children.
invariant(orChild);
invariant(orChild->getChildVector());
invariant(orChild->getChildVector()->size() > 1U);
// AND the existing root with each OR child.
std::vector& orChildren = *orChild->getChildVector();
for (size_t i = 0; i < orChildren.size(); ++i) {
std::unique_ptr ama = stdx::make_unique();
ama->add(orChildren[i]);
ama->add(root->shallowClone().release());
orChildren[i] = ama.release();
}
// Normalize and sort the resulting match expression.
orChild = std::unique_ptr(CanonicalQuery::normalizeTree(orChild.release()));
CanonicalQuery::sortTree(orChild.get());
return orChild;
}
Status SubplanStage::planSubqueries() {
_orExpression = _query->root()->shallowClone();
if (isContainedOr(_orExpression.get())) {
_orExpression = rewriteToRootedOr(std::move(_orExpression));
invariant(CanonicalQuery::isValid(_orExpression.get(), _query->getParsed()).isOK());
}
for (size_t i = 0; i < _plannerParams.indices.size(); ++i) {
const IndexEntry& ie = _plannerParams.indices[i];
_indexMap[ie.keyPattern] = i;
LOG(5) << "Subplanner: index " << i << " is " << ie.toString();
}
const ExtensionsCallbackReal extensionsCallback(getOpCtx(), &_collection->ns());
for (size_t i = 0; i < _orExpression->numChildren(); ++i) {
// We need a place to shove the results from planning this branch.
_branchResults.push_back(new BranchPlanningResult());
BranchPlanningResult* branchResult = _branchResults.back();
MatchExpression* orChild = _orExpression->getChild(i);
// Turn the i-th child into its own query.
auto statusWithCQ =
CanonicalQuery::canonicalize(getOpCtx(), *_query, orChild, extensionsCallback);
if (!statusWithCQ.isOK()) {
mongoutils::str::stream ss;
ss << "Can't canonicalize subchild " << orChild->toString() << " "
<< statusWithCQ.getStatus().reason();
return Status(ErrorCodes::BadValue, ss);
}
branchResult->canonicalQuery = std::move(statusWithCQ.getValue());
// Plan the i-th child. We might be able to find a plan for the i-th child in the plan
// cache. If there's no cached plan, then we generate and rank plans using the MPS.
CachedSolution* rawCS;
if (PlanCache::shouldCacheQuery(*branchResult->canonicalQuery) &&
_collection->infoCache()
->getPlanCache()
->get(*branchResult->canonicalQuery, &rawCS)
.isOK()) {
// We have a CachedSolution. Store it for later.
LOG(5) << "Subplanner: cached plan found for child " << i << " of "
<< _orExpression->numChildren();
branchResult->cachedSolution.reset(rawCS);
} else {
// No CachedSolution found. We'll have to plan from scratch.
LOG(5) << "Subplanner: planning child " << i << " of " << _orExpression->numChildren();
// We don't set NO_TABLE_SCAN because peeking at the cache data will keep us from
// considering any plan that's a collscan.
Status status = QueryPlanner::plan(*branchResult->canonicalQuery,
_plannerParams,
&branchResult->solutions.mutableVector());
if (!status.isOK()) {
mongoutils::str::stream ss;
ss << "Can't plan for subchild " << branchResult->canonicalQuery->toString() << " "
<< status.reason();
return Status(ErrorCodes::BadValue, ss);
}
LOG(5) << "Subplanner: got " << branchResult->solutions.size() << " solutions";
if (0 == branchResult->solutions.size()) {
// If one child doesn't have an indexed solution, bail out.
mongoutils::str::stream ss;
ss << "No solutions for subchild " << branchResult->canonicalQuery->toString();
return Status(ErrorCodes::BadValue, ss);
}
}
}
return Status::OK();
}
namespace {
/**
* On success, applies the index tags from 'branchCacheData' (which represent the winning
* plan for 'orChild') to 'compositeCacheData'.
*/
Status tagOrChildAccordingToCache(PlanCacheIndexTree* compositeCacheData,
SolutionCacheData* branchCacheData,
MatchExpression* orChild,
const std::map& indexMap) {
invariant(compositeCacheData);
// We want a well-formed *indexed* solution.
if (NULL == branchCacheData) {
// For example, we don't cache things for 2d indices.
mongoutils::str::stream ss;
ss << "No cache data for subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
if (SolutionCacheData::USE_INDEX_TAGS_SOLN != branchCacheData->solnType) {
mongoutils::str::stream ss;
ss << "No indexed cache data for subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
// Add the index assignments to our original query.
Status tagStatus =
QueryPlanner::tagAccordingToCache(orChild, branchCacheData->tree.get(), indexMap);
if (!tagStatus.isOK()) {
mongoutils::str::stream ss;
ss << "Failed to extract indices from subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
// Add the child's cache data to the cache data we're creating for the main query.
compositeCacheData->children.push_back(branchCacheData->tree->clone());
return Status::OK();
}
} // namespace
Status SubplanStage::choosePlanForSubqueries(PlanYieldPolicy* yieldPolicy) {
// This is the skeleton of index selections that is inserted into the cache.
std::unique_ptr cacheData(new PlanCacheIndexTree());
for (size_t i = 0; i < _orExpression->numChildren(); ++i) {
MatchExpression* orChild = _orExpression->getChild(i);
BranchPlanningResult* branchResult = _branchResults[i];
if (branchResult->cachedSolution.get()) {
// We can get the index tags we need out of the cache.
Status tagStatus = tagOrChildAccordingToCache(
cacheData.get(), branchResult->cachedSolution->plannerData[0], orChild, _indexMap);
if (!tagStatus.isOK()) {
return tagStatus;
}
} else if (1 == branchResult->solutions.size()) {
QuerySolution* soln = branchResult->solutions.front();
Status tagStatus = tagOrChildAccordingToCache(
cacheData.get(), soln->cacheData.get(), orChild, _indexMap);
if (!tagStatus.isOK()) {
return tagStatus;
}
} else {
// N solutions, rank them.
// We already checked for zero solutions in planSubqueries(...).
invariant(!branchResult->solutions.empty());
_ws->clear();
// We pass the SometimesCache option to the MPS because the SubplanStage currently does
// not use the CachedPlanStage's eviction mechanism. We therefore are more conservative
// about putting a potentially bad plan into the cache in the subplan path.
// We temporarily add the MPS to _children to ensure that we pass down all
// save/restore/invalidate messages that can be generated if pickBestPlan yields.
invariant(_children.empty());
_children.emplace_back(
stdx::make_unique(getOpCtx(),
_collection,
branchResult->canonicalQuery.get(),
MultiPlanStage::CachingMode::SometimesCache));
ON_BLOCK_EXIT([&] {
invariant(_children.size() == 1); // Make sure nothing else was added to _children.
_children.pop_back();
});
MultiPlanStage* multiPlanStage = static_cast(child().get());
// Dump all the solutions into the MPS.
for (size_t ix = 0; ix < branchResult->solutions.size(); ++ix) {
PlanStage* nextPlanRoot;
invariant(StageBuilder::build(getOpCtx(),
_collection,
*branchResult->canonicalQuery,
*branchResult->solutions[ix],
_ws,
&nextPlanRoot));
// Takes ownership of solution with index 'ix' and 'nextPlanRoot'.
multiPlanStage->addPlan(branchResult->solutions.releaseAt(ix), nextPlanRoot, _ws);
}
Status planSelectStat = multiPlanStage->pickBestPlan(yieldPolicy);
if (!planSelectStat.isOK()) {
return planSelectStat;
}
if (!multiPlanStage->bestPlanChosen()) {
mongoutils::str::stream ss;
ss << "Failed to pick best plan for subchild "
<< branchResult->canonicalQuery->toString();
return Status(ErrorCodes::BadValue, ss);
}
QuerySolution* bestSoln = multiPlanStage->bestSolution();
// Check that we have good cache data. For example, we don't cache things
// for 2d indices.
if (NULL == bestSoln->cacheData.get()) {
mongoutils::str::stream ss;
ss << "No cache data for subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
if (SolutionCacheData::USE_INDEX_TAGS_SOLN != bestSoln->cacheData->solnType) {
mongoutils::str::stream ss;
ss << "No indexed cache data for subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
// Add the index assignments to our original query.
Status tagStatus = QueryPlanner::tagAccordingToCache(
orChild, bestSoln->cacheData->tree.get(), _indexMap);
if (!tagStatus.isOK()) {
mongoutils::str::stream ss;
ss << "Failed to extract indices from subchild " << orChild->toString();
return Status(ErrorCodes::BadValue, ss);
}
cacheData->children.push_back(bestSoln->cacheData->tree->clone());
}
}
// Must do this before using the planner functionality.
sortUsingTags(_orExpression.get());
// Use the cached index assignments to build solnRoot. Takes ownership of '_orExpression'.
QuerySolutionNode* solnRoot = QueryPlannerAccess::buildIndexedDataAccess(
*_query, _orExpression.release(), false, _plannerParams.indices, _plannerParams);
if (NULL == solnRoot) {
mongoutils::str::stream ss;
ss << "Failed to build indexed data path for subplanned query\n";
return Status(ErrorCodes::BadValue, ss);
}
LOG(5) << "Subplanner: fully tagged tree is " << solnRoot->toString();
// Takes ownership of 'solnRoot'
_compositeSolution.reset(
QueryPlannerAnalysis::analyzeDataAccess(*_query, _plannerParams, solnRoot));
if (NULL == _compositeSolution.get()) {
mongoutils::str::stream ss;
ss << "Failed to analyze subplanned query";
return Status(ErrorCodes::BadValue, ss);
}
LOG(5) << "Subplanner: Composite solution is " << _compositeSolution->toString();
// Use the index tags from planning each branch to construct the composite solution,
// and set that solution as our child stage.
_ws->clear();
PlanStage* root;
invariant(StageBuilder::build(
getOpCtx(), _collection, *_query, *_compositeSolution.get(), _ws, &root));
invariant(_children.empty());
_children.emplace_back(root);
return Status::OK();
}
Status SubplanStage::choosePlanWholeQuery(PlanYieldPolicy* yieldPolicy) {
// Clear out the working set. We'll start with a fresh working set.
_ws->clear();
// Use the query planning module to plan the whole query.
std::vector rawSolutions;
Status status = QueryPlanner::plan(*_query, _plannerParams, &rawSolutions);
if (!status.isOK()) {
return Status(ErrorCodes::BadValue,
"error processing query: " + _query->toString() +
" planner returned error: " + status.reason());
}
OwnedPointerVector solutions(rawSolutions);
// We cannot figure out how to answer the query. Perhaps it requires an index
// we do not have?
if (0 == solutions.size()) {
return Status(ErrorCodes::BadValue,
str::stream() << "error processing query: " << _query->toString()
<< " No query solutions");
}
if (1 == solutions.size()) {
PlanStage* root;
// Only one possible plan. Run it. Build the stages from the solution.
verify(StageBuilder::build(getOpCtx(), _collection, *_query, *solutions[0], _ws, &root));
invariant(_children.empty());
_children.emplace_back(root);
// This SubplanStage takes ownership of the query solution.
_compositeSolution.reset(solutions.popAndReleaseBack());
return Status::OK();
} else {
// Many solutions. Create a MultiPlanStage to pick the best, update the cache,
// and so on. The working set will be shared by all candidate plans.
invariant(_children.empty());
_children.emplace_back(new MultiPlanStage(getOpCtx(), _collection, _query));
MultiPlanStage* multiPlanStage = static_cast(child().get());
for (size_t ix = 0; ix < solutions.size(); ++ix) {
if (solutions[ix]->cacheData.get()) {
solutions[ix]->cacheData->indexFilterApplied = _plannerParams.indexFiltersApplied;
}
// version of StageBuild::build when WorkingSet is shared
PlanStage* nextPlanRoot;
verify(StageBuilder::build(
getOpCtx(), _collection, *_query, *solutions[ix], _ws, &nextPlanRoot));
// Takes ownership of 'solutions[ix]' and 'nextPlanRoot'.
multiPlanStage->addPlan(solutions.releaseAt(ix), nextPlanRoot, _ws);
}
// Delegate the the MultiPlanStage's plan selection facility.
Status planSelectStat = multiPlanStage->pickBestPlan(yieldPolicy);
if (!planSelectStat.isOK()) {
return planSelectStat;
}
return Status::OK();
}
}
Status SubplanStage::pickBestPlan(PlanYieldPolicy* yieldPolicy) {
// Adds the amount of time taken by pickBestPlan() to executionTimeMillis. There's lots of
// work that happens here, so this is needed for the time accounting to make sense.
ScopedTimer timer(getClock(), &_commonStats.executionTimeMillis);
// Plan each branch of the $or.
Status subplanningStatus = planSubqueries();
if (!subplanningStatus.isOK()) {
return choosePlanWholeQuery(yieldPolicy);
}
// Use the multi plan stage to select a winning plan for each branch, and then construct
// the overall winning plan from the resulting index tags.
Status subplanSelectStat = choosePlanForSubqueries(yieldPolicy);
if (!subplanSelectStat.isOK()) {
return choosePlanWholeQuery(yieldPolicy);
}
return Status::OK();
}
bool SubplanStage::isEOF() {
// If we're running we best have a runner.
invariant(child());
return child()->isEOF();
}
PlanStage::StageState SubplanStage::doWork(WorkingSetID* out) {
if (isEOF()) {
return PlanStage::IS_EOF;
}
invariant(child());
return child()->work(out);
}
unique_ptr SubplanStage::getStats() {
_commonStats.isEOF = isEOF();
unique_ptr ret = make_unique(_commonStats, STAGE_SUBPLAN);
ret->children.emplace_back(child()->getStats());
return ret;
}
bool SubplanStage::branchPlannedFromCache(size_t i) const {
return NULL != _branchResults[i]->cachedSolution.get();
}
const SpecificStats* SubplanStage::getSpecificStats() const {
return NULL;
}
} // namespace mongo