222 lines
7.1 KiB
JavaScript
222 lines
7.1 KiB
JavaScript
/**
|
|
* Tests for classic query optimization with partial indices: do not generate fetch stage in the
|
|
* plan if a query predicate is satisfied by the filter expression of the chosen partial index. If
|
|
* the fetch phase is needed for another reason, make sure that the predicate is not in the fetch
|
|
* filter.
|
|
*
|
|
* @tags: [
|
|
* # the test conflicts with hidden wildcard indexes
|
|
* assumes_no_implicit_index_creation,
|
|
* does_not_support_stepdowns,
|
|
* multiversion_incompatible,
|
|
* requires_fcv_70,
|
|
* ]
|
|
*/
|
|
|
|
import {
|
|
assertFetchFilter,
|
|
assertNoFetchFilter,
|
|
assertStagesForExplainOfCommand,
|
|
getWinningPlanFromExplain,
|
|
isIxscan,
|
|
} from "jstests/libs/query/analyze_plan.js";
|
|
|
|
function flagVal(n) {
|
|
return (n % 5 > 3) ? true : false;
|
|
}
|
|
|
|
function stateVal(n) {
|
|
const states = ["open", "closed", "unknown"];
|
|
return states[n % 3];
|
|
}
|
|
|
|
function getDocs(len, start = 0) {
|
|
return Array.from({length: len}, (_, i) => ({
|
|
_id: start + i,
|
|
a: i,
|
|
b: i + 3,
|
|
c: [i, i + 5],
|
|
flag: flagVal(i),
|
|
state: stateVal(i),
|
|
array: [{a: i, state: stateVal(i)}, {b: i}]
|
|
}));
|
|
}
|
|
|
|
const coll = db.partial_index_opt;
|
|
coll.drop();
|
|
assert.commandWorked(coll.insertMany(getDocs(100)));
|
|
assert.commandWorked(coll.insertMany([
|
|
{
|
|
_id: 100,
|
|
a: 100,
|
|
state: "open",
|
|
array: [{a: 100, state: "closed"}, {a: 101, state: "closed"}]
|
|
},
|
|
{_id: 101, a: 101, state: "open", array: [{a: 101, state: "open"}]},
|
|
{_id: 102, a: 102, state: "closed", array: [{a: 102, state: "open"}]}
|
|
]));
|
|
|
|
const expectedStagesCount = ["COUNT", "COUNT_SCAN"];
|
|
|
|
assert.commandWorked(coll.createIndex({a: 1}, {"partialFilterExpression": {flag: true}}));
|
|
let cmdObj = {find: coll.getName(), filter: {flag: true, a: 4}, projection: {_id: 0, a: 1}};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// The following plan has a fetch phase because of the projection, but no filter on it.
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {flag: true, a: 4},
|
|
projection: {a: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Count command.
|
|
cmdObj = {
|
|
count: coll.getName(),
|
|
query: {flag: true, a: 4}
|
|
};
|
|
assertStagesForExplainOfCommand({
|
|
coll: coll,
|
|
cmdObj: cmdObj,
|
|
expectedStages: expectedStagesCount,
|
|
stagesNotExpected: ["FETCH"]
|
|
});
|
|
|
|
// Partial index with filter expression with conjunction.
|
|
assert.commandWorked(coll.createIndex(
|
|
{a: 1}, {name: "a_1_range", "partialFilterExpression": {a: {$gte: 20, $lte: 40}}}));
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {a: {$gte: 20, $lte: 40}},
|
|
projection: {_id: 0, a: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {a: {$gte: 25, $lte: 30}},
|
|
projection: {_id: 0, a: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Partial index with compound key.
|
|
assert.commandWorked(coll.createIndex({a: 1, b: 1}, {"partialFilterExpression": {flag: true}}));
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {a: {$gte: 50}, b: {$in: [55, 57, 59, 62]}, flag: true},
|
|
projection: {_id: 0, a: 1, b: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Filter expression with conjunction on multiple fields.
|
|
assert.commandWorked(coll.createIndex(
|
|
{b: 1}, {name: "b_1_state_open", "partialFilterExpression": {state: "open", b: {$gt: 50}}}));
|
|
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {state: "open", b: {$gt: 80}},
|
|
projection: {_id: 0, b: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
cmdObj = {
|
|
count: coll.getName(),
|
|
query: {state: "open", b: {$gt: 50}}
|
|
};
|
|
assertStagesForExplainOfCommand({
|
|
coll: coll,
|
|
cmdObj: cmdObj,
|
|
expectedStages: expectedStagesCount,
|
|
stagesNotExpected: ["FETCH"]
|
|
});
|
|
|
|
// Index filter expression with $exists.
|
|
assert.commandWorked(coll.createIndex(
|
|
{a: 1}, {name: "a_1_b_exists", "partialFilterExpression": {b: {$exists: true}}}));
|
|
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {a: {$gte: 90}, b: {$exists: true}},
|
|
projection: {_id: 0, a: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Filter expression in a multi-key index.
|
|
assert.commandWorked(
|
|
coll.createIndex({c: 1}, {name: "c_1_a", partialFilterExpression: {a: {$lte: 30}}}));
|
|
|
|
cmdObj = {
|
|
count: coll.getName(),
|
|
query: {c: {$lte: 50}, a: {$lte: 30}}
|
|
};
|
|
assertStagesForExplainOfCommand({
|
|
coll: coll,
|
|
cmdObj: cmdObj,
|
|
expectedStages: expectedStagesCount,
|
|
stagesNotExpected: ["FETCH"]
|
|
});
|
|
|
|
// The following plan has a fetch phase, but no filter on 'a'.
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {c: {$lte: 50}, a: {$lte: 30}},
|
|
projection: {_id: 0, c: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Test that the same filter expression under $elemMatch will not be removed from the fetch filter.
|
|
assert.commandWorked(
|
|
coll.createIndex({a: 1}, {name: "a_1_state_open", "partialFilterExpression": {state: "open"}}));
|
|
|
|
let predicate = {
|
|
a: {$gte: 100},
|
|
state: "open",
|
|
array: {$elemMatch: {$and: [{a: {$gte: 100}}, {state: "open"}]}}
|
|
};
|
|
let fetchFilter = {
|
|
"array": {"$elemMatch": {"$and": [{"a": {"$gte": 100}}, {"state": {"$eq": "open"}}]}}
|
|
};
|
|
assertFetchFilter({coll: coll, predicate: predicate, expectedFilter: fetchFilter, nReturned: 1});
|
|
|
|
// Index on $elemMatch predicate. Test that the index filter predicate is removed from the fetch
|
|
// filter while $elemMatch predicate is preserved.
|
|
assert.commandWorked(coll.createIndex(
|
|
{"array.a": 1}, {name: "array_a_1_state_open", "partialFilterExpression": {state: "open"}}));
|
|
|
|
predicate = {
|
|
state: "open",
|
|
array: {$elemMatch: {$and: [{a: {$gte: 100}}, {state: "open"}]}}
|
|
};
|
|
fetchFilter = {
|
|
"array": {"$elemMatch": {"$and": [{"a": {"$gte": 100}}, {"state": {"$eq": "open"}}]}}
|
|
};
|
|
assertFetchFilter({coll: coll, predicate: predicate, expectedFilter: fetchFilter, nReturned: 1});
|
|
|
|
// Test for index filter expression over nested field.
|
|
assert.commandWorked(coll.createIndex(
|
|
{"array.a": 1},
|
|
{name: "array_a_1_array_state_open", "partialFilterExpression": {"array.state": "open"}}));
|
|
|
|
cmdObj = {
|
|
find: coll.getName(),
|
|
filter: {$and: [{"array.a": {$gte: 100}}, {"array.state": "open"}]},
|
|
projection: {_id: 0, array: 1}
|
|
};
|
|
assertNoFetchFilter({coll: coll, cmdObj: cmdObj});
|
|
|
|
// Tests that the query predicate is not removed if it is a subset of an $or index filter.
|
|
assert.commandWorked(coll.createIndex(
|
|
{a: 1}, {name: "a_1_or", "partialFilterExpression": {$or: [{b: {$gte: 80}}, {flag: "true"}]}}));
|
|
|
|
predicate = {
|
|
$and: [{a: {$gte: 75}}, {b: {$gte: 80}}]
|
|
};
|
|
fetchFilter = {
|
|
"b": {"$gte": 80}
|
|
};
|
|
assertFetchFilter({coll: coll, predicate: predicate, expectedFilter: fetchFilter, nReturned: 23});
|
|
|
|
const exp =
|
|
coll.find({$and: [{a: {$gte: 90}}, {$or: [{b: {$gte: 80}}, {flag: "true"}]}]}).explain();
|
|
assert(isIxscan(db, getWinningPlanFromExplain(exp)), "Expected index scan, got " + tojson(exp));
|