summaryrefslogtreecommitdiff
path: root/jstests/core/wildcard_index_cached_plans.js
blob: 50c59865377993e2c42fbdc7e14f984af61945bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
 * Test that cached plans which use wildcard indexes work.
 *
 * @tags: [
 *   # This test attempts to perform queries and introspect the server's plan cache entries using
 *   # the $planCacheStats aggregation source. Both operations must be routed to the primary, and
 *   # the latter only supports 'local' readConcern.
 *   assumes_read_preference_unchanged,
 *   assumes_read_concern_unchanged,
 *   does_not_support_stepdowns,
 *   # If the balancer is on and chunks are moved, the plan cache can have entries with isActive:
 *   # false when the test assumes they are true because the query has already been run many times.
 *   assumes_balancer_off,
 *   inspects_whether_plan_cache_entry_is_active,
 *   # This test makes assertions about the types of plans produced by the query engine, which has
 *   # changed from the classic engine starting in version 5.0.
 *   requires_fcv_50,
 * ]
 */
(function() {
"use strict";

load('jstests/libs/analyze_plan.js');              // For getPlanStage().
load("jstests/libs/collection_drop_recreate.js");  // For assert[Drop|Create]Collection.
load('jstests/libs/fixture_helpers.js');  // For getPrimaryForNodeHostingDatabase and isMongos.
load("jstests/libs/sbe_util.js");         // For checkSBEEnabled.

const coll = db.wildcard_cached_plans;
coll.drop();

assert.commandWorked(coll.createIndex({"b.$**": 1}));
assert.commandWorked(coll.createIndex({"a": 1}));

// In order for the plan cache to be used, there must be more than one plan available. Insert
// data into the collection such that the b.$** index will be far more selective than the index
// on 'a' for the query {a: 1, b: 1}.
for (let i = 0; i < 1000; i++) {
    assert.commandWorked(coll.insert({a: 1}));
}
assert.commandWorked(coll.insert({a: 1, b: 1}));

function getCacheEntryForQuery(query) {
    const aggRes = FixtureHelpers.getPrimaryForNodeHostingDatabase(db)
                       .getCollection(coll.getFullName())
                       .aggregate([
                           {$planCacheStats: {}},
                           {$match: {createdFromQuery: {query: query, sort: {}, projection: {}}}}
                       ])
                       .toArray();
    assert.lte(aggRes.length, 1);
    if (aggRes.length > 0) {
        return aggRes[0];
    }
    return null;
}

function getPlanCacheKeyFromExplain(explainRes) {
    const hash = FixtureHelpers.isMongos(db)
        ? explainRes.queryPlanner.winningPlan.shards[0].planCacheKey
        : explainRes.queryPlanner.planCacheKey;
    assert.eq(typeof (hash), "string");
    return hash;
}

function getPlanCacheKey(query) {
    return getPlanCacheKeyFromExplain(assert.commandWorked(coll.explain().find(query).finish()));
}

const query = {
    a: 1,
    b: 1
};

// The plan cache should be empty.
assert.eq(getCacheEntryForQuery(query), null);

// Run the query twice, once to create the cache entry, and again to make the cache entry
// active.
for (let i = 0; i < 2; i++) {
    assert.eq(coll.find(query).itcount(), 1);
}

// The plan cache should no longer be empty. Check that the chosen plan uses the b.$** index.
const cacheEntry = getCacheEntryForQuery(query);
assert.neq(cacheEntry, null);
assert.eq(cacheEntry.isActive, true);
// Should be at least two plans: one using the {a: 1} index and the other using the b.$** index.
assert.gte(cacheEntry.creationExecStats.length, 2, tojson(cacheEntry.plans));

const isSBEEnabled = checkSBEEnabled(db);

// In SBE index scan stage does not serialize key pattern in execution stats, so we use IXSCAN from
// the query plan instead.
const plan = isSBEEnabled ? cacheEntry.cachedPlan.queryPlan
                          : cacheEntry.creationExecStats[0].executionStages;
const ixScanStage = getPlanStage(plan, "IXSCAN");
assert.neq(ixScanStage, null, () => tojson(plan));
assert.eq(ixScanStage.keyPattern, {"$_path": 1, "b": 1}, () => tojson(plan));

// Run the query again. This time it should use the cached plan. We should get the same result
// as earlier.
assert.eq(coll.find(query).itcount(), 1);

// Now run a query where b is null. This should have a different shape key from the previous
// query since $** indexes are sparse.
const queryWithBNull = {
    a: 1,
    b: null
};
for (let i = 0; i < 2; i++) {
    assert.eq(coll.find({a: 1, b: null}).itcount(), 1000);
}
assert.neq(getPlanCacheKey(queryWithBNull), getPlanCacheKey(query));

// There should only have been one solution for the above query, so it would not get cached.
assert.eq(getCacheEntryForQuery({a: 1, b: null}), null);

// Check that indexability discriminators work with collations.
(function() {
// Create wildcard index with a collation.
assertDropAndRecreateCollection(db, coll.getName(), {collation: {locale: "en_US", strength: 1}});
assert.commandWorked(coll.createIndex({"b.$**": 1}));

// Run a query which uses a different collation from that of the index, but does not use
// string bounds.
const queryWithoutStringExplain =
    coll.explain().find({a: 5, b: 5}).collation({locale: "fr"}).finish();
let ixScans = getPlanStages(getWinningPlan(queryWithoutStringExplain.queryPlanner), "IXSCAN");
assert.eq(ixScans.length, FixtureHelpers.numberOfShardsForCollection(coll));
assert.eq(ixScans[0].keyPattern, {$_path: 1, b: 1});

// Run a query which uses a different collation from that of the index and does have string
// bounds.
const queryWithStringExplain =
    coll.explain().find({a: 5, b: "a string"}).collation({locale: "fr"}).finish();
ixScans = getPlanStages(getWinningPlan(queryWithStringExplain.queryPlanner), "IXSCAN");
assert.eq(ixScans.length, 0);

// Check that the shapes are different since the query which matches on a string will not
// be eligible to use the b.$** index (since the index has a different collation).
assert.neq(getPlanCacheKeyFromExplain(queryWithoutStringExplain),
           getPlanCacheKeyFromExplain(queryWithStringExplain));
})();

// Check that indexability discriminators work with partial wildcard indexes.
(function() {
assertDropAndRecreateCollection(db, coll.getName());
assert.commandWorked(coll.createIndex({"$**": 1}, {partialFilterExpression: {a: {$lte: 5}}}));

// Run a query for a value included by the partial filter expression.
const queryIndexedExplain = coll.find({a: 4}).explain();
let ixScans = getPlanStages(getWinningPlan(queryIndexedExplain.queryPlanner), "IXSCAN");
assert.eq(ixScans.length, FixtureHelpers.numberOfShardsForCollection(coll));
assert.eq(ixScans[0].keyPattern, {$_path: 1, a: 1});

// Run a query which tries to get a value not included by the partial filter expression.
const queryUnindexedExplain = coll.find({a: 100}).explain();
ixScans = getPlanStages(getWinningPlan(queryUnindexedExplain.queryPlanner), "IXSCAN");
assert.eq(ixScans.length, 0);

// Check that the shapes are different since the query which searches for a value not
// included by the partial filter expression won't be eligible to use the $** index.
assert.neq(getPlanCacheKeyFromExplain(queryIndexedExplain),
           getPlanCacheKeyFromExplain(queryUnindexedExplain));
})();
})();