Query nyata jarang menyaring satu field. Layar katalog biasanya memfilter kategori dan rentang harga sekaligus, lalu mengurutkan hasilnya. Kebutuhan itulah yang dijawab compound index — index atas dua field atau lebih dalam satu struktur B-tree. Chapter ini membahas cara kerjanya, aturan prefix yang menentukan query mana yang bisa memanfaatkannya, pola ESR untuk merancang urutan field, serta kapan dua index terpisah bisa — dan biasanya tidak — dipakai bersama.
Peta materi Chapter ini:
- Membuat compound index dan membaca namanya.
- Aturan prefix: kombinasi query yang didukung dan yang tidak.
- Pola ESR (Equality, Sort, Range) sebagai panduan resmi urutan field.
- Index intersection: dua index bekerja sama — jarang, dan kalah dari compound.
Membangun Index Majemuk #
Compound index dibuat dengan memberikan lebih dari satu field pada createIndex(). Siapkan katalog delapan buku kita:
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: 18 },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi", stok: 30 },
{ judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", stok: 9 },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra", stok: 15 },
{ 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: 4 }
])
db.buku.createIndex({ kategori: 1, harga: 1 })Output yang Dihasilkan:
switched to db latihan_index
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3b3c474dbaf33f076fe5c'),
'1': ObjectId('6ad3b3c474dbaf33f076fe5d'),
'2': ObjectId('6ad3b3c474dbaf33f076fe5e'),
'3': ObjectId('6ad3b3c474dbaf33f076fe5f'),
'4': ObjectId('6ad3b3c474dbaf33f076fe60'),
'5': ObjectId('6ad3b3c474dbaf33f076fe61'),
'6': ObjectId('6ad3b3c474dbaf33f076fe62'),
'7': ObjectId('6ad3b3c474dbaf33f076fe63')
}
}
kategori_1_harga_1Index kategori_1_harga_1 menyimpan pasangan nilai (kategori, harga) yang terurut menurut kategori terlebih dahulu, lalu harga di dalam tiap kategori — seperti buku telepon yang dikelompokkan per kota lalu diurutkan per nama. Urutan penulisan field di createIndex() menentukan urutan pengelompokan ini, dan urutan itulah yang menumbuhkan aturan prefix.
Aturan Prefix #
Compound index { kategori: 1, harga: 1 } memiliki prefix yang bisa dipakai: { kategori: 1 } saja, atau { kategori: 1, harga: 1 } penuh. Yang tidak dimiliki: { harga: 1 } — karena harga hanya terurut di dalam kategori, tidak secara global.
Mari kita buktikan keempat kombinasi tersebut dengan explain():
function ringkas(kueri) {
const tahap = kueri.queryPlanner.winningPlan
if (!tahap.inputStage) return tahap.stage
return tahap.inputStage.stage + " lewat " + tahap.inputStage.indexName
}
printjson({
kategoriSaja: ringkas(db.buku.find({ kategori: "fiksi" }).explain()),
kategoriDanHarga: ringkas(db.buku.find({ kategori: "fiksi", harga: { $gte: 90000 } }).explain()),
kategoriDanStok: ringkas(db.buku.find({ kategori: "fiksi", stok: { $lt: 10 } }).explain()),
hargaSaja: ringkas(db.buku.find({ harga: 89000 }).explain())
})Output yang Dihasilkan:
[Function: ringkas]
{
kategoriSaja: 'IXSCAN lewat kategori_1_harga_1',
kategoriDanHarga: 'IXSCAN lewat kategori_1_harga_1',
kategoriDanStok: 'IXSCAN lewat kategori_1_harga_1',
hargaSaja: 'COLLSCAN'
}Hasil tersebut dirangkum dalam tabel berikut — pola yang berlaku untuk compound index tiga field { a: 1, b: 1, c: 1 }:
| Query filter | Memakai index? | Penjelasan |
|---|---|---|
{ a } | Ya, penuh | Prefix { a } tersedia |
{ a, b } | Ya, penuh | Prefix { a, b } tersedia |
{ a, b, c } | Ya, penuh | Seluruh key pattern cocok |
{ a, c } | Ya, sebagian | Hanya a yang mempersempit batas; c disaring setelah fetch |
{ b } atau { c } | Tidak | Bukan prefix — COLLSCAN |
{ b, c } | Tidak | Tetap bukan prefix |
Kasus kategoriDanStok di output di atas adalah contoh baris { a, c }: index tetap terpakai untuk prefix kategori, sedangkan stok disaring sebagai residu — document yang lolos batas kategori diperiksa satu per satu. Index membantu, tetapi tidak selektif query { kategori, harga } yang mempersempit dua batas sekaligus.
Pola ESR: Equality, Sort, Range #
Field dengan equality (nilai tepat seperti kategori: "fiksi") harus berada paling depan, disusul field yang melayani sort(), dan terakhir field rentang ($gte, $lt). Aturan ini dikenal sebagai pola ESR — Equality, Sort, Range dari dokumentasi resmi MongoDB.
Alasannya mekanis: equality memangkas area pencarian ke satu blok kecil; field sort yang berada tepat setelahnya membuat hasil keluar dari index sudah terurut; field range paling belakang karena rentang memang harus memindai sekumpulan kunci, dan posisinya di ujung membuat pemindaian itu terjadi pada blok terkecil.
Bandingkan dua index untuk query “buku fiksi dengan harga di atas 80000, urut stok terbanyak”:
function temukanIXSCAN(tahap) {
while (tahap) {
if (tahap.stage === "IXSCAN") return tahap.indexName
tahap = tahap.inputStage
}
return "(tanpa index)"
}
const sebelum = db.buku.find({ kategori: "fiksi", harga: { $gte: 80000 } }).sort({ stok: -1 }).explain()
db.buku.createIndex({ kategori: 1, stok: -1, harga: 1 })
const sesudah = db.buku.find({ kategori: "fiksi", harga: { $gte: 80000 } }).sort({ stok: -1 }).explain()
printjson({
indexER: {
tahapTeratas: sebelum.queryPlanner.winningPlan.stage,
indexTerpakai: temukanIXSCAN(sebelum.queryPlanner.winningPlan)
},
indexESR: {
tahapTeratas: sesudah.queryPlanner.winningPlan.stage,
indexTerpakai: temukanIXSCAN(sesudah.queryPlanner.winningPlan)
}
})Output yang Dihasilkan:
[Function: temukanIXSCAN]
kategori_1_stok_-1_harga_1
{
indexER: {
tahapTeratas: 'SORT',
indexTerpakai: 'kategori_1_harga_1'
},
indexESR: {
tahapTeratas: 'FETCH',
indexTerpakai: 'kategori_1_stok_-1_harga_1'
}
}Mari kita bedah dua rencana tersebut:
- Index ER (
kategori_1_harga_1): equality dan range terlayani, tetapistoktidak ada di index. Planner harus menjalankan tahapSORT— seluruh hasil dikumpulkan lalu diurutkan di memori, dengan batas 100 MB sebagaimana dibahas di Chapter Sorting. - Index ESR (
kategori_1_stok_-1_harga_1):kategorimenjadi equality,stok: -1mengambil alih pengurutan,hargamenutup sebagai range. Tahap teratas langsungFETCH— tidak adaSORTsama sekali karena index sudah mengembalikan hasil terurut. - Arah
-1padastoksengaja dipilih agar arah index sama dengan arah sort — pembahasan mendalam ada di Chapter Index Ordering.
Index Intersection #
Bagaimana jika kita membuat dua index terpisah, { kategori: 1 } dan { harga: 1 }, untuk query yang menyaring keduanya? MongoDB mampu menggabungkan dua index dalam satu rencana (tahap AND_SORTED/AND_HASH), tetapi planner lebih sering memilih salah satu yang paling menjanjikan:
const hapus1 = db.buku.dropIndex("kategori_1_stok_-1_harga_1")
const hapus2 = db.buku.dropIndex("kategori_1_harga_1")
db.buku.createIndex({ kategori: 1 })
db.buku.createIndex({ harga: 1 })
const rencana = db.buku.find({ kategori: "fiksi", harga: { $gte: 90000 } }).explain()
printjson({
indexTerpilih: temukanIXSCAN(rencana.queryPlanner.winningPlan),
rencanaDitolak: rencana.queryPlanner.rejectedPlans.map(p => temukanIXSCAN(p))
})Output yang Dihasilkan:
kategori_1
harga_1
{
indexTerpilih: 'kategori_1',
rencanaDitolak: [
'harga_1'
]
}Planner memilih kategori_1 saja dan menyaring harga sebagai residu — harga_1 masuk daftar rejectedPlans. Intersection memang kadang menang pada kasus tertentu, tetapi hampir selalu kalah dari satu compound index yang dirancang benar: intersection harus menggabungkan dua hasil pemindaian, sementara compound index memangkas langsung di satu struktur.
Rangkuman #
- Compound index
{ a, b }terurut menurutalalub; namanya adalah rangkaian field dan arah, misalnyakategori_1_harga_1. - Aturan prefix: hanya kombinasi yang berawal dari field terdepan yang terlayani;
{ b }atau{ b, c }tidak memakai index{ a, b, c }. - Pola ESR (Equality → Sort → Range) menentukan urutan field optimal; index ESR menghapus tahap
SORTyang index ER masih tanggung. - Query dengan field non-prefix tetap bisa memakai index sebagian — untuk prefix-nya saja.
- Index intersection jarang terpilih dan kalah dari compound index yang sesuai; jangan mengandalkannya dalam perancangan.