/** * Copyright (C) 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. */ /** * This file tests db/query/plan_ranker.cpp and db/query/multi_plan_runner.cpp. */ #include "mongo/client/dbclientcursor.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database.h" #include "mongo/db/exec/multi_plan.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/instance.h" #include "mongo/db/json.h" #include "mongo/db/query/get_runner.h" #include "mongo/db/query/qlog.h" #include "mongo/db/query/query_knobs.h" #include "mongo/db/query/query_planner.h" #include "mongo/db/query/query_planner_test_lib.h" #include "mongo/db/query/stage_builder.h" #include "mongo/dbtests/dbtests.h" namespace mongo { // How we access the external setParameter testing bool. extern bool internalQueryForceIntersectionPlans; } // namespace mongo namespace PlanRankingTests { static const char* ns = "unittests.PlanRankingTests"; class PlanRankingTestBase { public: PlanRankingTestBase() : _internalQueryForceIntersectionPlans(internalQueryForceIntersectionPlans) { Client::WriteContext ctx(ns); _client.dropCollection(ns); } virtual ~PlanRankingTestBase() { // Restore external setParameter testing bool. internalQueryForceIntersectionPlans = _internalQueryForceIntersectionPlans; } void insert(const BSONObj& obj) { Client::WriteContext ctx(ns); _client.insert(ns, obj); } void addIndex(const BSONObj& obj) { Client::WriteContext ctx(ns); _client.ensureIndex(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) { Client::ReadContext ctx(ns); Collection* collection = ctx.ctx().db()->getCollection(ns); QueryPlannerParams plannerParams; fillOutPlannerParams(collection, cq, &plannerParams); // Turn this off otherwise it pops up in some plans. plannerParams.options &= ~QueryPlannerParams::KEEP_MUTATIONS; // Plan. vector 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(collection, cq)); 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(collection, *solutions[i], ws, &root)); // Takes ownership of all arguments. _mps->addPlan(solutions[i], root, ws); } _mps->pickBestPlan(); // This is what sets a backup plan, should we test for it. 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(); } /** * Was a backup plan picked during the ranking process? */ bool hasBackupPlan() const { ASSERT(NULL != _mps.get()); return _mps->hasBackupPlan(); } 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: static DBDirectClient _client; scoped_ptr _mps; // Holds the value of global "internalQueryForceIntersectionPlans" setParameter flag. // Restored at end of test invocation regardless of test result. bool _internalQueryForceIntersectionPlans; }; DBDirectClient PlanRankingTestBase::_client; // static const int PlanRankingTestBase::N = internalQueryPlanEvaluationWorks + 1000; /** * 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); // {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()); // 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: {filter: null, 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)); } // 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); // 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: {filter: null, 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)); } 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); // 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)); } 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); // 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)); } 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); // 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)); } // 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); // {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)); } 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()); // 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)); } // Look for A Space Odyssey. CanonicalQuery* cq; verify(CanonicalQuery::canonicalize(ns, BSON("foo" << 2001), &cq).isOK()); ASSERT(NULL != 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())); } }; /** * Index intersection solutions can be covered when single-index solutions * are not. If the single-index solutions need to do a lot of fetching, * then ixisect should win. */ class PlanRankingIxisectCovered : public PlanRankingTestBase { public: void run() { // Neither 'a' nor 'b' is selective. for (int i = 0; i < N; ++i) { insert(BSON("a" << 1 << "b" << 1)); } // Add indices on 'a' and 'b'. addIndex(BSON("a" << 1)); addIndex(BSON("b" << 1)); // Query {a:1, b:1}, and project out all fields other than 'a' and 'b'. CanonicalQuery* cq; ASSERT(CanonicalQuery::canonicalize(ns, BSON("a" << 1 << "b" << 1), BSONObj(), BSON("_id" << 0 << "a" << 1 << "b" << 1), &cq).isOK()); ASSERT(NULL != cq); // We should choose an ixisect plan because it requires fewer fetches. // Takes ownership of cq. QuerySolution* soln = pickBestPlan(cq); ASSERT(QueryPlannerTestLib::solutionMatches( "{proj: {spec: {_id:0,a:1,b:1}, node: {andSorted: {nodes: [" "{ixscan: {filter: null, pattern: {a:1}}}," "{ixscan: {filter: null, pattern: {b:1}}}]}}}}", soln->root.get())); } }; /** * Use the same data, same indices, and same query as the previous * test case, except without the projection. The query is not covered * by the index in this case, which means that there is no advantage * to an index intersection solution. */ class PlanRankingIxisectNonCovered : public PlanRankingTestBase { public: void run() { // Neither 'a' nor 'b' is selective. for (int i = 0; i < N; ++i) { insert(BSON("a" << 1 << "b" << 1)); } // Add indices on 'a' and 'b'. addIndex(BSON("a" << 1)); addIndex(BSON("b" << 1)); // Query {a:1, b:1}. CanonicalQuery* cq; ASSERT(CanonicalQuery::canonicalize(ns, BSON("a" << 1 << "b" << 1), &cq).isOK()); ASSERT(NULL != cq); // The intersection is large, and ixisect does not make the // query covered. We should NOT choose an intersection plan. QuerySolution* soln = pickBestPlan(cq); 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); } }; /** * Index intersection solutions may require fewer fetches even if it does not make the * query covered. The ixisect plan will scan as many index keys as the union of the two * single index plans, but only needs to retrieve full documents for the intersection * of the two plans---this could mean fewer fetches! */ class PlanRankingNonCoveredIxisectFetchesLess : public PlanRankingTestBase { public: void run() { // Set up data so that the following conditions hold: // 1) Documents matching {a: 1} are of high cardinality. // 2) Documents matching {b: 1} are of high cardinality. // 3) Documents matching {a: 1, b: 1} are of low cardinality--- // the intersection is small. // 4) At least one of the documents in the intersection is // returned during the trial period. insert(BSON("a" << 1 << "b" << 1)); for (int i = 0; i < N/2; ++i) { insert(BSON("a" << 1 << "b" << 2)); } for (int i = 0; i < N/2; ++i) { insert(BSON("a" << 2 << "b" << 1)); } // Add indices on 'a' and 'b'. addIndex(BSON("a" << 1)); addIndex(BSON("b" << 1)); // Neither the predicate on 'b' nor the predicate on 'a' is // very selective: both retrieve about half the documents. // However, the intersection is very small, which makes // the intersection plan desirable. CanonicalQuery* cq; ASSERT(CanonicalQuery::canonicalize(ns, fromjson("{a: 1, b: 1}"), &cq).isOK()); ASSERT(NULL != cq); QuerySolution* soln = pickBestPlan(cq); ASSERT(QueryPlannerTestLib::solutionMatches( "{fetch: {filter: null, node: {andSorted: {nodes: [" "{ixscan: {filter: null, pattern: {a:1}}}," "{ixscan: {filter: null, pattern: {b:1}}}]}}}}", soln->root.get())); } }; /** * If the intersection is small, an AND_SORTED plan may be able to * hit EOF before the single index plans. */ class PlanRankingIxisectHitsEOFFirst : public PlanRankingTestBase { public: void run() { // Set up the data so that for the query {a: 1, b: 1}, the // intersection is empty. The single index plans have to do // more fetching from disk in order to determine that the result // set is empty. As a result, the intersection plan hits EOF first. for (int i = 0; i < 30; ++i) { insert(BSON("a" << 1 << "b" << 2)); } for (int i = 0; i < 30; ++i) { insert(BSON("a" << 2 << "b" << 1)); } for (int i = 0; i < N; ++i) { insert(BSON("a" << 2 << "b" << 2)); } // Add indices on 'a' and 'b'. addIndex(BSON("a" << 1)); addIndex(BSON("b" << 1)); CanonicalQuery* cq; ASSERT(CanonicalQuery::canonicalize(ns, fromjson("{a: 1, b: 1}"), &cq).isOK()); ASSERT(NULL != cq); // Choose the index intersection plan. QuerySolution* soln = pickBestPlan(cq); ASSERT(QueryPlannerTestLib::solutionMatches( "{fetch: {filter: null, node: {andSorted: {nodes: [" "{ixscan: {filter: null, pattern: {a:1}}}," "{ixscan: {filter: null, pattern: {b:1}}}]}}}}", soln->root.get())); } }; /** * If we query on 'a', 'b', and 'c' with indices on all three fields, * then there are three possible size-2 index intersections to consider. * Make sure we choose the right one. */ class PlanRankingChooseBetweenIxisectPlans : public PlanRankingTestBase { public: void run() { // Set up the data so that for the query {a: 1, b: 1, c: 1}, the intersection // between 'b' and 'c' is small, and the other intersections are larger. for (int i = 0; i < 10; ++i) { insert(BSON("a" << 1 << "b" << 1 << "c" << 1)); } for (int i = 0; i < 10; ++i) { insert(BSON("a" << 2 << "b" << 1 << "c" << 1)); } for (int i = 0; i < N/2; ++i) { insert(BSON("a" << 1 << "b" << 1 << "c" << 2)); insert(BSON("a" << 1 << "b" << 2 << "c" << 1)); } // Add indices on 'a', 'b', and 'c'. addIndex(BSON("a" << 1)); addIndex(BSON("b" << 1)); addIndex(BSON("c" << 1)); CanonicalQuery* cq; ASSERT(CanonicalQuery::canonicalize(ns, fromjson("{a: 1, b: 1, c: 1}"), &cq).isOK()); ASSERT(NULL != cq); // Intersection between 'b' and 'c' should hit EOF while the // other plans are busy fetching documents. QuerySolution* soln = pickBestPlan(cq); ASSERT(QueryPlannerTestLib::solutionMatches( "{fetch: {filter: {a:1}, node: {andSorted: {nodes: [" "{ixscan: {filter: null, pattern: {b:1}}}," "{ixscan: {filter: null, pattern: {c:1}}}]}}}}", 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)); } // 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); // 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)); } // 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); // Use index on 'b'. QuerySolution* soln = pickBestPlan(cq); std::cerr << "PlanRankingWorkPlansLongEnough: soln=" << soln->toString() << std::endl; ASSERT(QueryPlannerTestLib::solutionMatches( "{fetch: {node: {ixscan: {pattern: {b: 1}}}}}", soln->root.get())); } }; class All : public Suite { public: All() : Suite( "query_plan_ranking" ) {} void setupTests() { add(); add(); add(); add(); add(); add(); add(); add(); // TODO: These don't work without counting FETCH and FETCH is now gone. // add(); // add(); // add(); // add(); // add(); add(); add(); } } planRankingAll; } // namespace PlanRankingTest