В уроке про 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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.