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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
|
//
// A view of a collection against which operations are explained rather than executed
// normally.
//
var Explainable = (function() {
var parseVerbosity = function(verbosity) {
// Truthy non-strings are interpreted as "allPlansExecution" verbosity.
if (verbosity && (typeof verbosity !== "string")) {
return "allPlansExecution";
}
// Falsy non-strings are interpreted as "queryPlanner" verbosity.
if (!verbosity && (typeof verbosity !== "string")) {
return "queryPlanner";
}
// All verbosity strings are passed through. Server validates if it is a known option.
return verbosity;
};
var throwOrReturn = function(explainResult) {
if (("ok" in explainResult && !explainResult.ok) || explainResult.$err) {
throw _getErrorWithCode(explainResult, "explain failed: " + tojson(explainResult));
}
return explainResult;
};
var buildExplainCmd = function(innerCmd, verbosity) {
var explainCmd = {"explain": innerCmd, "verbosity": verbosity};
// If "maxTimeMS" is set on innerCmd, it needs to be propagated to the top-level
// of explainCmd so that it has the intended effect.
if (innerCmd.hasOwnProperty("maxTimeMS")) {
explainCmd.maxTimeMS = innerCmd.maxTimeMS;
}
return explainCmd;
};
function constructor(collection, verbosity) {
//
// Private vars.
//
this._collection = collection;
this._verbosity = parseVerbosity(verbosity);
//
// Public methods.
//
this.getCollection = function() {
return this._collection;
};
this.getVerbosity = function() {
return this._verbosity;
};
this.setVerbosity = function(verbosity) {
this._verbosity = parseVerbosity(verbosity);
return this;
};
this.help = function() {
print("Explainable operations");
print("\t.aggregate(...) - explain an aggregation operation");
print("\t.count(...) - explain a count operation");
print("\t.distinct(...) - explain a distinct operation");
print("\t.find(...) - get an explainable query");
print("\t.findAndModify(...) - explain a findAndModify operation");
print("\t.mapReduce(...) - explain a mapReduce operation");
print("\t.remove(...) - explain a remove operation");
print("\t.update(...) - explain an update operation");
print("Explainable collection methods");
print("\t.getCollection()");
print("\t.getVerbosity()");
print("\t.setVerbosity(verbosity)");
return __magicNoPrint;
};
//
// Pretty representations.
//
this.toString = function() {
return "Explainable(" + this._collection.getFullName() + ")";
};
this.shellPrint = function() {
return this.toString();
};
//
// Explainable operations.
//
this.aggregate = function(pipeline, extraOpts) {
if (!(pipeline instanceof Array)) {
// Support legacy varargs form. (Also handles db.foo.aggregate())
pipeline = Array.from(arguments);
extraOpts = {};
}
// Add the explain option.
let extraOptsCopy = Object.extend({}, (extraOpts || {}));
// For compatibility with 3.4 and older versions, when the verbosity is "queryPlanner",
// we use the explain option to the aggregate command. Otherwise we issue an explain
// command wrapping the agg command, which is supported by newer versions of the server.
if (this._verbosity === "queryPlanner") {
extraOptsCopy.explain = true;
return this._collection.aggregate(pipeline, extraOptsCopy);
} else {
// The aggregate command requires a cursor field.
if (!extraOptsCopy.hasOwnProperty("cursor")) {
extraOptsCopy = Object.extend(extraOptsCopy, {cursor: {}});
}
let aggCmd = Object.extend(
{"aggregate": this._collection.getName(), "pipeline": pipeline}, extraOptsCopy);
let explainCmd = buildExplainCmd(aggCmd, this._verbosity);
let explainResult = this._collection.runReadCommand(explainCmd);
return throwOrReturn(explainResult);
}
};
this.count = function(query, options) {
query = this.find(query);
return QueryHelpers._applyCountOptions(query, options).count();
};
/**
* .explain().find() and .find().explain() mean the same thing. In both cases, we use
* the DBExplainQuery abstraction in order to construct the proper explain command to send
* to the server.
*/
this.find = function() {
var cursor = this._collection.find.apply(this._collection, arguments);
return new DBExplainQuery(cursor, this._verbosity);
};
this.findAndModify = function(params) {
var famCmd = Object.extend({"findAndModify": this._collection.getName()}, params);
var explainCmd = buildExplainCmd(famCmd, this._verbosity);
var explainResult = this._collection.runReadCommand(explainCmd);
return throwOrReturn(explainResult);
};
this.distinct = function(keyString, query, options) {
var distinctCmd = {
distinct: this._collection.getName(),
key: keyString,
query: query || {}
};
if (options && options.hasOwnProperty("collation")) {
distinctCmd.collation = options.collation;
}
if (options && options.hasOwnProperty("maxTimeMS")) {
distinctCmd.maxTimeMS = options.maxTimeMS;
}
var explainCmd = buildExplainCmd(distinctCmd, this._verbosity);
var explainResult = this._collection.runReadCommand(explainCmd);
return throwOrReturn(explainResult);
};
this.remove = function() {
var parsed = this._collection._parseRemove.apply(this._collection, arguments);
var query = parsed.query;
var justOne = parsed.justOne;
var collation = parsed.collation;
var bulk = this._collection.initializeOrderedBulkOp();
var removeOp = bulk.find(query);
if (collation) {
removeOp.collation(collation);
}
if (justOne) {
removeOp.removeOne();
} else {
removeOp.remove();
}
var explainCmd = bulk.convertToExplainCmd(this._verbosity);
var explainResult = this._collection.runCommand(explainCmd);
return throwOrReturn(explainResult);
};
this.update = function() {
var parsed = this._collection._parseUpdate.apply(this._collection, arguments);
var query = parsed.query;
var updateSpec = parsed.updateSpec;
var upsert = parsed.upsert;
var multi = parsed.multi;
var collation = parsed.collation;
var arrayFilters = parsed.arrayFilters;
var hint = parsed.hint;
var bulk = this._collection.initializeOrderedBulkOp();
var updateOp = bulk.find(query);
if (hint) {
updateOp.hint(hint);
}
if (upsert) {
updateOp = updateOp.upsert();
}
if (collation) {
updateOp.collation(collation);
}
if (arrayFilters) {
updateOp.arrayFilters(arrayFilters);
}
if (multi) {
updateOp.update(updateSpec);
} else {
updateOp.updateOne(updateSpec);
}
var explainCmd = bulk.convertToExplainCmd(this._verbosity);
var explainResult = this._collection.runCommand(explainCmd);
return throwOrReturn(explainResult);
};
this.mapReduce = function(map, reduce, optionsObjOrOutString) {
assert(optionsObjOrOutString, "Must supply the 'optionsObjOrOutString ' argument");
const mapReduceCmd = {mapreduce: this._collection.getName(), map: map, reduce: reduce};
if (typeof (optionsObjOrOutString) == "string")
mapReduceCmd["out"] = optionsObjOrOutString;
else
Object.extend(mapReduceCmd, optionsObjOrOutString);
const explainCmd = buildExplainCmd(mapReduceCmd, this._verbosity);
const explainResult = this._collection.runCommand(explainCmd);
return throwOrReturn(explainResult);
};
}
//
// Public static methods.
//
constructor.parseVerbosity = parseVerbosity;
constructor.throwOrReturn = throwOrReturn;
return constructor;
})();
/**
* Provides a namespace for explain-related helper functions.
*/
let Explain = (function() {
/**
* Given explain output from the server for a query that used the slot-based execution engine
* (SBE), presents this information in a more consumable format. This function requires that the
* explain output comes from a system where SBE is enabled on all nodes, and wil throw an
* exception if it observes explain output from the classic execution engine.
*
* Rather than an object, this function returns an array of information about each candidate
* plan. The first element of the array is always the winning plan. Each of the plans is
* represented by an object with the following schema:
*
* {
* planNumber: <number>,
* queryPlan: <obj>,
* summaryStats: <obj>,
* trialSummaryStats: <obj>,
* slotBasedPlan: <string>,
* slotBasedPlanVerbose: <obj>,
* trialSlotBasedPlanVerbose: <obj>,
* }
*
* The meanings of the returned fields:
* - 'planNumber': An integer giving the index of the plan in the array. The winning plan is
* 0, the first candidate is 1, and so on. Allows plans to be identified easily.
* - 'queryPlan' - A description of the query plan, using a representation which should be
* familiar to consumers of explains from versions before SBE. Importantly, each node in the
* plan tree may be augmented with the following:
* * A field called "execStats". These are derived execution stats for the node, obtained
* based on the full SBE exec stats output. Only present for the winning plan.
* * A field called "trialExecStats". This is identical in meaning to "execStats", but shows
* stats from the multi-planner's trial execution period. These stats are derived from the
* 'allPlansExecution' section.
* - 'summaryStats' - An object containing high-level execution stats for the plan as a whole
* (e.g. "totalKeysExamined" and "totalDocsExamined"). Only present for the winning plan.
* - 'trialSummaryStats' - Similar to "summaryStats", but describes execution stats from the
* multi-planner's trial execution period. These stats are derived from the
* 'allPlansExecution' section.
* - 'slotBasedPlan' - A string giving a concise representation of the SBE plan.
* - 'slotBasedPlanVerbose' - The full SBE plan with execution stats associated with every
* node. only present for the winning plan.
* - 'trialSlotBasedPlanVerbose' - The SBE plans reported in the 'allPlansExecution' section,
* whose runtime stats describe what happened during the multi-planner's trial execution
* period.
*
* This function will work for find-style explain output as well as aggregate explain output. In
* the case of agg, it produces output in the same format by reformatting the contents of the
* $cursor stage.
*
* It is also designed to work for explain output from sharded find commands and sharded agg
* commands. In such cases, the output is an object with one key per shard:
*
* {
* <shardName1>: [...],
* <shardName2>: [...],
* ...
* }
*
* Each shard reports information about its candidate plans in the same format as desgined
* above.
*
* The second argument, 'fieldsToKeep', is an array of field names from the above schema. For
* instance, a common use case would be to just see 'planNumber' and 'queryPlan', in which case
* the caller should pass a vlaue of ["planNumber", "queryPlan"]. This argument is optional, and
* defaults to generating the full verbose output.
*
* This may mutate 'explain'. Callers which want to leave 'explain' unmodified should create a
* deep copy prior to calling this function.
*/
function sbeReformatExperimental(explain, fieldsToKeep) {
const kCommonStats = [
"nReturned",
"opens",
"closes",
"saveState",
"restoreState",
"isEOF",
"executionTimeMillisEstimate",
];
const kSummaryStats = [
"executionSuccess",
"nReturned",
"executionTimeMillis",
"totalKeysExamined",
"totalDocsExamined",
];
const kTrialSummaryStats = [
"nReturned",
"executionTimeMillisEstimate",
"totalKeysExamined",
"totalDocsExamined",
];
const kAggregateStats = [
"seeks",
"numReads",
];
// Given either a query solution or an SBE tree represented by 'root', walks the tree and
// calls 'callbackFn()' for each node. This is done in a bottom-up manner so that the
// callback is executed last on the root.
function walkTree(root, callbackFn) {
if ("inputStages" in root) {
for (var i = 0; i < root.inputStages.length; i++) {
walkTree(root.inputStages[i], callbackFn);
}
}
for (let childStageName
of ["inputStage", "thenStage", "elseStage", "outerStage", "innerStage"]) {
if (childStageName in root) {
walkTree(root[childStageName], callbackFn);
}
}
callbackFn(root);
}
function aggregateStat(target, statObjName, propName, init, value, aggFunc) {
if (!target.hasOwnProperty(statObjName)) {
target[statObjName] = {};
}
const statObj = target[statObjName];
if (!statObj.hasOwnProperty(propName)) {
statObj[propName] = init;
}
statObj[propName] = aggFunc(statObj[propName], value);
}
function takeLatest(target, statObjName, propName, value) {
aggregateStat(target, statObjName, propName, 0, value, (oldVal, newVal) => newVal);
}
function sumIfPresent(target, statObjName, propName, value) {
if (!value) {
return;
}
aggregateStat(
target, statObjName, propName, 0, value, (oldVal, newVal) => oldVal + newVal);
}
function fillAggregatedExecStats(sbeExecStats, querySolution, statObjName) {
const executionStages = sbeExecStats.executionStages;
// Construct a map from "planNodeId" to the object representing that plan node in the
// serialization of the query solution.
let nodeIdToNode = {};
walkTree(querySolution, (node) => {
nodeIdToNode[node.planNodeId] = node;
});
// This time walk the SBE plan and attribute stats to the corresponding node in the
// query solution tree.
walkTree(executionStages, (node) => {
// Find the QSN to which we should attribute this SBE stage's runtime stats.
const correspondingNode = nodeIdToNode[node.planNodeId];
assert.neq(correspondingNode, null);
assert.neq(correspondingNode, undefined);
// We generally only care about the common stats of the subtree as a whole
// associated with a query solution node, so use 'takeLatest()' to report the stats
// associated with the root node of this subtree.
for (let stat of kCommonStats) {
takeLatest(correspondingNode, statObjName, stat, node[stat]);
}
for (let stat of kAggregateStats) {
sumIfPresent(correspondingNode, statObjName, stat, node[stat]);
}
});
}
function addSummaryStats(executionStats, summaryStatsName, statsToAdd, target) {
target[summaryStatsName] = {};
let summaryStats = target[summaryStatsName];
for (let stat of statsToAdd) {
summaryStats[stat] = executionStats[stat];
}
}
function handleExecutionStats(executionStats, reformatted) {
// First, specially handle the full execution stats associated with the winning plan.
assert(typeof executionStats === "object");
assert(executionStats.hasOwnProperty("executionStages"));
reformatted[0].slotBasedPlanVerbose = executionStats.executionStages;
fillAggregatedExecStats(executionStats, reformatted[0].queryPlan, "execStats");
addSummaryStats(executionStats, "summaryStats", kSummaryStats, reformatted[0]);
const allPlansExecution = executionStats.allPlansExecution;
if (!allPlansExecution) {
return;
}
if (!Array.isArray(allPlansExecution)) {
throw Error("expected 'allPlansExecution' section to be an array");
}
if (allPlansExecution.length === 0) {
// This can happen if the user requested 'allPlansExecution' verbosity, but the
// query only had one solution (so there was no runtime planning).
return;
}
assert.eq(reformatted.length, allPlansExecution.length);
for (let i = 0; i < reformatted.length; ++i) {
assert(allPlansExecution[i].hasOwnProperty("executionStages"));
reformatted[i].trialSlotBasedPlanVerbose = allPlansExecution[i].executionStages;
fillAggregatedExecStats(
allPlansExecution[i], reformatted[i].queryPlan, "trialExecStats");
addSummaryStats(
allPlansExecution[i], "trialSummaryStats", kTrialSummaryStats, reformatted[i]);
}
}
function project(reformatted, fieldsToKeep) {
for (let plan of reformatted) {
for (let field of Object.keys(plan)) {
if (!fieldsToKeep.has(field)) {
delete plan[field];
}
}
}
}
function doReformatting(queryPlanner, executionStats, fieldsToKeep) {
assert(queryPlanner.hasOwnProperty("winningPlan"));
assert(queryPlanner.hasOwnProperty("rejectedPlans"));
const winningPlan = queryPlanner.winningPlan;
const rejectedPlans = queryPlanner.rejectedPlans;
if (!winningPlan.hasOwnProperty("slotBasedPlan")) {
// Part of this query did not use SBE.
throw Error("Found winning plan which did not use SBE");
}
// We will assign a number to each candidate plan, in order to make referring the plans
// easier.
let planNumber = 0;
const reformatted = [];
winningPlan.planNumber = planNumber++;
reformatted.push(winningPlan);
for (let plan of rejectedPlans) {
if (!plan.hasOwnProperty("slotBasedPlan")) {
throw Error("Found rejected plan which did not use SBE, planNumber: " +
planNumber);
}
plan.planNumber = planNumber++;
reformatted.push(plan);
}
// Only massage the execution stats for this particular piece of the explain output if
// this node used the slot-based execution engine for this query. The reformatting done
// here is tied to the SBE explain format.
if (executionStats) {
handleExecutionStats(executionStats, reformatted);
}
if (fieldsToKeep) {
project(reformatted, fieldsToKeep);
}
return reformatted;
}
function doShardedFindReformatting(fullExplain, fieldsToKeep) {
assert(fullExplain.hasOwnProperty("queryPlanner"));
const queryPlanner = fullExplain.queryPlanner;
// Return an object where the keys are the names of the shards, and the values is a
// reformatted array similar to what would be returned for an explain from a standalone
// or replica set node.
const result = {};
assert(queryPlanner.hasOwnProperty("winningPlan"));
const winningPlan = queryPlanner.winningPlan;
assert(winningPlan.hasOwnProperty("shards"));
const shardsQueryPlanner = winningPlan.shards;
for (let shard of shardsQueryPlanner) {
assert(shard.hasOwnProperty("shardName"));
assert(shard.hasOwnProperty("winningPlan"));
assert(!shard.hasOwnProperty("executionStats"));
const shardName = shard.shardName;
// We're just handling the "queryPlanner" section here, so we don't pass execution
// stats information.
result[shardName] = doReformatting(shard, undefined, fieldsToKeep);
}
const executionStats = fullExplain.executionStats;
if (!executionStats) {
return result;
}
// Next handle "executionStats", which is an entirely separate section from
// "queryPlanner" and shows execution-level information gathered from each shard.
assert(executionStats.hasOwnProperty("executionStages"));
const executionStages = executionStats.executionStages;
assert(executionStages.hasOwnProperty("shards"));
const shardsExecStats = executionStages.shards;
for (let shard of shardsExecStats) {
assert(shard.hasOwnProperty("shardName"));
const shardName = shard.shardName;
assert(result.hasOwnProperty(shardName));
const shardResults = result[shardName];
handleExecutionStats(shard, shardResults);
// We may add more fields here, so re-apply the projection.
if (fieldsToKeep) {
project(shardResults, fieldsToKeep);
}
}
return result;
}
function doFindReformatting(fullExplain, fieldsToKeep) {
assert(fullExplain.hasOwnProperty("queryPlanner"));
const queryPlanner = fullExplain.queryPlanner;
// The "executionStats" field may or may not be present.
const executionStats = fullExplain.executionStats;
return doReformatting(queryPlanner, executionStats, fieldsToKeep);
}
function doAggReformatting(fullExplain, fieldsToKeep) {
// We are explaining an aggregate operation. Look for the $cursor stage and report
// information about that.
const stages = fullExplain.stages;
assert.gt(stages.length, 0, stages);
const firstStage = stages[0];
if (!firstStage.hasOwnProperty("$cursor")) {
throw Error("could not find $cursor stage");
}
// The contents of the $cursor stage should be similarly formatted to an explain of a
// find operation, so reformat these inner contents.
const cursorStage = firstStage.$cursor;
return doFindReformatting(cursorStage, fieldsToKeep);
}
function doShardedAggReformatting(fullExplain, fieldsToKeep) {
const result = {};
// Sharded agg explain result reports the full explains from each of the shards. All we
// need to do is reformat the explains from the shards one-by-one.
for (let shardName of Object.keys(fullExplain.shards)) {
const shardExplain = fullExplain.shards[shardName];
result[shardName] = doUnshardedReformatting(shardExplain, fieldsToKeep);
}
return result;
}
/**
* Performs reformatting for an explain from a single mongod. Infers whether the explain is
* a find explain or an aggregate explain.
*/
function doUnshardedReformatting(fullExplain, fieldsToKeep) {
// The presence of a top-level "stages" array identifies that this is aggregation
// explain output from a mongod.
if (fullExplain.hasOwnProperty("stages")) {
return doAggReformatting(fullExplain, fieldsToKeep);
}
// If we are not in any of the other special cases, then assume that this is a find
// explain from a mongod.
return doFindReformatting(fullExplain, fieldsToKeep);
}
if (fieldsToKeep && !Array.isArray(fieldsToKeep)) {
throw Error("second argument must be undefined or an array");
}
const fieldsToKeepSet = fieldsToKeep ? new Set(fieldsToKeep) : undefined;
// Bail out if the explain version indicates that the classic engine was used.
if (explain.explainVersion && explain.explainVersion !== "2") {
throw Error("'explainVersion' is " + explain.explainVersion + " but expected '2'");
}
// Infer what kind of explain we're dealing with. Only sharded agg results will report a
// "shards" array at the top-level.
if (explain.hasOwnProperty("shards")) {
return doShardedAggReformatting(explain, fieldsToKeepSet);
}
const queryPlanner = explain.queryPlanner;
const isShardedFind = queryPlanner && queryPlanner.mongosPlannerVersion === 1;
if (isShardedFind) {
return doShardedFindReformatting(explain, fieldsToKeepSet);
}
// We've handled the sharded cases already, so we now assume that the given explain output
// is from a single mongod.
return doUnshardedReformatting(explain, fieldsToKeepSet);
}
function help() {
print(
`
Explain.sbeReformatExperimental(<explain>, <fieldsToKeep>)
\tExperimental helper which takes explain output from a system where the slot-based
\tengine (SBE) is enabled and presents it in a more consumable fashion. Returns an
\tarray of plans where each plan has the following schema:
\t {
\t planNumber: <number>,
\t queryPlan: <obj>,
\t summaryStats: <obj>,
\t trialSummaryStats: <obj>,
\t slotBasedPlan: <string>,
\t slotBasedPlanVerbose: <obj>,
\t trialSlotBasedPlanVerbose: <obj>,
\t }
\t<explain> - output from an explain operation
\t<fieldsToKeep> - an optional array of field names to include for each candidate plan
`);
return __magicNoPrint;
}
return {
help: help,
sbeReformatExperimental: sbeReformatExperimental,
};
})();
/**
* This is the user-facing method for creating an Explainable from a collection.
*/
DBCollection.prototype.explain = function(verbosity) {
return new Explainable(this, verbosity);
};
|