SERVER-83215 Add generic logic to support fast paths that bypass the optimizer

This commit is contained in:
henrinikku 2023-11-22 17:51:45 +00:00 committed by Evergreen Agent
parent dd8ecb087a
commit 7ecd6bd0fa
13 changed files with 882 additions and 9 deletions

View File

@ -0,0 +1,81 @@
/**
* This test verifies that the optimizer fast path is not used for queries that would otherwise be
* eligible but contain operations the current fast path implementations don't support (e.g. a
* projection).
*/
import {planHasStage} from "jstests/libs/analyze_plan.js";
import {
checkCascadesOptimizerEnabled,
checkFastPathEnabled,
} from "jstests/libs/optimizer_utils.js";
if (!checkCascadesOptimizerEnabled(db)) {
jsTestLog("Skipping test because the optimizer is not enabled");
quit();
}
if (!checkFastPathEnabled(db)) {
jsTestLog("Skipping test because fast paths are not enabled");
quit();
}
function assertNotUsingFastPath(explainCmd) {
const explain = assert.commandWorked(explainCmd);
assert(!planHasStage(db, explain, "FASTPATH"));
}
const coll = db.non_eligible_queries;
coll.drop();
{
// Empty find with a projection should not use fast path.
const explain = coll.explain().find({}, {b: 1}).finish();
assertNotUsingFastPath(explain);
}
{
// Empty find with a sort spec should not use fast path.
const explain = coll.explain().find({}).sort({b: 1}).finish();
assertNotUsingFastPath(explain);
}
{
// Empty find with limit should not use fast path.
const explain = coll.explain().find({}).limit(3).finish();
assertNotUsingFastPath(explain);
}
{
// Empty find with skip should not use fast path.
const explain = coll.explain().find({}).skip(3).finish();
assertNotUsingFastPath(explain);
}
{
// Pipeline with $project should not use fast path.
let explain = coll.explain().aggregate([{$match: {}}, {$project: {a: 1}}]);
assertNotUsingFastPath(explain);
explain = coll.explain().aggregate([{$project: {a: 1}}]);
assertNotUsingFastPath(explain);
}
{
// Pipeline with $sort should not use fast path.
let explain = coll.explain().aggregate([{$match: {}}, {$sort: {a: 1}}]);
assertNotUsingFastPath(explain);
explain = coll.explain().aggregate([{$sort: {a: 1}}]);
assertNotUsingFastPath(explain);
}
{
// Pipeline with $limit should not use fast path.
let explain = coll.explain().aggregate([{$match: {}}, {$limit: 3}]);
assertNotUsingFastPath(explain);
explain = coll.explain().aggregate([{$limit: 3}]);
assertNotUsingFastPath(explain);
}
{
// Pipeline with $skip should not use fast path.
let explain = coll.explain().aggregate([{$match: {}}, {$skip: 3}]);
assertNotUsingFastPath(explain);
explain = coll.explain().aggregate([{$skip: 3}]);
assertNotUsingFastPath(explain);
}

View File

