Kita telah berkali-kali menyebut “planner memilih index ini” — pertanyaannya, dari mana kita tahu? Jawabannya adalah explain(): method yang memaksa MongoDB membuka kartu-kartunya, mulai dari rencana eksekusi yang dipilih, index yang ditolak, hingga berapa kunci dan document yang benar-benar diperiksa. Seluruh diagnosis performa di section ini bermuara ke satu alat ini. Chapter ini membedah tiga tingkat kedalamannya, membaca pohon stage, dan menguji hint() untuk menimpa keputusan planner.
Peta materi Chapter ini:
- Tiga tingkat verbosity:
queryPlanner,executionStats,allPlansExecution. - Membaca pohon
winningPlandari akar hinggaindexBounds. - Metrik eksekusi:
nReturned,totalKeysExamined,totalDocsExamined. rejectedPlansdan pemaksaan rencana lewathint().
Tiga Tingkat Kedalaman #
Siapkan katalog dengan dua index yang bersaing, lalu bandingkan isi hasil explain() pada ketiga tingkatnya:
use latihan_index
db.buku.drop()
db.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi" },
{ judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi" },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi" },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi" },
{ judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi" },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra" }
])
db.buku.createIndex({ kategori: 1, harga: 1 })
db.buku.createIndex({ kategori: 1 })
printjson({
queryPlanner: Object.keys(db.buku.find({ kategori: "fiksi" }).explain()),
executionStats: Object.keys(db.buku.find({ kategori: "fiksi" }).explain("executionStats")),
allPlansExecution: Object.keys(db.buku.find({ kategori: "fiksi" }).explain("allPlansExecution"))
})Output yang Dihasilkan:
switched to db latihan_index
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3bf9f668893bcfe9e1d8a'),
'1': ObjectId('6ad3bf9f668893bcfe9e1d8b'),
'2': ObjectId('6ad3bf9f668893bcfe9e1d8c'),
'3': ObjectId('6ad3bf9f668893bcfe9e1d8d'),
'4': ObjectId('6ad3bf9f668893bcfe9e1d8e'),
'5': ObjectId('6ad3bf9f668893bcfe9e1d8f')
}
}
kategori_1_harga_1
kategori_1
{
queryPlanner: [
'explainVersion',
'queryPlanner',
'queryShapeHash',
'command',
'serverInfo',
'serverParameters',
'ok',
'$clusterTime',
'operationTime'
],
executionStats: [
'explainVersion',
'queryPlanner',
'executionStats',
'queryShapeHash',
'command',
'serverInfo',
'serverParameters',
'ok',
'$clusterTime',
'operationTime'
],
allPlansExecution: [
'explainVersion',
'queryPlanner',
'executionStats',
'queryShapeHash',
'command',
'serverInfo',
'serverParameters',
'ok',
'$clusterTime',
'operationTime'
]
}| Tingkat | Argumen | Isi tambahan | Gunanya |
|---|---|---|---|
| Rencana saja | explain() | queryPlanner | Cek index mana yang dipilih |
| Rencana + eksekusi | explain("executionStats") | + executionStats | Ukur kerja nyata rencana pemenang |
| Semua rencana | explain("allPlansExecution") | + statistik tiap kandidat | Bandingkan rencana pemenang vs yang ditolak |
Tingkat kedua dan ketiga sama-sama menambahkan field executionStats — bedanya pada isi: mode executionStats mengisi statistik rencana pemenang saja, sedangkan allPlansExecution turut mengisi array allPlansExecuted berisi statistik seluruh rencana kandidat. Semakin dalam tingkatnya, semakin mahal biayanya — allPlansExecution menjalankan semua rencana kandidat sungguhan, jadi jangan dipasang di setiap query produksi. Untuk rutinitas diagnosis, executionStats adalah titik seimbang.
Membaca Pohon winningPlan #
queryPlanner.winningPlan adalah pohon stage yang dibaca dari akar ke daun — akar adalah langkah terakhir sebelum hasil sampai ke klien. Mari cetak pohonnya untuk query kategori plus rentang harga:
const rencana = db.buku.find({ kategori: "fiksi", harga: { $lt: 90000 } }).explain()
printjson(rencana.queryPlanner.winningPlan)Output yang Dihasilkan:
{
isCached: false,
stage: 'FETCH',
nss: 'latihan_index.buku',
inputStage: {
stage: 'IXSCAN',
nss: 'latihan_index.buku',
keyPattern: {
kategori: 1,
harga: 1
},
indexName: 'kategori_1_harga_1',
isMultiKey: false,
multiKeyPaths: {
kategori: [],
harga: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
kategori: [
'["fiksi", "fiksi"]'
],
harga: [
'[-inf, 90000)'
]
}
}
}Pohonnya berdua tingkat: FETCH di akar dengan IXSCAN di bawahnya. Bacaannya dari bawah ke atas: IXSCAN memindai index kategori_1_harga_1 dalam batas kategori: ["fiksi","fiksi"] dan harga: "[-inf, 90000)", lalu setiap kunci yang lolos di-fetch document aslinya oleh FETCH. Istilah-istilah yang akan sering kita jumpai:
| Stage | Arti |
|---|---|
COLLSCAN | Pemindaian seluruh collection tanpa index |
IXSCAN | Pemindaian index dengan batas kunci |
FETCH | Mengambil document berdasar penunjuk dari index |
SORT | Pengurutan di memori — sinyal index belum melayani sort |
PROJECTION_COVERED | Projection dipenuhi dari index (Chapter Covered Queries) |
indexBounds pantas dibaca teliti: harga: "[-inf, 90000)" menandakan batas atas terbuka — nilai tepat 90000 tidak ikut. Rentang yang lebar (misalnya [-inf, inf]) berarti index dipakai hanya sebagai jalan masuk, bukan sebagai penyaring.
Metrik Eksekusi #
Tingkat executionStats menambahkan angka-angka yang menjawab “berapa kerja sebenarnya?”:
const eksekusi = db.buku.find({ kategori: "fiksi", harga: { $lt: 90000 } }).explain("executionStats")
printjson({
nReturned: eksekusi.executionStats.nReturned,
totalKeysExamined: eksekusi.executionStats.totalKeysExamined,
totalDocsExamined: eksekusi.executionStats.totalDocsExamined,
executionTimeMillis: eksekusi.executionStats.executionTimeMillis
})Output yang Dihasilkan:
{
nReturned: 2,
totalKeysExamined: 2,
totalDocsExamined: 2,
executionTimeMillis: 0
}Interpretasinya: dua kunci index diperiksa, dua document dibuka, dua hasil kembali — rasio sempurna 1:1:1. Semakin jauh angka-angka ini saling menjauh, semakin banyak kerja sia-sia:
| Metrik | Arti | Ideal |
|---|---|---|
nReturned | Document yang dikembalikan ke klien | — |
totalKeysExamined | Entri index yang diperiksa | Mendekati nReturned |
totalDocsExamined | Document yang dibuka dari disk | Mendekati nReturned; 0 bila covered |
executionTimeMillis | Durasi eksekusi | Kontekstual — bandingkan antar rencana |
COLLSCAN selalu menunjukkan totalDocsExamined setara jumlah document yang discan, dengan totalKeysExamined: 0 karena tidak ada index yang terlibat.
Rencana Cadangan dan hint() #
Ketika beberapa index layak, planner mengadu semuanya dan menyimpan yang kalah di rejectedPlans. Query kita memiliki dua kandidat — compound dan single field:
const dua = db.buku.find({ kategori: "fiksi", harga: { $lt: 90000 } }).explain()
printjson({
terpilih: dua.queryPlanner.winningPlan.inputStage.indexName,
ditolak: dua.queryPlanner.rejectedPlans.map(p => p.inputStage.indexName)
})
const dipaksa = db.buku.find({ kategori: "fiksi", harga: { $lt: 90000 } }).hint({ kategori: 1 }).explain("executionStats")
printjson({
terpaksa: dipaksa.queryPlanner.winningPlan.inputStage.indexName,
nReturned: dipaksa.executionStats.nReturned,
totalKeysExamined: dipaksa.executionStats.totalKeysExamined,
totalDocsExamined: dipaksa.executionStats.totalDocsExamined
})Output yang Dihasilkan:
{
terpilih: 'kategori_1_harga_1',
ditolak: [
'kategori_1'
]
}
{
terpaksa: 'kategori_1',
nReturned: 2,
totalKeysExamined: 3,
totalDocsExamined: 3
}Planner memilih kategori_1_harga_1 dan menolak kategori_1 — keputusan yang terbukti benar ketika hint({ kategori: 1 }) memaksa index yang kalah: tiga kunci dan tiga document diperiksa untuk dua hasil, karena harga tinggal disaring satu per satu setelah fetch. hint() menerima key pattern ({ kategori: 1 }) maupun nama index ("kategori_1") — dan ingat dari Chapter Index Properties: index tersembunyi tidak bisa di-hint.
Rangkuman #
explain()punya tiga tingkat: rencana saja, rencana plus statistik eksekusi pemenang, dan statistik seluruh kandidat (allPlansExecution).winningPlandibaca dari akar ke daun; pasangan paling umum adalahFETCH→IXSCAN, danindexBoundsmenunjukkan seberapa sempit batas pencarian.- Rasio
nReturned:totalKeysExamined:totalDocsExaminedyang rapat menandakan index bekerja efisien. rejectedPlansmemperlihatkan kandidat yang kalah;hint()memaksa pilihan tertentu untuk diagnosis, dengan hati-hati bila dipakai jangka panjang.