Di database relasional, kolom yang tidak diisi selalu ada — nilainya NULL. Di MongoDB, document yang tidak menyimpan sebuah field berbeda dari document yang menyimpan field bernilai null: keduanya adalah kondisi terpisah di dalam BSON. Sayangnya, query { field: null } memperlakukan keduanya sebagai sama, dan perilaku inilah yang paling sering membuat laporan menghitung data yang sebenarnya belum terisi. Chapter ini membedah ketiga alat pembeda — kesamaan null, $type, dan $exists — beserta pola query “field terisi” yang dipakai di hampir semua aplikasi produksi.

Peta materi Chapter ini:

  • { field: null } yang menangkap dua kondisi sekaligus.
  • $type: "null" dan $exists sebagai pembeda.
  • Pola $exists + $ne: null untuk “field terisi”.

Data Uji: Diskon yang Tidak Selalu Ada #

Field hargaDiskon sengaja dibuat dalam tiga keadaan: berisi angka, berisi null (belum ada diskon, sengaja ditandai), dan tidak disimpan sama sekali:

js
use latihan_query
db.buku.drop()

Output yang Dihasilkan:

text
switched to db latihan_query
true
js
db.buku.insertMany([
  { judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, hargaDiskon: 15, kategori: "fiksi" },
  { judul: "Bumi", penulis: "Tere Liye", harga: 89000, hargaDiskon: null, kategori: "fiksi" },
  { judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, hargaDiskon: 20, kategori: "nonfiksi" },
  { judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi" },
  { judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi" },
  { judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, hargaDiskon: null, kategori: "referensi" }
])

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad391cc16d5d5c7440b7bf0'),
    '1': ObjectId('6ad391cc16d5d5c7440b7bf1'),
    '2': ObjectId('6ad391cc16d5d5c7440b7bf2'),
    '3': ObjectId('6ad391cc16d5d5c7440b7bf3'),
    '4': ObjectId('6ad391cc16d5d5c7440b7bf4'),
    '5': ObjectId('6ad391cc16d5d5c7440b7bf5')
  }
}

Status hargaDiskon per buku: angka pada Laut Bercerita (15) dan Filosofi Teras (20), null pada Bumi dan Kamus Bahasa Indonesia, serta tidak ada pada Atomic Habits dan Rumah Kaca.

{ field: null } Menangkap Dua Kondisi #

Query pertama mencari buku yang “tidak sedang diskon”:

js
db.buku.find({ hargaDiskon: null }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Bumi' },
  { judul: 'Atomic Habits' },
  { judul: 'Rumah Kaca' },
  { judul: 'Kamus Bahasa Indonesia' }
]

Empat buku kembali: dua yang bernilai null dan dua yang tidak memiliki field hargaDiskon. MongoDB memperlakukan field yang hilang seolah bernilai null saat mencocokkan kesamaan — perilaku yang dijanjikan sejak Chapter Comparison Operators (di sana $ne ikut menangkap document tanpa field; ini sisi kebalikannya). Untuk “belum diisi” memang itu yang kita mau; untuk “sengaja kosong” hasilnya tercampur.

Pembuktian ekstremnya: query field yang tidak pernah ada di document mana pun tetap mengembalikan seluruh collection:

js
db.buku.find({ kodeRak: null }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Laut Bercerita' },
  { judul: 'Bumi' },
  { judul: 'Filosofi Teras' },
  { judul: 'Atomic Habits' },
  { judul: 'Rumah Kaca' },
  { judul: 'Kamus Bahasa Indonesia' }
]

Tidak ada satu pun buku yang menyimpan kodeRak, sehingga keenamnya dicocokkan. Kesamaan null bukan pembuktian bahwa field ada dan kosong — hanya bukti bahwa nilainya tidak berisi data.

Pembeda: $type dan $exists #

Dua operator memisahkan kondisi yang tercampur itu. $type: "null" hanya menerima nilai yang benar-benar bertipe BSON null:

js
db.buku.find({ hargaDiskon: { $type: "null" } }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[ { judul: 'Bumi' }, { judul: 'Kamus Bahasa Indonesia' } ]

Hanya Bumi dan Kamus Bahasa Indonesia — document yang menyimpan null secara eksplisit — yang lolos; field yang hilang tidak punya tipe apa pun sehingga tidak diuji. Sementara itu $exists menguji keberadaan field itu sendiri, terlepas dari isinya:

js
db.buku.find({ hargaDiskon: { $exists: true } }, { _id: 0, judul: 1, hargaDiskon: 1 })
db.buku.find({ hargaDiskon: { $exists: false } }, { _id: 0, judul: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Laut Bercerita', hargaDiskon: 15 },
  { judul: 'Bumi', hargaDiskon: null },
  { judul: 'Filosofi Teras', hargaDiskon: 20 },
  { judul: 'Kamus Bahasa Indonesia', hargaDiskon: null }
]
[ { judul: 'Atomic Habits' }, { judul: 'Rumah Kaca' } ]

Mari kita bedah pasangan hasilnya:

  1. $exists: true: empat buku yang menyimpan field hargaDiskon — termasuk yang isinya null, karena $exists hanya melihat ada-tidaknya field.
  2. $exists: false: dua buku tanpa field tersebut; $exists tidak peduli nilai, sehingga null tidak masuk daftar ini.

Pola “Field Terisi”: $exists + $ne null #

Kebutuhan paling umum di aplikasi: mencari document yang field-nya benar-benar berisi nilai. Syaratnya dua lapis — field ada, dan isinya bukan null:

js
db.buku.find({ hargaDiskon: { $exists: true, $ne: null } }, { _id: 0, judul: 1, hargaDiskon: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Laut Bercerita', hargaDiskon: 15 },
  { judul: 'Filosofi Teras', hargaDiskon: 20 }
]

Hanya dua buku berdiskon angka yang tersisa. Menariknya, hasil yang sama bisa dicapai tanpa $exists:

js
db.buku.find({ hargaDiskon: { $ne: null } }, { _id: 0, judul: 1, hargaDiskon: 1 })

Output yang Dihasilkan:

text
[
  { judul: 'Laut Bercerita', hargaDiskon: 15 },
  { judul: 'Filosofi Teras', hargaDiskon: 20 }
]

Alasannya mengikuti logika kesamaan null di atas: field yang hilang dianggap sama dengan null, sehingga $ne: null otomatis menolak document tanpa field — $exists: true menjadi redundan. Meski begitu, banyak basis kode tetap menulis keduanya eksplisit karena maksudnya terbaca jelas: “ada dan terisi”. Ringkasan ketiganya:

QueryMenangkap
{ field: null }bernilai null dan field tidak ada
{ field: { $type: "null" } }hanya bernilai null
{ field: { $exists: true } }field ada (termasuk isinya null)
{ field: { $exists: false } }field tidak ada
{ field: { $ne: null } }field ada dan isinya bukan null

Rangkuman #

  • null (nilai eksplisit) dan field yang hilang adalah dua kondisi berbeda di BSON, tetapi { field: null } mencocokkan keduanya.
  • $type: "null" hanya menangkap nilai null sungguhan; $exists menguji ada-tidaknya field tanpa peduli isi.
  • $ne: null setara “ada dan tidak null” — field yang hilang ikut tertolak; kombinasi eksplisit $exists: true + $ne: null populer karena mudah dibaca.
  • Sparse index bisa berinteraksi dengan query null — tunda penggunaannya sampai pembahasan Section Indexing.