Setiap index yang kita buat menulis dirinya sendiri pada setiap insert, update, dan delete — biaya yang dibayar tanpa kecuali. Imbalannya hanya datang kalau index itu benar-benar dipakai query. Dari sekian index yang menumpuk di collection produksi, hampir selalu ada yang sudah lama tidak melayani apa pun: sisa eksperimen, query yang sudah berubah, atau fitur yang hilang. MongoDB mencatat pemakaian setiap index lewat tahap agregasi $indexStats, sehingga keputusan menjatuhkan index bisa berbasis data, bukan firasat.

Peta materi Chapter ini:

  • Membaca $indexStats: accesses.ops dan accesses.since.
  • Menggabungkannya dengan getIndexes() menjadi laporan satu layar.
  • Pertimbangan sebelum menjatuhkan index yang tampak tak terpakai.

Membaca $indexStats #

$indexStats adalah tahap agregasi yang mengembalikan satu document per index. Siapkan collection dengan tiga index, lalu gunakan dua di antaranya:

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" }
])
db.penjualan.createIndex({ kota: 1 })
db.penjualan.createIndex({ channel: 1 })
db.penjualan.createIndex({ harga_total: 1 })
db.penjualan.find({ kota: "Jakarta" }).itcount()
db.penjualan.find({ channel: "online" }).itcount()

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
  }
}
kota_1
channel_1
harga_total_1
5
6

Dua query di akhir blok sengaja memakai index kota_1 dan channel_1; index harga_total_1 tidak pernah disentuh. Sekarang kita baca catatan pemakaiannya:

js
db.penjualan.aggregate([ { $indexStats: {} } ])

Output yang Dihasilkan:

text
[
  {
    name: 'channel_1',
    key: { channel: 1 },
    host: 'c-6ad366ea-145cf406-1e00df7b3d2b:27017',
    accesses: { ops: Long('1'), since: ISODate('2026-10-17T17:40:35.455Z') }
  },
  {
    name: '_id_',
    key: { _id: 1 },
    host: 'c-6ad366ea-145cf406-1e00df7b3d2b:27017',
    accesses: { ops: Long('0'), since: ISODate('2026-10-17T17:40:35.336Z') }
  },
  {
    name: 'kota_1',
    key: { kota: 1 },
    host: 'c-6ad366ea-145cf406-1e00df7b3d2b:27017',
    accesses: { ops: Long('1'), since: ISODate('2026-10-17T17:40:35.360Z') }
  },
  {
    name: 'harga_total_1',
    key: { harga_total: 1 },
    host: 'c-6ad366ea-145cf406-1e00df7b3d2b:27017',
    accesses: { ops: Long('0'), since: ISODate('2026-10-17T17:40:35.558Z') }
  }
]

Field spec pada tiap entri dipangkas dari output asli agar ringkas; nilai since dan host di server Anda tentu berbeda, dan hasil runCommand() yang tampil di chapter ini juga dipadatkan dengan membuang metadata waktu replikasi. Field yang penting untuk dibaca:

  1. accesses.ops: berapa kali index ini dipakai oleh query sejak penghitung dimulai. Angka ini mengukur pemakaian pembacaan — index tetap ditulis pada setiap operasi tulis meski ops bernilai 0, jadi biaya maintenance tidak tercermin di sini.
  2. accesses.since: titik waktu mulai penghitung. Penghitung dimulai saat index dibuat dan kembali nol saat server direstart — dua index yang dibuat bersamaan bisa tampak berbeda pemakaiannya semata karena umur penghitungnya berbeda.
  3. host: nama anggota server yang melaporkan. Pada replica set, setiap anggota punya catatannya sendiri — jalankan di semua anggota (termasuk secondary yang melayani query baca) sebelum menyimpulkan sebuah index tak terpakai.

Pada output di atas polanya jelas: channel_1 dan kota_1 menunjukkan ops di atas nol, sementara harga_total_1 bernilai 0 — kandidat kuat index menganggur. Index _id_ juga bernilai 0, tetapi index ini wajib ada dan tidak bisa dijatuhkan.

Laporan Satu Layar #

$indexStats sendiri tidak menampilkan definisi index, sedangkan getIndexes() tidak menampilkan pemakaian — laporan yang lengkap menggabungkan keduanya. Sedikit JavaScript di mongosh merangkainya:

js
const pemakaian = new Map(
  db.penjualan.aggregate([ { $indexStats: {} } ]).toArray().map(s => [s.name, s.accesses.ops])
)
for (const idx of db.penjualan.getIndexes()) {
  print(idx.name.padEnd(16), "ops:", String(pemakaian.get(idx.name)))
}

Output yang Dihasilkan:

text
_id_             ops: 0
kota_1           ops: 1
channel_1        ops: 1
harga_total_1    ops: 0

Laporan seperti ini layak dijalankan berkala — misalnya setiap kuartal — dan hasilnya disimpan sebagai bahan pembanding. Menjatuhkan index yang bersalah:

js
db.penjualan.dropIndex("harga_total_1")

Output yang Dihasilkan:

text
{ nIndexesWas: 4, ok: 1 }

Pertimbangan Sebelum Menjatuhkan Index #

Lima hal yang perlu dicek sebelum dropIndex() dijalankan:

PertimbanganAlasannya
Jendela pengamatan cukup panjangQuery berkala (laporan, batch malam) bisa memakai index jarang tapi kritis
Bukan index uniqueIndex unik menegakkan aturan data, bukan hanya mempercepat query — menjatuhkannya mengubah jaminan
Bukan index TTLIndex TTL menjalankan pembersihan document otomatis; pemakaian rendah memang sifatnya
Definisi tersimpanSimpan output getIndexes() sebelum drop agar index mudah dibuat ulang bila ternyata dibutuhkan
Dampak tulis terukurKeuntungan drop paling terasa pada collection dengan beban tulis tinggi; pada collection jarang ditulis, index menganggur tidak terlalu merugikan

Membuat ulang index pada collection besar adalah operasi mahal yang memakan waktu dan sumber daya — di cluster produksi lazimnya dibangun ulang di latar belakang dengan opsi build tertentu. Itulah sebabnya urutan kerjanya: amati kuartalan, simpan definisi, jatuhkan, lalu pantau beberapa hari pertama setelahnya.

Rangkuman #

  • $indexStats melaporkan accesses.ops (jumlah pemakaian oleh query) dan accesses.since (awal penghitung) untuk setiap index.
  • Penghitung reset saat server restart; ops tidak mengukur biaya maintenance tulis yang tetap berjalan untuk setiap index.
  • Gabungkan $indexStats dengan getIndexes() untuk laporan definisi plus pemakaian dalam satu layar.
  • Ukur pemakaian minimal satu kuartal, periksa status unique/TTL, dan simpan definisi sebelum menjatuhkan index.