SERVER-117390: Add costs to join explain output (#46771)

GitOrigin-RevId: 28b807193fbb173023fbc427b8b0442f25151d99
This commit is contained in:
HanaPearlman 2026-01-22 14:51:48 -05:00 committed by MongoDB Bot
parent 7f877a2cfa
commit fcb2dea011
4 changed files with 40 additions and 26 deletions

View File

@ -195,7 +195,14 @@ export function normalizePlan(plan, flatten = true) {
}
// Expand this array if you find new fields which are inconsistent across different test runs.
const ignoreFields = ["isCached", "indexVersion", "planNodeId", "cardinalityEstimate", "estimatesMetadata"];
const ignoreFields = [
"isCached",
"indexVersion",
"planNodeId",
"cardinalityEstimate",
"costEstimate",
"estimatesMetadata",
];
// Iterates over the plan while ignoring the `ignoreFields`, to create flattened stages whenever
// `kExplainChildFieldNames` are encountered.

View File

@ -54,9 +54,15 @@ function runTest(pipeline) {
if (stage.stage.includes("JOIN_EMBEDDING") || stage.stage.includes("COLLSCAN")) {
assert(
stage.hasOwnProperty("cardinalityEstimate"),
"Estimates not found in stage: " + tojson(stage) + ", " + tojson(explain),
"Cardinality estimate not found in stage: " + tojson(stage) + ", " + tojson(explain),
);
assert.gt(stage.cardinalityEstimate, 0, "Cardinality estimate is not greater than 0");
assert(
stage.hasOwnProperty("costEstimate"),
"Cost estimate not found in stage: " + tojson(stage) + ", " + tojson(explain),
);
// TODO SERVER-117480: Change this assert to be strictly greater than zero.
assert.gte(stage.costEstimate, 0, "Cost estimate is not greater than 0");
}
}
}

View File

@ -155,18 +155,19 @@ std::vector<QSNJoinPredicate> makeJoinPreds(const JoinReorderingContext& ctx,
return preds;
}
void addEstimateIfExplain(const JoinReorderingContext& ctx,
const PlanEnumeratorContext& peCtx,
QuerySolutionNode* node,
NodeSet set,
cost_based_ranker::EstimateMap& estimates) {
void addEstimatesIfExplain(const JoinReorderingContext& ctx,
const PlanEnumeratorContext& peCtx,
QuerySolutionNode* node,
NodeSet set,
const JoinCostEstimate& cost,
cost_based_ranker::EstimateMap& estimates) {
if (!ctx.explain) {
return;
}
// TODO SERVER-116505: Populate estimates map with cost information when available.
auto ce = peCtx.getJoinCardinalityEstimator()->getOrEstimateSubsetCardinality(set);
estimates.emplace(node, cost_based_ranker::QSNEstimate{.outCE = ce});
estimates.emplace(node,
cost_based_ranker::QSNEstimate{.outCE = ce, .cost = cost.getTotalCost()});
}
// Forward-declare because of mutual recursion.
@ -181,21 +182,22 @@ std::unique_ptr<QuerySolutionNode> buildQSNFromJoinPlan(const JoinReorderingCont
JoinPlanNodeId nodeId,
cost_based_ranker::EstimateMap& estimates) {
std::unique_ptr<QuerySolutionNode> qsn;
std::visit(
OverloadedVisitor{[&](const JoiningNode& join) {
qsn = buildQSNFromJoiningNode(ctx, peCtx, join, estimates);
addEstimateIfExplain(ctx, peCtx, qsn.get(), join.bitset, estimates);
},
[&](const BaseNode& base) {
// TODO SERVER-111913: Avoid this clone
qsn = base.soln->root()->clone();
addEstimateIfExplain(
ctx, peCtx, qsn.get(), NodeSet().set(base.node), estimates);
},
[&](const INLJRHSNode& ip) {
qsn = createIndexProbeQSN(ctx.joinGraph.getNode(ip.node), ip.entry);
}},
peCtx.registry().get(nodeId));
std::visit(OverloadedVisitor{
[&](const JoiningNode& join) {
qsn = buildQSNFromJoiningNode(ctx, peCtx, join, estimates);
addEstimatesIfExplain(
ctx, peCtx, qsn.get(), join.bitset, join.cost, estimates);
},
[&](const BaseNode& base) {
// TODO SERVER-111913: Avoid this clone
qsn = base.soln->root()->clone();
addEstimatesIfExplain(
ctx, peCtx, qsn.get(), NodeSet().set(base.node), base.cost, estimates);
},
[&](const INLJRHSNode& ip) {
qsn = createIndexProbeQSN(ctx.joinGraph.getNode(ip.node), ip.entry);
}},
peCtx.registry().get(nodeId));
return qsn;
}

View File

@ -295,8 +295,7 @@ void statsToBSON(const QuerySolutionNode* node,
// Cost and cardinality of the stage.
if (estimates.contains(node)) {
const auto& est = estimates.at(node);
// TODO SERVER-116505: Add cost here when available, possibly differentiating costs from the
// join module vs CBR.
bob->append("costEstimate", est.cost.toDouble());
bob->append("cardinalityEstimate", est.outCE.toDouble());
BSONObjBuilder metadataBob(bob->subobjStart("estimatesMetadata"));
metadataBob.append("ceSource", toStringData(est.outCE.source()));