@ -15,6 +15,13 @@ export function checkPlanCacheParameterization(theDB) {
return false;
}
export function checkFastPathEnabled(theDB) {
const isDisabled =
theDB.adminCommand({getParameter: 1, internalCascadesOptimizerDisableFastPath: 1})
.internalCascadesOptimizerDisableFastPath;
return !isDisabled;
}
/**
* Utility for checking if the experimental Cascades optimizer code path is enabled (checks
* framework control for M4+).

View File

@ -0,0 +1,61 @@
/**
* This test verifies that the optimizer fast path is not used for queries against sharded
* collections.
*/
import {planHasStage} from "jstests/libs/analyze_plan.js";
const bonsaiSettings = {
internalQueryFrameworkControl: "tryBonsai",
featureFlagCommonQueryFramework: true,
// TODO SERVER-80582: Uncomment this setting. Some sharding code calls empty find on an
// unsharded collection internally, which causes this test to fail as that fast path is not
// implemented yet. internalCascadesOptimizerDisableFastPath: false,
};
const st = new ShardingTest({
shards: 2,
mongos: 1,
other: {
shardOptions: {
setParameter: {
...bonsaiSettings,
"failpoint.enableExplainInBonsai": tojson({mode: "alwaysOn"}),
}
},
mongosOptions: {setParameter: {...bonsaiSettings}},
}
});
const db = st.getDB("test");
const coll = db[jsTestName()];
coll.drop();
assert.commandWorked(coll.insertMany([...Array(100).keys()].map(i => {
return {_id: i, a: 1};
})));
st.shardColl(coll.getName(), {_id: 1}, {_id: 50}, {_id: 51});
function assertNotUsingFastPath(explainCmd) {
const explain = assert.commandWorked(explainCmd);
assert(!planHasStage(db, explain, "FASTPATH"));
}
{
// Empty find on a sharded collection should not use fast path.
const explain = coll.explain().find().finish();
assertNotUsingFastPath(explain);
}
{
// Pipeline with empty match on a sharded collection should not use fast path.
const explain = coll.explain().aggregate([{$match: {}}]);
assertNotUsingFastPath(explain);
}
{
// Empty aggregate on a sharded collection should not use fast path.
const explain = coll.explain().aggregate([]);
assertNotUsingFastPath(explain);
}
st.stop();

View File

