summaryrefslogtreecommitdiff
path: root/jstests/core/plan_cache_commands.js
blob: 9554e3017b2bd6ba716d171ef046d6ffae32a3b3 (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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/**
 * Plan cache commands
 * 
 * Cache-wide Commands
 * - planCacheListQueryShapes
 * - planCacheClear
 *       Removes plans for one or all query shapes.
 * - planCacheListPlans
 */ 

var t = db.jstests_plan_cache_commands;
t.drop();

// Insert some data so we don't go to EOF.
t.save({a: 1, b: 1});
t.save({a: 2, b: 2});

// We need two indices so that the MultiPlanRunner is executed.
t.ensureIndex({a: 1});
t.ensureIndex({a: 1, b:1});

// Run the query.
var queryA1 = {a: 1, b:1};
var projectionA1 = {_id: 0, a: 1};
var sortA1 = {a: -1};
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
// We now expect the two indices to be compared and a cache entry to exist.


//
// tests for planCacheListQueryShapes
// Returns a list of query shapes for the queries currently cached in the collection.
//

// Utility function to list query shapes in cache.
function getShapes() {
    var res = t.runCommand('planCacheListQueryShapes');
    print('planCacheListQueryShapes() = ' + tojson(res));
    assert.commandWorked(res, 'planCacheListQueryShapes failed');
    assert(res.hasOwnProperty('shapes'), 'shapes missing from planCacheListQueryShapes result');
    return res.shapes;
    
}

// Attempting to retrieve cache information on non-existent collection is an error.
var missingCollection = db.jstests_query_cache_missing;
missingCollection.drop();
assert.commandFailed(missingCollection.runCommand('planCacheListQueryShapes'));

// Retrieve query shapes from the test collection
// Number of shapes should match queries executed by multi-plan runner.
var shapes = getShapes();
assert.eq(1, shapes.length, 'unexpected number of shapes in planCacheListQueryShapes result');
assert.eq({query: queryA1, sort: sortA1, projection: projectionA1}, shapes[0],
          'unexpected query shape returned from planCacheListQueryShapes');



//
// Tests for planCacheClear (one query shape)
//

// Invalid key should be an error.
assert.commandFailed(t.runCommand('planCacheClear', {query: {unknownfield: 1}}));

// Run a new query shape and drop it from the cache
assert.eq(1, t.find({a: 2, b: 2}).itcount(), 'unexpected document count');
assert.eq(2, getShapes().length, 'unexpected cache size after running 2nd query');
assert.commandWorked(t.runCommand('planCacheClear', {query: {a: 1, b: 1}}));
assert.eq(1, getShapes().length, 'unexpected cache size after dropping 2nd query from cache');



//
// Tests for planCacheListPlans
//

// Utility function to list plans for a query.
function getPlans(query, sort, projection) {
    var key = {query: query, sort: sort, projection: projection};
    var res = t.runCommand('planCacheListPlans', key);
    assert.commandWorked(res, 'planCacheListPlans(' + tojson(key, '', true) + ' failed');
    assert(res.hasOwnProperty('plans'), 'plans missing from planCacheListPlans(' +
           tojson(key, '', true) + ') result');
    return res.plans;
}

// Invalid key should be an error.
assert.commandFailed(t.runCommand('planCacheListPlans', {query: {unknownfield: 1}}));

// Retrieve plans for valid cache entry.
var plans = getPlans(queryA1, sortA1, projectionA1);
assert.eq(2, plans.length, 'unexpected number of plans cached for query');

// Print every plan
// Plan details/feedback verified separately in section after Query Plan Revision tests.
print('planCacheListPlans result:');
for (var i = 0; i < plans.length; i++) {
    print('plan ' + i + ': ' + tojson(plans[i]));
}



//
// Tests for planCacheClear
//

// Drop query cache. This clears all cached queries in the collection.
res = t.runCommand('planCacheClear');
print('planCacheClear() = ' + tojson(res));
assert.commandWorked(res, 'planCacheClear failed');
assert.eq(0, getShapes().length, 'plan cache should be empty after successful planCacheClear()');



//
// Query Plan Revision
// http://docs.mongodb.org/manual/core/query-plans/#query-plan-revision
// As collections change over time, the query optimizer deletes the query plan and re-evaluates
// after any of the following events:
// - The collection receives 1,000 write operations.
// - The reIndex rebuilds the index.
// - You add or drop an index.
// - The mongod process restarts.
//

// Case 1: The collection receives 1,000 write operations.
// Steps:
//     Populate cache. Cache should contain 1 key after running query.
//     Insert 1000 documents.
//     Cache should be cleared.
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
assert.eq(1, getShapes().length, 'plan cache should not be empty after query');
for (var i = 0; i < 1000; i++) {
    t.save({b: i});
}
assert.eq(0, getShapes().length, 'plan cache should be empty after adding 1000 documents.');

// Case 2: The reIndex rebuilds the index.
// Steps:
//     Populate the cache with 1 entry.
//     Run reIndex on the collection.
//     Confirm that cache is empty.
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
assert.eq(1, getShapes().length, 'plan cache should not be empty after query');
res = t.reIndex();
print('reIndex result = ' + tojson(res));
assert.eq(0, getShapes().length, 'plan cache should be empty after reIndex operation');

// Case 3: You add or drop an index.
// Steps:
//     Populate the cache with 1 entry.
//     Add an index.
//     Confirm that cache is empty.
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
assert.eq(1, getShapes().length, 'plan cache should not be empty after query');
t.ensureIndex({b: 1});
assert.eq(0, getShapes().length, 'plan cache should be empty after adding index');

// Case 4: The mongod process restarts
// Not applicable.



//
// Tests for plan reason and feedback in planCacheListPlans
//

// Generate more plans for test query by adding indexes (compound and sparse).
// This will also clear the plan cache.
t.ensureIndex({a: -1}, {sparse: true});
t.ensureIndex({a: 1, b: 1});

// Implementation note: feedback stats is calculated after 20 executions.
// See PlanCacheEntry::kMaxFeedback.
var numExecutions = 100;
for (var i = 0; i < numExecutions; i++) {
    assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'query failed');
}

