Field array adalah warga kelas satu di MongoDB: satu buku boleh memiliki banyak tag, banyak penilaian pembaca, dan banyak penulis. Pertanyaannya — kalau kita meng-index field tag yang berisi ["fiksi", "sastra", "penghargaan"], apa persisnya yang disimpan di index? Jawabannya adalah multikey index: MongoDB memecah array menjadi satu kunci index per elemen, secara otomatis, tanpa opsi khusus. Chapter ini membahas perilaku otomatis tersebut, peran $elemMatch, dan satu batasan penting yang berlaku pada compound index.
Peta materi Chapter ini:
- Multikey index terbentuk otomatis pada field array — bukti lewat
isMultiKey. $elemMatchdan pentingnya menyaring elemen yang sama.- Batasan satu field array per compound index.
Index yang Terbentuk Otomatis pada Array #
Siapkan katalog dengan dua field array: tag berisi string, dan penilaian berisi object penilaian pembaca:
use latihan_index
db.buku.drop()
db.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi", tag: ["fiksi", "sastra", "penghargaan"], stok: 12, penilaian: [{ pembaca: "Budi", bintang: 5 }, { pembaca: "Siti", bintang: 4 }] },
{ judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi", tag: ["fiksi", "remaja", "fantasi"], stok: 25, penilaian: [{ pembaca: "Budi", bintang: 4 }, { pembaca: "Dewi", bintang: 5 }] },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi", tag: ["filsafat", "pengembangan diri"], stok: 18, penilaian: [{ pembaca: "Siti", bintang: 5 }] },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi", tag: ["pengembangan diri", "terjemahan"], stok: 30, penilaian: [{ pembaca: "Dewi", bintang: 4 }, { pembaca: "Eka", bintang: 5 }] },
{ judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", tag: ["fiksi", "remaja"], stok: 9, penilaian: [{ pembaca: "Budi", bintang: 3 }] },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra", tag: ["sastra", "sejarah"], stok: 15, penilaian: [{ pembaca: "Eka", bintang: 4 }] },
{ judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", harga: 65000, kategori: "puisi", tag: ["puisi", "sastra"], stok: 8, penilaian: [{ pembaca: "Siti", bintang: 5 }, { pembaca: "Eka", bintang: 4 }] },
{ judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", harga: 250000, kategori: "referensi", tag: ["referensi", "sains"], stok: 4, penilaian: [{ pembaca: "Dewi", bintang: 4 }] }
])
db.buku.createIndex({ tag: 1 })Output yang Dihasilkan:
switched to db latihan_index
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3b458617a1e966d2df7e6'),
'1': ObjectId('6ad3b458617a1e966d2df7e7'),
'2': ObjectId('6ad3b458617a1e966d2df7e8'),
'3': ObjectId('6ad3b458617a1e966d2df7e9'),
'4': ObjectId('6ad3b458617a1e966d2df7ea'),
'5': ObjectId('6ad3b458617a1e966d2df7eb'),
'6': ObjectId('6ad3b458617a1e966d2df7ec'),
'7': ObjectId('6ad3b458617a1e966d2df7ed')
}
}
tag_1Tidak ada opsi khusus pada createIndex({ tag: 1 }) — perintah ini bentuknya sama persis dengan index field biasa. MongoDB yang mendeteksi bertipe array lalu mengubah index menjadi multikey: setiap elemen array mendapat kunci tersendiri yang menunjuk ke document yang sama.
| Document | Field tag | Kunci index yang dibuat |
|---|---|---|
| Laut Bercerita | ["fiksi", "sastra", "penghargaan"] | 3 kunci: fiksi, sastra, penghargaan |
| Bumi | ["fiksi", "remaja", "fantasi"] | 3 kunci: fiksi, remaja, fantasi |
| Rumah Kaca | ["fiksi", "remaja"] | 2 kunci: fiksi, remaja |
Bukti multikey-nya terlihat pada rencana eksekusi — perhatikan field isMultiKey dan multiKeyPaths:
db.buku.find({ tag: "sastra" }, { _id: 0, judul: 1, tag: 1 })
const rencana = db.buku.find({ tag: "sastra" }).explain()
printjson(rencana.queryPlanner.winningPlan.inputStage)Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita', tag: [ 'fiksi', 'sastra', 'penghargaan' ] },
{ judul: 'Bumi Manusia', tag: [ 'sastra', 'sejarah' ] },
{ judul: 'Hujan Bulan Juni', tag: [ 'puisi', 'sastra' ] }
]
{
stage: 'IXSCAN',
nss: 'latihan_index.buku',
keyPattern: {
tag: 1
},
indexName: 'tag_1',
isMultiKey: true,
multiKeyPaths: {
tag: [
'tag'
]
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
tag: [
'["sastra", "sastra"]'
]
}
}Tiga hal pada output tersebut:
isMultiKey: true: index ini multikey — satu document bisa terwakili banyak kunci.multiKeyPaths: { tag: ['tag'] }: jalur field yang membuat index menjadi multikey, berguna pada compound index untuk mengetahui field mana yang array.indexBoundssempit: querytag: "sastra"melompat langsung ke kunci"sastra"— tiga document ditemukan tanpa memindai yang lain, meskipun array tiap buku berbeda panjang.
$elemMatch: Menyaring Elemen yang Sama #
Array of object membawa jebakan klasik. Coba cari “buku yang dinilai Budi dengan 4 bintang” memakai dotted path:
db.buku.find({ "penilaian.pembaca": "Budi", "penilaian.bintang": 4 }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Laut Bercerita' }, { judul: 'Bumi' } ]Hasilnya dua buku — padahal pada Laut Bercerita, yang memberi 4 bintang adalah Siti, bukan Budi. Query dotted path dicocokkan terhadap seluruh array: Budi memang ada di elemen pertama, dan 4 memang ada di elemen kedua, sehingga document lolos meski keduanya tidak pernah berada di elemen yang sama.
Operator $elemMatch menuntut kedua kondisi dipenuhi oleh satu elemen yang sama:
db.buku.find({ penilaian: { $elemMatch: { pembaca: "Budi", bintang: 4 } } }, { _id: 0, judul: 1 })Output yang Dihasilkan:
[ { judul: 'Bumi' } ]Sekarang hanya Bumi yang lolos — satu-satunya buku yang punya elemen { pembaca: "Budi", bintang: 4 } utuh. Bagaimana multikey index melayani query ini? Buat index pada path bertingkat dan periksa rencananya:
db.buku.createIndex({ "penilaian.bintang": 1 })
const rencana = db.buku.find({ penilaian: { $elemMatch: { pembaca: "Budi", bintang: 4 } } }).explain()
printjson(rencana.queryPlanner.winningPlan)Output yang Dihasilkan:
penilaian.bintang_1
{
isCached: false,
stage: 'FETCH',
filter: {
penilaian: {
'$elemMatch': {
'$and': [
{
bintang: {
'$eq': 4
}
},
{
pembaca: {
'$eq': 'Budi'
}
}
]
}
}
},
nss: 'latihan_index.buku',
inputStage: {
stage: 'IXSCAN',
nss: 'latihan_index.buku',
keyPattern: {
'penilaian.bintang': 1
},
indexName: 'penilaian.bintang_1',
isMultiKey: true,
multiKeyPaths: {
'penilaian.bintang': [
'penilaian'
]
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
'penilaian.bintang': [
'[4, 4]'
]
}
}
}Pembagian kerjanya rapi: IXSCAN pada index multikey penilaian.bintang_1 memangkas kandidat lewat batas [4, 4] — hanya document yang memiliki elemen mana pun bernilai 4 yang diambil. Tahap FETCH kemudian menguji ulang $elemMatch lengkap untuk memastikan elemen bernilai 4 itu juga milik pembaca Budi. Index memangkas, fetch memastikan.
Batasan: Satu Field Array per Compound Index #
Compound index boleh memuat field array — tetapi paling satu. Jika dua field yang di-index sama-sama bertipe array, MongoDB menolak karena kombinasi kuncinya akan berlipat kurang dari semestinya (setiap pasangan elemen antar dua array harus ada, dan duplikasi penunjuknya ambigu). Uji batasnya:
try {
db.buku.createIndex({ tag: 1, "penilaian.bintang": 1 })
} catch (e) {
print("Galat: " + e.message.split(":: caused by ::").pop().trim())
}Output yang Dihasilkan:
Galat: cannot index parallel arrays [penilaian] [tag]Index { judul: 1, tag: 1 } tetap sah — judul string, tag array, jumlah field array tepat satu. Yang dilarang adalah dua array sekaligus seperti tag dan penilaian di atas.
Rangkuman #
createIndex()pada field array otomatis menjadi multikey: satu kunci per elemen, ditandaiisMultiKey: truedanmultiKeyPathspada explain.- Query nilai array — termasuk
$elemMatch— tetap dilayani indexBounds yang sempit; tahap FETCH menguji ulang kondisi lengkapnya. - Dotted path pada array of object bisa menghasilkan positif palsu;
$elemMatchmemastikan kondisi dipenuhi elemen yang sama. - Compound index maksimal memuat satu field array; dua array paralel ditolak dengan galat
cannot index parallel arrays.