@ -1576,6 +1576,8 @@ execEnv.Library(
'query/classic_stage_builder.cpp',
'query/cost_model/on_coefficients_change_updater_impl.cpp',
'query/cqf_command_utils.cpp',
'query/cqf_fast_paths.cpp',
'query/cqf_fast_paths_utils.cpp',
'query/cqf_get_executor.cpp',
'query/explain.cpp',
'query/find.cpp',

View File

@ -100,6 +100,7 @@
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/query/collection_query_info.h"
#include "mongo/db/query/cqf_command_utils.h"
#include "mongo/db/query/cqf_fast_paths.h"
#include "mongo/db/query/cqf_get_executor.h"
#include "mongo/db/query/cursor_response.h"
#include "mongo/db/query/explain.h"
@ -1436,13 +1437,28 @@ Status runAggregate(OperationContext* opCtx,
optimizer::QueryHints queryHints = getHintsFromQueryKnobs();
const bool fastIndexNullHandling = queryHints._fastIndexNullHandling;
auto timeBegin = Date_t::now();
auto maybeExec = getSBEExecutorViaCascadesOptimizer(opCtx,
expCtx,
nss,
collections,
std::move(queryHints),
request.getHint(),
pipeline.get());
auto maybeExec = [&] {
// If the query is eligible for a fast path, use the fast path plan instead of
// invoking the optimizer.
if (auto fastPathExec = optimizer::fast_path::tryGetSBEExecutorViaFastPath(
opCtx,
expCtx,
nss,
collections,
request.getExplain().has_value(),
request.getHint().has_value(),
pipeline.get())) {
return fastPathExec;
}
return getSBEExecutorViaCascadesOptimizer(opCtx,
expCtx,
nss,
collections,
std::move(queryHints),
request.getHint(),
pipeline.get());
}();
if (maybeExec) {
// Pass ownership of the pipeline to the executor. This is done to allow binding of
// parameters to use views onto the constants living in the MatchExpression (in the

View File

@ -449,6 +449,7 @@ env.CppUnitTest(
"ce_mode_parameter_test.cpp",
"classic_stage_builder_test.cpp",
"count_command_test.cpp",
"cqf_fast_paths_test.cpp",
"cursor_response_test.cpp",
"query_shape/distinct_cmd_shape_test.cpp",
"query_shape/find_cmd_shape_test.cpp",

View File

@ -0,0 +1,265 @@
/**
* Copyright (C) 2023-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/cqf_fast_paths.h"
#include "mongo/db/query/cqf_command_utils.h"
#include "mongo/db/query/cqf_fast_paths_utils.h"
#include "mongo/db/query/plan_yield_policy_sbe.h"
#include "mongo/db/query/query_planner_params.h"
#include "mongo/util/assert_util.h"
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
namespace mongo::optimizer::fast_path {
namespace {
/**
* Holds all information required to construct SBE plans for queries that have a fast path
* implementation.
*/
struct ExecTreeGeneratorParams {
const UUID collectionUuid;
PlanYieldPolicy* yieldPolicy;
const BSONObj& filter;
};
using ExecTreeResult = std::pair<std::unique_ptr<sbe::PlanStage>, stage_builder::PlanStageData>;
/**
* Interface for implementing a fast path for a simple query of certain shape. Responsible for SBE
* plan generation.
*/
class ExecTreeGenerator {
public:
virtual ~ExecTreeGenerator() = default;
virtual ExecTreeResult generateExecTree(const ExecTreeGeneratorParams& params) const = 0;
virtual BSONObj generateExplain() const = 0;
};
/**
* ABTPrinter implementation that passes on the given explain BSON. Used for implementing
* explain for fast paths.
*/
class FastPathPrinter : public AbstractABTPrinter {
public:
FastPathPrinter(BSONObj fastPathExplain) : _explainBSON(std::move(fastPathExplain)) {}
BSONObj explainBSON() const override final {
return _explainBSON;
}
std::string getPlanSummary() const override final {
return "COLLSCAN";
}
private:
const BSONObj _explainBSON;
};
// We can use a BSON object representing the filter for fast and simple comparison.
using FastPathMap = FilterComparator::Map<std::unique_ptr<ExecTreeGenerator>>;
FastPathMap fastPathMap{FilterComparator::kInstance.makeLessThan()};
/**
* Do not call this method directly. Instead, use the REGISTER_FAST_PATH macro defined in this
* file.
*/
void registerExecTreeGenerator(BSONObj shape,
std::unique_ptr<ExecTreeGenerator> execTreeGenerator) {
tassert(8321506,
"Did not expect 'shape' to contain '_id' field or a dotted path",
!containsSpecialField(shape));
fastPathMap.insert({shape, std::move(execTreeGenerator)});
}
bool canUseFastPath(const bool hasIndexHint,
const MultipleCollectionAccessor& collections,
const CanonicalQuery* canonicalQuery,
const Pipeline* pipeline) {
if (internalCascadesOptimizerDisableFastPath.load()) {
return false;
}
if (internalQueryDefaultDOP.load() > 1) {
// The current fast path implementations don't support parallel scan plans.
return false;
}
if (hasIndexHint) {
// The current fast path implementations only deal with collection scans.
return false;
}
if (canonicalQuery) {
const auto& findRequest = canonicalQuery->getFindCommandRequest();
if (canonicalQuery->getProj() || canonicalQuery->getSortPattern() ||
findRequest.getLimit() || findRequest.getSkip()) {
// The current fast path implementations don't support
// projections or sorting.
return false;
}
}
if (pipeline) {
const auto& sources = pipeline->getSources();
if (sources.size() > 1 ||
(sources.size() == 1 &&
dynamic_cast<DocumentSourceMatch*>(sources.front().get()) == nullptr)) {
// The current fast path implementations only support queries containing a single filter
// with a simple predicate.
return false;
}
}
if (!collections.getMainCollection()) {
// TODO SERVER-83267: Enable once we have a fast path for non-existent collections.
return false;
}
const bool isSharded = collections.isAcquisition()
? collections.getMainAcquisition().getShardingDescription().isSharded()
: collections.getMainCollection().isSharded_DEPRECATED();
if (isSharded) {
// The current fast path implementations don't support shard filtering.
return false;
}
return true;
}
BSONObj extractQueryFilter(const CanonicalQuery* canonicalQuery, const Pipeline* pipeline) {
if (canonicalQuery) {
return canonicalQuery->getQueryObj();
}
if (pipeline) {
return pipeline->getInitialQuery();
}
tasserted(8217100, "Expected canonicalQuery or pipeline.");
}
const ExecTreeGenerator* getFastPathExecTreeGenerator(const BSONObj& filter) {
auto generatorIt = fastPathMap.find(filter);
if (generatorIt == fastPathMap.end()) {
OPTIMIZER_DEBUG_LOG(
8321501, 5, "Query not eligible for a fast path.", "query"_attr = filter.toString());
return {};
}
OPTIMIZER_DEBUG_LOG(
8321502, 5, "Using a fast path for query", "query"_attr = filter.toString());
return generatorIt->second.get();
}
/**
* Implements fast path SBE plan generation for an empty query.
*/
class EmptyQueryExecTreeGenerator : public ExecTreeGenerator {
public:
ExecTreeResult generateExecTree(const ExecTreeGeneratorParams& params) const override {
// TODO SERVER-80582
MONGO_UNIMPLEMENTED_TASSERT(8321504);
}
virtual BSONObj generateExplain() const override {
// TODO SERVER-80582
MONGO_UNIMPLEMENTED_TASSERT(8321505);
}
};
REGISTER_FAST_PATH_EXEC_TREE_GENERATOR(Empty, {}, std::make_unique<EmptyQueryExecTreeGenerator>());
} // namespace
boost::optional<ExecParams> tryGetSBEExecutorViaFastPath(
OperationContext* opCtx,
boost::intrusive_ptr<ExpressionContext> expCtx,
const NamespaceString& nss,
const MultipleCollectionAccessor& collections,
const bool hasExplain,
const bool hasIndexHint,
const Pipeline* pipeline,
const CanonicalQuery* canonicalQuery) {
if (!canUseFastPath(hasIndexHint, collections, canonicalQuery, pipeline)) {
return {};
}
const auto filter = extractQueryFilter(canonicalQuery, pipeline);
auto generator = getFastPathExecTreeGenerator(filter);
if (!generator) {
return {};
}
std::unique_ptr<PlanYieldPolicySBE> sbeYieldPolicy =
PlanYieldPolicySBE::make(opCtx, PlanYieldPolicy::YieldPolicy::YIELD_AUTO, collections, nss);
ExecTreeGeneratorParams params{
collections.getMainCollection()->uuid(), sbeYieldPolicy.get(), filter};
auto [sbePlan, data] = generator->generateExecTree(params);
{
sbe::DebugPrinter p;
OPTIMIZER_DEBUG_LOG(6264802, 5, "Lowered SBE plan", "plan"_attr = p.print(*sbePlan.get()));
}
sbePlan->attachToOperationContext(opCtx);
if (expCtx->mayDbProfile) {
sbePlan->markShouldCollectTimingInfo();
}
auto explain = hasExplain ? generator->generateExplain() : BSONObj{};
sbePlan->prepare(data.env.ctx);
CurOp::get(opCtx)->stopQueryPlanningTimer();
return {{opCtx,
nullptr /*solution*/,
{std::move(sbePlan), std::move(data)},
std::make_unique<FastPathPrinter>(std::move(explain)),
QueryPlannerParams::Options::DEFAULT,
nss,
std::move(sbeYieldPolicy),
false /*isFromPlanCache*/,
true /* generatedByBonsai */,
nullptr /*pipelineMatchExpr*/}};
}
boost::optional<ExecParams> tryGetSBEExecutorViaFastPath(
const MultipleCollectionAccessor& collections, const CanonicalQuery* query) {
auto hasIndexHint = !query->getFindCommandRequest().getHint().isEmpty();
return tryGetSBEExecutorViaFastPath(query->getOpCtx(),
query->getExpCtx(),
query->nss(),
collections,
query->getExplain(),
hasIndexHint,
nullptr /*pipeline*/,
query);
}
} // namespace mongo::optimizer::fast_path

View File

@ -0,0 +1,75 @@
/**
* Copyright (C) 2023-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 <boost/optional/optional.hpp>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <cstddef>
#include "mongo/base/init.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/pipeline/pipeline.h"
#include "mongo/db/query/canonical_query.h"
#include "mongo/db/query/cqf_get_executor.h"
#include "mongo/db/query/multiple_collection_accessor.h"
namespace mongo::optimizer::fast_path {
#define REGISTER_FAST_PATH_EXEC_TREE_GENERATOR(key, pattern, generator) \
MONGO_INITIALIZER(addFastPath_##key) \
(InitializerContext*) { \
registerExecTreeGenerator(pattern, generator); \
}
/**
* Returns the arguments to create a PlanExecutor for the given CanonicalQuery.
*/
boost::optional<ExecParams> tryGetSBEExecutorViaFastPath(
const MultipleCollectionAccessor& collections, const CanonicalQuery* query);
/**
* Returns the arguments to create a PlanExecutor for the given Pipeline.
*
* The CanonicalQuery parameter allows for code reuse between functions in this file and should not
* be set by callers.
*/
boost::optional<ExecParams> tryGetSBEExecutorViaFastPath(
OperationContext* opCtx,
boost::intrusive_ptr<ExpressionContext> expCtx,
const NamespaceString& nss,
const MultipleCollectionAccessor& collections,
bool hasExplain,
bool hasIndexHint,
const Pipeline* pipeline,
const CanonicalQuery* canonicalQuery = nullptr);
} // namespace mongo::optimizer::fast_path

View File

@ -0,0 +1,148 @@
/**
* Copyright (C) 2023-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 <memory>
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/json.h"
#include "mongo/db/query/cqf_fast_paths_utils.h"
#include "mongo/unittest/assert.h"
#include "mongo/unittest/framework.h"
namespace mongo::optimizer::fast_path {
namespace {
bool filterMatchesPattern(BSONObj& filter, BSONObj& pattern) {
return FilterComparator::kInstance.compare(filter, pattern) == 0;
}
TEST(CqfFastPathsTest, FilterComparatorMatchesEqOnTopLevelField) {
auto filter = fromjson("{a: \"123\"}");
auto pattern = BSON("ignored" << 0);
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
filter = fromjson("{a: {\"$eq\": \"123\"}}");
pattern = BSON("ignored" << BSON("$eq" << 0));
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorMatchesEqObj) {
auto filter = fromjson("{a: {\"$eq\": {b: \"123\", c: 123}}}");
auto pattern = BSON("ignored" << BSON("$eq" << 0));
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchEqFiltersOfDifferentShapes) {
auto filter = fromjson("{a: \"123\"}");
auto pattern = BSON("ignored" << BSON("$eq" << 0));
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
filter = fromjson("{a: {\"$eq\": \"123\"}}");
pattern = BSON("ignored" << 0);
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorMatchesLtOnTopLevelField) {
auto filter = fromjson("{a: {\"$lt\": \"123\"}}");
auto pattern = BSON("ignored" << BSON("$lt" << 0));
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchLtOnNestedField) {
auto filter = fromjson("{\"a.b.c\": {\"$lt\": \"123\"}}");
auto pattern = BSON("ignored" << BSON("$lt" << 0));
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
filter = fromjson("{a: {b: {c: {\"$lt\": \"123\"}}}}");
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchDifferentOps) {
auto filter = fromjson("{a: {\"$eq\": \"123\"}}");
auto pattern = BSON("ignored" << BSON("$lt" << 0));
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
filter = fromjson("{a: {\"$gt\": \"123\"}}");
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchSameOpsWithDifferentSubExprs) {
auto filter = fromjson(R"({a: {$eq: {$concat: ["1", "2"]}}})");
auto pattern = BSON("ignored" << BSON("$eq" << 0));
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
filter = fromjson(R"({a: {$eq: "$field"}})");
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchSingleOpWithConjunction) {
auto filter = fromjson(R"({ignored: {$lt: 5}, b: {$gt: 5}})");
auto pattern = BSON("ignored" << BSON("$lt" << 0));
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorMatchesEmptyWithEmpty) {
BSONObj filter{};
BSONObj pattern{};
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchEmptyWithNonEmpty) {
BSONObj filter{};
auto pattern = BSON("ignored" << 0);
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorDoesNotMatchEquivalentConjunctionsInDifferentOrder) {
// This test illustrates a limitation of the comparator where the ordering of predicates
// matters.
auto filter = fromjson(R"({f1: {$gt: 5}, f2: {$lt: 10}})");
auto pattern = fromjson(R"({ignore: {$lt: 10}, ignore: {$gt: 5}})");
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
filter = fromjson(R"({ignore: {$lt: 1, $gt: 1}})");
pattern = fromjson(R"({f1: {$gt: 1, $lt: 1}})");
ASSERT_FALSE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorMatchesConstantContainingSpecialFields) {
auto filter = fromjson(R"({ignored: {$eq: {_id: 1, "a.b.c": 2}}})");
auto pattern = BSON("ignored" << BSON("$eq" << 0));
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
TEST(CqfFastPathsTest, FilterComparatorMatchesConjunctionWithDifferentConstants) {
auto filter = fromjson(R"({a: {$lt: {x: 10}, $gt: {y: 50}}})");
auto pattern = fromjson(R"({ignored: {$lt: 0, $gt: 0}})");
ASSERT_TRUE(filterMatchesPattern(filter, pattern));
}
} // namespace
} // namespace mongo::optimizer::fast_path

View File

@ -0,0 +1,125 @@
/**
* Copyright (C) 2023-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/cqf_fast_paths_utils.h"
namespace mongo::optimizer::fast_path {
namespace {
bool isSpecialField(const StringData& field) {
return field == "_id" || field.find(".") != std::string::npos;
}
bool isExpression(const StringData& field) {
return !field.empty() && field[0] == '$';
}
} // namespace
const FilterComparator FilterComparator::kInstance{};
int FilterComparator::compare(const BSONObj& lhs, const BSONObj& rhs) const {
for (BSONObjIterator lhsIt{lhs}, rhsIt{rhs};;) {
const auto& lhsChild = lhsIt.next();
const auto& rhsChild = rhsIt.next();
if (lhsChild.eoo() && rhsChild.eoo()) {
return 0;
}
if (lhsChild.eoo()) {
return -1;
}
if (rhsChild.eoo()) {
return 1;
}
const auto& lhsField = lhsChild.fieldNameStringData();
const auto& rhsField = rhsChild.fieldNameStringData();
if (isSpecialField(lhsField) || isSpecialField(rhsField)) {
// We currently don't want to match predicates on the '_id' field or a dotted field
// (e.g. '{"a.b.c": 1}').
return lhsChild.woCompare(rhsChild);
}
if (isExpression(lhsField) || isExpression(rhsField)) {
// Ensure we have the same expression on both sides.
if (auto diff = lhsField.compare(rhsField)) {
return diff;
}
if (containsExpression(lhsChild) || containsExpression(rhsChild)) {
// We have nested expressions, do a strict comparison. Note that while we don't
// currently want to match nested expressions, the comparator still needs to
// implement a strict weak ordering between predicates.
return lhsChild.woCompare(rhsChild);
}
} else if (lhsChild.isABSONObj() && rhsChild.isABSONObj()) {
if (auto diff = compare(lhsChild.Obj(), rhsChild.Obj())) {
return diff;
}
} else if (lhsChild.isABSONObj() || rhsChild.isABSONObj()) {
// Different shape before reaching an expression.
return lhsChild.woCompare(rhsChild);
}
// Non-object constants are treated as equal.
}
MONGO_UNREACHABLE_TASSERT(8321500);
}
bool FilterComparator::containsExpression(const BSONObj& obj) const {
for (auto&& elem : obj) {
if (isExpression(elem.fieldNameStringData())) {
return true;
}
if (containsExpression(elem)) {
return true;
}
}
return false;
}
bool FilterComparator::containsExpression(const BSONElement& elem) const {
return elem.valueStringDataSafe().startsWith("$") ||
(elem.isABSONObj() && containsExpression(elem.Obj()));
}
bool containsSpecialField(const BSONObj& obj) {
for (auto&& elem : obj) {
if (isSpecialField(elem.fieldNameStringData())) {
return true;
}
if (elem.isABSONObj() && containsSpecialField(elem.Obj())) {
return true;
}
}
return false;
}
} // namespace mongo::optimizer::fast_path

View File

@ -0,0 +1,73 @@
/**
* Copyright (C) 2023-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/base/string_data.h"
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobj.h"
#include "mongo/bson/bsonobj_comparator_interface.h"
#include "mongo/util/assert_util.h"
namespace mongo::optimizer::fast_path {
/**
* Custom comparator for checking if a query is eligible for a fast path.
*
* Compares shape while ignoring field names until an expression is reached. After reaching an
* expression reached, all constant values are considered equal.
*
* Note that this is not a generic comparator. We are currently disregarding the following
* limitations intentionally because we only need to match queries with at most a single predicate
* on a top-level field:
* - The ordering of the predicates matters.
* - Nested expressions always result in a strict comparison.
* - Predicates on the '_id' field or a dotted path always result in a strict comparison.
*/
struct FilterComparator final : public BSONObj::ComparatorInterface {
static const FilterComparator kInstance;
int compare(const BSONObj& left, const BSONObj& right) const final;
void hash_combine(size_t& seed, const BSONObj& toHash) const final {
MONGO_UNREACHABLE_TASSERT(8321503);
}
private:
bool containsExpression(const BSONObj& obj) const;
bool containsExpression(const BSONElement& elem) const;
};
/**
* Checks whether the given BSONObj contains the '_id' field or a dotted path.
*/
bool containsSpecialField(const BSONObj& obj);
} // namespace mongo::optimizer::fast_path

