Performa aplikasi tidak dinilai dari kesan cepat atau lambat, melainkan dari rencana eksekusi dan penghitung server yang dilaporkan explain(). Topik ini menutup proyek dengan menyisir performa pola baca aplikasi pencatatan pada koleksi pengeluaran, membandingkan rencana sebelum dan sesudah indeks, serta memverifikasi bahwa setiap pola baca memakai indeks yang disiapkan untuknya. Koleksi diisi enam ratus dokumen agar selisih pekerjaan servernya terlihat jelas. Pembandingan memakai field yang stabil antareksekusi, sehingga kesimpulan tidak berubah hanya karena beban server sedang berbeda.
Data acuan dan rencana tanpa indeks#
Data acuan dibuat oleh kode agar setiap angka pada penghitungan dapat direproduksi. Enam puluh hari mulai 1 Juni 2026 diisi sepuluh transaksi per hari, sehingga koleksi memuat enam ratus dokumen dengan kategori yang berputar dan jumlah yang berpola. Data ini dihasilkan dari tanggal tetap, bukan dari waktu saat ini, sehingga isinya sama pada setiap eksekusi.
use pencatatanswitched to db pencatatanconst daftarKategori = ["makan", "transportasi", "belanja", "hiburan", "kesehatan"]
const docs = []
const tambah = function (h, k) {
docs.push({
tanggal: new Date(Date.UTC(2026, 5, 1) + h * 86400000),
kategori: daftarKategori[(h + k) % 5],
jumlah: NumberDecimal(String(((h * 37 + k * 13) % 90 + 10) * 1000)),
catatan: "catatan " + (h * 10 + k + 1)
})
}
for (let h = 0; h < 60; h++) {
for (let k = 0; k < 10; k++) {
tambah(h, k)
}
}
const hasil = db.pengeluaran.insertMany(docs)
print("Dokumen tersimpan: " + db.pengeluaran.countDocuments())
print("Indeks awal: " + db.pengeluaran.getIndexes().length)Dokumen tersimpan: 600
Indeks awal: 1Pola baca pertama yang diuji adalah daftar pengeluaran satu kategori selama satu bulan, yaitu query yang memakai indeks komposit pada aplikasi. Query ini menyaring kategori dengan kesetaraan, menyaring rentang tanggal, lalu mengurutkan hasilnya menurun menurut tanggal. Rencananya dibaca melalui explain() dengan mode queryPlanner tanpa indeks apa pun selain indeks bawaan _id_.
db.pengeluaran.find({ kategori: "makan", tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain().queryPlanner.winningPlan{
isCached: false,
stage: 'SORT',
sortPattern: { tanggal: -1 },
memLimit: 104857600,
type: 'simple',
inputStage: {
stage: 'COLLSCAN',
filter: {
'$and': [
{ kategori: { '$eq': 'makan' } },
{ tanggal: { '$lt': ISODate('2026-07-01T00:00:00.000Z') } },
{ tanggal: { '$gte': ISODate('2026-06-01T00:00:00.000Z') } }
]
},
direction: 'forward'
}
}Keluaran menunjukkan pohon rencana dua tahap. Tahap SORT di atasnya mengurutkan dokumen di dalam memori, sedangkan tahap COLLSCAN di bawahnya memindai seluruh koleksi dengan filter kategori dan rentang tanggal. Belum ada tahap IXSCAN, sehingga query ini masih memakai bentuk rencana yang paling mahal untuk koleksi besar.
Destrukturisasi winningPlan dan penghitung eksekusi#
Membaca rencana tidak harus menelusuri seluruh dokumen explain. Destrukturisasi menarik field tertentu dari keluaran explain() ke dalam variabel sebelum dicetak, sehingga yang tampil hanya field yang stabil dan relevan untuk diagnosis. Keluaran explain lengkap juga memuat field yang berubah antareksekusi, seperti hash query dan informasi server, sehingga pencetakan penuhnya justru menyulitkan perbandingan. Field yang dipakai di sini adalah stage pada setiap tingkat pohon rencana.
const p = db.pengeluaran.find({ kategori: "makan", tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("queryPlanner").queryPlanner
print("tahap teratas: " + p.winningPlan.stage)
print("tahap pemindaian: " + p.winningPlan.inputStage.stage)tahap teratas: SORT
tahap pemindaian: COLLSCANStruktur rencana sudah jelas, dan langkah berikutnya adalah mengukur pekerjaannya. Mode executionStats mengeksekusi query lalu melaporkan penghitung jumlah, yaitu nReturned untuk dokumen yang dikembalikan, totalKeysExamined untuk kunci indeks yang dibaca, dan totalDocsExamined untuk dokumen yang dibaca. Ketiganya bernilai tetap antareksekusi, sedangkan penghitung waktu tidak, sehingga perbandingan hanya memakai ketiganya.
const st = db.pengeluaran.find({ kategori: "makan", tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("executionStats").executionStats
print("nReturned: " + st.nReturned)
print("totalKeysExamined: " + st.totalKeysExamined)
print("totalDocsExamined: " + st.totalDocsExamined)nReturned: 60
totalKeysExamined: 0
totalDocsExamined: 600Enam ratus dokumen dipindai untuk mengembalikan enam puluh dokumen, dan totalKeysExamined bernilai nol karena tidak ada indeks yang dibaca. Selisih sepuluh kali lipat antara jumlah yang diperiksa dan yang dikembalikan adalah pekerjaan yang terbuang, dan inilah angka yang harus turun. Ketiga penghitung yang sama dipakai lagi setelah indeks dibuat, sehingga perbandingannya mengukur akibat indeks pada query yang sama, bukan membandingkan dua query yang berbeda.
Indeks komposit untuk pola per kategori#
Indeks komposit dibuat dengan kategori pada posisi pertama dan tanggal menurun pada posisi kedua. Urutan itu mengikuti panduan ESR (Equality, Sort, Range), yaitu field kesetaraan lebih dahulu, lalu field pengurutan, lalu field rentang. Pola baca per kategori menyaring kategori dengan kesetaraan, memakai tanggal untuk pengurutan dan rentang sekaligus, sehingga satu indeks cukup untuk ketiganya.
db.pengeluaran.createIndex({ kategori: 1, tanggal: -1 })kategori_1_tanggal_-1Rencananya diperiksa ulang dengan query yang sama melalui destrukturisasi.
const p2 = db.pengeluaran.find({ kategori: "makan", tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("queryPlanner").queryPlanner
print("tahap teratas: " + p2.winningPlan.stage)
print("tahap indeks: " + p2.winningPlan.inputStage.stage)
print("indeks dipakai: " + p2.winningPlan.inputStage.indexName)
print("arah: " + p2.winningPlan.inputStage.direction)tahap teratas: FETCH
tahap indeks: IXSCAN
indeks dipakai: kategori_1_tanggal_-1
arah: forwardPohon rencananya berubah menjadi bentuk yang lebih murah. Tahap IXSCAN menelusuri indeks kategori_1_tanggal_-1, dan urutan tanggal menurun sudah dijamin susunan kunci sehingga tahap SORT tidak muncul lagi. Tahap FETCH di atasnya mengambil dokumen lengkap, dan arah penelusurannya maju sesuai susunan kunci indeks.
const st2 = db.pengeluaran.find({ kategori: "makan", tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("executionStats").executionStats
print("nReturned: " + st2.nReturned)
print("totalKeysExamined: " + st2.totalKeysExamined)
print("totalDocsExamined: " + st2.totalDocsExamined)nReturned: 60
totalKeysExamined: 60
totalDocsExamined: 60Penghitungnya berubah signifikan. Kunci yang dibaca turun menjadi enam puluh dan dokumen yang diperiksa turun dari enam ratus menjadi enam puluh, sedangkan dokumen yang dikembalikan tetap enam puluh. Rasio 1:1 antara ketiga penghitung berarti tidak ada lagi pekerjaan yang terbuang pada query ini.
Verifikasi indeks untuk seluruh pola baca#
Keberadaan indeks bukan berarti semua query terbantu. Pola baca kedua aplikasi adalah daftar seluruh pengeluaran satu bulan tanpa menyaring kategori. Query semacam ini tidak menyentuh awalan kategori, sehingga indeks komposit tidak dapat dipakai dan rencananya kembali berupa pemindaian koleksi.
const p3 = db.pengeluaran.find({ tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("queryPlanner").queryPlanner
print("tahap teratas: " + p3.winningPlan.stage)
print("tahap pemindaian: " + p3.winningPlan.inputStage.stage)tahap teratas: SORT
tahap pemindaian: COLLSCANIndeks kedua dibuat khusus untuk pola ini, yaitu tanggal menurun tunggal, lalu pola baca yang sama diperiksa ulang bersama penghitung eksekusinya.
db.pengeluaran.createIndex({ tanggal: -1 })tanggal_-1const p4 = db.pengeluaran.find({ tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("queryPlanner").queryPlanner
print("tahap teratas: " + p4.winningPlan.stage)
print("tahap indeks: " + p4.winningPlan.inputStage.stage)
print("indeks dipakai: " + p4.winningPlan.inputStage.indexName)
const st4 = db.pengeluaran.find({ tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } }).sort({ tanggal: -1 }).explain("executionStats").executionStats
print("nReturned: " + st4.nReturned)
print("totalKeysExamined: " + st4.totalKeysExamined)
print("totalDocsExamined: " + st4.totalDocsExamined)tahap teratas: FETCH
tahap indeks: IXSCAN
indeks dipakai: tanggal_-1
nReturned: 300
totalKeysExamined: 300
totalDocsExamined: 300Tiga ratus dokumen Juni dikembalikan dengan membaca tiga ratus kunci dan tiga ratus dokumen, sedangkan sebelum indeks ini server memindai enam ratus dokumen. Tabel berikut merangkum pola baca aplikasi beserta indeks yang wajib muncul pada rencananya.
| Pola baca aplikasi | Bentuk syarat | Indeks yang wajib dipakai |
|---|---|---|
| daftar pengeluaran satu kategori untuk satu bulan | kesetaraan kategori, rentang dan urutan tanggal | kategori_1_tanggal_-1 |
| daftar seluruh pengeluaran satu bulan | rentang dan urutan tanggal | tanggal_-1 |
| ringkasan bulanan per kategori | rentang tanggal pada tahap $match | tanggal_-1 |
Pola baca ketiga adalah ringkasan bulanan per kategori, yaitu pipeline agregasi $match dan $group dari aplikasi. Rencananya diverifikasi dengan explain() pada pipeline yang sama tanpa tahap $sort. Tahap $sort tidak disertakan karena pengurutannya diterapkan pada hasil $group, sehingga tidak memengaruhi pemilihan indeks pada tahap pembacaan dokumen.
const pa = db.pengeluaran.explain("queryPlanner").aggregate([
{ $match: { tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } } },
{ $group: { _id: "$kategori", total: { $sum: "$jumlah" } } }
]).queryPlanner.winningPlan.queryPlan
print("tahap teratas: " + pa.stage)
print("tahap pemindaian: " + pa.inputStage.inputStage.stage)
print("indeks dipakai: " + pa.inputStage.inputStage.indexName)
const sta = db.pengeluaran.explain("executionStats").aggregate([
{ $match: { tanggal: { $gte: ISODate("2026-06-01"), $lt: ISODate("2026-07-01") } } },
{ $group: { _id: "$kategori", total: { $sum: "$jumlah" } } }
]).executionStats
print("nReturned: " + sta.nReturned)
print("totalKeysExamined: " + sta.totalKeysExamined)
print("totalDocsExamined: " + sta.totalDocsExamined)tahap teratas: GROUP
tahap pemindaian: IXSCAN
indeks dipakai: tanggal_-1
nReturned: 5
totalKeysExamined: 300
totalDocsExamined: 300Tahap teratas GROUP mengolah hasil tahap FETCH, sedangkan dokumen pada tahap FETCH ditemukan melalui IXSCAN pada indeks tanggal_-1. Server membaca tiga ratus kunci dan tiga ratus dokumen untuk menghasilkan lima kelompok ringkasan. Seluruh pola baca aplikasi kini terverifikasi memakai indeks yang disiapkan untuknya.
Kesalahan umum#
Kesalahan pertama adalah menjadikan waktu eksekusi sebagai satu-satunya bukti. Nilai waktu berubah antareksekusi karena cache dan beban server, sehingga query yang sama dapat melaporkan selisih berlipat pada dua pengukuran berurutan. Perbandingan rencana memakai penghitung jumlah yang stabil, yaitu totalKeysExamined dan totalDocsExamined.
Kesalahan kedua adalah menganggap indeks otomatis dipakai setelah dibuat. Indeks komposit tidak melayani query yang syaratnya tidak menyentuh awalannya, sebagaimana terlihat pada pola baca rentang tanggal murni yang tetap memindai koleksi setelah indeks komposit terpasang. Setiap pola baca diverifikasi melalui indexName pada winningPlan setelah indeksnya dibuat. Perubahan daftar indeks juga dapat menggeser rencana query lain, sehingga verifikasi diulang setiap kali indeks ditambah atau dihapus.
Kesalahan ketiga adalah memasang indeks untuk setiap field tanpa menimbang pola akses. Setiap indeks menambah biaya penulisan dan memori, sedangkan indeks yang tak pernah dipakai hanya membebani sisi tulis. Daftar pola baca aplikasi menentukan daftar indeksnya, dan indeks yang tak terpakai sebaiknya dihapus.
Ringkasan#
- Rencana query dibaca dari
winningPlanmelalui destrukturisasi, denganCOLLSCANsebagai pemindaian koleksi danIXSCANsebagai pembacaan indeks. - Penghitung
nReturned,totalKeysExamined, dantotalDocsExaminedbernilai stabil, sehingga perbandingan sebelum dan sesudah indeks memakai ketiganya. - Indeks komposit
kategori_1_tanggal_-1mengikuti panduan ESR dan mengubah query per kategori dari memindai enam ratus dokumen menjadi membaca enam puluh kunci. - Setiap pola baca aplikasi diverifikasi satu per satu, dan pola tanpa kesetaraan kategori membutuhkan indeks
tanggal_-1sendiri.
Referensi#
- db.collection.explain() — MongoDB Manual — Spesifikasi explain beserta tiga tingkat verbositasnya.
- Explain Results — MongoDB Manual
— Referensi field keluaran explain, dari
winningPlanhingga penghitung eksekusi. - Analyze Query Performance — MongoDB Manual — Panduan membaca statistik eksekusi untuk menilai performa query.
- ESR (Equality, Sort, Range) Guideline — MongoDB Manual — Panduan urutan field pada indeks komposit.
- db.collection.createIndex() — MongoDB Manual — Spesifikasi pembuatan indeks pada koleksi.