SERVER-117622 Disallow hashed index from SERVER-99889 from running in new executor, using engine selection (#48869)
Co-authored-by: Matthew Boros <matt.boros@mongodb.com> GitOrigin-RevId: 39662237882fb49ce2651b12de66bd04f820dda4
This commit is contained in:
parent
31c8b232b7
commit
0b50ea103e
@ -627,6 +627,7 @@ mongo_cc_unit_test(
|
||||
"//src/mongo/db:query_exec",
|
||||
"//src/mongo/db/pipeline:aggregation_request_helper",
|
||||
"//src/mongo/db/pipeline:expression_context_for_test",
|
||||
"//src/mongo/db/query/compiler/physical_model/query_solution:query_solution_test_util",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -33,8 +33,19 @@ mongo_cc_unit_test(
|
||||
tags = ["mongo_unittest_sixth_group"],
|
||||
deps = [
|
||||
":query_solution",
|
||||
":query_solution_test_util",
|
||||
"//src/mongo/db/query:query_planner",
|
||||
"//src/mongo/db/query:query_test_service_context",
|
||||
"//src/mongo/db/query/collation:collator_interface_mock",
|
||||
],
|
||||
)
|
||||
|
||||
mongo_cc_library(
|
||||
name = "query_solution_test_util",
|
||||
srcs = [
|
||||
"query_solution_test_util.cpp",
|
||||
],
|
||||
deps = [
|
||||
":query_solution",
|
||||
"//src/mongo:base",
|
||||
],
|
||||
)
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
#include "mongo/db/query/compiler/parsers/matcher/expression_parser.h"
|
||||
#include "mongo/db/query/compiler/physical_model/interval/interval.h"
|
||||
#include "mongo/db/query/compiler/physical_model/query_solution/eof_node_type.h"
|
||||
#include "mongo/db/query/compiler/physical_model/query_solution/query_solution_test_util.h"
|
||||
#include "mongo/db/query/planner_wildcard_helpers.h"
|
||||
#include "mongo/db/query/query_test_service_context.h"
|
||||
#include "mongo/db/query/wildcard_test_utils.h"
|
||||
@ -110,22 +111,6 @@ bool operator!=(const ProvidedSortSet& lhs, const ProvidedSortSet& rhs) {
|
||||
namespace {
|
||||
|
||||
using namespace mongo;
|
||||
/**
|
||||
* Make a minimal IndexEntry from just a key pattern. A dummy name will be added if none provided.
|
||||
*/
|
||||
IndexEntry buildSimpleIndexEntry(const BSONObj& kp, std::string name = "test_foo") {
|
||||
return {kp,
|
||||
IndexNames::nameToType(IndexNames::findPluginName(kp)),
|
||||
IndexConfig::kLatestIndexVersion,
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
false,
|
||||
CoreIndexInfo::Identifier(std::move(name)),
|
||||
{},
|
||||
nullptr};
|
||||
}
|
||||
|
||||
void assertNamespaceVectorsAreEqual(const std::vector<NamespaceStringOrUUID>& secondaryNssVector,
|
||||
const std::vector<NamespaceStringOrUUID>& expectedNssVector) {
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (C) 2026-present MongoDB, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 Server Side 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.
|
||||
*/
|
||||
|
||||
#include "mongo/db/query/compiler/physical_model/query_solution/query_solution_test_util.h"
|
||||
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/bson/json.h"
|
||||
#include "mongo/db/matcher/expression.h"
|
||||
#include "mongo/db/namespace_string.h"
|
||||
#include "mongo/db/query/compiler/ce/sampling/sampling_estimator.h"
|
||||
#include "mongo/db/query/compiler/optimizer/cost_based_ranker/cardinality_estimator.h"
|
||||
#include "mongo/db/query/compiler/optimizer/cost_based_ranker/cbr_test_utils.h"
|
||||
#include "mongo/db/query/compiler/optimizer/cost_based_ranker/estimates.h"
|
||||
#include "mongo/db/query/compiler/optimizer/index_bounds_builder/index_bounds_builder.h"
|
||||
#include "mongo/db/query/compiler/physical_model/index_bounds/index_bounds.h"
|
||||
#include "mongo/platform/compiler.h"
|
||||
|
||||
namespace mongo {
|
||||
|
||||
/**
|
||||
* Make a minimal IndexEntry from just a key pattern. A dummy name will be added if none provided.
|
||||
*/
|
||||
IndexEntry buildSimpleIndexEntry(const BSONObj& kp, std::string name) {
|
||||
return {kp,
|
||||
IndexNames::nameToType(IndexNames::findPluginName(kp)),
|
||||
IndexConfig::kLatestIndexVersion,
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
false,
|
||||
CoreIndexInfo::Identifier(std::move(name)),
|
||||
{},
|
||||
nullptr};
|
||||
}
|
||||
|
||||
} // namespace mongo
|
||||
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (C) 2026-present MongoDB, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 Server Side 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/query/compiler/physical_model/query_solution/query_solution.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
namespace mongo {
|
||||
|
||||
/**
|
||||
* Make a minimal IndexEntry from just a key pattern. A dummy name will be added if none provided.
|
||||
*/
|
||||
IndexEntry buildSimpleIndexEntry(const BSONObj& kp, std::string name = "test_foo");
|
||||
|
||||
} // namespace mongo
|
||||
@ -51,37 +51,6 @@
|
||||
|
||||
namespace mongo {
|
||||
namespace {
|
||||
/**
|
||||
* Returns true iff 'descriptor' has fields A and B where all of the following hold
|
||||
*
|
||||
* - A is a path prefix of B
|
||||
* - A is a hashed field in the index
|
||||
* - B is a non-hashed field in the index
|
||||
*
|
||||
* TODO SERVER-99889 this is a workaround for an SBE stage builder bug.
|
||||
*/
|
||||
bool indexHasHashedPathPrefixOfNonHashedPath(const IndexDescriptor* descriptor) {
|
||||
boost::optional<StringData> hashedPath;
|
||||
for (const auto& elt : descriptor->keyPattern()) {
|
||||
if (elt.valueStringDataSafe() == "hashed") {
|
||||
// Indexes may only contain one hashed field.
|
||||
hashedPath = elt.fieldNameStringData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hashedPath == boost::none) {
|
||||
// No hashed fields in the index.
|
||||
return false;
|
||||
}
|
||||
// Check if 'hashedPath' is a path prefix for any field in the index.
|
||||
for (const auto& elt : descriptor->keyPattern()) {
|
||||
if (expression::isPathPrefixOf(hashedPath.get(), elt.fieldNameStringData())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if 'collection' has an index that contains two fields, one of which is a path prefix
|
||||
* of the other, where the prefix field is hashed. Indexes can only contain one hashed field.
|
||||
@ -102,30 +71,13 @@ bool collectionHasIndexWithHashedPathPrefixOfNonHashedPath(const CollectionPtr&
|
||||
indexCatalog->getIndexIterator(IndexCatalog::InclusionPolicy::kReady);
|
||||
while (indexIter->more()) {
|
||||
const IndexCatalogEntry* entry = indexIter->next();
|
||||
if (indexHasHashedPathPrefixOfNonHashedPath(entry->descriptor())) {
|
||||
if (indexHasHashedPathPrefixOfNonHashedPath(entry->descriptor()->keyPattern())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasNodeOfType(const QuerySolutionNode* node, StageType type) {
|
||||
if (node->getType() == type) {
|
||||
return true;
|
||||
}
|
||||
for (auto&& child : node->children) {
|
||||
if (hasNodeOfType(child.get(), type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isPlanSbeEligible(const QuerySolution* solution) {
|
||||
// Distinct scan plans not supported in SBE.
|
||||
return !hasNodeOfType(solution->root(), StageType::STAGE_DISTINCT_SCAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given query can be executed with the SBE engine based on the canonical query.
|
||||
*
|
||||
@ -180,7 +132,8 @@ bool isQuerySbeCompatible(const CollectionPtr& collection,
|
||||
|
||||
// Queries against collections with a particular shape of compound hashed indexes are not
|
||||
// supported.
|
||||
if (collection && collectionHasIndexWithHashedPathPrefixOfNonHashedPath(collection, expCtx)) {
|
||||
if (!feature_flags::gFeatureFlagGetExecutorDeferredEngineChoice.isEnabled() && collection &&
|
||||
collectionHasIndexWithHashedPathPrefixOfNonHashedPath(collection, expCtx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -99,6 +99,9 @@ void visit(F&& f, const QuerySolutionNode& node) {
|
||||
case STAGE_EQ_LOOKUP_UNWIND:
|
||||
f(static_cast<const EqLookupUnwindNode&>(node));
|
||||
break;
|
||||
case STAGE_DISTINCT_SCAN:
|
||||
f(static_cast<const DistinctNode&>(node));
|
||||
break;
|
||||
default:
|
||||
f(node);
|
||||
break;
|
||||
@ -197,21 +200,38 @@ public:
|
||||
static_assert(HasPreVisit<LookupUnwindRule, EqLookupUnwindNode>);
|
||||
|
||||
/**
|
||||
* This rule matches when:
|
||||
* 1. There is at least one IXSCAN in the tree.
|
||||
*
|
||||
* TODO SERVER-117622: Implement this rule.
|
||||
* This rule matches:
|
||||
* 1. A query solution that has at least one DISTINCT_SCAN node.
|
||||
*/
|
||||
class IxScanRule {
|
||||
class DistinctScanRule {
|
||||
public:
|
||||
void preVisit(RuleEngine& engine, const IndexScanNode& node) {
|
||||
void preVisit(RuleEngine& engine, const DistinctNode& node) {
|
||||
engine.match();
|
||||
}
|
||||
};
|
||||
static_assert(HasPreVisit<IxScanRule, IndexScanNode>);
|
||||
static_assert(HasPreVisit<DistinctScanRule, DistinctNode>);
|
||||
|
||||
/**
|
||||
* This rule matches:
|
||||
* 1. A query solution that has at least one IXSCAN, whose selected key pattern contains both a
|
||||
* hashed index and a dotted path for it (SERVER-99889).
|
||||
*/
|
||||
class HashedIndexScanPatternRule {
|
||||
public:
|
||||
void preVisit(RuleEngine& engine, const IndexScanNode& node) {
|
||||
if (indexHasHashedPathPrefixOfNonHashedPath(node.index.keyPattern)) {
|
||||
engine.match();
|
||||
}
|
||||
}
|
||||
};
|
||||
static_assert(HasPreVisit<HashedIndexScanPatternRule, IndexScanNode>);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isPlanSbeEligible(const QuerySolution* solution) {
|
||||
return !treeMatchesAny(solution, DistinctScanRule(), HashedIndexScanPatternRule());
|
||||
}
|
||||
|
||||
EngineChoice engineSelectionForPlan(const QuerySolution* solution) {
|
||||
LOGV2_DEBUG(11986305,
|
||||
1,
|
||||
@ -222,4 +242,26 @@ EngineChoice engineSelectionForPlan(const QuerySolution* solution) {
|
||||
: EngineChoice::kClassic;
|
||||
}
|
||||
|
||||
bool indexHasHashedPathPrefixOfNonHashedPath(const BSONObj& keyPattern) {
|
||||
boost::optional<StringData> hashedPath;
|
||||
for (const auto& elt : keyPattern) {
|
||||
if (elt.valueStringDataSafe() == "hashed") {
|
||||
// Indexes may only contain one hashed field.
|
||||
hashedPath = elt.fieldNameStringData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hashedPath == boost::none) {
|
||||
// No hashed fields in the index.
|
||||
return false;
|
||||
}
|
||||
// Check if 'hashedPath' is a path prefix for any field in the index.
|
||||
for (const auto& elt : keyPattern) {
|
||||
if (expression::isPathPrefixOf(hashedPath.get(), elt.fieldNameStringData())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@ -35,8 +35,24 @@
|
||||
namespace mongo {
|
||||
|
||||
/**
|
||||
* Returns 'true' for all query solution plans that are enabled and should be dispatched to SBE.
|
||||
* Returns 'false' for query plans that can not be executed in SBE.
|
||||
*/
|
||||
bool isPlanSbeEligible(const QuerySolution* solution);
|
||||
|
||||
/**
|
||||
* Returns the engine of choice for executing the specified query plan.
|
||||
*/
|
||||
EngineChoice engineSelectionForPlan(const QuerySolution* solution);
|
||||
|
||||
/**
|
||||
* Returns true iff 'keyPattern' has fields A and B where all of the following hold
|
||||
*
|
||||
* - A is a path prefix of B
|
||||
* - A is a hashed field in the index
|
||||
* - B is a non-hashed field in the index
|
||||
*
|
||||
* TODO SERVER-99889 this is a workaround for an SBE stage builder bug.
|
||||
*/
|
||||
bool indexHasHashedPathPrefixOfNonHashedPath(const BSONObj& keyPattern);
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
@ -30,46 +30,42 @@
|
||||
#include "mongo/db/query/engine_selection_plan.h"
|
||||
|
||||
#include "mongo/bson/json.h"
|
||||
#include "mongo/db/pipeline/document_source_lookup.h"
|
||||
#include "mongo/db/pipeline/expression_context_for_test.h"
|
||||
#include "mongo/db/query/canonical_query.h"
|
||||
#include "mongo/db/query/compiler/optimizer/index_bounds_builder/index_bounds_builder.h"
|
||||
#include "mongo/db/query/compiler/physical_model/query_solution/query_solution_test_util.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
#include "mongo/util/assert_util.h"
|
||||
|
||||
namespace mongo {
|
||||
|
||||
namespace {
|
||||
class EngineSelectionPlanFixture : public mongo::unittest::Test {
|
||||
public:
|
||||
EngineSelectionPlanFixture()
|
||||
: nss(NamespaceString::createNamespaceString_forTest("testdb.coll")) {}
|
||||
|
||||
// TODO(SERVER-117622): Share fieldsToKeyPattern and buildSimpleIndexEntry with cbr_test_utils.h.
|
||||
BSONObj fieldsToKeyPattern(const std::vector<std::string>& indexFields) {
|
||||
BSONObjBuilder bob;
|
||||
for (auto& fieldName : indexFields) {
|
||||
bob.append(fieldName, 1);
|
||||
std::unique_ptr<QuerySolution> makeDistinctScanPlan(BSONObj indexKeys) {
|
||||
auto distinct = std::make_unique<DistinctNode>(nss, buildSimpleIndexEntry(indexKeys));
|
||||
auto solution = std::make_unique<QuerySolution>();
|
||||
solution->setRoot(std::move(distinct));
|
||||
return solution;
|
||||
}
|
||||
return bob.obj();
|
||||
}
|
||||
|
||||
IndexEntry buildSimpleIndexEntry(const std::vector<std::string>& indexFields) {
|
||||
BSONObj kp = fieldsToKeyPattern(indexFields);
|
||||
return {kp,
|
||||
IndexNames::nameToType(IndexNames::findPluginName(kp)),
|
||||
IndexConfig::kLatestIndexVersion,
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
false,
|
||||
CoreIndexInfo::Identifier("test_foo"),
|
||||
{},
|
||||
nullptr};
|
||||
}
|
||||
std::unique_ptr<QuerySolution> makeIndexScanFetchPlan(BSONObj indexKeys) {
|
||||
auto indexScan = std::make_unique<IndexScanNode>(nss, buildSimpleIndexEntry(indexKeys));
|
||||
auto fetch = std::make_unique<FetchNode>(std::move(indexScan), nss);
|
||||
|
||||
TEST(GetExecutor, LookupUnwind) {
|
||||
auto solution = std::make_unique<QuerySolution>();
|
||||
solution->setRoot(std::move(fetch));
|
||||
return solution;
|
||||
}
|
||||
|
||||
protected:
|
||||
NamespaceString nss;
|
||||
};
|
||||
|
||||
TEST_F(EngineSelectionPlanFixture, LookupUnwind) {
|
||||
auto nssLocal = NamespaceString::createNamespaceString_forTest("testdb.collLocal");
|
||||
auto nssForeign = NamespaceString::createNamespaceString_forTest("testdb.collForeign");
|
||||
|
||||
std::vector<std::string> indexFields = {"a"};
|
||||
BSONObj indexFields = fromjson("{a: 1}");
|
||||
auto indexScan = std::make_unique<IndexScanNode>(nssLocal, buildSimpleIndexEntry(indexFields));
|
||||
auto lookupUnwind =
|
||||
std::make_unique<EqLookupUnwindNode>(std::move(indexScan),
|
||||
@ -87,6 +83,37 @@ TEST(GetExecutor, LookupUnwind) {
|
||||
ASSERT_TRUE(engineSelectionForPlan(solution.get()) == EngineChoice::kSbe);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
// Test eligibility of DISTINCT_SCAN plans.
|
||||
TEST_F(EngineSelectionPlanFixture, DistinctScanEligibility) {
|
||||
BSONObj indexFields = fromjson("{a: 1}");
|
||||
|
||||
std::unique_ptr<QuerySolution> solution = makeDistinctScanPlan(indexFields);
|
||||
ASSERT_FALSE(isPlanSbeEligible(solution.get()));
|
||||
}
|
||||
|
||||
// Test eligibility of FETCH + IXSCAN plans with hashed indexes.
|
||||
TEST_F(EngineSelectionPlanFixture, HashedIndexIxScanEligibility) {
|
||||
// Hashed index containing the SERVER-99889 pattern.
|
||||
{
|
||||
BSONObj indexFields = fromjson("{a: 1, m: 'hashed', 'm.m1': 1}");
|
||||
std::unique_ptr<QuerySolution> solution = makeIndexScanFetchPlan(indexFields);
|
||||
ASSERT_FALSE(isPlanSbeEligible(solution.get()));
|
||||
}
|
||||
|
||||
// Single hashed index.
|
||||
{
|
||||
BSONObj indexFields = fromjson("{a: 'hashed'}");
|
||||
std::unique_ptr<QuerySolution> solution = makeIndexScanFetchPlan(indexFields);
|
||||
ASSERT_TRUE(isPlanSbeEligible(solution.get()));
|
||||
}
|
||||
}
|
||||
|
||||
// Test selection of FETCH + IXSCAN plans.
|
||||
TEST_F(EngineSelectionPlanFixture, FetchIxScanSelection) {
|
||||
BSONObj indexFields = fromjson("{a: 1}");
|
||||
|
||||
std::unique_ptr<QuerySolution> solution = makeIndexScanFetchPlan(indexFields);
|
||||
ASSERT_EQ(engineSelectionForPlan(solution.get()), EngineChoice::kClassic);
|
||||
}
|
||||
|
||||
} // namespace mongo
|
||||
|
||||
Loading…
Reference in New Issue
Block a user