Blog toko buku kita punya artikel dengan ratusan komentar, tetapi halaman artikel hanya perlu menampilkan judul dan tiga komentar paling disukai. Kalau seluruh komentar di-embed, setiap kunjungan halaman — termasuk yang hanya membaca paragraf pertama — memindahkan ratusan kilobyte data yang tidak diminta. Subset Pattern memotongnya: subset data panas disimpan di document induk, seluruh data tetap lengkap di collection terpisah. Ukuran document turun, memori kerja turun, halaman tetap cepat.

Masalah: Document Gemuk dan Working Set #

Working set adalah data panas yang aktif dibaca oleh aplikasi dan seharusnya muat di RAM. Document yang membawa seluruh sejarah komentarnya mencemari working set dengan data dingin: komentar nomor 200 jarang dibaca siapa pun, tetapi ikut termuat setiap kali document artikel disentuh — oleh query, oleh index, oleh cache WiredTiger. Semakin gemuk document, semakin sedikit document panas yang muat di memori, dan semakin sering MongoDB jatuh ke disk.

Solusi: Subset Panas di Induk #

Kita bagi menjadi dua tempat: collection komentar sebagai sumber lengkap, dan field komentarTeratas pada artikel sebagai cache tiga komentar teratas:

js
use latihan_modeling
db.dropDatabase()
db.artikel.insertOne({
  _id: 7,
  judul: "Rekomendasi Buku Desember",
  penulis: "Dewi",
  komentarTeratas: [
    { nama: "Budi", teks: "Daftar yang mantap!", jumlahSuka: 42 },
    { nama: "Siti", teks: "Saya sudah beli dua judul.", jumlahSuka: 31 },
    { nama: "Rina", teks: "Ada rekomendasi fiksi sains?", jumlahSuka: 12 }
  ]
})
db.komentar.insertMany([
  { artikelId: 7, nama: "Budi", teks: "Daftar yang mantap!", jumlahSuka: 42 },
  { artikelId: 7, nama: "Siti", teks: "Saya sudah beli dua judul.", jumlahSuka: 31 },
  { artikelId: 7, nama: "Rina", teks: "Ada rekomendasi fiksi sains?", jumlahSuka: 12 },
  { artikelId: 7, nama: "Agus", teks: "Harga masih ramai kantong.", jumlahSuka: 8 },
  { artikelId: 7, nama: "Tono", teks: "Mohon versi e-book-nya.", jumlahSuka: 5 },
  { artikelId: 7, nama: "Lia", teks: "Suka sampulnya.", jumlahSuka: 2 }
])

Output yang Dihasilkan:

text
switched to db latihan_modeling
{ ok: 1, dropped: 'latihan_modeling' }
{ acknowledged: true, insertedId: 7 }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39a986519acc1e0a090a8'),
    '1': ObjectId('6ad39a986519acc1e0a090a9'),
    '2': ObjectId('6ad39a986519acc1e0a090aa'),
    '3': ObjectId('6ad39a986519acc1e0a090ab'),
    '4': ObjectId('6ad39a986519acc1e0a090ac'),
    '5': ObjectId('6ad39a986519acc1e0a090ad')
  }
}

Tiga komentar teratas tersalin ke komentarTeratas; enam komentar lengkap — termasuk ketiganya — hidup di komentar. Jalur panas kini beratnya ringan:

js
db.artikel.findOne({ _id: 7 }, { judul: 1, komentarTeratas: 1 })

Output yang Dihasilkan:

text
{
  _id: 7,
  judul: 'Rekomendasi Buku Desember',
  komentarTeratas: [
    { nama: 'Budi', teks: 'Daftar yang mantap!', jumlahSuka: 42 },
    { nama: 'Siti', teks: 'Saya sudah beli dua judul.', jumlahSuka: 31 },
    { nama: 'Rina', teks: 'Ada rekomendasi fiksi sains?', jumlahSuka: 12 }
  ]
}

Satu query kecil merender seluruh bagian atas halaman. Pada skenario nyata dengan ratusan komentar, document artikel kita tidak ikut membengkak — ukurannya dikunci oleh jumlah komentar yang di-cache, bukan oleh total komentar yang masuk.

