mongo/jstests/core/expr_index_use.js

238 lines
11 KiB
JavaScript

// Confirms expected index use when performing a match with a $expr statement.
(function() {
"use strict";
load("jstests/libs/analyze_plan.js");
const coll = db.expr_index_use;
coll.drop();
assert.commandWorked(coll.insert({a: {b: 1}}));
assert.commandWorked(coll.insert({a: {b: [1]}}));
assert.commandWorked(coll.insert({a: [{b: 1}]}));
assert.commandWorked(coll.insert({a: [{b: [1]}]}));
assert.commandWorked(coll.createIndex({"a.b": 1}));
assert.commandWorked(coll.insert({c: {d: 1}}));
assert.commandWorked(coll.createIndex({"c.d": 1}));
assert.commandWorked(coll.insert({e: [{f: 1}]}));
assert.commandWorked(coll.createIndex({"e.f": 1}));
assert.commandWorked(coll.insert({g: {h: [1]}}));
assert.commandWorked(coll.createIndex({"g.h": 1}));
assert.commandWorked(coll.insert({i: 1, j: [1]}));
assert.commandWorked(coll.createIndex({i: 1, j: 1}));
assert.commandWorked(coll.insert({k: 1, l: "abc"}));
assert.commandWorked(coll.createIndex({k: 1, l: "text"}));
assert.commandWorked(coll.insert({x: 0}));
assert.commandWorked(coll.insert({x: 1, y: 1}));
assert.commandWorked(coll.insert({x: 2, y: 2}));
assert.commandWorked(coll.insert({x: 3, y: 10}));
assert.commandWorked(coll.insert({y: 20}));
assert.commandWorked(coll.createIndex({x: 1, y: 1}));
assert.commandWorked(coll.insert({w: 123}));
assert.commandWorked(coll.insert({}));
assert.commandWorked(coll.insert({w: null}));
assert.commandWorked(coll.insert({w: undefined}));
assert.commandWorked(coll.insert({w: NaN}));
assert.commandWorked(coll.insert({w: "foo"}));
assert.commandWorked(coll.insert({w: "FOO"}));
assert.commandWorked(coll.insert({w: {z: 1}}));
assert.commandWorked(coll.insert({w: {z: 2}}));
assert.commandWorked(coll.createIndex({w: 1}));
assert.commandWorked(coll.createIndex({"w.z": 1}));
/**
* Executes the expression 'expr' as both a find and an aggregate. Then confirms
* 'metricsToCheck', which is an object containing:
* - nReturned: The number of documents the pipeline is expected to return.
* - expectedIndex: Either an index specification object when index use is expected or
* 'null' if a collection scan is expected.
*/
function confirmExpectedExprExecution(expr, metricsToCheck, collation) {
assert(metricsToCheck.hasOwnProperty("nReturned"),
"metricsToCheck must contain an nReturned field");
let aggOptions = {};
if (collation) {
aggOptions.collation = collation;
}
const pipeline = [{$match: {$expr: expr}}];
// Verify that $expr returns the correct number of results when run inside the $match stage
// of an aggregate.
assert.eq(metricsToCheck.nReturned, coll.aggregate(pipeline, aggOptions).itcount());
// Verify that $expr returns the correct number of results when run in a find command.
let cursor = coll.find({$expr: expr});
if (collation) {
cursor = cursor.collation(collation);
}
assert.eq(metricsToCheck.nReturned, cursor.itcount());
// Verify that $expr returns the correct number of results when evaluated inside a $project,
// with optimizations inhibited. We expect the plan to be COLLSCAN.
const pipelineWithProject = [
{$_internalInhibitOptimization: {}},
{$project: {result: {$cond: [expr, true, false]}}},
{$match: {result: true}}
];
assert.eq(metricsToCheck.nReturned, coll.aggregate(pipelineWithProject, aggOptions).itcount());
let explain = coll.explain("executionStats").aggregate(pipelineWithProject, aggOptions);
assert(getAggPlanStage(explain, "COLLSCAN"), tojson(explain));
// Verifies that there are no rejected plans, and that the winning plan uses the expected
// index.
//
// 'getPlanStageFunc' is a function which can be called to obtain stage-specific information
// from the explain output. There are different versions of this function for find and
// aggregate explain output.
function verifyExplainOutput(explain, getPlanStageFunc) {
assert(!hasRejectedPlans(explain), tojson(explain));
if (metricsToCheck.hasOwnProperty("expectedIndex")) {
const stage = getPlanStageFunc(explain, "IXSCAN");
assert.neq(null, stage, tojson(explain));
assert(stage.hasOwnProperty("keyPattern"), tojson(explain));
assert.docEq(stage.keyPattern, metricsToCheck.expectedIndex, tojson(explain));
} else {
assert(getPlanStageFunc(explain, "COLLSCAN"), tojson(explain));
}
}
explain = assert.commandWorked(coll.explain("executionStats").aggregate(pipeline, aggOptions));
verifyExplainOutput(explain, getPlanStage);
cursor = coll.explain("executionStats").find({$expr: expr});
if (collation) {
cursor = cursor.collation(collation);
}
explain = assert.commandWorked(cursor.finish());
verifyExplainOutput(explain, getPlanStage);
}
// Comparison of field and constant.
confirmExpectedExprExecution({$eq: ["$x", 1]}, {nReturned: 1, expectedIndex: {x: 1, y: 1}});
confirmExpectedExprExecution({$eq: [1, "$x"]}, {nReturned: 1, expectedIndex: {x: 1, y: 1}});
// $and with both children eligible for index use.
confirmExpectedExprExecution({$and: [{$eq: ["$x", 2]}, {$eq: ["$y", 2]}]},
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
// $and with one child eligible for index use and one that is not.
confirmExpectedExprExecution({$and: [{$eq: ["$x", 1]}, {$eq: ["$x", "$y"]}]},
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
// $and with one child eligible for index use and a second child containing a $or where one of
// the two children are eligible.
confirmExpectedExprExecution(
{$and: [{$eq: ["$x", 1]}, {$or: [{$eq: ["$x", "$y"]}, {$eq: ["$y", 1]}]}]},
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
// Equality comparison against non-multikey dotted path field is expected to use an index.
confirmExpectedExprExecution({$eq: ["$c.d", 1]}, {nReturned: 1, expectedIndex: {"c.d": 1}});
// $lt, $lte, $gt, $gte, $in, $ne, and $cmp are not expected to use an index. This is because we
// have not yet implemented a rewrite of these operators to indexable MatchExpression.
confirmExpectedExprExecution({$lt: ["$x", 1]}, {nReturned: 20});
confirmExpectedExprExecution({$lt: [1, "$x"]}, {nReturned: 2});
confirmExpectedExprExecution({$lte: ["$x", 1]}, {nReturned: 21});
confirmExpectedExprExecution({$lte: [1, "$x"]}, {nReturned: 3});
confirmExpectedExprExecution({$gt: ["$x", 1]}, {nReturned: 2});
confirmExpectedExprExecution({$gt: [1, "$x"]}, {nReturned: 20});
confirmExpectedExprExecution({$gte: ["$x", 1]}, {nReturned: 3});
confirmExpectedExprExecution({$gte: [1, "$x"]}, {nReturned: 21});
confirmExpectedExprExecution({$in: ["$x", [1, 3]]}, {nReturned: 2});
confirmExpectedExprExecution({$cmp: ["$x", 1]}, {nReturned: 22});
confirmExpectedExprExecution({$ne: ["$x", 1]}, {nReturned: 22});
// Comparison with an array value is not expected to use an index.
confirmExpectedExprExecution({$eq: ["$a.b", [1]]}, {nReturned: 2});
confirmExpectedExprExecution({$eq: ["$w", [1]]}, {nReturned: 0});
// A constant expression is not expected to use an index.
confirmExpectedExprExecution(1, {nReturned: 23});
confirmExpectedExprExecution(false, {nReturned: 0});
confirmExpectedExprExecution({$eq: [1, 1]}, {nReturned: 23});
confirmExpectedExprExecution({$eq: [0, 1]}, {nReturned: 0});
// Comparison of 2 fields is not expected to use an index.
confirmExpectedExprExecution({$eq: ["$x", "$y"]}, {nReturned: 20});
// Comparison against multikey field not expected to use an index.
confirmExpectedExprExecution({$eq: ["$a.b", 1]}, {nReturned: 1});
confirmExpectedExprExecution({$eq: ["$e.f", [1]]}, {nReturned: 1});
confirmExpectedExprExecution({$eq: ["$e.f", 1]}, {nReturned: 0});
confirmExpectedExprExecution({$eq: ["$g.h", [1]]}, {nReturned: 1});
confirmExpectedExprExecution({$eq: ["$g.h", 1]}, {nReturned: 0});
// Comparison against a non-multikey field of a multikey index can use an index
const metricsToCheck = {
nReturned: 1
};
metricsToCheck.expectedIndex = {
i: 1,
j: 1
};
confirmExpectedExprExecution({$eq: ["$i", 1]}, metricsToCheck);
metricsToCheck.nReturned = 0;
confirmExpectedExprExecution({$eq: ["$i", 2]}, metricsToCheck);
// Equality to NaN can use an index.
confirmExpectedExprExecution({$eq: ["$w", NaN]}, {nReturned: 1, expectedIndex: {w: 1}});
// Equality to undefined and equality to missing cannot use an index.
confirmExpectedExprExecution({$eq: ["$w", undefined]}, {nReturned: 16});
confirmExpectedExprExecution({$eq: ["$w", "$$REMOVE"]}, {nReturned: 16});
// Equality to null can use an index.
confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: 1}});
// Equality inside a nested object can use a non-multikey index.
confirmExpectedExprExecution({$eq: ["$w.z", 2]}, {nReturned: 1, expectedIndex: {"w.z": 1}});
// Test that the collation is respected. Since the collations do not match, we should not use
// the index.
const caseInsensitiveCollation = {
locale: "en_US",
strength: 2
};
if (db.getMongo().useReadCommands()) {
confirmExpectedExprExecution({$eq: ["$w", "FoO"]}, {nReturned: 2}, caseInsensitiveCollation);
}
// Test equality queries against a hashed index.
assert.commandWorked(coll.dropIndex({w: 1}));
assert.commandWorked(coll.createIndex({w: "hashed"}));
confirmExpectedExprExecution({$eq: ["$w", 123]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
confirmExpectedExprExecution({$eq: ["$w", NaN]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
confirmExpectedExprExecution({$eq: ["$w", undefined]}, {nReturned: 16});
confirmExpectedExprExecution({$eq: ["$w", "$$REMOVE"]}, {nReturned: 16});
// Equality match against text index prefix is expected to fail. Equality predicates are
// required against the prefix fields of a text index, but currently $eq inside $expr does not
// qualify.
assert.throws(
() => coll.aggregate([{$match: {$expr: {$eq: ["$k", 1]}, $text: {$search: "abc"}}}]).itcount());
// Test that equality match in $expr respects the collection's default collation, both when
// there is an index with a matching collation and when there isn't.
assert.commandWorked(db.runCommand({drop: coll.getName()}));
assert.commandWorked(db.createCollection(coll.getName(), {collation: caseInsensitiveCollation}));
assert.commandWorked(coll.insert({a: "foo", b: "bar"}));
assert.commandWorked(coll.insert({a: "FOO", b: "BAR"}));
assert.commandWorked(coll.createIndex({a: 1}));
assert.commandWorked(coll.createIndex({b: 1}, {collation: {locale: "simple"}}));
confirmExpectedExprExecution({$eq: ["$a", "foo"]}, {nReturned: 2, expectedIndex: {a: 1}});
confirmExpectedExprExecution({$eq: ["$b", "bar"]}, {nReturned: 2});
})();