Indeks multikey

Mengindeks field bertipe array dan perilaku multikey yang menyertainya.

Daftar isi

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.

mongosh
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())
text Keluaran
switched to db kamus

Dokumen tersimpan: 500
mongosh
const bersih = db.entri.dropIndexes()
print("Indeks non-_id dibersihkan: " + (bersih.ok === 1))
text Keluaran
Indeks non-_id dibersihkan: true

Hitung dokumen yang sinonimnya memuat akurat, lalu ukur pekerjaan server melalui explain("executionStats").

mongosh
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)
text Keluaran
Dokumen cocok: 95

totalKeysExamined: 0

totalDocsExamined: 500

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

mongosh
db.entri.createIndex({ sinonim: 1 })
db.entri.find({ sinonim: "akurat" }).explain("queryPlanner").queryPlanner.winningPlan
text Keluaran
sinonim_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.

mongosh
const st2 = db.entri.find({ sinonim: "akurat" }).explain("executionStats").executionStats
print("nReturned: " + st2.nReturned)
print("totalKeysExamined: " + st2.totalKeysExamined)
print("totalDocsExamined: " + st2.totalDocsExamined)
text Keluaran
nReturned: 95

totalKeysExamined: 95

totalDocsExamined: 95

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

mongosh
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)
text Keluaran
kategori_1_sinonim_1

isMultiKey: true

nReturned: 35

totalKeysExamined: 35

totalDocsExamined: 35

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

mongosh
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.winningPlan
text Keluaran
makna.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.

mongosh
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)
text Keluaran
totalDocsExamined kategori: 0

nReturned kategori: 167
mongosh
const sta = db.entri.find({ sinonim: "akurat" }, { _id: 0, sinonim: 1 }).explain("executionStats")
print("totalDocsExamined sinonim: " + sta.executionStats.totalDocsExamined)
print("nReturned sinonim: " + sta.executionStats.nReturned)
text Keluaran
totalDocsExamined sinonim: 95

nReturned sinonim: 95

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

mongosh
try {
  db.entri.createIndex({ sinonim: 1, suku: 1 })
} catch (e) {
  print("Kode error: " + e.codeName)
  print("Penyebab: " + e.message.split(":: caused by :: ").pop())
}
text Keluaran
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 isMultiKey serta multiKeyPaths.
  • 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#