Metode explain() menampilkan rencana eksekusi yang dipilih server untuk sebuah query, beserta statistik eksekusinya apabila diminta. Topik ini membahas tiga mode explain, cara membaca pohon winningPlan tahap demi tahap, penghitung executionStats yang stabil antareksekusi, serta verifikasi bahwa sebuah query benar-benar memakai indeks yang telah disiapkan. Demonstrasi memakai basis data kamus dengan koleksi entri berisi lima ratus dokumen. Setelah mengikuti topik ini, performa query dapat didiagnosis dari keluaran explain secara sistematis, bukan dari dugaan.
Tiga mode explain#
explain() dapat dipanggil pada kursor dengan tiga tingkat verbositas. Mode queryPlanner merupakan mode bawaan, yaitu explain() tanpa argumen, dan hanya menampilkan rencana yang dipilih tanpa mengeksekusi query. Mode executionStats menjalankan query hingga selesai lalu melaporkan statistik eksekusinya, sedangkan mode allPlansExecution mengeksekusi query dan membandingkan seluruh rencana kandidat yang dipertimbangkan perencana query (query planner). Verifikasi struktur rencana cukup memakai mode queryPlanner, sedangkan pengukuran pekerjaan server memerlukan mode executionStats.
Tanamlah data contoh berikut pada basis data kamus, lalu bersihkan indeks lama agar demonstrasi dimulai dari kondisi yang hanya memuat indeks bawaan _id_.
use kamus
const sukuAwal = ["ba", "ta", "ka", "me", "pa", "sa", "ni", "go", "lu", "wi"]
const sukuAkhir = ["tah", "rin", "sup", "kan", "del", "mus", "pel", "rong", "wis", "dak"]
const daftarKategori = ["nomina", "verba", "adjektiva"]
const docs = []
const tambah = function (i, j, k) {
docs.push({
kata: sukuAwal[i] + sukuAkhir[j],
homonim: k + 1,
kategori: daftarKategori[(i + j + k) % 3],
frekuensi: ((i * 53 + j * 29 + k * 7) % 900) + 100,
tahun: 2015 + ((i + j * 2 + k) % 10)
})
}
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
for (let k = 0; k < 5; k++) {
tambah(i, j, k)
}
}
}
const hasil = db.entri.insertMany(docs)
print("Dokumen tersimpan: " + db.entri.countDocuments())switched to db kamus
Dokumen tersimpan: 500const bersih = db.entri.dropIndexes()
print("Indeks non-_id dibersihkan: " + (bersih.ok === 1))Indeks non-_id dibersihkan: trueQuery berikut menyaring kategori verba dan mengurutkan hasilnya menurut frekuensi menurun. Rencananya dibaca melalui explain() tanpa argumen, yang sama dengan explain("queryPlanner").
db.entri.find({ kategori: "verba" }).sort({ frekuensi: -1 }).explain().queryPlanner.winningPlan{
isCached: false,
stage: 'SORT',
sortPattern: { frekuensi: -1 },
memLimit: 104857600,
type: 'simple',
inputStage: {
stage: 'COLLSCAN',
filter: { kategori: { '$eq': 'verba' } },
direction: 'forward'
}
}Keluaran menunjukkan rencana berbentuk pohon bertingkat. Tahap teratas SORT mengurutkan dokumen di dalam memori, dan tahap COLLSCAN di bawahnya memindai seluruh koleksi dengan filter kategori. Tidak ada tahap IXSCAN karena belum ada indeks yang cocok, sehingga query ini berjalan dengan bentuk rencana yang paling mahal untuk koleksi besar.
Membaca pohon winningPlan#
Field winningPlan memuat rencana yang terpilih, dan strukturnya dibaca dari tahap terbawah ke tahap teratas. Tahap terbawah biasanya bertugas membaca data, sedangkan setiap tahap di atasnya mengolah keluaran tahap di bawahnya. Field indexBounds pada tahap IXSCAN memperlihatkan rentang kunci yang dilalui, sehingga kecocokan antara batas kunci dan syarat query dapat diperiksa secara langsung.
Buat indeks pada kata dan indeks komposit pada kategori serta frekuensi, lalu amati rencana query yang menyaring dua field sekaligus.
db.entri.createIndex({ kata: 1 })
db.entri.createIndex({ kategori: 1, frekuensi: -1 })
db.entri.find({ kategori: "verba", homonim: 2 }).sort({ frekuensi: -1 }).explain("queryPlanner").queryPlanner.winningPlankata_1
kategori_1_frekuensi_-1
{
isCached: false,
stage: 'FETCH',
filter: { homonim: { '$eq': 2 } },
inputStage: {
stage: 'IXSCAN',
keyPattern: { kategori: 1, frekuensi: -1 },
indexName: 'kategori_1_frekuensi_-1',
isMultiKey: false,
multiKeyPaths: { kategori: [], frekuensi: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
kategori: [ '["verba", "verba"]' ],
frekuensi: [ '[MaxKey, MinKey]' ]
}
}
}Pohon rencananya terdiri atas dua tahap. Tahap IXSCAN menelusuri indeks kategori_1_frekuensi_-1 dengan batas kunci kategori bernilai verba. Urutan frekuensi menurun sudah dijamin oleh susunan kunci, sehingga tahap SORT tidak muncul. Tahap FETCH di atasnya mengambil dokumen lengkap dan menerapkan filter homonim yang tidak didukung indeks. Field yang paling sering diperiksa saat diagnosis adalah indexName, indexBounds, dan direction.
Seluruh pohon tersebut dapat diringkas menjadi beberapa field yang stabil melalui destrukturisasi, yaitu pemisahan nilai ke dalam variabel sebelum dicetak.
const p = db.entri.find({ kategori: "verba", homonim: 2 }).sort({ frekuensi: -1 }).explain("queryPlanner").queryPlanner
print("tahap teratas: " + p.winningPlan.stage)
print("tahap indeks: " + p.winningPlan.inputStage.stage)
print("indeks dipakai: " + p.winningPlan.inputStage.indexName)
print("arah: " + p.winningPlan.inputStage.direction)tahap teratas: FETCH
tahap indeks: IXSCAN
indeks dipakai: kategori_1_frekuensi_-1
arah: forwardMengukur eksekusi dengan executionStats#
Mode executionStats mengeksekusi query lalu melaporkan penghitung yang menggambarkan pekerjaan server. Tiga penghitung yang paling berguna adalah nReturned untuk jumlah dokumen yang dikembalikan, totalKeysExamined untuk jumlah kunci indeks yang dibaca, dan totalDocsExamined untuk jumlah dokumen yang dibaca. Field executionTimeMillis juga tersedia pada keluaran mode ini, tetapi nilainya berubah antareksekusi karena bergantung pada cache dan beban server. Karena itu, perbandingan antarrencana lebih andal memakai ketiga penghitung jumlah yang nilainya tetap.
Ukur query yang sama dengan rencana FETCH dan IXSCAN sebelumnya.
const st = db.entri.find({ kategori: "verba", homonim: 2 }).sort({ frekuensi: -1 }).explain("executionStats").executionStats
print("nReturned: " + st.nReturned)
print("totalKeysExamined: " + st.totalKeysExamined)
print("totalDocsExamined: " + st.totalDocsExamined)nReturned: 34
totalKeysExamined: 167
totalDocsExamined: 167Server membaca 167 kunci dan mengambil seluruh 167 dokumen kategori verba, tetapi hanya 34 dokumen yang dikembalikan karena filter homonim dievaluasi pada tahap FETCH setelah dokumen diambil. Selisih antara jumlah yang diperiksa dan jumlah yang dikembalikan menandakan pekerjaan yang terbuang. Bandingkan dengan query kesamaan tunggal yang sepenuhnya didukung indeks.
const st2 = db.entri.find({ kata: "batah" }).explain("executionStats").executionStats
print("nReturned: " + st2.nReturned)
print("totalKeysExamined: " + st2.totalKeysExamined)
print("totalDocsExamined: " + st2.totalDocsExamined)nReturned: 5
totalKeysExamined: 5
totalDocsExamined: 5Rasio 1:1 pada ketiga penghitung berarti setiap kunci yang dibaca langsung menghasilkan dokumen yang dikembalikan, sehingga tidak ada pekerjaan yang terbuang. Tujuan penyetelan performa adalah mendekati rasio tersebut, baik melalui indeks yang tepat maupun melalui perbaikan bentuk query.
Rencana kandidat dan verifikasi indeks#
Perencana query menilai lebih dari satu rencana kandidat apabila beberapa indeks cocok dengan syarat query. Rencana dengan biaya terendah terpilih sebagai winningPlan, sedangkan kandidat yang kalah tercatat pada field rejectedPlans. Query berikut menyaring kata dan kategori sekaligus, dan kedua field tersebut masing-masing menjadi awalan indeks yang berbeda.
const p2 = db.entri.find({ kata: "batah", kategori: "verba" }).explain("queryPlanner").queryPlanner
print("rencana menang: " + p2.winningPlan.inputStage.indexName)
print("kandidat kalah: " + p2.rejectedPlans.length)rencana menang: kata_1
kandidat kalah: 1Server memilih indeks kata_1 karena nilai batah membatasi perjalanan kunci pada lima entri, sedangkan nilai verba pada indeks kategori mencakup 167 entri. Satu kandidat yang kalah tercatat pada rejectedPlans, lengkap dengan pohon tahapnya sehingga alasan kalahnya dapat ditelusuri lebih lanjut. Cara verifikasi pemakaian indeks dengan demikian sederhana, yaitu memeriksa indexName pada winningPlan dan memastikan indeks yang muncul adalah indeks yang memang disiapkan untuk query tersebut. Pemeriksaan ini sebaiknya diulang setiap kali indeks baru dibuat atau dihapus, karena perubahan daftar indeks dapat mengubah rencana yang terpilih.
Kesalahan umum#
Kesalahan yang lazim adalah mencari statistik eksekusi pada mode bawaan. Mode queryPlanner tidak mengeksekusi query, sehingga field executionStats tidak ada pada keluarannya dan pencarian field tersebut menghasilkan undefined.
const tanpaStat = db.entri.find({ kata: "batah" }).explain()
print("executionStats dari mode queryPlanner: " + tanpaStat.executionStats)executionStats dari mode queryPlanner: undefinedPerbaikannya adalah memanggil explain("executionStats") setiap kali penghitung eksekusi diperlukan. Nilai undefined mudah terlewat karena tercetak tanpa pesan apa pun, sedangkan penyebabnya hanya perbedaan mode pada pemanggilan explain.
Kesalahan lain adalah menjadikan executionTimeMillis sebagai tolok ukur tunggal. Nilai tersebut memang benar untuk satu eksekusi tertentu, tetapi berubah pada eksekusi berikutnya karena cache dan beban server. Dua query yang sama dapat melaporkan waktu yang berbeda beberapa kali lipat, sedangkan penghitung jumlah dokumen dan kunci tetap sama. Bandingkan rencana melalui penghitung jumlah tersebut, dan ulangi eksekusi beberapa kali apabila waktu perlu diamati.
Kesalahan ketiga adalah menilai rencana hanya dari tahap teratasnya. Tahap SORT atau FETCH di atas pohon terlihat wajar, padahal tahap di bawahnya dapat berupa COLLSCAN yang memindai seluruh koleksi, sehingga pohon perlu diikuti sampai tahap terbawah sebelum rencana dinyatakan sehat.
Ringkasan#
- explain() memiliki tiga mode, yaitu
queryPlanneryang hanya menampilkan rencana,executionStatsyang mengeksekusi dan melaporkan statistik, sertaallPlansExecutionyang membandingkan seluruh kandidat. winningPlandibaca dari tahap terbawah, denganIXSCANsebagai pembacaan indeks,FETCHsebagai pengambilan dokumen, danindexBoundssebagai batas kunci yang dilalui.- Penghitung
nReturned,totalKeysExamined, dantotalDocsExaminedbernilai stabil antareksekusi, sedangkanexecutionTimeMillistidak, sehingga perbandingan rencana memakai ketiga penghitung tersebut. - Pemakaian indeks diverifikasi melalui
indexNamepadawinningPlan, dan kandidat yang kalah tercatat padarejectedPlans.
Referensi#
- db.collection.explain() — MongoDB Manual — Spesifikasi metode explain beserta tiga tingkat verbositasnya.
- Explain Results — MongoDB Manual
— Referensi field keluaran explain, dari
winningPlanhinggaexecutionStats. - Analyze Query Performance — MongoDB Manual — Panduan membaca statistik eksekusi untuk menilai performa query.
- Query Plans — MongoDB Manual — Penjelasan pemilihan rencana, kandidat rencana, dan cache rencana.