summaryrefslogtreecommitdiff
path: root/jstests/noPassthroughWithMongod/plan_cache_replanning.js
blob: d293413f0fc8b14da91e2ba91d55ef2ae27ed98e (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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
 * This test will attempt to create a scenario where the plan cache entry for a given query shape
 * oscillates. It achieves this by creating two indexes, A and B, on a collection, and interleaving
 * queries which are "ideal" for index A with queries that are "ideal" for index B.
 */
(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/sbe_util.js");                  // For checkSBEEnabled.

const isSbeEnabled = checkSBEEnabled(db);

let coll = assertDropAndRecreateCollection(db, "plan_cache_replanning");

function getCachedPlanForQuery(filter) {
    const planCacheKey = getPlanCacheKeyFromShape({query: filter, collection: coll, db: db});
    const matchingCacheEntries = coll.getPlanCache().list([{$match: {planCacheKey: planCacheKey}}]);
    assert.eq(matchingCacheEntries.length, 1, coll.getPlanCache().list());
    return matchingCacheEntries[0];
}

/**
 * Asserts that the plan contained in the plan cache 'entry' is an index scan plan over the index
 * with the given 'indexName'.
 *
 * Also verifies that the query hash matches the provided 'expectedQueryHash'.
 */
function assertPlanHasIxScanStage(entry, indexName, expectedQueryHash) {
    assert.eq(entry.queryHash, expectedQueryHash, entry);

    const cachedPlan = getCachedPlan(entry.cachedPlan);
    if (isSbeEnabled) {
        // The $planCacheStats output for the SBE plan cache only contains an debug string
        // representation of the execution plan. Rather than parse this string, we just check that
        // the index name appears somewhere in the plan.
        assert.eq(entry.version, "2", entry);
        assert(cachedPlan.hasOwnProperty("stages"));
        const planDebugString = cachedPlan.stages;
        assert(planDebugString.includes(indexName), entry);
    } else {
        assert.eq(entry.version, "1", entry);
        const stage = getPlanStage(cachedPlan, "IXSCAN");
        assert.neq(stage, null, entry);
        assert.eq(indexName, stage.indexName, entry);
    }
}

// Carefully construct a collection so that some queries will do well with an {a: 1} index and
// others with a {b: 1} index.
for (let i = 1000; i < 1100; i++) {
    assert.commandWorked(coll.insert({a: 1, b: i}));
}

for (let i = 1000; i < 1100; i++) {
    assert.commandWorked(coll.insert({a: i, b: 2}));
}

// This query will be quick with {a: 1} index, and far slower {b: 1} index. With the {a: 1} index,
// the server should only need to examine one document. Using {b: 1}, it will have to scan through
// each document which has 2 as the value of the 'b' field.
const aIndexQuery = {
    a: 1099,
    b: 2
};
// Opposite of 'aIndexQuery'. Should be quick if the {b: 1} index is used, and slower if the {a: 1}
// index is used.
const bIndexQuery = {
    a: 1,
    b: 1099
};

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

// Run a query where the {b: 1} index will easily win.
assert.eq(1, coll.find(bIndexQuery).itcount());

// The plan cache should now hold an inactive entry.
let entry = getCachedPlanForQuery(bIndexQuery);
let queryHash = entry.queryHash;
let entryWorks = entry.works;
assert.eq(entry.isActive, false);
assertPlanHasIxScanStage(entry, "b_1", queryHash);

// Re-run the query. The inactive cache entry should be promoted to an active entry.
assert.eq(1, coll.find(bIndexQuery).itcount());
entry = getCachedPlanForQuery(bIndexQuery);
assert.eq(entry.isActive, true);
assert.eq(entry.works, entryWorks);
assertPlanHasIxScanStage(entry, "b_1", queryHash);

// Now we will attempt to oscillate the cache entry by interleaving queries which should use the
// {a:1} and {b:1} index. When the plan using the {b: 1} index is in the cache, running a query
// which should use the {a: 1} index will perform very poorly, and trigger replanning (and vice
// versa).

// The {b: 1} plan is currently in the cache. Run the query which should use the {a: 1} index. The
// current cache entry will be deactivated, and then the cache entry for the {a: 1} will overwrite
// it (as active).
assert.eq(1, coll.find(aIndexQuery).itcount());
entry = getCachedPlanForQuery(aIndexQuery);
assert.eq(entry.isActive, true);
assertPlanHasIxScanStage(entry, "a_1", queryHash);

// Run the query which should use the {b: 1} index.
assert.eq(1, coll.find(bIndexQuery).itcount());
entry = getCachedPlanForQuery(bIndexQuery);
assert.eq(entry.isActive, true);
assertPlanHasIxScanStage(entry, "b_1", queryHash);

// The {b: 1} plan is again in the cache. Run the query which should use the {a: 1} index.
assert.eq(1, coll.find(aIndexQuery).itcount());
entry = getCachedPlanForQuery(aIndexQuery);
assert.eq(entry.isActive, true);
assertPlanHasIxScanStage(entry, "a_1", queryHash);

// The {a: 1} plan is back in the cache. Run the query which would perform better on the plan using
// the {b: 1} index, and ensure that plan gets written to the cache.
assert.eq(1, coll.find(bIndexQuery).itcount());
entry = getCachedPlanForQuery(bIndexQuery);
entryWorks = entry.works;
assert.eq(entry.isActive, true);
assertPlanHasIxScanStage(entry, "b_1", queryHash);

// Now run a plan that will perform poorly with both indices (it will be required to scan 500
// documents). This will result in replanning (and the cache entry being deactivated). However, the
// new plan will have a very high works value, and will replace the existing cache entry with a new
// cache entry whose works value got updated to the new higher value.
for (let i = 0; i < 500; i++) {
    assert.commandWorked(coll.insert({a: 3, b: 3}));
}
assert.eq(500, coll.find({a: 3, b: 3}).itcount());

// The cache entry should have been deactivated.
entry = getCachedPlanForQuery({a: 3, b: 3});
assert.eq(entry.isActive, false);
assertPlanHasIxScanStage(entry, "a_1", queryHash);

// The works value should have doubled.
assert.eq(entry.works, entryWorks * 2);

// Drop and recreate the collection. Now we test that the query system does not replan in cases
// where the plan is performing only slightly less efficiently than the cached plan.
coll = assertDropAndRecreateCollection(db, "plan_cache_replanning");

{
    assert.commandWorked(coll.createIndex({selectiveKey: 1, tiebreak: 1}));
    assert.commandWorked(coll.createIndex({notSelectiveKey: 1, tiebreak: 1}));

    // These are the possible values used for the 'tiebreak' field. The 'tiebreak' field is included
    // to guarantee that certain documents are inspected before others to ensure that the plan may
    // see documents which don't match the filter and "waste" work reading these documents.
    const kTieBreakLow = 0;
    const kTieBreakHigh = 1;

    // Special value of 'selectiveKey' for which the plan using {selectiveKey:1, tiebreak:1} is
    // *slightly* less efficient, but still far better than the plan using {nonSelectiveKey:1,
    // tiebreak:1}.
    const kSpecialSelectiveKey = 2;

    // The query using 'filterOnSelectiveKey' should require slightly fewer reads than
    // 'filterOnSelectiveKeySpecialValue'. We will check that running
    // 'filterOnSelectiveKeySpecialValue' when there is a cache entry generated from
    // 'filterOnSelectiveKey' does *not* cause replanning.
    const filterOnSelectiveKey = {notSelectiveKey: {$lt: 50}, selectiveKey: 3};
    const filterOnSelectiveKeySpecialValue = {
        notSelectiveKey: {$lt: 50},
        selectiveKey: kSpecialSelectiveKey
    };

    // Insert 110 documents for each value of selectiveKey from 1-10. We use the number 110 docs
    // because it is greater than the because it is greater than the predefined doc limit of 101 for
    // cached planning and multi-planning.
    for (let i = 0; i < 10; ++i) {
        for (let j = 0; j < 110; ++j) {
            assert.commandWorked(
                coll.insert({notSelectiveKey: 10, selectiveKey: i, tiebreak: kTieBreakHigh}));
        }
    }

    // Now add one extra document so the plan requires a few more reads/works when the value of
    // 'selectiveKey' is 'kSpecialSelectiveKey'. We use a low value of 'tiebreak' to ensure that
    // this special, non-matching document is inspected before the documents which do match the
    // filter.
    assert.commandWorked(coll.insert(
        {notSelectiveKey: 55, selectiveKey: kSpecialSelectiveKey, tiebreak: kTieBreakLow}));

    // Now we run a query using the special value of 'selectiveKey' until the plan gets cached. We
    // run it twice to make the cache entry active.
    for (let i = 0; i < 2; ++i) {
        assert.eq(110, coll.find(filterOnSelectiveKeySpecialValue).itcount());
    }

    // Now look at the cache entry and store the values for works, keysExamined.
    entry = getCachedPlanForQuery(filterOnSelectiveKeySpecialValue);
    queryHash = entry.queryHash;
    const specialValueCacheEntryWorks = entry.works;

    // Execution stats from when the plan cache entry was created are not exposed from the SBE plan
    // cache.
    let specialValueCacheEntryKeysExamined;
    if (!isSbeEnabled) {
        specialValueCacheEntryKeysExamined = entry.creationExecStats[0].totalKeysExamined;
    }

    assert.eq(entry.isActive, true, entry);
    assertPlanHasIxScanStage(entry, "selectiveKey_1_tiebreak_1", queryHash);

    // Clear the plan cache for the collection.
    coll.getPlanCache().clear();

    // Now run the query on the non-special value of 'selectiveKey' until it gets cached.
    for (let i = 0; i < 2; ++i) {
        assert.eq(110, coll.find(filterOnSelectiveKey).itcount());
    }

    entry = getCachedPlanForQuery(filterOnSelectiveKey);
    assert.eq(entry.isActive, true, entry);
    assertPlanHasIxScanStage(entry, "selectiveKey_1_tiebreak_1", queryHash);

    // The new cache entry's plan should have used fewer works (and examined fewer keys) compared
    // to the old cache entry's, since the query on the special value is slightly less efficient.
    assert.lt(entry.works, specialValueCacheEntryWorks, entry);
    if (!isSbeEnabled) {
        assert.lt(entry.creationExecStats[0].totalKeysExamined,
                  specialValueCacheEntryKeysExamined,
                  entry);
    }

    // Now run the query on the "special" value again and check that replanning does not happen
    // even though the plan is slightly less efficient than the one in the cache.
    assert.eq(110, coll.find(filterOnSelectiveKeySpecialValue).itcount());

    // Check that the cache entry hasn't changed.
    const entryAfterRunningSpecialQuery = getCachedPlanForQuery(filterOnSelectiveKey);
    assert.eq(entryAfterRunningSpecialQuery.isActive, true);
    assertPlanHasIxScanStage(entry, "selectiveKey_1_tiebreak_1", queryHash);

    assert.eq(entry.works, entryAfterRunningSpecialQuery.works, entryAfterRunningSpecialQuery);
    if (!isSbeEnabled) {
        assert.eq(entryAfterRunningSpecialQuery.creationExecStats[0].totalKeysExamined,
                  entry.creationExecStats[0].totalKeysExamined,
                  entryAfterRunningSpecialQuery);
    }
}
})();