diff options
Diffstat (limited to 'src/mongo/db/query/planner_access.cpp')
| -rw-r--r-- | src/mongo/db/query/planner_access.cpp | 320 |
1 files changed, 106 insertions, 214 deletions
diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp index de294ad661a..e05dfd8a5d1 100644 --- a/src/mongo/db/query/planner_access.cpp +++ b/src/mongo/db/query/planner_access.cpp @@ -45,21 +45,14 @@ #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_geo.h" -#include "mongo/db/matcher/expression_internal_expr_comparison.h" -#include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_text.h" -#include "mongo/db/matcher/expression_tree.h" -#include "mongo/db/namespace_string.h" -#include "mongo/db/query/index_bounds.h" #include "mongo/db/query/index_bounds_builder.h" #include "mongo/db/query/index_tag.h" #include "mongo/db/query/indexability.h" -#include "mongo/db/query/parsed_find_command.h" #include "mongo/db/query/planner_wildcard_helpers.h" #include "mongo/db/query/query_knobs_gen.h" #include "mongo/db/query/query_planner.h" #include "mongo/db/query/query_planner_common.h" -#include "mongo/db/query/record_id_range.h" #include "mongo/db/record_id_helpers.h" #include "mongo/logv2/log.h" #include "mongo/util/transitional_tools_do_not_use/vector_spooling.h" @@ -235,37 +228,27 @@ bool affectedByCollator(const BSONElement& element) { } } -// Set 'curr' to 'newMin' if 'newMin' < 'curr' -void setLowestRecord(boost::optional<RecordIdBound>& curr, const RecordIdBound& newMin) { - if (!curr || newMin.recordId() < curr->recordId()) { - curr = newMin; +void setMinRecord(CollectionScanNode* collScan, const BSONObj& min) { + const auto newMinRecord = record_id_helpers::keyForObj(min); + if (!collScan->minRecord || newMinRecord > collScan->minRecord->recordId()) { + collScan->minRecord = RecordIdBound(newMinRecord, min); } } -// Set 'curr' to 'newMax' if 'newMax' > 'curr' -void setHighestRecord(boost::optional<RecordIdBound>& curr, const RecordIdBound& newMax) { - if (!curr || newMax.recordId() > curr->recordId()) { - curr = newMax; +void setMaxRecord(CollectionScanNode* collScan, const BSONObj& max) { + const auto newMaxRecord = record_id_helpers::keyForObj(max); + if (!collScan->maxRecord || newMaxRecord < collScan->maxRecord->recordId()) { + collScan->maxRecord = RecordIdBound(newMaxRecord, max); } } -// Set 'curr' to 'newMin' if 'newMin' < 'curr' -void setLowestRecord(boost::optional<RecordIdBound>& curr, const BSONObj& newMin) { - setLowestRecord(curr, RecordIdBound(record_id_helpers::keyForObj(newMin), newMin)); -} - -// Set 'curr' to 'newMax' if 'newMax' > 'curr' -void setHighestRecord(boost::optional<RecordIdBound>& curr, const BSONObj& newMax) { - setHighestRecord(curr, RecordIdBound(record_id_helpers::keyForObj(newMax), newMax)); -} - // Returns whether element is not affected by collators or query and collection collators are // compatible. bool compatibleCollator(const QueryPlannerParams& params, const CollatorInterface* queryCollator, const BSONElement& element) { auto const collCollator = params.clusteredCollectionCollator; - bool compatible = CollatorInterface::collatorsMatch(queryCollator, collCollator); + bool compatible = !queryCollator || (collCollator && *queryCollator == *collCollator); return compatible || !affectedByCollator(element); } @@ -276,8 +259,7 @@ bool compatibleCollator(const QueryPlannerParams& params, void handleRIDRangeMinMax(const CanonicalQuery& query, CollectionScanNode* collScan, const QueryPlannerParams& params, - const CollatorInterface* collator, - RecordIdRange& recordRange) { + const CollatorInterface* collator) { BSONObj minObj = query.getFindCommandRequest().getMin(); BSONObj maxObj = query.getFindCommandRequest().getMax(); if (minObj.isEmpty() && maxObj.isEmpty()) { @@ -297,170 +279,88 @@ void handleRIDRangeMinMax(const CanonicalQuery& query, if (!maxObj.isEmpty() && compatibleCollator(params, collator, maxObj.firstElement())) { // max() is exclusive. // Assumes clustered collection scans are only supported with the forward direction. - recordRange.maybeNarrowMax( - IndexBoundsBuilder::objFromElement(maxObj.firstElement(), collator), - false /* NOT inclusive*/); + collScan->boundInclusion = + CollectionScanParams::ScanBoundInclusion::kIncludeStartRecordOnly; + setMaxRecord(collScan, IndexBoundsBuilder::objFromElement(maxObj.firstElement(), collator)); } if (!minObj.isEmpty() && compatibleCollator(params, collator, minObj.firstElement())) { // The min() is inclusive as are bounded collection scans by default. - recordRange.maybeNarrowMin( - IndexBoundsBuilder::objFromElement(minObj.firstElement(), collator), - true /* inclusive*/); + setMinRecord(collScan, IndexBoundsBuilder::objFromElement(minObj.firstElement(), collator)); } } /** * Helper function to add an RID range to collection scans. - * If the query solution tree contains a collection scan node with a suitable comparison predicate - * on '_id', we add a minRecord and maxRecord on the collection node. - * - * Returns true if the MatchExpression is a comparison against the cluster key which either: - * 1) is guaranteed to exclude values of the cluster key which are affected by collation or - * 2) may return values of the cluster key which are affected by collation, but the query and - * collection collations match. - * Otherwise, returns false. - * - * For example, assuming the cluster key is "_id": - * Given {a: {$eq: 2}}, we return false, because the comparison is not against the cluster key. - * Given {_id: {$gte: 5}}, we return true, because this comparison against the cluster key excludes - * keys which are affected by collations. - * Given {_id: {$eq: "str"}}, we return true only if the query and collection collations match. - * + * If the query solution tree contains a collection scan node with a suitable comparison + * predicate on '_id', we add a minRecord and maxRecord on the collection node. */ -[[nodiscard]] bool handleRIDRangeScan(const MatchExpression* conjunct, - CollectionScanNode* collScan, - const QueryPlannerParams& params, - const CollatorInterface* collator, - RecordIdRange& recordRange) { +void handleRIDRangeScan(const MatchExpression* conjunct, + CollectionScanNode* collScan, + const QueryPlannerParams& params, + const CollatorInterface* collator) { invariant(params.clusteredInfo); if (conjunct == nullptr) { - return false; + return; } auto* andMatchPtr = dynamic_cast<const AndMatchExpression*>(conjunct); if (andMatchPtr != nullptr) { - bool atLeastOneConjunctCompatibleCollation = false; for (size_t index = 0; index < andMatchPtr->numChildren(); index++) { - if (handleRIDRangeScan( - andMatchPtr->getChild(index), collScan, params, collator, recordRange)) { - atLeastOneConjunctCompatibleCollation = true; - } + handleRIDRangeScan(andMatchPtr->getChild(index), collScan, params, collator); } - - // If one of the conjuncts excludes values of the cluster key which are affected by - // collation, then the entire $and will also exclude those values. - return atLeastOneConjunctCompatibleCollation; + return; } if (conjunct->path() != clustered_util::getClusterKeyFieldName(params.clusteredInfo->getIndexSpec())) { // No match on the cluster key. - return false; - } - - // TODO SERVER-62707: Allow $in with regex to use a clustered index. - auto inMatch = dynamic_cast<const InMatchExpression*>(conjunct); - if (inMatch && !inMatch->hasRegex()) { - // Iterate through the $in equalities to find the min/max values. The min/max bounds for the - // collscan need to be loose enough to cover all of these values. - boost::optional<RecordIdBound> minBound; - boost::optional<RecordIdBound> maxBound; - - bool allEltsCollationCompatible = true; - for (const auto& element : inMatch->getEqualities()) { - if (compatibleCollator(params, collator, element)) { - const auto collated = IndexBoundsBuilder::objFromElement(element, collator); - setLowestRecord(minBound, collated); - setHighestRecord(maxBound, collated); - } else { - // Set coarse min/max bounds based on type when we can't set tight bounds. - allEltsCollationCompatible = false; - - BSONObjBuilder bMin; - bMin.appendMinForType("", element.type()); - setLowestRecord(minBound, bMin.obj()); - - BSONObjBuilder bMax; - bMax.appendMaxForType("", element.type()); - setHighestRecord(maxBound, bMax.obj()); - } - } - - // {min,max}RecordId will bound the range of ids scanned to the highest and lowest present - // in the InMatchExpression, but the filter is still required to filter to _exactly_ the - // requested matches. - - // Finally, tighten the collscan bounds with the min/max bounds for the $in. - recordRange.intersectRange(minBound, maxBound); - return allEltsCollationCompatible; + return; } - auto match = dynamic_cast<const ComparisonMatchExpressionBase*>(conjunct); + auto match = dynamic_cast<const ComparisonMatchExpression*>(conjunct); if (match == nullptr) { - return false; // Not a comparison match expression. + return; // Not a comparison match expression. } const auto& element = match->getData(); - if (!ComparisonMatchExpressionBase::isInternalExprComparison(match->matchType())) { - // Internal comparisons e.g., $_internalExprGt do _not_ carry type bracketing - // semantics (consistent with `$expr{$gt:[a,b]}`). - // For other comparisons which _do_ perform type bracketing, the RecordId bounds - // may be tightened here. - BSONObjBuilder minb; - minb.appendMinForType("", element.type()); - recordRange.maybeNarrowMin(minb.obj(), true /* inclusive */); + // Set coarse min/max bounds based on type in case we can't set tight bounds. + BSONObjBuilder minb; + minb.appendMinForType("", element.type()); + setMinRecord(collScan, minb.obj()); - BSONObjBuilder maxb; - maxb.appendMaxForType("", element.type()); - recordRange.maybeNarrowMax(maxb.obj(), true /* inclusive */); - } + BSONObjBuilder maxb; + maxb.appendMaxForType("", element.type()); + setMaxRecord(collScan, maxb.obj()); bool compatible = compatibleCollator(params, collator, element); if (!compatible) { - return false; // Collator affects probe and it's not compatible with collection's collator. + return; // Collator affects probe and it's not compatible with collection's collator. } // Even if the collations don't match at this point, it's fine, - // because the bounds exclude values that use it. - const BSONObj collated = IndexBoundsBuilder::objFromElement(element, collator); - using MType = MatchExpression::MatchType; - switch (match->matchType()) { - case MType::EQ: - case MType::INTERNAL_EXPR_EQ: - recordRange.maybeNarrowMin(collated, true /* inclusive */); - recordRange.maybeNarrowMax(collated, true /* inclusive */); - break; - case MType::LT: - case MType::INTERNAL_EXPR_LT: - recordRange.maybeNarrowMax(collated, false /* EXclusive */); - break; - case MType::LTE: - case MType::INTERNAL_EXPR_LTE: - recordRange.maybeNarrowMax(collated, true /* inclusive */); - break; - case MType::GT: - case MType::INTERNAL_EXPR_GT: - recordRange.maybeNarrowMin(collated, false /* EXclusive */); - break; - case MType::GTE: - case MType::INTERNAL_EXPR_GTE: - recordRange.maybeNarrowMin(collated, true /* inclusive */); - break; - default:; + // because the bounds exclude values that use it + collScan->hasCompatibleCollation = true; + + const auto collated = IndexBoundsBuilder::objFromElement(element, collator); + if (dynamic_cast<const EqualityMatchExpression*>(match)) { + setMinRecord(collScan, collated); + setMaxRecord(collScan, collated); + } else if (dynamic_cast<const LTMatchExpression*>(match) || + dynamic_cast<const LTEMatchExpression*>(match)) { + setMaxRecord(collScan, collated); + } else if (dynamic_cast<const GTMatchExpression*>(match) || + dynamic_cast<const GTEMatchExpression*>(match)) { + setMinRecord(collScan, collated); } - return true; } } // namespace std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::makeCollectionScan( const CanonicalQuery& query, bool tailable, const QueryPlannerParams& params, int direction) { - // The following are expensive to look up, so only do it once for each. - const mongo::NamespaceString nss = query.nss(); - const bool isOplog = nss.isOplog(); // Make the (only) node, a collection scan. auto csn = std::make_unique<CollectionScanNode>(); csn->name = query.ns(); @@ -471,7 +371,6 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::makeCollectionScan( csn->shouldWaitForOplogVisibility = params.options & QueryPlannerParams::OPLOG_SCAN_WAIT_FOR_VISIBLE; csn->direction = direction; - csn->isOplog = isOplog; if (params.clusteredInfo) { csn->clusteredIndex = params.clusteredInfo->getIndexSpec(); @@ -548,29 +447,14 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::makeCollectionScan( auto queryCollator = query.getCollator(); auto collCollator = params.clusteredCollectionCollator; - csn->hasCompatibleCollation = CollatorInterface::collatorsMatch(queryCollator, collCollator); + csn->hasCompatibleCollation = + !queryCollator || (collCollator && *queryCollator == *collCollator); if (params.clusteredInfo && !csn->resumeAfterRecordId) { // This is a clustered collection. Attempt to perform an efficient, bounded collection scan - // via minRecord and maxRecord if applicable. During this process, we will check if the - // query is guaranteed to exclude values of the cluster key which are affected by collation. - // If so, then even if the query and collection collations differ, the collation difference - // won't affect the query results. In that case, we can say hasCompatibleCollation is true. - - RecordIdRange recordRange; - // min/max records may have been set if oplog or change collection. - recordRange.intersectRange(csn->minRecord, csn->maxRecord); - bool compatibleCollation = - handleRIDRangeScan(csn->filter.get(), csn.get(), params, queryCollator, recordRange); - csn->hasCompatibleCollation |= compatibleCollation; - - handleRIDRangeMinMax(query, csn.get(), params, queryCollator, recordRange); - - csn->minRecord = recordRange.getMin(); - csn->maxRecord = recordRange.getMax(); - - csn->boundInclusion = CollectionScanParams::makeInclusion(recordRange.isMinInclusive(), - recordRange.isMaxInclusive()); + // via minRecord and maxRecord if applicable. + handleRIDRangeScan(csn->filter.get(), csn.get(), params, queryCollator); + handleRIDRangeMinMax(query, csn.get(), params, queryCollator); } return csn; @@ -1230,13 +1114,47 @@ std::vector<std::unique_ptr<QuerySolutionNode>> QueryPlannerAccess::collapseEqui } /** - * This helper determines if a query can be covered depending on the query projection. + * Returns true if this is a null query that can retrieve all the information it needs directly from + * the index, and so does not need a FETCH stage on top of it. Returns false otherwise. */ -bool projNeedsFetch(const CanonicalQuery& query, const QueryPlannerParams& params) { +bool isCoveredNullQuery(const CanonicalQuery& query, + MatchExpression* root, + IndexTag* tag, + const vector<IndexEntry>& indices, + const QueryPlannerParams& params) { + // Sparse indexes and hashed indexes should not use this optimization as they will require a + // FETCH stage with a filter. + if (indices[tag->index].sparse || indices[tag->index].type == IndexType::INDEX_HASHED) { + return false; + } + + // When the index is not multikey, we can support a query on an indexed field searching for null + // values. This optimization can only be done when the index is not multikey, otherwise empty + // arrays in the collection will be treated as null/undefined by the index. When the index is + // multikey, we can support a query searching for both null and empty array values. + const auto multikeyIndex = indices[tag->index].multikey; + if (root->matchType() == MatchExpression::MatchType::MATCH_IN) { + // Check that the query matches null values, if the index is not multikey, or null and empty + // array values, if the index is multikey. Note that the query may match values other than + // null (and empty array). + const auto node = static_cast<const InMatchExpression*>(root); + if (!node->hasNull() || (multikeyIndex && !node->hasEmptyArray())) { + return false; + } + } else if (ComparisonMatchExpressionBase::isEquality(root->matchType()) && !multikeyIndex) { + // Check that the query matches null values. + const auto node = static_cast<const ComparisonMatchExpressionBase*>(root); + if (node->getData().type() != BSONType::jstNULL) { + return false; + } + } else { + return false; + } + // If nothing is being projected, the query is fully covered without a fetch. // This is trivially true for a count query. if (params.options & QueryPlannerParams::Options::IS_COUNT) { - return false; + return true; } // This optimization can only be used for find when the index covers the projection completely. @@ -1245,7 +1163,7 @@ bool projNeedsFetch(const CanonicalQuery& query, const QueryPlannerParams& param // in the multikey case). Hence, only find queries projecting _id are covered. auto proj = query.getProj(); if (!proj) { - return true; + return false; } // We can cover projections on _id and generated fields and expressions depending only on _id. @@ -1257,38 +1175,10 @@ bool projNeedsFetch(const CanonicalQuery& query, const QueryPlannerParams& param // Note that it is not possible to project onto dotted paths of _id here, since they may be // null or missing, and the index cannot differentiate between the two cases, so we would // still need a FETCH stage. - if (projFields.size() == 1 && *projFields.begin() == "_id") { - return false; - } + return projFields.size() == 1 && *projFields.begin() == "_id"; } - return true; -} - -/** - * This helper updates a MAYBE_COVERED query tightness to one of EXACT, INEXACT_COVERED, or - * INEXACT_FETCH, depending on whether we need a FETCH/filter to answer the query projection. - */ -void refineTightnessForMaybeCoveredQuery(const CanonicalQuery& query, - const QueryPlannerParams& params, - IndexBoundsBuilder::BoundsTightness& tightnessOut) { - // We need to refine the tightness in case we have a "MAYBE_COVERED" tightness bound which - // depends on the query's projection. We will not have information about the projection - // later on in order to make this determination, so we do it here. - const bool noFetchNeededForProj = !projNeedsFetch(query, params); - if (tightnessOut == IndexBoundsBuilder::EXACT_MAYBE_COVERED) { - if (noFetchNeededForProj) { - tightnessOut = IndexBoundsBuilder::EXACT; - } else { - tightnessOut = IndexBoundsBuilder::INEXACT_FETCH; - } - } else if (tightnessOut == IndexBoundsBuilder::INEXACT_MAYBE_COVERED) { - if (noFetchNeededForProj) { - tightnessOut = IndexBoundsBuilder::INEXACT_COVERED; - } else { - tightnessOut = IndexBoundsBuilder::INEXACT_FETCH; - } - } + return false; } bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, @@ -1332,6 +1222,11 @@ bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, // If we're here, we now know that 'child' can use an index directly and the index is // over the child's field. + // We need to track if this is a covered null query so that we can have this information + // at hand when handling the filter on an indexed AND. + scanState.isCoveredNullQuery = + isCoveredNullQuery(query, child, scanState.ixtag, indices, params); + // If 'child' is a NOT, then the tag we're interested in is on the NOT's // child node. if (MatchExpression::NOT == child->matchType()) { @@ -1364,7 +1259,6 @@ bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, verify(scanState.currentIndexNumber == scanState.ixtag->index); scanState.tightness = IndexBoundsBuilder::INEXACT_FETCH; mergeWithLeafNode(child, &scanState); - refineTightnessForMaybeCoveredQuery(query, params, scanState.tightness); handleFilter(&scanState); } else { if (nullptr != scanState.currentScan.get()) { @@ -1384,7 +1278,6 @@ bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, &scanState.tightness, scanState.getCurrentIETBuilder()); - refineTightnessForMaybeCoveredQuery(query, params, scanState.tightness); handleFilter(&scanState); } } @@ -1800,12 +1693,6 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::_buildIndexedDataAccess( return soln; } - // We may be able to avoid adding an extra fetch stage even though the bounds are - // inexact, for instance if the query is counting null values on an indexed field - // without projecting that field. We therefore convert "MAYBE_COVERED" bounds into - // either EXACT or INEXACT, depending on the query projection. - refineTightnessForMaybeCoveredQuery(query, params, tightness); - // If the bounds are exact, the set of documents that satisfy the predicate is // exactly equal to the set of documents that the scan provides. // @@ -1813,7 +1700,11 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::_buildIndexedDataAccess( // superset of documents that satisfy the predicate, and we must check the // predicate. - if (tightness == IndexBoundsBuilder::EXACT) { + // We may also be able to avoid adding an extra fetch stage even though the bounds are + // inexact because the query is counting null values on an indexed field without + // projecting that field. + if (tightness == IndexBoundsBuilder::EXACT || + isCoveredNullQuery(query, root, tag, indices, params)) { return soln; } else if (tightness == IndexBoundsBuilder::INEXACT_COVERED && !indices[tag->index].multikey) { @@ -1959,9 +1850,10 @@ void QueryPlannerAccess::handleFilterAnd(ScanBuildingState* scanState) { // should always be affixed as a filter. We keep 'curChild' in the $and // for affixing later. ++scanState->curChild; - } else if (scanState->tightness == IndexBoundsBuilder::EXACT) { - // The tightness of the bounds is exact. We want to remove this child so that when control - // returns to handleIndexedAnd we know that we don't need it to create a FETCH stage. + } else if (scanState->tightness == IndexBoundsBuilder::EXACT || scanState->isCoveredNullQuery) { + // The tightness of the bounds is exact or we are dealing with a covered null query. + // Either way, we want to remove this child so that when control returns to handleIndexedAnd + // we know that we don't need it to create a FETCH stage. root->getChildVector()->erase(root->getChildVector()->begin() + scanState->curChild); } else if (scanState->tightness == IndexBoundsBuilder::INEXACT_COVERED && (INDEX_TEXT == index.type || !index.multikey)) { |
