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:
Felipe Farinon 2026-03-04 17:43:57 -05:00 committed by MongoDB Bot
parent 31c8b232b7
commit 0b50ea103e
9 changed files with 245 additions and 104 deletions

View File

@ -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",
],
)

View File

@ -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",
],
)

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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