Files
mongo/jstests/core/query/explain/explain_count.js
Steve McClure 1ffbc6c2e9 SERVER-109432: Autofix JS var usage to favor let (#40637)
GitOrigin-RevId: 9674b7db36a0f3f650d39c1e3fb2ad6ff2141cfb
2025-08-28 19:21:01 +00:00

195 lines
8.7 KiB
JavaScript

// Test running explains on count commands.
//
// @tags: [
// requires_fastcount,
// requires_fcv_82,
// ]
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {assertExplainCount, getPlanStage, getPlanStages} from "jstests/libs/query/analyze_plan.js";
let collName = "jstests_explain_count";
let t = db[collName];
t.drop();
/**
* Given an explain output from a COUNT_SCAN stage, check that a indexBounds field is present.
*/
function checkCountScanIndexExplain(explain, startKey, endKey, startInclusive, endInclusive) {
let countStage = getPlanStage(explain.executionStats.executionStages, "COUNT_SCAN");
assert.eq(countStage.stage, "COUNT_SCAN");
assert("indexBounds" in countStage);
assert.eq(countStage.indexBounds.startKey, startKey);
assert.eq(countStage.indexBounds.endKey, endKey);
assert.eq(countStage.indexBounds.startKeyInclusive, startInclusive);
assert.eq(countStage.indexBounds.endKeyInclusive, endInclusive);
}
/**
* Ensure that the SHARDING_FILTER's child stage is an IXSCAN (and not a fetch). This is to ensure
* sharded clusters can still run the count command with a predicate on indexed fields reasonably
* fast. Assumes that the shard key is part of the index.
*/
function checkShardingFilterIndexScanExplain(explain, keyName, bounds) {
const filterStages = getPlanStages(explain.executionStats.executionStages, "SHARDING_FILTER");
assert.gt(filterStages.length, 0, "No SHARDING_FILTER stage found");
for (const filterStage of filterStages) {
assert.eq(filterStage.stage, "SHARDING_FILTER", filterStage);
const ixScanStage = filterStage.inputStage;
assert.eq(ixScanStage.stage, "IXSCAN");
assert("indexBounds" in ixScanStage);
assert.eq(ixScanStage.indexBounds[keyName].length, 1);
const expectedBoundsArr = JSON.parse(ixScanStage.indexBounds[keyName][0]);
assert.eq(expectedBoundsArr, bounds);
}
}
/**
* Check that the explain from a count command run on a collection with a usable index for the
* predicate produces a reasonable plan. On sharded collections, we expect to have an IXSCAN
* followed by a SHARDING_FILTER. Otherwise, the COUNT_SCAN stage should be used.
*/
function checkIndexedCountWithPred(db, explain, keyName, bounds) {
assert.eq(bounds.length, 2);
if (FixtureHelpers.isMongos(db) && FixtureHelpers.isSharded(db[collName])) {
// On sharded collections we have a SHARDING_FILTER with a child that's an IXSCAN.
checkShardingFilterIndexScanExplain(explain, keyName, bounds);
} else {
// On a standalone we just do a COUNT_SCAN over the {a: 1, _id: 1} index.
const min = {a: bounds[0], _id: {"$minKey": 1}};
const max = {a: bounds[1], _id: {"$maxKey": 1}};
checkCountScanIndexExplain(explain, min, max, true, true);
}
}
// TODO(SERVER-18047): Remove database creation once explain behavior is unified between replica
// sets and sharded clusters.
if (FixtureHelpers.isMongos(db) || TestData.testingReplicaSetEndpoint) {
// Create database
assert.commandWorked(db.adminCommand({"enableSharding": db.getName()}));
}
// Collection does not exist.
assert.eq(0, t.count());
let explain = assert.commandWorked(db.runCommand({explain: {count: collName}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 0});
// Collection does not exist with skip, limit, and/or query.
let result = assert.commandWorked(db.runCommand({count: collName, skip: 3}));
assert.eq(0, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, skip: 3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 0});
result = assert.commandWorked(db.runCommand({count: collName, limit: 3}));
assert.eq(0, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, limit: 3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 0});
result = assert.commandWorked(db.runCommand({count: collName, limit: -3}));
assert.eq(0, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, limit: -3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 0});
result = assert.commandWorked(db.runCommand({count: collName, limit: -3, skip: 4}));
assert.eq(0, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, limit: -3, skip: 4}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 0});
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 1}, limit: -3, skip: 4}));
assert.eq(0, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, query: {a: 1}, limit: -3, skip: 4}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 0});
// Now add a bit of data to the collection.
// On sharded clusters, we'll want the shard key to be indexed, so we make _id part of the index.
// This means counts will not have to fetch from the document in order to get the shard key.
t.createIndex({a: 1, _id: 1});
for (let i = 0; i < 10; i++) {
t.insert({_id: i, a: 1});
}
// Trivial count with no skip, limit, or query.
assert.eq(10, t.count());
explain = assert.commandWorked(db.runCommand({explain: {count: collName}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 10});
// Trivial count with skip.
result = assert.commandWorked(db.runCommand({count: collName, skip: 3}));
assert.eq(7, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, skip: 3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 7});
// Trivial count with limit.
result = assert.commandWorked(db.runCommand({count: collName, limit: 3}));
assert.eq(3, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, limit: 3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 3});
// Trivial count with negative limit.
result = assert.commandWorked(db.runCommand({count: collName, limit: -3}));
assert.eq(3, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, limit: -3}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 3});
// Trivial count with both limit and skip.
result = assert.commandWorked(db.runCommand({count: collName, limit: -3, skip: 4}));
assert.eq(3, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, limit: -3, skip: 4}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 3});
// With a query.
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 1}}));
assert.eq(10, result.n);
explain = assert.commandWorked(db.runCommand({explain: {count: collName, query: {a: 1}}, verbosity: "executionStats"}));
assertExplainCount({explainResults: explain, expectedCount: 10});
checkIndexedCountWithPred(db, explain, "a", [1.0, 1.0]);
// With a query and skip.
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 1}, skip: 3}));
assert.eq(7, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, query: {a: 1}, skip: 3}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 7});
checkIndexedCountWithPred(db, explain, "a", [1.0, 1.0]);
// With a query and limit.
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 1}, limit: 3}));
assert.eq(3, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, query: {a: 1}, limit: 3}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 3});
checkIndexedCountWithPred(db, explain, "a", [1.0, 1.0]);
// Insert one more doc for the last few tests.
t.insert({a: 2});
// Case where all results are skipped.
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 2}, skip: 2}));
assert.eq(0, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, query: {a: 2}, skip: 2}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 0});
checkIndexedCountWithPred(db, explain, "a", [2, 2]);
// Case where we have a limit, but we don't hit it.
result = assert.commandWorked(db.runCommand({count: collName, query: {a: 2}, limit: 2}));
assert.eq(1, result.n);
explain = assert.commandWorked(
db.runCommand({explain: {count: collName, query: {a: 2}, limit: 2}, verbosity: "executionStats"}),
);
assertExplainCount({explainResults: explain, expectedCount: 1});
checkIndexedCountWithPred(db, explain, "a", [2, 2]);