Skip to content

Commit 4f11366

Browse files
HCSPeteMongoDB Bot
authored andcommitted
SERVER-83119 Qualify a ClusteredIndexscan as an allowed operation when notablescan is set. (#17342)
GitOrigin-RevId: 0556bfe
1 parent 8e5774b commit 4f11366

15 files changed

Lines changed: 260 additions & 63 deletions

etc/backports_required_for_multiversion_tests.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,12 @@ last-continuous:
553553
ticket: SERVER-84130
554554
- test_file: jstests/noPassthrough/disallow_new_transactions_at_shutdown_on_mongos.js
555555
ticket: SERVER-77667
556+
- test_file: jstests/core/notablescan.js
557+
ticket: SERVER-83119
558+
- test_file: jstests/sharding/clustered_coll_scan.js
559+
ticket: SERVER-83119
560+
- test_file: jstests/sharding/eof_plan.js
561+
ticket: SERVER-83119
556562
suites: null
557563
last-lts:
558564
all:
@@ -1164,4 +1170,10 @@ last-lts:
11641170
ticket: SERVER-84130
11651171
- test_file: jstests/noPassthrough/disallow_new_transactions_at_shutdown_on_mongos.js
11661172
ticket: SERVER-77667
1173+
- test_file: jstests/core/notablescan.js
1174+
ticket: SERVER-83119
1175+
- test_file: jstests/sharding/clustered_coll_scan.js
1176+
ticket: SERVER-83119
1177+
- test_file: jstests/sharding/eof_plan.js
1178+
ticket: SERVER-83119
11671179
suites: null

jstests/core/notablescan.js

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,69 @@
1616
// tenant_migration_incompatible,
1717
// ]
1818

19-
let t = db.test_notablescan;
20-
t.drop();
19+
import {isClusteredIxscan} from "jstests/libs/analyze_plan.js";
20+
import {assertDropAndRecreateCollection} from "jstests/libs/collection_drop_recreate.js";
2121

22-
try {
23-
assert.commandWorked(db._adminCommand({setParameter: 1, notablescan: true}));
24-
// commented lines are SERVER-2222
25-
if (0) { // SERVER-2222
26-
assert.throws(function() {
27-
t.find({a: 1}).toArray();
28-
});
29-
}
30-
t.save({a: 1});
31-
assert.throws(function() {
32-
t.count({a: 1});
33-
});
22+
function checkError(err) {
23+
assert.includes(err.toString(), "'notablescan'");
24+
}
25+
26+
const colName = jsTestName();
27+
let coll = db.getCollection(colName);
28+
coll.drop();
29+
30+
assert.commandWorked(db.adminCommand({setParameter: 1, notablescan: true}));
31+
32+
{
3433
if (0) {
34+
// TODO: SERVER-2222 This should actually throw an error as it performs a collection
35+
// scan.
3536
assert.throws(function() {
36-
t.find({}).toArray();
37+
coll.find({a: 1}).toArray();
3738
});
3839
}
39-
assert.eq(1, t.find({}).itcount()); // SERVER-274
4040

41+
coll.insert({a: 1});
4142
let err = assert.throws(function() {
42-
t.find({a: 1}).toArray();
43+
coll.count({a: 1});
4344
});
44-
assert.includes(err.toString(), "No indexed plans available, and running with 'notablescan'");
45+
checkError(err);
46+
47+
// TODO: SERVER-2222 This should actually throw an error as it performs a collection scan.
48+
assert.eq(1, coll.find({}).itcount());
49+
50+
err = assert.throws(function() {
51+
coll.find({a: 1}).toArray();
52+
});
53+
checkError(err);
4554

4655
err = assert.throws(function() {
47-
t.find({a: 1}).hint({$natural: 1}).toArray();
56+
coll.find({a: 1}).hint({$natural: 1}).toArray();
4857
});
4958
assert.includes(err.toString(), "$natural");
50-
assert.includes(err.toString(), "notablescan");
51-
52-
t.createIndex({a: 1});
53-
assert.eq(0, t.find({a: 1, b: 1}).itcount());
54-
assert.eq(1, t.find({a: 1, b: null}).itcount());
55-
} finally {
56-
// We assume notablescan was false before this test started and restore that
57-
// expected value.
58-
assert.commandWorked(db._adminCommand({setParameter: 1, notablescan: false}));
59+
checkError(err);
60+
61+
coll.createIndex({a: 1});
62+
assert.eq(0, coll.find({a: 1, b: 1}).itcount());
63+
assert.eq(1, coll.find({a: 1, b: null}).itcount());
64+
}
65+
66+
{ // Run the testcase with a clustered index.
67+
assertDropAndRecreateCollection(db, colName, {clusteredIndex: {key: {_id: 1}, unique: true}})
68+
coll = db.getCollection(colName);
69+
assert.commandWorked(coll.insert({_id: 22}));
70+
assert.eq(1, coll.find({_id: 22}).itcount());
71+
let plan = coll.find({_id: 22}).explain();
72+
// Make sure the plan has a clustered index scan.
73+
assert(isClusteredIxscan(db, plan));
74+
75+
// Make sure the same works with an aggregate.
76+
assert.eq(1, coll.aggregate([{$match: {_id: 22}}]).itcount());
77+
plan = coll.explain().aggregate([{$match: {_id: 22}}]);
78+
// Make sure the plan has a clustered index scan.
79+
assert(isClusteredIxscan(db, plan));
80+
assert.commandWorked(
81+
db.runCommand({aggregate: colName, pipeline: [{$match: {_id: 22}}], cursor: {}}));
5982
}
83+
// Set it back to the original value.
84+
assert.commandWorked(db.adminCommand({setParameter: 1, notablescan: false}));
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Testing if mongos can deal with clustered collections and the related clustered IDX scan bounds
3+
* (SERVER-83119)
4+
*/
5+
6+
import {isClusteredIxscan} from "jstests/libs/analyze_plan.js";
7+
import {assertDropAndRecreateCollection} from "jstests/libs/collection_drop_recreate.js";
8+
9+
const st = new ShardingTest({
10+
shards: 2,
11+
mongos: 1,
12+
});
13+
14+
st.s.adminCommand({enableSharding: "test"});
15+
16+
const db = st.getDB("test");
17+
// Create the collection as a clustered collection.
18+
const coll = assertDropAndRecreateCollection(
19+
db, jsTestName(), {clusteredIndex: {key: {_id: 1}, unique: true}})
20+
st.shardColl(coll, {a: 1});
21+
// First of all check that we can execute the query.
22+
assert.commandWorked(coll.insertMany([...Array(10).keys()].map(i => {
23+
return {_id: i, a: i};
24+
})));
25+
26+
{
27+
var explain = coll.find({_id: 2}).explain();
28+
// Make sure that we have a clusteredIDXScan in the plan.
29+
30+
assert(isClusteredIxscan(db, explain));
31+
assert.commandWorked(
32+
st.getPrimaryShard("test").adminCommand({setParameter: 1, notablescan: 1}));
33+
// Do the same thing only with notablescan enabled.
34+
explain = coll.find({_id: 2}).explain();
35+
assert(isClusteredIxscan(db, explain));
36+
// Sanity count check.
37+
assert.eq(1, coll.find({_id: 2}).itcount());
38+
}
39+
// Test the same with aggregate.
40+
{
41+
var explain = coll.explain().aggregate([{$match: {_id: 22}}]);
42+
assert(isClusteredIxscan(db, explain));
43+
}
44+
45+
st.stop();

jstests/sharding/eof_plan.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* In the case of an always false predicate the shards will return an EOF plan. Additionally this
3+
* tests that mongos can deal with EOF plans coming from the shard.
4+
*/
5+
6+
import {getWinningPlanFromExplain} from 'jstests/libs/analyze_plan.js'
7+
const st = new ShardingTest({
8+
shards: 2,
9+
mongos: 1,
10+
});
11+
12+
st.s.adminCommand({enableSharding: "test"});
13+
const db = st.getDB("test");
14+
const coll = db[jsTestName()];
15+
st.shardColl(coll, {array: 1});
16+
17+
assert.commandWorked(coll.insertMany([...Array(10).keys()].map(i => {
18+
return {_id: i, a: 1};
19+
})));
20+
21+
{
22+
assert.eq(coll.aggregate([
23+
{"$match": {"array": {"$all": []}}},
24+
{"$sort": {"a": -1, "_id": 1}},
25+
{"$limit": 6}
26+
])
27+
.itcount(),
28+
0);
29+
const explain = coll.explain().aggregate([{"$match": {"array": {"$all": []}}}]);
30+
// check that we received an EOF plan
31+
assert.eq(getWinningPlanFromExplain(explain).stage, "EOF");
32+
st.stop();
33+
}

src/mongo/db/query/plan_explainer_sbe.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ void statsToBSON(const QuerySolutionNode* node,
107107

108108
StageType nodeType = node->getType();
109109
if ((nodeType == STAGE_COLLSCAN) &&
110-
static_cast<const CollectionScanNode*>(node)->doSbeClusteredCollectionScan()) {
111-
bob->append("stage", sbeClusteredCollectionScanToString());
110+
static_cast<const CollectionScanNode*>(node)->doClusteredCollectionScanSbe()) {
111+
bob->append("stage", clusteredCollectionScanSbeToString());
112112
} else {
113113
bob->append("stage", stageTypeToString(node->getType()));
114114
}

src/mongo/db/query/query_planner.cpp

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,9 @@ string optionString(size_t options) {
649649
case QueryPlannerParams::GENERATE_PER_COLUMN_FILTERS:
650650
ss << "GENERATE_PER_COLUMN_FILTERS ";
651651
break;
652+
case QueryPlannerParams::STRICT_NO_TABLE_SCAN:
653+
ss << "STRICT_NO_TABLE_SCAN ";
654+
break;
652655
case QueryPlannerParams::DEFAULT:
653656
MONGO_UNREACHABLE;
654657
break;
@@ -1082,13 +1085,42 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> singleSolution(
10821085
return {std::move(out)};
10831086
}
10841087

1085-
bool canTableScan(const QueryPlannerParams& params) {
1086-
return !(params.options & QueryPlannerParams::NO_TABLE_SCAN);
1088+
// If no table scan option is set the planner may not return any plan containing a collection scan.
1089+
// Yet clusteredIdxScans are still allowed as they are not a full collection scan but a bounded
1090+
// collection scan.
1091+
bool noTableScan(const QueryPlannerParams& params) {
1092+
return (params.options & QueryPlannerParams::NO_TABLE_SCAN);
1093+
}
1094+
1095+
// Used internally if the planner should also avoid retruning a plan containing a clusteredIDX scan.
1096+
bool noTableAndClusteredIDXScan(const QueryPlannerParams& params) {
1097+
return (params.options & QueryPlannerParams::STRICT_NO_TABLE_SCAN);
1098+
}
1099+
1100+
bool isClusteredScan(QuerySolutionNode* node) {
1101+
if (node->getType() == STAGE_COLLSCAN) {
1102+
auto collectionScanSolnNode = dynamic_cast<CollectionScanNode*>(node);
1103+
return (collectionScanSolnNode->doClusteredCollectionScanClassic() ||
1104+
collectionScanSolnNode->doClusteredCollectionScanSbe());
1105+
}
1106+
return false;
1107+
}
1108+
1109+
// Check if this is a real coll scan or a hidden ClusteredIDX scan.
1110+
bool isColusteredIDXScanSoln(QuerySolution* collscanSoln) {
1111+
if (collscanSoln->root()->getType() == STAGE_SHARDING_FILTER) {
1112+
auto child = collscanSoln->root()->children.begin();
1113+
return isClusteredScan(child->get());
1114+
}
1115+
if (collscanSoln->root()->getType() == STAGE_COLLSCAN) {
1116+
return isClusteredScan(collscanSoln->root());
1117+
}
1118+
return false;
10871119
}
10881120

10891121
StatusWith<std::vector<std::unique_ptr<QuerySolution>>> attemptCollectionScan(
10901122
const CanonicalQuery& query, bool isTailable, const QueryPlannerParams& params) {
1091-
if (!canTableScan(params)) {
1123+
if (noTableScan(params)) {
10921124
return Status(ErrorCodes::NoQueryExecutionPlans,
10931125
"not allowed to output a collection scan because 'notablescan' is enabled");
10941126
}
@@ -1689,9 +1721,9 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
16891721

16901722
// No indexed plans? We must provide a collscan if possible or else we can't run the query.
16911723
bool collScanRequired = 0 == out.size();
1692-
if (collScanRequired && !canTableScan(params)) {
1724+
if (collScanRequired && noTableAndClusteredIDXScan(params)) {
16931725
return Status(ErrorCodes::NoQueryExecutionPlans,
1694-
"No indexed plans available, and running with 'notablescan'");
1726+
"No indexed plans available, and running with 'notablescan' 1");
16951727
}
16961728

16971729
bool clusteredCollection = params.clusteredInfo.has_value();
@@ -1706,6 +1738,7 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
17061738
return Status(ErrorCodes::NoQueryExecutionPlans, "No query solutions");
17071739
}
17081740

1741+
bool isClusteredIDXScan = false;
17091742
if (possibleToCollscan && (collscanRequested || collScanRequired || clusteredCollection)) {
17101743
boost::optional<int> clusteredScanDirection =
17111744
QueryPlannerCommon::determineClusteredScanDirection(query, params);
@@ -1715,7 +1748,7 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
17151748
return Status(ErrorCodes::NoQueryExecutionPlans,
17161749
"Failed to build collection scan soln");
17171750
}
1718-
1751+
isClusteredIDXScan = isColusteredIDXScanSoln(collscanSoln.get());
17191752
// We consider collection scan in the following cases:
17201753
// 1. collScanRequested - specifically requested by caller.
17211754
// 2. collScanRequired - there are no other possible plans, so we fallback to full scan.
@@ -1736,8 +1769,15 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
17361769
out.push_back(std::move(collscanSoln));
17371770
}
17381771
}
1772+
// Make sure to respect the notablescan option. A clustered IDX scan is allowed even under a
1773+
// NOTABLE option. Only in the case of a strict NOTABLE scan option a clustered IDX scan is not
1774+
// allowed. This option is used in mongoS for shardPruning.
1775+
invariant(out.size() > 0);
1776+
if (collScanRequired && noTableScan(params) && !isClusteredIDXScan) {
1777+
return Status(ErrorCodes::NoQueryExecutionPlans,
1778+
"No indexed plans available, and running with 'notablescan' 2");
1779+
}
17391780

1740-
invariant(!out.empty());
17411781
return {std::move(out)};
17421782
} // QueryPlanner::plan
17431783

src/mongo/db/query/query_planner_params.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ struct QueryPlannerParams {
160160
// applied per-column. This is off by default, since the execution side doesn't support it
161161
// yet.
162162
GENERATE_PER_COLUMN_FILTERS = 1 << 11,
163+
164+
// This is an extension to the NO_TABLE_SCAN parameter. This more stricter option will also
165+
// avoid a CLUSTEREDIDX_SCAN which comes built into a collection scan when the collection is
166+
// clustered.
167+
STRICT_NO_TABLE_SCAN = 1 << 12,
163168
};
164169

165170
// See Options enum above.

src/mongo/db/query/query_solution.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,33 @@ void CollectionScanNode::computeProperties() {
393393
}
394394
}
395395

396+
/*
397+
* IndexBounds exist when this collection scan is a ClusteredIndexScan. Since we need the index
398+
* bounds very seldomly it is built adHoc instead of precomputed.
399+
*/
400+
IndexBounds CollectionScanNode::getIndexBounds() const {
401+
tassert(
402+
8311900,
403+
"Requesting index bounds on a non ClusteredIndexScan (hidden in a collection scan node)",
404+
!(doClusteredCollectionScanClassic() || doClusteredCollectionScanSbe()));
405+
406+
IndexBounds clusteredIdxScanBounds;
407+
BSONObjBuilder maxRecordBson;
408+
maxRecord->appendToBSONAs(&maxRecordBson, clusteredIndex->getName().value());
409+
BSONObjBuilder minRecordBson;
410+
minRecord->appendToBSONAs(&minRecordBson, clusteredIndex->getName().value());
411+
clusteredIdxScanBounds.endKey = maxRecordBson.obj();
412+
clusteredIdxScanBounds.startKey = minRecordBson.obj();
413+
return clusteredIdxScanBounds;
414+
}
415+
396416
void CollectionScanNode::appendToString(str::stream* ss, int indent) const {
397417
addIndent(ss, indent);
398-
*ss << "COLLSCAN\n";
418+
if (doClusteredCollectionScanClassic() || doClusteredCollectionScanSbe()) {
419+
*ss << "CLUSTERED_IDXSCAN\n";
420+
} else {
421+
*ss << "COLLSCAN\n";
422+
}
399423
addIndent(ss, indent + 1);
400424
*ss << "ns = " << toStringForLogging(nss) << '\n';
401425
if (nullptr != filter) {
@@ -413,6 +437,9 @@ std::unique_ptr<QuerySolutionNode> CollectionScanNode::clone() const {
413437
copy->tailable = this->tailable;
414438
copy->direction = this->direction;
415439
copy->isClustered = this->isClustered;
440+
copy->minRecord = this->minRecord;
441+
copy->maxRecord = this->maxRecord;
442+
copy->clusteredIndex = this->clusteredIndex;
416443
copy->isOplog = this->isOplog;
417444
copy->shouldTrackLatestOplogTimestamp = this->shouldTrackLatestOplogTimestamp;
418445
copy->assertTsHasNotFallenOff = this->assertTsHasNotFallenOff;

src/mongo/db/query/query_solution.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ struct CollectionScanNode : public QuerySolutionNodeWithSortSet {
550550
}
551551

552552
// Tells whether this scan will be performed as a clustered collection scan in SBE.
553-
bool doSbeClusteredCollectionScan() const {
553+
bool doClusteredCollectionScanSbe() const {
554554
return (isClustered && !isOplog && (minRecord || maxRecord || resumeAfterRecordId));
555555
}
556556

@@ -563,6 +563,8 @@ struct CollectionScanNode : public QuerySolutionNodeWithSortSet {
563563
eligibleForPlanCache = false;
564564
}
565565

566+
IndexBounds getIndexBounds() const;
567+
566568
std::unique_ptr<QuerySolutionNode> clone() const final;
567569

568570
void hash(absl::HashState state) const override {

0 commit comments

Comments
 (0)