Embedding vs referencing

Menimbang embedding dan referencing sebagai dua strategi dasar desain schema MongoDB.

Daftar isi

MongoDB tidak memaksakan satu bentuk tabel seperti basis data relasional, sehingga setiap rancangan diawali dengan keputusan tentang letak data yang saling terkait. Strategi pertama adalah embedding, yaitu menanam subdokumen atau array di dalam dokumen induk. Strategi kedua adalah referencing, yaitu menyimpan ObjectId milik dokumen lain sebagai penghubung antarkoleksi. Topik ini membahas cara kerja keduanya pada basis data kampus, biaya baca yang menyertainya, serta kriteria menimbang yang melibatkan pola akses, ukuran data, dan pertumbuhan.

Embedding menanam data terkait di dalam dokumen induk#

Embedding menyimpan data anak sebagai subdokumen atau array di dalam dokumen induknya, sehingga satu pembacaan dokumen mengembalikan seluruh data yang dibutuhkan. Bentuk ini dipilih apabila data anak hanya bermakna bersama induknya, jarang dibaca secara terpisah, dan tidak bertumbuh tanpa batas. Contoh pada domain kampus adalah alamat mahasiswa, yang hampir selalu ditampilkan bersama profil mahasiswa itu sendiri.

mongosh
use kampus
text Keluaran
switched to db kampus
mongosh
db.mahasiswa.insertOne({
  nama: "Budi Santoso",
  nim: "10112001",
  alamat: {
    jalan: "Jl. Kenanga No. 7",
    kota: "Bandung",
    kodePos: "40135"
  }
})
text Keluaran
{
  acknowledged: true,
  insertedId: ObjectId('6ae8992ef71ce5016d8de666')
}
mongosh
db.mahasiswa.findOne({ nim: "10112001" })
text Keluaran
{
  _id: ObjectId('6ae8992ef71ce5016d8de666'),
  nama: 'Budi Santoso',
  nim: '10112001',
  alamat: { jalan: 'Jl. Kenanga No. 7', kota: 'Bandung', kodePos: '40135' }
}

Satu permintaan findOne cukup karena alamat terbawa bersama profilnya. Tidak ada query kedua dan tidak ada tahap penggabungan di sisi aplikasi. Pembaruan alamat juga cukup satu updateOne pada satu dokumen. Perubahan seluruh field pada satu dokumen berlaku secara atomik, sehingga tidak ada keadaan ketika kota sudah berubah sedangkan kode posnya masih lama.

Field di dalam subdokumen tetap dapat diquery secara langsung dengan notasi titik, misalnya alamat.kota. MongoDB mencocokkan field bersarang sebagaimana field tingkat atas, sehingga data yang ditanam tetap dapat dicari tanpa membongkar strukturnya. Proyeksi juga berlaku pada subdokumen, sehingga halaman yang hanya membutuhkan alamat dapat memilih field alamat saja tanpa membawa field lainnya.

Referencing menyimpan ObjectId sebagai penghubung#

Referencing menyimpan ObjectId milik dokumen lain pada sebuah field, sehingga setiap entitas berdiri di koleksinya sendiri. Bentuk ini dipilih apabila data anak diakses secara terpisah, dibaca tanpa induknya, atau bertumbuh besar. Pada domain kampus, dosen adalah entitas mandiri yang memiliki jadwal mengajar dan publikasi, sehingga relasi dosen sebagai wali akademik mahasiswa dipetakan dengan rujukan.

mongosh
db.dosen.insertMany([
  { nama: "Dr. Ali Rahman", bidang: "Statistika" },
  { nama: "Dr. Susi Andani", bidang: "Basis Data" }
])
text Keluaran
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ae8992ef71ce5016d8de667'),
    '1': ObjectId('6ae8992ef71ce5016d8de668')
  }
}
mongosh
const dosen = db.dosen.findOne({ nama: "Dr. Ali Rahman" })
db.mahasiswa.insertOne({ nama: "Dewi Lestari", nim: "10112002", waliId: dosen._id })
text Keluaran
{
  acknowledged: true,
  insertedId: ObjectId('6ae8992ef71ce5016d8de669')
}

Field waliId memuat ObjectId milik dokumen dosen yang dirujuk. Relasi hanya tersambung penuh pada saat data dibaca, yaitu ketika query mencari dokumen dosen berdasarkan nilai waliId tersebut. Dokumen mahasiswa dan dokumen dosen diperbarui masing-masing di koleksinya sendiri, dan rujukan di antara keduanya tidak berubah oleh pembaruan tersebut.

Rujukan bekerja tanpa salinan data pada sisi mana pun. Dokumen dosen tidak menyimpan daftar mahasiswa yang diwalikan, karena daftar itu dapat dibaca dari arah lain dengan query db.mahasiswa.find({ waliId: ... }). Desain semacam ini menghindari dua sumber kebenaran, yaitu dua salinan data relasi yang sama yang harus dijaga tetap sinkron. Setiap relasi baru cukup ditulis pada dokumen yang menyimpan rujukannya, yaitu dokumen mahasiswa.

Membaca rujukan dengan dua langkah atau $lookup#

Pembacaan data hasil referencing membutuhkan langkah tambahan yang tidak ada pada embedding. Dua pola yang lazim adalah pembacaan dua langkah di sisi aplikasi dan penggabungan di dalam server dengan tahap $lookup. Keduanya menghasilkan data yang sama, sedangkan pemilihannya menyesuaikan jumlah dokumen yang dibaca.

