Field array muncul secara alami pada dokumen MongoDB, misalnya daftar sinonim pada sebuah entri kamus, dan indeks multikey (multikey) membuat field semacam itu tetap dapat diquery dengan cepat. Topik ini membahas cara query array dicocokkan sebelum indeks ada, pembuatan indeks multikey beserta penandanya pada rencana query, pemakaian field array di dalam indeks komposit, perilaku multikey pada array bersarang, serta batasan query tertutup dan dua field array pada satu indeks. Demonstrasi memakai basis data kamus dengan koleksi entri berisi lima ratus dokumen. Setelah mengikuti topik ini, field array dapat diindeks dengan mengetahui perilaku yang menyertainya.
Query pada field array#
Query pada field array mencocokkan dokumen bila salah satu elemennya memenuhi syarat, sehingga find({ sinonim: "akurat" }) mengembalikan setiap entri yang daftar sinonimnya memuat kata tersebut. Pencocokan elemen tunggal ini berbeda dengan pencocokan array utuh yang menuntut kesamaan seluruh isinya. Tanpa indeks, server tetap menjalankan pemindaian koleksi karena pencocokan per elemen harus diuji pada setiap dokumen.
Tanamlah data contoh berikut. Setiap dokumen memuat field sinonim berisi dua kata, field suku berisi dua suku kata pembentuk kata utamanya, serta field makna berisi array subdokumen yang masing-masing memuat satu arti.
use kamus
const sukuAwal = ["ba", "ta", "ka", "me", "pa", "sa", "ni", "go", "lu", "wi"]
const sukuAkhir = ["tah", "rin", "sup", "kan", "del", "mus", "pel", "rong", "wis", "dak"]
const daftarKategori = ["nomina", "verba", "adjektiva"]
const daftarSinonim = ["cepat", "gesit", "laju", "ringan", "tepat", "akurat", "rapi", "jelas", "padat", "lentur"]
const daftarArti = ["utama", "murni", "tepat", "luas", "kasar", "halus"]
const docs = []
const tambah = function (i, j, k) {
docs.push({
kata: sukuAwal[i] + sukuAkhir[j],
homonim: k + 1,
kategori: daftarKategori[(i + j + k) % 3],
sinonim: [daftarSinonim[(i + k) % 10], daftarSinonim[(j + 3) % 10]],
suku: [sukuAwal[i], sukuAkhir[j]],
makna: [{ arti: daftarArti[(i * 3 + j + k) % 6] }]
})
}
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
for (let k = 0; k < 5; k++) {
tambah(i, j, k)
}
}
}
const hasil = db.entri.insertMany(docs)
print("Dokumen tersimpan: " + db.entri.countDocuments())switched to db kamus
Dokumen tersimpan: 500const bersih = db.entri.dropIndexes()
print("Indeks non-_id dibersihkan: " + (bersih.ok === 1))Indeks non-_id dibersihkan: trueHitung dokumen yang sinonimnya memuat akurat, lalu ukur pekerjaan server melalui explain("executionStats").
print("Dokumen cocok: " + db.entri.countDocuments({ sinonim: "akurat" }))
const st1 = db.entri.find({ sinonim: "akurat" }).explain("executionStats").executionStats
print("totalKeysExamined: " + st1.totalKeysExamined)
print("totalDocsExamined: " + st1.totalDocsExamined)Dokumen cocok: 95
totalKeysExamined: 0
totalDocsExamined: 500Server memeriksa lima ratus dokumen untuk menemukan 95 dokumen yang cocok. Setiap tambahan dokumen membuat pemindaian ini semakin mahal, sehingga field array yang sering diquery layak diindeks.
Membuat indeks multikey#
Sintaks pembuatan indeksnya sama dengan field biasa, yaitu createIndex dengan pasangan field dan arah. Server mengenali field bernilai array selama pembangunan indeks dan mengubah indeksnya menjadi multikey secara otomatis, yaitu menyimpan satu kunci untuk setiap elemen array dari setiap dokumen. Karena itu jumlah kunci indeks bisa lebih banyak daripada jumlah dokumen, dan ukuran indeksnya tumbuh mengikuti panjang array terpanjang. Dokumen yang fieldnya bukan array tetap menyumbang satu kunci, sehingga jenis nilai yang beragam pada satu field tidak menghambat pembuatan indeks.
Buat indeks pada field sinonim, lalu periksa rencana query yang sama.
db.entri.createIndex({ sinonim: 1 })
db.entri.find({ sinonim: "akurat" }).explain("queryPlanner").queryPlanner.winningPlansinonim_1
{
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { sinonim: 1 },
indexName: 'sinonim_1',
isMultiKey: true,
multiKeyPaths: { sinonim: [ 'sinonim' ] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { sinonim: [ '["akurat", "akurat"]' ] }
}
}Field isMultiKey bernilai true, dan field multiKeyPaths menyebut field sinonim sebagai jalur array. Kedua penanda ini dibawa server pada setiap rencana yang memakai indeks multikey, sehingga perilaku indeks dapat diverifikasi tanpa menebak.
const st2 = db.entri.find({ sinonim: "akurat" }).explain("executionStats").executionStats
print("nReturned: " + st2.nReturned)
print("totalKeysExamined: " + st2.totalKeysExamined)
print("totalDocsExamined: " + st2.totalDocsExamined)nReturned: 95
totalKeysExamined: 95
totalDocsExamined: 95Jumlah kunci yang diperiksa sama dengan jumlah dokumen yang dikembalikan karena batas kunci ["akurat", "akurat"] hanya melalui kunci bernilai itu. Setiap dokumen yang cocok menyumbang tepat satu kunci akurat pada indeks, dan dokumen tanpa nilai tersebut tidak pernah disentuh.
Indeks komposit dengan satu field array#
Field array dapat menjadi bagian indeks komposit selama hanya satu field array yang terlibat. Indeks { kategori: 1, sinonim: 1 } menyimpan satu kunci untuk setiap kombinasi kategori dan elemen sinonim, sehingga query yang menyaring keduanya tetap dilayani oleh satu indeks. Nilai kategori menempati bagian depan kunci dan mempersempit perjalanan indeks sejak awal, sedangkan elemen array hanya menentukan bagian belakang kuncinya. Susunan inilah yang membuat kombinasi penyaringan field biasa dengan elemen array tetap efisien tanpa indeks tambahan.
Buat indeks komposit tersebut, lalu ukur query yang menyaring kategori dan sinonim sekaligus.
db.entri.createIndex({ kategori: 1, sinonim: 1 })
const p1 = db.entri.find({ kategori: "verba", sinonim: "cepat" }).explain("executionStats")
print("isMultiKey: " + p1.queryPlanner.winningPlan.inputStage.isMultiKey)
print("nReturned: " + p1.executionStats.nReturned)
print("totalKeysExamined: " + p1.executionStats.totalKeysExamined)
print("totalDocsExamined: " + p1.executionStats.totalDocsExamined)kategori_1_sinonim_1
isMultiKey: true
nReturned: 35
totalKeysExamined: 35
totalDocsExamined: 35Server memeriksa 35 kunci dan 35 dokumen untuk mengembalikan 35 dokumen kategori verba yang sinonimnya memuat cepat. Penanda isMultiKey tetap bernilai true karena salah satu jalurnya, yaitu sinonim, adalah array, sedangkan field kategori tetap bertindak sebagai penyaring kesamaan pada bagian depan kunci.
Multikey pada array bersarang#
Array dapat berisi subdokumen, dan field di dalam subdokumen tersebut dapat diindeks dengan penulisan jalur titik, yaitu nama field induk dan field di dalamnya yang dipisahkan tanda titik. Indeks pada jalur makna.arti menyimpan satu kunci untuk field arti pada setiap elemen array makna, dan server menetapkan status multikey karena jalur induknya adalah array. Penanda multiKeyPaths pada rencana query memuat jalur array induknya, yaitu makna, sehingga asal perilaku multikey tetap terbaca meskipun field yang diindeks bukan array itu sendiri.
Buat indeks pada jalur makna.arti, lalu periksa rencana query yang menyaring arti utama beserta jumlah dokumennya.
db.entri.createIndex({ "makna.arti": 1 })
print("Dokumen makna utama: " + db.entri.countDocuments({ "makna.arti": "utama" }))
db.entri.find({ "makna.arti": "utama" }).explain("queryPlanner").queryPlanner.winningPlanmakna.arti_1
Dokumen makna utama: 85
{
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { 'makna.arti': 1 },
indexName: 'makna.arti_1',
isMultiKey: true,
multiKeyPaths: { 'makna.arti': [ 'makna' ] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { 'makna.arti': [ '["utama", "utama"]' ] }
}
}Tahap IXSCAN memakai indeks makna.arti_1 dengan batas kunci "utama", dan kedua penanda multikey muncul pada rencana. Field multiKeyPaths menyebut jalur terindeks makna.arti dengan makna sebagai jalur arraynya. Pola yang sama berlaku pada kedalaman bersarang mana pun, yaitu server menandai setiap jalur array yang dilalui, sehingga indeks pada dokumen bersarang tetap dapat diaudit melalui explain.
Batasan query tertutup#
Query tertutup (covered query) adalah query yang seluruh field yang diquery dan diproyeksikan tersedia pada indeks. Akibatnya, server tidak mengambil dokumen apa pun dari koleksi dan totalDocsExamined bernilai nol. Indeks multikey dapat menutup query pada field biasa yang menjadi bagian indeksnya. Sebaliknya, query pada field array tidak dapat ditutup oleh indeks multikey, dan server selalu mengambil dokumen dari koleksi untuk memenuhi proyeksi field array tersebut.
Kedua bentuk query berikut memakai indeks multikey kategori_1_sinonim_1. Query pertama hanya menyentuh field kategori, sedangkan query kedua menyentuh field array sinonim.
const stc = db.entri.find({ kategori: "verba" }, { _id: 0, kategori: 1 }).explain("executionStats")
print("totalDocsExamined kategori: " + stc.executionStats.totalDocsExamined)
print("nReturned kategori: " + stc.executionStats.nReturned)totalDocsExamined kategori: 0
nReturned kategori: 167const sta = db.entri.find({ sinonim: "akurat" }, { _id: 0, sinonim: 1 }).explain("executionStats")
print("totalDocsExamined sinonim: " + sta.executionStats.totalDocsExamined)
print("nReturned sinonim: " + sta.executionStats.nReturned)totalDocsExamined sinonim: 95
nReturned sinonim: 95Query pada kategori mengembalikan 167 dokumen tanpa membaca satu dokumen pun dari koleksi, karena nilai yang diproyeksikan sudah lengkap pada kunci indeks. Jumlah kunci yang diperiksa lebih banyak daripada hasilnya karena indeks multikey menyimpan satu kunci per elemen sinonim, dan server menggabungkan kunci yang menunjuk dokumen yang sama sebelum mengembalikan hasil. Query pada sinonim memeriksa 95 dokumen untuk 95 hasil, artinya dokumen tetap diambil meskipun indeksnya dipakai. Batasan ini perlu diperhitungkan saat menilai manfaat indeks multikey untuk query yang memproyeksikan isi array.
Kesalahan umum#
Batasan yang paling sering menabrak adalah indeks komposit dengan dua field array sekaligus. Server menolak indeks semacam ini karena jumlah kombinasi kuncinya meledak, yaitu setiap elemen array pertama harus dipasangkan dengan setiap elemen array kedua untuk dokumen yang sama.
try {
db.entri.createIndex({ sinonim: 1, suku: 1 })
} catch (e) {
print("Kode error: " + e.codeName)
print("Penyebab: " + e.message.split(":: caused by :: ").pop())
}Kode error: CannotIndexParallelArrays
Penyebab: cannot index parallel arrays [suku] [sinonim]Pesan errornya menyebut cannot index parallel arrays diikuti nama kedua field. Penolakan terjadi pada saat pembuatan indeks, bukan saat query dijalankan, sehingga daftar indeks melalui getIndexes menjadi tempat memastikan indeks mana yang benar-benar berhasil dibuat. Perbaikannya adalah memilih satu field array saja untuk diindeks, misalnya mempertahankan sinonim dan menggugurkan suku, atau membuat dua indeks terpisah apabila keduanya memang diquery secara mandiri. Dua indeks terpisah tetap melayani query yang menyaring dua array sekaligus, tetapi server hanya memakai salah satunya dan menyaring field array yang tersisa pada dokumen yang telah diambil. Apabila kedua array harus difilter bersama dalam satu query secara efisien, struktur dokumen perlu ditinjau ulang, misalnya mengganti salah satu array menjadi field tunggal.
Ringkasan#
- Query pada field array mencocokkan dokumen bila salah satu elemennya memenuhi syarat, dan tanpa indeks seluruh koleksi tetap dipindai.
- Indeks pada field array maupun jalur di dalam array otomatis menjadi multikey, yaitu menyimpan satu kunci per elemen, dan penandanya terbaca pada field
isMultiKeysertamultiKeyPaths. - Indeks komposit boleh memuat satu field array, sedangkan dua field array pada satu indeks ditolak server melalui error
cannot index parallel arrays. - Indeks multikey menutup query pada field biasa, tetapi query yang memproyeksikan field array selalu mengambil dokumen dari koleksi.
Referensi#
- Multikey Indexes — MongoDB Manual — Dokumentasi perilaku indeks multikey, jalur bersarang, dan batasannya.
- db.collection.createIndex() — MongoDB Manual — Spesifikasi pembuatan indeks pada field array maupun field biasa.
- Query array — MongoDB Manual — Panduan pencocokan elemen array pada query MongoDB.
- Query Optimization — MongoDB Manual — Penjelasan query tertutup dan syarat proyeksinya.