diff options
Diffstat (limited to 'src/mongo/db/query/planner_access.cpp')
| -rw-r--r-- | src/mongo/db/query/planner_access.cpp | 244 |
1 files changed, 155 insertions, 89 deletions
diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp index e05dfd8a5d1..ef0956dc56f 100644 --- a/src/mongo/db/query/planner_access.cpp +++ b/src/mongo/db/query/planner_access.cpp @@ -228,27 +228,37 @@ bool affectedByCollator(const BSONElement& element) { } } -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 'newMin' if 'newMin' < 'curr' +void setLowestRecord(boost::optional<RecordIdBound>& curr, const RecordIdBound& newMin) { + if (!curr || newMin.recordId() < curr->recordId()) { + curr = newMin; } } -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 'newMax' if 'newMax' > 'curr' +void setHighestRecord(boost::optional<RecordIdBound>& curr, const RecordIdBound& newMax) { + if (!curr || newMax.recordId() > curr->recordId()) { + curr = newMax; } } +// 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 = !queryCollator || (collCollator && *queryCollator == *collCollator); + bool compatible = CollatorInterface::collatorsMatch(queryCollator, collCollator); return compatible || !affectedByCollator(element); } @@ -281,47 +291,106 @@ void handleRIDRangeMinMax(const CanonicalQuery& query, // Assumes clustered collection scans are only supported with the forward direction. collScan->boundInclusion = CollectionScanParams::ScanBoundInclusion::kIncludeStartRecordOnly; - setMaxRecord(collScan, IndexBoundsBuilder::objFromElement(maxObj.firstElement(), collator)); + setLowestRecord(collScan->maxRecord, + IndexBoundsBuilder::objFromElement(maxObj.firstElement(), collator)); } if (!minObj.isEmpty() && compatibleCollator(params, collator, minObj.firstElement())) { // The min() is inclusive as are bounded collection scans by default. - setMinRecord(collScan, IndexBoundsBuilder::objFromElement(minObj.firstElement(), collator)); + setHighestRecord(collScan->minRecord, + 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. + * 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. + * */ -void handleRIDRangeScan(const MatchExpression* conjunct, - CollectionScanNode* collScan, - const QueryPlannerParams& params, - const CollatorInterface* collator) { +[[nodiscard]] bool handleRIDRangeScan(const MatchExpression* conjunct, + CollectionScanNode* collScan, + const QueryPlannerParams& params, + const CollatorInterface* collator) { invariant(params.clusteredInfo); if (conjunct == nullptr) { - return; + return false; } auto* andMatchPtr = dynamic_cast<const AndMatchExpression*>(conjunct); if (andMatchPtr != nullptr) { + bool atLeastOneConjunctCompatibleCollation = false; for (size_t index = 0; index < andMatchPtr->numChildren(); index++) { - handleRIDRangeScan(andMatchPtr->getChild(index), collScan, params, collator); + if (handleRIDRangeScan(andMatchPtr->getChild(index), collScan, params, collator)) { + atLeastOneConjunctCompatibleCollation = true; + } } - return; + + // 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; } if (conjunct->path() != clustered_util::getClusterKeyFieldName(params.clusteredInfo->getIndexSpec())) { // No match on the cluster key. - return; + 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()); + } + } + + // Finally, tighten the collscan bounds with the min/max bounds for the $in. + if (minBound) { + setHighestRecord(collScan->minRecord, *minBound); + } + if (maxBound) { + setLowestRecord(collScan->maxRecord, *maxBound); + } + return allEltsCollationCompatible; } auto match = dynamic_cast<const ComparisonMatchExpression*>(conjunct); if (match == nullptr) { - return; // Not a comparison match expression. + return false; // Not a comparison match expression. } const auto& element = match->getData(); @@ -329,32 +398,32 @@ void handleRIDRangeScan(const MatchExpression* conjunct, // 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()); + setHighestRecord(collScan->minRecord, minb.obj()); BSONObjBuilder maxb; maxb.appendMaxForType("", element.type()); - setMaxRecord(collScan, maxb.obj()); + setLowestRecord(collScan->maxRecord, maxb.obj()); bool compatible = compatibleCollator(params, collator, element); if (!compatible) { - return; // Collator affects probe and it's not compatible with collection's collator. + return false; // 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 - collScan->hasCompatibleCollation = true; - const auto collated = IndexBoundsBuilder::objFromElement(element, collator); if (dynamic_cast<const EqualityMatchExpression*>(match)) { - setMinRecord(collScan, collated); - setMaxRecord(collScan, collated); + setHighestRecord(collScan->minRecord, collated); + setLowestRecord(collScan->maxRecord, collated); } else if (dynamic_cast<const LTMatchExpression*>(match) || dynamic_cast<const LTEMatchExpression*>(match)) { - setMaxRecord(collScan, collated); + setLowestRecord(collScan->maxRecord, collated); } else if (dynamic_cast<const GTMatchExpression*>(match) || dynamic_cast<const GTEMatchExpression*>(match)) { - setMinRecord(collScan, collated); + setHighestRecord(collScan->minRecord, collated); } + + return true; } } // namespace @@ -447,13 +516,18 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::makeCollectionScan( auto queryCollator = query.getCollator(); auto collCollator = params.clusteredCollectionCollator; - csn->hasCompatibleCollation = - !queryCollator || (collCollator && *queryCollator == *collCollator); + csn->hasCompatibleCollation = CollatorInterface::collatorsMatch(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. - handleRIDRangeScan(csn->filter.get(), csn.get(), params, queryCollator); + // 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. + bool compatibleCollation = + handleRIDRangeScan(csn->filter.get(), csn.get(), params, queryCollator); + csn->hasCompatibleCollation |= compatibleCollation; + handleRIDRangeMinMax(query, csn.get(), params, queryCollator); } @@ -1114,47 +1188,13 @@ std::vector<std::unique_ptr<QuerySolutionNode>> QueryPlannerAccess::collapseEqui } /** - * 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. + * This helper determines if a query can be covered depending on the query projection. */ -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; - } - +bool projNeedsFetch(const CanonicalQuery& query, const QueryPlannerParams& params) { // 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 true; + return false; } // This optimization can only be used for find when the index covers the projection completely. @@ -1163,7 +1203,7 @@ bool isCoveredNullQuery(const CanonicalQuery& query, // in the multikey case). Hence, only find queries projecting _id are covered. auto proj = query.getProj(); if (!proj) { - return false; + return true; } // We can cover projections on _id and generated fields and expressions depending only on _id. @@ -1175,10 +1215,38 @@ bool isCoveredNullQuery(const CanonicalQuery& query, // 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. - return projFields.size() == 1 && *projFields.begin() == "_id"; + if (projFields.size() == 1 && *projFields.begin() == "_id") { + return false; + } } - return false; + 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; + } + } } bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, @@ -1222,11 +1290,6 @@ 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()) { @@ -1259,6 +1322,7 @@ 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()) { @@ -1278,6 +1342,7 @@ bool QueryPlannerAccess::processIndexScans(const CanonicalQuery& query, &scanState.tightness, scanState.getCurrentIETBuilder()); + refineTightnessForMaybeCoveredQuery(query, params, scanState.tightness); handleFilter(&scanState); } } @@ -1693,6 +1758,12 @@ 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. // @@ -1700,11 +1771,7 @@ std::unique_ptr<QuerySolutionNode> QueryPlannerAccess::_buildIndexedDataAccess( // superset of documents that satisfy the predicate, and we must check the // predicate. - // 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)) { + if (tightness == IndexBoundsBuilder::EXACT) { return soln; } else if (tightness == IndexBoundsBuilder::INEXACT_COVERED && !indices[tag->index].multikey) { @@ -1850,10 +1917,9 @@ 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 || 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. + } 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. root->getChildVector()->erase(root->getChildVector()->begin() + scanState->curChild); } else if (scanState->tightness == IndexBoundsBuilder::INEXACT_COVERED && (INDEX_TEXT == index.type || !index.multikey)) { |
