summaryrefslogtreecommitdiff
path: root/src/mongo/dbtests/plan_ranking.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/dbtests/plan_ranking.cpp')
-rw-r--r--src/mongo/dbtests/plan_ranking.cpp1045
1 files changed, 513 insertions, 532 deletions
diff --git a/src/mongo/dbtests/plan_ranking.cpp b/src/mongo/dbtests/plan_ranking.cpp
index 0e73a5c992d..77e34fc95f3 100644
--- a/src/mongo/dbtests/plan_ranking.cpp
+++ b/src/mongo/dbtests/plan_ranking.cpp
@@ -51,585 +51,566 @@
namespace mongo {
- // How we access the external setParameter testing bool.
- extern bool internalQueryForceIntersectionPlans;
+// How we access the external setParameter testing bool.
+extern bool internalQueryForceIntersectionPlans;
- extern bool internalQueryPlannerEnableHashIntersection;
+extern bool internalQueryPlannerEnableHashIntersection;
} // namespace mongo
namespace PlanRankingTests {
- using std::unique_ptr;
- using std::vector;
+using std::unique_ptr;
+using std::vector;
- static const char* ns = "unittests.PlanRankingTests";
+static const char* ns = "unittests.PlanRankingTests";
- class PlanRankingTestBase {
- public:
- PlanRankingTestBase()
- : _internalQueryForceIntersectionPlans(internalQueryForceIntersectionPlans),
- _enableHashIntersection(internalQueryPlannerEnableHashIntersection),
- _client(&_txn) {
+class PlanRankingTestBase {
+public:
+ PlanRankingTestBase()
+ : _internalQueryForceIntersectionPlans(internalQueryForceIntersectionPlans),
+ _enableHashIntersection(internalQueryPlannerEnableHashIntersection),
+ _client(&_txn) {
+ // Run all tests with hash-based intersection enabled.
+ internalQueryPlannerEnableHashIntersection = true;
- // Run all tests with hash-based intersection enabled.
- internalQueryPlannerEnableHashIntersection = true;
+ OldClientWriteContext ctx(&_txn, ns);
+ _client.dropCollection(ns);
+ }
- OldClientWriteContext ctx(&_txn, ns);
- _client.dropCollection(ns);
- }
+ virtual ~PlanRankingTestBase() {
+ // Restore external setParameter testing bools.
+ internalQueryForceIntersectionPlans = _internalQueryForceIntersectionPlans;
+ internalQueryPlannerEnableHashIntersection = _enableHashIntersection;
+ }
- virtual ~PlanRankingTestBase() {
- // Restore external setParameter testing bools.
- internalQueryForceIntersectionPlans = _internalQueryForceIntersectionPlans;
- internalQueryPlannerEnableHashIntersection = _enableHashIntersection;
- }
+ void insert(const BSONObj& obj) {
+ OldClientWriteContext ctx(&_txn, ns);
+ _client.insert(ns, obj);
+ }
- void insert(const BSONObj& obj) {
- OldClientWriteContext ctx(&_txn, ns);
- _client.insert(ns, obj);
- }
+ void addIndex(const BSONObj& obj) {
+ ASSERT_OK(dbtests::createIndex(&_txn, ns, obj));
+ }
- void addIndex(const BSONObj& obj) {
- ASSERT_OK(dbtests::createIndex(&_txn, ns, obj));
+ /**
+ * Use the MultiPlanRunner to pick the best plan for the query 'cq'. Goes through
+ * normal planning to generate solutions and feeds them to the MPR.
+ *
+ * Takes ownership of 'cq'. Caller DOES NOT own the returned QuerySolution*.
+ */
+ QuerySolution* pickBestPlan(CanonicalQuery* cq) {
+ AutoGetCollectionForRead ctx(&_txn, ns);
+ Collection* collection = ctx.getCollection();
+
+ QueryPlannerParams plannerParams;
+ fillOutPlannerParams(&_txn, collection, cq, &plannerParams);
+ // Turn this off otherwise it pops up in some plans.
+ plannerParams.options &= ~QueryPlannerParams::KEEP_MUTATIONS;
+
+ // Plan.
+ vector<QuerySolution*> solutions;
+ Status status = QueryPlanner::plan(*cq, plannerParams, &solutions);
+ ASSERT(status.isOK());
+
+ ASSERT_GREATER_THAN_OR_EQUALS(solutions.size(), 1U);
+
+ // Fill out the MPR.
+ _mps.reset(new MultiPlanStage(&_txn, collection, cq));
+ std::unique_ptr<WorkingSet> ws(new WorkingSet());
+ // Put each solution from the planner into the MPR.
+ for (size_t i = 0; i < solutions.size(); ++i) {
+ PlanStage* root;
+ ASSERT(StageBuilder::build(&_txn, collection, *solutions[i], ws.get(), &root));
+ // Takes ownership of all (actually some) arguments.
+ _mps->addPlan(solutions[i], root, ws.get());
}
+ // This is what sets a backup plan, should we test for it.
+ PlanYieldPolicy yieldPolicy(NULL, PlanExecutor::YIELD_MANUAL);
+ _mps->pickBestPlan(&yieldPolicy);
+ ASSERT(_mps->bestPlanChosen());
- /**
- * Use the MultiPlanRunner to pick the best plan for the query 'cq'. Goes through
- * normal planning to generate solutions and feeds them to the MPR.
- *
- * Takes ownership of 'cq'. Caller DOES NOT own the returned QuerySolution*.
- */
- QuerySolution* pickBestPlan(CanonicalQuery* cq) {
- AutoGetCollectionForRead ctx(&_txn, ns);
- Collection* collection = ctx.getCollection();
-
- QueryPlannerParams plannerParams;
- fillOutPlannerParams(&_txn, collection, cq, &plannerParams);
- // Turn this off otherwise it pops up in some plans.
- plannerParams.options &= ~QueryPlannerParams::KEEP_MUTATIONS;
-
- // Plan.
- vector<QuerySolution*> solutions;
- Status status = QueryPlanner::plan(*cq, plannerParams, &solutions);
- ASSERT(status.isOK());
-
- ASSERT_GREATER_THAN_OR_EQUALS(solutions.size(), 1U);
-
- // Fill out the MPR.
- _mps.reset(new MultiPlanStage(&_txn, collection, cq));
- std::unique_ptr<WorkingSet> ws(new WorkingSet());
- // Put each solution from the planner into the MPR.
- for (size_t i = 0; i < solutions.size(); ++i) {
- PlanStage* root;
- ASSERT(StageBuilder::build(&_txn, collection, *solutions[i], ws.get(), &root));
- // Takes ownership of all (actually some) arguments.
- _mps->addPlan(solutions[i], root, ws.get());
- }
- // This is what sets a backup plan, should we test for it.
- PlanYieldPolicy yieldPolicy(NULL, PlanExecutor::YIELD_MANUAL);
- _mps->pickBestPlan(&yieldPolicy);
- ASSERT(_mps->bestPlanChosen());
-
- size_t bestPlanIdx = _mps->bestPlanIdx();
- ASSERT_LESS_THAN(bestPlanIdx, solutions.size());
-
- // And return a pointer to the best solution.
- return _mps->bestSolution();
- }
+ size_t bestPlanIdx = _mps->bestPlanIdx();
+ ASSERT_LESS_THAN(bestPlanIdx, solutions.size());
- /**
- * Was a backup plan picked during the ranking process?
- */
- bool hasBackupPlan() const {
- ASSERT(NULL != _mps.get());
- return _mps->hasBackupPlan();
- }
+ // And return a pointer to the best solution.
+ return _mps->bestSolution();
+ }
- protected:
- // A large number, which must be larger than the number of times
- // candidate plans are worked by the multi plan runner. Used for
- // determining the number of documents in the tests below.
- static const int N;
+ /**
+ * Was a backup plan picked during the ranking process?
+ */
+ bool hasBackupPlan() const {
+ ASSERT(NULL != _mps.get());
+ return _mps->hasBackupPlan();
+ }
- OperationContextImpl _txn;
+protected:
+ // A large number, which must be larger than the number of times
+ // candidate plans are worked by the multi plan runner. Used for
+ // determining the number of documents in the tests below.
+ static const int N;
- private:
- // Holds the value of global "internalQueryForceIntersectionPlans" setParameter flag.
- // Restored at end of test invocation regardless of test result.
- bool _internalQueryForceIntersectionPlans;
+ OperationContextImpl _txn;
- // Holds the value of the global set parameter so it can be restored at the end
- // of the test.
- bool _enableHashIntersection;
+private:
+ // Holds the value of global "internalQueryForceIntersectionPlans" setParameter flag.
+ // Restored at end of test invocation regardless of test result.
+ bool _internalQueryForceIntersectionPlans;
- unique_ptr<MultiPlanStage> _mps;
+ // Holds the value of the global set parameter so it can be restored at the end
+ // of the test.
+ bool _enableHashIntersection;
- DBDirectClient _client;
- };
+ unique_ptr<MultiPlanStage> _mps;
- // static
- const int PlanRankingTestBase::N = internalQueryPlanEvaluationWorks + 1000;
+ DBDirectClient _client;
+};
- /**
- * Test that the "prefer ixisect" parameter works.
- */
- class PlanRankingIntersectOverride : public PlanRankingTestBase {
- public:
- void run() {
- // 'a' is very selective, 'b' is not.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << i << "b" << 1));
- }
-
- // Add indices on 'a' and 'b'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // Run the query {a:4, b:1}.
- CanonicalQuery* cq;
- verify(CanonicalQuery::canonicalize(ns, BSON("a" << 100 << "b" << 1), &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // {a:100} is super selective so choose that.
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}",
- soln->root.get()));
-
- // Turn on the "force intersect" option.
- // This will be reverted by PlanRankingTestBase's destructor when the test completes.
- internalQueryForceIntersectionPlans = true;
-
- // And run the same query again.
- ASSERT(CanonicalQuery::canonicalize(ns, BSON("a" << 100 << "b" << 1), &cq).isOK());
- std::unique_ptr<CanonicalQuery> killCq2(cq);
-
- // With the "ranking picks ixisect always" option we pick an intersection plan that uses
- // both the {a:1} and {b:1} indices even though it performs poorly.
-
- // Takes ownership of cq.
- soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}",
- soln->root.get()));
- }
- };
+// static
+const int PlanRankingTestBase::N = internalQueryPlanEvaluationWorks + 1000;
- /**
- * Test that a hashed AND solution plan is picked along with a non-blocking backup solution.
- */
- class PlanRankingIntersectWithBackup : public PlanRankingTestBase {
- public:
- void run() {
- // 'a' is very selective, 'b' is not.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << i << "b" << 1));
- }
-
- // Add indices on 'a' and 'b'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // Run the query {a:1, b:{$gt:1}.
- CanonicalQuery* cq;
- verify(CanonicalQuery::canonicalize(ns, BSON("a" << 1 << "b" << BSON("$gt" << 1)),
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Turn on the "force intersect" option.
- // This will be reverted by PlanRankingTestBase's destructor when the test completes.
- internalQueryForceIntersectionPlans = true;
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {andHash: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}",
- soln->root.get()));
-
- // Confirm that a backup plan is available.
- ASSERT(hasBackupPlan());
+/**
+ * Test that the "prefer ixisect" parameter works.
+ */
+class PlanRankingIntersectOverride : public PlanRankingTestBase {
+public:
+ void run() {
+ // 'a' is very selective, 'b' is not.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << i << "b" << 1));
}
- };
- /**
- * Two plans hit EOF at the same time, but one is covered. Make sure that we prefer the covered
- * plan.
- */
- class PlanRankingPreferCovered : public PlanRankingTestBase {
- public:
- void run() {
- // Insert data {a:i, b:i}. Index {a:1} and {a:1, b:1}, query on 'a', projection on 'a'
- // and 'b'. Should prefer the second index as we can pull the 'b' data out.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << i << "b" << i));
- }
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << 1 << "b" << 1));
-
- // Query for a==27 with projection that wants 'a' and 'b'. BSONObj() is for sort.
- CanonicalQuery* cq;
- ASSERT(CanonicalQuery::canonicalize(ns,
- BSON("a" << 27),
- BSONObj(),
- BSON("_id" << 0 << "a" << 1 << "b" << 1),
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
-
- // Prefer the fully covered plan.
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{proj: {spec: {_id:0, a:1, b:1}, node: {ixscan: {pattern: {a: 1, b:1}}}}}",
- soln->root.get()));
+ // Add indices on 'a' and 'b'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // Run the query {a:4, b:1}.
+ CanonicalQuery* cq;
+ verify(CanonicalQuery::canonicalize(ns, BSON("a" << 100 << "b" << 1), &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // {a:100} is super selective so choose that.
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}", soln->root.get()));
+
+ // Turn on the "force intersect" option.
+ // This will be reverted by PlanRankingTestBase's destructor when the test completes.
+ internalQueryForceIntersectionPlans = true;
+
+ // And run the same query again.
+ ASSERT(CanonicalQuery::canonicalize(ns, BSON("a" << 100 << "b" << 1), &cq).isOK());
+ std::unique_ptr<CanonicalQuery> killCq2(cq);
+
+ // With the "ranking picks ixisect always" option we pick an intersection plan that uses
+ // both the {a:1} and {b:1} indices even though it performs poorly.
+
+ // Takes ownership of cq.
+ soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * Test that a hashed AND solution plan is picked along with a non-blocking backup solution.
+ */
+class PlanRankingIntersectWithBackup : public PlanRankingTestBase {
+public:
+ void run() {
+ // 'a' is very selective, 'b' is not.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << i << "b" << 1));
}
- };
- /**
- * No plan produces any results or hits EOF. In this case we should never choose an index
- * intersection solution.
- */
- class PlanRankingAvoidIntersectIfNoResults : public PlanRankingTestBase {
- public:
- void run() {
- // We insert lots of copies of {a:1, b:1, c: 20}. We have the indices {a:1} and {b:1},
- // and the query is {a:1, b:1, c: 999}. No data that matches the query but we won't
- // know that during plan ranking. We don't want to choose an intersection plan here.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << 1 << "b" << 1 << "c" << 20));
- }
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // There is no data that matches this query but we don't know that until EOF.
- CanonicalQuery* cq;
- BSONObj queryObj = BSON("a" << 1 << "b" << 1 << "c" << 99);
- ASSERT(CanonicalQuery::canonicalize(ns, queryObj, &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
-
- // Anti-prefer the intersection plan.
- bool bestIsScanOverA = QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}",
- soln->root.get());
- bool bestIsScanOverB = QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {ixscan: {pattern: {b: 1}}}}}",
- soln->root.get());
- ASSERT(bestIsScanOverA || bestIsScanOverB);
+ // Add indices on 'a' and 'b'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // Run the query {a:1, b:{$gt:1}.
+ CanonicalQuery* cq;
+ verify(CanonicalQuery::canonicalize(ns, BSON("a" << 1 << "b" << BSON("$gt" << 1)), &cq)
+ .isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Turn on the "force intersect" option.
+ // This will be reverted by PlanRankingTestBase's destructor when the test completes.
+ internalQueryForceIntersectionPlans = true;
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {node: {andHash: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}",
+ soln->root.get()));
+
+ // Confirm that a backup plan is available.
+ ASSERT(hasBackupPlan());
+ }
+};
+
+/**
+ * Two plans hit EOF at the same time, but one is covered. Make sure that we prefer the covered
+ * plan.
+ */
+class PlanRankingPreferCovered : public PlanRankingTestBase {
+public:
+ void run() {
+ // Insert data {a:i, b:i}. Index {a:1} and {a:1, b:1}, query on 'a', projection on 'a'
+ // and 'b'. Should prefer the second index as we can pull the 'b' data out.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << i << "b" << i));
}
- };
- /**
- * No plan produces any results or hits EOF. In this case we should prefer covered solutions to
- * non-covered solutions.
- */
- class PlanRankingPreferCoveredEvenIfNoResults : public PlanRankingTestBase {
- public:
- void run() {
- // We insert lots of copies of {a:1, b:1}. We have the indices {a:1} and {a:1, b:1},
- // the query is for a doc that doesn't exist, but there is a projection over 'a' and
- // 'b'. We should prefer the index that provides a covered query.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << 1 << "b" << 1));
- }
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << 1 << "b" << 1));
-
- // There is no data that matches this query ({a:2}). Both scans will hit EOF before
- // returning any data.
-
- CanonicalQuery* cq;
- ASSERT(CanonicalQuery::canonicalize(ns,
- BSON("a" << 2),
- BSONObj(),
- BSON("_id" << 0 << "a" << 1 << "b" << 1),
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
- // Prefer the fully covered plan.
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{proj: {spec: {_id:0, a:1, b:1}, node: {ixscan: {pattern: {a: 1, b:1}}}}}",
- soln->root.get()));
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ // Query for a==27 with projection that wants 'a' and 'b'. BSONObj() is for sort.
+ CanonicalQuery* cq;
+ ASSERT(CanonicalQuery::canonicalize(
+ ns, BSON("a" << 27), BSONObj(), BSON("_id" << 0 << "a" << 1 << "b" << 1), &cq)
+ .isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+
+ // Prefer the fully covered plan.
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{proj: {spec: {_id:0, a:1, b:1}, node: {ixscan: {pattern: {a: 1, b:1}}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * No plan produces any results or hits EOF. In this case we should never choose an index
+ * intersection solution.
+ */
+class PlanRankingAvoidIntersectIfNoResults : public PlanRankingTestBase {
+public:
+ void run() {
+ // We insert lots of copies of {a:1, b:1, c: 20}. We have the indices {a:1} and {b:1},
+ // and the query is {a:1, b:1, c: 999}. No data that matches the query but we won't
+ // know that during plan ranking. We don't want to choose an intersection plan here.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << 1 << "b" << 1 << "c" << 20));
}
- };
- /**
- * We have an index on "a" which is somewhat selective and an index on "b" which is highly
- * selective (will cause an immediate EOF). Make sure that a query with predicates on both "a"
- * and "b" will use the index on "b".
- */
- class PlanRankingPreferImmediateEOF : public PlanRankingTestBase {
- public:
- void run() {
- // 'a' is very selective, 'b' is not.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << i << "b" << 1));
- }
-
- // Add indices on 'a' and 'b'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // Run the query {a:N+1, b:1}. (No such document.)
- CanonicalQuery* cq;
- verify(CanonicalQuery::canonicalize(ns, BSON("a" << N + 1 << "b" << 1), &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // {a: 100} is super selective so choose that.
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}",
- soln->root.get()));
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // There is no data that matches this query but we don't know that until EOF.
+ CanonicalQuery* cq;
+ BSONObj queryObj = BSON("a" << 1 << "b" << 1 << "c" << 99);
+ ASSERT(CanonicalQuery::canonicalize(ns, queryObj, &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+
+ // Anti-prefer the intersection plan.
+ bool bestIsScanOverA = QueryPlannerTestLib::solutionMatches(
+ "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}", soln->root.get());
+ bool bestIsScanOverB = QueryPlannerTestLib::solutionMatches(
+ "{fetch: {node: {ixscan: {pattern: {b: 1}}}}}", soln->root.get());
+ ASSERT(bestIsScanOverA || bestIsScanOverB);
+ }
+};
+
+/**
+ * No plan produces any results or hits EOF. In this case we should prefer covered solutions to
+ * non-covered solutions.
+ */
+class PlanRankingPreferCoveredEvenIfNoResults : public PlanRankingTestBase {
+public:
+ void run() {
+ // We insert lots of copies of {a:1, b:1}. We have the indices {a:1} and {a:1, b:1},
+ // the query is for a doc that doesn't exist, but there is a projection over 'a' and
+ // 'b'. We should prefer the index that provides a covered query.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << 1 << "b" << 1));
}
- };
- /**
- * Same as PlanRankingPreferImmediateEOF, but substitute a range predicate on "a" for the
- * equality predicate on "a". The presence of the range predicate has an impact on the
- * intersection plan that is raced against the single-index plans: since "a" no longer generates
- * point interval bounds, the results of the index scan aren't guaranteed to be returned in
- * RecordId order, and so the intersection plan uses the AND_HASHED stage instead of the
- * AND_SORTED stage. It is still the case that the query should pick the plan that uses index
- * "b", instead of the plan that uses index "a" or the (hashed) intersection plan.
- */
- class PlanRankingPreferImmediateEOFAgainstHashed : public PlanRankingTestBase {
- public:
- void run() {
- // 'a' is very selective, 'b' is not.
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << i << "b" << 1));
- }
-
- // Add indices on 'a' and 'b'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // Run the query {a:N+1, b:1}. (No such document.)
- CanonicalQuery* cq;
- verify(CanonicalQuery::canonicalize(ns, BSON("a" << BSON("$gte" << N + 1)
- << "b" << 1), &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // {a: 100} is super selective so choose that.
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}",
- soln->root.get()));
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ // There is no data that matches this query ({a:2}). Both scans will hit EOF before
+ // returning any data.
+
+ CanonicalQuery* cq;
+ ASSERT(CanonicalQuery::canonicalize(
+ ns, BSON("a" << 2), BSONObj(), BSON("_id" << 0 << "a" << 1 << "b" << 1), &cq)
+ .isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+ // Prefer the fully covered plan.
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{proj: {spec: {_id:0, a:1, b:1}, node: {ixscan: {pattern: {a: 1, b:1}}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * We have an index on "a" which is somewhat selective and an index on "b" which is highly
+ * selective (will cause an immediate EOF). Make sure that a query with predicates on both "a"
+ * and "b" will use the index on "b".
+ */
+class PlanRankingPreferImmediateEOF : public PlanRankingTestBase {
+public:
+ void run() {
+ // 'a' is very selective, 'b' is not.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << i << "b" << 1));
}
- };
- /**
- * We have an index on _id and a query over _id with a sort. Ensure that we don't pick a
- * collscan as the best plan even though the _id-scanning solution doesn't produce any results.
- */
- class PlanRankingNoCollscan : public PlanRankingTestBase {
- public:
- void run() {
- for (int i = 0; i < N; ++i) {
- insert(BSON("_id" << i));
- }
-
- addIndex(BSON("_id" << 1));
-
- // Run a query with a sort. The blocking sort won't produce any data during the
- // evaluation period.
- CanonicalQuery* cq;
- BSONObj queryObj = BSON("_id" << BSON("$gte" << 20 << "$lte" << 200));
- BSONObj sortObj = BSON("c" << 1);
- BSONObj projObj = BSONObj();
- ASSERT(CanonicalQuery::canonicalize(ns,
- queryObj,
- sortObj,
- projObj,
- &cq).isOK());
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
-
- // The best must not be a collscan.
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{sort: {pattern: {c: 1}, limit: 0, node: {"
- "fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {_id: 1}}}}}}}}",
- soln->root.get()));
+ // Add indices on 'a' and 'b'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // Run the query {a:N+1, b:1}. (No such document.)
+ CanonicalQuery* cq;
+ verify(CanonicalQuery::canonicalize(ns, BSON("a" << N + 1 << "b" << 1), &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // {a: 100} is super selective so choose that.
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}", soln->root.get()));
+ }
+};
+
+/**
+ * Same as PlanRankingPreferImmediateEOF, but substitute a range predicate on "a" for the
+ * equality predicate on "a". The presence of the range predicate has an impact on the
+ * intersection plan that is raced against the single-index plans: since "a" no longer generates
+ * point interval bounds, the results of the index scan aren't guaranteed to be returned in
+ * RecordId order, and so the intersection plan uses the AND_HASHED stage instead of the
+ * AND_SORTED stage. It is still the case that the query should pick the plan that uses index
+ * "b", instead of the plan that uses index "a" or the (hashed) intersection plan.
+ */
+class PlanRankingPreferImmediateEOFAgainstHashed : public PlanRankingTestBase {
+public:
+ void run() {
+ // 'a' is very selective, 'b' is not.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << i << "b" << 1));
}
- };
- /**
- * No indices are available, output a collscan.
- */
- class PlanRankingCollscan : public PlanRankingTestBase {
- public:
- void run() {
- // Insert data for which we have no index.
- for (int i = 0; i < N; ++i) {
- insert(BSON("foo" << i));
- }
-
- // Look for A Space Odyssey.
- CanonicalQuery* cq;
- verify(CanonicalQuery::canonicalize(ns, BSON("foo" << 2001), &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Takes ownership of cq.
- QuerySolution* soln = pickBestPlan(cq);
-
- // The best must be a collscan.
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{cscan: {dir: 1, filter: {foo: 2001}}}",
- soln->root.get()));
+ // Add indices on 'a' and 'b'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // Run the query {a:N+1, b:1}. (No such document.)
+ CanonicalQuery* cq;
+ verify(CanonicalQuery::canonicalize(ns, BSON("a" << BSON("$gte" << N + 1) << "b" << 1), &cq)
+ .isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // {a: 100} is super selective so choose that.
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {filter: {b:1}, node: {ixscan: {pattern: {a: 1}}}}}", soln->root.get()));
+ }
+};
+
+/**
+ * We have an index on _id and a query over _id with a sort. Ensure that we don't pick a
+ * collscan as the best plan even though the _id-scanning solution doesn't produce any results.
+ */
+class PlanRankingNoCollscan : public PlanRankingTestBase {
+public:
+ void run() {
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("_id" << i));
}
- };
- /**
- * When no other information is available, prefer solutions without
- * a blocking sort stage.
- */
- class PlanRankingAvoidBlockingSort : public PlanRankingTestBase {
- public:
- void run() {
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << 1 << "d" << i));
- }
-
- // The index {d: 1, e: 1} provides the desired sort order,
- // while index {a: 1, b: 1} can be used to answer the
- // query predicate, but does not provide the sort.
- addIndex(BSON("a" << 1 << "b" << 1));
- addIndex(BSON("d" << 1 << "e" << 1));
-
- // Query: find({a: 1}).sort({d: 1})
- CanonicalQuery* cq;
- ASSERT(CanonicalQuery::canonicalize(ns,
- BSON("a" << 1),
- BSON("d" << 1), // sort
- BSONObj(), // projection
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // No results will be returned during the trial period,
- // so we expect to choose {d: 1, e: 1}, as it allows us
- // to avoid the sort stage.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {d:1,e:1}}}}}",
- soln->root.get()));
+ addIndex(BSON("_id" << 1));
+
+ // Run a query with a sort. The blocking sort won't produce any data during the
+ // evaluation period.
+ CanonicalQuery* cq;
+ BSONObj queryObj = BSON("_id" << BSON("$gte" << 20 << "$lte" << 200));
+ BSONObj sortObj = BSON("c" << 1);
+ BSONObj projObj = BSONObj();
+ ASSERT(CanonicalQuery::canonicalize(ns, queryObj, sortObj, projObj, &cq).isOK());
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+
+ // The best must not be a collscan.
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{sort: {pattern: {c: 1}, limit: 0, node: {"
+ "fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {_id: 1}}}}}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * No indices are available, output a collscan.
+ */
+class PlanRankingCollscan : public PlanRankingTestBase {
+public:
+ void run() {
+ // Insert data for which we have no index.
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("foo" << i));
}
- };
- /**
- * Make sure we run candidate plans for long enough when none of the
- * plans are producing results.
- */
- class PlanRankingWorkPlansLongEnough : public PlanRankingTestBase {
- public:
- void run() {
- for (int i = 0; i < N; ++i) {
- insert(BSON("a" << 1));
- insert(BSON("a" << 1 << "b" << 1 << "c" << i));
- }
-
- // Indices on 'a' and 'b'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // Solutions using either 'a' or 'b' will take a long time to start producing
- // results. However, an index scan on 'b' will start producing results sooner
- // than an index scan on 'a'.
- CanonicalQuery* cq;
- ASSERT(CanonicalQuery::canonicalize(ns,
- fromjson("{a: 1, b: 1, c: {$gte: 5000}}"),
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Use index on 'b'.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {ixscan: {pattern: {b: 1}}}}}",
- soln->root.get()));
+ // Look for A Space Odyssey.
+ CanonicalQuery* cq;
+ verify(CanonicalQuery::canonicalize(ns, BSON("foo" << 2001), &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Takes ownership of cq.
+ QuerySolution* soln = pickBestPlan(cq);
+
+ // The best must be a collscan.
+ ASSERT(QueryPlannerTestLib::solutionMatches("{cscan: {dir: 1, filter: {foo: 2001}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * When no other information is available, prefer solutions without
+ * a blocking sort stage.
+ */
+class PlanRankingAvoidBlockingSort : public PlanRankingTestBase {
+public:
+ void run() {
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << 1 << "d" << i));
}
- };
- /**
- * Suppose we have two plans which are roughly equivalent, other than that
- * one uses an index which involves doing a lot more skipping of index keys.
- * Prefer the plan which does not have to do this index key skipping.
- */
- class PlanRankingAccountForKeySkips : public PlanRankingTestBase {
- public:
- void run() {
- for (int i = 0; i < 100; ++i) {
- insert(BSON("a" << i << "b" << i << "c" << i));
- }
-
- // These indices look equivalent to the ranker for the query below unless we account
- // for key skipping. We should pick index {a: 1} if we account for key skipping
- // properly.
- addIndex(BSON("b" << 1 << "c" << 1));
- addIndex(BSON("a" << 1));
-
- CanonicalQuery* cq;
- ASSERT(CanonicalQuery::canonicalize(ns,
- fromjson("{a: 9, b: {$ne: 10}, c: 9}"),
- &cq).isOK());
- ASSERT(NULL != cq);
- std::unique_ptr<CanonicalQuery> killCq(cq);
-
- // Expect to use index {a: 1, b: 1}.
- QuerySolution* soln = pickBestPlan(cq);
- ASSERT(QueryPlannerTestLib::solutionMatches(
- "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}",
- soln->root.get()));
+ // The index {d: 1, e: 1} provides the desired sort order,
+ // while index {a: 1, b: 1} can be used to answer the
+ // query predicate, but does not provide the sort.
+ addIndex(BSON("a" << 1 << "b" << 1));
+ addIndex(BSON("d" << 1 << "e" << 1));
+
+ // Query: find({a: 1}).sort({d: 1})
+ CanonicalQuery* cq;
+ ASSERT(CanonicalQuery::canonicalize(ns,
+ BSON("a" << 1),
+ BSON("d" << 1), // sort
+ BSONObj(), // projection
+ &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // No results will be returned during the trial period,
+ // so we expect to choose {d: 1, e: 1}, as it allows us
+ // to avoid the sort stage.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {d:1,e:1}}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * Make sure we run candidate plans for long enough when none of the
+ * plans are producing results.
+ */
+class PlanRankingWorkPlansLongEnough : public PlanRankingTestBase {
+public:
+ void run() {
+ for (int i = 0; i < N; ++i) {
+ insert(BSON("a" << 1));
+ insert(BSON("a" << 1 << "b" << 1 << "c" << i));
}
- };
-
- class All : public Suite {
- public:
- All() : Suite( "query_plan_ranking" ) {}
-
- void setupTests() {
- add<PlanRankingIntersectOverride>();
- add<PlanRankingIntersectWithBackup>();
- add<PlanRankingPreferCovered>();
- add<PlanRankingAvoidIntersectIfNoResults>();
- add<PlanRankingPreferCoveredEvenIfNoResults>();
- add<PlanRankingPreferImmediateEOF>();
- add<PlanRankingPreferImmediateEOFAgainstHashed>();
- add<PlanRankingNoCollscan>();
- add<PlanRankingCollscan>();
- add<PlanRankingAvoidBlockingSort>();
- add<PlanRankingWorkPlansLongEnough>();
- add<PlanRankingAccountForKeySkips>();
+
+ // Indices on 'a' and 'b'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // Solutions using either 'a' or 'b' will take a long time to start producing
+ // results. However, an index scan on 'b' will start producing results sooner
+ // than an index scan on 'a'.
+ CanonicalQuery* cq;
+ ASSERT(CanonicalQuery::canonicalize(ns, fromjson("{a: 1, b: 1, c: {$gte: 5000}}"), &cq)
+ .isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Use index on 'b'.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches("{fetch: {node: {ixscan: {pattern: {b: 1}}}}}",
+ soln->root.get()));
+ }
+};
+
+/**
+ * Suppose we have two plans which are roughly equivalent, other than that
+ * one uses an index which involves doing a lot more skipping of index keys.
+ * Prefer the plan which does not have to do this index key skipping.
+ */
+class PlanRankingAccountForKeySkips : public PlanRankingTestBase {
+public:
+ void run() {
+ for (int i = 0; i < 100; ++i) {
+ insert(BSON("a" << i << "b" << i << "c" << i));
}
- };
- SuiteInstance<All> planRankingAll;
+ // These indices look equivalent to the ranker for the query below unless we account
+ // for key skipping. We should pick index {a: 1} if we account for key skipping
+ // properly.
+ addIndex(BSON("b" << 1 << "c" << 1));
+ addIndex(BSON("a" << 1));
+
+ CanonicalQuery* cq;
+ ASSERT(
+ CanonicalQuery::canonicalize(ns, fromjson("{a: 9, b: {$ne: 10}, c: 9}"), &cq).isOK());
+ ASSERT(NULL != cq);
+ std::unique_ptr<CanonicalQuery> killCq(cq);
+
+ // Expect to use index {a: 1, b: 1}.
+ QuerySolution* soln = pickBestPlan(cq);
+ ASSERT(QueryPlannerTestLib::solutionMatches("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}",
+ soln->root.get()));
+ }
+};
+
+class All : public Suite {
+public:
+ All() : Suite("query_plan_ranking") {}
+
+ void setupTests() {
+ add<PlanRankingIntersectOverride>();
+ add<PlanRankingIntersectWithBackup>();
+ add<PlanRankingPreferCovered>();
+ add<PlanRankingAvoidIntersectIfNoResults>();
+ add<PlanRankingPreferCoveredEvenIfNoResults>();
+ add<PlanRankingPreferImmediateEOF>();
+ add<PlanRankingPreferImmediateEOFAgainstHashed>();
+ add<PlanRankingNoCollscan>();
+ add<PlanRankingCollscan>();
+ add<PlanRankingAvoidBlockingSort>();
+ add<PlanRankingWorkPlansLongEnough>();
+ add<PlanRankingAccountForKeySkips>();
+ }
+};
+
+SuiteInstance<All> planRankingAll;
} // namespace PlanRankingTest