explain() mode queryPlanner (Chapter sebelumnya) hanya menunjukkan niat: index mana yang akan dipakai. Mode executionStats mengeksekusi query sungguhan sambil mencatat tagihannya — berapa kunci index diperiksa, berapa document dibaca, berapa lama waktunya. Dua query bisa sama-sama mengembalikan lima hasil, tetapi yang satu membaca lima document dan yang lain lima juta; angka-angka inilah yang membedakan perbaikan performa asli dari tebakan. Chapter ini berfokus pada cara membaca dan menilai angka-angka tersebut.

Peta materi Chapter ini:

  • Menjalankan explain("executionStats") pada query tanpa index dan dengan index.
  • Empat metrik utama: nReturned, totalKeysExamined, totalDocsExamined, executionTimeMillis.
  • Rasio efisiensi dan heuristik kondisi sehat.

Menjalankan Query dan Mencatat Tagihannya #

Siapkan kembali collection transaksi toko buku, kali ini tanpa index agar kita bisa membandingkan dua kondisi:

js
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" }
])

Output yang Dihasilkan:

text
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
  }
}

Mode executionStats diaktifkan dengan memberi argumen string pada explain(). Jalankan pada query transaksi kota Jakarta — sebelum index dibuat:

js
db.penjualan.find({ kota: "Jakarta" }).explain("executionStats")

Output yang Dihasilkan:

