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.
  • $elemMatch dan 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:

js
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:

text
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_1

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

DocumentField tagKunci 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:

js
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:

text
[
  { 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:

  1. isMultiKey: true: index ini multikey — satu document bisa terwakili banyak kunci.
  2. multiKeyPaths: { tag: ['tag'] }: jalur field yang membuat index menjadi multikey, berguna pada compound index untuk mengetahui field mana yang array.
  3. indexBounds sempit: query tag: "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:

js
db.buku.find({ "penilaian.pembaca": "Budi", "penilaian.bintang": 4 }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[ { 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:

js
db.buku.find({ penilaian: { $elemMatch: { pembaca: "Budi", bintang: 4 } } }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[ { 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:

js
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:

text
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:

js
try {
  db.buku.createIndex({ tag: 1, "penilaian.bintang": 1 })
} catch (e) {
  print("Galat: " + e.message.split(":: caused by ::").pop().trim())
}

Output yang Dihasilkan:

text
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, ditandai isMultiKey: true dan multiKeyPaths pada 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; $elemMatch memastikan kondisi dipenuhi elemen yang sama.
  • Compound index maksimal memuat satu field array; dua array paralel ditolak dengan galat cannot index parallel arrays.