mongosh
const mhs = db.mahasiswa.findOne({ nim: "10112002" })
const wali = db.dosen.findOne({ _id: mhs.waliId })
({ mahasiswa: mhs.nama, wali: wali.nama, bidang: wali.bidang })
text Keluaran
{
  mahasiswa: 'Dewi Lestari',
  wali: 'Dr. Ali Rahman',
  bidang: 'Statistika'
}
mongosh
db.mahasiswa.aggregate([
  { $match: { waliId: { $exists: true } } },
  { $lookup: {
    from: "dosen",
    localField: "waliId",
    foreignField: "_id",
    as: "wali"
  } },
  { $unwind: "$wali" },
  { $project: { _id: 0, mahasiswa: "$nama", wali: "$wali.nama" } }
])
text Keluaran
[ { mahasiswa: 'Dewi Lestari', wali: 'Dr. Ali Rahman' } ]

Tahap $lookup menggabungkan koleksi dosen ke dalam pipeline berdasarkan kecocokan waliId dengan _id, sehingga hasilnya sudah utuh tanpa langkah tambahan di aplikasi. Untuk satu dokumen, dua langkah findOne lebih ringkas karena hanya dua dokumen yang dibaca. Untuk laporan yang melibatkan banyak mahasiswa sekaligus, satu pipeline $lookup lebih efisien daripada mengulang dua langkah untuk setiap baris.

Kedua pola mengandalkan kesamaan nilai yang persis, bukan kesamaan nama field, sehingga waliId harus memuat ObjectId yang benar-benar menunjuk dokumen dosen. Apabila dokumen dosen yang dirujuk sudah dihapus, pola dua langkah mengembalikan null yang perlu diperiksa aplikasi, sedangkan tahap $unwind pada pipeline sebelumnya membuang baris mahasiswa tersebut. Perbedaan perilaku ini penting diketahui sebelum bentuk polanya dipilih.

Kriteria menimbang embedding dan referencing#

Pilihan di antara kedua strategi mengikuti pola akses, bukan kebiasaan memodelkan tabel. Tabel berikut merangkum perbedaan keduanya pada empat pertimbangan utama.

PertimbanganEmbeddingReferencing
Pembacaansatu pembacaan dokumendua langkah atau $lookup
Kemandirian dataanak hanya bermakna bersama indukanak diakses sendiri
Pertumbuhandibatasi ukuran dokumentumbuh di koleksinya sendiri
Pembaruansatu dokumen, berlaku atomikdua koleksi, masing-masing diperbarui

Frekuensi akses bersama menjadi pertimbangan pertama. Data yang hampir selalu dibaca bersama induknya cocok ditanam, sedangkan data yang dibaca sendiri lebih baik dirujuk. Alamat mahasiswa muncul pada kartu mahasiswa dan halaman profil, sehingga embedding menghindari query tambahan pada setiap pembacaan tersebut.

Ukuran dan pertumbuhan menjadi pertimbangan kedua. Ukuran satu dokumen MongoDB dibatasi 16 MB, sehingga data yang jumlahnya tidak terkendali, misalnya log atau riwayat transaksi, tidak boleh ditanam pada induknya. Referencing menempatkan data seperti itu pada koleksi tersendiri yang dapat diindeks dan dibaca per bagian. Pembaruan menjadi pertimbangan ketiga, karena data yang ditanam ikut berubah bersama induknya dalam satu operasi atomik, sedangkan data yang dirujuk diperbarui di koleksinya masing-masing.

Kesalahan umum#

Kesalahan pertama adalah menanam data yang ternyata diakses terpisah. Dokumen mahasiswa yang menyertakan seluruh riwayat pembayaran membuat setiap pembacaan profil membawa data yang tidak dibutuhkan, dan ukuran dokumennya terus membengkak. Ukuran yang membengkak juga memperlambat pembacaan yang tidak membutuhkan riwayat pembayaran, karena MongoDB tetap membaca dokumen secara utuh. Perbaikannya adalah memindahkan data tersebut ke koleksi sendiri, kemudian merujuknya dengan ObjectId.

Kesalahan kedua adalah merujuk data yang selalu dibaca bersama. Profil dosen yang dirujuk dari setiap mata kuliah memaksa dua pembacaan pada halaman yang memang menampilkan keduanya. Biaya dua langkah semacam ini tidak terasa selama data masih kecil, sehingga masalah kinerjanya baru terlihat pada laporan yang membaca banyak baris sekaligus. Apabila data itu selalu tampil bersama dan tidak bertumbuh besar, penanaman menghasilkan bacaan yang lebih murah.

Kesalahan ketiga adalah menganggap referencing setara dengan foreign key. MongoDB tidak menegakkan rujukan secara otomatis, sehingga nilai waliId yang menunjuk dokumen dosen yang sudah dihapus tetap lolos saat ditulis. Pemeriksaan keberadaan rujukan sebelum penghapusan, atau pengalihan wali secara eksplisit, menjadi bagian dari operasi penghapusan dosen.

Ringkasan#

  • Embedding menanam data terkait sebagai subdokumen atau array di dalam dokumen induk, sehingga satu pembacaan mengembalikan semuanya.
  • Referencing menyimpan ObjectId dokumen lain dan menyambung relasi saat query melalui dua langkah pembacaan atau tahap $lookup.
  • Embedding menguntungkan data yang dibaca bersama, jarang berubah, dan tidak bertumbuh besar, sedangkan referencing menguntungkan data yang diakses sendiri atau tumbuh besar.
  • Batas ukuran dokumen 16 MB dan sifat atomik pembaruan satu dokumen menjadi dasar penimbangan strategi.

Referensi#