summaryrefslogtreecommitdiff
path: root/jstests/core/expr_index_use.js
diff options
context:
space:
mode:
authorDavid Storch <david.storch@10gen.com>2018-01-02 19:23:56 -0500
committerDavid Storch <david.storch@10gen.com>2018-01-04 13:29:33 -0500
commit9282f7813d97756f0ab1e635d57e84b7145560d7 (patch)
tree2290734bce1fc654f1f3e5879af59129c165921c /jstests/core/expr_index_use.js
parent2790db08f218a0c909b1508bc2f1650e7e81c6c3 (diff)
downloadmongo-9282f7813d97756f0ab1e635d57e84b7145560d7.tar.gz
SERVER-31760 Optimize $expr to allow index use for equality predicates.
Diffstat (limited to 'jstests/core/expr_index_use.js')
-rw-r--r--jstests/core/expr_index_use.js246
1 files changed, 246 insertions, 0 deletions
diff --git a/jstests/core/expr_index_use.js b/jstests/core/expr_index_use.js
new file mode 100644
index 00000000000..adfce04a72a
--- /dev/null
+++ b/jstests/core/expr_index_use.js
@@ -0,0 +1,246 @@
+// Confirms expected index use when performing a match with a $expr statement.
+
+(function() {
+ "use strict";
+
+ load("jstests/libs/analyze_plan.js");
+
+ const isMMAPv1 = jsTest.options().storageEngine === "mmapv1";
+
+ const coll = db.expr_index_use;
+ coll.drop();
+
+ assert.writeOK(coll.insert({a: {b: 1}}));
+ assert.writeOK(coll.insert({a: {b: [1]}}));
+ assert.writeOK(coll.insert({a: [{b: 1}]}));
+ assert.writeOK(coll.insert({a: [{b: [1]}]}));
+ assert.commandWorked(coll.createIndex({"a.b": 1}));
+
+ assert.writeOK(coll.insert({c: {d: 1}}));
+ assert.commandWorked(coll.createIndex({"c.d": 1}));
+
+ assert.writeOK(coll.insert({e: [{f: 1}]}));
+ assert.commandWorked(coll.createIndex({"e.f": 1}));
+
+ assert.writeOK(coll.insert({g: {h: [1]}}));
+ assert.commandWorked(coll.createIndex({"g.h": 1}));
+
+ assert.writeOK(coll.insert({i: 1, j: [1]}));
+ assert.commandWorked(coll.createIndex({i: 1, j: 1}));
+
+ assert.writeOK(coll.insert({k: 1, l: "abc"}));
+ assert.commandWorked(coll.createIndex({k: 1, l: "text"}));
+
+ assert.writeOK(coll.insert({x: 0}));
+ assert.writeOK(coll.insert({x: 1, y: 1}));
+ assert.writeOK(coll.insert({x: 2, y: 2}));
+ assert.writeOK(coll.insert({x: 3, y: 10}));
+ assert.writeOK(coll.insert({y: 20}));
+ assert.commandWorked(coll.createIndex({x: 1, y: 1}));
+
+ assert.writeOK(coll.insert({w: 123}));
+ assert.writeOK(coll.insert({}));
+ assert.writeOK(coll.insert({w: null}));
+ assert.writeOK(coll.insert({w: undefined}));
+ assert.writeOK(coll.insert({w: NaN}));
+ assert.writeOK(coll.insert({w: "foo"}));
+ assert.writeOK(coll.insert({w: "FOO"}));
+ assert.writeOK(coll.insert({w: {z: 1}}));
+ assert.writeOK(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, getAggPlanStage);
+
+ cursor = coll.explain("executionStats").find({$expr: expr});
+ if (collation) {
+ cursor = cursor.collation(collation);
+ }
+ explain = assert.commandWorked(cursor.finish());
+ verifyExplainOutput(
+ explain,
+ (explain, stage) => getPlanStage(explain.executionStats.executionStages, stage));
+ }
+
+ // 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, on storage
+ // engines other than MMAPv1.
+ const metricsToCheck = {nReturned: 1};
+ if (!isMMAPv1) {
+ 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});
+
+ // Test that equality to null queries can use a sparse index.
+ assert.commandWorked(coll.dropIndex({w: "hashed"}));
+ assert.commandWorked(coll.createIndex({w: 1}, {sparse: true}));
+ confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: 1}});
+
+ // 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.writeOK(coll.insert({a: "foo", b: "bar"}));
+ assert.writeOK(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});
+})();