plans = getPlans(queryA1, sortA1, projectionA1);

// This should be obvious but feedback is available only for the first (winning) plan.
print('planCacheListPlans result (after adding indexes and completing 20 executions):');
for (var i = 0; i < plans.length; i++) {
    print('plan ' + i + ': ' + tojson(plans[i]));
    assert.gt(plans[i].reason.score, 0, 'plan ' + i + ' score is invalid');
    if (i > 0) {
        assert.lte(plans[i].reason.score, plans[i-1].reason.score,
                   'plans not sorted by score in descending order. ' +
                   'plan ' + i + ' has a score that is greater than that of the previous plan');
    }
    assert(plans[i].reason.stats.hasOwnProperty('type'), 'no stats inserted for plan ' + i);
}

// feedback meaningful only for plan 0
// feedback is capped at 20
assert.eq(20, plans[0].feedback.nfeedback, 'incorrect nfeedback');
assert.gt(plans[0].feedback.averageScore, 0, 'invalid average score');



//
// Tests for shell helpers
//

// Reset collection data and indexes.
t.drop();
var n = 200;
for (var i = 0; i < n; i++) {
    t.save({a:i, b: i});
}
t.ensureIndex({a: 1});
t.ensureIndex({b: 1});
t.ensureIndex({a: 1, b: 1});

// Repopulate plan cache with 3 query shapes.
var queryB = {a: {$gte: 0}, b: {$gte: 0}};
var projectionB = {_id: 0, b: 1};
var sortB = {b: -1};
assert.eq(n, t.find(queryB, projectionB).sort(sortB).itcount(), 'unexpected document count');
assert.eq(n, t.find(queryB, projectionB).itcount(), 'unexpected document count');
assert.eq(n, t.find(queryB).sort(sortB).itcount(), 'unexpected document count');
assert.eq(n, t.find(queryB).itcount(), 'unexpected document count');
assert.eq(4, getShapes().length, 'unexpected number of query shapes in plan cache');

//
// PlanCache.getName
//

var planCache = t.getPlanCache();
assert.eq(t.getName(), planCache.getName(), 'name of plan cache should match collection');

//
// PlanCache.help
//
planCache.help();

//
// shellPrint
//

print('plan cache:');
print(planCache);

//
// collection.getPlanCache().listQueryShapes
//

