Раздел 36 · MongoDB с нуля

explain() и покрывающие запросы

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В уроке про explain мы смотрели на три цифры: сколько ключей и документов прочитано и сколько вернулось. В прошлом уроке добавили цепочку этапов и сравнили индексы через hint. Сегодня разберём отчёт explain подробнее: откуда сервер берёт план, какие планы он отверг и что значат поля внутри этапов. А в конце найдём запросы, которые отвечают, вообще не открывая документы.

Отчёт курьера

Курьер перед выездом выбирает маршрут. Он прикидывает три варианта, пробует каждый на первых сотнях метров, выбирает самый бодрый и записывает в журнал: «поехал по набережной, через мост отказался, там пробка». После доставки к записи добавляется трек: сколько ехал, где стоял.

explain устроен так же и состоит из двух главных частей:

  • queryPlanner это журнал выбора: какой план победил и какие отвергнуты.
  • executionStats это трек: как прошёл победивший план, этап за этапом.

Есть и третий режим, explain('allPlansExecution'), он добавляет треки пробных заездов отвергнутых планов. Он нужен редко.

Все примеры сняты с настоящего MongoDB 8 на коллекции из ста тысяч заказов из урока про ESR. Три индекса из того урока на месте, функции stages и plan тоже.

Как сервер выбирает план

Добавим индекс под страницу «заказы покупателя»:

db.orders.createIndex({ customer: 1, amount: 1 })

И спросим про заказы покупателя дешевле пяти тысяч:

const p = db.orders.find({ customer: 'c42', amount: { $lt: 5000 } }).explain().queryPlanner
p.winningPlan.inputStage.indexName
p.rejectedPlans.map(r => r.inputStage.indexName)
p.winningPlan.inputStage.indexBounds
customer_1_amount_1
[ 'amount_1_status_1_day_1' ]
{ customer: [ '["c42", "c42"]' ], amount: [ '[-inf, 5000)' ] }

Под условие подходили два индекса: новый customer_1_amount_1 и amount_1_status_1_day_1 из прошлого урока, он тоже начинается с amount. Сервер выбрал первый, второй лежит в rejectedPlans.

Выбирает планировщик не по формуле, а пробным забегом. Он запускает все планы-кандидаты понемногу и смотрит, какой быстрее набирает документы. Победителя запоминает в кэше планов: следующий запрос той же формы, с другим покупателем или суммой, пойдёт сразу по нему, без забега. Поэтому в плане есть поле isCached: оно говорит, взят ли план из кэша.

Это отличается от PostgreSQL, где планировщик считает стоимость по статистике, как мы видели в уроке про EXPLAIN ANALYZE. Классический планировщик MongoDB статистику распределения значений не ведёт и решает пробным забегом.

Этапы изнутри

Возьмём запрос по покупателю, который просит только customer и amount, и посмотрим на все поля трека:

db.orders.find({ customer: 'c42' }, { _id: 0, customer: 1, amount: 1 }).explain('executionStats').executionStats.executionStages

Ответ длинный, вот главное, остальное заменено многоточием:

{
  isCached: false,
  stage: 'PROJECTION_COVERED',
  nReturned: 50,
  executionTimeMillisEstimate: 0,
  works: 51,
  advanced: 50,
  ...
  transformBy: { _id: 0, customer: 1, amount: 1 },
  inputStage: {
    stage: 'IXSCAN',
    nReturned: 50,
    ...
    keyPattern: { customer: 1, amount: 1 },
    indexName: 'customer_1_amount_1',
    isMultiKey: false,
    ...
    indexBounds: { customer: [ '["c42", "c42"]' ], amount: [ '[MinKey, MaxKey]' ] },
    keysExamined: 50,
    seeks: 1,
    ...
  }
}

Этапы вложены друг в друга, внешний получает результаты от inputStage. У каждого этапа свои счётчики:

  • nReturned: сколько результатов этап отдал наружу.
  • works: сколько шагов сделал. На каждый шаг этап либо отдаёт результат (advanced), либо ещё ищет.
  • keysExamined у IXSCAN: сколько ключей индекса прочитано, docsExamined у FETCH: сколько документов.
  • seeks: сколько раз сервер «прыгал» в новое место индекса. Здесь один прыжок к c42 и чтение подряд.

Общие totalKeysExamined и totalDocsExamined, на которые мы смотрели раньше, это суммы этих счётчиков по всем этапам.

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.