View File

@ -113,6 +113,7 @@
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/query/collection_query_info.h"
#include "mongo/db/query/cqf_command_utils.h"
#include "mongo/db/query/cqf_fast_paths.h"
#include "mongo/db/query/cqf_get_executor.h"
#include "mongo/db/query/find_command.h"
#include "mongo/db/query/index_bounds.h"
@ -1719,8 +1720,18 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> getExecutor(
if (isEligibleForBonsai(*canonicalQuery, opCtx, mainColl)) {
optimizer::QueryHints queryHints = getHintsFromQueryKnobs();
const bool fastIndexNullHandling = queryHints._fastIndexNullHandling;
auto maybeExec = getSBEExecutorViaCascadesOptimizer(
collections, std::move(queryHints), canonicalQuery.get());
auto maybeExec = [&] {
// If the query is eligible for a fast path, use the fast path plan instead of
// invoking the optimizer.
if (auto fastPathExec = optimizer::fast_path::tryGetSBEExecutorViaFastPath(
collections, canonicalQuery.get())) {
return fastPathExec;
}
return getSBEExecutorViaCascadesOptimizer(
collections, std::move(queryHints), canonicalQuery.get());
}();
if (maybeExec) {
auto exec = uassertStatusOK(makeExecFromParams(
std::move(canonicalQuery), nullptr /*pipeline*/, std::move(*maybeExec)));

View File

@ -956,6 +956,14 @@ server_parameters:
cpp_vartype: AtomicWord<bool>
default: true
internalCascadesOptimizerDisableFastPath:
description: "Disables fast paths that shortcut the optimizer for some trivial queries."
set_at: [ startup, runtime ]
cpp_varname: "internalCascadesOptimizerDisableFastPath"
cpp_vartype: AtomicWord<bool>
default: true
test_only: true
internalQueryFrameworkControl:
description: "Knob to control the optimizer/execution engine to use."
set_at: [ startup, runtime ]