text
{
  explainVersion: '1',
  queryPlanner: {
    winningPlan: {
      isCached: false,
      stage: 'COLLSCAN',
      filter: { kota: { '$eq': 'Jakarta' } },
      direction: 'forward'
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 5,
    executionTimeMillis: 0,
    totalKeysExamined: 0,
    totalDocsExamined: 12,
    executionStages: {
      stage: 'COLLSCAN',
      filter: { kota: { '$eq': 'Jakarta' } },
      nReturned: 5,
      works: 13,
      advanced: 5,
      needTime: 7,
      isEOF: 1,
      docsExamined: 12,
      seeks: 0
    }
  },
  command: {
    find: 'penjualan',
    filter: { kota: 'Jakarta' },
    '$db': 'latihan_perf'
  },
  ok: 1
}

Seperti Chapter sebelumnya, output explain() di sini dipadatkan — field teknis (nss, multiKeyPaths, serverInfo, serverParameters, metadata replikasi) dipangkas agar fokus pada angka. Bagian pentingnya berada di field executionStats: pada collection 12 document, server membaca seluruh 12 document (totalDocsExamined) untuk mengembalikan 5 hasil (nReturned) — sisa 7 document dibaca sia-sia karena tidak ada index yang bisa memangkas pencarian. Sekarang buat index dan jalankan query yang persis sama:

js
db.penjualan.createIndex({ kota: 1 })
db.penjualan.find({ kota: "Jakarta" }).explain("executionStats")

Output yang Dihasilkan:

text
kota_1
{
  explainVersion: '1',
  queryPlanner: {
    winningPlan: {
      isCached: false,
      stage: 'FETCH',
      inputStage: {
        stage: 'IXSCAN',
        keyPattern: { kota: 1 },
        indexName: 'kota_1',
        indexBounds: { kota: [ '["Jakarta", "Jakarta"]' ] }
      }
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 5,
    executionTimeMillis: 0,
    totalKeysExamined: 5,
    totalDocsExamined: 5,
    executionStages: {
      stage: 'FETCH',
      nReturned: 5,
      works: 6,
      advanced: 5,
      docsExamined: 5,
      inputStage: {
        stage: 'IXSCAN',
        nReturned: 5,
        works: 6,
        advanced: 5,
        keysExamined: 5,
        seeks: 1
      }
    }
  },
  command: {
    find: 'penjualan',
    filter: { kota: 'Jakarta' },
    '$db': 'latihan_perf'
  },
  ok: 1
}

Perbandingannya lurus: dengan index, hanya 5 kunci diperiksa dan 5 document dibaca untuk 5 hasil. Tidak ada pembacaan yang sia-sia — itulah ciri query yang sehat.

Empat Metrik Utama #

MetrikArtiCara membacanya
nReturnedJumlah document yang lolos ke hasilHarus dekat dengan ekspektasi bisnis query
totalKeysExaminedTotal kunci index yang diperiksa semua tahap0 berarti tidak ada index yang terpakai
totalDocsExaminedTotal document yang dibaca dari collectionSumber biaya terbesar pada data besar
executionTimeMillisTotal waktu eksekusi server (milidetik)Acuan kasar; sangat dipengaruhi cache dan beban

Dua metrik pertama adalah pasangan diagnosis: totalKeysExamined yang besar dengan nReturned kecil berarti index yang dipakai tidak selektif — rentang indexBounds-nya terlalu lebar. Sementara totalDocsExamined yang jauh di atas nReturned menandakan server membaca banyak document yang akhirnya dibuang filter — indikasi index yang tidak cocok atau query yang tidak bisa memanfaatkan index.

executionStages memuat versi per-tahap dari angka-angka tersebut: setiap stage (FETCH, IXSCAN, SORT) punya nReturned, keysExamined, dan docsExamined sendiri. Total di level atas adalah penjumlahannya — struktur pohonnya kita bedah tuntas di Chapter berikutnya.

Rasio Efisiensi #

Cara paling cepat menilai sebuah rencana adalah membandingkan ketiga angka tersebut dalam bentuk rasio. Kondisi ideal untuk query ber-index adalah nReturned : totalKeysExamined : totalDocsExamined = 1 : 1 : 1 — setiap pemeriksaan menghasilkan satu document yang memang dikirim ke klien. Mari kita hitung untuk kedua rencana tadi:

js
const tanpaIndex = { nReturned: 5, totalKeysExamined: 0, totalDocsExamined: 12 }
const denganIndex = db.penjualan.find({ kota: "Jakarta" }).explain("executionStats").executionStats
print("COLLSCAN  -> nReturned:", tanpaIndex.nReturned, "| keys:", tanpaIndex.totalKeysExamined, "| docs:", tanpaIndex.totalDocsExamined)
print("IXSCAN    -> nReturned:", denganIndex.nReturned, "| keys:", denganIndex.totalKeysExamined, "| docs:", denganIndex.totalDocsExamined)
print("rasio docs/hasil -> COLLSCAN:", (tanpaIndex.totalDocsExamined / tanpaIndex.nReturned).toFixed(2), "| IXSCAN:", (denganIndex.totalDocsExamined / denganIndex.nReturned).toFixed(2))

Output yang Dihasilkan:

text
COLLSCAN  -> nReturned: 5 | keys: 0 | docs: 12

IXSCAN    -> nReturned: 5 | keys: 5 | docs: 5

rasio docs/hasil -> COLLSCAN: 2.40 | IXSCAN: 1.00

Rasio totalDocsExamined / nReturned pada COLLSCAN adalah 2,40 — setiap hasil menanggung pembacaan 2,4 document. Angka ini terlihat jinak karena collection kita kecil; pada satu juta document dengan sebaran kota yang sama, rasionya menjadi sekitar 200.000 dan query mulai terasa berat. Rasio 1,00 pada IXSCAN tidak pernah berubah sebesar apa pun collection — itulah alasan index disebut penskala: biayanya tumbuh mengikuti jumlah hasil, bukan jumlah data.

Rasio yang sehat menurut pengalaman praktis:

KondisinReturned : keys : docsTindakan
Ideal1 : 1 : 1Tidak ada yang perlu dibenahi
Wajar1 : beberapa : 1Index masih selektif; pantau bila data membesar
Boros1 : banyak : banyakPerbaiki index atau bentuk query
Tanpa index1 : 0 : semuaBuat index pada field filter

Rangkuman #

  • explain("executionStats") mengeksekusi query sungguhan dan melaporkan biayanya; mode queryPlanner hanya menunjukkan rencana.
  • Empat angka kunci: nReturned, totalKeysExamined, totalDocsExamined, executionTimeMillis — dua yang tengah adalah yang paling bisa diandalkan.
  • Rasio ideal ber-index adalah 1 : 1 : 1; COLLSCAN selalu berakhir totalDocsExamined = seluruh collection.
  • Waktu eksekusi pada dataset kecil tidak bisa dijadikan bukti; biaya pemeriksaan adalah penentu yang stabil.