diff options
author | Alya Berciu <alya.berciu@mongodb.com> | 2023-03-27 12:53:53 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2023-03-27 14:30:48 +0000 |
commit | c9ddb90ef8e0b6cdac2b13fbd58d34b5d312b132 (patch) | |
tree | 5db2363c46ffdbb88313684f0a1141031075b49a | |
parent | c2670522b84041a51724f7fec6f03c605aaf3d6a (diff) | |
download | mongo-c9ddb90ef8e0b6cdac2b13fbd58d34b5d312b132.tar.gz |
SERVER-75169 Disallow OR of IXSCAN on ineligible CWI
-rw-r--r-- | jstests/core/index/wildcard/compound_wildcard_index_or.js | 69 | ||||
-rw-r--r-- | src/mongo/db/query/planner_access.cpp | 32 | ||||
-rw-r--r-- | src/mongo/db/query/planner_wildcard_helpers.cpp | 55 | ||||
-rw-r--r-- | src/mongo/db/query/planner_wildcard_helpers.h | 12 |
4 files changed, 131 insertions, 37 deletions
diff --git a/jstests/core/index/wildcard/compound_wildcard_index_or.js b/jstests/core/index/wildcard/compound_wildcard_index_or.js new file mode 100644 index 00000000000..874d01e4fb6 --- /dev/null +++ b/jstests/core/index/wildcard/compound_wildcard_index_or.js @@ -0,0 +1,69 @@ +/** + * Tests that compound wildcard indexes with queries on non-wildcard prefix are not scanned in OR + * queries. + * + * @tags: [ + * assumes_against_mongod_not_mongos, + * assumes_read_concern_local, + * assumes_balancer_off, + * does_not_support_stepdowns, + * featureFlagCompoundWildcardIndexes, + * requires_fcv_70, + * ] + */ +(function() { +"use strict"; + +load("jstests/aggregation/extras/utils.js"); // For arrayEq(). +load("jstests/libs/wildcard_index_helpers.js"); // For WildcardIndexHelpers. + +const documentList = [ + { + _id: 428, + "str": "Chicken RAM Nepal", + }, + { + _id: 1311, + "str": "navigate Stravenue", + "obj": { + _id: 1313, + "obj": { + _id: 1314, + "obj": { + _id: 1315, + "obj": { + _id: 1316, + "obj": {}, + }, + }, + }, + }, + }, +]; + +const pipeline = + [{$match: {$or: [{"str": {$regex: /^Chicken/}}, {"obj.obj.obj.obj.obj": {$exists: true}}]}}]; + +const indexList = [{"obj.obj.obj.$**": 1}, {"str": -1, "obj.obj.obj.obj.$**": -1}]; + +const coll = db[jsTestName() + "_wild"]; +const wild = db[jsTestName() + "_no_idx"]; + +coll.drop(); +wild.drop(); + +assert.commandWorked(coll.insertMany(documentList)); +assert.commandWorked(wild.insertMany(documentList)); + +assert.commandWorked(wild.createIndexes(indexList)); + +const noIdxResult = coll.aggregate(pipeline).toArray(); +const idxResult = wild.aggregate(pipeline).toArray(); + +assertArrayEq({expected: documentList, actual: noIdxResult}); +assertArrayEq({expected: noIdxResult, actual: idxResult}); + +// We want to make sure we do not use an IXSCAN to answer the ineligible prefix. +const explain = assert.commandWorked(wild.explain('executionStats').aggregate(pipeline)); +WildcardIndexHelpers.assertExpectedIndexIsNotUsed(explain, "str_-1_obj.obj.obj.obj.$**_-1"); +})(); diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp index 95b014ddf0b..ed4b7ebcd91 100644 --- a/src/mongo/db/query/planner_access.cpp +++ b/src/mongo/db/query/planner_access.cpp @@ -1497,20 +1497,16 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::buildIndexedAnd( // Short-circuit: an AND of one child is just the child. if (ixscanNodes.size() == 1) { - if (feature_flags::gFeatureFlagCompoundWildcardIndexes.isEnabledAndIgnoreFCV() && - ixscanNodes[0]->getType() == StageType::STAGE_IXSCAN && root->numChildren() > 0) { - const auto* ixScanNode = static_cast<IndexScanNode*>(ixscanNodes[0].get()); - const auto& index = ixScanNode->index; - if (index.type == INDEX_WILDCARD && - wildcard_planning::canOnlyAnswerWildcardPrefixQuery(index, ixScanNode->bounds)) { - // If we get here, we have a compound wildcard index which can answer one or more of - // the predicates in the $and, but we also have at least one additional node - // attached to the filter. Normally, we would be able to satisfy this case using a - // FETCH + FILTER + IXSCAN; however, in the case of a $not query which is not - // supported by the index, the index entry will be expanded in such a way that we - // won't be able to satisfy the query. - return nullptr; - } + if (root->numChildren() > 0 && + feature_flags::gFeatureFlagCompoundWildcardIndexes.isEnabledAndIgnoreFCV() && + wildcard_planning::canOnlyAnswerWildcardPrefixQuery(ixscanNodes)) { + // If we get here, we have a compound wildcard index which can answer one or more of the + // predicates in the $and, but we also have at least one additional node attached to the + // filter. Normally, we would be able to satisfy this case using a FETCH + FILTER + + // IXSCAN; however, in the case of a $not query which is not supported by the index, the + // index entry will be expanded in such a way that we won't be able to satisfy the + // query. + return nullptr; } andResult = std::move(ixscanNodes[0]); } else { @@ -1643,6 +1639,14 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::buildIndexedOr( } else { std::vector<bool> shouldReverseScan; + if (feature_flags::gFeatureFlagCompoundWildcardIndexes.isEnabledAndIgnoreFCV() && + wildcard_planning::canOnlyAnswerWildcardPrefixQuery(ixscanNodes)) { + // If we get here, we have a an OR of IXSCANs, one of which is a compound wildcard + // index, but at least one of them can only support a FETCH + IXSCAN on queries on the + // prefix. This means this plan will produce incorrect results. + return nullptr; + } + if (query.getSortPattern()) { // If all ixscanNodes can provide the sort, shouldReverseScan is populated with which // scans to reverse. diff --git a/src/mongo/db/query/planner_wildcard_helpers.cpp b/src/mongo/db/query/planner_wildcard_helpers.cpp index 77e89356cd6..662d3c26792 100644 --- a/src/mongo/db/query/planner_wildcard_helpers.cpp +++ b/src/mongo/db/query/planner_wildcard_helpers.cpp @@ -464,6 +464,33 @@ boost::optional<IndexEntry> createExpandedIndexEntry(const IndexEntry& wildcardI wildcardFieldPos); return entry; } + +/** + * Determines if an expanded index entry can satisfy a query on a wildcard field with a FETCH + * (for e.g., it may only be able to answer a query on the prefix if the wildcard field is being + * queried with an incompatible $not predicate). + * + * Note: we could just use 'index.keyPattern' here for this check, but then we would have to iterate + * through the entire pattern to get to the field at 'wildcardPos'. + */ +bool canOnlyAnswerWildcardPrefixQuery(const IndexEntry& index, const IndexBounds& bounds) { + tassert(7444000, "Expected a wildcard index.", index.type == INDEX_WILDCARD); + tassert(7444001, + "A wildcard index should always have a virtual $_path field at wildcardFieldPos - 1.", + bounds.fields[index.wildcardFieldPos - 1].name == "$_path"_sd); + + if (index.wildcardFieldPos == 1) { + // This is either a single-field wildcard index, or a compound wildcard index without a + // prefix. + return false; + } + + // If the index entry was not expanded to include a second $_path field, we cannot answer a + // query on a wildcard field with an IXSCAN + FETCH if the predicate itself is, for e.g. an + // ineligible $not query, because we won't retrieve documents where the wildcard field is + // missing from the IXSCAN. + return bounds.fields[index.wildcardFieldPos].name != "$_path"_sd; +} } // namespace void expandWildcardIndexEntry(const IndexEntry& wildcardIndex, @@ -548,23 +575,19 @@ void expandWildcardIndexEntry(const IndexEntry& wildcardIndex, } } -bool canOnlyAnswerWildcardPrefixQuery(const IndexEntry& index, const IndexBounds& bounds) { - tassert(7444000, "Expected a wildcard index.", index.type == INDEX_WILDCARD); - tassert(7444001, - "A wildcard index should always have a virtual $_path field at wildcardFieldPos - 1.", - bounds.fields[index.wildcardFieldPos - 1].name == "$_path"_sd); - - if (index.wildcardFieldPos == 1) { - // This is either a single-field wildcard index, or a compound wildcard index without a - // prefix. +bool canOnlyAnswerWildcardPrefixQuery( + const std::vector<std::unique_ptr<QuerySolutionNode>>& ixscanNodes) { + return std::any_of(ixscanNodes.begin(), ixscanNodes.end(), [](const auto& node) { + if (node->getType() == StageType::STAGE_IXSCAN) { + const auto* ixScanNode = static_cast<IndexScanNode*>(node.get()); + const auto& index = ixScanNode->index; + if (index.type == INDEX_WILDCARD && + canOnlyAnswerWildcardPrefixQuery(index, ixScanNode->bounds)) { + return true; + } + } return false; - } - - // If the index entry was not expanded to include a second $_path field, we cannot answer a - // query on a wildcard field with an IXSCAN + FETCH if the predicate itself is, for e.g. an - // ineligible $not query, because we won't retrieve documents where the wildcard field is - // missing from the IXSCAN. - return bounds.fields[index.wildcardFieldPos].name != "$_path"_sd; + }); } BoundsTightness translateWildcardIndexBoundsAndTightness( diff --git a/src/mongo/db/query/planner_wildcard_helpers.h b/src/mongo/db/query/planner_wildcard_helpers.h index de22942d452..b42968cf16f 100644 --- a/src/mongo/db/query/planner_wildcard_helpers.h +++ b/src/mongo/db/query/planner_wildcard_helpers.h @@ -56,14 +56,12 @@ void expandWildcardIndexEntry(const IndexEntry& wildcardIndex, std::vector<IndexEntry>* out); /** - * Determines if an expanded index entry can satisfy a query on a wildcard field with a FETCH - * (for e.g., it may only be able to answer a query on the prefix if the wildcard field is being - * queried with an incompatible $not predicate). - * - * Note: we could just use 'index.keyPattern' here for this check, but then we would have to iterate - * through the entire pattern to get to the field at 'wildcardPos'. + * Determines if any of the expanded index entries in the input 'ixscanNodes' can satisfy a query on + * a wildcard field with a FETCH (for e.g., it may only be able to answer a query on the prefix if + * the wildcard field is being queried with an incompatible $not predicate). */ -bool canOnlyAnswerWildcardPrefixQuery(const IndexEntry& index, const IndexBounds& bounds); +bool canOnlyAnswerWildcardPrefixQuery( + const std::vector<std::unique_ptr<QuerySolutionNode>>& ixscanNodes); /** * In certain circumstances, it is necessary to adjust the bounds and tightness generated by the |