Output explain() sering ditakuti karena panjangnya — padahal intinya berbentuk pohon kecil yang menyatakan alur kerja server: tahap apa saja dijalankan dan dari mana data mengalir. Chapter sebelumnya membaca angka executionStats; chapter ini bedah strukturnya. Setelah memahami cara membaca pohon winningPlan dari daun ke akar, nama-nama stage seperti IXSCAN, FETCH, dan SORT berubah dari teka-teki menjadi kalimat sederhana: “index dipindai, document diambil, lalu diurutkan”.
Peta materi Chapter ini:
- Membaca pohon
winningPlanlewatinputStage, dari tahap terdalam keluar. - Kamus stage yang paling sering dijumpai.
- Tiga variasi query pada data yang sama, plus query geospasial.
- Kombinasi
SORT+IXSCANdan artirejectedPlans.
Pohon Stage Dibaca dari Daun #
winningPlan adalah pohon bersarang. Setiap node menyimpan stage (jenis pekerjaan) dan bila tahap itu mengonsumsi tahap lain, inputStage berisi node anaknya. Aturan membacanya satu saja: mulai dari tahap paling dalam, lalu naik — data mengalir dari daun ke akar, dan akar adalah hasil yang dikirim ke klien. Query find({ kota: "Jakarta" }) dengan index menghasilkan pohon ini:
Diagram di atas menggambarkan pohon dua tahap: IXSCAN (daun) memindai B-tree index kota_1 dan menyerahkan lokasi document; FETCH (akar) mengambil document utuh dari collection. Bila query juga memproyeksikan field atau mengurutkan, tahap PROJECTION_SIMPLE atau SORT akan membungkus FETCH sebagai induk baru. Kamus stage yang paling sering muncul:
| Stage | Peran | Biasanya muncul saat |
|---|---|---|
IXSCAN | Memindai kunci index | Filter/sort bisa dilayani index |
COLLSCAN | Memindai seluruh collection | Tidak ada index yang cocok |
FETCH | Mengambil document asli berdasar lokasi dari index | Hampir selalu di atas IXSCAN |
SORT | Mengurutkan hasil di memori | Urutan sort tidak terpenuhi index |
PROJECTION_SIMPLE | Memangkas field hasil | Query memakai projection |
LIMIT | Membatasi jumlah hasil | Query memakai limit() |
GEO_NEAR_2DSPHERE | Pencarian geospasial terurut jarak | Query $nearSphere pada index 2dsphere |
SHARD_MERGE | Menggabungkan hasil dari beberapa shard | Cluster ter-shard (tidak tersedia di latihan) |
Tiga Variasi pada Data yang Sama #
Siapkan data dan index standar section ini:
use latihan_perf
db.penjualan.drop()
db.penjualan.insertMany([
{ _id: 1, waktu: ISODate("2026-03-02T09:15:00Z"), judul_buku: "Laut Bercerita", jumlah: 1, harga_total: 98000, kota: "Jakarta", channel: "toko" },
{ _id: 2, waktu: ISODate("2026-03-02T11:40:00Z"), judul_buku: "Bumi", jumlah: 2, harga_total: 178000, kota: "Bandung", channel: "online" },
{ _id: 3, waktu: ISODate("2026-03-03T10:05:00Z"), judul_buku: "Filosofi Teras", jumlah: 1, harga_total: 89000, kota: "Jakarta", channel: "online" },
{ _id: 4, waktu: ISODate("2026-03-03T14:30:00Z"), judul_buku: "Atomic Habits", jumlah: 3, harga_total: 345000, kota: "Surabaya", channel: "toko" },
{ _id: 5, waktu: ISODate("2026-03-04T08:20:00Z"), judul_buku: "Rumah Kaca", jumlah: 1, harga_total: 72000, kota: "Bandung", channel: "toko" },
{ _id: 6, waktu: ISODate("2026-03-04T13:55:00Z"), judul_buku: "Bumi", jumlah: 1, harga_total: 89000, kota: "Jakarta", channel: "toko" },
{ _id: 7, waktu: ISODate("2026-03-05T09:45:00Z"), judul_buku: "Hujan Bulan Juni", jumlah: 2, harga_total: 130000, kota: "Yogyakarta", channel: "online" },
{ _id: 8, waktu: ISODate("2026-03-05T16:10:00Z"), judul_buku: "Laut Bercerita", jumlah: 1, harga_total: 98000, kota: "Surabaya", channel: "online" },
{ _id: 9, waktu: ISODate("2026-03-06T10:30:00Z"), judul_buku: "Atomic Habits", jumlah: 1, harga_total: 115000, kota: "Jakarta", channel: "online" },
{ _id: 10, waktu: ISODate("2026-03-06T15:25:00Z"), judul_buku: "Bumi", jumlah: 4, harga_total: 356000, kota: "Yogyakarta", channel: "toko" },
{ _id: 11, waktu: ISODate("2026-03-07T09:00:00Z"), judul_buku: "Filosofi Teras", jumlah: 2, harga_total: 178000, kota: "Bandung", channel: "online" },
{ _id: 12, waktu: ISODate("2026-03-07T17:35:00Z"), judul_buku: "Hujan Bulan Juni", jumlah: 1, harga_total: 65000, kota: "Jakarta", channel: "toko" }
])
db.penjualan.createIndex({ kota: 1 })
db.penjualan.createIndex({ kota: 1, waktu: 1 })Output yang Dihasilkan:
switched to db latihan_perf
true
{
acknowledged: true,
insertedIds: {
'0': 1,
'1': 2,
'2': 3,
'3': 4,
'4': 5,
'5': 6,
'6': 7,
'7': 8,
'8': 9,
'9': 10,
'10': 11,
'11': 12
}
}
kota_1
kota_1_waktu_1Variasi 1 — filter sederhana. Query transaksi Jakarta memakai index tunggal kota_1:
db.penjualan.find({ kota: "Jakarta" }).explain()Output yang Dihasilkan:
{
explainVersion: '1',
queryPlanner: {
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { kota: 1 },
indexName: 'kota_1',
direction: 'forward',
indexBounds: { kota: [ '["Jakarta", "Jakarta"]' ] }
}
},
rejectedPlans: [
{
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { kota: 1, waktu: 1 },
indexName: 'kota_1_waktu_1',
direction: 'forward',
indexBounds: {
kota: [ '["Jakarta", "Jakarta"]' ],
waktu: [ '[MinKey, MaxKey]' ]
}
}
}
]
},
command: {
find: 'penjualan',
filter: { kota: 'Jakarta' },
'$db': 'latihan_perf'
},
ok: 1
}Output explain() di chapter ini terus dipadatkan dengan cara yang sama seperti dua chapter sebelumnya. Variasi 2 — filter + projection. Menambahkan projection menambah satu tahap di akar pohon:
db.penjualan.find({ kota: "Jakarta" }, { judul_buku: 1, harga_total: 1, _id: 0 }).explain()Output yang Dihasilkan:
{
explainVersion: '1',
queryPlanner: {
winningPlan: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
transformBy: { judul_buku: 1, harga_total: 1, _id: 0 },
inputStage: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { kota: 1 },
indexName: 'kota_1',
direction: 'forward',
indexBounds: { kota: [ '["Jakarta", "Jakarta"]' ] }
}
}
},
rejectedPlans: [
{
stage: 'PROJECTION_SIMPLE',
transformBy: { judul_buku: 1, harga_total: 1, _id: 0 },
inputStage: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { kota: 1, waktu: 1 },
indexName: 'kota_1_waktu_1',
indexBounds: {
kota: [ '["Jakarta", "Jakarta"]' ],
waktu: [ '[MinKey, MaxKey]' ]
}
}
}
}
]
},
command: {
find: 'penjualan',
filter: { kota: 'Jakarta' },
projection: { judul_buku: 1, harga_total: 1, _id: 0 },
'$db': 'latihan_perf'
},
ok: 1
}Variasi 3 — sort tanpa index pendukung. Mengurutkan berdasarkan harga_total — field yang tidak punya index:
db.penjualan.find().sort({ harga_total: -1 }).limit(3).explain()Output yang Dihasilkan:
{
explainVersion: '1',
queryPlanner: {
winningPlan: {
isCached: false,
stage: 'SORT',
sortPattern: { harga_total: -1 },
memLimit: 104857600,
limitAmount: 3,
type: 'simple',
inputStage: {
stage: 'COLLSCAN',
direction: 'forward'
}
},
rejectedPlans: []
},
command: {
find: 'penjualan',
filter: {},
sort: { harga_total: -1 },
limit: 3,
'$db': 'latihan_perf'
},
ok: 1
}Mari kita baca ketiganya dari daun ke akar:
- Variasi 1 membentuk
FETCH→IXSCAN: pemindaian index menghasilkan lokasi document,FETCHmenuntaskannya. FieldindexBoundsmenunjukkan rentang yang diperiksa —[["Jakarta", "Jakarta"]]berarti tepat satu titik nilai. - Variasi 2 membungkus pohon yang sama dengan
PROJECTION_SIMPLEdi akar: setelah document utuh diambil, tahap ini membuang field selainjudul_bukudanharga_totalsebelum hasil dikirim. Perhatikan bahwaFETCHtetap membaca document penuh — projection memangkas payload jaringan, bukan pembacaan di server (topik Chapter Projection dan Ukuran Payload). - Variasi 3 tidak bisa memakai kedua index
kota— urutanharga_totaltidak ada di dalamnya — sehingga daunnyaCOLLSCAN, di atasnyaSORTyang mengurutkan di memori. FieldlimitAmount: 3pada tahapSORTmenunjukkan pengurutan model top-k: server hanya mempertahankan 3 hasil teratas, bukan array penuh yang terurut. RantaiSORTdi atasCOLLSCANtetap sinyal klasik “sort tanpa index” pada data besar.
Membaca Rantai dengan Cepat #
Untuk pemeriksaan cepat sehari-hari, kita tidak perlu membedah seluruh output — cukup rantai nama stage-nya. Fungsi kecil berikut berjalan dari akar ke daun dan menyambungkan namanya:
const rantaiStage = (rencana) => {
const tahap = []
let s = rencana
while (s) { tahap.push(s.stage); s = s.inputStage }
return tahap.join(" <- ")
}
print("filter:", rantaiStage(db.penjualan.find({ kota: "Jakarta" }).explain().queryPlanner.winningPlan))
print("filter+projection:", rantaiStage(db.penjualan.find({ kota: "Jakarta" }, { judul_buku: 1 }).explain().queryPlanner.winningPlan))
print("sort mahal:", rantaiStage(db.penjualan.find().sort({ harga_total: -1 }).limit(3).explain().queryPlanner.winningPlan))
print("sort index majemuk:", rantaiStage(db.penjualan.find({ kota: "Bandung" }).sort({ waktu: 1 }).explain().queryPlanner.winningPlan))Output yang Dihasilkan:
filter: FETCH <- IXSCAN
filter+projection: PROJECTION_SIMPLE <- FETCH <- IXSCAN
sort mahal: SORT <- COLLSCAN
sort index majemuk: FETCH <- IXSCANBaris keempat adalah kombinasi yang layak dihafal: filter kota + sort waktu terlayani penuh oleh index majemuk { kota: 1, waktu: 1 } — rantainya hanya FETCH → IXSCAN, tanpa tahap SORT. Index yang berawalan field filter dan dilanjutkan field sort sudah menyimpan data dalam urutan yang diminta, sehingga langkah pengurutan di memori tidak pernah ada. Bandingkan dengan baris ketiga yang harus mengurutkan hasil COLLSCAN di memori.
Stage geospasial punya daun sendiri. Dengan index 2dsphere, query jarak menghasilkan GEO_NEAR_2DSPHERE — satu tahap yang sekaligus memindai index dan mengembalikan document terurut jarak:
db.toko.drop()
db.toko.insertMany([
{ nama: "Toko Kemang", kota: "Jakarta", lokasi: { type: "Point", coordinates: [106.82, -6.26] } },
{ nama: "Toko Dago", kota: "Bandung", lokasi: { type: "Point", coordinates: [107.62, -6.89] } },
{ nama: "Toko Tunjungan", kota: "Surabaya", lokasi: { type: "Point", coordinates: [112.75, -7.26] } }
])
db.toko.createIndex({ lokasi: "2dsphere" })
print("geo:", rantaiStage(db.toko.find({
lokasi: { $nearSphere: { $geometry: { type: "Point", coordinates: [106.82, -6.26] }, $maxDistance: 100000 } }
}).explain().queryPlanner.winningPlan))Output yang Dihasilkan:
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3b28ff20fb76c9e55e349'),
'1': ObjectId('6ad3b28ff20fb76c9e55e34a'),
'2': ObjectId('6ad3b28ff20fb76c9e55e34b')
}
}
lokasi_2dsphere
geo: GEO_NEAR_2DSPHERERantai satu tahap itu berarti GEO_NEAR_2DSPHERE sekaligus memindai index geospasial dan mengembalikan document terurut jarak — tidak ada FETCH terpisah. ObjectId pada output Anda tentu berbeda.
rejectedPlans dan isCached #
Dua field pelengkap di queryPlanner menutup pembacaan ini. rejectedPlans berisi pohon-pohon kandidat yang kalah kompetisi — bentuknya sama dengan winningPlan, sehingga cara membacanya juga sama; variasi 1 dan 2 di atas masing-masing menampilkan index majemuk kota_1_waktu_1 yang kalah. Index yang bentuknya tidak cocok dengan query (misalnya berawalan field yang tidak difilter) tidak dijadikan kandidat sejak awal, jadi tidak muncul di sana. Sementara isCached: true pada winningPlan menandakan rencana ini diambil dari plan cache, bukan hasil kompetisi segar — perencanaan jadi lebih pendek, seperti dibahas di Chapter Query Planner.
Rangkuman #
winningPlanadalah pohon stage; bacalah dariinputStageterdalam (sumber data) ke akar (hasil ke klien).- Rantai paling umum:
IXSCAN→FETCH, dibungkusPROJECTION_SIMPLE,SORT, atauLIMITsesuai bentuk query. SORTdi atasCOLLSCANadalah sinyal sort tanpa index; index majemuk berawalan field filter lalu field sort menghapus tahapSORT.rejectedPlansmemuat pohon kandidat yang kalah;isCachedmenandai rencana yang berasal dari plan cache.- Query geospasial pada index
2dspheremenghasilkan tahapGEO_NEAR_2DSPHERE;SHARD_MERGEbaru muncul di cluster ter-shard.