explain()

Membaca rencana query dengan explain() untuk memverifikasi pemakaian indeks.

Daftar isi

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_.

mongosh
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())
text Keluaran
switched to db kamus

Dokumen tersimpan: 500
mongosh
const bersih = db.entri.dropIndexes()
print("Indeks non-_id dibersihkan: " + (bersih.ok === 1))
text Keluaran
Indeks non-_id dibersihkan: true

Query berikut menyaring kategori verba dan mengurutkan hasilnya menurut frekuensi menurun. Rencananya dibaca melalui explain() tanpa argumen, yang sama dengan explain("queryPlanner").

mongosh
db.entri.find({ kategori: "verba" }).sort({ frekuensi: -1 }).explain().queryPlanner.winningPlan
text Keluaran
{
  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.

mongosh
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.winningPlan
text Keluaran
kata_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.

mongosh
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)
text Keluaran
tahap teratas: FETCH

tahap indeks: IXSCAN

indeks dipakai: kategori_1_frekuensi_-1

arah: forward

Mengukur 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.

mongosh
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)
text Keluaran
nReturned: 34

totalKeysExamined: 167

totalDocsExamined: 167

Server 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.

mongosh
const st2 = db.entri.find({ kata: "batah" }).explain("executionStats").executionStats
print("nReturned: " + st2.nReturned)
print("totalKeysExamined: " + st2.totalKeysExamined)
print("totalDocsExamined: " + st2.totalDocsExamined)
text Keluaran
nReturned: 5

totalKeysExamined: 5

totalDocsExamined: 5

Rasio 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.

mongosh
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)
text Keluaran
rencana menang: kata_1

kandidat kalah: 1

Server 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.

mongosh
const tanpaStat = db.entri.find({ kata: "batah" }).explain()
print("executionStats dari mode queryPlanner: " + tanpaStat.executionStats)
text Keluaran
executionStats dari mode queryPlanner: undefined

Perbaikannya 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 queryPlanner yang hanya menampilkan rencana, executionStats yang mengeksekusi dan melaporkan statistik, serta allPlansExecution yang membandingkan seluruh kandidat.
  • winningPlan dibaca dari tahap terbawah, dengan IXSCAN sebagai pembacaan indeks, FETCH sebagai pengambilan dokumen, dan indexBounds sebagai batas kunci yang dilalui.
  • Penghitung nReturned, totalKeysExamined, dan totalDocsExamined bernilai stabil antareksekusi, sedangkan executionTimeMillis tidak, sehingga perbandingan rencana memakai ketiga penghitung tersebut.
  • Pemakaian indeks diverifikasi melalui indexName pada winningPlan, dan kandidat yang kalah tercatat pada rejectedPlans.

Referensi#