/** * Copyright (C) 2020-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 * . * * 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/exec/plan_cache_util.h" #include #include #include #include #include "mongo/base/string_data.h" #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/basic_types_gen.h" #include "mongo/db/exec/multi_plan.h" #include "mongo/db/query/canonical_query_encoder.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/db/query/find_command.h" #include "mongo/db/query/index_entry.h" #include "mongo/db/query/plan_cache/classic_plan_cache.h" #include "mongo/db/query/plan_cache/plan_cache_callbacks.h" #include "mongo/db/query/plan_cache/plan_cache_key_factory.h" #include "mongo/db/query/plan_cache/sbe_plan_cache.h" #include "mongo/db/query/plan_explainer_factory.h" #include "mongo/db/query/plan_explainer_impl.h" #include "mongo/db/query/stage_builder/stage_builder_util.h" #include "mongo/db/query/stage_types.h" #include "mongo/db/service_context.h" #include "mongo/logv2/log.h" #include "mongo/logv2/log_attr.h" #include "mongo/logv2/log_component.h" #include "mongo/logv2/redaction.h" #include "mongo/stdx/unordered_map.h" #include "mongo/util/overloaded_visitor.h" #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery namespace mongo { namespace plan_cache_util { namespace { void logTieForBest(std::string&& query, double winnerScore, double runnerUpScore, std::string winnerPlanSummary, std::string runnerUpPlanSummary) { LOGV2_DEBUG(20594, 1, "Winning plan tied with runner-up, skip caching", "query"_attr = redact(query), "winnerScore"_attr = winnerScore, "winnerPlanSummary"_attr = winnerPlanSummary, "runnerUpScore"_attr = runnerUpScore, "runnerUpPlanSummary"_attr = runnerUpPlanSummary); } void logNotCachingZeroResults(std::string&& query, double score, std::string winnerPlanSummary) { LOGV2_DEBUG(20595, 1, "Winning plan had zero results, skip caching", "query"_attr = redact(query), "winnerScore"_attr = score, "winnerPlanSummary"_attr = winnerPlanSummary); } void logNotCachingNoData(std::string&& solution) { LOGV2_DEBUG(20596, 5, "Not caching query because this solution has no cache data", "solutions"_attr = redact(solution)); } /** * Returns a pair of PlanExplainers consisting of the explainer of 'winningPlan' and the explainer * of the second best in 'candidates'. If there are less than two candidates, second one remains * nullptr. */ std::pair, std::unique_ptr> getClassicPlanExplainers( const plan_ranker::CandidatePlan& winningPlan, const plan_ranker::PlanRankingDecision& ranking, const std::vector& candidates) { std::unique_ptr winnerExplainer = plan_explainer_factory::make(winningPlan.root); std::unique_ptr runnerUpExplainer; if (ranking.candidateOrder.size() > 1) { invariant(ranking.candidateOrder.size() > 1U); auto runnerUpIdx = ranking.candidateOrder[1]; runnerUpExplainer = plan_explainer_factory::make(candidates[runnerUpIdx].root); } return std::make_pair(std::move(winnerExplainer), std::move(runnerUpExplainer)); } bool shouldCacheBasedOnQueryAndPlan(const CanonicalQuery& query, const QuerySolution* winningPlan) { if (!query.isUncacheableSbe() && shouldCacheQuery(query) && winningPlan->isEligibleForPlanCache()) { if (!winningPlan->cacheData) { logNotCachingNoData(winningPlan->toString()); return false; } else { return true; } } return false; } void updateClassicPlanCacheFromClassicCandidatesImpl( OperationContext* opCtx, const CollectionPtr& collection, const CanonicalQuery& query, ReadsOrWorks readsOrWorks, std::unique_ptr ranking, std::vector& candidates) { auto winnerIdx = ranking->candidateOrder[0]; invariant(winnerIdx >= 0 && winnerIdx < candidates.size()); auto& winningPlan = candidates[winnerIdx]; if (!shouldCacheBasedOnQueryAndPlan(query, winningPlan.solution.get())) { return; } auto buildDebugInfoFn = [&]() -> plan_cache_debug_info::DebugInfo { return buildDebugInfo(query, std::move(ranking)); }; auto printCachedPlanFn = [](const SolutionCacheData& plan) { return plan.toString(); }; PlanCacheCallbacksImpl callbacks{query, buildDebugInfoFn, printCachedPlanFn}; winningPlan.solution->cacheData->indexFilterApplied = winningPlan.solution->indexFilterApplied; winningPlan.solution->cacheData->solutionHash = winningPlan.solution->hash(); auto isSensitive = CurOp::get(opCtx)->getShouldOmitDiagnosticInformation(); auto key = plan_cache_key_factory::make(query, collection); uassertStatusOK( CollectionQueryInfo::get(collection) .getPlanCache() ->set(std::move(key), winningPlan.solution->cacheData->clone(), readsOrWorks, opCtx->getServiceContext()->getPreciseClockSource()->now(), &callbacks, isSensitive ? PlanSecurityLevel::kSensitive : PlanSecurityLevel::kNotSensitive, boost::none /* worksGrowthCoefficient */)); } void updateSbePlanCache(OperationContext* opCtx, const MultipleCollectionAccessor& collections, const CanonicalQuery& query, NumReads nReads, const QuerySolution* soln, std::unique_ptr cachedPlan) { auto buildDebugInfoFn = [soln]() -> plan_cache_debug_info::DebugInfoSBE { return buildDebugInfo(soln); }; auto printCachedPlanFn = [](const sbe::CachedSbePlan& plan) { sbe::DebugPrinter p; return p.print(*plan.root.get()); }; PlanCacheCallbacksImpl callbacks{query, buildDebugInfoFn, printCachedPlanFn}; auto isSensitive = CurOp::get(opCtx)->getShouldOmitDiagnosticInformation(); uassertStatusOK(sbe::getPlanCache(opCtx).set( plan_cache_key_factory::make( query, collections, canonical_query_encoder::Optimizer::kSbeStageBuilders), std::move(cachedPlan), nReads, opCtx->getServiceContext()->getPreciseClockSource()->now(), &callbacks, isSensitive ? PlanSecurityLevel::kSensitive : PlanSecurityLevel::kNotSensitive, boost::none /* worksGrowthCoefficient */)); } } // namespace void updateClassicPlanCacheFromClassicCandidatesForSbeExecution( OperationContext* opCtx, const CollectionPtr& collection, const CanonicalQuery& query, NumReads reads, std::unique_ptr ranking, std::vector& candidates) { invariant(query.isSbeCompatible()); updateClassicPlanCacheFromClassicCandidatesImpl( opCtx, collection, query, reads, std::move(ranking), candidates); } void updateClassicPlanCacheFromClassicCandidatesForClassicExecution( OperationContext* opCtx, const CollectionPtr& collection, const CanonicalQuery& query, std::unique_ptr ranking, std::vector& candidates) { ReadsOrWorks nWorks = NumWorks{ranking->stats.candidatePlanStats[0]->common.works}; updateClassicPlanCacheFromClassicCandidatesImpl( opCtx, collection, query, nWorks, std::move(ranking), candidates); } void updateSbePlanCacheWithNumReads( OperationContext* opCtx, const MultipleCollectionAccessor& collections, const CanonicalQuery& query, NumReads nReads, const std::pair, stage_builder::PlanStageData>& sbePlanAndData, const QuerySolution* winningSolution) { if (!shouldCacheBasedOnQueryAndPlan(query, winningSolution)) { return; } // Clone the winning SBE plan and its auxiliary data. auto cachedPlan = std::make_unique( sbePlanAndData.first->clone(), sbePlanAndData.second, winningSolution->hash()); cachedPlan->indexFilterApplied = winningSolution->indexFilterApplied; updateSbePlanCache(opCtx, collections, query, nReads, winningSolution, std::move(cachedPlan)); } void updateSbePlanCacheWithPinnedEntry(OperationContext* opCtx, const MultipleCollectionAccessor& collections, const CanonicalQuery& query, const QuerySolution& solution, const sbe::PlanStage& root, stage_builder::PlanStageData stageData) { const CollectionPtr& collection = collections.getMainCollection(); if (collection && !query.isUncacheableSbe() && shouldCacheQuery(query) && solution.isEligibleForPlanCache()) { sbe::PlanCacheKey key = plan_cache_key_factory::make( query, collections, canonical_query_encoder::Optimizer::kSbeStageBuilders); // Store a copy of the root and corresponding data, as well as the hash of the QuerySolution // that led to this cache entry. auto plan = std::make_unique( root.clone(), std::move(stageData), solution.hash()); plan->indexFilterApplied = solution.indexFilterApplied; bool shouldOmitDiagnosticInformation = CurOp::get(opCtx)->getShouldOmitDiagnosticInformation(); sbe::getPlanCache(opCtx).setPinned( key, canonical_query_encoder::computeHash( canonical_query_encoder::encodeForPlanCacheCommand(query)), std::move(plan), opCtx->getServiceContext()->getPreciseClockSource()->now(), buildDebugInfo(&solution), shouldOmitDiagnosticInformation); } } plan_cache_debug_info::DebugInfo buildDebugInfo( const CanonicalQuery& query, std::unique_ptr decision) { // Strip projections on $-prefixed fields, as these are added by internal callers of the // system and are not considered part of the user projection. const FindCommandRequest& findCommand = query.getFindCommandRequest(); BSONObjBuilder projBuilder; for (auto elem : findCommand.getProjection()) { if (elem.fieldName()[0] == '$') { continue; } projBuilder.append(elem); } plan_cache_debug_info::CreatedFromQuery createdFromQuery = plan_cache_debug_info::CreatedFromQuery{ findCommand.getFilter().getOwned(), findCommand.getSort().getOwned(), projBuilder.obj().getOwned(), query.getCollator() ? query.getCollator()->getSpec().toBSON() : BSONObj()}; return {std::move(createdFromQuery), std::move(decision)}; } plan_cache_debug_info::DebugInfoSBE buildDebugInfo(const QuerySolution* solution) { plan_cache_debug_info::DebugInfoSBE debugInfo; if (!solution || !solution->root()) return debugInfo; std::queue queue; queue.push(solution->root()); // Look through the QuerySolution to collect some static stat details. while (!queue.empty()) { auto node = queue.front(); queue.pop(); invariant(node); switch (node->getType()) { case STAGE_COUNT_SCAN: { auto csn = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(csn->index.identifier.catalogName); break; } case STAGE_DISTINCT_SCAN: { auto dn = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(dn->index.identifier.catalogName); break; } case STAGE_GEO_NEAR_2D: { auto geo2d = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(geo2d->index.identifier.catalogName); break; } case STAGE_GEO_NEAR_2DSPHERE: { auto geo2dsphere = static_cast(node); debugInfo.mainStats.indexesUsed.push_back( geo2dsphere->index.identifier.catalogName); break; } case STAGE_IXSCAN: { auto ixn = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(ixn->index.identifier.catalogName); break; } case STAGE_COLUMN_SCAN: { auto cisn = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(cisn->indexEntry.identifier.catalogName); break; } case STAGE_TEXT_MATCH: { auto tn = static_cast(node); debugInfo.mainStats.indexesUsed.push_back(tn->index.identifier.catalogName); break; } case STAGE_COLLSCAN: { debugInfo.mainStats.collectionScans++; auto csn = static_cast(node); if (!csn->tailable) { debugInfo.mainStats.collectionScansNonTailable++; } break; } case STAGE_EQ_LOOKUP: { auto eln = static_cast(node); auto& secondaryStats = debugInfo.secondaryStats[eln->foreignCollection]; if (eln->lookupStrategy == EqLookupNode::LookupStrategy::kIndexedLoopJoin) { tassert(6466200, "Index join lookup should have an index entry", eln->idxEntry); secondaryStats.indexesUsed.push_back(eln->idxEntry->identifier.catalogName); } else { secondaryStats.collectionScans++; } [[fallthrough]]; } default: break; } for (auto&& child : node->children) { queue.push(child.get()); } } debugInfo.planSummary = solution->summaryString(); return debugInfo; } void ClassicPlanCacheWriter::operator()(const CanonicalQuery& cq, MultiPlanStage& mps, std::unique_ptr ranking, std::vector& candidates) const { // Note this function is also called by ConditionalClassicPlanCacheWriter. if (_executeInSbe) { auto stats = mps.getStats(); auto nReads = computeNumReadsFromWorks(*stats, *ranking); updateClassicPlanCacheFromClassicCandidatesForSbeExecution( _opCtx, _collection.getCollectionPtr(), cq, nReads, std::move(ranking), candidates); } else { // We've been asked to write a works value, for classic execution. updateClassicPlanCacheFromClassicCandidatesForClassicExecution( _opCtx, _collection.getCollectionPtr(), cq, std::move(ranking), candidates); } } void ConditionalClassicPlanCacheWriter::operator()( const CanonicalQuery& cq, MultiPlanStage& mps, std::unique_ptr ranking, std::vector& candidates) const { if (shouldCacheBasedOnCachingMode(cq, *ranking, candidates)) { ClassicPlanCacheWriter::operator()(cq, mps, std::move(ranking), candidates); } } bool ConditionalClassicPlanCacheWriter::shouldCacheBasedOnCachingMode( const CanonicalQuery& cq, const plan_ranker::PlanRankingDecision& ranking, const std::vector& candidates) const { switch (_planCachingMode) { case Mode::AlwaysCache: { return true; } case Mode::NeverCache: { return false; } case Mode::SometimesCache: { auto winnerIdx = ranking.candidateOrder[0]; invariant(winnerIdx >= 0 && winnerIdx < candidates.size()); auto& winningPlan = candidates[winnerIdx]; auto&& [winnerExplainer, runnerUpExplainer] = getClassicPlanExplainers(winningPlan, ranking, candidates); if (ranking.tieForBest()) { // These arrays having two or more entries is implied by 'tieForBest'. invariant(ranking.scores.size() > 1U); invariant(runnerUpExplainer); logTieForBest(cq.toStringShort(), ranking.scores[0], ranking.scores[1], winnerExplainer->getPlanSummary(), runnerUpExplainer->getPlanSummary()); // The winning plan tied with the runner-up and we're using "sometimes cache" mode. // We will not write a plan cache entry. return false; } if (ranking.stats.candidatePlanStats[0]->common.advanced == 0) { // We're using the "sometimes cache" mode, and the winning plan produced no results // during the plan ranking trial period. We will not write a plan cache entry. logNotCachingZeroResults( cq.toStringShort(), ranking.scores[0], winnerExplainer->getPlanSummary()); return false; } // The winning plan produced more than zero results and there was no tie between the // winner and runner up. In this case, we allow the winning plan to be cached. return true; } } MONGO_UNREACHABLE; } NumReads computeNumReadsFromWorks(const PlanStageStats& stats, const plan_ranker::PlanRankingDecision& ranking) { auto winnerIdx = ranking.candidateOrder[0]; auto summary = collectExecutionStatsSummary(&stats, winnerIdx); return NumReads{summary.totalKeysExamined + summary.totalDocsExamined}; } } // namespace plan_cache_util } // namespace mongo