summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlya Berciu <alya.berciu@mongodb.com>2023-03-27 12:53:53 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-03-27 14:30:48 +0000
commitc9ddb90ef8e0b6cdac2b13fbd58d34b5d312b132 (patch)
tree5db2363c46ffdbb88313684f0a1141031075b49a
parentc2670522b84041a51724f7fec6f03c605aaf3d6a (diff)
downloadmongo-c9ddb90ef8e0b6cdac2b13fbd58d34b5d312b132.tar.gz
SERVER-75169 Disallow OR of IXSCAN on ineligible CWI
-rw-r--r--jstests/core/index/wildcard/compound_wildcard_index_or.js69
-rw-r--r--src/mongo/db/query/planner_access.cpp32
-rw-r--r--src/mongo/db/query/planner_wildcard_helpers.cpp55
-rw-r--r--src/mongo/db/query/planner_wildcard_helpers.h12
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