summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorJacob Evans <jacob.evans@10gen.com>2019-01-04 13:15:58 -0500
committerJacob Evans <jacob.evans@10gen.com>2019-01-29 13:53:10 -0500
commit41557fac170a905cb7b0cca7564e4c0ec48bbefd (patch)
tree5dc37a78b02e03ceefa34f75abc13b5062e17a0e /src/mongo
parentb4ee2a119c65d1f2a0cb2288c1242fbf4f74a543 (diff)
downloadmongo-41557fac170a905cb7b0cca7564e4c0ec48bbefd.tar.gz
SERVER-38695 Make QuerySolutionNode subclasses for projection fast-paths
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/db/exec/delete.cpp25
-rw-r--r--src/mongo/db/exec/projection.cpp18
-rw-r--r--src/mongo/db/exec/projection.h30
-rw-r--r--src/mongo/db/exec/projection_exec.h6
-rw-r--r--src/mongo/db/exec/update.cpp20
-rw-r--r--src/mongo/db/query/explain.cpp4
-rw-r--r--src/mongo/db/query/get_executor.cpp28
-rw-r--r--src/mongo/db/query/plan_ranker.cpp4
-rw-r--r--src/mongo/db/query/planner_analysis.cpp213
-rw-r--r--src/mongo/db/query/query_planner_test_lib.cpp24
-rw-r--r--src/mongo/db/query/query_solution.cpp50
-rw-r--r--src/mongo/db/query/query_solution.h135
-rw-r--r--src/mongo/db/query/query_solution_test.cpp40
-rw-r--r--src/mongo/db/query/stage_builder.cpp44
-rw-r--r--src/mongo/db/query/stage_types.h6
15 files changed, 402 insertions, 245 deletions
diff --git a/src/mongo/db/exec/delete.cpp b/src/mongo/db/exec/delete.cpp
index 71e6f6a5628..e71ec2adcaf 100644
--- a/src/mongo/db/exec/delete.cpp
+++ b/src/mongo/db/exec/delete.cpp
@@ -294,16 +294,21 @@ long long DeleteStage::getNumDeleted(const PlanExecutor& exec) {
// If the collection exists, the delete plan may either have a delete stage at the root, or (for
// findAndModify) a projection stage wrapping a delete stage.
- if (StageType::STAGE_PROJECTION == exec.getRootStage()->stageType()) {
- invariant(exec.getRootStage()->getChildren().size() == 1U);
- invariant(StageType::STAGE_DELETE == exec.getRootStage()->child()->stageType());
- const SpecificStats* stats = exec.getRootStage()->child()->getSpecificStats();
- return static_cast<const DeleteStats*>(stats)->docsDeleted;
- } else {
- invariant(StageType::STAGE_DELETE == exec.getRootStage()->stageType());
- const auto* deleteStats =
- static_cast<const DeleteStats*>(exec.getRootStage()->getSpecificStats());
- return deleteStats->docsDeleted;
+ switch (exec.getRootStage()->stageType()) {
+ case StageType::STAGE_PROJECTION_DEFAULT:
+ case StageType::STAGE_PROJECTION_COVERED:
+ case StageType::STAGE_PROJECTION_SIMPLE: {
+ invariant(exec.getRootStage()->getChildren().size() == 1U);
+ invariant(StageType::STAGE_DELETE == exec.getRootStage()->child()->stageType());
+ const SpecificStats* stats = exec.getRootStage()->child()->getSpecificStats();
+ return static_cast<const DeleteStats*>(stats)->docsDeleted;
+ }
+ default: {
+ invariant(StageType::STAGE_DELETE == exec.getRootStage()->stageType());
+ const auto* deleteStats =
+ static_cast<const DeleteStats*>(exec.getRootStage()->getSpecificStats());
+ return deleteStats->docsDeleted;
+ }
}
}
diff --git a/src/mongo/db/exec/projection.cpp b/src/mongo/db/exec/projection.cpp
index 18debacd536..0a559fb2a1c 100644
--- a/src/mongo/db/exec/projection.cpp
+++ b/src/mongo/db/exec/projection.cpp
@@ -121,8 +121,9 @@ StatusWith<BSONObj> provideMetaFieldsAndPerformExec(const ProjectionExec& exec,
ProjectionStage::ProjectionStage(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
- std::unique_ptr<PlanStage> child)
- : PlanStage(opCtx, std::move(child), kStageType), _projObj(projObj), _ws(*ws) {}
+ std::unique_ptr<PlanStage> child,
+ const char* stageType)
+ : PlanStage(opCtx, std::move(child), stageType), _projObj(projObj), _ws(*ws) {}
// static
void ProjectionStage::getSimpleInclusionFields(const BSONObj& projObj, FieldSet* includedFields) {
@@ -183,7 +184,7 @@ PlanStage::StageState ProjectionStage::doWork(WorkingSetID* out) {
std::unique_ptr<PlanStageStats> ProjectionStage::getStats() {
_commonStats.isEOF = isEOF();
- auto ret = std::make_unique<PlanStageStats>(_commonStats, STAGE_PROJECTION);
+ auto ret = std::make_unique<PlanStageStats>(_commonStats, stageType());
auto projStats = std::make_unique<ProjectionStats>(_specificStats);
projStats->projObj = _projObj;
@@ -197,10 +198,10 @@ ProjectionStageDefault::ProjectionStageDefault(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
std::unique_ptr<PlanStage> child,
- const MatchExpression* fullExpression,
+ const MatchExpression& fullExpression,
const CollatorInterface* collator)
- : ProjectionStage(opCtx, projObj, ws, std::move(child)),
- _exec(opCtx, projObj, fullExpression, collator) {}
+ : ProjectionStage(opCtx, projObj, ws, std::move(child), "PROJECTION_DEFAULT"),
+ _exec(opCtx, projObj, &fullExpression, collator) {}
Status ProjectionStageDefault::transform(WorkingSetMember* member) const {
// The default no-fast-path case.
@@ -234,7 +235,8 @@ ProjectionStageCovered::ProjectionStageCovered(OperationContext* opCtx,
WorkingSet* ws,
std::unique_ptr<PlanStage> child,
const BSONObj& coveredKeyObj)
- : ProjectionStage(opCtx, projObj, ws, std::move(child)), _coveredKeyObj(coveredKeyObj) {
+ : ProjectionStage(opCtx, projObj, ws, std::move(child), "PROJECTION_COVERED"),
+ _coveredKeyObj(coveredKeyObj) {
invariant(projObjHasOwnedData());
// Figure out what fields are in the projection.
getSimpleInclusionFields(_projObj, &_includedFields);
@@ -289,7 +291,7 @@ ProjectionStageSimple::ProjectionStageSimple(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
std::unique_ptr<PlanStage> child)
- : ProjectionStage(opCtx, projObj, ws, std::move(child)) {
+ : ProjectionStage(opCtx, projObj, ws, std::move(child), "PROJECTION_SIMPLE") {
invariant(projObjHasOwnedData());
// Figure out what fields are in the projection.
getSimpleInclusionFields(_projObj, &_includedFields);
diff --git a/src/mongo/db/exec/projection.h b/src/mongo/db/exec/projection.h
index b28cdcd9302..e229d12a219 100644
--- a/src/mongo/db/exec/projection.h
+++ b/src/mongo/db/exec/projection.h
@@ -30,7 +30,6 @@
#pragma once
-
#include "mongo/db/exec/plan_stage.h"
#include "mongo/db/exec/projection_exec.h"
#include "mongo/db/jsobj.h"
@@ -50,18 +49,13 @@ protected:
ProjectionStage(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
- std::unique_ptr<PlanStage> child);
+ std::unique_ptr<PlanStage> child,
+ const char* stageType);
public:
- static constexpr const char* kStageType = "PROJECTION";
-
bool isEOF() final;
StageState doWork(WorkingSetID* out) final;
- StageType stageType() const final {
- return STAGE_PROJECTION;
- }
-
std::unique_ptr<PlanStageStats> getStats() final;
const SpecificStats* getSpecificStats() const final {
@@ -106,15 +100,19 @@ private:
class ProjectionStageDefault final : public ProjectionStage {
public:
/**
- * ProjectionNode::DEFAULT should use this for construction.
+ * ProjectionNodeDefault should use this for construction.
*/
ProjectionStageDefault(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
std::unique_ptr<PlanStage> child,
- const MatchExpression* fullExpression,
+ const MatchExpression& fullExpression,
const CollatorInterface* collator);
+ StageType stageType() const final {
+ return STAGE_PROJECTION_DEFAULT;
+ }
+
private:
Status transform(WorkingSetMember* member) const final;
@@ -130,7 +128,7 @@ private:
class ProjectionStageCovered final : public ProjectionStage {
public:
/**
- * ProjectionNode::COVERED_ONE_INDEX should obtain a fast-path object through this constructor.
+ * ProjectionNodeCovered should obtain a fast-path object through this constructor.
*/
ProjectionStageCovered(OperationContext* opCtx,
const BSONObj& projObj,
@@ -138,6 +136,10 @@ public:
std::unique_ptr<PlanStage> child,
const BSONObj& coveredKeyObj);
+ StageType stageType() const final {
+ return STAGE_PROJECTION_COVERED;
+ }
+
private:
Status transform(WorkingSetMember* member) const final;
@@ -164,13 +166,17 @@ private:
class ProjectionStageSimple final : public ProjectionStage {
public:
/**
- * ProjectionNode::SIMPLE_DOC should obtain a fast-path object through this constructor.
+ * ProjectionNodeSimple should obtain a fast-path object through this constructor.
*/
ProjectionStageSimple(OperationContext* opCtx,
const BSONObj& projObj,
WorkingSet* ws,
std::unique_ptr<PlanStage> child);
+ StageType stageType() const final {
+ return STAGE_PROJECTION_SIMPLE;
+ }
+
private:
Status transform(WorkingSetMember* member) const final;
diff --git a/src/mongo/db/exec/projection_exec.h b/src/mongo/db/exec/projection_exec.h
index 4d79808b0d7..e330184f3a7 100644
--- a/src/mongo/db/exec/projection_exec.h
+++ b/src/mongo/db/exec/projection_exec.h
@@ -141,9 +141,9 @@ public:
const int64_t recordId = 0) const;
/**
- * Performs a projection given a function to retrieve fields by name. This function handles
- * projections which do not qualify for the COVERED_ONE_INDEX fast-path but are still coverd by
- * indices.
+ * Performs a projection given index 'KeyData' to directly retrieve results. This function
+ * handles projections which do not qualify for the ProjectionNodeCovered fast-path but are
+ * still covered by indices.
*/
StatusWith<BSONObj> projectCovered(
const std::vector<IndexKeyDatum>& keyData,
diff --git a/src/mongo/db/exec/update.cpp b/src/mongo/db/exec/update.cpp
index 0dfaacddc87..1aa07aa2bff 100644
--- a/src/mongo/db/exec/update.cpp
+++ b/src/mongo/db/exec/update.cpp
@@ -831,14 +831,18 @@ const UpdateStats* UpdateStage::getUpdateStats(const PlanExecutor* exec) {
// If the collection exists, then we expect the root of the plan tree to either be an update
// stage, or (for findAndModify) a projection stage wrapping an update stage.
- if (StageType::STAGE_PROJECTION == exec->getRootStage()->stageType()) {
- invariant(exec->getRootStage()->getChildren().size() == 1U);
- invariant(StageType::STAGE_UPDATE == exec->getRootStage()->child()->stageType());
- const SpecificStats* stats = exec->getRootStage()->child()->getSpecificStats();
- return static_cast<const UpdateStats*>(stats);
- } else {
- invariant(StageType::STAGE_UPDATE == exec->getRootStage()->stageType());
- return static_cast<const UpdateStats*>(exec->getRootStage()->getSpecificStats());
+ switch (exec->getRootStage()->stageType()) {
+ case StageType::STAGE_PROJECTION_DEFAULT:
+ case StageType::STAGE_PROJECTION_COVERED:
+ case StageType::STAGE_PROJECTION_SIMPLE: {
+ invariant(exec->getRootStage()->getChildren().size() == 1U);
+ invariant(StageType::STAGE_UPDATE == exec->getRootStage()->child()->stageType());
+ const SpecificStats* stats = exec->getRootStage()->child()->getSpecificStats();
+ return static_cast<const UpdateStats*>(stats);
+ }
+ default:
+ invariant(StageType::STAGE_UPDATE == exec->getRootStage()->stageType());
+ return static_cast<const UpdateStats*>(exec->getRootStage()->getSpecificStats());
}
}
diff --git a/src/mongo/db/query/explain.cpp b/src/mongo/db/query/explain.cpp
index ad299239b82..0e54916922d 100644
--- a/src/mongo/db/query/explain.cpp
+++ b/src/mongo/db/query/explain.cpp
@@ -512,7 +512,9 @@ void Explain::statsToBSON(const PlanStageStats& stats,
} else if (STAGE_LIMIT == stats.stageType) {
LimitStats* spec = static_cast<LimitStats*>(stats.specific.get());
bob->appendNumber("limitAmount", spec->limit);
- } else if (STAGE_PROJECTION == stats.stageType) {
+ } else if (STAGE_PROJECTION_DEFAULT == stats.stageType ||
+ STAGE_PROJECTION_COVERED == stats.stageType ||
+ STAGE_PROJECTION_SIMPLE == stats.stageType) {
ProjectionStats* spec = static_cast<ProjectionStats*>(stats.specific.get());
bob->append("transformBy", spec->projObj);
} else if (STAGE_RECORD_STORE_FAST_COUNT == stats.stageType) {
diff --git a/src/mongo/db/query/get_executor.cpp b/src/mongo/db/query/get_executor.cpp
index c3bdcae3786..549d752d440 100644
--- a/src/mongo/db/query/get_executor.cpp
+++ b/src/mongo/db/query/get_executor.cpp
@@ -398,7 +398,7 @@ StatusWith<PrepareExecutionResult> prepareExecution(OperationContext* opCtx,
canonicalQuery->getProj()->getProjObj(),
ws,
std::move(root),
- canonicalQuery->root(),
+ *canonicalQuery->root(),
canonicalQuery->getCollator());
} else {
root = make_unique<ProjectionStageSimple>(
@@ -792,7 +792,7 @@ StatusWith<unique_ptr<PlanStage>> applyProjection(OperationContext* opCtx,
proj,
ws,
std::unique_ptr<PlanStage>(root.release()),
- cq->root(),
+ *cq->root(),
cq->getCollator())};
}
@@ -1360,8 +1360,12 @@ bool turnIxscanIntoDistinctIxscan(QuerySolution* soln,
QuerySolutionNode* root = soln->root.get();
// Root stage must be a project.
- if (STAGE_PROJECTION != root->getType()) {
- return false;
+ switch (root->getType()) {
+ default:
+ return false;
+ case STAGE_PROJECTION_DEFAULT:
+ case STAGE_PROJECTION_COVERED:
+ case STAGE_PROJECTION_SIMPLE:;
}
// Child should be either an ixscan or fetch.
@@ -1474,7 +1478,13 @@ bool turnIxscanIntoDistinctIxscan(QuerySolution* soln,
// If there is a fetch node, then there is no need for the projection. The fetch node should
// become the new root, with the distinct as its child. The PROJECT=>FETCH=>IXSCAN tree
// should become FETCH=>DISTINCT_SCAN.
- invariant(STAGE_PROJECTION == root->getType());
+ switch (root->getType()) {
+ default:
+ MONGO_UNREACHABLE;
+ case STAGE_PROJECTION_DEFAULT:
+ case STAGE_PROJECTION_COVERED:
+ case STAGE_PROJECTION_SIMPLE:;
+ }
invariant(STAGE_FETCH == root->children[0]->getType());
invariant(STAGE_IXSCAN == root->children[0]->children[0]->getType());
@@ -1491,7 +1501,13 @@ bool turnIxscanIntoDistinctIxscan(QuerySolution* soln,
fetchNode->children[0] = distinctNode.release();
} else {
// There is no fetch node. The PROJECT=>IXSCAN tree should become PROJECT=>DISTINCT_SCAN.
- invariant(STAGE_PROJECTION == root->getType());
+ switch (root->getType()) {
+ default:
+ MONGO_UNREACHABLE;
+ case STAGE_PROJECTION_DEFAULT:
+ case STAGE_PROJECTION_COVERED:
+ case STAGE_PROJECTION_SIMPLE:;
+ }
invariant(STAGE_IXSCAN == root->children[0]->getType());
// Take ownership of the index scan node, detaching it from the solution tree.
diff --git a/src/mongo/db/query/plan_ranker.cpp b/src/mongo/db/query/plan_ranker.cpp
index a8260b22c7f..897e664e7f1 100644
--- a/src/mongo/db/query/plan_ranker.cpp
+++ b/src/mongo/db/query/plan_ranker.cpp
@@ -214,7 +214,9 @@ double PlanRanker::scoreTree(const PlanStageStats* stats) {
// We only do this when we have a projection stage because we have so many jstests that
// check bounds even when a collscan plan is just as good as the ixscan'd plan :(
double noFetchBonus = epsilon;
- if (hasStage(STAGE_PROJECTION, stats) && hasStage(STAGE_FETCH, stats)) {
+ if ((hasStage(STAGE_PROJECTION_DEFAULT, stats) || hasStage(STAGE_PROJECTION_COVERED, stats) ||
+ hasStage(STAGE_PROJECTION_SIMPLE, stats)) &&
+ hasStage(STAGE_FETCH, stats)) {
noFetchBonus = 0;
}
diff --git a/src/mongo/db/query/planner_analysis.cpp b/src/mongo/db/query/planner_analysis.cpp
index 0bbce034fcd..8cdc7d91f43 100644
--- a/src/mongo/db/query/planner_analysis.cpp
+++ b/src/mongo/db/query/planner_analysis.cpp
@@ -291,6 +291,112 @@ void geoSkipValidationOn(const std::set<StringData>& twoDSphereFields,
}
}
+/**
+ * If any field is missing from the list of fields the projection wants, we are not covered.
+ */
+auto isCoveredOrAlreadyFetched(const vector<StringData>& fields,
+ const QuerySolutionNode& solnRoot) {
+ for (size_t i = 0; i < fields.size(); ++i) {
+ if (!solnRoot.hasField(fields[i].toString()))
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Checks all properties that exclude a projection from being simple.
+ */
+auto isSimpleProjection(const CanonicalQuery& query) {
+ return !query.getProj()->wantIndexKey() && !query.getProj()->wantSortKey() &&
+ !query.getProj()->hasDottedFieldPath() && !query.getProj()->requiresDocument();
+}
+
+/**
+ * If 'solnRoot' is returning index key data from a single index, returns the associated index key
+ * pattern. Otherwise, returns an empty object.
+ */
+auto produceCoveredKeyObj(QuerySolutionNode* solnRoot) {
+ vector<QuerySolutionNode*> leafNodes;
+ getLeafNodes(solnRoot, &leafNodes);
+
+ // Both the IXSCAN and DISTINCT stages provide covered key data.
+ if (1 == leafNodes.size()) {
+ if (STAGE_IXSCAN == leafNodes[0]->getType()) {
+ IndexScanNode* ixn = static_cast<IndexScanNode*>(leafNodes[0]);
+ return ixn->index.keyPattern;
+ } else if (STAGE_DISTINCT_SCAN == leafNodes[0]->getType()) {
+ DistinctNode* dn = static_cast<DistinctNode*>(leafNodes[0]);
+ return dn->index.keyPattern;
+ }
+ }
+ return BSONObj();
+}
+
+/**
+ * When projection needs to be added to the solution tree, this function chooses between the default
+ * implementation and one of the fast paths.
+ */
+std::unique_ptr<ProjectionNode> analyzeProjection(const CanonicalQuery& query,
+ std::unique_ptr<QuerySolutionNode> solnRoot,
+ const bool hasSortStage) {
+ const QueryRequest& qr = query.getQueryRequest();
+
+ // If there's no sort stage but we have a sortKey meta-projection, we need to add a stage to
+ // generate the sort key computed data.
+ auto addSortKeyGeneratorStageIfNeeded = [&]() {
+ if (!hasSortStage && query.getProj()->wantSortKey()) {
+ auto keyGenNode = std::make_unique<SortKeyGeneratorNode>();
+ keyGenNode->sortSpec = qr.getSort();
+ keyGenNode->children.push_back(solnRoot.release());
+ solnRoot = std::move(keyGenNode);
+ }
+ };
+
+ LOG(5) << "PROJECTION: Current plan is:\n" << redact(solnRoot->toString());
+
+ // If the projection requires the entire document we add a fetch stage if not present. Otherwise
+ // we add a fetch stage if we are not covered and not returnKey.
+ if ((query.getProj()->requiresDocument() && !solnRoot->fetched()) ||
+ (!isCoveredOrAlreadyFetched(query.getProj()->getRequiredFields(), *solnRoot) &&
+ !query.getProj()->wantIndexKey())) {
+ auto fetch = std::make_unique<FetchNode>();
+ fetch->children.push_back(solnRoot.release());
+ solnRoot = std::move(fetch);
+ }
+
+ // There are two projection fast paths available for simple inclusion projections that don't
+ // need an index key or sort key, don't have any dotted-path inclusions, and don't have the
+ // 'requiresDocument' property: the ProjectionNodeSimple fast-path for plans that have a fetch
+ // stage and the ProjectionNodeCovered for plans with an index scan that the projection can
+ // cover. Plans that don't meet all the requirements for these fast path projections will all
+ // use ProjectionNodeDefault, which is able to handle all projections, covered or otherwise.
+ if (isSimpleProjection(query)) {
+ // If the projection is simple, but not covered, use 'ProjectionNodeSimple'.
+ if (solnRoot->fetched()) {
+ addSortKeyGeneratorStageIfNeeded();
+ return std::make_unique<ProjectionNodeSimple>(
+ std::move(solnRoot), *query.root(), qr.getProj(), *query.getProj());
+ } else {
+ // If we're here we're not fetched so we're covered. Let's see if we can get out of
+ // using the default projType. If 'solnRoot' is an index scan we can use the faster
+ // covered impl.
+ BSONObj coveredKeyObj = produceCoveredKeyObj(solnRoot.get());
+ if (!coveredKeyObj.isEmpty()) {
+ addSortKeyGeneratorStageIfNeeded();
+ return std::make_unique<ProjectionNodeCovered>(std::move(solnRoot),
+ *query.root(),
+ qr.getProj(),
+ *query.getProj(),
+ std::move(coveredKeyObj));
+ }
+ }
+ }
+
+ addSortKeyGeneratorStageIfNeeded();
+ return std::make_unique<ProjectionNodeDefault>(
+ std::move(solnRoot), *query.root(), qr.getProj(), *query.getProj());
+}
+
} // namespace
// static
@@ -620,7 +726,7 @@ std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
const CanonicalQuery& query,
const QueryPlannerParams& params,
std::unique_ptr<QuerySolutionNode> solnRoot) {
- auto soln = stdx::make_unique<QuerySolution>();
+ auto soln = std::make_unique<QuerySolution>();
soln->filterData = query.getQueryObj();
soln->indexFilterApplied = params.indexFiltersApplied;
@@ -649,9 +755,9 @@ std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
}
if (fetch) {
- FetchNode* fetch = new FetchNode();
- fetch->children.push_back(solnRoot.release());
- solnRoot.reset(fetch);
+ FetchNode* fetchNode = new FetchNode();
+ fetchNode->children.push_back(solnRoot.release());
+ solnRoot.reset(fetchNode);
}
}
@@ -665,7 +771,7 @@ std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
// This can happen if we need to create a blocking sort stage and we're not allowed to.
if (!solnRoot) {
- return NULL;
+ return nullptr;
}
// A solution can be blocking if it has a blocking sort stage or
@@ -675,7 +781,6 @@ std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
const QueryRequest& qr = query.getQueryRequest();
-
if (qr.getSkip()) {
auto skip = std::make_unique<SkipNode>();
skip->skip = *qr.getSkip();
@@ -684,102 +789,12 @@ std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
}
// Project the results.
- if (NULL != query.getProj()) {
- LOG(5) << "PROJECTION: Current plan is:\n" << redact(solnRoot->toString());
-
- ProjectionNode::ProjectionType projType = ProjectionNode::DEFAULT;
- BSONObj coveredKeyObj;
-
- if (query.getProj()->requiresDocument()) {
- // If the projection requires the entire document, somebody must fetch.
- if (!solnRoot->fetched()) {
- FetchNode* fetch = new FetchNode();
- fetch->children.push_back(solnRoot.release());
- solnRoot.reset(fetch);
- }
- } else if (!query.getProj()->wantIndexKey()) {
- // The only way we're here is if it's a simple inclusion projection. Often such
- // simple projections are eligible for an optimized execution path. However, in some
- // special cases (e.g. dotted paths or the presence of a sortKey $meta projection), we
- // may have to fall back to the default execution path.
- const vector<StringData>& fields = query.getProj()->getRequiredFields();
- bool covered = true;
- for (size_t i = 0; i < fields.size(); ++i) {
- if (!solnRoot->hasField(fields[i].toString())) {
- covered = false;
- break;
- }
- }
-
- // If any field is missing from the list of fields the projection wants,
- // a fetch is required.
- if (!covered) {
- FetchNode* fetch = new FetchNode();
- fetch->children.push_back(solnRoot.release());
- solnRoot.reset(fetch);
-
- // It's simple but we'll have the full document and we should just iterate
- // over that.
- projType = ProjectionNode::SIMPLE_DOC;
- } else {
- if (solnRoot->fetched()) {
- // Fetched implies hasObj() so let's run with that.
- projType = ProjectionNode::SIMPLE_DOC;
- } else {
- // If we're here we're not fetched so we're covered. Let's see if we can
- // get out of using the default projType. If there's only one leaf
- // underneath and it's giving us index data we can use the faster covered
- // impl.
- vector<QuerySolutionNode*> leafNodes;
- getLeafNodes(solnRoot.get(), &leafNodes);
-
- if (1 == leafNodes.size()) {
- // Both the IXSCAN and DISTINCT stages provide covered key data.
- if (STAGE_IXSCAN == leafNodes[0]->getType()) {
- projType = ProjectionNode::COVERED_ONE_INDEX;
- IndexScanNode* ixn = static_cast<IndexScanNode*>(leafNodes[0]);
- coveredKeyObj = ixn->index.keyPattern;
- } else if (STAGE_DISTINCT_SCAN == leafNodes[0]->getType()) {
- projType = ProjectionNode::COVERED_ONE_INDEX;
- DistinctNode* dn = static_cast<DistinctNode*>(leafNodes[0]);
- coveredKeyObj = dn->index.keyPattern;
- }
- }
- }
- }
-
- // If we have a $meta sortKey, just use the project default path, as currently the
- // project fast paths cannot handle $meta sortKey projections.
- //
- // Similarly, the fast paths cannot handle dotted field paths.
- if (query.getProj()->wantSortKey() || query.getProj()->hasDottedFieldPath()) {
- projType = ProjectionNode::DEFAULT;
- }
- }
+ if (query.getProj()) {
+ solnRoot = analyzeProjection(query, std::move(solnRoot), hasSortStage);
// If we don't have a covered project, and we're not allowed to put an uncovered one in,
// bail out.
- if (solnRoot->fetched() &&
- (params.options & QueryPlannerParams::NO_UNCOVERED_PROJECTIONS)) {
+ if (solnRoot->fetched() && params.options & QueryPlannerParams::NO_UNCOVERED_PROJECTIONS)
return nullptr;
- }
-
- // If there's no sort stage but we have a sortKey meta-projection, we need to add a stage to
- // generate the sort key computed data.
- if (!hasSortStage && query.getProj()->wantSortKey()) {
- SortKeyGeneratorNode* keyGenNode = new SortKeyGeneratorNode();
- keyGenNode->sortSpec = qr.getSort();
- keyGenNode->children.push_back(solnRoot.release());
- solnRoot.reset(keyGenNode);
- }
-
- // We now know we have whatever data is required for the projection.
- ProjectionNode* projNode = new ProjectionNode(*query.getProj());
- projNode->children.push_back(solnRoot.release());
- projNode->fullExpression = query.root();
- projNode->projection = qr.getProj();
- projNode->projType = projType;
- projNode->coveredKeyObj = coveredKeyObj;
- solnRoot.reset(projNode);
} else {
// If there's no projection, we must fetch, as the user wants the entire doc.
if (!solnRoot->fetched() && !(params.options & QueryPlannerParams::IS_COUNT)) {
diff --git a/src/mongo/db/query/query_planner_test_lib.cpp b/src/mongo/db/query/query_planner_test_lib.cpp
index dc57f0c00fc..8cc616dbbc0 100644
--- a/src/mongo/db/query/query_planner_test_lib.cpp
+++ b/src/mongo/db/query/query_planner_test_lib.cpp
@@ -576,7 +576,9 @@ bool QueryPlannerTestLib::solutionMatches(const BSONObj& testSoln,
}
return childrenMatch(andSortedObj, asn, relaxBoundsCheck);
- } else if (STAGE_PROJECTION == trueSoln->getType()) {
+ } else if (STAGE_PROJECTION_DEFAULT == trueSoln->getType() ||
+ STAGE_PROJECTION_COVERED == trueSoln->getType() ||
+ STAGE_PROJECTION_SIMPLE == trueSoln->getType()) {
const ProjectionNode* pn = static_cast<const ProjectionNode*>(trueSoln);
BSONElement el = testSoln["proj"];
@@ -589,11 +591,21 @@ bool QueryPlannerTestLib::solutionMatches(const BSONObj& testSoln,
BSONElement projType = projObj["type"];
if (!projType.eoo()) {
string projTypeStr = projType.str();
- if (!((pn->projType == ProjectionNode::DEFAULT && projTypeStr == "default") ||
- (pn->projType == ProjectionNode::SIMPLE_DOC && projTypeStr == "simple") ||
- (pn->projType == ProjectionNode::COVERED_ONE_INDEX &&
- projTypeStr == "coveredIndex"))) {
- return false;
+ switch (pn->getType()) {
+ case StageType::STAGE_PROJECTION_DEFAULT:
+ if (projTypeStr != "default")
+ return false;
+ break;
+ case StageType::STAGE_PROJECTION_COVERED:
+ if (projTypeStr != "coveredIndex")
+ return false;
+ break;
+ case StageType::STAGE_PROJECTION_SIMPLE:
+ if (projTypeStr != "simple")
+ return false;
+ break;
+ default:
+ MONGO_UNREACHABLE;
}
}
diff --git a/src/mongo/db/query/query_solution.cpp b/src/mongo/db/query/query_solution.cpp
index 2dc730a4f5a..61ca12f60e5 100644
--- a/src/mongo/db/query/query_solution.cpp
+++ b/src/mongo/db/query/query_solution.cpp
@@ -858,14 +858,7 @@ void ProjectionNode::appendToString(mongoutils::str::stream* ss, int indent) con
addIndent(ss, indent + 1);
*ss << "proj = " << projection.toString() << '\n';
addIndent(ss, indent + 1);
- if (DEFAULT == projType) {
- *ss << "type = DEFAULT\n";
- } else if (COVERED_ONE_INDEX == projType) {
- *ss << "type = COVERED_ONE_INDEX\n";
- } else {
- invariant(SIMPLE_DOC == projType);
- *ss << "type = SIMPLE_DOC\n";
- }
+ *ss << "type = " << projectionImplementationTypeToString() << '\n';
addCommon(ss, indent);
addIndent(ss, indent + 1);
*ss << "Child:" << '\n';
@@ -896,21 +889,46 @@ void ProjectionNode::computeProperties() {
}
}
-QuerySolutionNode* ProjectionNode::clone() const {
- ProjectionNode* copy = new ProjectionNode(parsed);
- cloneBaseData(copy);
+void ProjectionNode::cloneProjectionData(ProjectionNode* copy) const {
+ // ProjectionNode should not populate filter. This should be a no-op.
+ if (this->filter)
+ copy->filter = this->filter->shallowClone();
copy->_sorts = this->_sorts;
+}
- // This MatchExpression* is owned by the canonical query, not by the
- // ProjectionNode. Just copying the pointer is fine.
- copy->fullExpression = this->fullExpression;
+ProjectionNode* ProjectionNodeDefault::clone() const {
+ auto copy = std::make_unique<ProjectionNodeDefault>(
+ std::unique_ptr<QuerySolutionNode>(children[0]->clone()),
+ fullExpression,
+ projection,
+ parsed);
+ ProjectionNode::cloneProjectionData(copy.get());
+ return copy.release();
+}
- copy->projection = this->projection;
+ProjectionNode* ProjectionNodeCovered::clone() const {
+ auto copy = std::make_unique<ProjectionNodeCovered>(
+ std::unique_ptr<QuerySolutionNode>(children[0]->clone()),
+ fullExpression,
+ projection,
+ parsed,
+ coveredKeyObj);
+ ProjectionNode::cloneProjectionData(copy.get());
+ return copy.release();
+}
- return copy;
+ProjectionNode* ProjectionNodeSimple::clone() const {
+ auto copy = std::make_unique<ProjectionNodeSimple>(
+ std::unique_ptr<QuerySolutionNode>(children[0]->clone()),
+ fullExpression,
+ projection,
+ parsed);
+ ProjectionNode::cloneProjectionData(copy.get());
+ return copy.release();
}
+
//
// SortKeyGeneratorNode
//
diff --git a/src/mongo/db/query/query_solution.h b/src/mongo/db/query/query_solution.h
index 491d6d99e5b..4e4d724b555 100644
--- a/src/mongo/db/query/query_solution.h
+++ b/src/mongo/db/query/query_solution.h
@@ -32,6 +32,7 @@
#include <memory>
+#include "mongo/base/string_data.h"
#include "mongo/bson/bsonobj_comparator_interface.h"
#include "mongo/db/fts/fts_query.h"
#include "mongo/db/jsobj.h"
@@ -50,6 +51,12 @@ class GeoNearExpression;
*/
struct QuerySolutionNode {
QuerySolutionNode() {}
+
+ /**
+ * Constructs a QuerySolutionNode with a single child.
+ */
+ QuerySolutionNode(std::unique_ptr<QuerySolutionNode> child) : children{child.release()} {}
+
virtual ~QuerySolutionNode() {
for (size_t i = 0; i < children.size(); ++i) {
delete children[i];
@@ -520,40 +527,26 @@ struct IndexScanNode : public QuerySolutionNode {
std::set<StringData> multikeyFields;
};
-struct ProjectionNode : public QuerySolutionNode {
- /**
- * We have a few implementations of the projection functionality. The most general
- * implementation 'DEFAULT' is much slower than the fast-path implementations
- * below. We only really have all the information available to choose a projection
- * implementation at planning time.
- */
- enum ProjectionType {
- // This is the most general implementation of the projection functionality. It handles
- // every case.
- DEFAULT,
-
- // This is a fast-path for when the projection is fully covered by one index.
- COVERED_ONE_INDEX,
-
- // This is a fast-path for when the projection only has inclusions on non-dotted fields.
- SIMPLE_DOC,
- };
-
- ProjectionNode(ParsedProjection proj)
- : _sorts(SimpleBSONObjComparator::kInstance.makeBSONObjSet()),
- fullExpression(NULL),
- projType(DEFAULT),
- parsed(proj) {}
-
- virtual ~ProjectionNode() {}
-
- virtual StageType getType() const {
- return STAGE_PROJECTION;
- }
-
- virtual void computeProperties();
+/**
+ * We have a few implementations of the projection functionality. They are chosen by constructing
+ * a type derived from this abstract struct. The most general implementation 'ProjectionNodeDefault'
+ * is much slower than the fast-path implementations. We only really have all the information
+ * available to choose a projection implementation at planning time.
+ */
+struct ProjectionNode : QuerySolutionNode {
+ ProjectionNode(std::unique_ptr<QuerySolutionNode> child,
+ const MatchExpression& fullExpression,
+ BSONObj projection,
+ ParsedProjection parsed)
+ : QuerySolutionNode(std::move(child)),
+ _sorts(SimpleBSONObjComparator::kInstance.makeBSONObjSet()),
+ fullExpression(fullExpression),
+ projection(std::move(projection)),
+ parsed(parsed) {}
+
+ void computeProperties() final;
- virtual void appendToString(mongoutils::str::stream* ss, int indent) const;
+ void appendToString(mongoutils::str::stream* ss, int indent) const final;
/**
* Data from the projection node is considered fetch iff the child provides fetched data.
@@ -584,29 +577,89 @@ struct ProjectionNode : public QuerySolutionNode {
return _sorts;
}
- QuerySolutionNode* clone() const;
+protected:
+ void cloneProjectionData(ProjectionNode* copy) const;
+
+public:
+ /**
+ * Identify projectionImplementation type as a string.
+ */
+ virtual StringData projectionImplementationTypeToString() const = 0;
BSONObjSet _sorts;
// The full query tree. Needed when we have positional operators.
// Owned in the CanonicalQuery, not here.
- MatchExpression* fullExpression;
+ const MatchExpression& fullExpression;
// Given that we don't yet have a MatchExpression analogue for the expression language, we
// use a BSONObj.
BSONObj projection;
- // What implementation of the projection algorithm should we use?
- ProjectionType projType;
-
ParsedProjection parsed;
+};
+
+/**
+ * This is the most general implementation of the projection functionality. It handles every case.
+ */
+struct ProjectionNodeDefault final : ProjectionNode {
+ using ProjectionNode::ProjectionNode;
+
+ StageType getType() const final {
+ return STAGE_PROJECTION_DEFAULT;
+ }
+
+ ProjectionNode* clone() const final;
+
+ StringData projectionImplementationTypeToString() const final {
+ return "DEFAULT"_sd;
+ }
+};
+
+/**
+ * This is a fast-path for when the projection is fully covered by one index.
+ */
+struct ProjectionNodeCovered final : ProjectionNode {
+ ProjectionNodeCovered(std::unique_ptr<QuerySolutionNode> child,
+ const MatchExpression& fullExpression,
+ BSONObj projection,
+ ParsedProjection parsed,
+ BSONObj coveredKeyObj)
+ : ProjectionNode(std::move(child), fullExpression, projection, parsed),
+ coveredKeyObj(std::move(coveredKeyObj)) {}
+
+ StageType getType() const final {
+ return STAGE_PROJECTION_COVERED;
+ }
+
+ ProjectionNode* clone() const final;
+
+ StringData projectionImplementationTypeToString() const final {
+ return "COVERED_ONE_INDEX"_sd;
+ }
- // Only meaningful if projType == COVERED_ONE_INDEX. This is the key pattern of the index
- // supplying our covered data. We can pre-compute which fields to include and cache that
- // data for later if we know we only have one index.
+ // This is the key pattern of the index supplying our covered data. We can pre-compute which
+ // fields to include and cache that data for later if we know we only have one index.
BSONObj coveredKeyObj;
};
+/**
+ * This is a fast-path for when the projection only has inclusions on non-dotted fields.
+ */
+struct ProjectionNodeSimple final : ProjectionNode {
+ using ProjectionNode::ProjectionNode;
+
+ StageType getType() const final {
+ return STAGE_PROJECTION_SIMPLE;
+ }
+
+ ProjectionNode* clone() const final;
+
+ StringData projectionImplementationTypeToString() const final {
+ return "SIMPLE_DOC"_sd;
+ }
+};
+
struct SortKeyGeneratorNode : public QuerySolutionNode {
StageType getType() const final {
return STAGE_SORT_KEY_GENERATOR;
diff --git a/src/mongo/db/query/query_solution_test.cpp b/src/mongo/db/query/query_solution_test.cpp
index c8555503e7c..c084b1c089c 100644
--- a/src/mongo/db/query/query_solution_test.cpp
+++ b/src/mongo/db/query/query_solution_test.cpp
@@ -28,6 +28,8 @@
* it in the license file.
*/
+#include <utility>
+
#include "mongo/platform/basic.h"
#include "mongo/bson/bsontypes.h"
@@ -712,8 +714,7 @@ TEST(QuerySolutionTest, IndexScanNodeHasFieldExcludesSimpleBoundsStringFieldWhen
ASSERT_FALSE(node.hasField("b"));
}
-std::unique_ptr<ParsedProjection> createParsedProjection(const BSONObj& query,
- const BSONObj& projObj) {
+auto createMatchExprAndParsedProjection(const BSONObj& query, const BSONObj& projObj) {
QueryTestServiceContext serviceCtx;
auto opCtx = serviceCtx.makeOperationContext();
const CollatorInterface* collator = nullptr;
@@ -732,7 +733,8 @@ std::unique_ptr<ParsedProjection> createParsedProjection(const BSONObj& query,
<< status.toString());
}
ASSERT(out);
- return std::unique_ptr<ParsedProjection>(out);
+ return std::make_pair(std::move(queryMatchExpr.getValue()),
+ std::unique_ptr<ParsedProjection>(out));
}
TEST(QuerySolutionTest, InclusionProjectionPreservesSort) {
@@ -742,10 +744,12 @@ TEST(QuerySolutionTest, InclusionProjectionPreservesSort) {
BSONObj projection = BSON("a" << 1);
BSONObj match;
- auto parsedProjection = createParsedProjection(match, projection);
+ auto matchExprAndParsedProjection = createMatchExprAndParsedProjection(match, projection);
- ProjectionNode proj{*parsedProjection};
- proj.children.push_back(node.release());
+ ProjectionNodeDefault proj{std::move(node),
+ *matchExprAndParsedProjection.first,
+ projection,
+ *matchExprAndParsedProjection.second};
proj.computeProperties();
ASSERT_EQ(proj.getSort().size(), 1U);
@@ -759,10 +763,12 @@ TEST(QuerySolutionTest, ExclusionProjectionDoesNotPreserveSort) {
BSONObj projection = BSON("a" << 0);
BSONObj match;
- auto parsedProjection = createParsedProjection(match, projection);
+ auto matchExprAndParsedProjection = createMatchExprAndParsedProjection(match, projection);
- ProjectionNode proj{*parsedProjection};
- proj.children.push_back(node.release());
+ ProjectionNodeDefault proj{std::move(node),
+ *matchExprAndParsedProjection.first,
+ projection,
+ *matchExprAndParsedProjection.second};
proj.computeProperties();
ASSERT_EQ(proj.getSort().size(), 0U);
@@ -774,10 +780,12 @@ TEST(QuerySolutionTest, InclusionProjectionTruncatesSort) {
BSONObj projection = BSON("a" << 1);
BSONObj match;
- auto parsedProjection = createParsedProjection(match, projection);
+ auto matchExprAndParsedProjection = createMatchExprAndParsedProjection(match, projection);
- ProjectionNode proj{*parsedProjection};
- proj.children.push_back(node.release());
+ ProjectionNodeDefault proj{std::move(node),
+ *matchExprAndParsedProjection.first,
+ projection,
+ *matchExprAndParsedProjection.second};
proj.computeProperties();
ASSERT_EQ(proj.getSort().size(), 1U);
@@ -790,10 +798,12 @@ TEST(QuerySolutionTest, ExclusionProjectionTruncatesSort) {
BSONObj projection = BSON("b" << 0);
BSONObj match;
- auto parsedProjection = createParsedProjection(match, projection);
+ auto matchExprAndParsedProjection = createMatchExprAndParsedProjection(match, projection);
- ProjectionNode proj{*parsedProjection};
- proj.children.push_back(node.release());
+ ProjectionNodeDefault proj{std::move(node),
+ *matchExprAndParsedProjection.first,
+ projection,
+ *matchExprAndParsedProjection.second};
proj.computeProperties();
ASSERT_EQ(proj.getSort().size(), 1U);
diff --git a/src/mongo/db/query/stage_builder.cpp b/src/mongo/db/query/stage_builder.cpp
index eb3a5354fcb..96c98d71251 100644
--- a/src/mongo/db/query/stage_builder.cpp
+++ b/src/mongo/db/query/stage_builder.cpp
@@ -138,30 +138,38 @@ PlanStage* buildStages(OperationContext* opCtx,
return new SortKeyGeneratorStage(
opCtx, childStage, ws, keyGenNode->sortSpec, cq.getCollator());
}
- case STAGE_PROJECTION: {
- const ProjectionNode* pn = static_cast<const ProjectionNode*>(root);
+ case STAGE_PROJECTION_DEFAULT: {
+ auto pn = static_cast<const ProjectionNodeDefault*>(root);
unique_ptr<PlanStage> childStage{
buildStages(opCtx, collection, cq, qsol, pn->children[0], ws)};
if (nullptr == childStage) {
return nullptr;
}
-
- switch (pn->projType) {
- case ProjectionNode::DEFAULT:
- return new ProjectionStageDefault(opCtx,
- pn->projection,
- ws,
- std::move(childStage),
- pn->fullExpression,
- cq.getCollator());
- case ProjectionNode::COVERED_ONE_INDEX:
- invariant(!pn->coveredKeyObj.isEmpty());
- return new ProjectionStageCovered(
- opCtx, pn->projection, ws, std::move(childStage), pn->coveredKeyObj);
- case ProjectionNode::SIMPLE_DOC:
- return new ProjectionStageSimple(
- opCtx, pn->projection, ws, std::move(childStage));
+ return new ProjectionStageDefault(opCtx,
+ pn->projection,
+ ws,
+ std::move(childStage),
+ pn->fullExpression,
+ cq.getCollator());
+ }
+ case STAGE_PROJECTION_COVERED: {
+ auto pn = static_cast<const ProjectionNodeCovered*>(root);
+ unique_ptr<PlanStage> childStage{
+ buildStages(opCtx, collection, cq, qsol, pn->children[0], ws)};
+ if (nullptr == childStage) {
+ return nullptr;
+ }
+ return new ProjectionStageCovered(
+ opCtx, pn->projection, ws, std::move(childStage), pn->coveredKeyObj);
+ }
+ case STAGE_PROJECTION_SIMPLE: {
+ auto pn = static_cast<const ProjectionNodeSimple*>(root);
+ unique_ptr<PlanStage> childStage{
+ buildStages(opCtx, collection, cq, qsol, pn->children[0], ws)};
+ if (nullptr == childStage) {
+ return nullptr;
}
+ return new ProjectionStageSimple(opCtx, pn->projection, ws, std::move(childStage));
}
case STAGE_LIMIT: {
const LimitNode* ln = static_cast<const LimitNode*>(root);
diff --git a/src/mongo/db/query/stage_types.h b/src/mongo/db/query/stage_types.h
index 6fbdba5ea36..7759aa3bdba 100644
--- a/src/mongo/db/query/stage_types.h
+++ b/src/mongo/db/query/stage_types.h
@@ -76,7 +76,11 @@ enum StageType {
STAGE_MULTI_PLAN,
STAGE_OR,
- STAGE_PROJECTION,
+
+ // Projection has three alternate implementations.
+ STAGE_PROJECTION_DEFAULT,
+ STAGE_PROJECTION_COVERED,
+ STAGE_PROJECTION_SIMPLE,
// Stages for running aggregation pipelines.
STAGE_CHANGE_STREAM_PROXY,