missingCollection.drop();
assert.throws(function() { missingCollection.getPlanCache().listQueryShapes() });
assert.eq(getShapes(), planCache.listQueryShapes(),
          'unexpected collection.getPlanCache().listQueryShapes() shell helper result');

//
// collection.getPlanCache().getPlansByQuery
//

// should error on non-existent collection.
assert.throws(function() { planCache.getPlansByQuery({unknownfield: 1}) });
// should error on missing required field query.
assert.throws(function() { planCache.getPlansByQuery() });

// Invoke with various permutations of required (query) and optional (projection, sort) arguments.
assert.eq(getPlans(queryB, sortB, projectionB), planCache.getPlansByQuery(queryB, projectionB,
                                                                          sortB),
          'plans from collection.getPlanCache().getPlansByQuery() different from command result');
assert.eq(getPlans(queryB, {}, projectionB), planCache.getPlansByQuery(queryB, projectionB),
          'plans from collection.getPlanCache().getPlansByQuery() different from command result');
assert.eq(getPlans(queryB, sortB, {}), planCache.getPlansByQuery(queryB, undefined, sortB),
          'plans from collection.getPlanCache().getPlansByQuery() different from command result');
assert.eq(getPlans(queryB, {}, {}), planCache.getPlansByQuery(queryB),
          'plans from collection.getPlanCache().getPlansByQuery() different from command result');

//
// collection.getPlanCache().clearPlansByQuery
//

// should error on non-existent collection.
assert.throws(function() { planCache.clearPlansByQuery({unknownfield: 1}) });
// should error on missing required field query.
assert.throws(function() { planCache.clearPlansByQuery() });

// Invoke with various permutations of required (query) and optional (projection, sort) arguments.
planCache.clearPlansByQuery(queryB, projectionB, sortB);
assert.eq(3, getShapes().length,
          'query shape not dropped after running collection.getPlanCache().clearPlansByQuery()');

planCache.clearPlansByQuery(queryB, projectionB);
assert.eq(2, getShapes().length,
          'query shape not dropped after running collection.getPlanCache().clearPlansByQuery()');

planCache.clearPlansByQuery(queryB, undefined, sortB);
assert.eq(1, getShapes().length,
          'query shape not dropped after running collection.getPlanCache().clearPlansByQuery()');

planCache.clearPlansByQuery(queryB);
assert.eq(0, getShapes().length,
          'query shape not dropped after running collection.getPlanCache().clearPlansByQuery()');


//
// collection.getPlanCache().clear
//

assert.throws(function() { missingCollection.getPlanCache().clear() });
// Re-populate plan cache with 1 query shape.
assert.eq(n, t.find(queryB, projectionB).sort(sortB).itcount(), 'unexpected document count');
assert.eq(1, getShapes().length, 'plan cache should not be empty after running cacheable query');
// Clear cache.
planCache.clear();
assert.eq(0, getShapes().length, 'plan cache not empty after clearing');



//
// explain and plan cache
// Running explain should not mutate the plan cache.
//

planCache.clear();

// MultiPlanRunner explain
var multiPlanRunnerExplain = t.find(queryB, projectionB).sort(sortB).explain(true);

print('multi plan runner explain = ' + tojson(multiPlanRunnerExplain));

assert.eq(0, getShapes().length, 'explain should not mutate plan cache');




//
// SERVER-12796: Plans for queries that return zero
// results should not be cached.
//

t.drop();

t.ensureIndex({a: 1});
t.ensureIndex({b: 1});

for (var i = 0; i < 200; i++) {
    t.save({a: 1, b: 1});
}
t.save({a: 2, b: 2});

// A query with zero results that does not hit EOF should not be cached...
assert.eq(0, t.find({c: 0}).itcount(), 'unexpected count');
assert.eq(0, getShapes().length, 'unexpected number of query shapes in plan cache');

// ...but a query with zero results that hits EOF will be cached.
assert.eq(1, t.find({a: 2, b: 2}).itcount(), 'unexpected count');
assert.eq(1, getShapes().length, 'unexpected number of query shapes in plan cache');

// A query that returns results but does not hit EOF will also be cached.
assert.eq(200, t.find({a: {$gte: 0}, b:1}).itcount(), 'unexpected count');
assert.eq(2, getShapes().length, 'unexpected number of query shapes in plan cache');