Tidak semua document di collection bernilai sama bagi query. Buku yang stoknya habis jarang dicari pembeli, transaksi yang sudah selesai jarang dibuka lagi — namun index biasa tetap mencatat semuanya tanpa kecuali. Partial index mengizinkan kita menyeleksi: hanya document yang memenuhi kondisi tertentu yang masuk index. Hasilnya index lebih kecil, tulis lebih ringan, dan query panas tetap cepat.

Peta materi Chapter ini:

  • Membuat partial index dengan partialFilterExpression.
  • Syarat query agar bisa memakai partial index.
  • Kombinasi unique + partial untuk field opsional seperti email.
  • Posisi partial index dibanding sparse index.

Index Hanya untuk Document Terpilih #

Opsi partialFilterExpression menerima filter query yang menentukan siapa saja yang boleh masuk index. Siapkan katalog yang sebagian bukunya kehabisan stok:

js
use latihan_index
db.buku.drop()
db.buku.insertMany([
  { judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi", stok: 12 },
  { judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi", stok: 25 },
  { judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi", stok: 0 },
  { judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", stok: 9 },
  { judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", harga: 65000, kategori: "puisi", stok: 8 },
  { judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", harga: 250000, kategori: "referensi", stok: 0 }
])
db.buku.createIndex({ harga: 1 }, { partialFilterExpression: { stok: { $gt: 0 } } })

Output yang Dihasilkan:

text
switched to db latihan_index
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3bd5ba3a7ad00ef8340c0'),
    '1': ObjectId('6ad3bd5ba3a7ad00ef8340c1'),
    '2': ObjectId('6ad3bd5ba3a7ad00ef8340c2'),
    '3': ObjectId('6ad3bd5ba3a7ad00ef8340c3'),
    '4': ObjectId('6ad3bd5ba3a7ad00ef8340c4'),
    '5': ObjectId('6ad3bd5ba3a7ad00ef8340c5')
  }
}
harga_1

Index harga_1 kini hanya berisi empat entri — buku berstok nol (“Filosofi Teras” dan “Ensiklopedia Sains Populer”) tidak pernah masuk. Mari uji bagaimana planner memperlakukan dua query yang mirip:

js
function ringkas(kueri) {
  const tahap = kueri.queryPlanner.winningPlan
  return tahap.inputStage ? tahap.inputStage.stage + " lewat " + tahap.inputStage.indexName : tahap.stage
}
printjson({
  denganFilterStok: ringkas(db.buku.find({ stok: { $gt: 0 }, harga: { $lt: 90000 } }).explain()),
  tanpaFilterStok: ringkas(db.buku.find({ harga: { $lt: 90000 } }).explain())
})

Output yang Dihasilkan:

text
[Function: ringkas]
{
  denganFilterStok: 'IXSCAN lewat harga_1',
  tanpaFilterStok: 'COLLSCAN'
}

Perbedaan keduanya tegas. Query pertama menyertakan stok: { $gt: 0 } — kondisi yang sama dengan partial filter — sehingga planner yakin hasilnya identik dengan isi index dan memilih IXSCAN. Query kedua hanya menyaring harga; karena index tidak memuat buku berstok nol, memakai index berisiko kehilangan hasil, maka planner mundur ke COLLSCAN.

Kombinasi Unique dan Partial #

Gabungan paling populer dari partial index adalah unique + partial untuk field opsional. Anggota toko buku boleh tidak punya email, tetapi email yang terdaftar tidak boleh kembar:

js
db.anggota.drop()
db.anggota.insertMany([
  { nama: "Budi", email: "[email protected]" },
  { nama: "Siti" },
  { nama: "Dewi" }
])
db.anggota.createIndex(
  { email: 1 },
  { unique: true, partialFilterExpression: { email: { $exists: true } } }
)
db.anggota.insertOne({ nama: "Eka" })
try {
  db.anggota.insertOne({ nama: "Fajar", email: "[email protected]" })
} catch (e) {
  print("Kode galat: " + e.code)
  print("Pesan: " + e.message)
}

Output yang Dihasilkan:

text
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3bd5ba3a7ad00ef8340c6'),
    '1': ObjectId('6ad3bd5ba3a7ad00ef8340c7'),
    '2': ObjectId('6ad3bd5ba3a7ad00ef8340c8')
  }
}
email_1
{
  acknowledged: true,
  insertedId: ObjectId('6ad3bd5ba3a7ad00ef8340c9')
}
Kode galat: 11000
Pesan: E11000 duplicate key error collection: latihan_index.anggota index: email_1 dup key: { email: "[email protected]" }

Empat document tanpa email hidup berdampingan tanpa masalah — mereka berada di luar jangkauan index. Sebaliknya, email [email protected] yang coba didaftarkan Fajar ditolak dengan galat E11000 karena keunikan hanya ditegakkan pada document yang memiliki field email. Solusi klasik masalah “field unik yang opsional” berakhir tanpa trik apa pun.

Partial atau Sparse #

MongoDB punya satu fitur lawas yang mirip: sparse index, yang hanya meng-index document yang memiliki field terkait (dibahas penuh di Chapter Sparse Index). Tabel ringkasnya:

AspekPartial indexSparse index
Kriteria masuk indexFilter bebas: $gt, $exists, $eq, $and, tipe fieldSatu syarat saja: field ada
Presisi seleksiTinggi — bisa stok > 0, status: "aktif"Rendah — hanya ada/tidaknya field
Dokumentasi resmiDirekomendasikanDigantikan posisinya oleh partial

Dokumentasi MongoDB menyatakan partial index unggul dalam kontrol: sparse hanya tahu “field ini ada”, partial bisa menyaring “field ini ada dan nilainya memenuhi kondisi”. Aturan praktisnya sederhana — untuk index baru yang butuh seleksi, pilih partialFilterExpression; sparse tinggal dikenali pada sistem lama.

Rangkuman #

  • createIndex({ field: 1 }, { partialFilterExpression: { ... } }) hanya meng-index document yang lolos filter — index lebih kecil dan tulis lebih ringan.
  • Query harus memuat kondisi yang sama agar planner mau memakai partial index; tanpa itu, planner memilih COLLSCAN demi kebenaran hasil.
  • Kombinasi unique + partial menyelesaikan pola “field unik opsional”: document tanpa field bebas berjumlah banyak, yang punya field dijaga keunikannya.
  • Dokumentasi resmi merekomendasikan partial index menggantikan sparse index untuk kebutuhan seleksi apa pun.