Collection nyata jarang rapi: sebagian anggota toko buku mendaftar dengan email, sebagian hanya menyimpan nomor telepon. Index biasa tetap mencatat semua document — termasuk yang tidak memiliki field ter-index, dengan nilai null sebagai kuncinya. Sparse index menolak perilaku itu: hanya document yang benar-benar memiliki field terkait yang mendapat entri index. Chapter ini menguji perilakunya, pola klasik unique + sparse, dan memetakan bedanya dengan partial index yang kita bahas sebelumnya.

Peta materi Chapter ini:

  • Membuat sparse index dan melihat isinya di getIndexes().
  • Efek nyata: query { field: null } yang tak bisa memakai index.
  • Pola klasik unique + sparse untuk email opsional.
  • Tabel perbedaan sparse vs partial index.

Melewati Document Tanpa Field #

Opsi sparse: true membuat index melompati document yang tidak memiliki field ter-index. Bandingkan dengan index biasa, yang menyimpan kunci null untuk document tanpa field tersebut:

js
use latihan_index
db.anggota.drop()
db.anggota.insertMany([
  { nama: "Budi", email: "[email protected]", telepon: "0812-1111-2222" },
  { nama: "Siti", telepon: "0813-3333-4444" },
  { nama: "Dewi", email: "[email protected]" },
  { nama: "Eka", telepon: "0815-5555-6666" }
])
db.anggota.createIndex({ email: 1 }, { sparse: true })
db.anggota.getIndexes()

Output yang Dihasilkan:

text
switched to db latihan_index
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3bd988caddf0d30ef1467'),
    '1': ObjectId('6ad3bd988caddf0d30ef1468'),
    '2': ObjectId('6ad3bd988caddf0d30ef1469'),
    '3': ObjectId('6ad3bd988caddf0d30ef146a')
  }
}
email_1
[
  {
    v: 2,
    key: { _id: 1 },
    name: '_id_',
    collation: { locale: 'simple' }
  },
  {
    v: 2,
    key: { email: 1 },
    name: 'email_1',
    sparse: true,
    collation: { locale: 'simple' }
  }
]

Entri email_1 membawa sparse: true — dari empat document, hanya dua yang masuk index: milik Budi dan Dewi. Document Siti dan Eka tidak pernah menyentuh index, sehingga ukurannya lebih kecil dan setiap insert tanpa email terbebas dari biaya tulis index.

Query null yang Tak Terlayani Index #

Konsekuensi sparseness muncul pada query nilai null. Di MongoDB, filter { email: null } cocok untuk document yang memiliki email bernilai null maupun yang tidak memiliki field email sama sekali. Karena document tanpa field memang tidak ada di dalam sparse index, planner menolak memakainya:

js
printjson(db.anggota.find({ email: null }, { _id: 0, nama: 1 }).toArray())
const rencanaSparse = db.anggota.find({ email: null }).explain()
print("Sparse:   " + rencanaSparse.queryPlanner.winningPlan.stage)

db.anggota.dropIndex("email_1")
db.anggota.createIndex({ email: 1 })
const rencanaBiasa = db.anggota.find({ email: null }).explain()
print("Biasa:    " + rencanaBiasa.queryPlanner.winningPlan.inputStage.stage + " lewat " + rencanaBiasa.queryPlanner.winningPlan.inputStage.indexName)

Output yang Dihasilkan:

text
[
  {
    nama: 'Siti'
  },
  {
    nama: 'Eka'
  }
]

Sparse:   COLLSCAN

{
  nIndexesWas: 2,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1792261528, i: 9 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1792261528, i: 9 })
}
email_1

Biasa:    IXSCAN lewat email_1

Query menemukan Siti dan Eka di kedua skenario — hasilnya sama karena COLLSCAN memeriksa seluruh collection. Lalu index email_1 dihapus (respon nIndexesWas: 2) dan dibuat ulang tanpa opsi sparse. Bedanya ada pada rencana eksekusi: sparse index memaksa COLLSCAN, sedangkan index biasa melayani lewat IXSCAN karena kunci null tersimpan di dalamnya. Inilah trade-off sparse: hemat ruang, tetapi query terkait field yang hilang menjadi perlahan pada collection besar.

Pola Klasik Unique dan Sparse #

Sebelum partial index ada, kombinasi unique + sparse adalah cara resmi menangani “field unik yang opsional”. Dua document tanpa email tidak saling bertabrakan karena keduanya tidak masuk index:

js
db.anggota.dropIndex("email_1")
db.anggota.createIndex({ email: 1 }, { unique: true, sparse: true })
db.anggota.insertOne({ nama: "Fajar", telepon: "0816-7777-8888" })
try {
  db.anggota.insertOne({ nama: "Gilang", email: "[email protected]" })
} catch (e) {
  print("Kode galat: " + e.code)
  print("Pesan: " + e.message)
}

Output yang Dihasilkan:

text
{
  nIndexesWas: 2,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1792261528, i: 15 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1792261528, i: 15 })
}
email_1
{
  acknowledged: true,
  insertedId: ObjectId('6ad3bd988caddf0d30ef146b')
}
Kode galat: 11000
Pesan: E11000 duplicate key error collection: latihan_index.anggota index: email_1 dup key: { email: "[email protected]" }

Blok di atas menghapus kembali index biasa lalu membangun versi unique + sparse-nya. Insert Fajar (tanpa email) diterima meskipun Eka dan Siti juga tidak punya email — bagi index, mereka semua tidak ada. Insert Gilang ditolak E11000 karena emailnya menabrak milik Budi. Perhatikan satu celah halus: sparse index membiarkan satu document dengan email: null eksplisit (field ada, bernilai null) — hanya document tanpa field yang dilompati. Celah inilah salah satu alasan partial index dianggap lebih presisi.

Sparse atau Partial #

AspekSparse indexPartial index
Syarat masuk indexField ada (apa pun nilainya)Filter bebas: $gt, $eq, $type, $and, dan kombinasi
Document email: null eksplisitMasuk index (bisa bentrok dengan unique)Bisa disaring lewat { email: { $type: "string" } }
Presisi seleksiBinari: ada atau tidakRinci: nilai dan tipe bisa diuji
Rekomendasi dokumentasiUntuk kompatibilitas sistem lamaPilihan utama untuk kasus baru

Aturan email: { $exists: true } pada partial filter menghasilkan efek yang sama dengan sparse — plus kemampuan menyaring lebih jauh. Dokumentasi resmi menyebut partial index lebih unggul untuk kasus apa pun yang sparse mampu jawab; gunakan sparse hanya saat menemukannya di sistem yang sudah berjalan.

Rangkuman #

  • createIndex({ field: 1 }, { sparse: true }) hanya meng-index document yang memiliki field tersebut; document lain tak pernah menulis ke index.
  • Query { field: null } tidak bisa dilayani sparse index (COLLSCAN) karena document tanpa field memang tidak ada di dalamnya.
  • Kombinasi unique + sparse menangani field unik opsional, tetapi tetap membolehkan satu null eksplisit — celah yang tidak dimiliki partial index.
  • Untuk kebutuhan baru, dokumentasi merekomendasikan partial index yang lebih presi lewat partialFilterExpression.