Berapa besar subset yang tepat? Patokannya desain halaman: hitung berapa komentar yang benar-benar tampil sebelum pembaca mengeklik “lihat semua” — itulah angka cache yang ideal. Men-cache terlalu sedikit berarti halaman harus membuka collection kedua terlalu cepat; men-cache terlalu banyak berarti mengulangi masalah document gemuk yang mau kita hindari. Angka tiga sampai sepuluh adalah zona yang umum dipakai aplikasi konten.

Membaca Sisa Komentar #

Pembaca yang mengeklik “lihat semua komentar” berpindah ke collection lengkap, lengkap dengan pagination sort + skip + limit:

js
db.komentar.find({ artikelId: 7 }, { nama: 1, teks: 1, jumlahSuka: 1, _id: 0 }).sort({ jumlahSuka: -1 }).skip(3).limit(3)

Output yang Dihasilkan:

text
[
  { nama: 'Agus', teks: 'Harga masih ramai kantong.', jumlahSuka: 8 },
  { nama: 'Tono', teks: 'Mohon versi e-book-nya.', jumlahSuka: 5 },
  { nama: 'Lia', teks: 'Suka sampulnya.', jumlahSuka: 2 }
]

Query ini melewati tiga komentar teratas (yang sudah tampil lewat cache) lalu mengambil tiga berikutnya. Index pada { artikelId: 1, jumlahSuka: -1 } membuat urutan ini efisien pada collection sebesar apa pun.

Pola ini tidak selalu layak: kalau artikel kita memang pendek komentar-komentarnya (misalnya selalu di bawah dua puluh), embed penuh tanpa collection kedua lebih sederhana dan tetap sehat. Subset Pattern menjadi bernilai ketika ekor datanya nyata — ratusan hingga ribuan komentar — sementara kebutuhan bacanya terkonsentrasi di ujung.

Menyegarkan Cache #

komentarTeratas adalah duplikasi — dan seperti semua duplikasi, ia harus disegarkan. Komentar lama yang tiba-tiba mendapat lonjakan suka harus naik ke cache. Alurnya dua langkah: ambil tiga teratas dari sumber kebenaran, timpa cache:

js
db.komentar.updateOne({ nama: "Agus" }, { $inc: { jumlahSuka: 40 } })
const teratas = db.komentar.find({ artikelId: 7 }, { _id: 0, nama: 1, teks: 1, jumlahSuka: 1 }).sort({ jumlahSuka: -1 }).limit(3).toArray()
db.artikel.updateOne({ _id: 7 }, { $set: { komentarTeratas: teratas } })
db.artikel.findOne({ _id: 7 }, { komentarTeratas: 1 })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  _id: 7,
  komentarTeratas: [
    { nama: 'Agus', teks: 'Harga masih ramai kantong.', jumlahSuka: 48 },
    { nama: 'Budi', teks: 'Daftar yang mantap!', jumlahSuka: 42 },
    { nama: 'Siti', teks: 'Saya sudah beli dua judul.', jumlahSuka: 31 }
  ]
}

Bedah alurnya:

  1. $inc pada komentar Agus — simulasikan lonjakan 40 suka baru; komentarnya kini unggul 48 suka.
  2. find().sort().limit().toArray() — membaca tiga teratas langsung dari collection komentar sebagai array; baris ini tidak mencetak apa pun karena hasilnya ditampung variabel teratas.
  3. $set: { komentarTeratas: teratas } — menimpa cache dengan nilai segar; komentar Siti (31 suka) turun, Agus (48 suka) naik.
  4. findOne penutup — memastikan cache baru sudah terpasang di document artikel.

Rangkuman #

  • Subset Pattern menyimpan subset data panas (komentar teratas) di document induk dan seluruh data di collection terpisah.
  • Document induk tetap ramping seberapa pun jumlah total datanya — working set ikut mengecil dan halaman tetap satu query.
  • Sisa data dibaca dari collection lengkap dengan pagination; index gabungan menjaga query itu efisien.
  • Cache adalah duplikasi yang harus disegarkan: ambil subset terbaru dari sumber kebenaran, lalu $set ke induk.

Data panas yang berulang setiap menit punya pola khusus sendiri — Bucket Pattern untuk deret waktu — bahasan chapter berikutnya.