summaryrefslogtreecommitdiff
path: root/jstests/core/wildcard_index_cached_plans.js
blob: 9b0b0c33b6d2c5775da51b6de5817ce37ac39b06 (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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
 * 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,
 * ]
 */
(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.
load("jstests/libs/sbe_explain_helpers.js");  // For engineSpecificAssertion.

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

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

const sbePlanCacheEnabled = checkSBEEnabled(db, ["featureFlagSbePlanCache", "featureFlagSbeFull"]);

// 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 match = {
        planCacheKey: getPlanCacheKeyFromShape({query: query, collection: coll, db: db})
    };
    const aggRes = FixtureHelpers.getPrimaryForNodeHostingDatabase(db)
                       .getCollection(coll.getFullName())
                       .aggregate([{$planCacheStats: {}}, {$match: match}])
                       .toArray();
    assert.lte(aggRes.length, 1);
    if (aggRes.length > 0) {
        return aggRes[0];
    }
    return null;
}

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.
let cacheEntry = getCacheEntryForQuery(query);
assert.neq(cacheEntry, null);
assert.eq(cacheEntry.isActive, true);
if (!sbePlanCacheEnabled) {
    // 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));

    // In SBE index scan stage does not serialize key pattern in execution stats, so we use IXSCAN
    // from the query plan instead.
    const sbeIxScan = function() {
        const cachedPlan = cacheEntry.cachedPlan;
        if (!cachedPlan)
            return null;
        if (!cachedPlan.queryPlan)
            return null;
        return getPlanStage(cachedPlan.queryPlan, "IXSCAN");
    }();

    const classicIxScan = function() {
        const execStats = cacheEntry.creationExecStats;
        if (!execStats)
            return null;
        const elem = execStats[0];
        if (!elem)
            return null;
        if (!elem.executionStages)
            return null;
        return getPlanStage(elem.executionStages, "IXSCAN");
    }();
    const expectedKeyPattern = {"$_path": 1, "b": 1};
    const classicKeyPatternMatch =
        classicIxScan !== null && bsonWoCompare(classicIxScan.keyPattern, expectedKeyPattern) === 0;
    const sbeKeyPatternmatch =
        sbeIxScan !== null && bsonWoCompare(sbeIxScan.keyPattern, expectedKeyPattern) === 0;
    engineSpecificAssertion(classicKeyPatternMatch, sbeKeyPatternmatch, db, tojson(cacheEntry));
}

// 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(getPlanCacheKeyFromShape({query: queryWithBNull, collection: coll, db: db}),
           getPlanCacheKeyFromShape({query: query, collection: coll, db: db}));

// There should only have been one solution for the above query, so it would get cached only by the
// SBE plan cache.
cacheEntry = getCacheEntryForQuery({a: 1, b: null});
if (sbePlanCacheEnabled) {
    assert.neq(cacheEntry, null);
    assert.eq(cacheEntry.isActive, true, cacheEntry);
    assert.eq(cacheEntry.isPinned, true, cacheEntry);
} else {
    assert.eq(cacheEntry, 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, db),
           getPlanCacheKeyFromExplain(queryWithStringExplain, db));
})();

// 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, db),
           getPlanCacheKeyFromExplain(queryUnindexedExplain, db));
})();
})();